Skip to content

Commit 57b3d19

Browse files
committed
feat(e7-3-ui): Roundtrip OK/FAIL badge per row in DataInspector
Follow-up to E7-3 (adefb18) — wires the data.roundtrip_check RPC into the UI so users see per-row OK/FAIL badges (green/red) with tooltip showing byte_diff + decoded preview. vbgui/src/components/DataInspector.tsx: - New tokenizer path input + "Check roundtrip" button (disabled until both parquet path and tokenizer path are set). - roundtrip state map keyed by row_idx; on click fires data.roundtrip_check RPC and stores results. - Inline badge next to "row #N": green "Roundtrip OK" or red "Roundtrip FAIL" with title=byte_diff + decoded_preview. Tests (+3 vitest): - tokenizer input + button render; button disabled when paths empty - button enables when both paths are filled - after Load + Check: data-roundtrip-{i} badges appear with OK/FAIL text per row matching the RPC payload Regression: vbgui vitest 153 / 0 failed. Closes cppmega-mlx-bb0.15.
1 parent f11eebd commit 57b3d19

2 files changed

Lines changed: 132 additions & 1 deletion

File tree

vbgui/src/components/DataInspector.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ function isArrayRibbon(v: unknown): v is unknown[] {
3939
return Array.isArray(v);
4040
}
4141

