| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- "use client";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import {
- Background, BackgroundVariant, Controls, Panel, ReactFlow, ReactFlowProvider,
- useReactFlow, type Node,
- } from "@xyflow/react";
- import {
- AlertTriangle, CheckCircle2, Focus, GitBranch, Layers3, LoaderCircle,
- RefreshCw, Search, Sparkles, Workflow, X,
- } from "lucide-react";
- import { FlowCard } from "@/components/FlowCard";
- import { Inspector } from "@/components/Inspector";
- import { PhaseFrameNode } from "@/components/PhaseFrameNode";
- import { fetchGraph, fetchRuns } from "@/lib/api";
- import { buildCanvas, PHASE_LABELS, type CanvasData } from "@/lib/flow";
- import type { FlowNodeRecord, Phase, RunGraph, RunSummary } from "@/lib/types";
- const nodeTypes = { flowCard: FlowCard, phaseFrame: PhaseFrameNode };
- const ALL_PHASES: Phase[] = ["phase-1", "phase-2", "phase-3", "delivery"];
- function LoadingState() {
- return <main className="centerState"><LoaderCircle className="spin" /><h1>正在装载 fake mission graph</h1><p>连接本地 FastAPI :8788</p></main>;
- }
- function ErrorState({ message, retry }: { message: string; retry: () => void }) {
- return <main className="centerState error"><AlertTriangle /><h1>可视化后端未就绪</h1><p>{message}</p><button onClick={retry}><RefreshCw size={16} />重新连接</button></main>;
- }
- function Canvas({ graph, selected, onSelect }: {
- graph: RunGraph;
- selected: FlowNodeRecord | null;
- onSelect: (node: FlowNodeRecord | null) => void;
- }) {
- const [query, setQuery] = useState("");
- const [phases, setPhases] = useState<Set<Phase>>(new Set(ALL_PHASES));
- const { fitView } = useReactFlow();
- const canvas = useMemo(() => buildCanvas(graph, phases, query), [graph, phases, query]);
- const togglePhase = (phase: Phase) => setPhases((current) => {
- const next = new Set(current);
- if (next.has(phase)) next.delete(phase); else next.add(phase);
- return next.size ? next : new Set([phase]);
- });
- const selectNode = useCallback((_: React.MouseEvent, node: Node<CanvasData>) => {
- if (node.data.kind === "card") onSelect(node.data.item);
- }, [onSelect]);
- const matched = graph.nodes.filter((item) => {
- const node = canvas.nodes.find((candidate) => candidate.id === item.id);
- return node?.data.kind === "card" && !node.data.dimmed && !node.hidden;
- }).length;
- return <>
- <header className={`topShell ${selected ? "withInspector" : ""}`}>
- <div className="brand"><span><Workflow size={22} /></span><div><strong>Mission Control</strong><small>智能创作构建系统</small></div></div>
- <div className="runTitle"><span>BUILD #{graph.run.script_build_id}</span><strong>{graph.run.title}</strong></div>
- <div className="topStats">
- <span className="fakeBadge"><Sparkles size={13} /> FAKE DATA</span>
- <span><CheckCircle2 size={14} /> {graph.run.status}</span>
- <span><GitBranch size={14} /> {graph.run.node_count} nodes</span>
- </div>
- </header>
- <aside className="phaseRail">
- <div className="railTitle"><Layers3 size={16} /><span>流程阶段</span></div>
- {ALL_PHASES.map((phase) => <button key={phase} className={phases.has(phase) ? "active" : ""} onClick={() => togglePhase(phase)}>
- <span className={`phaseDot phase-${phase}`} />
- <span>{PHASE_LABELS[phase]}</span>
- <small>{graph.nodes.filter((item) => item.phase === phase).length}</small>
- </button>)}
- <div className="railDivider" />
- <button onClick={() => void fitView({ padding: 0.06, duration: 320 })}><Focus size={15} /><span>适应全图</span></button>
- <div className="checkpoint"><small>CURRENT CHECKPOINT</small><code>{graph.run.checkpoint}</code></div>
- </aside>
- <section className={`canvasShell ${selected ? "withInspector" : ""}`}>
- <ReactFlow
- nodes={canvas.nodes.map((node) => ({ ...node, selected: node.id === selected?.id }))}
- edges={canvas.edges}
- nodeTypes={nodeTypes}
- onNodeClick={selectNode}
- onPaneClick={() => onSelect(null)}
- onInit={(instance) => void instance.fitView({ padding: 0.06, duration: 0 })}
- minZoom={0.06}
- maxZoom={1.4}
- nodesConnectable={false}
- zoomOnDoubleClick={false}
- panOnScroll
- zoomOnPinch
- proOptions={{ hideAttribution: true }}
- >
- <Background variant={BackgroundVariant.Dots} gap={24} size={1.15} color="#383b44" />
- <Controls position="bottom-left" showInteractive={false} />
- <Panel position="top-left" className="canvasToolbar">
- <label><Search size={16} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索节点、模型、Task kind…" />{query ? <button onClick={() => setQuery("")}><X size={14} /></button> : null}</label>
- <span>{query ? `${matched} / ${graph.nodes.length}` : `${graph.nodes.length} nodes · ${graph.edges.length} edges`}</span>
- </Panel>
- <Panel position="bottom-right" className="legend">
- <span><i className="legend-control" />控制流</span>
- <span><i className="legend-artifact" />Artifact</span>
- <span><i className="legend-validation" />Validation</span>
- <span><i className="legend-publication" />Publication</span>
- </Panel>
- </ReactFlow>
- </section>
- {selected ? <Inspector item={selected} graph={graph} onClose={() => onSelect(null)} /> : null}
- </>;
- }
- function MissionControlInner() {
- const [runs, setRuns] = useState<RunSummary[]>([]);
- const [graph, setGraph] = useState<RunGraph | null>(null);
- const [selected, setSelected] = useState<FlowNodeRecord | null>(null);
- const [error, setError] = useState<string | null>(null);
- const [nonce, setNonce] = useState(0);
- useEffect(() => {
- const controller = new AbortController();
- setError(null);
- Promise.all([fetchRuns(controller.signal), fetchGraph(10001, controller.signal)])
- .then(([nextRuns, nextGraph]) => { setRuns(nextRuns); setGraph(nextGraph); })
- .catch((reason: unknown) => {
- if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : "未知错误");
- });
- return () => controller.abort();
- }, [nonce]);
- if (error) return <ErrorState message={error} retry={() => setNonce((value) => value + 1)} />;
- if (!graph || !runs.length) return <LoadingState />;
- return <main className="appFrame"><Canvas graph={graph} selected={selected} onSelect={setSelected} /></main>;
- }
- export function MissionControl() {
- return <ReactFlowProvider><MissionControlInner /></ReactFlowProvider>;
- }
|