diff --git a/apps/web/src/app/workflow-editor.ts b/apps/web/src/app/workflow-editor.ts index 25a4585..bfe9e8f 100644 --- a/apps/web/src/app/workflow-editor.ts +++ b/apps/web/src/app/workflow-editor.ts @@ -1,6 +1,6 @@ -// @ts-nocheck // Bespoke Agent Builder - Client Logic +import type { WorkflowConnection, WorkflowNode, WorkflowRunResult } from '@agentic/types'; import { runWorkflowStream, resumeWorkflow, fetchConfig, fetchRun } from '../services/api'; import { renderMarkdown, escapeHtml } from './markdown'; @@ -24,9 +24,218 @@ const IF_CONDITION_OPERATORS = [ { value: 'equal', label: 'Equal' }, { value: 'contains', label: 'Contains' } ]; -const DEFAULT_IF_CONDITION = { operator: 'equal', value: '' }; +const DEFAULT_IF_CONDITION: IfCondition = { operator: 'equal', value: '' }; + +type WorkflowState = 'idle' | 'running' | 'paused'; + +type Point = { + x: number; + y: number; +}; + +type Viewport = Point & { + scale: number; +}; + +type IfConditionOperator = 'equal' | 'contains'; + +type IfCondition = { + operator: IfConditionOperator; + value: string; +}; + +type WorkflowNodeData = { + collapsed?: boolean; + initialInput?: string; + agentName?: string; + systemPrompt?: string; + userPrompt?: string; + model?: string; + reasoningEffort?: string; + tools?: Record; + prompt?: string; + condition?: string; + conditions?: IfCondition[]; + [key: string]: unknown; +}; + +type EditorNode = WorkflowNode; + +type ConnectionStart = { + nodeId: string; + handle: string; + x: number; + y: number; +}; + +type ApprovalRequest = { + nodeId: string; + container: HTMLElement; + approveBtn: HTMLButtonElement; + rejectBtn: HTMLButtonElement; +}; + +type RunHistoryEntry = { + role: string; + content: string; +}; + +type DropdownItem = { + value: string; + label: string; +}; + +type DropdownConfig = { + placeholder: string; + items: DropdownItem[]; + selectedValue: string; + width: string; + onSelect: (value: string) => void; +}; + +type DropdownInstance = { + destroy?: () => void; +}; + +type DropdownCtor = new (container: HTMLElement, config: DropdownConfig) => DropdownInstance; + +type SplitPanelInstance = { + getLeftPanel: () => HTMLElement; + getRightPanel: () => HTMLElement; +}; + +type SplitPanelCtor = new ( + container: Element, + options: { initialSplit: number; minLeft: number; minRight: number } +) => SplitPanelInstance; + +type ModalInstance = { + open: () => void; + close: () => void; + destroy: () => void; +}; + +type ModalFooterButton = { + label: string; + type: 'primary' | 'secondary'; + onClick: (_event: Event, instance: ModalInstance) => void; +}; + +type ModalCtor = new (options: { + size: 'small' | 'medium' | 'large'; + title: string; + content: HTMLElement; + footerButtons: ModalFooterButton[]; + onClose: () => void; +}) => ModalInstance; + +type WorkflowGraphInput = { + nodes?: EditorNode[]; + connections?: WorkflowConnection[]; +}; + +type WorkflowGraphPayload = { + nodes: EditorNode[]; + connections: WorkflowConnection[]; +}; export class WorkflowEditor { + private modelOptions: string[]; + + private modelEfforts: Record; + + private nodes: EditorNode[]; + + private connections: WorkflowConnection[]; + + private nextNodeId: number; + + private selectedNodeId: string | null; + + private isDragging: boolean; + + private dragOffsetWorld: Point; + + private viewport: Viewport; + + private isPanning: boolean; + + private panStart: Point; + + private viewportStart: Point; + + private tempConnection: SVGPathElement | null; + + private connectionStart: ConnectionStart | null; + + private reconnectingConnection: number | null; + + private canvas: HTMLElement | null; + + private canvasStage: HTMLElement | null; + + private nodesLayer: HTMLElement | null; + + private connectionsLayer: SVGSVGElement | HTMLElement | null; + + private chatMessages: HTMLElement | null; + + private initialPrompt: HTMLInputElement | HTMLTextAreaElement | null; + + private runButton: HTMLButtonElement | null; + + private cancelRunButton: HTMLButtonElement | null; + + private zoomValue: HTMLElement | null; + + private workflowState: WorkflowState; + + private rightPanel: HTMLElement | null; + + private pendingAgentMessage: HTMLElement | null; + + private currentPrompt: string; + + private pendingApprovalRequest: ApprovalRequest | null; + + private activeRunController: AbortController | null; + + private lastLlmResponseContent: string | null; + + private splitPanelCtorPromise: Promise | null; + + private dropdownCtorPromise: Promise | null; + + private modalCtorPromise: Promise | null; + + private stateReady: boolean; + + private saveTimer: ReturnType | null; + + private pollTimer: ReturnType | null; + + private splitPanel: SplitPanelInstance | null; + + private currentRunId: string | null; + + private runHistory: RunHistoryEntry[]; + + private getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); + } + + private isValidGraphInput(value: unknown): value is WorkflowGraphPayload { + if (!value || typeof value !== 'object') return false; + const graph = value as { nodes?: unknown; connections?: unknown }; + if (!Array.isArray(graph.nodes) || !Array.isArray(graph.connections)) return false; + return graph.nodes.every((node) => { + if (!node || typeof node !== 'object') return false; + const candidate = node as { id?: unknown; type?: unknown }; + return typeof candidate.id === 'string' && typeof candidate.type === 'string'; + }); + } + constructor() { this.modelOptions = [...DEFAULT_MODEL_OPTIONS]; this.modelEfforts = { ...DEFAULT_MODEL_EFFORTS }; @@ -52,21 +261,24 @@ export class WorkflowEditor { this.nodesLayer = document.getElementById('nodes-layer'); this.connectionsLayer = document.getElementById('connections-layer'); this.chatMessages = document.getElementById('chat-messages'); - this.initialPrompt = document.getElementById('initial-prompt'); - this.runButton = document.getElementById('btn-run'); - this.cancelRunButton = document.getElementById('btn-cancel-run'); + this.initialPrompt = document.getElementById('initial-prompt') as HTMLInputElement | HTMLTextAreaElement | null; + this.runButton = document.getElementById('btn-run') as HTMLButtonElement | null; + this.cancelRunButton = document.getElementById('btn-cancel-run') as HTMLButtonElement | null; this.zoomValue = document.getElementById('zoom-value'); - this.workflowState = 'idle'; // 'idle' | 'running' | 'paused' + this.workflowState = 'idle'; this.rightPanel = document.getElementById('right-panel'); this.pendingAgentMessage = null; this.currentPrompt = ''; this.pendingApprovalRequest = null; this.activeRunController = null; this.lastLlmResponseContent = null; + this.currentRunId = null; + this.runHistory = []; this.splitPanelCtorPromise = null; this.dropdownCtorPromise = null; this.modalCtorPromise = null; + this.splitPanel = null; this.stateReady = false; this.saveTimer = null; this.pollTimer = null; @@ -89,8 +301,8 @@ export class WorkflowEditor { this.stateReady = true; this.saveWorkflowState(); await this.recoverRun(); - }).catch((err) => { - console.error('Workflow editor initialization failed', err); + }).catch((err: unknown) => { + console.error('Workflow editor initialization failed', this.getErrorMessage(err)); }); window.addEventListener('beforeunload', () => { @@ -104,29 +316,35 @@ export class WorkflowEditor { }); } - async getDropdownCtor() { + async getDropdownCtor(): Promise { if (!this.dropdownCtorPromise) { const origin = window.location.origin; const dropdownModulePath = `${origin}/design-system/components/dropdown/dropdown.js`; - this.dropdownCtorPromise = import(/* @vite-ignore */ dropdownModulePath).then((mod) => mod.default); + this.dropdownCtorPromise = import(/* @vite-ignore */ dropdownModulePath).then( + (mod) => (mod as { default: DropdownCtor }).default + ); } return this.dropdownCtorPromise; } - async getSplitPanelCtor() { + async getSplitPanelCtor(): Promise { if (!this.splitPanelCtorPromise) { const origin = window.location.origin; const splitPanelModulePath = `${origin}/design-system/components/split-panel/split-panel.js`; - this.splitPanelCtorPromise = import(/* @vite-ignore */ splitPanelModulePath).then((mod) => mod.default); + this.splitPanelCtorPromise = import(/* @vite-ignore */ splitPanelModulePath).then( + (mod) => (mod as { default: SplitPanelCtor }).default + ); } return this.splitPanelCtorPromise; } - async getModalCtor() { + async getModalCtor(): Promise { if (!this.modalCtorPromise) { const origin = window.location.origin; const modalModulePath = `${origin}/design-system/components/modal/modal.js`; - this.modalCtorPromise = import(/* @vite-ignore */ modalModulePath).then((mod) => mod.default); + this.modalCtorPromise = import(/* @vite-ignore */ modalModulePath).then( + (mod) => (mod as { default: ModalCtor }).default + ); } return this.modalCtorPromise; } @@ -155,7 +373,7 @@ export class WorkflowEditor { // Canvas now has correct dimensions — reposition the Start node if it's // still the only node (i.e. no default-workflow.json was loaded yet or at all) - const startNode = this.nodes.find(n => n.type === 'start'); + const startNode = this.nodes.find((n) => n.type === 'start'); if (startNode && this.nodes.length === 1) { const pos = this.getDefaultStartPosition(); startNode.x = pos.x; @@ -167,7 +385,13 @@ export class WorkflowEditor { } } - async setupDropdown(container, items, selectedValue, placeholder, onSelect) { + async setupDropdown( + container: HTMLElement, + items: DropdownItem[], + selectedValue: string, + placeholder: string, + onSelect: (value: string) => void + ): Promise { const DropdownCtor = await this.getDropdownCtor(); const dropdown = new DropdownCtor(container, { placeholder, @@ -191,7 +415,8 @@ export class WorkflowEditor { this.zoomValue.textContent = `${Math.round(this.viewport.scale * 100)}%`; } - screenToWorld(clientX, clientY) { + screenToWorld(clientX: number, clientY: number): Point { + if (!this.canvas) return { x: 0, y: 0 }; const rect = this.canvas.getBoundingClientRect(); return { x: (clientX - rect.left - this.viewport.x) / this.viewport.scale, @@ -200,7 +425,7 @@ export class WorkflowEditor { } getPrimaryAgentName() { - const agentNode = this.nodes.find(n => n.type === 'agent'); + const agentNode = this.nodes.find((n) => n.type === 'agent'); if (agentNode && agentNode.data) { const name = (agentNode.data.agentName || '').trim(); if (name) return name; @@ -208,7 +433,7 @@ export class WorkflowEditor { return 'Agent'; } - getNodeWidth(node) { + getNodeWidth(node: EditorNode | null | undefined): number { if (!node) return DEFAULT_NODE_WIDTH; if (node.data && !node.data.collapsed) { return EXPANDED_NODE_WIDTH; @@ -220,31 +445,32 @@ export class WorkflowEditor { return el ? (el.offsetWidth || DEFAULT_NODE_WIDTH) : DEFAULT_NODE_WIDTH; } - normalizeIfCondition(condition) { - const normalizedOperator = condition?.operator === 'contains' ? 'contains' : 'equal'; - const rawValue = condition?.value; + normalizeIfCondition(condition: unknown): IfCondition { + const asRecord = (typeof condition === 'object' && condition !== null) ? condition as Partial : undefined; + const candidateOperator = asRecord?.operator; + const rawValue = asRecord?.value; return { - operator: normalizedOperator, + operator: candidateOperator === 'contains' ? 'contains' : 'equal', value: typeof rawValue === 'string' ? rawValue : '' }; } - getIfConditionHandle(index) { + getIfConditionHandle(index: number): string { return `${IF_CONDITION_HANDLE_PREFIX}${index}`; } - getIfConditionIndexFromHandle(handle) { + getIfConditionIndexFromHandle(handle: string | undefined): number | null { if (handle === 'true') return 0; if (typeof handle !== 'string' || !handle.startsWith(IF_CONDITION_HANDLE_PREFIX)) return null; const parsedIndex = Number.parseInt(handle.slice(IF_CONDITION_HANDLE_PREFIX.length), 10); return Number.isInteger(parsedIndex) && parsedIndex >= 0 ? parsedIndex : null; } - getIfPortTop(index) { + getIfPortTop(index: number): number { return IF_PORT_BASE_TOP + (index * IF_PORT_STEP); } - getIfConditions(node) { + getIfConditions(node: EditorNode): IfCondition[] { if (!node.data) node.data = {}; if (!Array.isArray(node.data.conditions) || node.data.conditions.length === 0) { node.data.conditions = [{ ...DEFAULT_IF_CONDITION }]; @@ -253,12 +479,13 @@ export class WorkflowEditor { return node.data.conditions; } - removeIfCondition(node, conditionIndex) { + removeIfCondition(node: EditorNode, conditionIndex: number): void { const conditions = this.getIfConditions(node); if (conditions.length <= 1) return; + if (!node.data) return; node.data.conditions = conditions.filter((_, index) => index !== conditionIndex); - this.connections = this.connections.reduce((nextConnections, connection) => { + this.connections = this.connections.reduce((nextConnections, connection) => { if (connection.source !== node.id) { nextConnections.push(connection); return nextConnections; @@ -285,7 +512,7 @@ export class WorkflowEditor { }, []); } - getOutputPortCenterYOffset(node, sourceHandle) { + getOutputPortCenterYOffset(node: EditorNode, sourceHandle?: string): number { if (node.type === 'if') { if (sourceHandle === IF_FALLBACK_HANDLE) { return this.getIfPortTop(this.getIfConditions(node).length) + 6; @@ -301,12 +528,12 @@ export class WorkflowEditor { return 24; } - setWorkflowState(state) { + setWorkflowState(state: WorkflowState): void { this.workflowState = state; this.updateRunButton(); } - setRunButtonHint(reason) { + setRunButtonHint(reason: string | null): void { if (!this.runButton) return; if (reason) { this.runButton.setAttribute('data-disabled-hint', reason); @@ -315,10 +542,10 @@ export class WorkflowEditor { } } - isAbortError(error) { + isAbortError(error: unknown): boolean { if (!error) return false; - if (error.name === 'AbortError') return true; - const message = typeof error.message === 'string' ? error.message : ''; + if (error instanceof Error && error.name === 'AbortError') return true; + const message = error instanceof Error ? error.message : ''; return message.toLowerCase().includes('aborted'); } @@ -338,7 +565,7 @@ export class WorkflowEditor { } getRunDisableReason() { - const startNodes = this.nodes.filter(node => node.type === 'start'); + const startNodes = this.nodes.filter((node: any) => node.type === 'start'); if (startNodes.length === 0) { return 'Add a Start node to run the workflow.'; } @@ -346,9 +573,9 @@ export class WorkflowEditor { return 'Use only one Start node before running.'; } - const nodeIdSet = new Set(this.nodes.map(node => node.id)); + const nodeIdSet = new Set(this.nodes.map((node: any) => node.id)); const hasBrokenConnection = this.connections.some( - (conn) => !nodeIdSet.has(conn.source) || !nodeIdSet.has(conn.target) + (conn: any) => !nodeIdSet.has(conn.source) || !nodeIdSet.has(conn.target) ); if (hasBrokenConnection) { return 'Fix broken connections before running.'; @@ -356,7 +583,7 @@ export class WorkflowEditor { const startNode = startNodes[0]; const adjacency = new Map(); - this.connections.forEach((conn) => { + this.connections.forEach((conn: any) => { if (!adjacency.has(conn.source)) adjacency.set(conn.source, []); adjacency.get(conn.source).push(conn); }); @@ -371,7 +598,7 @@ export class WorkflowEditor { while (queue.length > 0) { const nodeId = queue.shift(); const next = adjacency.get(nodeId) || []; - next.forEach((conn) => { + next.forEach((conn: any) => { if (!reachable.has(conn.target)) { reachable.add(conn.target); queue.push(conn.target); @@ -387,16 +614,16 @@ export class WorkflowEditor { if (!reachable.has(node.id)) continue; if (node.type === 'if') { const outgoing = adjacency.get(node.id) || []; - const hasConditionBranch = outgoing.some((conn) => this.getIfConditionIndexFromHandle(conn.sourceHandle) !== null); - const hasFallbackBranch = outgoing.some((conn) => conn.sourceHandle === IF_FALLBACK_HANDLE); + const hasConditionBranch = outgoing.some((conn: any) => this.getIfConditionIndexFromHandle(conn.sourceHandle) !== null); + const hasFallbackBranch = outgoing.some((conn: any) => conn.sourceHandle === IF_FALLBACK_HANDLE); if (!hasConditionBranch && !hasFallbackBranch) { return 'Connect at least one branch for each Condition node.'; } } if (node.type === 'approval' || node.type === 'input') { const outgoing = adjacency.get(node.id) || []; - const hasApprove = outgoing.some((conn) => conn.sourceHandle === 'approve'); - const hasReject = outgoing.some((conn) => conn.sourceHandle === 'reject'); + const hasApprove = outgoing.some((conn: any) => conn.sourceHandle === 'approve'); + const hasReject = outgoing.some((conn: any) => conn.sourceHandle === 'reject'); if (!hasApprove && !hasReject) { return 'Connect at least one branch for each approval node.'; } @@ -435,7 +662,7 @@ export class WorkflowEditor { } } - appendStatusMessage(text, type = '') { + appendStatusMessage(text: any, type: any = '') { if (!this.chatMessages) return; const message = document.createElement('div'); message.className = `chat-message status ${type}`; @@ -444,7 +671,7 @@ export class WorkflowEditor { this.chatMessages.scrollTop = this.chatMessages.scrollHeight; } - logManualUserMessage(text) { + logManualUserMessage(text: any) { this.appendChatMessage(text, 'user'); if (!this.runHistory) this.runHistory = []; this.runHistory.push({ role: 'user', content: text }); @@ -483,27 +710,7 @@ export class WorkflowEditor { } } - renderEffortSelect(node) { - const select = document.createElement('select'); - select.className = 'input ds-select'; - const options = this.modelEfforts[node.data.model] || this.modelEfforts[this.modelOptions[0]] || []; - if (!options.includes(node.data.reasoningEffort)) { - node.data.reasoningEffort = options[0]; - } - options.forEach(optValue => { - const opt = document.createElement('option'); - opt.value = optValue; - opt.text = optValue.charAt(0).toUpperCase() + optValue.slice(1); - if (node.data.reasoningEffort === optValue) opt.selected = true; - select.appendChild(opt); - }); - select.addEventListener('change', (e) => { - node.data.reasoningEffort = e.target.value; - }); - return select; - } - - zoomCanvas(stepPercent) { + zoomCanvas(stepPercent: any) { if (!this.canvas) return; const snappedScale = Math.round(this.viewport.scale * 10) / 10; const delta = stepPercent / 100; @@ -524,14 +731,15 @@ export class WorkflowEditor { initDragAndDrop() { const draggables = document.querySelectorAll('.draggable-node'); - draggables.forEach(el => { - el.addEventListener('dragstart', (e) => { + draggables.forEach((el: any) => { + el.addEventListener('dragstart', (e: any) => { e.dataTransfer.setData('type', el.dataset.type); }); }); - this.canvas.addEventListener('dragover', (e) => e.preventDefault()); - this.canvas.addEventListener('drop', (e) => { + if (!this.canvas) return; + this.canvas.addEventListener('dragover', (e: any) => e.preventDefault()); + this.canvas.addEventListener('drop', (e: any) => { e.preventDefault(); const type = e.dataTransfer.getData('type'); const worldPos = this.screenToWorld(e.clientX, e.clientY); @@ -541,9 +749,10 @@ export class WorkflowEditor { initCanvasInteractions() { if (this.canvas) { - this.canvas.addEventListener('mousedown', (e) => { + const canvas = this.canvas; + this.canvas.addEventListener('mousedown', (e: any) => { const isHint = e.target.classList && e.target.classList.contains('canvas-hint'); - const isBackground = e.target === this.canvas || + const isBackground = e.target === canvas || e.target === this.canvasStage || e.target === this.connectionsLayer || e.target === this.nodesLayer || @@ -551,14 +760,14 @@ export class WorkflowEditor { if (isBackground) { e.preventDefault(); this.isPanning = true; - this.canvas.classList.add('panning'); + canvas.classList.add('panning'); this.panStart = { x: e.clientX, y: e.clientY }; this.viewportStart = { ...this.viewport }; } }); } - document.addEventListener('mousemove', (e) => { + document.addEventListener('mousemove', (e: any) => { if (this.isPanning) { this.viewport.x = this.viewportStart.x + (e.clientX - this.panStart.x); this.viewport.y = this.viewportStart.y + (e.clientY - this.panStart.y); @@ -569,7 +778,7 @@ export class WorkflowEditor { if (this.isDragging && this.selectedNodeId) { if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return; - const node = this.nodes.find(n => n.id === this.selectedNodeId); + const node = this.nodes.find((n: any) => n.id === this.selectedNodeId); if (node) { const pointer = this.screenToWorld(e.clientX, e.clientY); node.x = pointer.x - this.dragOffsetWorld.x; @@ -584,7 +793,7 @@ export class WorkflowEditor { } }); - document.addEventListener('mouseup', (e) => { + document.addEventListener('mouseup', (e: any) => { if (this.isPanning) { this.isPanning = false; if (this.canvas) { @@ -618,12 +827,16 @@ export class WorkflowEditor { } initButtons() { - document.getElementById('btn-run').addEventListener('click', () => this.runWorkflow()); + if (this.runButton) { + this.runButton.addEventListener('click', () => this.runWorkflow()); + } const cancelRunBtn = document.getElementById('btn-cancel-run'); if (cancelRunBtn) { cancelRunBtn.addEventListener('click', () => this.cancelRunningWorkflow()); } - document.getElementById('btn-clear').addEventListener('click', async () => { + const clearBtn = document.getElementById('btn-clear'); + if (clearBtn) { + clearBtn.addEventListener('click', async () => { const confirmed = await this.openConfirmModal({ title: 'Clear Canvas', message: 'Remove all nodes and connections from the canvas?', @@ -643,12 +856,6 @@ export class WorkflowEditor { } this.setWorkflowState('idle'); }); - - if (this.approveBtn) { - this.approveBtn.addEventListener('click', () => this.submitApprovalDecision('approve')); - } - if (this.rejectBtn) { - this.rejectBtn.addEventListener('click', () => this.submitApprovalDecision('reject')); } const zoomInBtn = document.getElementById('btn-zoom-in'); @@ -660,12 +867,12 @@ export class WorkflowEditor { initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${window.location.host}`); - ws.onmessage = (_event) => { + ws.onmessage = (_event: any) => { // Placeholder for future real-time feedback }; } - async openConfirmModal(options = {}) { + async openConfirmModal(options: any = {}) { const { title = 'Confirm', message = 'Are you sure?', @@ -678,7 +885,7 @@ export class WorkflowEditor { const content = document.createElement('p'); content.textContent = message; - return await new Promise((resolve) => { + return await new Promise((resolve) => { let confirmed = false; const modal = new ModalCtor({ @@ -689,12 +896,12 @@ export class WorkflowEditor { { label: cancelLabel, type: 'secondary', - onClick: (_event, instance) => instance.close() + onClick: (_event: any, instance: any) => instance.close() }, { label: confirmLabel, type: 'primary', - onClick: (_event, instance) => { + onClick: (_event: any, instance: any) => { confirmed = true; instance.close(); } @@ -716,14 +923,14 @@ export class WorkflowEditor { // --- NODE MANAGEMENT --- - addNode(type, x, y) { + addNode(type: string, x: number, y: number): void { const normalizedType = type === 'input' ? 'approval' : type; - const node = { + const node: EditorNode = { id: `node_${this.nextNodeId++}`, type: normalizedType, x, y, - data: this.getDefaultData(normalizedType) + data: this.getDefaultData(normalizedType) as WorkflowNodeData }; this.nodes.push(node); this.scheduleSave(); @@ -731,10 +938,10 @@ export class WorkflowEditor { this.updateRunButton(); } - upgradeLegacyNodes(shouldRender = false) { + upgradeLegacyNodes(shouldRender: boolean = false) { let updated = false; const ifNodeIds = new Set(); - this.nodes.forEach(node => { + this.nodes.forEach((node) => { if (node.type === 'input') { node.type = 'approval'; if (node.data && node.data.prompt === undefined) { @@ -779,7 +986,7 @@ export class WorkflowEditor { async loadConfig() { try { const cfg = await fetchConfig(); - const enabledProviders = (cfg.providers ?? []).filter(p => p.enabled); + const enabledProviders = (cfg.providers ?? []).filter((p: any) => p.enabled); if (enabledProviders.length === 0) return; const options: string[] = []; const efforts: Record = {}; @@ -819,7 +1026,7 @@ export class WorkflowEditor { }, 500); } - saveRunId(runId) { + saveRunId(runId: string) { try { localStorage.setItem(WorkflowEditor.RUN_KEY, runId); } catch { /* ignore */ } } @@ -840,8 +1047,8 @@ export class WorkflowEditor { try { const raw = localStorage.getItem(WorkflowEditor.STORAGE_KEY); if (raw) { - const graph = JSON.parse(raw); - if (graph.nodes?.length) { + const graph = JSON.parse(raw) as unknown; + if (this.isValidGraphInput(graph) && graph.nodes.length > 0) { this.loadWorkflow(graph); return; // localStorage wins — skip server fetch } @@ -858,14 +1065,15 @@ export class WorkflowEditor { try { const res = await fetch('/api/default-workflow'); if (!res.ok) return; - const graph = await res.json(); + const graph = await res.json() as unknown; + if (!this.isValidGraphInput(graph)) return; this.loadWorkflow(graph); } catch { // keep the default start node already rendered synchronously } } - loadWorkflow(graph) { + loadWorkflow(graph: WorkflowGraphInput) { this.nodes = graph.nodes ?? []; this.connections = graph.connections ?? []; const maxId = this.nodes.reduce((max, n) => { @@ -878,7 +1086,7 @@ export class WorkflowEditor { } addDefaultStartNode() { - const startExists = this.nodes.some(n => n.type === 'start'); + const startExists = this.nodes.some((n) => n.type === 'start'); if (startExists) return; const { x, y } = this.getDefaultStartPosition(); this.addNode('start', x, y); @@ -899,7 +1107,7 @@ export class WorkflowEditor { return { x, y }; } - getDefaultData(type) { + getDefaultData(type: any) { switch (type) { case 'agent': return { @@ -926,14 +1134,14 @@ export class WorkflowEditor { } } - nodeHasSettings(node) { + nodeHasSettings(node: any) { if (!node) return false; return ['agent', 'if', 'approval'].includes(node.type); } - deleteNode(id) { - this.nodes = this.nodes.filter(n => n.id !== id); - this.connections = this.connections.filter(c => c.source !== id && c.target !== id); + deleteNode(id: string) { + this.nodes = this.nodes.filter((n) => n.id !== id); + this.connections = this.connections.filter((c) => c.source !== id && c.target !== id); this.render(); this.updateRunButton(); } @@ -941,14 +1149,15 @@ export class WorkflowEditor { // --- RENDERING --- render() { + if (!this.nodesLayer || !this.connectionsLayer) return; this.nodesLayer.innerHTML = ''; this.connectionsLayer.innerHTML = ''; - this.nodes.forEach(n => this.renderNode(n)); + this.nodes.forEach((n) => this.renderNode(n)); this.renderConnections(); // renderConnections() already calls scheduleSave() this.updateRunButton(); } - renderNode(node) { + renderNode(node: any) { const el = document.createElement('div'); el.className = `node box card shadowed ${node.type === 'start' ? 'start-node' : ''}`; el.id = node.id; @@ -976,7 +1185,7 @@ export class WorkflowEditor { const controls = document.createElement('div'); controls.className = 'node-controls'; - let collapseBtn = null; + let collapseBtn: HTMLButtonElement | null = null; let updateCollapseIcon = () => {}; if (hasSettings) { collapseBtn = document.createElement('button'); @@ -984,11 +1193,12 @@ export class WorkflowEditor { collapseBtn.className = 'button button-tertiary button-small icon-btn collapse'; collapseBtn.innerHTML = ''; updateCollapseIcon = () => { + if (!collapseBtn) return; collapseBtn.title = node.data.collapsed ? 'Open settings' : 'Close settings'; el.classList.toggle('expanded', !node.data.collapsed); }; updateCollapseIcon(); - collapseBtn.addEventListener('mousedown', (e) => { + collapseBtn.addEventListener('mousedown', (e: any) => { e.stopPropagation(); node.data.collapsed = !node.data.collapsed; updateCollapseIcon(); @@ -1004,7 +1214,7 @@ export class WorkflowEditor { delBtn.className = 'button button-tertiary button-small icon-btn delete'; delBtn.innerHTML = ''; delBtn.title = 'Delete Node'; - delBtn.addEventListener('mousedown', async (e) => { + delBtn.addEventListener('mousedown', async (e: any) => { e.stopPropagation(); const confirmed = await this.openConfirmModal({ title: 'Delete Node', @@ -1019,7 +1229,7 @@ export class WorkflowEditor { header.appendChild(controls); // Drag Handler - header.addEventListener('mousedown', (e) => { + header.addEventListener('mousedown', (e: any) => { const interactingWithCollapse = collapseBtn && collapseBtn.contains(e.target); const interactingWithDelete = delBtn && delBtn.contains(e.target); if (interactingWithCollapse || interactingWithDelete) return; @@ -1034,7 +1244,7 @@ export class WorkflowEditor { }; }); - header.addEventListener('dblclick', (e) => { + header.addEventListener('dblclick', (e: any) => { if (!hasSettings) return; e.stopPropagation(); node.data.collapsed = !node.data.collapsed; @@ -1059,10 +1269,12 @@ export class WorkflowEditor { // Ports this.renderPorts(node, el); - this.nodesLayer.appendChild(el); + if (this.nodesLayer) { + this.nodesLayer.appendChild(el); + } } - updateNodeHeader(node) { + updateNodeHeader(node: any) { const el = document.getElementById(node.id); if (!el) return; const headerLabel = el.querySelector('.node-header span'); @@ -1071,7 +1283,7 @@ export class WorkflowEditor { } } - renderNodePosition(node) { + renderNodePosition(node: any) { const el = document.getElementById(node.id); if (el) { el.style.left = `${node.x}px`; @@ -1079,7 +1291,7 @@ export class WorkflowEditor { } } - getNodeLabel(node) { + getNodeLabel(node: any) { if (node.type === 'agent') { const name = (node.data.agentName || 'Agent').trim() || 'Agent'; return `${escapeHtml(name)}`; @@ -1091,7 +1303,7 @@ export class WorkflowEditor { return `${node.type}`; } - getNodePreviewHTML(node) { + getNodePreviewHTML(node: any) { let text: string; if (node.type === 'agent') { const name = (node.data.agentName || 'Agent').trim(); @@ -1116,8 +1328,8 @@ export class WorkflowEditor { const enabledToolIcons = node.type === 'agent' ? TOOLS_CONFIG - .filter(t => (node.data.tools || {})[t.key]) - .map(t => ``) + .filter((t: any) => (node.data.tools || {})[t.key]) + .map((t: any) => ``) .join('') : ''; @@ -1126,10 +1338,12 @@ export class WorkflowEditor { // --- IN-NODE FORMS --- - renderNodeForm(node, container) { + renderNodeForm(node: EditorNode, container: HTMLElement) { container.innerHTML = ''; + if (!node.data) node.data = {}; + const data = node.data; - const buildLabel = (text) => { + const buildLabel = (text: string) => { const label = document.createElement('label'); label.textContent = text; return label; @@ -1141,10 +1355,10 @@ export class WorkflowEditor { const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'input'; - nameInput.value = node.data.agentName || 'Agent'; + nameInput.value = data.agentName || 'Agent'; nameInput.placeholder = 'e.g., Research Agent'; - nameInput.addEventListener('input', (e) => { - node.data.agentName = e.target.value; + nameInput.addEventListener('input', (e: any) => { + data.agentName = e.target.value; this.updatePreview(node); this.updateNodeHeader(node); }); @@ -1155,9 +1369,9 @@ export class WorkflowEditor { const sysInput = document.createElement('textarea'); sysInput.className = 'input textarea-input'; sysInput.placeholder = 'Define the agent\'s role, persona, or instructions.'; - sysInput.value = node.data.systemPrompt || ''; - sysInput.addEventListener('input', (e) => { - node.data.systemPrompt = e.target.value; + sysInput.value = data.systemPrompt || ''; + sysInput.addEventListener('input', (e: any) => { + data.systemPrompt = e.target.value; this.updatePreview(node); }); container.appendChild(sysInput); @@ -1167,9 +1381,9 @@ export class WorkflowEditor { const userInput = document.createElement('textarea'); userInput.className = 'input textarea-input'; userInput.placeholder = 'Use {{PREVIOUS_OUTPUT}} to include the previous node\'s output.'; - userInput.value = node.data.userPrompt ?? '{{PREVIOUS_OUTPUT}}'; - userInput.addEventListener('input', (e) => { - node.data.userPrompt = e.target.value; + userInput.value = data.userPrompt ?? '{{PREVIOUS_OUTPUT}}'; + userInput.addEventListener('input', (e: any) => { + data.userPrompt = e.target.value; this.scheduleSave(); }); container.appendChild(userInput); @@ -1181,11 +1395,11 @@ export class WorkflowEditor { container.appendChild(modelDropdown); this.setupDropdown( modelDropdown, - this.modelOptions.map(m => ({ value: m, label: m.toUpperCase() })), - node.data.model || this.modelOptions[0], + this.modelOptions.map((m) => ({ value: m, label: m.toUpperCase() })), + data.model || this.modelOptions[0], 'Select model', (value) => { - node.data.model = value; + data.model = value; this.updatePreview(node); this.render(); } @@ -1196,38 +1410,45 @@ export class WorkflowEditor { const effortDropdown = document.createElement('div'); effortDropdown.className = 'ds-dropdown'; container.appendChild(effortDropdown); - const effortOptions = (this.modelEfforts[node.data.model] || this.modelEfforts[this.modelOptions[0]] || []).map(optValue => ({ + const modelEfforts = data.model ? this.modelEfforts[data.model] : undefined; + const effortSource = modelEfforts || this.modelEfforts[this.modelOptions[0]] || []; + const effortOptions: DropdownItem[] = effortSource.map((optValue: string) => ({ value: optValue, label: optValue.charAt(0).toUpperCase() + optValue.slice(1) })); - const selectedEffort = effortOptions.find(o => o.value === node.data.reasoningEffort)?.value || effortOptions[0].value; - node.data.reasoningEffort = selectedEffort; - this.setupDropdown( - effortDropdown, - effortOptions, - selectedEffort, - 'Select effort', - (value) => { - node.data.reasoningEffort = value; - this.scheduleSave(); - } - ); + const selectedEffort = + effortOptions.find((o) => o.value === data.reasoningEffort)?.value + ?? effortOptions[0]?.value + ?? ''; + if (selectedEffort) { + data.reasoningEffort = selectedEffort; + this.setupDropdown( + effortDropdown, + effortOptions, + selectedEffort, + 'Select effort', + (value) => { + data.reasoningEffort = value; + this.scheduleSave(); + } + ); + } // Tools container.appendChild(buildLabel('Tools')); const toolsList = document.createElement('div'); toolsList.className = 'tools-checkbox-group'; - TOOLS_CONFIG.forEach(tool => { + TOOLS_CONFIG.forEach((tool: any) => { const label = document.createElement('label'); label.className = 'input-checkbox input-checkbox-small'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; - checkbox.checked = Boolean(node.data.tools?.[tool.key]); + checkbox.checked = Boolean(data.tools?.[tool.key]); checkbox.addEventListener('change', () => { - if (!node.data.tools) node.data.tools = {}; - node.data.tools[tool.key] = checkbox.checked; + if (!data.tools) data.tools = {}; + data.tools[tool.key] = checkbox.checked; this.updatePreview(node); }); @@ -1263,7 +1484,7 @@ export class WorkflowEditor { const conditionsList = document.createElement('div'); conditionsList.className = 'condition-list'; - conditions.forEach((condition, index) => { + conditions.forEach((condition: any, index: any) => { const row = document.createElement('div'); row.className = 'condition-row'; @@ -1275,9 +1496,9 @@ export class WorkflowEditor { IF_CONDITION_OPERATORS, condition.operator, 'Select operator', - (value) => { - conditions[index].operator = value; - node.data.conditions = conditions.map((entry) => this.normalizeIfCondition(entry)); + (value: any) => { + conditions[index].operator = value === 'contains' ? 'contains' : 'equal'; + data.conditions = conditions.map((entry: any) => this.normalizeIfCondition(entry)); this.updatePreview(node); } ); @@ -1287,9 +1508,9 @@ export class WorkflowEditor { valueInput.className = 'input'; valueInput.placeholder = 'Value'; valueInput.value = condition.value || ''; - valueInput.addEventListener('input', (e) => { + valueInput.addEventListener('input', (e: any) => { conditions[index].value = e.target.value; - node.data.conditions = conditions.map((entry) => this.normalizeIfCondition(entry)); + data.conditions = conditions.map((entry: any) => this.normalizeIfCondition(entry)); this.updatePreview(node); }); row.appendChild(valueInput); @@ -1300,7 +1521,7 @@ export class WorkflowEditor { removeConditionButton.className = 'button button-tertiary button-small icon-btn condition-remove-btn'; removeConditionButton.title = `Remove condition ${index + 1}`; removeConditionButton.innerHTML = ''; - removeConditionButton.addEventListener('click', (e) => { + removeConditionButton.addEventListener('click', (e: any) => { e.preventDefault(); this.removeIfCondition(node, index); this.updatePreview(node); @@ -1318,9 +1539,9 @@ export class WorkflowEditor { addConditionButton.type = 'button'; addConditionButton.className = 'button button-secondary button-small add-condition-btn'; addConditionButton.textContent = '+ Add Condition'; - addConditionButton.addEventListener('click', (e) => { + addConditionButton.addEventListener('click', (e: any) => { e.preventDefault(); - node.data.conditions = [...conditions, { ...DEFAULT_IF_CONDITION }]; + data.conditions = [...conditions, { ...DEFAULT_IF_CONDITION }]; this.updatePreview(node); this.render(); }); @@ -1331,10 +1552,10 @@ export class WorkflowEditor { const pInput = document.createElement('input'); pInput.type = 'text'; pInput.className = 'input'; - pInput.value = node.data.prompt || ''; + pInput.value = data.prompt || ''; pInput.placeholder = 'Message shown to user when approval is required'; - pInput.addEventListener('input', (e) => { - node.data.prompt = e.target.value; + pInput.addEventListener('input', (e: any) => { + data.prompt = e.target.value; this.scheduleSave(); }); container.appendChild(pInput); @@ -1344,7 +1565,7 @@ export class WorkflowEditor { } } - updatePreview(node) { + updatePreview(node: any) { const el = document.getElementById(node.id); if(!el) return; const preview = el.querySelector('.node-preview'); @@ -1354,7 +1575,7 @@ export class WorkflowEditor { // --- PORTS & CONNECTIONS (Updated for Arrows) --- - renderPorts(node, el) { + renderPorts(node: any, el: any) { if (node.type !== 'start') { const portIn = this.createPort(node.id, 'input', 'port-in'); el.appendChild(portIn); @@ -1363,7 +1584,7 @@ export class WorkflowEditor { if (node.type !== 'end') { if (node.type === 'if') { const conditions = this.getIfConditions(node); - conditions.forEach((condition, index) => { + conditions.forEach((condition: any, index: any) => { const operatorLabel = condition.operator === 'contains' ? 'Contains' : 'Equal'; const conditionValue = condition.value || ''; const title = `Condition ${index + 1}: ${operatorLabel} "${conditionValue}"`; @@ -1395,7 +1616,7 @@ export class WorkflowEditor { } } - createPort(nodeId, handle, className, title = '', top = null) { + createPort(nodeId: string, handle: string, className: string, title = '', top: number | null = null): HTMLDivElement { const port = document.createElement('div'); port.className = `port ${className}`; if (title) port.title = title; @@ -1406,16 +1627,16 @@ export class WorkflowEditor { port.dataset.handle = handle; if (handle === 'input') { - port.addEventListener('mouseup', (e) => this.onPortMouseUp(e, nodeId, handle)); + port.addEventListener('mouseup', (e: any) => this.onPortMouseUp(e, nodeId, handle)); } else { - port.addEventListener('mousedown', (e) => this.onPortMouseDown(e, nodeId, handle)); + port.addEventListener('mousedown', (e: any) => this.onPortMouseDown(e, nodeId, handle)); } return port; } // --- CONNECTION LOGIC (Same as before but renders arrows via CSS) --- - onPortMouseDown(e, nodeId, handle) { + onPortMouseDown(e: any, nodeId: any, handle: any) { e.stopPropagation(); e.preventDefault(); if (!this.connectionsLayer) return; @@ -1428,13 +1649,14 @@ export class WorkflowEditor { this.connectionsLayer.appendChild(this.tempConnection); } - updateTempConnection(e) { + updateTempConnection(e: any) { if (!this.connectionStart) return; + if (!this.tempConnection) return; const world = this.screenToWorld(e.clientX, e.clientY); this.tempConnection.setAttribute('d', this.getPathD(this.connectionStart.x, this.connectionStart.y, world.x, world.y)); } - onPortMouseUp(e, nodeId, handle) { + onPortMouseUp(e: any, nodeId: any, handle: any) { e.stopPropagation(); if (this.connectionStart && this.connectionStart.nodeId !== nodeId) { // If we're reconnecting an existing connection, create new connection with updated target @@ -1472,7 +1694,7 @@ export class WorkflowEditor { } } - onConnectionLineMouseDown(e, connection, connIndex) { + onConnectionLineMouseDown(e: any, connection: WorkflowConnection, connIndex: number) { e.stopPropagation(); e.preventDefault(); if (!this.connectionsLayer) return; @@ -1480,7 +1702,7 @@ export class WorkflowEditor { // Track that we're reconnecting this connection this.reconnectingConnection = connIndex; - const sourceNode = this.nodes.find(n => n.id === connection.source); + const sourceNode = this.nodes.find((n: any) => n.id === connection.source); if (!sourceNode) return; const startYOffset = this.getOutputPortCenterYOffset(sourceNode, connection.sourceHandle); @@ -1489,11 +1711,11 @@ export class WorkflowEditor { const startY = sourceNode.y + startYOffset; const world = this.screenToWorld(e.clientX, e.clientY); - this.connectionStart = { - nodeId: connection.source, - handle: connection.sourceHandle, - x: startX, - y: startY + this.connectionStart = { + nodeId: connection.source, + handle: connection.sourceHandle ?? '', + x: startX, + y: startY }; // Remove the original connection temporarily @@ -1510,15 +1732,16 @@ export class WorkflowEditor { renderConnections() { if (!this.connectionsLayer) return; + const connectionsLayer = this.connectionsLayer; // Clear only permanent lines - const lines = Array.from(this.connectionsLayer.querySelectorAll('.connection-line')); - lines.forEach(line => { + const lines = Array.from(connectionsLayer.querySelectorAll('.connection-line')); + lines.forEach((line: any) => { if (line !== this.tempConnection) line.remove(); }); - this.connections.forEach((conn, index) => { - const sourceNode = this.nodes.find(n => n.id === conn.source); - const targetNode = this.nodes.find(n => n.id === conn.target); + this.connections.forEach((conn: any, index: any) => { + const sourceNode = this.nodes.find((n: any) => n.id === conn.source); + const targetNode = this.nodes.find((n: any) => n.id === conn.target); if (!sourceNode || !targetNode) return; const startYOffset = this.getOutputPortCenterYOffset(sourceNode, conn.sourceHandle); @@ -1536,24 +1759,24 @@ export class WorkflowEditor { path.dataset.sourceNodeId = conn.source; path.dataset.sourceHandle = conn.sourceHandle; path.dataset.targetNodeId = conn.target; - path.addEventListener('mousedown', (e) => this.onConnectionLineMouseDown(e, conn, index)); - this.connectionsLayer.appendChild(path); + path.addEventListener('mousedown', (e: any) => this.onConnectionLineMouseDown(e, conn, index)); + connectionsLayer.appendChild(path); }); this.scheduleSave(); } - getPathD(startX, startY, endX, endY) { + getPathD(startX: number, startY: number, endX: number, endY: number): string { const controlPointOffset = Math.abs(endX - startX) * 0.5; return `M ${startX} ${startY} C ${startX + controlPointOffset} ${startY}, ${endX - controlPointOffset} ${endY}, ${endX} ${endY}`; } - formatApprovalMessage(decision, note) { + formatApprovalMessage(decision: any, note: any) { const base = decision === 'approve' ? 'User approved this step.' : 'User rejected this step.'; const trimmedNote = (note || '').trim(); return trimmedNote ? `${base} Feedback: ${trimmedNote}` : base; } - replaceApprovalWithResult(decision, note) { + replaceApprovalWithResult(decision: any, note: any) { if (!this.pendingApprovalRequest?.container) return; const container = this.pendingApprovalRequest.container; @@ -1590,10 +1813,10 @@ export class WorkflowEditor { this.pendingApprovalRequest = null; } - showApprovalMessage(nodeId) { + showApprovalMessage(nodeId: any) { if (!this.chatMessages) return; this.clearApprovalMessage(); - const node = this.nodes.find(n => n.id === nodeId); + const node = this.nodes.find((n: any) => n.id === nodeId); const messageText = node?.data?.prompt || 'Approval required before continuing.'; const message = document.createElement('div'); @@ -1634,13 +1857,13 @@ export class WorkflowEditor { this.pendingApprovalRequest = null; } - setApprovalButtonsDisabled(disabled) { + setApprovalButtonsDisabled(disabled: any) { if (!this.pendingApprovalRequest) return; this.pendingApprovalRequest.approveBtn.disabled = disabled; this.pendingApprovalRequest.rejectBtn.disabled = disabled; } - extractWaitingNodeId(logs = []) { + extractWaitingNodeId(logs: any = []) { if (!Array.isArray(logs)) return null; for (let i = logs.length - 1; i >= 0; i -= 1) { if (logs[i].type === 'wait_input') { @@ -1650,16 +1873,16 @@ export class WorkflowEditor { return null; } - selectNode(id) { + selectNode(id: any) { this.selectedNodeId = id; - document.querySelectorAll('.node').forEach(el => el.classList.remove('selected')); + document.querySelectorAll('.node').forEach((el: any) => el.classList.remove('selected')); const el = document.getElementById(id); if (el) el.classList.add('selected'); } // --- CHAT PANEL HELPERS --- - appendChatMessage(text, role = 'system', agentName?: string) { + appendChatMessage(text: any, role: any = 'system', agentName?: string) { if (!this.chatMessages) return; const message = document.createElement('div'); message.className = `chat-message ${role}`; @@ -1685,13 +1908,13 @@ export class WorkflowEditor { this.chatMessages.scrollTop = this.chatMessages.scrollHeight; } - startChatSession(_promptText) { + startChatSession(_promptText: any) { if (!this.chatMessages) return; this.chatMessages.innerHTML = ''; this.showAgentSpinner(); } - mapLogEntryToRole(entry) { + mapLogEntryToRole(entry: any) { const type = entry.type || ''; if (type.includes('llm_response')) return 'agent'; if (type.includes('llm_error') || type === 'error') return 'error'; @@ -1699,20 +1922,20 @@ export class WorkflowEditor { return null; } - formatLogContent(entry) { + formatLogContent(entry: any) { const content = entry.content; return typeof content === 'string' ? content : ''; } getAgentNameForNode(nodeId: string): string { - const node = this.nodes.find(n => n.id === nodeId); + const node = this.nodes.find((n: any) => n.id === nodeId); return (node?.data?.agentName || '').trim() || 'Agent'; } - onLogEntry(entry) { + onLogEntry(entry: any) { const type = entry.type || ''; if (type === 'step_start') { - const node = this.nodes.find(n => n.id === entry.nodeId); + const node = this.nodes.find((n: any) => n.id === entry.nodeId); if (node?.type === 'agent') { this.showAgentSpinner(this.getAgentNameForNode(entry.nodeId)); } @@ -1733,12 +1956,12 @@ export class WorkflowEditor { } } - renderChatFromLogs(logs = []) { + renderChatFromLogs(logs: any = []) { if (!this.chatMessages) return; this.chatMessages.innerHTML = ''; this.lastLlmResponseContent = null; let messageShown = false; - logs.forEach(entry => { + logs.forEach((entry: any) => { const role = this.mapLogEntryToRole(entry); if (!role) return; // Only show the user's initial input (before any agent has responded) @@ -1763,7 +1986,7 @@ export class WorkflowEditor { if (this.workflowState !== 'idle') return; this.upgradeLegacyNodes(); - const startNode = this.nodes.find(n => n.type === 'start'); + const startNode = this.nodes.find((n) => n.type === 'start'); if (!startNode) { alert('Add a Start node and connect your workflow before running.'); return; @@ -1771,11 +1994,12 @@ export class WorkflowEditor { this.setWorkflowState('running'); - this.currentPrompt = this.initialPrompt.value || ''; + this.currentPrompt = this.initialPrompt?.value || ''; this.startChatSession(this.currentPrompt); this.lastLlmResponseContent = null; // Update Start Node with initial input + if (!startNode.data) startNode.data = {}; startNode.data.initialInput = this.currentPrompt; const graph = { @@ -1788,14 +2012,14 @@ export class WorkflowEditor { try { const result = await runWorkflowStream( graph, - (entry) => this.onLogEntry(entry), - { signal: controller.signal, onStart: (id) => this.saveRunId(id) } + (entry: any) => this.onLogEntry(entry), + { signal: controller.signal, onStart: (id: string) => this.saveRunId(id) } ); this.handleRunResult(result, true); } catch (e) { if (this.isAbortError(e)) return; - this.appendChatMessage(e.message, 'error'); + this.appendChatMessage(this.getErrorMessage(e), 'error'); this.appendStatusMessage('Failed', 'failed'); this.hideAgentSpinner(); this.setWorkflowState('idle'); @@ -1806,12 +2030,12 @@ export class WorkflowEditor { } } - handleRunResult(result, fromStream = false) { + handleRunResult(result: WorkflowRunResult, fromStream = false) { if (!fromStream && result.logs) { this.renderChatFromLogs(result.logs); } const hasLlmError = Array.isArray(result.logs) - ? result.logs.some(entry => (entry?.type || '').includes('llm_error')) + ? result.logs.some((entry) => (entry?.type || '').includes('llm_error')) : false; if (result.status === 'paused' && result.waitingForInput) { @@ -1839,10 +2063,8 @@ export class WorkflowEditor { this.currentRunId = null; } else { this.clearApprovalMessage(); - if (result.status !== 'paused') { - this.hideAgentSpinner(); - this.setWorkflowState('idle'); - } + this.hideAgentSpinner(); + this.setWorkflowState('idle'); } } @@ -1883,7 +2105,7 @@ export class WorkflowEditor { } } - pollForRun(runId, knownLogCount) { + pollForRun(runId: string, knownLogCount: number) { this.pollTimer = setTimeout(async () => { this.pollTimer = null; let result; @@ -1917,7 +2139,7 @@ export class WorkflowEditor { }, 2000); } - async submitApprovalDecision(decision) { + async submitApprovalDecision(decision: any) { if (!this.currentRunId) return; this.setApprovalButtonsDisabled(true); const note = ''; @@ -1932,7 +2154,7 @@ export class WorkflowEditor { this.handleRunResult(result); } catch (e) { if (this.isAbortError(e)) return; - this.appendChatMessage(e.message, 'error'); + this.appendChatMessage(this.getErrorMessage(e), 'error'); this.appendStatusMessage('Failed', 'failed'); this.hideAgentSpinner(); this.setWorkflowState('idle');