/** * SettingsDrawer — settings panel rendered inside ModeShell's drawer. * * Sections: * - Input: deadzone, curve, smoothing, momentum, anchor mode, invert * - Training: learning rate, weight decay * - Exploration: spread, noise floor/cap/growth/decay, * auto-explore (toggle, interval, intensity) * - Output: global curve, smoothing, slew rate, freeze * - Per-param overrides: ParamEditor list * - Advanced: weight health histogram, gradient flow, session presets * * Sections are collapsible. Per-param-override editor renders inline (also * accessible via double-tap on slider in the live readout — wired in * ModeShell). The drawer is a child of `ModeShell`'s `drawerContent` slot. */ import { Component, createSignal, For, Show, createMemo, createEffect } from 'solid-js'; import { ParamEditor, type ParamOverride as PrimitiveParamOverride } from '../primitives/ParamEditor'; import { Slider } from '../primitives/Slider'; import { PillToggle } from '../primitives/PillToggle'; import { WeightHealth } from '../primitives/WeightHealth'; import { GradientFlow } from '../primitives/GradientFlow'; import { Heatmap } from '../primitives/Heatmap'; import { ProgressRing } from '../primitives/ProgressRing'; import { LossPlot } from '../primitives/LossPlot'; import { inputStore } from '../stores/input-store'; import { outputStore } from '../stores/output-store'; import { explorationStore } from '../stores/exploration-store'; import { modeStore, defaultParamOverride, type ParamOverride } from '../stores/mode-store'; import { sessionStore } from '../stores/session-store'; import { mlStore } from '../stores/ml-store'; import { coreBus } from '../stores/bus'; import { ZOOM_MIN, ZOOM_MAX, DEADZONE_MAX, INPUT_CURVE_MIN, INPUT_CURVE_MAX, SMOOTHING_MAX, } from '../input/pipeline'; import { GLOBAL_CURVE_MIN, GLOBAL_CURVE_MAX } from '../output/pipeline'; import { layerNorms, gradientStatuses, weightHistogram, weightStatus, layerStatsToStatus, layerWeightCounts, } from '../features/weight-health'; import { saveNamedPreset, loadNamedPreset, buildShareUrl } from '../features/session-preset'; import { captureA, toggleAB, acceptB, revertToA } from '../features/snapshots'; import type { ModeRuntime } from './mode-runtime'; import type { ModeSchema } from './generated'; import type { Param } from './generated/types'; import type { CurveName } from '../output/curves'; import type { HeatmapColorMode } from '../features/heatmap-sampler'; import styles from './SettingsDrawer.module.css'; export interface SettingsDrawerProps { schema: ModeSchema; runtime: ModeRuntime; /** Show advanced section (weight health, gradient flow, session presets). */ showAdvanced?: boolean; } interface Section { id: string; title: string; defaultOpen?: boolean; render: () => any; } export const SettingsDrawer: Component = (props) => { const [open, setOpen] = createSignal>({ input: false, training: false, exploration: false, output: false, overrides: false, advanced: false, snapshots: false, }); const toggle = (id: string) => setOpen((s) => ({ ...s, [id]: !s[id] })); // ----- Per-param overrides UI ---------------------------------------- const overrides = () => modeStore.state.overrides[props.schema.mode_id] ?? {}; const getOverride = (param: Param): PrimitiveParamOverride => { const cur = overrides()[param.name]; if (cur) { return { min: cur.min, max: cur.max, curve: cur.curve as CurveName, curveParam: cur.curveParam, muted: cur.muted, pinned: cur.pinned, fixedValue: cur.fixedValue, }; } return { min: param.min, max: param.max, curve: param.curve as CurveName, muted: false, pinned: false, fixedValue: param.default, }; }; const writeOverride = (param: Param, next: PrimitiveParamOverride) => { const stored: ParamOverride = { min: next.min, max: next.max, curve: next.curve, curveParam: next.curveParam, muted: next.muted, pinned: next.pinned, fixedValue: next.fixedValue, // Frozen flag is local to the runtime (set via output-store freezeMask) // — keep it false in the persistent override unless the user explicitly // toggles it. frozen: overrides()[param.name]?.frozen ?? false, }; modeStore.setOverride(props.schema.mode_id, param.name, stored); }; // ----- Per-output health (advanced section) ----------------------------- const [advancedExpanded, setAdvancedExpanded] = createSignal(props.showAdvanced ?? false); const histogram = createMemo(() => { const w = mlStore.weights(); return weightHistogram(w); }); const status = createMemo(() => weightStatus(mlStore.weights())); // Gradient flow: capture before/after weights on training events. const [normHistory, setNormHistory] = createSignal([]); const [gradStatuses, setGradStatuses] = createSignal>([]); let lastBeforeWeights: Float32Array | null = null; const offTrainStart = coreBus.on('snap.push', (e) => { if (e.tag === 'before train' && mlStore.iml) { lastBeforeWeights = mlStore.getWeights(); } }); const offTrainEnd = coreBus.on('ml.trained', () => { if (lastBeforeWeights && mlStore.iml) { const after = mlStore.getWeights(); const arch = mlStore.iml.architecture; const sizes = layerWeightCounts({ inputSize: arch.inputSize, hidden: arch.hidden, outputSize: arch.outputSize, }); const norms = layerNorms(lastBeforeWeights, after, sizes); setNormHistory(norms); setGradStatuses(gradientStatuses(norms)); lastBeforeWeights = null; } }); // Session-preset save / share UI state const [presetName, setPresetName] = createSignal(''); const [shareUrl, setShareUrl] = createSignal(''); // Snapshot list (long-press-undo equivalent) const snapshots = () => sessionStore.listSnapshots(); // Cleanup on unmount (Solid handles via owner; we only need to detach bus // subs explicitly because they live across renders). // We can't use onCleanup here because this component is recreated per-mode, // but bus.on returns an unsubscribe. Defer cleanup to when the component // unmounts via createEffect cleanup function: createEffect(() => { return () => { offTrainStart(); offTrainEnd(); }; }); // ----- A/B compare visibility ------------------------------------------ const ab = () => sessionStore.state.ab; const live = () => ab().live; // --------------------------------------------------------------------- // Render helpers // --------------------------------------------------------------------- const SectionHeader = (h: { id: string; title: string; count?: number }) => ( ); return (
{/* Snapshots / undo / A/B */}
0}>
    {(s) => (
  • )}

