Skip to content

Commit 3ae9e3c

Browse files
committed
feat(vbgui): general-purpose HelpIcon + explanation modal
Per architect's request: every value should expose a "?" icon that opens a clear modal explaining what the value is, why it's used, and (crucially) why apparent contradictions like nh*head_dim != H are actually valid in this codebase. vbgui/src/components/HelpIcon.tsx: - HELP_TOPICS registry keyed by topic-id, each entry has { title, what, why, example?, reference? }. - <HelpIcon topic="…" /> renders a 14px circular '?' button with data-testid='help-icon-{topic}'. Clicking opens a modal (data-testid='help-modal-{topic}') with sectioned content: help-modal-what, help-modal-why, help-modal-example, help-modal-reference. - Modal closes via backdrop click, X button, or programmatically. - Unknown topics render a help-modal-missing placeholder so the icon never crashes — graceful degradation as new fields are added before help is written. Initial topics covering DimEnvEditor + F56b: dim_env_H, dim_env_nh, dim_env_head_dim, dim_env_B, dim_env_S, symbolic_dim_mismatch. The symbolic_dim_mismatch entry directly addresses the architect's question — explains the decoupled-Q convention, cites Llama-3 vs DeepSeek-V3 as concrete examples, links to the DeepSeek-V3 paper. vbgui/src/components/DimEnvEditor.tsx: - Each input label now has an inline HelpIcon (topic=dim_env_{key}). - The inline mismatch indicator has its own HelpIcon (topic=symbolic_dim_mismatch) so the architect can click through to the why-it's-allowed explanation rather than being left guessing whether the warning is a bug or by design. vbgui/tests/HelpIcon.test.tsx — 5 unit tests: - Icon renders with deterministic testid. - Click opens modal with the topic content sections. - Unknown topic surfaces 'missing' placeholder. - symbolic_dim_mismatch why-section mentions 'decouple'. - Close button hides the modal. 20/20 vitest pass (HelpIcon + DimEnvEditor + App.integration). Foundation for follow-ups: same HelpIcon is reusable on GalleryTab column headers, BrickContextPanel fields, optim/loss params, sharding controls, etc. Adding help to a new field is a 1-liner + a HELP_TOPICS entry. Ref: cppmega-mlx-j3qa
1 parent 6273ad6 commit 3ae9e3c

3 files changed

Lines changed: 268 additions & 2 deletions

File tree

vbgui/src/components/DimEnvEditor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// calling onApply per H).
55

66
import { useState } from "react";
7+
import { HelpIcon } from "@/components/HelpIcon";
78

