Skip to content

Commit 7bc0b43

Browse files
committed
UX#2/#3/#4: dim_env + train ops → Sidebar tabs; live ckpt only when in-flight
Three connected redesigns. Previously the canvas top stacked five full-width strips (DimEnvEditor + TrainOptionsPanel + RunHistoryPicker + TrainLiveControls + ParallelCompose/Insert/Transplant), eating ~120 vertical px the architect never reads while the canvas idles. UX#2 — DimEnvEditor moves into the right Sidebar's Dimensions tab as a top section. Tab now renders the env editor (when dimEnv + onDimEnvApply are supplied) above the inferred-dimensions log. UX#3 — New Sidebar tab "Train Ops" hosts the K3-K8 train-options accordion + the warm-start RunHistoryPicker. Also folds in the Parallel-compose / Insert-into-edge / Transplant callbacks so structural splicing lives in one tab instead of three full canvas strips. UX#4 — TrainLiveControls (ckpt path / Trigger checkpoint / live LR / Apply lr) is now rendered only when trainInFlight === true. Idle gets the canvas back; the live strip disappears when the run finishes. App.tsx wiring: removed the four canvas strips, threads dimEnv, trainOptions, history, warm-start + splicing callbacks into Sidebar, lifts activeSidebarTab so a chip click in FeatureInjectionBar can deep-link to "rewriters" or "trainops". FeatureInjectionBar: stable EMPTY_APPLIED reference for the applied default — prevents the new useEffect (sync local from prop) from firing infinite re-renders when no applied list is passed. Tests (6 new, 541/541 vitest green): - TrainOpsTab.test.tsx (3) — mounts both panels, edits propagate, warm-start change fires - DimensionsTabEnvEditor.test.tsx (3) — env editor section appears only when dimEnv + onDimEnvApply provided, Apply fires
1 parent 562e109 commit 7bc0b43

7 files changed

Lines changed: 405 additions & 121 deletions

File tree

vbgui/src/App.tsx

Lines changed: 148 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,15 @@ import {
66
} from "@xyflow/react";
77

