Spaces:
Running
Running
| "use client"; | |
| import { useCallback, useMemo } from "react"; | |
| import { | |
| ReactFlow, | |
| Background, | |
| Controls, | |
| MarkerType, | |
| type Node, | |
| type Edge, | |
| type NodeMouseHandler, | |
| } from "@xyflow/react"; | |
| import "@xyflow/react/dist/style.css"; | |
| import { PhaseNode } from "./PhaseNode"; | |
| import type { PhaseState } from "@/lib/api"; | |
| import { scrollToPhaseCard, usePhaseState } from "@/hooks/usePhaseState"; | |
| // 11 nodes laid out in a 4-row grid (≤ 4 per row) — fits a 720×600 canvas at | |
| // fitView padding=0.25 so every node stays legible at default zoom. | |
| // | |
| // `phaseIndex` is the 0-based 7-phase index this graph-node maps to. | |
| const NODE_DEFS: Array<{ | |
| id: string; | |
| label: string; | |
| phase: string; | |
| phaseIndex: number; | |
| col: number; | |
| row: number; | |
| }> = [ | |
| { id: "ingest", label: "Event Ingest", phase: "Event Ingestion", phaseIndex: 0, col: 0, row: 0 }, | |
| { id: "preproc", label: "Preprocess + NER", phase: "Event Ingestion", phaseIndex: 0, col: 1, row: 0 }, | |
| { id: "auction", label: "USDC Auction", phase: "USDC Auction", phaseIndex: 1, col: 2, row: 0 }, | |
| { id: "translate", label: "Translation L1–L5", phase: "Translation Pipeline", phaseIndex: 2, col: 3, row: 0 }, | |
| { id: "debate", label: "Analyst Debate", phase: "Translation Pipeline", phaseIndex: 2, col: 3, row: 1 }, | |
| { id: "synth", label: "Synthesizer", phase: "Translation Pipeline", phaseIndex: 2, col: 2, row: 1 }, | |
| { id: "judges", label: "11-Judge Panel", phase: "11-Judge Panel", phaseIndex: 3, col: 1, row: 1 }, | |
| { id: "anchor", label: "Arc Anchor", phase: "On-chain Anchor", phaseIndex: 4, col: 0, row: 1 }, | |
| { id: "pmsubmit", label: "Polymarket Submit", phase: "Polymarket V2 Submission", phaseIndex: 5, col: 0, row: 2 }, | |
| { id: "stream", label: "Revenue Stream", phase: "Streaming Revenue", phaseIndex: 6, col: 1, row: 2 }, | |
| { id: "rep", label: "Reputation Update", phase: "Streaming Revenue", phaseIndex: 6, col: 2, row: 2 }, | |
| ]; | |
| const COL_GAP = 240; | |
| const ROW_GAP = 150; | |
| const NODE_TYPES = { phase: PhaseNode }; | |
| export function WorkflowOverview({ phases }: { phases?: PhaseState[] }) { | |
| const { activePhase, setActivePhase } = usePhaseState(); | |
| const statusByPhase = useMemo(() => { | |
| const map: Record<string, PhaseState["status"]> = {}; | |
| phases?.forEach((p) => { | |
| map[p.name] = p.status; | |
| }); | |
| return map; | |
| }, [phases]); | |
| const nodes: Node[] = useMemo( | |
| () => | |
| NODE_DEFS.map((def, idx) => ({ | |
| id: def.id, | |
| type: "phase", | |
| position: { x: def.col * COL_GAP, y: def.row * ROW_GAP }, | |
| data: { | |
| label: def.label, | |
| index: idx + 1, | |
| status: statusByPhase[def.phase] ?? "pending", | |
| isActive: activePhase === def.phaseIndex, | |
| phaseIndex: def.phaseIndex, | |
| }, | |
| })), | |
| [statusByPhase, activePhase], | |
| ); | |
| const edges: Edge[] = useMemo( | |
| () => | |
| NODE_DEFS.slice(0, -1).map((def, idx) => { | |
| const next = NODE_DEFS[idx + 1]; | |
| const status = statusByPhase[next.phase]; | |
| const isRunning = status === "running"; | |
| // Pick handles based on grid geometry so the snake pattern routes | |
| // without crossings: same-row → horizontal; same-col → vertical; | |
| // otherwise default to right→left. | |
| let sourceHandle = "r-source"; | |
| let targetHandle = "l-target"; | |
| if (def.row === next.row) { | |
| if (def.col < next.col) { | |
| sourceHandle = "r-source"; targetHandle = "l-target"; | |
| } else if (def.col > next.col) { | |
| sourceHandle = "l-source"; targetHandle = "r-target"; | |
| } | |
| } else if (def.col === next.col) { | |
| if (def.row < next.row) { | |
| sourceHandle = "b-source"; targetHandle = "t-target"; | |
| } else { | |
| sourceHandle = "t-source"; targetHandle = "b-target"; | |
| } | |
| } | |
| return { | |
| id: `${def.id}->${next.id}`, | |
| source: def.id, | |
| target: next.id, | |
| sourceHandle, | |
| targetHandle, | |
| animated: isRunning, | |
| style: { | |
| stroke: | |
| status === "completed" | |
| ? "rgba(52,211,153,0.7)" | |
| : isRunning | |
| ? "hsl(var(--primary))" | |
| : "hsl(var(--border))", | |
| strokeWidth: isRunning ? 3 : 2.4, | |
| }, | |
| markerEnd: { | |
| type: MarkerType.ArrowClosed, | |
| width: 14, | |
| height: 14, | |
| color: | |
| status === "completed" | |
| ? "rgba(52,211,153,0.8)" | |
| : isRunning | |
| ? "hsl(var(--primary))" | |
| : "hsl(var(--border))", | |
| }, | |
| }; | |
| }), | |
| [statusByPhase], | |
| ); | |
| const onNodeClick: NodeMouseHandler = useCallback( | |
| (_evt, node) => { | |
| const def = NODE_DEFS.find((d) => d.id === node.id); | |
| if (!def) return; | |
| setActivePhase(def.phaseIndex); | |
| scrollToPhaseCard(def.phaseIndex); | |
| }, | |
| [setActivePhase], | |
| ); | |
| return ( | |
| <div className="relative h-[420px] w-full rounded-xl border border-border/60 bg-card/40 grid-bg sm:h-[520px] md:h-[600px]"> | |
| <ReactFlow | |
| nodes={nodes} | |
| edges={edges} | |
| nodeTypes={NODE_TYPES} | |
| onNodeClick={onNodeClick} | |
| fitView | |
| fitViewOptions={{ padding: 0.25, minZoom: 0.7, maxZoom: 1.5 }} | |
| minZoom={0.4} | |
| maxZoom={1.6} | |
| panOnDrag | |
| zoomOnScroll={false} | |
| zoomOnPinch={false} | |
| zoomOnDoubleClick={false} | |
| panOnScroll={false} | |
| preventScrolling={false} | |
| edgesFocusable={false} | |
| proOptions={{ hideAttribution: true }} | |
| > | |
| <Background gap={24} color="hsl(var(--border))" /> | |
| <Controls | |
| showInteractive={false} | |
| className="!rounded-md !border !border-border/60 !bg-card/80" | |
| /> | |
| </ReactFlow> | |
| <div className="absolute top-2 right-2 z-10 text-[9px] uppercase tracking-wider text-muted-foreground/60 pointer-events-none"> | |
| drag to pan · use +/− buttons to zoom | |
| </div> | |
| <div className="absolute top-2 left-2 z-10 text-[9px] uppercase tracking-wider text-muted-foreground/60 pointer-events-none"> | |
| 11 graph nodes across 7 lifecycle phases | |
| </div> | |
| </div> | |
| ); | |
| } | |