import { useCallback, useEffect, useRef, useState } from 'react'; import { Background, Controls, Panel, ReactFlow, addEdge, useEdgesState, useNodesState } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { MarkerType } from '@xyflow/react'; import WorkflowEdge from './WorkflowEdge'; import WorkflowMinimap from './WorkflowMinimap'; import WorkflowNode from './WorkflowNode'; import WorkflowToolbar from './WorkflowToolbar'; const nodeTypes = { workflowNode: WorkflowNode }; const edgeTypes = { workflowEdge: WorkflowEdge, default: WorkflowEdge }; export const NODE_TEMPLATES = [ { type: 'start', label: 'Start / Trigger', iconName: 'Play', color: '#10B981', category: 'Flow' }, { type: 'process', label: 'Process', iconName: 'Cpu', color: '#3B82F6', category: 'Flow' }, { type: 'decision', label: 'Decision', iconName: 'GitBranch', color: '#F59E0B', category: 'Flow' }, { type: 'database', label: 'Database', iconName: 'Database', color: '#10B981', category: 'Infrastructure' }, { type: 'api', label: 'API Call', iconName: 'Globe', color: '#06B6D4', category: 'Integration' }, { type: 'auth', label: 'Auth Check', iconName: 'Lock', color: '#EF4444', category: 'Security' }, { type: 'notification', label: 'Notification', iconName: 'Bell', color: '#8B5CF6', category: 'Output' }, { type: 'code', label: 'Code Module', iconName: 'Code', color: '#84CC16', category: 'Code' }, { type: 'service', label: 'Microservice', iconName: 'Box', color: '#EC4899', category: 'Infrastructure' }, { type: 'queue', label: 'Message Queue', iconName: 'Layers', color: '#F97316', category: 'Infrastructure' }, { type: 'cloud', label: 'Cloud Service', iconName: 'Cloud', color: '#14B8A6', category: 'Infrastructure' }, ]; let nodeIdCounter = 1; export default function WorkflowDesigner({ initialNodes = [], initialEdges = [] }) { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [isDraggingOver, setIsDraggingOver] = useState(false); const [reactFlowInstance, setReactFlowInstance] = useState(null); const wrapperRef = useRef(null); useEffect(() => { setNodes(initialNodes); setEdges(initialEdges); }, [initialEdges, initialNodes, setEdges, setNodes]); const onConnect = useCallback( (params) => setEdges((current) => addEdge( { ...params, type: 'workflowEdge', markerEnd: { type: MarkerType.ArrowClosed, color: '#38BDF8' }, animated: true, }, current, ), ), [setEdges], ); const onDragStart = (event, template) => { event.dataTransfer.setData('application/workflow-node', JSON.stringify(template)); event.dataTransfer.effectAllowed = 'move'; }; const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; setIsDraggingOver(true); }, []); const onDrop = useCallback( (event) => { event.preventDefault(); setIsDraggingOver(false); const raw = event.dataTransfer.getData('application/workflow-node'); if (!raw || !reactFlowInstance) return; const template = JSON.parse(raw); const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); setNodes((current) => current.concat({ id: `node_${nodeIdCounter++}`, type: 'workflowNode', position, data: { ...template, nodeType: template.type, description: '' }, }), ); }, [reactFlowInstance, setNodes], ); const addNode = (template = NODE_TEMPLATES[0]) => { setNodes((current) => current.concat({ id: `node_${nodeIdCounter++}`, type: 'workflowNode', position: { x: 120 + current.length * 35, y: 120 + current.length * 24 }, data: { ...template, nodeType: template.type, description: '' }, }), ); }; const deleteSelected = () => { setNodes((current) => current.filter((node) => !node.selected)); setEdges((current) => current.filter((edge) => !edge.selected)); }; const exportWorkflow = () => { const blob = new Blob([JSON.stringify({ nodes, edges, timestamp: new Date().toISOString() }, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = 'codeatlas-workflow.json'; anchor.click(); URL.revokeObjectURL(url); }; return (
setIsDraggingOver(false)} nodeTypes={nodeTypes} edgeTypes={edgeTypes} fitView deleteKeyCode="Delete" style={{ background: isDraggingOver ? 'rgba(56,189,248,0.05)' : '#080b14' }} >
); }