Skip to content

Commit d6efa99

Browse files
committed
feat(ux): tensor-flow diagrams in HelpModal + ExplainModal
Closes cppmega-mlx-w2t6. Drops a tiny pure-SVG mini-framework at vbgui/src/components/diagrams/TensorDiagram.tsx exposing four primitives — Diagram (root container), Tensor (rounded-rect box with shape annotation), Op (operation chip), Arrow (straight or right-angle with optional label), Residual (dashed yellow skip connection) — themed to match the canvas (cyan tensors, indigo ops, slate arrows, yellow residuals on the dark slate background). Ten brick diagrams shipped in components/diagrams/bricks/: attention, gated_attention, mla, mlp, moe, mamba3, mlstm, gdn, rmsnorm, abs_pos_embed. Each diagram is 15-30 lines of declarative JSX laying out the canonical tensor flow (shapes, ops, residual path) from the upstream paper. Coverage extends to 17 topics via the registry (mla_absorb / mistral4_mla / gqa_sliding / cca_attention reuse the SDPA / MLA layouts; bailing_moe reuses MoE; kda reuses GDN). HelpModal + ExplainModal: both modals were already viewport-centered (inset:0 + flex center). Modal widths bumped 520 → 620px / 560 → 640px (both with maxWidth:92vw so phones still fit) so the diagrams render at full resolution. Diagrams slot between the Summary/What and Why/When-to-use sections, behind a "Tensor flow" label. Research grounding: launched two background research agents (Exa/Perplexity/Brave) to (a) compare SVG diagram libraries — picked pure inline SVG for ~0 KB bundle cost; (b) collect canonical tensor shapes per brick. Both reports informed the final code. Tests: 20 new vitest (17 per-brick render + 3 HelpModal slot integration). Regression: 506/506 vitest green.
1 parent 3060daf commit d6efa99

15 files changed

Lines changed: 757 additions & 3 deletions

vbgui/src/components/ExplainModal.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// button for optimizer/schedule entries.
33

44
import { useCatalog } from "@/hooks/useCatalog";
5+
import { TENSOR_DIAGRAMS } from "./diagrams";
56
import type { RpcClient } from "@/lib/rpc";
67

