'use client'; import * as React from 'react'; import { cn } from '../../lib/utils'; import { DualPaneWorkspace } from '../workspace/DualPaneWorkspace'; import { WorkflowCanvas } from './WorkflowCanvas'; import { NodePalette } from './NodePalette'; import { NodeInspector } from './NodeInspector'; import { addEdgeToGraph, removeEdgesFromGraph, removeNodesFromGraph, } from './graph-ops'; import type { NodeTypeDef, WfEdge, WfNode, WorkflowGraph } from './types'; export interface WorkflowEditorProps { /** The graph being edited (controlled when `onGraphChange` is provided). */ graph: WorkflowGraph; /** Node-type catalog for the palette + canvas enrichment + inspector. */ nodeTypes: NodeTypeDef[]; /** Emitted with the next graph whenever the editor mutates it. */ onGraphChange?: (graph: WorkflowGraph) => void; className?: string; } function nextNodeId(graph: WorkflowGraph): string { let i = graph.nodes.length + 1; const ids = new Set(graph.nodes.map((n) => n.id)); while (ids.has(`node-${i}`)) i += 1; return `node-${i}`; } /** * Visual workflow editor: a {@link NodePalette} + interactive * {@link WorkflowCanvas} on the left, a JSON-Schema-driven * {@link NodeInspector} on the right (via {@link DualPaneWorkspace}). Manages * selection internally and surfaces graph mutations (rename, param edits, * palette add) through `onGraphChange`. */ export function WorkflowEditor({ graph, nodeTypes, onGraphChange, className, }: WorkflowEditorProps) { const [selectedId, setSelectedId] = React.useState(null); const [selectedEdgeId, setSelectedEdgeId] = React.useState( null ); const typeIndex = React.useMemo(() => { const idx: Record = {}; for (const nt of nodeTypes) idx[nt.slug] = nt; return idx; }, [nodeTypes]); const selectedNode = selectedId ? graph.nodes.find((n) => n.id === selectedId) ?? null : null; const selectedType = selectedNode ? typeIndex[selectedNode.type] : undefined; const handleRename = React.useCallback( (nodeId: string, name: string) => { onGraphChange?.({ ...graph, nodes: graph.nodes.map((n) => (n.id === nodeId ? { ...n, name } : n)), }); }, [graph, onGraphChange] ); const handleParametersChange = React.useCallback( (nodeId: string, parameters: Record) => { onGraphChange?.({ ...graph, nodes: graph.nodes.map((n) => n.id === nodeId ? { ...n, parameters } : n ), }); }, [graph, onGraphChange] ); const handleAddNode = React.useCallback( (slug: string) => { const def = typeIndex[slug]; const id = nextNodeId(graph); const node: WfNode = { id, name: def?.name ?? slug, type: slug, parameters: {}, }; onGraphChange?.({ ...graph, nodes: [...graph.nodes, node] }); setSelectedId(id); }, [graph, onGraphChange, typeIndex] ); const handleConnectEdge = React.useCallback( (edge: WfEdge) => { onGraphChange?.(addEdgeToGraph(graph, edge)); }, [graph, onGraphChange] ); const handleDeleteNodes = React.useCallback( (nodeIds: string[]) => { onGraphChange?.(removeNodesFromGraph(graph, nodeIds)); if (selectedId && nodeIds.includes(selectedId)) setSelectedId(null); }, [graph, onGraphChange, selectedId] ); const handleDeleteEdges = React.useCallback( (edges: WfEdge[]) => { onGraphChange?.(removeEdgesFromGraph(graph, edges)); }, [graph, onGraphChange] ); const left = ( } right={ handleAddNode(slug)} onConnectEdge={onGraphChange ? handleConnectEdge : undefined} onDeleteNodes={onGraphChange ? handleDeleteNodes : undefined} onDeleteEdges={onGraphChange ? handleDeleteEdges : undefined} /> } /> ); return (
} />
); }