/** * Console — the FIVE real dock drawers (operator dock restructure). Each renderer * takes (ctx, depth); what shows is gated by depth (condensed | expanded): * * learn — Learning : feedback-mode selector, solo/arm chooser, live params * inputs — Inputs : input source * route — Outputs : per-output control matrix for the ACTIVE mode/backend, * a Mode-specific config section, and (Editor mode) the * MEMLNaut serial panel. The old separate "Synth" and * "Particle/Visual" drawers are REMOVED — their config now * lives here under the active Mode (TOP dock selector). * settings — Settings : icon style, input-map shape + feature flags * help — Help : keymap + the loop explanation * * The TOP dock selector ("Mode") chooses the active OUTPUT backend/target; this * drawer renders whatever that backend needs. * * Engine wiring: the feedback-mode pill → controller.setMode; the arm flags → * engine.feedback.setFocus; per-output rows write the shared MFParam store. * Where engine support does not yet exist the UI + state are wired and a TODO * references the relevant spec — no faked engine behaviour. */ 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'; import { OutputControlRow } from '../dock/OutputControlRow'; import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig'; import { shapeValues } from './model'; 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 { LearningIcon, InputsIcon, OutputsIcon, SettingsIcon, HelpIcon, } from './icons'; function Chip({ children, tone }: { children: ReactNode; tone?: string }) { return ( {children} ); } function SectionLabel({ children }: { children: ReactNode }) { return (
{children}
); } /** A 2+ segment selector pill that drives a typed value. */ function Segmented({ value, onChange, options, }: { value: T; onChange: (v: T) => void; options: { value: T; label: string }[]; }) { return (
{options.map((o) => { const on = value === o.value; return ( ); })}
); } // =========================================================================== // 1. LEARNING-BEHAVIOUR (dock-spec §1; rl-feedback-design) // =========================================================================== const FEEDBACK_OPTS: { value: FeedbackModeUI; label: string }[] = [ { value: 'geometric-dislike', label: 'Push away' }, { value: 'explore-and-place', label: 'Explore & place' }, ]; const FEEDBACK_DESC: Record = { 'geometric-dislike': 'Down carves the current sound away from what you like — directed repulsion (Mode 1).', 'explore-and-place': 'Down re-rolls the whole net into a scratchpad you audition; + places a liked sound (Mode 2).', }; /** * Solo behaviour. `mask-gradients` is the only variant the core actually * implements — `soloMode` has no other observable effect today (the other two * options need the C API's `train_masked` step). Rendered as a fixed, * non-selectable label rather than a picker that offers dead choices * (simplification audit L20). */ const SOLO_OPTS: { value: SoloMode; label: string }[] = [ { value: 'mask-gradients', label: 'Mask gradients' }, { value: 'zero-loss', label: 'Zero loss' }, { value: 'dont-care', label: "Don't-care mask" }, ]; const SOLO_DESC: Record = { 'mask-gradients': 'Column-freeze (default) — only the armed output moves; the rest stay bit-identical.', 'zero-loss': 'Expressive, but armed and unarmed outputs share hidden weights, so others can drift.', '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 ( <>
{FEEDBACK_OPTS.find((o) => o.value === ctx.feedbackMode)?.label} arm: {ctx.armedCount ? `${ctx.armedCount} output${ctx.armedCount > 1 ? 's' : ''}` : 'all'} {ctx.exploring && exploring…} {ctx.learningPaused && learning paused}
Down action · feedback mode {depth === 'expanded' && (

{FEEDBACK_DESC[ctx.feedbackMode]}

)} Exploration
hold to morph the whole net live · release to freeze
{depth === 'expanded' && (

Explore adds a slow random walk (Ornstein-Uhlenbeck) on the outputs so the sound roams; likes and dislikes registered mid-wander steer the net toward what you want. 0 = off. Jolt (firmware TogB1) and Explore (RVX1) drive the same core gestures as the MEMLNaut hardware.

)} Recorded examples
{ctx.datasetCount} example{ctx.datasetCount === 1 ? '' : 's'} forget every example & wipe the on-map marks
Current model {depth === 'expanded' && ( <> Solo / arm scope
{ctx.armedCount ? `${ctx.armedCount} armed — arm with the S button on each output row` : 'every live output learns'}
Solo behaviour {SOLO_OPTS.find((o) => o.value === ctx.soloMode)?.label}

{SOLO_DESC[ctx.soloMode]} Solo only freezes the rest as far as a shared network allows. The other two behaviours need real core support (`train_masked`) and aren't selectable yet.

Live training params )} {depth === 'expanded' && ( <> Feedback lab

State machine: idle → exploring → commit / cancel. While Explore & place is exploring, training is paused and the joystick auditions a random scratchpad net; + commits a placed anchor and restores the real net.

{ctx.xavierSpreadEnabled && ( )} Training health )} ); } // =========================================================================== // 2. INPUTS (workstream F; inputs-spec — modular input layer) // =========================================================================== const STATUS_TONE: Record = { ready: 'var(--accent)', connecting: 'var(--warn)', unavailable: 'var(--fg-dim)', error: 'var(--danger)', idle: 'var(--fg-dim)', }; const INPUT_MODE_OPTS: { value: InputMode; label: string }[] = [ { value: 'internal', label: 'Internal' }, { value: 'gamepad', label: 'Game Controller' }, { value: 'midi', label: 'MIDI' }, ]; /** Standard-mapping gamepad button → verdict legend (mirrors ConsoleApp). */ const GAMEPAD_LEGEND: { btn: string; action: string }[] = [ { btn: 'RB', action: 'Up · positive feedback' }, { btn: 'LB', action: 'Down · negative feedback' }, { btn: 'X', action: 'Randomise' }, { btn: 'Y', action: 'Nudge' }, { btn: 'B', action: 'Undo' }, { btn: 'A (hold)', action: 'Reposition — hold, move stick, release to place' }, ]; /** * Read-only INPUT meter — shows a learned MIDI control's live value. Deliberately * styled apart from the output Sliders (which are orange, interactive thumbs): * these are inset bars on the secondary accent with an "in" tag, so the user can * see at a glance that these feed the net rather than being driven by it. */ function MidiInputMeter({ label, value, onClear }: { label: string; value: number; onClear: () => void }) { const pct = Math.max(0, Math.min(1, value)); return (
{label}
{value.toFixed(2)}
); } function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { const inp = ctx.inputs; // The net is runtime-shaped (P2): each active axis drives its OWN input slot // 1:1. A mismatch means the net is over-provisioned (axes < slots, extra slots // zero-padded) or over capacity (axes > slots, extras dropped) — the reshape // offer resolves either. const inputMismatch = inp.axisCount > 0 && inp.axisCount !== inp.engineInputSize; const active = inp.sources.find((s) => s.enabled); const modeLabel = INPUT_MODE_OPTS.find((o) => o.value === inp.inputMode)?.label ?? 'Internal'; return ( <>
{modeLabel} {inp.inputMode !== 'internal' && {inp.axisCount} axes} {inputMismatch && ( {inp.axisCount > inp.engineInputSize ? `${inp.axisCount} axes › ${inp.engineInputSize} slots` : `net: ${inp.engineInputSize}-D`} )} {active && ( {active.status.state} )}
Input source {active && depth === 'expanded' && ( {active.status.message} )} {/* ---- Internal (XY pad / manifold) ---- */} {inp.inputMode === 'internal' && depth === 'expanded' && (

Drag the on-screen manifold / XY pad. Two axes feed the net directly — this is the default.

)} {/* ---- Game Controller ---- */} {inp.inputMode === 'gamepad' && depth === 'expanded' && ( <> Sticks Buttons
{GAMEPAD_LEGEND.map((g) => (
{g.btn} {g.action}
))}

Connect a controller and press any button to wake it. Sticks drive the input map; buttons fire the verdicts above.

)} {/* ---- MIDI ---- */} {inp.inputMode === 'midi' && depth === 'expanded' && ( <> Device {inp.midiInputs.length === 0 ? (

No MIDI inputs detected. Connect a device — it appears here automatically.

) : (
{inp.midiInputs.map((p) => ( ))}
)} MIDI Learn {inp.midiLearnArmed ? (
Move all of the controls you want to use, then click Done. Each knob or fader you touch becomes an input.
) : (
{inp.midiBindings.length > 0 && ( )}
)} {inp.midiBindings.length > 0 && ( <> Learned controls
{inp.midiBindings.map((b, i) => ( inp.clearMidiBinding(i)} /> ))}
)} )} {/* ---- Dedicated-dimensions note (only with >2 active axes) ---- */} {inp.axisCount > 2 && depth === 'expanded' && (

The net has {inp.engineInputSize} dedicated inputs — each active axis drives its own dimension 1:1 (no blending). {inputMismatch ? ` Its ${inp.engineInputSize} slots don't match the ${inp.axisCount} active axes; ` + `changing the layout offers a reshape to ${inp.axisCount} inputs (warm-started).` : ''}

)} ); } // =========================================================================== // 3. OUTPUTS / ROUTING (dock-spec §3, §4) // =========================================================================== const VISUAL_NAMES = [ 'Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate', ]; /** * Per-Mode config section shown ABOVE the per-output rows. The synth Mode shows * transport + tempo; the particle Mode names the outputs; MIDI/OSC/Editor show * their own affordances. Replaces the removed Synth + Visual drawers. */ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) { switch (ctx.outputMode) { case 'synth': return ( <> Transport
audio starts on the play gesture
{depth === 'expanded' && (

The active engine follows the selected mode ({ctx.mode.label}). {/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the 18-section group-override matrix are workstream E. Master volume + bpm sliders were deleted (simplification audit S16) — they drove no engine parameter. */}

)} ); case 'editor': return ( <> MEMLNaut · USB serial ); case 'particles': return depth === 'expanded' ? (

Flow-field visualiser driven by the first {Math.min(20, ctx.params.length)} outputs (no audio). {/* TODO(backends-spec §4): port FlowFieldVisualizer + visual preset chips. */}

) : null; case 'midi': // The full MIDI config (port picker, CC count, per-output CC/channel/name) // + preset bar render via OutputsBackendConfig in RoutingDrawer below. return depth === 'expanded' ? (

