Spaces:
Runtime error
Runtime error
File size: 10,336 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | "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;
|