42+
interface RoundtripRowResult {
43+
row_idx: number;
44+
matches: boolean;
45+
byte_diff: number;
46+
decoded_preview: string;
47+
original_bytes: number;
48+
decoded_bytes: number;
49+
}
50+
interface RoundtripCheckResult {
51+
rows: RoundtripRowResult[];
52+
pass_rate: number;
53+
tokenizer_capability: string;
54+
has_original_text: boolean;
55+
}
56+
4257
export function DataInspector({
4358
rpc, initialPath = "", pageSize = 16,
4459
}: DataInspectorProps): JSX.Element {
@@ -47,6 +62,9 @@ export function DataInspector({
4762
const [result, setResult] = useState<PreviewParquetResult | null>(null);
4863
const [error, setError] = useState<string | null>(null);
4964
const [enabled, setEnabled] = useState<Set<string>>(new Set());
65+
const [tokenizerSource, setTokenizerSource] = useState("");
66+
const [roundtrip, setRoundtrip] =
67+
useState<Map<number, RoundtripRowResult>>(new Map());
5068

5169
const load = useCallback(async (nextOffset: number) => {
5270
if (!path) return;
@@ -101,6 +119,29 @@ export function DataInspector({
101119
Load
102120
</button>
103121
</header>
122+
<header style={{ display: "flex", gap: 8, alignItems: "center" }}>
123+
<span style={{ fontSize: 11, color: "#6b7280" }}>Tokenizer:</span>
124+
<input data-testid="data-tokenizer-path"
125+
type="text" placeholder="/path/to/tokenizer.json (optional)"
126+
value={tokenizerSource}
127+
onChange={(e) => setTokenizerSource(e.target.value)}
128+
style={{ flex: 1, fontFamily: "monospace", fontSize: 11 }} />
129+
<button data-testid="data-roundtrip"
130+
disabled={!path || !tokenizerSource}
131+
onClick={async () => {
132+
try {
133+
const r = await rpc.call<RoundtripCheckResult>(
134+
"data.roundtrip_check",
135+
{ parquet_path: path, tokenizer_source: tokenizerSource,
136+
max_rows: pageSize });
137+
const m = new Map<number, RoundtripRowResult>();
138+
for (const row of r.rows) m.set(row.row_idx, row);
139+
setRoundtrip(m);
140+
} catch (e) { setError(String(e)); }
141+
}}>
142+
Check roundtrip
143+
</button>
144+
</header>
104145

105146
{error && (
106147
<div data-testid="data-error"
@@ -147,7 +188,23 @@ export function DataInspector({
147188
data-testid={`data-row-${row.row_index}`}
148189
style={{ padding: 6, borderBottom: "1px solid #f3f4f6",
149190
fontFamily: "monospace", fontSize: 11 }}>
150-
<div style={{ color: "#6b7280" }}>row #{row.row_index}</div>
191+
<div style={{ color: "#6b7280",
192+
display: "flex", gap: 6, alignItems: "center" }}>
193+
<span>row #{row.row_index}</span>
194+
{roundtrip.has(row.row_index) && (() => {
195+
const rt = roundtrip.get(row.row_index)!;
196+
return (
197+
<span data-testid={`data-roundtrip-${row.row_index}`}
198+
title={`byte_diff=${rt.byte_diff} · decoded="${rt.decoded_preview}"`}
199+
style={{ padding: "1px 6px", borderRadius: 3,
200+
fontSize: 10, fontWeight: 600,
201+
background: rt.matches ? "#dcfce7" : "#fee2e2",
202+
color: rt.matches ? "#166534" : "#991b1b" }}>
203+
Roundtrip {rt.matches ? "OK" : "FAIL"}
204+
</span>
205+
);
206+
})()}
207+
</div>
151208
<div style={{ display: "flex", flexWrap: "wrap", gap: 2,
152209
padding: "2px 0" }}>
153210
{row.tokens.map((tok, i) => (
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3+
import { DataInspector } from "@/components/DataInspector";
4+
import type { RpcClient } from "@/lib/rpc";
5+
6+
const PREVIEW = {
7+
rows: [
8+
{ row_index: 0, tokens: ["a", "b"], channels: {} },
9+
{ row_index: 1, tokens: ["c", "d"], channels: {} },
10+
],
11+
token_column: "input_ids",
12+
available_channels: [],
13+
bytes_per_token_avg: 2.5,
14+
bytes_per_token_p95: 4,
15+
bytes_per_token_max: 8,
16+
total_rows: 2,
17+
};
18+
19+
const ROUNDTRIP = {
20+
rows: [
21+
{ row_idx: 0, matches: true, byte_diff: 0,
22+
decoded_preview: "ab", original_bytes: 2, decoded_bytes: 2 },
23+
{ row_idx: 1, matches: false, byte_diff: 3,
24+
decoded_preview: "xy", original_bytes: 4, decoded_bytes: 2 },
25+
],
26+
pass_rate: 0.5,
27+
tokenizer_capability: "exact",
28+
has_original_text: true,
29+
};
30+
31+
function fakeRpc(): RpcClient {
32+
return {
33+
call: vi.fn(async (method: string) => {
34+
if (method === "data.preview_parquet") return PREVIEW;
35+
if (method === "data.roundtrip_check") return ROUNDTRIP;
36+
throw new Error("unexpected " + method);
37+
}),
38+
} as unknown as RpcClient;
39+
}
40+
41+
describe("DataInspector roundtrip badge (E7-3-UI)", () => {
42+
it("tokenizer input + Check roundtrip button render", () => {
43+
render(<DataInspector rpc={fakeRpc()} />);
44+
expect(screen.getByTestId("data-tokenizer-path")).toBeTruthy();
45+
const btn = screen.getByTestId("data-roundtrip") as HTMLButtonElement;
46+
expect(btn.disabled).toBe(true); // disabled until both paths filled
47+
});
48+
49+
it("Check roundtrip button enables when both paths filled", async () => {
50+
render(<DataInspector rpc={fakeRpc()} />);
51+
fireEvent.change(screen.getByTestId("data-path"),
52+
{ target: { value: "/p.parquet" } });
53+
fireEvent.change(screen.getByTestId("data-tokenizer-path"),
54+
{ target: { value: "/tok.json" } });
55+
const btn = screen.getByTestId("data-roundtrip") as HTMLButtonElement;
56+
expect(btn.disabled).toBe(false);
57+
});
58+
59+
it("Check roundtrip → per-row OK/FAIL badges appear", async () => {
60+
render(<DataInspector rpc={fakeRpc()} />);
61+
fireEvent.change(screen.getByTestId("data-path"),
62+
{ target: { value: "/p.parquet" } });
63+
fireEvent.click(screen.getByTestId("data-load"));
64+
await waitFor(() => screen.getByTestId("data-row-0"));
65+
fireEvent.change(screen.getByTestId("data-tokenizer-path"),
66+
{ target: { value: "/tok.json" } });
67+
fireEvent.click(screen.getByTestId("data-roundtrip"));
68+
await waitFor(() => screen.getByTestId("data-roundtrip-0"));
69+
expect(screen.getByTestId("data-roundtrip-0").textContent)
70+
.toContain("OK");
71+
expect(screen.getByTestId("data-roundtrip-1").textContent)
72+
.toContain("FAIL");
73+
});
74+
});

0 commit comments

Comments
 (0)