import { useState } from "react"; import { useAppStore } from "../../store/useAppStore"; import type { AgentStatus, FlowGraph } from "../../lib/types"; import { CardGrid, Notice, PageHeader, StatCard } from "../components"; import { Surface } from "../../components/primitives"; import { Icon } from "../../components/Icon"; import { FlowGraphView } from "./FlowGraph"; import { NodeDetail } from "./flow/NodeDetail"; import { LaneSummary } from "./flow/LaneSummary"; import { groupFlowLanes } from "./flow/lanes"; import { FLOW_TONE, STATUS_ORDER, statusOf } from "./flow/tone"; /** 全链路流程页(design/19 §2):实时渲染 ws.graph_update 驱动的单轮图 —— * 主干「输入 → 思考 → 工具 → 输出」+ 并行子代理泳道(子图容器), * 节点点开看参数 / 结果 / 实时日志。 */ export function FlowModule(): React.ReactNode { const graph = useAppStore((s) => s.flowGraph); const [selectedId, setSelectedId] = useState(null); const selected = graph?.nodes.find((n) => n.id === selectedId) ?? null; const grouping = graph ? groupFlowLanes(graph) : null; const selectedLane = selected ? laneLabelFor(selectedId, grouping) : undefined; return (
{graph ? ( <> } label="会话" value={{graph.sessionId ?? "web"}} sub={graph.id} tone="accent" /> } label="节点" value={graph.nodes.length} sub={`${graph.edges.length} 条连线`} tone="neutral" /> } label="泳道" value={grouping?.lanes.length ?? 0} sub="并行子代理分支" tone="accent" /> } label="耗时" value={fmtDuration(graph)} sub={`开始 ${fmtStart(graph)}`} tone="ok" /> setSelectedId(n.id)} /> {selected ? ( setSelectedId(null)} /> ) : null} ) : ( 暂无流程 —— 接入 /ws/chat 并发起一次会话后,将实时出现「输入 → 思考 → 工具 → 输出」连通图;并行子代理会各自成一条泳道。 )}
); } /** StatusStrip shows the live status census of the round. */ function StatusStrip({ graph }: { graph: FlowGraph }): React.ReactNode { return (
{STATUS_ORDER.map((s) => ( {FLOW_TONE[s].label} × {countStatus(graph, s)} ))}
); } /** laneLabelFor returns the label of the lane a node belongs to (its sub-agent * root), or of the lane the node itself opens. */ function laneLabelFor( nodeId: string | null, grouping: ReturnType | null, ): string | undefined { if (!nodeId || !grouping) return undefined; const band = grouping.bandOf.get(nodeId); const own = grouping.lanes.find((l) => l.id === nodeId); const laneId = band ?? own?.id; if (!laneId) return undefined; return grouping.lanes.find((l) => l.id === laneId)?.root.label; } /** countStatus returns how many nodes in graph are in the given status. */ function countStatus(graph: FlowGraph, status: AgentStatus): number { let n = 0; for (const node of graph.nodes) { if (statusOf(node) === status) n += 1; } return n; } /** fmtStart renders the round start time as a locale time, or "—" when absent. */ function fmtStart(graph: FlowGraph): string { if (!graph.startedAt) return "—"; const t = Date.parse(graph.startedAt); return Number.isNaN(t) ? "—" : new Date(t).toLocaleTimeString(); } /** fmtDuration renders the round duration from startedAt to endedAt (or now). */ function fmtDuration(graph: FlowGraph): string { if (!graph.startedAt) return "—"; const start = Date.parse(graph.startedAt); if (Number.isNaN(start)) return "—"; const end = graph.endedAt ? Date.parse(graph.endedAt) : Date.now(); const ms = Math.max(0, (Number.isNaN(end) ? Date.now() : end) - start); if (ms >= 60000) return `${(ms / 60000).toFixed(1)} min`; if (ms >= 1000) return `${(ms / 1000).toFixed(1)} s`; return `${ms} ms`; }