Skip to content

Commit 2669d02

Browse files
committed
feat(vbgui-fc): sidebar 5 tabs + top bar + bottom strip + spec reducer
Stage F-C of the Visual Builder GUI epic (cppmega-mlx-o0k). Lands the sidebar / chrome the canvas needs to become a working editor per VisualBuilderPlan.md §4.2-4.4. State: - src/state/spec.ts — SpecState single source of truth with LossState / OptimState / RewriterState / ShardingState / GotchaState fields, reducer covering loss.set / optim.add_group / optim.remove_group / rewriters.add+remove+reorder / sharding.set / gotchas.set / memory.set / verify.complete / backend.status. Plus memoryFillRatio + memoryColor helpers using the green<0.7 / yellow<0.9 / red thresholds from §4.2. Sidebar tabs (320px, vertical, single source of truth on parent state): - LossTab — kind dropdown with conditional params per kind (MTP k+beta, IFIM lambda_fim, MHC lambda_mhc, Custom function-name). - OptimTab — kind + per-group table (matcher/lr/wd) with + Add group and per-row × remove, grad_clip_norm + mixed_precision globals. - RewritersTab — add buttons for MTP/IFIM/MHC, ordered chip list with ▲/▼ reorder and × remove, optional Apply button. - ShardingTab — proposals card list with Accept, custom axis table (+ Add axis, per-row remove), three toggles (master_weights_fp32 / fp8_enabled / activation_checkpointing). - GotchasTab — severity-grouped chips (error/warning/info) with optional reference links and Auto-fix buttons for the known compile-mode footguns. Chrome: - TopBar (56px) — project name, preset launcher (12 presets), 8 topology selector, compile-mode dropdown (off/regional/whole_model), MemoryBar (color-coded fill), split-button Smoke/Full/Train run. - BottomStrip (32px) — backend connection dot, last verify latency, brick + fused-region count, help toggle. - MemoryBar — re-usable bar with green/yellow/red fill driven by memoryColor(state). App wires all of the above into one layout: TopBar over a row of Palette + FlowCanvas + Sidebar, then BottomStrip beneath. Tests (36 new, 65 total, vitest + jsdom): reducer transitions, memory helper thresholds, every tab + Sidebar tab switching, TopBar preset/topology/compile/run interactions, BottomStrip status fields, MemoryBar fill ratio. Build: vite build emits 575 kB JS / 16 kB CSS (gzip 173 kB / 2.6 kB). Typecheck clean. Closes cppmega-mlx-o0k.3.
1 parent a914d4b commit 2669d02

14 files changed

Lines changed: 1374 additions & 16 deletions

vbgui/src/App.tsx

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,105 @@
1-
import { useCallback, useState } from "react";
1+
import { useCallback, useReducer, useState } from "react";
22
import { ReactFlowProvider, type Edge, type Node } from "@xyflow/react";
3+
34
import { FlowCanvas } from "@/components/FlowCanvas";
45
import { Palette } from "@/components/Palette";
6+
import { Sidebar } from "@/components/Sidebar";
7+
import { TopBar, type RunMode } from "@/components/TopBar";
8+
import { BottomStrip } from "@/components/BottomStrip";
9+
10+
import {
11+
INITIAL_SPEC, specReducer, type TopologyFactory,
12+
} from "@/state/spec";
13+
import type { ShardingProposalView } from "@/components/sidebar/ShardingTab";
14+
15+
const PRESETS: readonly string[] = [
16+
"qwen3_next", "kimi_linear", "kimi_k2", "deepseek_v3",
17+
"deepseek_v4_flash", "gemma4", "mistral4", "ling26",
18+
"longcat", "nemotron3", "zaya1", "arcee_trinity",
19+
];
20+
21+
const TOPOLOGIES: readonly TopologyFactory[] = [
22+
"h100_8x", "h200_8x", "a100_8x", "b100_8x",
23+
"gb10_quarter", "tpu_v6e_8", "tpu_v5p_4", "m3_ultra_solo",
24+
];
525