Each output sends a real Web MIDI CC. Pick a port and set CC# / channel per output.

) : null; case 'osc': return depth === 'expanded' ? (

Each output sends to an OSC path with a physical range, over the WebSocket bridge.

) : null; default: return null; } } function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { const activeParams = ctx.params.slice(0, ctx.displayOutputCount); const values = shapeValues(activeParams, null); // bar uses shaped held/live value snapshot const counts = activeParams.reduce>((a, p) => { a[p.status] = (a[p.status] || 0) + 1; return a; }, {}); const mutedN = activeParams.filter((p) => p.muted).length; const modeDesc = outputModeDescriptor(ctx.outputMode); // The particle Mode names its outputs; otherwise use the param names. const nameFor = (idx: number, fallback: string) => ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback; const expanded = depth === 'expanded'; const rows = expanded ? activeParams : activeParams.slice(0, 6); return ( <>
{modeDesc.label} {activeParams.length} output{activeParams.length === 1 ? '' : 's'} live {counts.live || 0} fixed {counts.fixed || 0} off {counts.off || 0} muted {mutedN}
{ModeConfig(ctx, depth)} {/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */} {expanded && } Outputs · M mute · S arm · off/fixed/live
{rows.map((p) => { const i = ctx.params.indexOf(p); const labelled = { ...p, name: nameFor(i, p.name) }; return ( ctx.setParam(i, patch)} onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined} showCurve={expanded} /> ); })}
{!expanded && activeParams.length > 6 && ( +{activeParams.length - 6} more — expand to edit )} ); } // =========================================================================== // 4. SETTINGS (operator dock restructure — settings-store) // =========================================================================== const ICON_COLOUR_OPTS: { value: UnfocusedIconColour; label: string }[] = [ { value: 'off-white', label: 'Off-white' }, { value: 'white', label: 'White' }, { value: 'orange', label: 'Orange' }, ]; const INPUT_MAP_OPTS: { value: InputMapMode; label: string }[] = [ { value: 'follow-mode', label: 'Follow mode' }, { value: 'rectangular', label: 'Rectangular' }, { value: 'circular', label: 'Circular' }, ]; const NETWORK_RESIZE_OPTS: { value: NetworkResizePolicy; label: string }[] = [ { value: 'capacity', label: 'Keep capacity' }, { value: 'exact', label: 'Exact I/O' }, ]; const EXAMPLE_RESIZE_OPTS: { value: ExampleResizePolicy; label: string }[] = [ { value: 'adapt', label: 'Adapt' }, { value: 'clear', label: 'Clear' }, ]; function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) { const { settings, set } = useSettings(); return ( <> Icons set('monochromeIcons', v)} label="Monochrome icons" /> {depth === 'expanded' && ( <>
Unfocused icon colour
set('unfocusedIconColour', v as UnfocusedIconColour)} options={ICON_COLOUR_OPTS} />

