/** * Workflow Builder Store * * Zustand store for managing workflow builder state. */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { WorkflowCanvas, WorkflowNode, WorkflowEdge, WorkflowNodeData, WorkflowTemplate, ValidationResult, NodePaletteItem, WorkflowNodeType, NodeCategory, } from '../lib/workflow-builder/types'; import { validateCanvas } from '../lib/workflow-builder/yaml-converter'; // ============================================================================= // Store State // ============================================================================= interface WorkflowBuilderState { // Canvas state canvas: WorkflowCanvas | null; workflows: WorkflowCanvas[]; // Selection selectedNodeId: string | null; selectedEdgeId: string | null; // UI state isDragging: boolean; isDirty: boolean; isPreviewOpen: boolean; validation: ValidationResult | null; // Templates templates: WorkflowTemplate[]; // Available items for palette availableSkills: Array<{ id: string; name: string; description: string }>; availableHands: Array<{ id: string; name: string; actions: string[] }>; // Actions createNewWorkflow: (name: string, description?: string) => void; loadWorkflow: (id: string) => void; saveWorkflow: () => void; deleteWorkflow: (id: string) => void; // Node actions addNode: (type: WorkflowNodeType, position: { x: number; y: number }) => void; updateNode: (nodeId: string, data: Partial) => void; deleteNode: (nodeId: string) => void; duplicateNode: (nodeId: string) => void; // Edge actions addEdge: (source: string, target: string) => void; deleteEdge: (edgeId: string) => void; // Selection actions selectNode: (nodeId: string | null) => void; selectEdge: (edgeId: string | null) => void; // UI actions setDragging: (isDragging: boolean) => void; setPreviewOpen: (isOpen: boolean) => void; validate: () => ValidationResult; // Data loading setAvailableSkills: (skills: Array<{ id: string; name: string; description: string }>) => void; setAvailableHands: (hands: Array<{ id: string; name: string; actions: string[] }>) => void; // Canvas metadata updateCanvasMetadata: (updates: Partial>) => void; } // ============================================================================= // Default Node Data // ============================================================================= function getDefaultNodeData(type: WorkflowNodeType, _id: string): WorkflowNodeData { const base = { label: type.charAt(0).toUpperCase() + type.slice(1) }; switch (type) { case 'input': return { type: 'input', ...base, variableName: 'input', schema: undefined }; case 'llm': return { type: 'llm', ...base, template: '', isTemplateFile: false, jsonMode: false }; case 'skill': return { type: 'skill', ...base, skillId: '', inputMappings: {} }; case 'hand': return { type: 'hand', ...base, handId: '', action: '', params: {} }; case 'orchestration': return { type: 'orchestration', ...base, inputMappings: {} }; case 'condition': return { type: 'condition', ...base, condition: '', branches: [{ when: '', label: 'Branch 1' }], hasDefault: true }; case 'parallel': return { type: 'parallel', ...base, each: '${inputs.items}', maxWorkers: 4 }; case 'loop': return { type: 'loop', ...base, each: '${inputs.items}', itemVar: 'item', indexVar: 'index' }; case 'export': return { type: 'export', ...base, formats: ['json'] }; case 'http': return { type: 'http', ...base, url: '', method: 'GET', headers: {} }; case 'setVar': return { type: 'setVar', ...base, variableName: 'result', value: '' }; case 'delay': return { type: 'delay', ...base, ms: 1000 }; default: throw new Error(`Unknown node type: ${type}`); } } // ============================================================================= // Store Implementation // ============================================================================= export const useWorkflowBuilderStore = create()( persist( (set, get) => ({ // Initial state canvas: null, workflows: [], selectedNodeId: null, selectedEdgeId: null, isDragging: false, isDirty: false, isPreviewOpen: false, validation: null, templates: [], availableSkills: [], availableHands: [], // Workflow actions createNewWorkflow: (name, description) => { const canvas: WorkflowCanvas = { id: `workflow_${Date.now()}`, name, description, category: 'custom', nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 }, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: [], version: '1.0.0', }, }; set({ canvas, isDirty: false, selectedNodeId: null, selectedEdgeId: null, validation: null }); }, loadWorkflow: (id) => { const workflow = get().workflows.find(w => w.id === id); if (workflow) { set({ canvas: workflow, isDirty: false, selectedNodeId: null, selectedEdgeId: null }); } }, saveWorkflow: () => { const { canvas, workflows } = get(); if (!canvas) return; const updatedCanvas: WorkflowCanvas = { ...canvas, metadata: { ...canvas.metadata, updatedAt: new Date().toISOString(), }, }; const existingIndex = workflows.findIndex(w => w.id === canvas.id); let updatedWorkflows: WorkflowCanvas[]; if (existingIndex >= 0) { updatedWorkflows = [...workflows]; updatedWorkflows[existingIndex] = updatedCanvas; } else { updatedWorkflows = [...workflows, updatedCanvas]; } set({ workflows: updatedWorkflows, canvas: updatedCanvas, isDirty: false }); }, deleteWorkflow: (id) => { set(state => ({ workflows: state.workflows.filter(w => w.id !== id), canvas: state.canvas?.id === id ? null : state.canvas, })); }, // Node actions addNode: (type, position) => { const { canvas } = get(); if (!canvas) return; const id = `${type}_${Date.now()}`; const node: WorkflowNode = { id, type, position, data: getDefaultNodeData(type, id), }; set({ canvas: { ...canvas, nodes: [...canvas.nodes, node] }, isDirty: true, selectedNodeId: id, }); }, updateNode: (nodeId, data) => { const { canvas } = get(); if (!canvas) return; const updatedNodes = canvas.nodes.map(node => node.id === nodeId ? { ...node, data: { ...node.data, ...data } as WorkflowNodeData } : node ); set({ canvas: { ...canvas, nodes: updatedNodes }, isDirty: true }); }, deleteNode: (nodeId) => { const { canvas } = get(); if (!canvas) return; const updatedNodes = canvas.nodes.filter(n => n.id !== nodeId); const updatedEdges = canvas.edges.filter(e => e.source !== nodeId && e.target !== nodeId); set({ canvas: { ...canvas, nodes: updatedNodes, edges: updatedEdges }, isDirty: true, selectedNodeId: null, }); }, duplicateNode: (nodeId) => { const { canvas } = get(); if (!canvas) return; const node = canvas.nodes.find(n => n.id === nodeId); if (!node) return; const newId = `${node.type}_${Date.now()}`; const newNode: WorkflowNode = { ...node, id: newId, position: { x: node.position.x + 50, y: node.position.y + 50, }, data: { ...node.data, label: `${node.data.label} (copy)` } as WorkflowNodeData, }; set({ canvas: { ...canvas, nodes: [...canvas.nodes, newNode] }, isDirty: true, selectedNodeId: newId, }); }, // Edge actions addEdge: (source, target) => { const { canvas } = get(); if (!canvas) return; // Check if edge already exists const exists = canvas.edges.some(e => e.source === source && e.target === target); if (exists) return; const edge: WorkflowEdge = { id: `edge_${source}_${target}`, source, target, type: 'default', }; set({ canvas: { ...canvas, edges: [...canvas.edges, edge] }, isDirty: true }); }, deleteEdge: (edgeId) => { const { canvas } = get(); if (!canvas) return; set({ canvas: { ...canvas, edges: canvas.edges.filter(e => e.id !== edgeId) }, isDirty: true, }); }, // Selection actions selectNode: (nodeId) => set({ selectedNodeId: nodeId, selectedEdgeId: null }), selectEdge: (edgeId) => set({ selectedEdgeId: edgeId, selectedNodeId: null }), // UI actions setDragging: (isDragging) => set({ isDragging }), setPreviewOpen: (isOpen) => set({ isPreviewOpen: isOpen }), validate: () => { const { canvas } = get(); if (!canvas) { return { valid: false, errors: [{ nodeId: 'canvas', message: 'No workflow loaded', severity: 'error' as const }], warnings: [] }; } const result = validateCanvas(canvas); set({ validation: result }); return result; }, // Data loading setAvailableSkills: (skills) => set({ availableSkills: skills }), setAvailableHands: (hands) => set({ availableHands: hands }), // Canvas metadata updateCanvasMetadata: (updates) => { const { canvas } = get(); if (!canvas) return; set({ canvas: { ...canvas, ...updates }, isDirty: true }); }, }), { name: 'workflow-builder-storage', partialize: (state) => ({ workflows: state.workflows, templates: state.templates, }), } ) ); // ============================================================================= // Node Palette Items // ============================================================================= export const nodePaletteItems: NodePaletteItem[] = [ // Input category { type: 'input', label: 'Input', description: 'Define workflow input variables', icon: '📥', category: 'input', defaultData: { variableName: 'input' }, }, // AI category { type: 'llm', label: 'LLM Generate', description: 'Generate text using LLM', icon: '🤖', category: 'ai', defaultData: { template: '', jsonMode: false }, }, { type: 'skill', label: 'Skill', description: 'Execute a skill', icon: '⚡', category: 'ai', defaultData: { skillId: '', inputMappings: {} }, }, { type: 'orchestration', label: 'Skill Orchestration', description: 'Execute multiple skills in a DAG', icon: '🔀', category: 'ai', defaultData: { inputMappings: {} }, }, // Action category { type: 'hand', label: 'Hand', description: 'Execute a hand action', icon: '✋', category: 'action', defaultData: { handId: '', action: '', params: {} }, }, { type: 'http', label: 'HTTP Request', description: 'Make an HTTP request', icon: '🌐', category: 'action', defaultData: { url: '', method: 'GET', headers: {} }, }, { type: 'setVar', label: 'Set Variable', description: 'Set a variable value', icon: '📝', category: 'action', defaultData: { variableName: '', value: '' }, }, { type: 'delay', label: 'Delay', description: 'Pause execution', icon: '⏱️', category: 'action', defaultData: { ms: 1000 }, }, // Control category { type: 'condition', label: 'Condition', description: 'Branch based on condition', icon: '🔀', category: 'control', defaultData: { condition: '', branches: [{ when: '', label: 'Branch' }] }, }, { type: 'parallel', label: 'Parallel', description: 'Execute in parallel', icon: '⚡', category: 'control', defaultData: { each: '${inputs.items}', maxWorkers: 4 }, }, { type: 'loop', label: 'Loop', description: 'Iterate over items', icon: '🔄', category: 'control', defaultData: { each: '${inputs.items}', itemVar: 'item', indexVar: 'index' }, }, // Output category { type: 'export', label: 'Export', description: 'Export to file formats', icon: '📤', category: 'output', defaultData: { formats: ['json'] }, }, ]; // Group palette items by category export const paletteCategories: Record = { input: nodePaletteItems.filter(i => i.category === 'input'), ai: nodePaletteItems.filter(i => i.category === 'ai'), action: nodePaletteItems.filter(i => i.category === 'action'), control: nodePaletteItems.filter(i => i.category === 'control'), output: nodePaletteItems.filter(i => i.category === 'output'), };