"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 ( ); } // ── View toggle ─────────────────────────────────────────────────────────── function ViewButton({ label, testId, active, onClick, }: { label: string; testId: string; active: boolean; onClick: () => void; }) { return ( ); } // ── Empty state ─────────────────────────────────────────────────────────── function EmptyState() { return (

No compression run available.

Live data arrives via the WS compression channel.

); } // ── 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("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 ; } const run = runProp; return (
{/* ── Header ─────────────────────────────────────────────────────── */}
{run.mode} {run.comboId && ( {run.comboId} )} {run.requestId}
{/* View toggle: ReactFlow canvas (A2) ↔ waterfall list (A1) */}
setView("canvas")} /> setView("waterfall")} />
{fmt(run.originalTokens)} → {fmt(run.compressedTokens)} tok −{run.savingsPercent.toFixed(1)}% {isComplete && view === "canvas" && ( replay done )}
{/* ── Body: Canvas (A2) or Waterfall (A1) ──────────────────────────── */} {view === "waterfall" ? (
) : (
)} {/* ── Replay controls (canvas only — the waterfall is static) ──────── */} {view === "canvas" && (
{SPEEDS.map((s) => ( setSpeed(s)} /> ))}
{displayRun && currentFrame && ( step {displayRun?.steps.length ?? 0}/{run.steps.length} )}
)}
); } export default CompressionCockpit;