import { MarkerType, Position, type Edge, type Node } from "@xyflow/react"; import type { FlowNodeRecord, Phase, PhaseFrame, RunGraph } from "@/lib/types"; export type CardData = { kind: "card"; item: FlowNodeRecord; dimmed: boolean; } & Record; export type FrameData = { kind: "frame"; frame: PhaseFrame; } & Record; export type CanvasData = CardData | FrameData; export const PHASE_LABELS: Record = { "phase-1": "Phase 1", "phase-2": "Phase 2", "phase-3": "Phase 3", delivery: "Final Delivery", }; const EDGE_COLOR = { control: "#68707f", artifact: "#4ca6d8", validation: "#b68cff", publication: "#5ec48d", }; export function matchesNode(item: FlowNodeRecord, query: string): boolean { const value = query.trim().toLocaleLowerCase(); if (!value) return true; return [ item.title, item.subtitle, item.id, item.lane, item.status, item.task_kind ?? "", item.agent_role ?? "", item.record.model_name, ...item.tags, ].some((part) => part.toLocaleLowerCase().includes(value)); } export function buildCanvas( graph: RunGraph, enabledPhases: Set, query: string, ): { nodes: Node[]; edges: Edge[] } { const visibleIds = new Set( graph.nodes.filter((item) => enabledPhases.has(item.phase)).map((item) => item.id), ); const frameNodes: Node[] = graph.frames.map((frame) => { const phase = frame.id.replace("frame-", "") as Phase; return { id: frame.id, type: "phaseFrame", position: { x: frame.x, y: frame.y }, data: { kind: "frame", frame }, style: { width: frame.width, height: frame.height }, selectable: false, draggable: false, connectable: false, hidden: !enabledPhases.has(phase), zIndex: -10, }; }); const cardNodes: Node[] = graph.nodes.map((item) => ({ id: item.id, type: "flowCard", position: { x: item.x, y: item.y }, data: { kind: "card", item, dimmed: !matchesNode(item, query) }, sourcePosition: Position.Right, targetPosition: Position.Left, style: { width: item.width }, hidden: !visibleIds.has(item.id), zIndex: 2, })); const edges: Edge[] = graph.edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, label: edge.label ?? undefined, type: "smoothstep", animated: edge.animated, hidden: !visibleIds.has(edge.source) || !visibleIds.has(edge.target), markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16, color: EDGE_COLOR[edge.kind] }, style: { stroke: EDGE_COLOR[edge.kind], strokeWidth: edge.kind === "publication" ? 2.2 : 1.45 }, labelStyle: { fill: "#9aa0ab", fontSize: 10, fontWeight: 600 }, labelBgStyle: { fill: "#15171b", fillOpacity: 0.92 }, labelBgPadding: [5, 3] as [number, number], labelBgBorderRadius: 5, })); return { nodes: [...frameNodes, ...cardNodes], edges }; }