'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'; interface WorkflowStateNodeData { label?: string; name?: string; role?: string; type?: string; } interface RawTransitionShape { to?: string; target?: string; require?: { role?: string | string[]; }; } interface RawStateShape { id?: string; name: string; type?: string; role?: string; initial?: boolean; terminal?: boolean; on?: Record; } interface CompiledTransitionShape { to?: string; target?: string; requirements?: { roles?: string[]; }; } interface CompiledStateShape { initial?: boolean; terminal?: boolean; transitions?: Record; } interface ParsedDslShape { workflow?: string; initialState?: string; states?: RawStateShape[] | Record; dslDefinition?: string; } // 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; onSave?: (nodes: Node[], edges: Edge[]) => void; onDslChange?: (dsl: string) => void; } const createNode = ( name: string, yOffset: number, options?: { isCondition?: boolean; isStart?: boolean; isEnd?: boolean; role?: string; type?: string; } ): Node => { const isCondition = options?.isCondition === true; const isStart = options?.isStart === true; const isEnd = options?.isEnd === true; let nodeType: Node['type'] = '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; } return { id: name, type: nodeType, data: { label: isStart || isEnd ? name : `${name}\n(${options?.role || 'No Role'})`, name, role: options?.role, type: options?.type || (isStart ? 'START' : isEnd ? 'END' : 'TASK') }, position: { x: 250, y: yOffset }, style }; }; const createEdge = (source: string, target: string, label: string): Edge => ({ id: `e-${source}-${label}-${target}`, source, target, label, markerEnd: { type: MarkerType.ArrowClosed } }); function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } { const nodes: Node[] = []; const edges: Edge[] = []; let yOffset = 50; try { const parsedDsl = JSON.parse(dsl) as ParsedDslShape; if (typeof parsedDsl.dslDefinition === 'string') { return parseDSL(parsedDsl.dslDefinition); } if (Array.isArray(parsedDsl.states)) { parsedDsl.states.forEach((state) => { const stateName = state.name || state.id || `node-${Date.now()}`; const role = state.role || (Array.isArray(state.on?.SUBMIT?.require?.role) ? state.on?.SUBMIT?.require?.role.join(', ') : state.on?.SUBMIT?.require?.role); const isCondition = state.type === 'CONDITION'; const isStart = state.initial === true || state.type === 'START'; const isEnd = state.terminal === true || state.type === 'END'; nodes.push( createNode(stateName, yOffset, { isCondition, isStart, isEnd, role, type: state.type }) ); if (state.on) { Object.entries(state.on).forEach(([eventName, transition]) => { const target = transition?.to || transition?.target; if (target) { edges.push(createEdge(stateName, target, eventName)); } }); } yOffset += 120; }); return { nodes, edges }; } if (parsedDsl.states && typeof parsedDsl.states === 'object') { Object.entries(parsedDsl.states).forEach(([stateName, state]) => { const roles = state.transitions ? Object.values(state.transitions) .flatMap((transition) => transition.requirements?.roles || []) .filter((role, index, array) => array.indexOf(role) === index) : []; const isStart = parsedDsl.initialState === stateName || state.initial === true; const isEnd = state.terminal === true; nodes.push( createNode(stateName, yOffset, { isStart, isEnd, role: roles.join(', ') }) ); if (state.transitions) { Object.entries(state.transitions).forEach(([eventName, transition]) => { const target = transition?.to || transition?.target; if (target) { edges.push(createEdge(stateName, target, eventName)); } }); } yOffset += 120; }); } } catch (e) { // Failed to parse DSL as JSON - nodes/edges remain empty } 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); setNodes(newNodes.length > 0 ? newNodes : propNodes || initialNodes); setEdges(newNodes.length > 0 ? newEdges : propEdges || []); setTimeout(() => fitView(), 100); } }, [dslString, fitView, propEdges, propNodes, setEdges, setNodes]); 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 = () => { 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'; const nodeData = n.data as WorkflowStateNodeData; const stateObj: { name: string; type?: string; role?: string; initial?: boolean; terminal?: boolean; on?: Record } = { name: nodeData.name || nodeData.label?.split('\n')[0] || n.id, }; if (nodeData.type && nodeData.type !== 'START' && nodeData.type !== 'END' && nodeData.type !== 'TASK') { stateObj.type = nodeData.type; } if (nodeData.role && !isStartNode && !isEndNode) { stateObj.role = nodeData.role; } if (isStartNode) { 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); // DSL generated from visual builder 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 ( ) }