"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 (