"use client"; /* Self-rendered canvas connections. React Flow v12's own edge renderer doesn't draw loaded edges reliably in this dev setup (handle-bounds race), so we draw the wires ourselves inside the ViewportPortal - which lives in flow coordinate space, so it pans/zooms with the canvas. Native edges are hidden via CSS to avoid double-draw. New connections (onConnect) land in the same `edges` array, so hand-drawn wires show here too. */ import { ViewportPortal } from "@xyflow/react"; import { useMemo } from "react"; import { Icon } from "../icons"; import { type FlowEdge, type FlowNode } from "@/lib/graph"; const NODE_HEIGHT: Record = { start: 36, end: 36, agent: 90, deep_agent: 90, llm: 80, human_input: 80, classifier: 80, transform: 64, tool_call: 64, webhook_out: 64, emit_event: 64, retrieval: 64, }; const NODE_WIDTH = 230; // Router case-row geometry - must mirror ForgeNode's router body (header + expr line + rows). const ROUTER_GEOM = { header: 37, exprLine: 22, rowH: 24, pad: 6 }; function routerHeight(cfg: any): number { const rows = Object.keys(cfg?.cases || {}).length + 1; // + Else return ROUTER_GEOM.header + ROUTER_GEOM.exprLine + rows * ROUTER_GEOM.rowH + ROUTER_GEOM.pad; } /** Y offset (within the router node) of the case row an edge leaves from. */ function routerCaseY(cfg: any, edge: FlowEdge): number { const keys = [...Object.keys(cfg?.cases || {}), "__default__"]; let key: string | undefined = edge.sourceHandle?.startsWith("case:") ? edge.sourceHandle.slice(5) : undefined; if (!key || !keys.includes(key)) { // Infer from routing config: which case (or default) points at this edge's target? key = Object.entries(cfg?.cases || {}).find(([, tgt]) => tgt === edge.target)?.[0] ?? (cfg?.default === edge.target ? "__default__" : undefined); } const idx = key ? keys.indexOf(key) : 0; return ROUTER_GEOM.header + ROUTER_GEOM.exprLine + (idx < 0 ? 0 : idx) * ROUTER_GEOM.rowH + ROUTER_GEOM.rowH / 2; } export function EdgeOverlay({ nodes, edges, onRemove }: { nodes: FlowNode[]; edges: FlowEdge[]; onRemove: (edge: FlowEdge) => void }) { const byId = useMemo(() => Object.fromEntries(nodes.map((n) => [n.id, n])), [nodes]); const h = (n: any) => (n?.data?.nodeType === "router" ? routerHeight(n?.data?.config) : NODE_HEIGHT[n?.data?.nodeType] ?? 36); const edgeGeometry = (e: FlowEdge) => { const s = byId[e.source]; const t = byId[e.target]; if (!s || !t) return null; const tx = t.position.x, ty = t.position.y + h(t) / 2; // Sub-agent edge: an org-chart branch from the deep_agent's BOTTOM handle down into the // specialist's TOP-center (agent-as-tool), drawn dashed/accent and distinct from flow edges. if (e.sourceHandle === "subagents") { const sx = s.position.x + NODE_WIDTH / 2; const sy = s.position.y + h(s); const ttx = t.position.x + NODE_WIDTH / 2; const tty = t.position.y; const dy = Math.max(28, Math.abs(tty - sy) / 2); const d = `M ${sx},${sy} C ${sx},${sy + dy} ${ttx},${tty - dy} ${ttx},${tty}`; return { d, mx: (sx + ttx) / 2, my: (sy + tty) / 2, sub: true }; } const fromRouter = s.data?.nodeType === "router"; const sx = s.position.x + NODE_WIDTH; const sy = s.position.y + (fromRouter ? routerCaseY(s.data?.config, e) : h(s) / 2); const dx = Math.max(40, Math.abs(tx - sx) / 2); const d = `M ${sx},${sy} C ${sx + dx},${sy} ${tx - dx},${ty} ${tx},${ty}`; return { d, mx: (sx + tx) / 2, my: (sy + ty) / 2, sub: false }; }; return ( {edges.map((e) => { const geo = edgeGeometry(e); if (!geo) return null; return ; })} {edges.map((e) => { const geo = edgeGeometry(e); if (!geo) return null; return ( ); })} ); }