Spaces:
Runtime error
Runtime error
| "use client"; | |
| import { useMemo, useState } from "react"; | |
| import type { NodeTypes } from "@xyflow/react"; | |
| import { FlowCanvas } from "@/shared/components/flow/FlowCanvas"; | |
| import { EngineNode } from "./nodes/EngineNode"; | |
| import { IoNode } from "./nodes/IoNode"; | |
| import { compressionRunToFlow, type CompressionRunModel } from "./compressionFlowModel"; | |
| import { useCompressionReplay, type ReplaySpeed } from "./useCompressionReplay"; | |
| import { WaterfallInspector } from "./WaterfallInspector"; | |
| // ββ View modes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| type CockpitView = "canvas" | "waterfall"; | |
| // ββ Static node type map (defined outside to avoid re-creation) βββββββββββ | |
| const NODE_TYPES: NodeTypes = { | |
| engine: EngineNode as unknown as NodeTypes["engine"], | |
| io: IoNode as unknown as NodeTypes["io"], | |
| // The model also sets type "input"/"output" for IoNode β map both | |
| input: IoNode as unknown as NodeTypes["input"], | |
| output: IoNode as unknown as NodeTypes["output"], | |
| }; | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function fmt(n: number): string { | |
| return n.toLocaleString(); | |
| } | |
| // ββ Speed selector ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SPEEDS: ReplaySpeed[] = [0.3, 1, 3]; | |
| function SpeedButton({ | |
| s, | |
| active, | |
| onClick, | |
| }: { | |
| s: ReplaySpeed; | |
| active: boolean; | |
| onClick: () => void; | |
| }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className="px-2 py-0.5 rounded text-[11px] border transition-colors" | |
| style={{ | |
| borderColor: active ? "var(--color-primary)" : "var(--color-border)", | |
| color: active ? "var(--color-primary)" : "var(--color-text-muted)", | |
| background: active ? "var(--color-primary-subtle)" : "transparent", | |
| }} | |
| > | |
| {s}Γ | |
| </button> | |
| ); | |
| } | |
| // ββ View toggle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function ViewButton({ | |
| label, | |
| testId, | |
| active, | |
| onClick, | |
| }: { | |
| label: string; | |
| testId: string; | |
| active: boolean; | |
| onClick: () => void; | |
| }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| data-testid={testId} | |
| aria-pressed={active} | |
| className="px-2 py-0.5 rounded text-[11px] border transition-colors" | |
| style={{ | |
| borderColor: active ? "var(--color-primary)" : "var(--color-border)", | |
| color: active ? "var(--color-primary)" : "var(--color-text-muted)", | |
| background: active ? "var(--color-primary-subtle)" : "transparent", | |
| }} | |
| > | |
| {label} | |
| </button> | |
| ); | |
| } | |
| // ββ Empty state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function EmptyState() { | |
| return ( | |
| <div | |
| className="flex flex-col items-center justify-center h-full gap-3 text-muted" | |
| data-testid="compression-cockpit-empty" | |
| > | |
| <span className="text-3xl opacity-40">β</span> | |
| <p className="text-sm">No compression run available.</p> | |
| <p className="text-xs opacity-60">Live data arrives via the WS compression channel.</p> | |
| </div> | |
| ); | |
| } | |
| // ββ Props βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface CompressionCockpitProps { | |
| /** | |
| * The run to render. If omitted, an empty state is shown β the parent owns the | |
| * live data (via `useLiveCompression`) and passes its latest run here. | |
| */ | |
| run?: CompressionRunModel | null; | |
| } | |
| // ββ Main component ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * CompressionCockpit β A3 cockpit view (Compression Studio, Tela A). | |
| * | |
| * Renders the compression pipeline as a ReactFlow canvas (A2) with: | |
| * - A header showing run metadata (mode, comboId, total savings). | |
| * - Replay controls (play/pause/reset + speed selector). | |
| * - Graceful empty state when no run is available. | |
| * | |
| * Controlled component: it renders the `run` prop when supplied and the empty | |
| * state otherwise. The parent owns the live data β it subscribes via the | |
| * `useLiveCompression` hook and passes `lastRun` as `run` β which keeps this | |
| * component WS-free and unit-testable with a static run. | |
| */ | |
| export function CompressionCockpit({ run: runProp }: CompressionCockpitProps) { | |
| // Canvas (A2, ReactFlow) is the default; Waterfall (A1) is a plain-div list view of the same run. | |
| const [view, setView] = useState<CockpitView>("canvas"); | |
| // Replay drives frame-by-frame animation (cosmetic β sub-ms compression is sync) | |
| const { currentFrame, isPlaying, isComplete, speed, setSpeed, play, pause, reset } = | |
| useCompressionReplay(runProp ?? null); | |
| // Displayed run: replay frame while playing, else the full run | |
| const displayRun = currentFrame ?? runProp ?? null; | |
| // Build nodes/edges from the model | |
| const { nodes, edges } = useMemo(() => { | |
| if (!displayRun) return { nodes: [], edges: [] }; | |
| // compressionRunToFlow uses type "input"/"output" for IoNode β remap to "io" | |
| const raw = compressionRunToFlow(displayRun); | |
| const remappedNodes = raw.nodes.map((n) => { | |
| if (n.type === "input") { | |
| return { ...n, type: "io", data: { ...n.data, variant: "input" } }; | |
| } | |
| if (n.type === "output") { | |
| return { | |
| ...n, | |
| type: "io", | |
| data: { ...n.data, variant: "output", savingsPercent: displayRun.savingsPercent }, | |
| }; | |
| } | |
| return n; | |
| }); | |
| return { nodes: remappedNodes, edges: raw.edges }; | |
| }, [displayRun]); | |
| const fitKey = displayRun ? `${displayRun.requestId}-${nodes.length}` : "empty"; | |
| if (!runProp) { | |
| return <EmptyState />; | |
| } | |
| const run = runProp; | |
| return ( | |
| <div className="flex flex-col h-full gap-2" data-testid="compression-cockpit"> | |
| {/* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| <div className="flex items-center gap-3 px-3 py-2 rounded-lg border border-border bg-bg/60 shrink-0"> | |
| <span className="text-xs font-semibold text-muted uppercase tracking-wide">{run.mode}</span> | |
| {run.comboId && ( | |
| <span className="text-xs text-muted px-1.5 py-0.5 rounded bg-border/40 font-mono"> | |
| {run.comboId} | |
| </span> | |
| )} | |
| <span className="text-xs text-muted font-mono opacity-70 truncate">{run.requestId}</span> | |
| <div className="ml-auto flex items-center gap-2"> | |
| {/* View toggle: ReactFlow canvas (A2) β waterfall list (A1) */} | |
| <div className="flex items-center gap-1" role="group" aria-label="Cockpit view"> | |
| <ViewButton | |
| label="Canvas" | |
| testId="cockpit-view-canvas" | |
| active={view === "canvas"} | |
| onClick={() => setView("canvas")} | |
| /> | |
| <ViewButton | |
| label="Waterfall" | |
| testId="cockpit-view-waterfall" | |
| active={view === "waterfall"} | |
| onClick={() => setView("waterfall")} | |
| /> | |
| </div> | |
| <span className="text-[11px] text-muted"> | |
| {fmt(run.originalTokens)} β {fmt(run.compressedTokens)} tok | |
| </span> | |
| <span className="text-xs font-bold" style={{ color: "#22c55e" }}> | |
| β{run.savingsPercent.toFixed(1)}% | |
| </span> | |
| {isComplete && view === "canvas" && ( | |
| <span className="text-[10px] px-1.5 py-0.5 rounded border border-border text-muted"> | |
| replay done | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {/* ββ Body: Canvas (A2) or Waterfall (A1) ββββββββββββββββββββββββββββ */} | |
| {view === "waterfall" ? ( | |
| <div className="flex-1 min-h-0 overflow-auto rounded-lg border border-border bg-bg/60 p-3"> | |
| <WaterfallInspector run={run} /> | |
| </div> | |
| ) : ( | |
| <div className="flex-1 min-h-0 rounded-lg overflow-hidden border border-border"> | |
| <FlowCanvas | |
| nodes={nodes} | |
| edges={edges} | |
| nodeTypes={NODE_TYPES} | |
| fitKey={fitKey} | |
| className="h-full w-full" | |
| /> | |
| </div> | |
| )} | |
| {/* ββ Replay controls (canvas only β the waterfall is static) ββββββββ */} | |
| {view === "canvas" && ( | |
| <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-border bg-bg/60 shrink-0"> | |
| <button | |
| onClick={isPlaying ? pause : play} | |
| className="flex items-center gap-1.5 px-2.5 py-1 rounded border border-border text-xs hover:bg-border/30 transition-colors" | |
| aria-label={isPlaying ? "Pause replay" : "Play replay"} | |
| > | |
| {isPlaying ? "βΈ Pause" : "β· Replay"} | |
| </button> | |
| <button | |
| onClick={reset} | |
| className="px-2 py-1 rounded border border-border text-xs text-muted hover:bg-border/30 transition-colors" | |
| aria-label="Reset replay" | |
| > | |
| β³ | |
| </button> | |
| <div className="flex items-center gap-1 ml-2"> | |
| {SPEEDS.map((s) => ( | |
| <SpeedButton key={s} s={s} active={speed === s} onClick={() => setSpeed(s)} /> | |
| ))} | |
| </div> | |
| {displayRun && currentFrame && ( | |
| <span className="ml-auto text-[11px] text-muted"> | |
| step {displayRun?.steps.length ?? 0}/{run.steps.length} | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default CompressionCockpit; | |