78
export interface ExplainModalProps {
@@ -59,7 +60,8 @@ export function ExplainModal({
5960
onClick={(e) => e.stopPropagation()}
6061
style={{
6162
background: "white", borderRadius: 6, padding: 20,
62-
width: 560, maxHeight: "85vh", overflowY: "auto",
63+
width: 640, maxWidth: "92vw",
64+
maxHeight: "85vh", overflowY: "auto",
6365
}}>
6466
<header style={{ display: "flex", justifyContent: "space-between",
6567
alignItems: "center", marginBottom: 12 }}>
@@ -88,6 +90,21 @@ export function ExplainModal({
8890
<div style={LABEL}>Summary</div>
8991
<div data-testid="explain-modal-summary">{entry.summary}</div>
9092
</section>
93+
{(() => {
94+
// Tensor-flow diagram lookup — both prefix-namespaced
95+
// keys (e.g. "brick_attention") and bare category+name
96+
// pairs ("brick" + "attention") resolve through the same
97+
// registry.
98+
const key = `${category}_${name}`;
99+
const Diag = TENSOR_DIAGRAMS[key];
100+
return Diag ? (
101+
<section style={SECTION}
102+
data-testid="explain-modal-diagram">
103+
<div style={LABEL}>Tensor flow</div>
104+
<Diag />
105+
</section>
106+
) : null;
107+
})()}
91108

92109
<section style={SECTION}>
93110
<div style={LABEL}>When to use</div>

vbgui/src/components/HelpIcon.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// not gated on touching the component being explained.
44

55
import { useState } from "react";
6+
import { TENSOR_DIAGRAMS } from "./diagrams";
67

78
export interface HelpTopic {
89
title: string;
@@ -947,8 +948,9 @@ export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
947948
border: "1px solid rgba(255, 255, 255, 0.1)",
948949
borderRadius: "12px",
949950
padding: "24px",
950-
width: "520px",
951-
maxHeight: "85vh",
951+
width: "620px",
952+
maxWidth: "92vw",
953+
maxHeight: "88vh",
952954
overflowY: "auto",
953955
boxShadow: "0 20px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05)",
954956
display: "flex",
@@ -1016,6 +1018,15 @@ export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
10161018
<Section label="What" testid="help-modal-what">
10171019
{entry.what}
10181020
</Section>
1021+
{(() => {
1022+
const Diag = TENSOR_DIAGRAMS[topic];
1023+
return Diag ? (
1024+
<Section label="Tensor flow"
1025+
testid="help-modal-diagram">
1026+
<Diag />
1027+
</Section>
1028+
) : null;
1029+
})()}
10191030
<Section label="Why" testid="help-modal-why">
10201031
{entry.why}
10211032
</Section>
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* cppmega-mlx-w2t6: tiny SVG mini-framework for tensor-flow diagrams
3+
* inside HelpModal / ExplainModal.
4+
*
5+
* Each diagram is declarative JSX: a <Diagram> wrapper picks the
6+
* canvas size + dark-theme background, then primitives like
7+
* <Tensor>, <Op>, <Arrow>, <Residual>, <Group> position elements via
8+
* absolute x/y coords. The container automatically scales to its
9+
* container width while preserving the aspect ratio.
10+
*
11+
* Colour palette pinned to the rest of the canvas:
12+
* - background: rgba(15, 23, 42, 0.6) (#0f172a-ish)
13+
* - tensor box: fill #1e293b, stroke #22d3ee (cyan)
14+
* - op box: fill #312e81, stroke #818cf8 (indigo)
15+
* - arrow: #94a3b8 (slate-400)
16+
* - residual: #facc15 (yellow), dashed
17+
* - text: #f1f5f9 (slate-50)
18+
*/
19+
20+
import { type ReactNode } from "react";
21+
22+
23+
export const DIAG_THEME = {
24+
bg: "rgba(15, 23, 42, 0.6)",
25+
border: "rgba(255, 255, 255, 0.05)",
26+
tensorBg: "#1e293b",
27+
tensorFg: "#22d3ee",
28+
opBg: "#312e81",
29+
opFg: "#818cf8",
30+
arrow: "#94a3b8",
31+
residual: "#facc15",
32+
text: "#f1f5f9",
33+
textMuted: "#94a3b8",
34+
} as const;
35+
36+
37+
export interface DiagramProps {
38+
width?: number;
39+
height?: number;
40+
/** Diagram caption (rendered above the SVG). */
41+
caption?: string;
42+
children: ReactNode;
43+
}
44+
45+
/** Root SVG container. Scales to width via `width="100%"` on the
46+
* parent <svg>; coordinates inside are in the [0, width] × [0, height]
47+
* user-space frame. */
48+
export function Diagram({
49+
width = 480, height = 220, caption, children,
50+
}: DiagramProps): JSX.Element {
51+
return (
52+
<div data-testid="tensor-diagram" style={{
53+
background: DIAG_THEME.bg,
54+
border: `1px solid ${DIAG_THEME.border}`,
55+
borderRadius: 6,
56+
padding: 10,
57+
}}>
58+
{caption && (
59+
<div data-testid="tensor-diagram-caption" style={{
60+
color: DIAG_THEME.textMuted,
61+
fontSize: 11,
62+
marginBottom: 6,
63+
fontFamily: "ui-monospace, monospace",
64+
}}>
65+
{caption}
66+
</div>
67+
)}
68+
<svg viewBox={`0 0 ${width} ${height}`}
69+
width="100%" preserveAspectRatio="xMidYMid meet"
70+
style={{ display: "block" }}>
71+
<defs>
72+
<marker id="arrowhead" markerWidth="6" markerHeight="6"
73+
refX="6" refY="3" orient="auto" markerUnits="strokeWidth">
74+
<path d="M0,0 L6,3 L0,6 Z" fill={DIAG_THEME.arrow} />
75+
</marker>
76+
<marker id="arrowhead-residual" markerWidth="6" markerHeight="6"
77+
refX="6" refY="3" orient="auto" markerUnits="strokeWidth">
78+
<path d="M0,0 L6,3 L0,6 Z" fill={DIAG_THEME.residual} />
79+
</marker>
80+
</defs>
81+
{children}
82+
</svg>
83+
</div>
84+
);
85+
}
86+
87+
88+
export interface TensorProps {
89+
x: number;
90+
y: number;
91+
w?: number;
92+
h?: number;
93+
label: string;
94+
/** Shape annotation rendered below the label, e.g. "(B, S, H)". */
95+
shape?: string;
96+
testid?: string;
97+
}
98+
99+
/** Rounded-rect tensor block. */
100+
export function Tensor({
101+
x, y, w = 90, h = 44, label, shape, testid,
102+
}: TensorProps): JSX.Element {
103+
return (
104+
<g data-testid={testid}>
105+
<rect x={x} y={y} width={w} height={h} rx={6}
106+
fill={DIAG_THEME.tensorBg}
107+
stroke={DIAG_THEME.tensorFg}
108+
strokeWidth={1.5} />
109+
<text x={x + w / 2} y={y + (shape ? h / 2 - 2 : h / 2 + 4)}
110+
textAnchor="middle"
111+
fill={DIAG_THEME.text}
112+
fontSize={12} fontWeight={600}
113+
fontFamily="system-ui, sans-serif">
114+
{label}
115+
</text>
116+
{shape && (
117+
<text x={x + w / 2} y={y + h / 2 + 12}
118+
textAnchor="middle"
119+
fill={DIAG_THEME.textMuted}
120+
fontSize={9}
121+
fontFamily="ui-monospace, monospace">
122+
{shape}
123+
</text>
124+
)}
125+
</g>
126+
);
127+
}
128+
129+
130+
export interface OpProps {
131+
x: number;
132+
y: number;
133+
w?: number;
134+
h?: number;
135+
label: string;
136+
testid?: string;
137+
}
138+
139+
/** Operation node (matmul / softmax / silu / etc.). Hex-shaped. */
140+
export function Op({
141+
x, y, w = 70, h = 30, label, testid,
142+
}: OpProps): JSX.Element {
143+
return (
144+
<g data-testid={testid}>
145+
<rect x={x} y={y} width={w} height={h} rx={14}
146+
fill={DIAG_THEME.opBg}
147+
stroke={DIAG_THEME.opFg}
148+
strokeWidth={1.2} />
149+
<text x={x + w / 2} y={y + h / 2 + 4}
150+
textAnchor="middle"
151+
fill={DIAG_THEME.text}
152+
fontSize={11}
153+
fontFamily="system-ui, sans-serif">
154+
{label}
155+
</text>
156+
</g>
157+
);
158+
}
159+
160+
161+
export interface ArrowProps {
162+
x1: number; y1: number; x2: number; y2: number;
163+
/** Optional label rendered halfway along the arrow. */
164+
label?: string;
165+
/** Bend along the y axis at this fraction of x1→x2. */
166+
bendY?: number;
167+
testid?: string;
168+
}
169+
170+
/** Straight or right-angle arrow between two anchors. */
171+
export function Arrow({
172+
x1, y1, x2, y2, label, bendY, testid,
173+
}: ArrowProps): JSX.Element {
174+
let path: string;
175+
let midX: number, midY: number;
176+
if (bendY !== undefined) {
177+
const mx = x1 + (x2 - x1) * (bendY ?? 0.5);
178+
path = `M ${x1} ${y1} L ${mx} ${y1} L ${mx} ${y2} L ${x2} ${y2}`;
179+
midX = mx;
180+
midY = (y1 + y2) / 2;
181+
} else {
182+
path = `M ${x1} ${y1} L ${x2} ${y2}`;
183+
midX = (x1 + x2) / 2;
184+
midY = (y1 + y2) / 2;
185+
}
186+
return (
187+
<g data-testid={testid}>
188+
<path d={path} fill="none"
189+
stroke={DIAG_THEME.arrow} strokeWidth={1.5}
190+
markerEnd="url(#arrowhead)" />
191+
{label && (
192+
<text x={midX} y={midY - 5} textAnchor="middle"
193+
fill={DIAG_THEME.textMuted}
194+
fontSize={10}
195+
fontFamily="ui-monospace, monospace">
196+
{label}
197+
</text>
198+
)}
199+
</g>
200+
);
201+
}
202+
203+
204+
/** Dashed yellow residual line (skip connection). */
205+
export function Residual({
206+
x1, y1, x2, y2, label, bendY = 0.5, testid,
207+
}: ArrowProps): JSX.Element {
208+
const mx = x1 + (x2 - x1) * bendY;
209+
const path =
210+
`M ${x1} ${y1} L ${mx} ${y1} L ${mx} ${y2} L ${x2} ${y2}`;
211+
return (
212+
<g data-testid={testid}>
213+
<path d={path} fill="none"
214+
stroke={DIAG_THEME.residual} strokeWidth={1.5}
215+
strokeDasharray="4 4"
216+
markerEnd="url(#arrowhead-residual)" />
217+
{label && (
218+
<text x={mx + 4} y={(y1 + y2) / 2}
219+
fill={DIAG_THEME.residual}
220+
fontSize={10}
221+
fontFamily="ui-monospace, monospace">
222+
{label}
223+
</text>
224+
)}
225+
</g>
226+
);
227+
}
228+
229+
230+
/** A logical group/box around a sub-region of the diagram (e.g.
231+
* multi-head attention). Renders a faint outlined rectangle with a
232+
* small caption in the top-left corner. */
233+
export interface GroupProps {
234+
x: number; y: number; w: number; h: number;
235+
label?: string;
236+
testid?: string;
237+
children?: ReactNode;
238+
}
239+
240+
export function Group({
241+
x, y, w, h, label, testid, children,
242+
}: GroupProps): JSX.Element {
243+
return (
244+
<g data-testid={testid}>
245+
<rect x={x} y={y} width={w} height={h} rx={8}
246+
fill="none"
247+
stroke="rgba(148, 163, 184, 0.3)"
248+
strokeWidth={1}
249+
strokeDasharray="2 4" />
250+
{label && (
251+
<text x={x + 6} y={y - 4}
252+
fill={DIAG_THEME.textMuted}
253+
fontSize={9}
254+
fontFamily="ui-monospace, monospace">
255+
{label}
256+
</text>
257+
)}
258+
{children}
259+
</g>
260+
);
261+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* abs_pos_embed: y = x + W_pos[:S]
3+
* Ref: BERT / GPT — learned absolute positions.
4+
*/
5+
6+
import { Diagram, Tensor, Op, Arrow } from "../TensorDiagram";
7+
8+
export function AbsPosEmbedDiagram(): JSX.Element {
9+
return (
10+
<Diagram width={460} height={150}
11+
caption="abs_pos_embed: y = x + W_pos[:S]">
12+
<Tensor x={10} y={50} label="x" shape="(B,S,H)" />
13+
<Tensor x={10} y={100} label="W_pos" shape="(L_max,H)" />
14+
<Op x={180} y={75} label="slice + add" w={120} />
15+
<Tensor x={350} y={75} label="y" shape="(B,S,H)" />
16+
<Arrow x1={100} y1={72} x2={180} y2={92} />
17+
<Arrow x1={100} y1={122} x2={180} y2={102} label="[:S]" />
18+
<Arrow x1={300} y1={97} x2={350} y2={97} />
19+
</Diagram>
20+
);
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Vanilla SDPA attention (Llama-style):
3+
* x:(B,S,H) → {Q, K, V} = {Wq, Wk, Wv}(x)
4+
* → scores = Q · Kᵀ / √hd → softmax → · V
5+
* → output proj Wo → y:(B,S,H)
6+
* plus residual x + y
7+
*/
8+
9+
import { Diagram, Tensor, Op, Arrow, Residual } from "../TensorDiagram";
10+
11+
export function AttentionDiagram(): JSX.Element {
12+
return (
13+
<Diagram width={520} height={240}
14+
caption="vanilla SDPA: x → {Q,K,V} → softmax(QKᵀ/√hd)·V → Wo → y + x">
15+
<Tensor x={10} y={100} label="x" shape="(B,S,H)" testid="t-x" />
16+
<Tensor x={140} y={20} label="Q" shape="(B,nh,S,hd)" />
17+
<Tensor x={140} y={100} label="K" shape="(B,nh,S,hd)" />
18+
<Tensor x={140} y={180} label="V" shape="(B,nh,S,hd)" />
19+
<Op x={290} y={50} label="softmax(QKᵀ/√hd)" w={140} />
20+
<Tensor x={290} y={130} label="attn·V" shape="(B,nh,S,hd)" w={140} />
21+
<Tensor x={460} y={100} label="y" shape="(B,S,H)" w={50}
22+
testid="t-y" />
23+
<Arrow x1={100} y1={122} x2={140} y2={42} label="Wq" />
24+
<Arrow x1={100} y1={122} x2={140} y2={122} label="Wk" />
25+
<Arrow x1={100} y1={122} x2={140} y2={202} label="Wv" />
26+
<Arrow x1={230} y1={42} x2={290} y2={65} />
27+
<Arrow x1={230} y1={122} x2={290} y2={65} />
28+
<Arrow x1={230} y1={202} x2={290} y2={152} />
29+
<Arrow x1={360} y1={80} x2={360} y2={130} />
30+
<Arrow x1={430} y1={152} x2={460} y2={122} label="Wo" />
31+
<Residual x1={10} y1={140} x2={460} y2={140} bendY={0.05}
32+
label="+ x (residual)" />
33+
</Diagram>
34+
);
35+
}

0 commit comments

Comments
 (0)