import React from 'react' import ReactFlow, { Controls, Background, MiniMap, addEdge, applyEdgeChanges, applyNodeChanges, useNodesState, useEdgesState, Node, Edge, Connection, NodeChange, EdgeChange, } from 'reactflow' import ConfigNode from './components/graph/ConfigNode' import FunctionNode from './components/graph/FunctionNode' import RenderingNode from './components/graph/RenderingNode' import VariableNode from './components/graph/VariableNode' import FlowContext from './lib/flowContext' import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuTrigger, } from "@/components/ui/context-menu" // Generate short unique node ids when missing const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` const initialNodes: Node[] = [ { id: 'config-1', position: { x: 50, y: 50 }, data: { yaml: "# example:\nmessage: Hello from config" }, type: 'config', }, { id: 'render-1', position: { x: 350, y: 80 }, data: {}, type: 'render', }, ].map((n) => ({ ...n, id: n.id ?? genId() })) const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }] export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) const [rfInstance, setRfInstance] = React.useState(null) const wrapperRef = React.useRef(null) const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null) const [contextTarget, setContextTarget] = React.useState(null) const nodeTypes = React.useMemo( () => ({ config: ConfigNode, render: RenderingNode, variable: VariableNode, function: FunctionNode }), [] ) const onConnect = React.useCallback( (params: Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges] ) const onInit = React.useCallback((instance: any) => { setRfInstance(instance) }, []) const onCanvasContextMenu = React.useCallback((ev: React.MouseEvent) => { ev.preventDefault() const target = ev.target as HTMLElement const nodeEl = target.closest('.react-flow__node') as HTMLElement | null const handleEl = target.closest('.react-flow__handle') as HTMLElement | null const clientX = ev.clientX const clientY = ev.clientY lastClickRef.current = { clientX, clientY } if (nodeEl && !handleEl) { const nodeId = nodeEl.dataset?.id || nodeEl.getAttribute('data-id') || (nodeEl.id && nodeEl.id.startsWith('reactflow__node-') ? nodeEl.id.replace('reactflow__node-', '') : undefined) setContextTarget({ type: 'node', nodeId, clientX, clientY }) return } setContextTarget({ type: 'canvas', clientX, clientY }) }, []) const createNode = React.useCallback( (type: string) => { if (!rfInstance) return const click = contextTarget ?? lastClickRef.current const clientX = click?.clientX ?? window.innerWidth / 2 const clientY = click?.clientY ?? window.innerHeight / 2 const rect = wrapperRef.current?.getBoundingClientRect() const point = rect ? { x: clientX - rect.left, y: clientY - rect.top } : { x: clientX, y: clientY } let position = point try { // project to flow coords when possible position = rfInstance.project ? rfInstance.project(point) : point } catch (e) { // ignore and use raw point } const id = genId() const typeMap: Record = { config: 'config', render: 'render', variable: 'variable', function: 'function', } const dataMap: Record = { config: { yaml: '# Enter YAML here\n', title: `config-${id}` }, render: {}, variable: { value: '', valueType: 'string' }, function: { body: '// args[0], args[1], ...\nreturn args[0];' }, } const newNode: Node = { id, type: typeMap[type] ?? 'config', position: { x: position.x, y: position.y }, data: dataMap[type] ?? {}, } setNodes((nds) => nds.concat(newNode)) lastClickRef.current = null setContextTarget(null) }, [rfInstance, setNodes] ) const deleteNode = React.useCallback((id: string | undefined) => { if (!id) return setNodes((nds) => nds.filter((n) => n.id !== id)) setEdges((eds) => eds.filter((e) => e.source !== id && e.target !== id)) setContextTarget(null) }, [setNodes, setEdges]) return (
{contextTarget?.type === 'node' ? ( deleteNode(contextTarget.nodeId)}>Delete ) : ( <> New Node createNode('config')}>Config createNode('render')}>Renderer createNode('variable')}>Variable createNode('function')}>Function )}
) }