Focused / active icons are always accent orange. This sets the resting colour of unfocused icons (preview below).

)} Input map set('inputMap', v as InputMapMode)} options={INPUT_MAP_OPTS} /> {depth === 'expanded' && (

The 2D input surface: a rectangular XY map or a circular joystick-style disc. "Follow mode" uses the active mode's declared input (joystick → circular, else rectangular).

)} I/O editing
Network size
set('networkResizePolicy', v as NetworkResizePolicy)} options={NETWORK_RESIZE_OPTS} ariaLabel="Network resize policy" />
Existing examples
set('exampleResizePolicy', v as ExampleResizePolicy)} options={EXAMPLE_RESIZE_OPTS} ariaLabel="Example resize policy" /> {depth === 'expanded' && ( <>

Keep capacity edits mappings in place and reconstructs only when the active cards outgrow the network. Exact I/O keeps network arity equal to the cards. Surviving dimensions retain their identity and weights; exploration scratch state resets after an I/O edit.

set('addedInputExampleValue', v)} /> set('addedOutputExampleValue', v)} /> )} Chrome set('cornerRadius', Math.round(v))} /> {depth === 'expanded' && (

Roundness of buttons, control rows, dock icons and panels. Pills and the circular verdict buttons are intentionally exempt. Default 2px.

)} Experimental features set('xavierSpreadEnabled', v)} label="Xavier / spread randomisation" /> {depth === 'expanded' && (

Off by default: new networks and re-rolls use full-range uniform weights for broad, strongly varied mappings. Enable this to restore the legacy centred regime and its Learning-drawer switch.

)} ); } // =========================================================================== // 5. HELP // =========================================================================== const KEYS: [string, string][] = [ ['1', 'down − / explore'], ['2', 'commit +'], ['space / ↑', 'commit +'], ['↓', 'down − / explore'], ['3–5', 'open drawers'], ['\\', 'expand drawer'], ['z', 'undo'], ['[ ] =', 'split (composite)'], ['dbl-click mark', 'follow mouse (Esc exits)'], ]; function HelpDrawer() { return ( <> Keyboard
{KEYS.map(([k, v]) => (
{k} {v}
))}
The loop

Drag the manifold to explore. Hear something good → + to keep it. Wrong → − to push away or re-roll (set the behaviour in the Learning drawer). Went too far → undo. The dock reveals exactly as much machinery as you reach for.

Learn more ▶ Watch the explainers + interactive demos ); } export interface DrawerSection { /** Monochrome inline-SVG icon (currentColor-driven by the dock button). */ icon: ReactNode; /** Prior colour-emoji glyph, used when monochrome icons are OFF. */ glyph: string; label: string; render: (ctx: ConsoleCtx, depth: DrawerDepth) => ReactNode; } export const DRAWERS: Record = { learn: { icon: , glyph: '🧠', label: 'Learning', render: LearningDrawer }, inputs: { icon: , glyph: '🎚', label: 'Inputs', render: InputsDrawer }, route: { icon: , glyph: '🔀', label: 'Outputs', render: RoutingDrawer }, settings: { icon: , glyph: '⚙', label: 'Settings', render: (c, d) => }, help: { icon: , glyph: '?', label: 'Help', render: HelpDrawer }, };