626
export function App(): JSX.Element {
727
const [nodes, setNodes] = useState<Node[]>([]);
828
const [edges, setEdges] = useState<Edge[]>([]);
29+
const [projectName, setProjectName] = useState("untitled");
30+
const [proposals] = useState<ShardingProposalView[]>([]);
31+
const [spec, dispatch] = useReducer(specReducer, INITIAL_SPEC);
932

1033
const handleDropBrick = useCallback(
1134
(kind: string, position: { x: number; y: number }) => {
1235
setNodes((prev) => [
1336
...prev,
14-
{
15-
id: `${kind}_${prev.length + 1}`,
16-
type: "brick",
17-
position,
18-
data: { kind },
19-
},
37+
{ id: `${kind}_${prev.length + 1}`,
38+
type: "brick", position, data: { kind } },
2039
]);
2140
}, []);
2241

2342
const handleConnect = useCallback(
2443
(p: { source: string; target: string }) => {
2544
setEdges((prev) => [
2645
...prev,
27-
{ id: `${p.source}->${p.target}`,
28-
source: p.source, target: p.target,
46+
{ id: `${p.source}->${p.target}`, source: p.source, target: p.target,
2947
data: { severity: "info" } },
3048
]);
3149
}, []);
3250

51+
const handlePresetDrop = useCallback((_name: string) => {
52+
// F-A.2/F-D will resolve preset → specs via build_preset_specs RPC and
53+
// fan out into Node[] additions. Stub for now.
54+
}, []);
55+
56+
const handleRunPipeline = useCallback((_mode: RunMode) => {
57+
// F-D wires this to JSON-RPC pipeline.run; for now we only flag intent.
58+
}, []);
59+
3360
return (
3461
<ReactFlowProvider>
35-
<div style={{ display: "flex", height: "100vh", margin: 0 }}>
36-
<Palette />
37-
<FlowCanvas
38-
nodes={nodes}
39-
edges={edges}
40-
onConnect={handleConnect}
41-
onDropBrick={handleDropBrick}
62+
<div style={{ display: "flex", flexDirection: "column",
63+
height: "100vh", margin: 0 }}>
64+
<TopBar
65+
state={spec}
66+
projectName={projectName}
67+
presets={PRESETS}
68+
topologies={TOPOLOGIES}
69+
onProjectNameChange={setProjectName}
70+
onPresetDrop={handlePresetDrop}
71+
onTopologyChange={(t) => dispatch({ type: "sharding.set",
72+
sharding: { ...spec.sharding, topology: t } })}
73+
onCompileModeChange={(m) => dispatch({ type: "sharding.set",
74+
sharding: { ...spec.sharding, compile_mode: m } })}
75+
onRunPipeline={handleRunPipeline}
4276
/>
77+
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
78+
<Palette />
79+
<FlowCanvas
80+
nodes={nodes} edges={edges}
81+
onConnect={handleConnect}
82+
onDropBrick={handleDropBrick}
83+
/>
84+
<Sidebar
85+
loss={spec.loss}
86+
optim={spec.optim}
87+
rewriters={spec.rewriters}
88+
sharding={spec.sharding}
89+
gotchas={spec.gotchas}
90+
proposals={proposals}
91+
onLossApply={(l) => dispatch({ type: "loss.set", loss: l })}
92+
onOptimApply={(o) => dispatch({ type: "optim.set", optim: o })}
93+
onRewriterAdd={(r) => dispatch({ type: "rewriters.add", rewriter: r })}
94+
onRewriterRemove={(i) => dispatch({ type: "rewriters.remove", index: i })}
95+
onRewriterReorder={(f, t) =>
96+
dispatch({ type: "rewriters.reorder", from: f, to: t })}
97+
onShardingChange={(s) =>
98+
dispatch({ type: "sharding.set", sharding: s })}
99+
onShardingAccept={(_idx) => { /* F-D wires accept → topology update */ }}
100+
/>
101+
</div>
102+
<BottomStrip state={spec} fusedRegionCount={0} />
43103
</div>
44104
</ReactFlowProvider>
45105
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { SpecState } from "@/state/spec";
2+
3+
export interface BottomStripProps {
4+
state: SpecState;
5+
fusedRegionCount?: number;
6+
onHelpToggle?: () => void;
7+
}
8+
9+
const STATUS_COLOR: Record<SpecState["backend_status"], string> = {
10+
connected: "#10b981",
11+
reconnecting: "#d97706",
12+
disconnected: "#dc2626",
13+
};
14+
15+
const STATUS_LABEL: Record<SpecState["backend_status"], string> = {
16+
connected: "Backend connected",
17+
reconnecting: "Reconnecting…",
18+
disconnected: "Disconnected",
19+
};
20+
21+
export function BottomStrip({
22+
state, fusedRegionCount = 0, onHelpToggle,
23+
}: BottomStripProps): JSX.Element {
24+
return (
25+
<footer data-testid="bottom-strip"
26+
style={{ height: 32, display: "flex", alignItems: "center",
27+
gap: 16, padding: "0 12px",
28+
borderTop: "1px solid #e5e7eb",
29+
background: "#f9fafb",
30+
fontFamily: "system-ui, sans-serif", fontSize: 11 }}>
31+
<span data-testid="backend-status"
32+
style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
33+
<span data-testid="backend-status-dot"
34+
style={{ width: 8, height: 8, borderRadius: "50%",
35+
background: STATUS_COLOR[state.backend_status],
36+
animation: state.backend_status === "reconnecting"
37+
? "pulse 1.2s infinite" : "none" }} />
38+
{STATUS_LABEL[state.backend_status]}
39+
</span>
40+
<span data-testid="verify-latency">
41+
Verify: {state.last_verify_ms.toFixed(1)}ms
42+
</span>
43+
<span data-testid="brick-count">
44+
{state.brick_count} bricks, {fusedRegionCount} fused regions
45+
</span>
46+
<span style={{ flex: 1 }} />
47+
<button data-testid="help-toggle" onClick={onHelpToggle}>?</button>
48+
</footer>
49+
);
50+
}

vbgui/src/components/MemoryBar.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { memoryColor, type SpecState } from "@/state/spec";
2+
3+
export interface MemoryBarProps {
4+
state: SpecState;
5+
}
6+
7+
const COLOR_HEX: Record<"green" | "yellow" | "red", string> = {
8+
green: "#10b981", yellow: "#d97706", red: "#dc2626",
9+
};
10+
11+
function formatGB(n: number): string {
12+
return `${(n / 1024 ** 3).toFixed(2)} GB`;
13+
}
14+
15+
export function MemoryBar({ state }: MemoryBarProps): JSX.Element {
16+
const ratio = state.device_hbm_bytes > 0
17+
? Math.min(1, state.worst_rank_bytes / state.device_hbm_bytes)
18+
: 0;
19+
const color = COLOR_HEX[memoryColor(state)];
20+
const tooltip = `${formatGB(state.worst_rank_bytes)} / ${formatGB(state.device_hbm_bytes)}`;
21+
return (
22+
<div data-testid="memory-bar" title={tooltip}
23+
style={{ flex: 1, height: 24, background: "#e5e7eb",
24+
borderRadius: 4, overflow: "hidden",
25+
position: "relative" }}>
26+
<div data-testid="memory-bar-fill"
27+
style={{ width: `${ratio * 100}%`, height: "100%",
28+
background: color, transition: "width 200ms" }} />
29+
<div style={{ position: "absolute", inset: 0,
30+
display: "flex", alignItems: "center",
31+
justifyContent: "center",
32+
fontSize: 11, fontFamily: "system-ui, sans-serif",
33+
color: ratio > 0.5 ? "white" : "#111827" }}>
34+
{tooltip}
35+
</div>
36+
</div>
37+
);
38+
}

vbgui/src/components/Sidebar.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useState } from "react";
2+
import { LossTab } from "./sidebar/LossTab";
3+
import { OptimTab } from "./sidebar/OptimTab";
4+
import { RewritersTab } from "./sidebar/RewritersTab";
5+
import { ShardingTab, type ShardingProposalView } from "./sidebar/ShardingTab";
6+
import { GotchasTab } from "./sidebar/GotchasTab";
7+
import type {
8+
GotchaState, LossState, OptimState, RewriterState, ShardingState,
9+
} from "@/state/spec";
10+
11+
export type SidebarTab = "loss" | "optim" | "rewriters" | "sharding" | "gotchas";
12+
13+
export interface SidebarProps {
14+
loss: LossState;
15+
optim: OptimState;
16+
rewriters: RewriterState[];
17+
sharding: ShardingState;
18+
gotchas: GotchaState[];
19+
proposals: ShardingProposalView[];
20+
onLossApply: (l: LossState) => void;
21+
onOptimApply: (o: OptimState) => void;
22+
onRewriterAdd: (r: RewriterState) => void;
23+
onRewriterRemove: (i: number) => void;
24+
onRewriterReorder: (from: number, to: number) => void;
25+
onRewriterApply?: () => void;
26+
onShardingChange: (s: ShardingState) => void;
27+
onShardingAccept: (idx: number) => void;
28+
onGotchaAutoFix?: (id: string) => void;
29+
}
30+
31+
const TAB_LABELS: { key: SidebarTab; label: string }[] = [
32+
{ key: "loss", label: "Loss" },
33+
{ key: "optim", label: "Optim" },
34+
{ key: "rewriters", label: "Rewriters" },
35+
{ key: "sharding", label: "Sharding" },
36+
{ key: "gotchas", label: "Gotchas" },
37+
];
38+
39+
export function Sidebar(p: SidebarProps): JSX.Element {
40+
const [active, setActive] = useState<SidebarTab>("loss");
41+
return (
42+
<aside data-testid="sidebar"
43+
style={{ width: 320, background: "#fff",
44+
borderLeft: "1px solid #e5e7eb",
45+
display: "flex", flexDirection: "column",
46+
fontFamily: "system-ui, sans-serif" }}>
47+
<nav role="tablist" data-testid="sidebar-tabs"
48+
style={{ display: "flex", borderBottom: "1px solid #e5e7eb" }}>
49+
{TAB_LABELS.map((t) => (
50+
<button key={t.key}
51+
role="tab"
52+
aria-selected={active === t.key}
53+
data-testid={`sidebar-tab-${t.key}`}
54+
onClick={() => setActive(t.key)}
55+
style={{
56+
flex: 1, padding: "8px 4px", border: "none",
57+
background: active === t.key ? "#f3f4f6" : "transparent",
58+
cursor: "pointer", fontSize: 12,
59+
borderBottom: active === t.key
60+
? "2px solid #2563eb" : "2px solid transparent",
61+
}}>
62+
{t.label}
63+
</button>
64+
))}
65+
</nav>
66+
<div style={{ flex: 1, overflowY: "auto" }}>
67+
{active === "loss" && <LossTab loss={p.loss} onApply={p.onLossApply} />}
68+
{active === "optim" && <OptimTab optim={p.optim} onApply={p.onOptimApply} />}
69+
{active === "rewriters" && (
70+
<RewritersTab rewriters={p.rewriters}
71+
onAdd={p.onRewriterAdd}
72+
onRemove={p.onRewriterRemove}
73+
onReorder={p.onRewriterReorder}
74+
onApply={p.onRewriterApply} />
75+
)}
76+
{active === "sharding" && (
77+
<ShardingTab sharding={p.sharding} proposals={p.proposals}
78+
onAccept={p.onShardingAccept}
79+
onChange={p.onShardingChange} />
80+
)}
81+
{active === "gotchas" && (
82+
<GotchasTab gotchas={p.gotchas} onAutoFix={p.onGotchaAutoFix} />
83+
)}
84+
</div>
85+
</aside>
86+
);
87+
}

vbgui/src/components/TopBar.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useState } from "react";
2+
import { MemoryBar } from "./MemoryBar";
3+
import type { SpecState, TopologyFactory } from "@/state/spec";
4+
5+
export type RunMode = "smoke" | "full" | "train";
6+
7+
export interface TopBarProps {
8+
state: SpecState;
9+
projectName: string;
10+
presets: readonly string[];
11+
topologies: readonly TopologyFactory[];
12+
onProjectNameChange: (name: string) => void;
13+
onPresetDrop: (name: string) => void;
14+
onTopologyChange: (t: TopologyFactory) => void;
15+
onCompileModeChange: (m: SpecState["sharding"]["compile_mode"]) => void;
16+
onRunPipeline: (mode: RunMode) => void;
17+
}
18+
19+
export function TopBar(p: TopBarProps): JSX.Element {
20+
const [open, setOpen] = useState(false);
21+
return (
22+
<header data-testid="top-bar"
23+
style={{ height: 56, display: "flex", alignItems: "center",
24+
gap: 12, padding: "0 12px",
25+
borderBottom: "1px solid #e5e7eb",
26+
fontFamily: "system-ui, sans-serif", fontSize: 12 }}>
27+
<input data-testid="project-name"
28+
value={p.projectName}
29+
onChange={(e) => p.onProjectNameChange(e.target.value)}
30+
style={{ width: 160, fontWeight: 600 }} />
31+
32+
<select data-testid="preset-launcher" defaultValue=""
33+
onChange={(e) => { p.onPresetDrop(e.target.value);
34+
e.currentTarget.value = ""; }}>
35+
<option value="" disabled>Preset…</option>
36+
{p.presets.map((n) => <option key={n} value={n}>{n}</option>)}
37+
</select>
38+
39+
<select data-testid="topology-selector"
40+
value={p.state.sharding.topology}
41+
onChange={(e) =>
42+
p.onTopologyChange(e.target.value as TopologyFactory)}>
43+
{p.topologies.map((t) => <option key={t} value={t}>{t}</option>)}
44+
</select>
45+
46+
<select data-testid="compile-mode"
47+
value={p.state.sharding.compile_mode}
48+
onChange={(e) =>
49+
p.onCompileModeChange(
50+
e.target.value as SpecState["sharding"]["compile_mode"])}>
51+
<option value="off">compile: off</option>
52+
<option value="regional">compile: regional</option>
53+
<option value="whole_model">compile: whole_model ⚠</option>
54+
</select>
55+
56+
<MemoryBar state={p.state} />
57+
58+
<div style={{ position: "relative" }}>
59+
<button data-testid="run-pipeline"
60+
onClick={() => p.onRunPipeline("smoke")}>
61+
Smoke
62+
</button>
63+
<button data-testid="run-pipeline-toggle"
64+
onClick={() => setOpen((x) => !x)}></button>
65+
{open && (
66+
<div data-testid="run-pipeline-menu"
67+
style={{ position: "absolute", top: "100%", right: 0,
68+
background: "white", border: "1px solid #e5e7eb",
69+
boxShadow: "0 2px 6px rgba(0,0,0,0.08)",
70+
zIndex: 10 }}>
71+
<button data-testid="run-pipeline-full"
72+
onClick={() => { setOpen(false); p.onRunPipeline("full"); }}
73+
style={menuItem}>Full validate</button>
74+
<button data-testid="run-pipeline-train"
75+
onClick={() => { setOpen(false); p.onRunPipeline("train"); }}
76+
style={menuItem}>Train</button>
77+
</div>
78+
)}
79+
</div>
80+
</header>
81+
);
82+
}
83+
84+
const menuItem: React.CSSProperties = {
85+
display: "block", padding: "6px 12px", border: "none",
86+
background: "white", cursor: "pointer", textAlign: "left", width: "100%",
87+
};

0 commit comments

Comments
 (0)