'use client'; import { useCallback, useEffect } from 'react'; import ReactFlow, { Node, Edge, Controls, Background, useNodesState, useEdgesState, addEdge, Connection, ReactFlowProvider, Panel, MarkerType, useReactFlow, } from 'reactflow'; import 'reactflow/dist/style.css'; import { Button } from '@/components/ui/button'; import { Plus, Download, Save, Layout } from 'lucide-react'; // Define custom node styles (simplified for now) const nodeStyle = { padding: '10px 20px', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', fontWeight: 500, background: 'white', color: '#333', width: 180, // Increased width for role display textAlign: 'center' as const, whiteSpace: 'pre-wrap' as const, // Allow multiline }; const conditionNodeStyle = { ...nodeStyle, background: '#fef3c7', // Amber-100 borderColor: '#d97706', // Amber-600 borderStyle: 'dashed', borderRadius: '24px', // More rounded }; const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Start' }, position: { x: 250, y: 5 }, style: { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' }, }, ]; interface VisualWorkflowBuilderProps { initialNodes?: Node[]; initialEdges?: Edge[]; dslString?: string; // New prop onSave?: (nodes: Node[], edges: Edge[]) => void; onDslChange?: (dsl: string) => void; } function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } { const nodes: Node[] = []; const edges: Edge[] = []; let yOffset = 50; try { const parsedDsl = JSON.parse(dsl); const states = parsedDsl.states || []; states.forEach((state: { id?: string, name: string, type?: string, role?: string, initial?: boolean, terminal?: boolean, on?: Record }) => { const isCondition = state.type === 'CONDITION'; const isStart = state.initial === true || state.type === 'START'; const isEnd = state.terminal === true || state.type === 'END'; let nodeType = 'default'; let style = { ...nodeStyle }; if (isStart) { nodeType = 'input'; style = { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' }; } else if (isEnd) { nodeType = 'output'; style = { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' }; } else if (isCondition) { style = conditionNodeStyle; } nodes.push({ id: state.name || state.id || `node-${Date.now()}`, type: nodeType, data: { label: isStart || isEnd ? state.name : `${state.name}\n(${state.role || 'No Role'})`, name: state.name, role: state.role, type: state.type || (isStart ? 'START' : isEnd ? 'END' : 'TASK') }, position: { x: 250, y: yOffset }, style: style }); if (state.on) { const transitions = state.on; Object.keys(transitions).forEach((eventName) => { const trans = transitions[eventName]; if (trans && trans.to) { edges.push({ id: `e-${state.name || state.id || 'node'}-${trans.to}`, source: state.name || state.id || 'node', target: trans.to, label: eventName, markerEnd: { type: MarkerType.ArrowClosed } }); } }); } yOffset += 120; }); } catch (e) { console.error("Failed to parse DSL as JSON", e); } return { nodes, edges }; } function VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: propEdges, dslString, onSave, onDslChange }: VisualWorkflowBuilderProps) { const [nodes, setNodes, onNodesChange] = useNodesState(propNodes || initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(propEdges || []); const { fitView } = useReactFlow(); // Sync DSL to nodes when dslString changes useEffect(() => { if (dslString) { const { nodes: newNodes, edges: newEdges } = parseDSL(dslString); if (newNodes.length > 0) { setNodes(newNodes); setEdges(newEdges); // Fit view after update setTimeout(() => fitView(), 100); } } }, [dslString, setNodes, setEdges, fitView]); const onConnect = useCallback( (params: Connection) => setEdges((eds) => addEdge({ ...params, markerEnd: { type: MarkerType.ArrowClosed } }, eds)), [setEdges] ); const addNode = (type: string, label: string) => { const id = `${type}-${Date.now()}`; const nodeType = type === 'condition' ? 'CONDITION' : type === 'end' ? 'END' : type === 'start' ? 'START' : 'TASK'; const newNode: Node = { id, position: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 }, data: { label: label, name: label, role: 'User', type: nodeType }, style: { ...nodeStyle }, }; if (type === 'end') { newNode.style = { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' }; newNode.type = 'output'; } else if (type === 'start') { newNode.style = { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' }; newNode.type = 'input'; } else if (type === 'condition') { newNode.style = conditionNodeStyle; } setNodes((nds) => nds.concat(newNode)); }; const handleSave = () => { onSave?.(nodes, edges); }; // Generate JSON DSL const generateDSL = () => { let hasStart = false; const states = nodes.map(n => { const outgoingEdges = edges.filter(e => e.source === n.id); const onConfig: Record = {}; outgoingEdges.forEach(e => { const eventName = e.label || 'PROCEED'; onConfig[eventName as string] = { to: e.target }; }); const isStartNode = n.type === 'input'; const isEndNode = n.type === 'output'; if (isStartNode) hasStart = true; const stateObj: { name: string; type?: string; role?: string; initial?: boolean; terminal?: boolean; on?: Record } = { name: n.data.name || n.data.label.split('\n')[0], }; if (n.data.type && n.data.type !== 'START' && n.data.type !== 'END' && n.data.type !== 'TASK') { stateObj.type = n.data.type; } if (n.data.role && !isStartNode && !isEndNode) { stateObj.role = n.data.role; } if (isStartNode && !hasStart) { stateObj.initial = true; } if (isEndNode) { stateObj.terminal = true; } if (Object.keys(onConfig).length > 0) { stateObj.on = onConfig; } return stateObj; }); const dslObj = { workflow: "VISUAL_WORKFLOW", version: 1, states }; const dsl = JSON.stringify(dslObj, null, 2); console.log("Generated DSL:", dsl); onDslChange?.(dsl); alert("DSL Updated from Visual Builder!"); }; return (

Tip: Drag to connect nodes. Use backspace to delete selected nodes.

); } export function VisualWorkflowBuilder(props: VisualWorkflowBuilderProps) { return ( ) }