89
export interface DimEnvEditorProps {
910
value: Record<string, number>;
@@ -68,8 +69,10 @@ export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element
6869
fontFamily: "system-ui, sans-serif", fontSize: 12 }}>
6970
<strong data-testid="dim-env-editor-label">dim_env:</strong>
7071
{EDITABLE_KEYS.map((k: EditableKey) => (
71-
<label key={k} style={{ display: "inline-flex", gap: 4 }}>
72+
<label key={k} style={{ display: "inline-flex", alignItems: "center",
73+
gap: 4 }}>
7274
{k}
75+
<HelpIcon topic={`dim_env_${k}`} />
7376
<input
7477
data-testid={`dim-env-${k}`}
7578
type="number"
@@ -93,8 +96,10 @@ export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element
9396
</button>
9497
{mismatch && (
9598
<span data-testid="dim-env-inline-mismatch"
96-
style={{ color: "#92400e", marginLeft: 8 }}>
99+
style={{ color: "#92400e", marginLeft: 8,
100+
display: "inline-flex", alignItems: "center" }}>
97101
{mismatch}
102+
<HelpIcon topic="symbolic_dim_mismatch" />
98103
</span>
99104
)}
100105
{fixSetH && (

vbgui/src/components/HelpIcon.tsx

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// General-purpose "?" icon + explanation modal for any in-app value.
2+
// Topic content is centralised in `HELP_TOPICS` so writing the help is
3+
// not gated on touching the component being explained.
4+
5+
import { useState } from "react";
6+
7+
export interface HelpTopic {
8+
title: string;
9+
what: string;
10+
why: string;
11+
example?: string;
12+
reference?: string;
13+
}
14+
15+
export const HELP_TOPICS: Record<string, HelpTopic> = {
16+
// ----- dim_env -----
17+
dim_env_H: {
18+
title: "dim_env.H — residual stream width",
19+
what:
20+
"The hidden / model dimension that flows through residual " +
21+
"connections. Every brick that reads/writes the residual stream " +
22+
"exposes a tensor of shape [B, S, H].",
23+
why:
24+
"H controls memory + compute scaling roughly quadratically " +
25+
"(self-attention) or linearly (MLP) in the brick. Most " +
26+
"in-tree presets accept H as a constructor knob so the same " +
27+
"preset can be instantiated mini (H=128) or full (H=4096).",
28+
example:
29+
"MINI_HIDDEN=128 is the default for smoke runs; production " +
30+
"Llama-3 8B uses H=4096; DeepSeek-V3 uses H=7168.",
31+
},
32+
dim_env_nh: {
33+
title: "dim_env.nh — number of query heads",
34+
what:
35+
"How many attention heads the SDPA / GQA / MLA blocks split " +
36+
"queries into. Each head has dimension head_dim.",
37+
why:
38+
"More heads = more parallel attention patterns but smaller " +
39+
"per-head dim. The product nh*head_dim is the *query/output* " +
40+
"projection dim, which need NOT equal H — many architectures " +
41+
"use an internal W_Q : R^H → R^{nh*head_dim} projection so the " +
42+
"Q-space is decoupled from the residual stream.",
43+
example:
44+
"Llama-3 70B uses nh=64, head_dim=128, H=8192 (nh*head_dim==H). " +
45+
"DeepSeek-V3 MLA uses nh=128, head_dim=192, H=7168 (decoupled).",
46+
},
47+
dim_env_head_dim: {
48+
title: "dim_env.head_dim — per-head dimension",
49+
what:
50+
"The width of each attention head — both the per-head Q/K " +
51+
"projection size and the softmax-normalisation dim.",
52+
why:
53+
"Common values are 64, 96, 128, 256. Larger head_dim trades " +
54+
"more compute per head for fewer, richer heads. Combined " +
55+
"with nh, defines the attention sub-block compute as " +
56+
"nh * head_dim * H per token (roughly).",
57+
},
58+
dim_env_B: {
59+
title: "dim_env.B — batch size",
60+
what:
61+
"How many independent sequences are forwarded in one step.",
62+
why:
63+
"Higher B = more parallelism + better GPU utilisation, but " +
64+
"memory scales linearly with B. For the in-GUI smoke run " +
65+
"B=1 keeps things fast.",
66+
},
67+
dim_env_S: {
68+
title: "dim_env.S — sequence length",
69+
what:
70+
"Number of tokens per batch item.",
71+
why:
72+
"Attention memory scales O(B*S^2) for vanilla SDPA. The " +
73+
"GUI default S=64 keeps mini runs interactive.",
74+
},
75+
// ----- F56b convention -----
76+
symbolic_dim_mismatch: {
77+
title: "Why nh*head_dim != H is allowed (but warned)",
78+
what:
79+
"When dim_env pins H, nh, and head_dim, the codebase does " +
80+
"*not* enforce nh*head_dim == H — but it does warn.",
81+
why:
82+
"Many recent architectures (DeepSeek-V3 MLA, Gemma 3 MQA, " +
83+
"Qwen3) intentionally decouple the Q-space from the residual " +
84+
"stream by inserting an internal W_Q : R^H → R^{nh*head_dim} " +
85+
"projection. So the contradiction is a real-world pattern, " +
86+
"not a bug. The warning exists because in *most* user-typed " +
87+
"specs the mismatch is a typo (e.g. Llama-style where the " +
88+
"architect meant H = nh*head_dim).",
89+
example:
90+
"Llama-3 8B → H=4096, nh=32, head_dim=128 → 32*128=4096 ✓ " +
91+
"DeepSeek-V3 → H=7168, nh=128, head_dim=192 → 128*192=24576 " +
92+
"(decoupled Q via W_Q projection).",
93+
reference: "https://arxiv.org/abs/2412.19437",
94+
},
95+
};
96+
97+
export interface HelpIconProps {
98+
topic: keyof typeof HELP_TOPICS | string;
99+
size?: number;
100+
}
101+
102+
export function HelpIcon({ topic, size = 14 }: HelpIconProps): JSX.Element {
103+
const [open, setOpen] = useState(false);
104+
const entry = HELP_TOPICS[topic];
105+
106+
return (
107+
<>
108+
<button
109+
type="button"
110+
data-testid={`help-icon-${topic}`}
111+
aria-label={`Help: ${entry?.title ?? topic}`}
112+
onClick={() => setOpen(true)}
113+
style={{
114+
width: size, height: size, padding: 0,
115+
borderRadius: "50%", background: "#e5e7eb",
116+
color: "#374151", border: "none",
117+
fontSize: Math.max(9, size - 4), lineHeight: 1,
118+
cursor: "pointer", fontWeight: 700, marginLeft: 4,
119+
}}
120+
>
121+
?
122+
</button>
123+
{open && <HelpModal topic={topic} onClose={() => setOpen(false)} />}
124+
</>
125+
);
126+
}
127+
128+
export interface HelpModalProps {
129+
topic: string;
130+
onClose: () => void;
131+
}
132+
133+
export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
134+
const entry = HELP_TOPICS[topic];
135+
return (
136+
<div data-testid="help-modal-backdrop"
137+
role="dialog" aria-modal="true"
138+
onClick={onClose}
139+
style={{
140+
position: "fixed", inset: 0,
141+
background: "rgba(15,23,42,0.45)",
142+
display: "flex", alignItems: "center", justifyContent: "center",
143+
zIndex: 150, fontFamily: "system-ui, sans-serif",
144+
}}>
145+
<div data-testid={`help-modal-${topic}`}
146+
onClick={(e) => e.stopPropagation()}
147+
style={{
148+
background: "white", borderRadius: 6, padding: 20,
149+
width: 520, maxHeight: "80vh", overflowY: "auto",
150+
boxShadow: "0 10px 30px rgba(15,23,42,0.2)",
151+
}}>
152+
<header style={{ display: "flex", justifyContent: "space-between",
153+
alignItems: "center", marginBottom: 12 }}>
154+
<h3 data-testid="help-modal-title"
155+
style={{ margin: 0, fontSize: 15 }}>
156+
{entry?.title ?? topic}
157+
</h3>
158+
<button data-testid="help-modal-close" onClick={onClose}
159+
style={{ background: "none", border: "none",
160+
cursor: "pointer", fontSize: 18 }}>
161+
×
162+
</button>
163+
</header>
164+
{entry ? (
165+
<>
166+
<Section label="What" testid="help-modal-what">
167+
{entry.what}
168+
</Section>
169+
<Section label="Why" testid="help-modal-why">
170+
{entry.why}
171+
</Section>
172+
{entry.example && (
173+
<Section label="Example" testid="help-modal-example">
174+
<code style={{ fontFamily: "ui-monospace, monospace",
175+
fontSize: 12,
176+
background: "#f3f4f6", padding: "2px 4px",
177+
borderRadius: 2 }}>
178+
{entry.example}
179+
</code>
180+
</Section>
181+
)}
182+
{entry.reference && (
183+
<Section label="Reference" testid="help-modal-reference">
184+
<a href={entry.reference} target="_blank"
185+
rel="noopener noreferrer"
186+
style={{ color: "#2563eb" }}>
187+
{entry.reference}
188+
</a>
189+
</Section>
190+
)}
191+
</>
192+
) : (
193+
<p data-testid="help-modal-missing"
194+
style={{ color: "#6b7280", margin: 0 }}>
195+
(No explanation for <code>{topic}</code> yet.)
196+
</p>
197+
)}
198+
</div>
199+
</div>
200+
);
201+
}
202+
203+
function Section({
204+
label, testid, children,
205+
}: { label: string; testid: string; children: React.ReactNode }): JSX.Element {
206+
return (
207+
<section style={{ marginBottom: 10 }}>
208+
<div style={{ color: "#6b7280", fontSize: 11,
209+
textTransform: "uppercase", letterSpacing: 0.5,
210+
marginBottom: 2 }}>
211+
{label}
212+
</div>
213+
<div data-testid={testid} style={{ color: "#111827", fontSize: 13 }}>
214+
{children}
215+
</div>
216+
</section>
217+
);
218+
}

vbgui/tests/HelpIcon.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect } from "vitest";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import { HelpIcon, HELP_TOPICS } from "@/components/HelpIcon";
4+
5+
describe("HelpIcon + HelpModal", () => {
6+
it("renders the ? button with the topic-derived testid", () => {
7+
render(<HelpIcon topic="dim_env_H" />);
8+
expect(screen.getByTestId("help-icon-dim_env_H")).toBeDefined();
9+
});
10+
11+
it("clicking the icon opens the modal with topic title + sections", () => {
12+
render(<HelpIcon topic="dim_env_H" />);
13+
fireEvent.click(screen.getByTestId("help-icon-dim_env_H"));
14+
expect(screen.getByTestId("help-modal-dim_env_H")).toBeDefined();
15+
const title = screen.getByTestId("help-modal-title");
16+
expect(title.textContent).toContain("H");
17+
expect(screen.getByTestId("help-modal-what")).toBeDefined();
18+
expect(screen.getByTestId("help-modal-why")).toBeDefined();
19+
});
20+
21+
it("renders 'missing' panel for an unknown topic", () => {
22+
render(<HelpIcon topic="this_topic_does_not_exist" />);
23+
fireEvent.click(
24+
screen.getByTestId("help-icon-this_topic_does_not_exist"));
25+
expect(screen.getByTestId("help-modal-missing")).toBeDefined();
26+
});
27+
28+
it("symbolic_dim_mismatch topic explains the decoupled-Q convention", () => {
29+
expect(HELP_TOPICS.symbolic_dim_mismatch).toBeDefined();
30+
render(<HelpIcon topic="symbolic_dim_mismatch" />);
31+
fireEvent.click(screen.getByTestId("help-icon-symbolic_dim_mismatch"));
32+
const why = screen.getByTestId("help-modal-why");
33+
expect(why.textContent?.toLowerCase()).toContain("decouple");
34+
});
35+
36+
it("close button hides the modal", () => {
37+
render(<HelpIcon topic="dim_env_nh" />);
38+
fireEvent.click(screen.getByTestId("help-icon-dim_env_nh"));
39+
expect(screen.getByTestId("help-modal-dim_env_nh")).toBeDefined();
40+
fireEvent.click(screen.getByTestId("help-modal-close"));
41+
expect(screen.queryByTestId("help-modal-dim_env_nh")).toBeNull();
42+
});
43+
});

0 commit comments

Comments
 (0)