A / B compare

active: {live()} }> no capture yet
{/* Input pipeline */}
inputStore.setZoom(v)} /> inputStore.setDeadzone(v)} /> inputStore.setInputCurve(v)} /> inputStore.setSmoothing(v)} />
inputStore.config.anchorMode} onChange={(v) => inputStore.setAnchorMode(v as 'auto' | 'sticky' | 'center')} ariaLabel="Anchor mode" />
inputStore.config.momentumZoom} onChange={(v) => inputStore.setMomentumZoom(v as 'off' | 'gentle' | 'strong')} ariaLabel="Momentum zoom" />
{/* Training */}
explorationStore.setLearningRate(v)} /> explorationStore.setWeightDecay(v)} /> mlStore.state.lossHistory} width={320} height={70} />
{/* Exploration */}
explorationStore.setSpread(v)} /> explorationStore.setNoiseFloor(v)} /> explorationStore.setNoiseCap(v)} /> explorationStore.setNoiseGrowth(v)} /> explorationStore.setNoiseDecay(v)} />

Auto-explore

1} ariaLabel="Auto-explore active" />
explorationStore.setAutoExploreInterval(v)} /> explorationStore.setAutoExploreIntensity(v)} />
{/* Output */}
outputStore.setGlobalCurve(v)} /> outputStore.setSmoothing(v)} /> outputStore.setSlewRate(v)} />
{/* Per-param overrides */}
{(p) => ( getOverride(p)} onChange={(next) => writeOverride(p, next)} compact /> )}
{/* Advanced: weight health, gradient flow, heatmap */}

Weight health

Gradient flow (last train)

0} fallback={

No training run yet.

} >

Per-layer stats

{(stats, idx) => (
L{idx()} mean|w| {stats.meanAbs.toFixed(3)} · max|w| {stats.maxAbs.toFixed(3)} · {(stats.deadFrac * 100).toFixed(0)}% dead · {(stats.saturatingFrac * 100).toFixed(0)}% sat {layerStatsToStatus(stats)}
)}

Input heatmap

props.runtime.heatmap.colorMode()} onChange={(v) => props.runtime.heatmap.setColorMode(v as HeatmapColorMode)} ariaLabel="Heatmap color mode" />

Session preset

setPresetName(e.currentTarget.value)} />
0}>
    {(p) => (
  • {p.name}
  • )}
e.currentTarget.select()} />
); }; export default SettingsDrawer;