Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"use client";

import { useMemo } from "react";
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) ───────────

Expand Down Expand Up @@ -52,6 +57,36 @@ function SpeedButton({
);
}

// ── 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() {
Expand Down Expand Up @@ -93,6 +128,9 @@ export interface CompressionCockpitProps {
* 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);
Expand Down Expand Up @@ -141,33 +179,55 @@ export function CompressionCockpit({ run: runProp }: CompressionCockpitProps) {
</span>
)}
<span className="text-xs text-muted font-mono opacity-70 truncate">{run.requestId}</span>
<div className="ml-auto flex items-center gap-1.5">
<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 && (
{isComplete && view === "canvas" && (
<span className="text-[10px] px-1.5 py-0.5 rounded border border-border text-muted">
replay done
</span>
)}
</div>
</div>

{/* ── Canvas ─────────────────────────────────────────────────────── */}
<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>
{/* ── 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 ─────────────────────────────────────────────── */}
{/* ── 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}
Expand All @@ -194,6 +254,7 @@ export function CompressionCockpit({ run: runProp }: CompressionCockpitProps) {
</span>
)}
</div>
)}
</div>
);
}
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/ui/compressionCockpit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ function mount(ui: React.ReactElement): HTMLElement {
return container;
}

function click(el: Element | null): void {
act(() => {
el?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}

beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
Expand Down Expand Up @@ -146,4 +152,30 @@ describe("CompressionCockpit", () => {
const text = container.textContent ?? "";
expect(text).toContain("Replay");
});

it("offers a Canvas/Waterfall view toggle", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='cockpit-view-canvas']")).toBeTruthy();
expect(container.querySelector("[data-testid='cockpit-view-waterfall']")).toBeTruthy();
});

it("switches to the waterfall inspector and back to the canvas", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
// Default view is the ReactFlow canvas, waterfall hidden.
expect(container.querySelector(".react-flow")).toBeTruthy();
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeFalsy();

// Switch to the waterfall (A1) view — the previously-orphan component is now reachable.
click(container.querySelector("[data-testid='cockpit-view-waterfall']"));
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeTruthy();
expect(container.querySelector(".react-flow")).toBeFalsy();
expect(
container.querySelector("[data-testid='waterfall-total-savings']")?.textContent
).toContain("51.2");

// Switch back to the canvas.
click(container.querySelector("[data-testid='cockpit-view-canvas']"));
expect(container.querySelector(".react-flow")).toBeTruthy();
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeFalsy();
});
});
Loading