88
import { FlowCanvas } from "@/components/FlowCanvas";
9-
import { DimEnvEditor } from "@/components/DimEnvEditor";
109
import { layoutFlow } from "@/lib/elk";
1110
import { GalleryTab } from "@/components/GalleryTab";
1211
import { GalleryScaleDownSlider } from "@/components/GalleryScaleDownSlider";
1312
import { FeatureInjectionBar,
1413
type AppliedInjection } from "@/components/FeatureInjectionBar";
1514
import { SweepPanel } from "@/components/SweepPanel";
1615
import { TokenizerMatrixTab } from "@/components/TokenizerMatrixTab";
17-
import { TransplantBar } from "@/components/TransplantBar";
18-
import { InsertIntoEdgeBar } from "@/components/InsertIntoEdgeBar";
19-
import { ParallelComposeBar } from "@/components/ParallelComposeBar";
20-
import { TrainOptionsPanel, type TrainOptions } from "@/components/TrainOptionsPanel";
16+
import { type TrainOptions } from "@/components/TrainOptionsPanel";
2117
import { GenerationPanel } from "@/components/GenerationPanel";
22-
import { RunHistoryPicker } from "@/components/RunHistoryPicker";
2318
import { TrainLiveControls } from "@/components/TrainLiveControls";
2419
import { useGalleryCache } from "@/hooks/useGalleryCache";
2520
import { Palette } from "@/components/Palette";
@@ -326,6 +321,22 @@ export function App(): JSX.Element {
326321

327322
const [wizardPreset, setWizardPreset] = useState<string | null>(null);
328323

324+
// Tab state lifting for right Sidebar
325+
const [activeSidebarTab, setActiveSidebarTab] = useState<import("@/components/Sidebar").SidebarTab>("loss");
326+
// Floating live train controls slide-up visibility state
327+
const [showLivePanel, setShowLivePanel] = useState(false);
328+
329+
useEffect(() => {
330+
if (trainInFlight) {
331+
setShowLivePanel(true);
332+
} else {
333+
const t = setTimeout(() => {
334+
setShowLivePanel(false);
335+
}, 3000);
336+
return () => clearTimeout(t);
337+
}
338+
}, [trainInFlight]);
339+
329340
// Dynamic platform detection and filtering.
330341
const [platformInfo, setPlatformInfo] = useState<{
331342
active_device: string;
@@ -1640,117 +1651,14 @@ export function App(): JSX.Element {
16401651
applied={featureInjections}
16411652
onApply={handleFeatureInjectionApply}
16421653
onRemove={handleFeatureInjectionRemove}
1643-
/>
1644-
<DimEnvEditor value={dimEnv} onApply={setDimEnv} />
1645-
<TrainOptionsPanel
1646-
value={trainOptions}
1647-
onChange={setTrainOptions}
1648-
/>
1649-
<RunHistoryPicker
1650-
history={trainRunHistory}
1651-
selected={selectedWarmStartRunId}
1652-
onSelect={setSelectedWarmStartRunId}
1653-
/>
1654-
<TrainLiveControls
1655-
rpc={rpc}
1656-
trainInFlight={trainInFlight}
1657-
activeRunId={trainRunId}
1658-
onScheduleCheckpoint={(path) =>
1659-
setPendingCheckpointTrigger(path)}
1660-
activeLayoutState={{
1661-
projectName,
1662-
nodes,
1663-
edges,
1664-
spec,
1665-
dimEnv,
1666-
trainOptions,
1667-
}}
1668-
onLoadLayout={(layout: any) => {
1669-
if (layout.projectName) setProjectName(layout.projectName);
1670-
if (layout.nodes) setNodes(layout.nodes);
1671-
if (layout.edges) setEdges(layout.edges);
1672-
if (layout.spec) dispatch({ type: "spec.replace", spec: layout.spec });
1673-
if (layout.dimEnv) setDimEnv(layout.dimEnv);
1674-
if (layout.trainOptions) setTrainOptions(layout.trainOptions);
1675-
}}
1676-
/>
1677-
<ParallelComposeBar
1678-
onCompose={(composeNodes, composeEdges) => {
1679-
setNodes(composeNodes.map((cn) => ({
1680-
id: cn.id,
1681-
type: "brick",
1682-
position: cn.position,
1683-
data: { kind: cn.kind,
1684-
params: cn.params ?? {} } as never,
1685-
})));
1686-
setEdges(composeEdges.map((ce) => ({
1687-
id: `${ce.source}->${ce.target}`,
1688-
source: ce.source,
1689-
target: ce.target,
1690-
data: { severity: "info" },
1691-
})));
1692-
// Rebind head_outputs to the join's downstream
1693-
// norm so verify_build_spec finds the loss head.
1694-
const last = composeNodes[composeNodes.length - 1];
1695-
if (last) {
1696-
dispatch({ type: "loss.set", loss: {
1697-
...spec.loss,
1698-
head_outputs: [last.id],
1699-
}});
1654+
onChipClick={(name) => {
1655+
if (name.toLowerCase().includes("mtp") || name.toLowerCase().includes("rewriter")) {
1656+
setActiveSidebarTab("rewriters");
1657+
} else {
1658+
setActiveSidebarTab("trainops");
17001659
}
17011660
}}
17021661
/>
1703-
<InsertIntoEdgeBar
1704-
edges={edges.map((e) => ({
1705-
source: e.source, target: e.target,
1706-
}))}
1707-
onInsert={(kind, edge) => {
1708-
const baseName =
1709-
`${kind}_insert_${nodes.length}`;
1710-
// Layout: midpoint between src & dst nodes.
1711-
const src = nodes.find((n) => n.id === edge.source);
1712-
const dst = nodes.find((n) => n.id === edge.target);
1713-
const mid = src && dst
1714-
? { x: (src.position.x + dst.position.x) / 2,
1715-
y: (src.position.y + dst.position.y) / 2 + 50 }
1716-
: { x: 200, y: 280 };
1717-
setNodes((prev) => [
1718-
...prev,
1719-
{ id: baseName,
1720-
type: "brick",
1721-
position: mid,
1722-
data: { kind } as never },
1723-
]);
1724-
setEdges((prev) => [
1725-
...prev.filter((e) =>
1726-
!(e.source === edge.source
1727-
&& e.target === edge.target)),
1728-
{ id: `${edge.source}->${baseName}`,
1729-
source: edge.source, target: baseName,
1730-
data: { severity: "info" } },
1731-
{ id: `${baseName}->${edge.target}`,
1732-
source: baseName, target: edge.target,
1733-
data: { severity: "info" } },
1734-
]);
1735-
}}
1736-
/>
1737-
<TransplantBar
1738-
rpc={rpc}
1739-
presets={PRESETS}
1740-
onTransplant={(kind, params) => {
1741-
const baseName = `${kind}_xplant_${nodes.length}`;
1742-
setNodes((prev) => [
1743-
...prev,
1744-
{
1745-
id: baseName,
1746-
type: "brick",
1747-
position: { x: 60 + (prev.length * 40) % 600,
1748-
y: 280 },
1749-
data: { kind, params } as never,
1750-
},
1751-
]);
1752-
}}
1753-
/>
17541662
{spec.gotchas.some(
17551663
(g) => g.id === "v7_f56b_dim_env_mismatch") && (
17561664
<div
@@ -1931,6 +1839,50 @@ export function App(): JSX.Element {
19311839
]);
19321840
}}
19331841
/>
1842+
{showLivePanel && (
1843+
<div
1844+
data-testid="live-train-controls-floating"
1845+
style={{
1846+
position: "absolute",
1847+
bottom: 24,
1848+
left: "50%",
1849+
transform: "translateX(-50%)",
1850+
zIndex: 100,
1851+
background: "rgba(15, 23, 42, 0.92)",
1852+
backdropFilter: "blur(16px)",
1853+
border: "1px solid rgba(255, 255, 255, 0.15)",
1854+
borderRadius: 8,
1855+
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.6)",
1856+
padding: "8px 16px",
1857+
width: "fit-content",
1858+
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
1859+
}}
1860+
>
1861+
<TrainLiveControls
1862+
rpc={rpc}
1863+
trainInFlight={trainInFlight}
1864+
activeRunId={trainRunId}
1865+
onScheduleCheckpoint={(path) =>
1866+
setPendingCheckpointTrigger(path)}
1867+
activeLayoutState={{
1868+
projectName,
1869+
nodes,
1870+
edges,
1871+
spec,
1872+
dimEnv,
1873+
trainOptions,
1874+
}}
1875+
onLoadLayout={(layout: any) => {
1876+
if (layout.projectName) setProjectName(layout.projectName);
1877+
if (layout.nodes) setNodes(layout.nodes);
1878+
if (layout.edges) setEdges(layout.edges);
1879+
if (layout.spec) dispatch({ type: "spec.replace", spec: layout.spec });
1880+
if (layout.dimEnv) setDimEnv(layout.dimEnv);
1881+
if (layout.trainOptions) setTrainOptions(layout.trainOptions);
1882+
}}
1883+
/>
1884+
</div>
1885+
)}
19341886
</div>
19351887
{selectedBrickId && (() => {
19361888
const selected = nodes.find((n) => n.id === selectedBrickId);
@@ -2145,6 +2097,88 @@ export function App(): JSX.Element {
21452097
max_steps: 4,
21462098
});
21472099
}}
2100+
// Lifted active tab state
2101+
activeTab={activeSidebarTab}
2102+
onTabChange={setActiveSidebarTab}
2103+
// dim_env
2104+
dimEnv={dimEnv}
2105+
onDimEnvApply={setDimEnv}
2106+
// Train options
2107+
trainOptions={trainOptions}
2108+
onTrainOptionsChange={setTrainOptions}
2109+
trainRunHistory={trainRunHistory}
2110+
selectedWarmStartRunId={selectedWarmStartRunId}
2111+
onWarmStartSelect={setSelectedWarmStartRunId}
2112+
// Canvas splicing callbacks
2113+
onParallelCompose={(composeNodes, composeEdges) => {
2114+
setNodes(composeNodes.map((cn) => ({
2115+
id: cn.id,
2116+
type: "brick",
2117+
position: cn.position,
2118+
data: {
2119+
kind: cn.kind,
2120+
params: cn.params ?? {},
2121+
} as never,
2122+
})));
2123+
setEdges(composeEdges.map((ce) => ({
2124+
id: `${ce.source}->${ce.target}`,
2125+
source: ce.source,
2126+
target: ce.target,
2127+
data: { severity: "info" } as never,
2128+
})));
2129+
const last = composeNodes[composeNodes.length - 1];
2130+
if (last) {
2131+
dispatch({ type: "loss.set", loss: {
2132+
...spec.loss,
2133+
head_outputs: [last.id],
2134+
}});
2135+
}
2136+
}}
2137+
onInsertIntoEdge={(kind, selectedEdge) => {
2138+
const baseName = `${kind}_splice_${nodes.length}`;
2139+
// Midpoint layout:
2140+
const srcNode = nodes.find((n) => n.id === selectedEdge.source);
2141+
const dstNode = nodes.find((n) => n.id === selectedEdge.target);
2142+
const mid = srcNode && dstNode
2143+
? { x: (srcNode.position.x + dstNode.position.x) / 2,
2144+
y: (srcNode.position.y + dstNode.position.y) / 2 + 50 }
2145+
: { x: 500, y: 250 };
2146+
setNodes((prev) => [
2147+
...prev,
2148+
{
2149+
id: baseName,
2150+
type: "brick",
2151+
position: mid,
2152+
data: { kind } as never,
2153+
},
2154+
]);
2155+
setEdges((prev) => [
2156+
...prev.filter((e) => e.id !== `${selectedEdge.source}->${selectedEdge.target}`),
2157+
{
2158+
id: `${selectedEdge.source}->${baseName}`,
2159+
source: selectedEdge.source, target: baseName,
2160+
data: { severity: "info" } as never,
2161+
},
2162+
{
2163+
id: `${baseName}->${selectedEdge.target}`,
2164+
source: baseName, target: selectedEdge.target,
2165+
data: { severity: "info" } as never,
2166+
},
2167+
]);
2168+
}}
2169+
onTransplant={(kind, params) => {
2170+
const baseName = `${kind}_xplant_${nodes.length}`;
2171+
setNodes((prev) => [
2172+
...prev,
2173+
{
2174+
id: baseName,
2175+
type: "brick",
2176+
position: { x: 60 + (prev.length * 40) % 600,
2177+
y: 280 },
2178+
data: { kind, params } as never,
2179+
},
2180+
]);
2181+
}}
21482182
/>
21492183
</>
21502184
)}

vbgui/src/components/FeatureInjectionBar.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,29 @@ export interface FeatureInjectionBarProps {
3333
/** Optional list of already-applied injections; the bar uses this
3434
* to populate the applied-list display when the parent persists. */
3535
applied?: AppliedInjection[];
36+
/** Optional click callback. Invoked when user clicks the chip body,
37+
* useful to route sidebar tab changes. */
38+
onChipClick?: (name: string) => void;
3639
/** Optional remove callback. Called with the last-applied injection
3740
* matching the chip the user clicked × on. Parent is responsible
3841
* for popping the matching rewriter / brick. */
3942
onRemove?: (injection: AppliedInjection) => void;
4043
}
4144

45+
const EMPTY_APPLIED: AppliedInjection[] = [];
46+
4247
export function FeatureInjectionBar({
43-
rpc, onApply, applied = [], onRemove,
48+
rpc, onApply, applied = EMPTY_APPLIED, onRemove, onChipClick,
4449
}: FeatureInjectionBarProps): JSX.Element {
4550
const [options, setOptions] = useState<CatalogOption[]>([]);
4651
const [selected, setSelected] = useState<string>("");
4752
const [err, setErr] = useState<string | null>(null);
4853
const [local, setLocal] = useState<AppliedInjection[]>(applied);
4954

55+
useEffect(() => {
56+
setLocal(applied);
57+
}, [applied]);
58+
5059
useEffect(() => {
5160
let cancelled = false;
5261
(async () => {
@@ -158,11 +167,13 @@ export function FeatureInjectionBar({
158167
) : chipEntries.map(([name, { count }]) => (
159168
<span key={name}
160169
data-testid={`feature-injection-chip-${name}`}
170+
onClick={() => onChipClick?.(name)}
161171
style={{ display: "inline-flex", alignItems: "center",
162172
gap: 4, padding: "1px 4px 1px 6px",
163173
background: T.surface3,
164174
border: `1px solid ${T.border}`,
165-
borderRadius: 10, fontSize: 11, color: T.text }}>
175+
borderRadius: 10, fontSize: 11, color: T.text,
176+
cursor: onChipClick ? "pointer" : "default" }}>
166177
<span>{name}</span>
167178
{count > 1 && (
168179
<span data-testid={`feature-injection-chip-${name}-count`}
@@ -171,7 +182,7 @@ export function FeatureInjectionBar({
171182
</span>
172183
)}
173184
<button data-testid={`feature-injection-chip-${name}-remove`}
174-
onClick={() => removeOne(name)}
185+
onClick={(e) => { e.stopPropagation(); removeOne(name); }}
175186
title={count > 1 ? `remove one ${name}` : `remove ${name}`}
176187
style={{ background: "transparent", border: "none",
177188
color: T.textSecondary, cursor: "pointer",

0 commit comments

Comments
 (0)