From 4db0f498ebd97172b35a8d08d7c62d6c85a75e29 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sat, 25 Jul 2026 15:24:50 +0200 Subject: [PATCH] feat(manifold): show live model architecture in docks --- MAP.md | 5 +- manifold/ONBOARDING.md | 8 +- manifold/src/console/Drawers.tsx | 123 +++++++++++++++++++++++++++++- manifold/src/engine/engine-api.ts | 8 ++ 4 files changed, 137 insertions(+), 7 deletions(-) diff --git a/MAP.md b/MAP.md index 06abc5d..b5fa53c 100644 --- a/MAP.md +++ b/MAP.md @@ -40,7 +40,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. `worklet/nisps-processor.ts` (audio), thin WASM wrappers over the core pipelines + curve catalog (the TS `input-pipeline`/`output-pipeline`/`curves` implementations died at P4), `wasm-worker.ts`, `spine.ts` (the reactive spine BELOW React — `setInput` derives processed→ml→routed eagerly off-render), - `engine-api.ts` (`EngineApi` façade incl. `feedback.*` wrappers over the `nisps_ml_feedback_*` C ABI), + `engine-api.ts` (`EngineApi` façade incl. live architecture/weight/example metrics and `feedback.*` wrappers over the `nisps_ml_feedback_*` C ABI), `EngineProvider.tsx`/`useEngine.ts` (React binding via `useSyncExternalStore` version counter). nisps.js is loaded via fetch+indirect-eval (Emscripten MODULARIZE glue has no ES exports), base-aware via `document.baseURI` for the `/next` sub-path. @@ -48,7 +48,8 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. - `manifold/src/console/` — the convertible Console: `ConsoleApp`, `CompositeStage` (single-divider convertible with snap/magnetism/minimap-demotion), `OutputStage`/`SandwichStage`/`ParticleStage`/`Manifold` (canvas, rect↔circular + feedback markers), `Dock` (top Mode selector + 5 vertically-centred drawers), `Drawers` - (Learning/Inputs/Outputs/Settings/Help), `TrainingHealth` (real per-iteration loss curve from + (Learning/Inputs/Outputs/Settings/Help; Learning includes the live model-architecture inspector and + expanded Outputs owns the remaining scroll height), `TrainingHealth` (real per-iteration loss curve from `nisps_ml_loss_history` + per-layer weight health from `nisps_ml_get_layer_stats`; rendered only at the Learning drawer's `expanded` depth — that IS the advanced-surface flag), `VerdictCluster` (mode-aware), `OutputEditor`/`DualRange`/`CurvePad`, `icons.tsx` diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index 7c4be41..601cdd5 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -146,7 +146,8 @@ for the narrow pane. icons, vertically centred macOS-dock style; **BOTTOM** = sandwich toggle. - Five drawers (`DRAWERS` in `Drawers.tsx`, each has `.render(ctx, depth)` — condensed 360px panel vs expanded 80vw×80vh modal): - - **learn** — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm. + - **learn** — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm, + plus a live model-architecture strip (inputs/outputs highlighted; hidden layers expand on click). At `expanded` depth ONLY it also renders `TrainingHealth.tsx`: the real per-iteration loss curve (`EngineApi.lossHistory()` ← `nisps_ml_loss_history` ← `MLPCore::loss_history`) plus the per-layer weight-health table (`getLayerStats`). **`depth === 'expanded'` is Manifold's @@ -154,7 +155,8 @@ for the narrow pane. surface there rather than inventing one. The panel renders "no training run yet" when the core has no history; it never synthesises a curve. - **inputs** — enable/configure input sources (XY pad / MIDI / gamepad). - - **route** (label "Outputs") — per-output control matrix + per-backend config. + - **route** (label "Outputs") — per-output control matrix + per-backend config; at expanded depth + the output rows fill the remaining drawer height and scroll independently. - **settings** — icon style (monochrome/colour), input-map shape (xy/joystick/rect/circular), corner radius. - **help** — keymap pills + loop explanation. - `src/dock/` holds the output-routing internals used by the `route` drawer: @@ -200,7 +202,7 @@ a setting → `--r-*` tokens. - `engine-api.ts` — **`EngineApi`, the framework-neutral facade** everything in the UI talks to: `setInput/setInputs`, `getOutputs/routedOutput`, training (`addExample/train/trainAsync/evalLoss`), weights (`getWeights/setWeights/process/randomise`), telemetry - (`lossHistory/getLayerStats`), `subscribe/version/on`, plus nested `.feedback` and `.audio` + (`architecture/weightCount/exampleCount/lossHistory/getLayerStats`), `subscribe/version/on`, plus nested `.feedback` and `.audio` facades. **`lossHistory()` reads SPINE STATE, not the MLP handle** — an async train runs on the worker's mirror net, so the main handle's own history is empty for those runs; both paths publish to the spine. diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index 4f6d64f..9bc987e 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -20,7 +20,7 @@ * Where engine support does not yet exist the UI + state are wired and a TODO * references the relevant spec — no faked engine behaviour. */ -import type { ReactNode } from 'react'; +import { useState, type ReactNode } from 'react'; import { Badge, Button, PillToggle, Slider, Switch } from '../primitives'; import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types'; import type { InputMode } from '../inputs'; @@ -31,6 +31,7 @@ import { outputModeDescriptor } from './output-mode'; import { useSettings, unfocusedIconCss } from '../settings/settings-store'; import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store'; import type { ExampleResizePolicy, NetworkResizePolicy } from '../engine/io-reshape'; +import { useEngine, useEngineVersion } from '../engine'; import { EditorPanel } from '../serial/EditorPanel'; import { TrainingHealth } from './TrainingHealth'; import { @@ -157,6 +158,119 @@ const SOLO_DESC: Record = { 'dont-care': 'Each example stores a per-output mask so stale labels never pull unarmed outputs.', }; +function ModelArchitecture() { + const engine = useEngine(); + useEngineVersion(engine); + const [detailsOpen, setDetailsOpen] = useState(false); + + if (!engine) { + return

engine not ready

; + } + + const arch = engine.architecture; + const hidden = arch.hidden.filter((size) => size > 0); + const layerCount = arch.numLayers || hidden.length + 1; + const sizes = hidden.map((size) => size.toString()).join(' → '); + const stageStyle = (tone: string) => ({ + display: 'flex', + flexDirection: 'column' as const, + gap: 2, + minWidth: 0, + padding: '6px 8px', + border: `1px solid ${tone}`, + borderRadius: 'var(--r-1)', + background: 'var(--bg-2)', + }); + const labelStyle = { + fontSize: 9, + fontFamily: 'var(--font-mono)', + letterSpacing: '0.08em', + color: 'var(--fg-dim)', + }; + const valueStyle = { + fontSize: 12, + fontFamily: 'var(--font-mono)', + fontVariantNumeric: 'tabular-nums', + color: 'var(--fg)', + }; + + return ( +
+
+ + {layerCount} layer{layerCount === 1 ? '' : 's'} + + {engine.weightCount.toLocaleString()} weights + {engine.exampleCount} examples +
+
+
+ INPUT + {arch.inputSize} units +
+ +
+ OUTPUT + {arch.outputSize} units +
+
+ + {detailsOpen && ( +
+ {hidden.length > 0 ? ( + hidden.map((size, i) => ( +
+ HIDDEN {i + 1} + {size} units +
+ )) + ) : ( + no hidden layers + )} +
+ )} +
+ ); +} + function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { return ( <> @@ -227,6 +341,9 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { + Current model + + {depth === 'expanded' && ( <> Solo / arm scope @@ -646,7 +763,9 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { display: 'flex', flexDirection: 'column', gap: 4, - maxHeight: expanded ? 460 : 220, + flex: expanded ? 1 : undefined, + minHeight: expanded ? 0 : undefined, + maxHeight: expanded ? undefined : 220, overflow: 'auto', }} > diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts index a6d61df..f775017 100644 --- a/manifold/src/engine/engine-api.ts +++ b/manifold/src/engine/engine-api.ts @@ -404,6 +404,14 @@ export class EngineApi { return this.iml.architecture; } + get weightCount(): number { + return this.iml.weightCount; + } + + get exampleCount(): number { + return this.iml.exampleCount; + } + // ---- Direct handle access (advanced consumers; spine pipelines, etc.) ---- get ml(): WasmIML { return this.iml;