/** * Workflow Builder Component * * Visual workflow editor using React Flow for creating and editing * Pipeline DSL configurations. */ import { useCallback, useRef, useEffect } from 'react'; import { ReactFlow, Controls, Background, MiniMap, BackgroundVariant, Connection, addEdge, useNodesState, useEdgesState, Node, NodeChange, EdgeChange, Edge, NodeTypes, ReactFlowProvider, useReactFlow, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useWorkflowBuilderStore, paletteCategories } from '../../store/workflowBuilderStore'; import type { WorkflowNodeData, WorkflowNodeType } from '../../lib/workflow-builder/types'; // Import custom node components import { InputNode } from './nodes/InputNode'; import { LlmNode } from './nodes/LlmNode'; import { SkillNode } from './nodes/SkillNode'; import { HandNode } from './nodes/HandNode'; import { ConditionNode } from './nodes/ConditionNode'; import { ParallelNode } from './nodes/ParallelNode'; import { ExportNode } from './nodes/ExportNode'; import { HttpNode } from './nodes/HttpNode'; import { OrchestrationNode } from './nodes/OrchestrationNode'; import { NodePalette } from './NodePalette'; import { PropertyPanel } from './PropertyPanel'; import { WorkflowToolbar } from './WorkflowToolbar'; // ============================================================================= // Node Types Configuration // ============================================================================= const nodeTypes: NodeTypes = { input: InputNode, llm: LlmNode, skill: SkillNode, hand: HandNode, condition: ConditionNode, parallel: ParallelNode, export: ExportNode, http: HttpNode, orchestration: OrchestrationNode, }; // ============================================================================= // Main Component // ============================================================================= export function WorkflowBuilderInternal() { const reactFlowWrapper = useRef(null); const { screenToFlowPosition } = useReactFlow(); const { canvas, isDirty, selectedNodeId, validation, addNode, updateNode, deleteNode, addEdge: addStoreEdge, selectNode, saveWorkflow, validate, setDragging, } = useWorkflowBuilderStore(); // Local state for React Flow const [nodes, setNodes, onNodesChange] = useNodesState>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // Sync canvas state with React Flow useEffect(() => { if (canvas) { setNodes(canvas.nodes.map(n => ({ id: n.id, type: n.type, position: n.position, data: n.data as WorkflowNodeData, }))); setEdges(canvas.edges.map(e => ({ id: e.id, source: e.source, target: e.target, type: e.type || 'default', animated: true, }))); } else { setNodes([]); setEdges([]); } }, [canvas?.id]); // Handle node changes (position, selection) const handleNodesChange = useCallback( (changes: NodeChange>[]) => { onNodesChange(changes); // Sync position changes back to store for (const change of changes) { if (change.type === 'position' && change.position) { const node = nodes.find(n => n.id === change.id); if (node) { // Position updates are handled by React Flow internally } } if (change.type === 'select') { selectNode(change.selected ? change.id : null); } } }, [onNodesChange, nodes, selectNode] ); // Handle edge changes const handleEdgesChange = useCallback( (changes: EdgeChange[]) => { onEdgesChange(changes); }, [onEdgesChange] ); // Handle new connections const onConnect = useCallback( (connection: Connection) => { if (connection.source && connection.target) { addStoreEdge(connection.source, connection.target); setEdges((eds) => addEdge( { ...connection, type: 'default', animated: true, }, eds ) ); } }, [addStoreEdge, setEdges] ); // Handle node click const onNodeClick = useCallback( (_event: React.MouseEvent, node: Node) => { selectNode(node.id); }, [selectNode] ); // Handle pane click (deselect) const onPaneClick = useCallback(() => { selectNode(null); }, [selectNode]); // Handle drag over for palette items const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // Handle drop from palette const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const type = event.dataTransfer.getData('application/reactflow') as WorkflowNodeType; if (!type) return; const position = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); addNode(type, position); }, [screenToFlowPosition, addNode] ); // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Delete selected node if ((event.key === 'Delete' || event.key === 'Backspace') && selectedNodeId) { deleteNode(selectedNodeId); } // Save workflow if ((event.ctrlKey || event.metaKey) && event.key === 's') { event.preventDefault(); saveWorkflow(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedNodeId, deleteNode, saveWorkflow]); if (!canvas) { return (

No workflow loaded

); } return (
{/* Node Palette */} { setDragging(true); }} onDragEnd={() => { setDragging(false); }} /> {/* Canvas */}
{ switch (node.type) { case 'input': return '#10b981'; case 'llm': return '#8b5cf6'; case 'skill': return '#f59e0b'; case 'hand': return '#ef4444'; case 'export': return '#3b82f6'; default: return '#6b7280'; } }} maskColor="rgba(0, 0, 0, 0.1)" />
{/* Property Panel */} {selectedNodeId && ( n.id === selectedNodeId)?.data as WorkflowNodeData} onUpdate={(data) => updateNode(selectedNodeId, data)} onDelete={() => deleteNode(selectedNodeId)} onClose={() => selectNode(null)} /> )}
); } // Export with provider export function WorkflowBuilder() { return ( ); } export default WorkflowBuilder;