refactor(manifold): delete the dead console UI stratum

Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half).

- S15: the four-way focus/altitude system. setFocus was never called anywhere,
  so only the 'composite' branch was reachable. Deleted SplitStage,
  ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop,
  the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters
  kept; engine.feedback.setFocus (a different, live thing) untouched.
- S16 + L1: the decorative stratum that rendered real-looking controls driving
  nothing — A/B machinery, the fake seed, seededGradient + weightsRevision,
  snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel
  sliders with their Drawers rows. Each was confirmed self-referential first.
  NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside
  an active explore-and-place session used to pop a UI-only snapshot that
  restored noiseCap/seed. It is now simply inactive unless a genuine
  core-backed scratchpad undo exists. Geometric-dislike mode never had a real
  undo primitive in the core, so only the fake path is gone.
- S18: BackendAdvanced.tsx and its Drawers block. It self-described as a
  duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the
  same expanded drawer. OutputsBackendConfig already covers every backend.
- S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig
  actually read; deleted the Axes type + axes/setAxis and the
  preset/setPreset/offsetActive chain (permanently 'Sculpt'/false).
  KEPT ctx.modes and ctx.setModeId despite having no reader today — the
  Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing.
- L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot,
  Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS.
- L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS
  catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES,
  the surviving single catalogue.
- L20: the solo-mode selector's two unimplemented options no longer pretend to
  be selectable.
- L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six
  ControllerEngine members nothing called (the finding named three; the other
  three are used on the real EngineApi by debug/probe.ts, a different
  interface, so removing them from ControllerEngine is safe).

Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
This commit is contained in:
monkey-w1n5t0n 2026-07-21 12:49:25 +02:00
parent c98d25c255
commit 9b686eb312
23 changed files with 95 additions and 1902 deletions

View file

@ -1,8 +1,9 @@
/** /**
* Manifold app root. The convertible Console (ConsoleApp) wired to the real * Manifold app root. The convertible Console (ConsoleApp) wired to the real
* engine, mounted under EngineProvider. Defaults to the hero `focus="composite"` * engine, mounted under EngineProvider. ConsoleApp renders its single
* (the convertible centerpiece). The `?debug=1` probe is installed once the * "composite" stage unconditionally (the dead focus/altitude system was
* engine is live. * deleted 2026-07 simplification audit S15). The `?debug=1` probe is
* installed once the engine is live.
*/ */
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -75,7 +76,7 @@ export function App() {
return ( return (
<EngineProvider options={engineOptions()} fallback={<Loading />}> <EngineProvider options={engineOptions()} fallback={<Loading />}>
<ProbeInstaller /> <ProbeInstaller />
<ConsoleApp focus="composite" /> <ConsoleApp />
</EngineProvider> </EngineProvider>
); );
} }

View file

@ -14,46 +14,46 @@
* FeedbackMode::ExploreAndPlace (set on mount; the controller forwards the * FeedbackMode::ExploreAndPlace (set on mount; the controller forwards the
* IdleExploringPlacing lifecycle to engine.feedback.* nisps/ml/feedback.hpp, * IdleExploringPlacing lifecycle to engine.feedback.* nisps/ml/feedback.hpp,
* per docs/adr/rl-feedback-design.md). * per docs/adr/rl-feedback-design.md).
* - AltitudeNav switches `focus` via React state (in|split|out|composite), not
* by navigating to separate HTML files.
* - `c15` is labelled "Powerful Synth Engine" (in model.ts) "C15" never shows. * - `c15` is labelled "Powerful Synth Engine" (in model.ts) "C15" never shows.
* *
* UI-only state (params status/min/max/curve, snapshots, A/B seed, axes, * The dead focus/altitude system (AltitudeNav; SplitStage/ReadoutStrip/InputMini
* noiseCap, health/rev visuals) is preserved as faithful local React state. * stages; the `in`/`split`/`out` branches) was deleted 2026-07 (simplification
* audit S15) `setFocus` was never called anywhere, so `composite` was the
* only reachable stage. The decorative A/B toggle, fake seed, snapshot stack,
* fabricated health/gradient visuals, and the never-touched master
* volume/bpm/learning-rate/decay/spread-level sliders were deleted alongside it
* (S16/L1) each was write-only, read by nothing but its own control.
*
* UI-only state (params status/min/max/curve, noiseCap) is preserved as
* faithful local React state.
*/ */
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useEngine, useEngineVersion, ExplorationController } from '../engine'; import { useEngine, useEngineVersion, ExplorationController } from '../engine';
import { MF_MODES, modeEngineId, seededGradient, shapeValues } from './model'; import { MF_MODES, modeEngineId, shapeValues } from './model';
import type { MFParam } from './model'; import type { MFParam } from './model';
import { CompositeStage } from './CompositeStage'; import { CompositeStage } from './CompositeStage';
import { ParticleStage } from './ParticleStage'; import { ParticleStage } from './ParticleStage';
import { SandwichStage } from './SandwichStage'; import { SandwichStage } from './SandwichStage';
import { SplitStage } from './SplitStage';
import { OutputStage } from './OutputStage'; import { OutputStage } from './OutputStage';
import { InputMini } from './InputMini';
import { Manifold } from './Manifold'; import { Manifold } from './Manifold';
import { ReadoutStrip } from './ReadoutStrip';
import { VerdictCluster } from './VerdictCluster'; import { VerdictCluster } from './VerdictCluster';
import { Dock } from './Dock'; import { Dock } from './Dock';
import { ReshapeModal } from './ReshapeModal'; import { ReshapeModal } from './ReshapeModal';
import type { import type {
Axes,
ConsoleCtx, ConsoleCtx,
DrawerDepth, DrawerDepth,
DrawerKey, DrawerKey,
FeedbackMarker, FeedbackMarker,
FeedbackModeUI, FeedbackModeUI,
Focus,
OutputMode, OutputMode,
Pin, Pin,
Snapshot,
SoloMode, SoloMode,
} from './types'; } from './types';
import type { BackendId } from '../dock/output-state'; import type { BackendId } from '../dock/output-state';
import { buildArmMask } from '../dock/output-state'; import { buildArmMask } from '../dock/output-state';
import { FeedbackController, type ProtoFeedbackMode } from '../feedback'; import { FeedbackController, type ProtoFeedbackMode } from '../feedback';
import { DEFAULT_OUTPUT_MODE, OUTPUT_MODES, outputModeDescriptor } from './output-mode'; import { DEFAULT_OUTPUT_MODE, outputModeDescriptor } from './output-mode';
import { useSettings, resolveInputMap } from '../settings/settings-store'; import { useSettings, resolveInputMap } from '../settings/settings-store';
import { useBackendManager } from '../backends'; import { useBackendManager } from '../backends';
import { useInputLayer } from '../inputs'; import { useInputLayer } from '../inputs';
@ -77,8 +77,6 @@ declare global {
} }
} }
let SNAP_ID = 0;
/** Small pill-button style for the exploring-scratchpad banner controls. */ /** Small pill-button style for the exploring-scratchpad banner controls. */
function pillBtn(color: string): CSSProperties { function pillBtn(color: string): CSSProperties {
return { return {
@ -93,39 +91,22 @@ function pillBtn(color: string): CSSProperties {
}; };
} }
export interface ConsoleAppProps { export function ConsoleApp() {
focus?: Focus;
}
export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProps) {
const engine = useEngine(); const engine = useEngine();
const version = useEngineVersion(engine); const version = useEngineVersion(engine);
const { settings } = useSettings(); const { settings } = useSettings();
const [focus, setFocus] = useState<Focus>(initialFocus);
const [modeId, setModeId] = useState('paf_synth'); const [modeId, setModeId] = useState('paf_synth');
const mode = MF_MODES.find((m) => m.id === modeId) ?? MF_MODES[0]; const mode = MF_MODES.find((m) => m.id === modeId) ?? MF_MODES[0];
const [params, setParams] = useState<MFParam[]>(() => mode.params.map((p) => ({ ...p }))); const [params, setParams] = useState<MFParam[]>(() => mode.params.map((p) => ({ ...p })));
const [pos, setPos] = useState<[number, number]>([0.5, 0.5]); const [pos, setPos] = useState<[number, number]>([0.5, 0.5]);
// A/B seed-snapshot model (kept as visual parity; A holds a remembered weight
// snapshot conceptually — here we mirror the JSX's seed-based preview marker).
const [seed, setSeed] = useState(0.4);
const [axes, setAxes] = useState<Axes>({ boldness: 0.55, memory: 0.4, precision: 0.5 });
const [preset, setPreset] = useState('Sculpt');
const [noiseCap, setNoiseCap] = useState(0.12); const [noiseCap, setNoiseCap] = useState(0.12);
const [examples, setExamples] = useState(0); const [examples, setExamples] = useState(0);
const [addingExample, setAddingExample] = useState(false); const [addingExample, setAddingExample] = useState(false);
const [loss, setLoss] = useState<number[]>([]); const [loss, setLoss] = useState<number[]>([]);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
const [ab, setAB] = useState<'A' | 'B'>('B');
const [, setHoldingA] = useState(false);
const aRef = useRef<{ seed: number } | null>(null);
const [spread, setSpread] = useState(false); const [spread, setSpread] = useState(false);
const [tame, setTame] = useState(0.85);
const [health, setHealth] = useState(0.8);
const [rev, setRev] = useState(1);
const [active, setActive] = useState<DrawerKey | null>(null); const [active, setActive] = useState<DrawerKey | null>(null);
const [depth, setDepth] = useState<DrawerDepth>('condensed'); const [depth, setDepth] = useState<DrawerDepth>('condensed');
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon. // Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
@ -146,9 +127,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
const [picking, setPicking] = useState(false); const [picking, setPicking] = useState(false);
const [anchorCount, setAnchorCount] = useState(0); const [anchorCount, setAnchorCount] = useState(0);
const [undoDepth, setUndoDepth] = useState(0); const [undoDepth, setUndoDepth] = useState(0);
const [learningRate, setLearningRate] = useState(0.00001);
const [decay, setDecay] = useState(0.97);
const [spreadLevel, setSpreadLevel] = useState(0.6);
// Exploration gestures (Jolt held weight-morph + OU explore-intensity). The // Exploration gestures (Jolt held weight-morph + OU explore-intensity). The
// maths lives in the ExplorationController (engine/exploration.ts); these are // maths lives in the ExplorationController (engine/exploration.ts); these are
// the React-visible reflections the Learning drawer renders. // the React-visible reflections the Learning drawer renders.
@ -170,8 +148,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
const [vcvSendRaw, setVcvSendRaw] = useState(false); const [vcvSendRaw, setVcvSendRaw] = useState(false);
// Feedback markers plotted on the 2D map (both polarities; session-scoped). // Feedback markers plotted on the 2D map (both polarities; session-scoped).
const [markers, setMarkers] = useState<FeedbackMarker[]>([]); const [markers, setMarkers] = useState<FeedbackMarker[]>([]);
const [volume, setVolume] = useState(0.8);
const [bpm, setBpm] = useState(120);
const [audioStarted, setAudioStarted] = useState(false); const [audioStarted, setAudioStarted] = useState(false);
const [follow, setFollow] = useState(false); const [follow, setFollow] = useState(false);
const [split, setSplit] = useState(() => { const [split, setSplit] = useState(() => {
@ -181,7 +157,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
useEffect(() => { useEffect(() => {
localStorage.setItem('mf-composite-split', String(split)); localStorage.setItem('mf-composite-split', String(split));
}, [split]); }, [split]);
const [stripPinned, setStripPinned] = useState(true);
const [firstSession, setFirstSession] = useState(true); const [firstSession, setFirstSession] = useState(true);
const [pins, setPins] = useState<Pin[]>([]); const [pins, setPins] = useState<Pin[]>([]);
@ -192,7 +167,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
const controllerRef = useRef<FeedbackController | null>(null); const controllerRef = useRef<FeedbackController | null>(null);
if (engine && !controllerRef.current) { if (engine && !controllerRef.current) {
controllerRef.current = new FeedbackController(engine, { controllerRef.current = new FeedbackController(engine, {
seed: 0xfeedbacc,
spread: 0.6, spread: 0.6,
}); });
} }
@ -289,8 +263,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
setPos([0.5, 0.5]); setPos([0.5, 0.5]);
setExamples(0); setExamples(0);
setLoss([]); setLoss([]);
setSnapshots([]);
setSeed(0.4);
setFollow(false); setFollow(false);
setPins([]); setPins([]);
setMarkers([]); setMarkers([]);
@ -392,10 +364,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
if (!c || !c.isPicking()) return; if (!c || !c.isPicking()) return;
c.placeCommit(x, y); c.placeCommit(x, y);
setPos([x, y]); setPos([x, y]);
pushSnap('anchor');
pushMarker([x, y], 'positive'); pushMarker([x, y], 'positive');
syncController(); syncController();
setRev((r) => r + 1);
}; };
// Output backend transport (backends-spec). The manager consumes the engine // Output backend transport (backends-spec). The manager consumes the engine
@ -441,11 +411,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
[params, version, engine], [params, version, engine],
); );
const gradient = useMemo(() => seededGradient(rev), [rev]);
const pushSnap = (tag: string) =>
setSnapshots((s) => [...s, { id: ++SNAP_ID, tag, noise: noiseCap, seed }].slice(-50));
/** Plot a feedback marker at the input location it was given (session-scoped). */ /** Plot a feedback marker at the input location it was given (session-scoped). */
const pushMarker = (at: [number, number], polarity: 'positive' | 'negative') => const pushMarker = (at: [number, number], polarity: 'positive' | 'negative') =>
setMarkers((m) => [...m, { x: at[0], y: at[1], polarity }].slice(-200)); setMarkers((m) => [...m, { x: at[0], y: at[1], polarity }].slice(-200));
@ -473,17 +438,14 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
if (feedbackMode === 'explore-and-place') { if (feedbackMode === 'explore-and-place') {
if (c?.getState().exploring) { if (c?.getState().exploring) {
// Place the current candidate → next manifold tap chooses the location. // Place the current candidate → next manifold tap chooses the location.
pushSnap('place');
c.place(); c.place();
} else { } else {
// Not exploring: a plain positive reinforcement of the current mapping. // Not exploring: a plain positive reinforcement of the current mapping.
pushSnap('commit +');
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0)); c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
pushMarker(pos, 'positive'); pushMarker(pos, 'positive');
} }
} else { } else {
// Geometric dislike: thumbs-up = like + train. // Geometric dislike: thumbs-up = like + train.
pushSnap('like +');
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0)); c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
pushMarker(pos, 'positive'); pushMarker(pos, 'positive');
} }
@ -491,8 +453,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
forwardVcvFeedback('up'); forwardVcvFeedback('up');
syncController(); syncController();
setNoiseCap((n) => Math.max(0.02, n * 0.7)); setNoiseCap((n) => Math.max(0.02, n * 0.7));
setHealth((h) => Math.min(1, h + 0.08));
setRev((r) => r + 1);
const l = engine?.evalLoss(); const l = engine?.evalLoss();
setLoss((prev) => setLoss((prev) =>
[...prev, Number.isFinite(l) ? (l as number) : prev.length ? prev[prev.length - 1] : 0.5].slice(-120), [...prev, Number.isFinite(l) ? (l as number) : prev.length ? prev[prev.length - 1] : 0.5].slice(-120),
@ -508,10 +468,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// Enter the scratchpad (or, if already exploring, cancel back to the real // Enter the scratchpad (or, if already exploring, cancel back to the real
// net). NEVER a dislike — Mode 2 is positive-only. // net). NEVER a dislike — Mode 2 is positive-only.
if (c?.getState().exploring) { if (c?.getState().exploring) {
pushSnap('cancel explore');
c.cancel(); c.cancel();
} else { } else {
pushSnap('explore');
c?.enterExplore(); c?.enterExplore();
// VCV bridged mode: entering explore re-rolls the module's net too. // VCV bridged mode: entering explore re-rolls the module's net too.
forwardVcvFeedback('rand'); forwardVcvFeedback('rand');
@ -520,31 +478,21 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// Geometric dislike: push the current mapping away from this sound. Pass the // Geometric dislike: push the current mapping away from this sound. Pass the
// HEARD (post-pipeline, routed) vector — NOT the raw MLP output — so the // HEARD (post-pipeline, routed) vector — NOT the raw MLP output — so the
// core has a non-zero MSE derivative (see engine.feedback.dislikeGeometric). // core has a non-zero MSE derivative (see engine.feedback.dislikeGeometric).
pushSnap('dislike '); const action = c?.dislike(engine?.routedOutput() ?? new Float32Array(0));
const action = c?.dislike(
pos,
engine?.routedOutput() ?? new Float32Array(0),
noiseCap,
spread ? 1 : 0.6,
);
// GeometricColdStart (15): no positives yet → show the one-time prompt. // GeometricColdStart (15): no positives yet → show the one-time prompt.
if (action === 15) setColdStart(true); if (action === 15) setColdStart(true);
pushMarker(pos, 'negative'); pushMarker(pos, 'negative');
// VCV bridged mode: thumbs-down = negative verdict. // VCV bridged mode: thumbs-down = negative verdict.
forwardVcvFeedback('down'); forwardVcvFeedback('down');
setSeed((s) => s + (Math.random() - 0.5) * (noiseCap * 4 + 0.3));
setNoiseCap((n) => Math.min(0.5, n + 0.06)); setNoiseCap((n) => Math.min(0.5, n + 0.06));
setHealth((h) => Math.max(0.1, h - 0.06));
} }
syncController(); syncController();
setRev((r) => r + 1);
}; };
/** Long-press perturb / explicit re-roll. */ /** Long-press perturb / explicit re-roll. */
const reroll = () => { const reroll = () => {
const c = controllerRef.current; const c = controllerRef.current;
setFirstSession(false); setFirstSession(false);
pushSnap('re-roll');
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) { if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
// Re-roll the scratchpad net (undoable) without leaving the session. // Re-roll the scratchpad net (undoable) without leaving the session.
c.reroll(); c.reroll();
@ -555,10 +503,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// VCV bridged mode: re-roll the module's net too. // VCV bridged mode: re-roll the module's net too.
forwardVcvFeedback('rand'); forwardVcvFeedback('rand');
syncController(); syncController();
setSeed(Math.random() * 6);
setNoiseCap(0.4); setNoiseCap(0.4);
setHealth(0.5);
setRev((r) => r + 1);
}; };
/** /**
@ -575,51 +520,40 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
engine?.process(); engine?.process();
} }
syncController(); syncController();
setRev((r) => r + 1);
}; };
/** /**
* Undo. While exploring (Mode 2) this pops the scratchpad undo ring (reroll / * Undo. Only meaningful while exploring (Mode 2) pops the scratchpad undo
* nudge). Otherwise it falls back to the UI snapshot stack (visual A/B seed). * ring (reroll / nudge), which is real, engine-backed undo. Geometric dislike
* (Mode 1) and the non-exploring case have no real undo in the core, so the
* button is simply inactive there (the prior "undo" outside a scratchpad
* session only reverted a decorative UI snapshot stack deleted 2026-07,
* simplification audit S16).
*/ */
const undo = () => { const undo = () => {
const c = controllerRef.current; const c = controllerRef.current;
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) { if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
c.undo(); c.undo();
syncController(); syncController();
setRev((r) => r + 1);
return;
} }
setSnapshots((s) => {
if (!s.length) return s;
const last = s[s.length - 1];
setSeed(last.seed);
setNoiseCap(last.noise);
setRev((r) => r + 1);
return s.slice(0, -1);
});
}; };
// ---- Explore-and-place scratchpad ops surfaced to the dock + cluster ---- // ---- Explore-and-place scratchpad ops surfaced to the dock + cluster ----
const onExplore = () => { const onExplore = () => {
controllerRef.current?.enterExplore(); controllerRef.current?.enterExplore();
syncController(); syncController();
setRev((r) => r + 1);
}; };
const onScratchReroll = () => { const onScratchReroll = () => {
controllerRef.current?.reroll(); controllerRef.current?.reroll();
syncController(); syncController();
setRev((r) => r + 1);
}; };
const onScratchNudge = () => { const onScratchNudge = () => {
controllerRef.current?.nudge(); controllerRef.current?.nudge();
syncController(); syncController();
setRev((r) => r + 1);
}; };
const onScratchUndo = () => { const onScratchUndo = () => {
controllerRef.current?.undo(); controllerRef.current?.undo();
syncController(); syncController();
setRev((r) => r + 1);
}; };
const onPlace = () => { const onPlace = () => {
controllerRef.current?.place(); controllerRef.current?.place();
@ -629,13 +563,11 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
setBusy(true); setBusy(true);
controllerRef.current?.finalise(); controllerRef.current?.finalise();
syncController(); syncController();
setRev((r) => r + 1);
setBusy(false); setBusy(false);
}; };
const onCancelExplore = () => { const onCancelExplore = () => {
controllerRef.current?.cancel(); controllerRef.current?.cancel();
syncController(); syncController();
setRev((r) => r + 1);
}; };
const train = () => { const train = () => {
setBusy(true); setBusy(true);
@ -655,30 +587,11 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// Snapshot the current input → current (shaped) output as a training example. // Snapshot the current input → current (shaped) output as a training example.
engine?.addExample([pos[0], pos[1]], Array.from(values)); engine?.addExample([pos[0], pos[1]], Array.from(values));
setExamples((e) => e + 1); setExamples((e) => e + 1);
pushSnap('example');
train(); train();
}; };
const setParam = (i: number, patch: Partial<MFParam>) => const setParam = (i: number, patch: Partial<MFParam>) =>
setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p))); setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p)));
const cycleStatus = (i: number) =>
setParams((ps) =>
ps.map((p, j) =>
j === i
? { ...p, status: ({ off: 'fixed', fixed: 'live', live: 'off' } as const)[p.status] }
: p,
),
);
const toggleAB = () => {
if (ab === 'B') {
aRef.current = { seed };
setAB('A');
} else {
if (aRef.current) setSeed(aRef.current.seed);
setAB('B');
}
};
// keyboard accelerators // keyboard accelerators
useEffect(() => { useEffect(() => {
@ -702,13 +615,13 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
setActive((a) => (a === map[e.key] ? null : map[e.key])); setActive((a) => (a === map[e.key] ? null : map[e.key]));
setDepth('condensed'); setDepth('condensed');
} else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded')); } else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded'));
else if (focus === 'composite' && e.key === '[') { else if (e.key === '[') {
e.preventDefault(); e.preventDefault();
setSplit((s) => Math.max(0, s - 0.04)); setSplit((s) => Math.max(0, s - 0.04));
} else if (focus === 'composite' && e.key === ']') { } else if (e.key === ']') {
e.preventDefault(); e.preventDefault();
setSplit((s) => Math.min(1, s + 0.04)); setSplit((s) => Math.min(1, s + 0.04));
} else if (focus === 'composite' && (e.key === '=' || e.key === '0')) { } else if (e.key === '=' || e.key === '0') {
e.preventDefault(); e.preventDefault();
setSplit(0.5); setSplit(0.5);
} else if (e.key === ' ' || e.key === 'ArrowUp') { } else if (e.key === ' ' || e.key === 'ArrowUp') {
@ -791,11 +704,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
modeId, modeId,
setModeId, setModeId,
mode, mode,
axes,
setAxis: (k, v) => setAxes((s) => ({ ...s, [k]: v })),
preset,
setPreset,
offsetActive: preset !== 'Sculpt',
datasetCount: examples, datasetCount: examples,
loss, loss,
busy, busy,
@ -812,25 +720,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
setMarkers([]); setMarkers([]);
setPins([]); setPins([]);
}, },
snapshots,
onJump: (id) => {
const s = snapshots.find((x) => x.id === id);
if (s) {
setSeed(s.seed);
setNoiseCap(s.noise);
setRev((r) => r + 1);
}
},
params, params,
cycleStatus,
setParam, setParam,
outputBackend,
setOutputBackend: (b) => {
// Map a backend id back onto the active Mode (the Mode is the source of
// truth; the Outputs drawer drives it via setOutputMode).
const m = OUTPUT_MODES.find((om) => om.backend === b);
if (m) setOutputMode(m.id);
},
outputMode, outputMode,
setOutputMode, setOutputMode,
// ---- output backend transport ---- // ---- output backend transport ----
@ -853,16 +744,9 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
cvIdentify, cvIdentify,
cvDisconnect, cvDisconnect,
setParams: (next: MFParam[]) => setParams(next), setParams: (next: MFParam[]) => setParams(next),
markers,
inputs, inputs,
health,
gradient: gradient.norms,
gradientStatus: gradient.status,
weightsRevision: rev,
spread, spread,
setSpread, setSpread,
tame,
setTame,
noiseCap, noiseCap,
setNoiseCap, setNoiseCap,
// learning-behaviour // learning-behaviour
@ -874,12 +758,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
learningPaused, learningPaused,
armedCount: params.filter((p) => p.armed).length, armedCount: params.filter((p) => p.armed).length,
clearArmed: () => setParams((ps) => ps.map((p) => (p.armed ? { ...p, armed: false } : p))), clearArmed: () => setParams((ps) => ps.map((p) => (p.armed ? { ...p, armed: false } : p))),
learningRate,
setLearningRate,
decay,
setDecay,
spreadLevel,
setSpreadLevel,
// exploration gestures (Jolt + OU explore) // exploration gestures (Jolt + OU explore)
joltActive, joltActive,
onJoltPress, onJoltPress,
@ -889,10 +767,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// synth // synth
audioStarted, audioStarted,
onToggleAudio, onToggleAudio,
volume,
setVolume,
bpm,
setBpm,
// explore-and-place scratchpad session (workstream B) // explore-and-place scratchpad session (workstream B)
picking, picking,
anchorCount, anchorCount,
@ -909,8 +783,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
// Resolve the effective input-map shape from Settings + the mode's declared input. // Resolve the effective input-map shape from Settings + the mode's declared input.
const inputMapVariant = resolveInputMap(settings.inputMap, mode.input); const inputMapVariant = resolveInputMap(settings.inputMap, mode.input);
const healthColor =
health > 0.66 ? 'rgba(107,194,107,' : health > 0.33 ? 'rgba(245,196,94,' : 'rgba(255,68,102,';
const addPin = (p: [number, number]) => const addPin = (p: [number, number]) =>
setPins((ps) => [...ps, { x: p[0], y: p[1], color: 'rgba(255,106,0,0.16)' }]); setPins((ps) => [...ps, { x: p[0], y: p[1], color: 'rgba(255,106,0,0.16)' }]);
@ -926,41 +798,9 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
> >
<style>{`@keyframes mfDrawerIn{from{transform:translateX(16px)}to{transform:translateX(0)}}`}</style> <style>{`@keyframes mfDrawerIn{from{transform:translateX(16px)}to{transform:translateX(0)}}`}</style>
{/* ambient health glow at the screen edge */}
<div
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 25,
boxShadow: `inset 0 0 120px ${healthColor}${0.05 + (1 - health) * 0.12})`,
transition: 'box-shadow var(--dur-slow) var(--ease-console)',
}}
/>
{/* stage = manifold area (left of dock) */} {/* stage = manifold area (left of dock) */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 48, bottom: 0 }}> <div style={{ position: 'absolute', top: 0, left: 0, right: 48, bottom: 0 }}>
{focus === 'in' && (stripPinned || mode.cls !== 'Synth') && ( <div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 30 }}>
<ReadoutStrip
params={params}
values={values}
onChange={setParam}
pinned={stripPinned}
onTogglePin={() => setStripPinned((p) => !p)}
/>
</div>
)}
<div
style={{
position: 'absolute',
top: focus === 'in' && stripPinned ? 76 : 0,
left: 0,
right: 0,
bottom: 0,
}}
>
{sandwich ? ( {sandwich ? (
// Sandwich centre-stage: shrunken input (left) · landscape stack // Sandwich centre-stage: shrunken input (left) · landscape stack
// (centre, fills) · compact outputs (right). Replaces the Mode stage. // (centre, fills) · compact outputs (right). Replaces the Mode stage.
@ -1011,7 +851,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
</div> </div>
) : outputMode === 'particles' ? ( ) : outputMode === 'particles' ? (
<ParticleStage pos={pos} onMove={onMove} /> <ParticleStage pos={pos} onMove={onMove} />
) : focus === 'composite' ? ( ) : (
<CompositeStage <CompositeStage
split={split} split={split}
onSplit={setSplit} onSplit={setSplit}
@ -1028,46 +868,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
values={values} values={values}
onChange={setParam} onChange={setParam}
/> />
) : focus === 'split' ? (
<SplitStage
pos={pos}
onMove={onMove}
noiseCap={noiseCap}
pins={pins}
markers={markers}
variant={inputMapVariant}
follow={follow}
onLongPress={addPin}
params={params}
values={values}
onChange={setParam}
/>
) : focus === 'out' ? (
<>
<OutputStage params={params} values={values} onChange={setParam} />
<InputMini
mode={mode}
pos={pos}
onMove={onMove}
noiseCap={noiseCap}
corner="bottom-left"
variant={inputMapVariant}
/>
</>
) : (
<Manifold
pos={pos}
onMove={onMove}
noiseCap={noiseCap}
pins={pins}
markers={markers}
variant={inputMapVariant}
frozen={false}
follow={follow}
onLongPress={addPin}
picking={picking}
onPickLocation={onPickLocation}
/>
)} )}
{/* corner overlay — hidden in Particle mode (top axis bar owns that row) */} {/* corner overlay — hidden in Particle mode (top axis bar owns that row) */}
@ -1092,10 +892,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
onReroll={reroll} onReroll={reroll}
onNudge={nudgeNet} onNudge={nudgeNet}
onRandomise={reroll} onRandomise={reroll}
canUndo={feedbackMode === 'explore-and-place' && exploring ? undoDepth > 0 : snapshots.length > 0} canUndo={feedbackMode === 'explore-and-place' && exploring && undoDepth > 0}
ab={ab}
onToggleAB={toggleAB}
onHoldA={setHoldingA}
firstSession={firstSession} firstSession={firstSession}
feedbackMode={feedbackMode} feedbackMode={feedbackMode}
exploring={exploring} exploring={exploring}
@ -1205,11 +1002,10 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
</div> </div>
)} )}
{/* Global PICK-LOCATION capture overlay: works in any focus (composite/ {/* Global PICK-LOCATION capture overlay: the CompositeStage/ParticleStage
split stages don't expose picking). The directly-rendered Manifold don't expose picking directly, so this transparent overlay captures
(focus==='in') also handles picks + draws the reticle; this overlay the pointer-down and routes it to onPickLocation everywhere. */}
guarantees the placepick loop is reachable everywhere. */} {picking && (
{picking && focus !== 'in' && (
<div <div
onPointerDown={(e) => { onPointerDown={(e) => {
const r = e.currentTarget.getBoundingClientRect(); const r = e.currentTarget.getBoundingClientRect();

View file

@ -25,9 +25,7 @@ import { Badge, Button, PillToggle, Slider, Switch } from '../primitives';
import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types'; import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types';
import type { InputMode } from '../inputs'; import type { InputMode } from '../inputs';
import { OutputControlRow } from '../dock/OutputControlRow'; import { OutputControlRow } from '../dock/OutputControlRow';
import { BackendAdvanced } from '../dock/BackendAdvanced';
import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig'; import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig';
import { BACKENDS } from '../dock/output-state';
import { shapeValues } from './model'; import { shapeValues } from './model';
import { outputModeDescriptor } from './output-mode'; import { outputModeDescriptor } from './output-mode';
import { useSettings, unfocusedIconCss } from '../settings/settings-store'; import { useSettings, unfocusedIconCss } from '../settings/settings-store';
@ -139,6 +137,13 @@ const FEEDBACK_DESC: Record<FeedbackModeUI, string> = {
'explore-and-place': 'explore-and-place':
'Down re-rolls the whole net into a scratchpad you audition; + places a liked sound (Mode 2).', '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 }[] = [ const SOLO_OPTS: { value: SoloMode; label: string }[] = [
{ value: 'mask-gradients', label: 'Mask gradients' }, { value: 'mask-gradients', label: 'Mask gradients' },
{ value: 'zero-loss', label: 'Zero loss' }, { value: 'zero-loss', label: 'Zero loss' },
@ -234,25 +239,14 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
</span> </span>
</div> </div>
<SectionLabel>Solo behaviour</SectionLabel> <SectionLabel>Solo behaviour</SectionLabel>
<Segmented value={ctx.soloMode} onChange={ctx.setSoloMode} options={SOLO_OPTS} /> <Chip tone="var(--accent)">{SOLO_OPTS.find((o) => o.value === ctx.soloMode)?.label}</Chip>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}> <p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
{SOLO_DESC[ctx.soloMode]} Solo only freezes the rest as far as a shared network allows. {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.
</p> </p>
<SectionLabel>Live training params</SectionLabel> <SectionLabel>Live training params</SectionLabel>
<Slider label="noise cap" value={ctx.noiseCap} min={0} max={0.5} step={0.01} onChange={ctx.setNoiseCap} /> <Slider label="noise cap" value={ctx.noiseCap} min={0} max={0.5} step={0.01} onChange={ctx.setNoiseCap} />
<Slider label="spread" value={ctx.spreadLevel} min={0} max={1} step={0.01} onChange={ctx.setSpreadLevel} />
<Slider label="tame · output limiter" value={ctx.tame} min={0} max={1} step={0.01} onChange={ctx.setTame} />
<Slider
label="learning rate"
value={ctx.learningRate}
min={0.000001}
max={0.01}
step={0.000001}
onChange={ctx.setLearningRate}
format={(v) => v.toExponential(1)}
/>
<Slider label="decay" value={ctx.decay} min={0.8} max={1} step={0.001} onChange={ctx.setDecay} />
</> </>
)} )}
@ -569,17 +563,14 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
</Button> </Button>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span> <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
</div> </div>
<Slider label="master volume" value={ctx.volume} min={0} max={1} step={0.01} onChange={ctx.setVolume} />
{depth === 'expanded' && ( {depth === 'expanded' && (
<>
<SectionLabel>Tempo</SectionLabel>
<Slider label="bpm" value={ctx.bpm} min={40} max={220} step={1} onChange={ctx.setBpm} />
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}> <p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
The active engine follows the selected mode ({ctx.mode.label}). The active engine follows the selected mode ({ctx.mode.label}).
{/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the {/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the
18-section group-override matrix are workstream E. */} 18-section group-override matrix are workstream E. Master
volume + bpm sliders were deleted (simplification audit
S16) they drove no engine parameter. */}
</p> </p>
</>
)} )}
</> </>
); );
@ -624,7 +615,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
}, {}); }, {});
const mutedN = ctx.params.filter((p) => p.muted).length; const mutedN = ctx.params.filter((p) => p.muted).length;
const modeDesc = outputModeDescriptor(ctx.outputMode); const modeDesc = outputModeDescriptor(ctx.outputMode);
const backend = BACKENDS.find((b) => b.id === modeDesc.backend) ?? BACKENDS[0];
// The particle Mode names its outputs; otherwise use the param names. // The particle Mode names its outputs; otherwise use the param names.
const nameFor = (idx: number, fallback: string) => const nameFor = (idx: number, fallback: string) =>
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback; ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
@ -671,12 +661,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
{!expanded && ctx.params.length > 6 && ( {!expanded && ctx.params.length > 6 && (
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more expand to edit</span> <span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more expand to edit</span>
)} )}
{expanded && (
<>
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
</>
)}
</> </>
); );
} }

View file

@ -1,81 +0,0 @@
/**
* InputMini the input demoted to a compact secondary control for output-first
* views. Ported from `InputMini.jsx`.
*/
import { VirtualJoystick, XYPad } from '../primitives';
import type { MFMode } from './model';
export interface InputMiniProps {
mode: MFMode;
pos: [number, number];
onMove: (x: number, y: number) => void;
noiseCap?: number;
size?: number;
corner?: 'bottom-left' | 'bottom-right' | 'top-left';
/** Input-map shape override (Settings). Falls back to the mode's input. */
variant?: 'rectangular' | 'circular';
}
export function InputMini({
mode,
pos,
onMove,
size = 132,
corner = 'bottom-left',
variant,
}: InputMiniProps) {
const circular = variant ? variant === 'circular' : mode.input === 'joystick';
const place = {
'bottom-left': { bottom: 14, left: 14 },
'bottom-right': { bottom: 14, right: 14 },
'top-left': { top: 14, left: 14 },
}[corner];
return (
<div
style={{
position: 'absolute',
zIndex: 22,
...place,
background: 'var(--glass)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid var(--glass-line)',
borderRadius: 'var(--r-2)',
padding: 'var(--sp-2)',
display: 'flex',
flexDirection: 'column',
gap: 6,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
}}
>
<span
style={{
fontSize: 10,
color: 'var(--fg-dim)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
}}
>
input · {circular ? 'joy' : 'xy'}
</span>
<span
style={{ fontSize: 10, color: 'var(--fg-mute)', fontVariantNumeric: 'tabular-nums' }}
>
{pos[0].toFixed(2)},{pos[1].toFixed(2)}
</span>
</div>
{circular ? (
<VirtualJoystick size={size} position={pos} onMove={(x, y) => onMove(x, y)} />
) : (
<XYPad size={size} position={pos} onMove={(x, y) => onMove(x, y)} showGrid />
)}
</div>
);
}

View file

@ -1,219 +0,0 @@
/**
* ReadoutStrip the output heatmap as a thin top control strip. Same model as
* OutputStage. Ported from `ReadoutStrip.jsx`.
*/
import { useRef, useState } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import type { MFParam, ParamStatus } from './model';
import { OutputEditor } from './OutputEditor';
const RS_GROUP_COLOR: Record<string, string> = {
formant: '--accent',
pitch: '--accent-2',
amp: '--good',
filter: '--warn',
fx: '--info',
mod: '--accent-3',
};
const RS_NEXT: Record<ParamStatus, ParamStatus> = { off: 'fixed', fixed: 'live', live: 'off' };
export interface ReadoutStripProps {
params: MFParam[];
values: number[];
onChange: (i: number, patch: Partial<MFParam>) => void;
pinned: boolean;
onTogglePin: () => void;
}
export function ReadoutStrip({ params, values, onChange, pinned, onTogglePin }: ReadoutStripProps) {
const [open, setOpen] = useState<number | null>(null);
const timers = useRef<{ open: ReturnType<typeof setTimeout> | null; close: ReturnType<typeof setTimeout> | null }>({
open: null,
close: null,
});
const drag = useRef<{ i: number; moved: boolean; startY: number; el: HTMLDivElement | null; alt: boolean }>({
i: -1,
moved: false,
startY: 0,
el: null,
alt: false,
});
const scheduleOpen = (i: number) => {
if (timers.current.close) clearTimeout(timers.current.close);
if (timers.current.open) clearTimeout(timers.current.open);
timers.current.open = setTimeout(() => setOpen(i), 110);
};
const scheduleClose = () => {
if (timers.current.open) clearTimeout(timers.current.open);
if (timers.current.close) clearTimeout(timers.current.close);
timers.current.close = setTimeout(() => setOpen(null), 280);
};
const hold = () => {
if (timers.current.close) clearTimeout(timers.current.close);
};
const valFromEvent = (el: HTMLDivElement, clientY: number) => {
const r = el.getBoundingClientRect();
return Math.max(0, Math.min(1, 1 - (clientY - r.top) / r.height));
};
const down = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
e.currentTarget.setPointerCapture?.(e.pointerId);
drag.current = {
i,
moved: false,
startY: e.clientY,
el: e.currentTarget,
alt: e.altKey || e.metaKey,
};
};
const move = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
const d = drag.current;
if (d.i !== i) return;
if (Math.abs(e.clientY - d.startY) > 3) d.moved = true;
if (d.moved && !d.alt && d.el) onChange(i, { val: valFromEvent(d.el, e.clientY) });
};
const up = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
const d = drag.current;
if (d.i !== i) return;
if (d.alt && !d.moved) onChange(i, { status: RS_NEXT[params[i].status] || 'live' });
else if (!d.moved && d.el) onChange(i, { val: valFromEvent(d.el, e.clientY) });
drag.current = { i: -1, moved: false, startY: 0, el: null, alt: false };
};
return (
<div
style={{
display: 'flex',
alignItems: 'stretch',
gap: 2,
height: 76,
padding: '0 2px',
background: 'var(--glass)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
borderBottom: '1px solid var(--glass-line)',
position: 'relative',
zIndex: 30,
}}
>
<button
type="button"
onClick={onTogglePin}
title="Pin strip open"
style={{
flex: '0 0 auto',
width: 30,
border: 0,
background: 'transparent',
color: pinned ? 'var(--accent)' : 'var(--fg-dim)',
cursor: 'pointer',
fontSize: 'var(--fs-md)',
}}
>
{pinned ? '📌' : '▾'}
</button>
{params.map((p, i) => {
const eff = values[i] ?? 0;
const gc = `var(${RS_GROUP_COLOR[p.group] || '--accent'})`;
const dim = p.status === 'off';
const placeRight = i > params.length - 5;
return (
<div
key={i}
style={{
position: 'relative',
flex: 1,
minWidth: 0,
display: 'flex',
flexDirection: 'column',
}}
onPointerLeave={scheduleClose}
>
<div
onPointerEnter={() => scheduleOpen(i)}
style={{
textAlign: 'center',
fontSize: 8,
fontFamily: 'var(--font-mono)',
lineHeight: '11px',
cursor: 'help',
color: p.status === 'live' ? 'var(--fg-dim)' : gc,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{p.name}
</div>
<div
onPointerDown={(e) => down(e, i)}
onPointerMove={(e) => move(e, i)}
onPointerUp={(e) => up(e, i)}
onPointerCancel={(e) => up(e, i)}
style={{
position: 'relative',
flex: 1,
background: 'var(--bg)',
borderRadius: 2,
overflow: 'hidden',
cursor: 'ns-resize',
opacity: dim ? 0.5 : 1,
touchAction: 'none',
}}
>
<div
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: `${eff * 100}%`,
background: gc,
opacity: 0.25 + eff * 0.6,
transition: 'height 60ms linear',
}}
/>
{p.status === 'live' && (
<div
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: `${p.val * 100}%`,
height: 0,
borderTop: '1px dashed rgba(255,255,255,0.3)',
}}
/>
)}
{p.status !== 'live' && (
<div
style={{
position: 'absolute',
top: 1,
left: 0,
right: 0,
textAlign: 'center',
fontSize: 8,
color: p.status === 'fixed' ? 'var(--accent-2)' : 'var(--fg-dim)',
}}
>
{p.status === 'fixed' ? '⊟' : '∅'}
</div>
)}
</div>
{open === i && (
<OutputEditor
param={p}
onChange={(patch) => onChange(i, patch)}
onHold={hold}
onLeave={scheduleClose}
place={{ top: 'calc(100% + 6px)', [placeRight ? 'right' : 'left']: 0 }}
/>
)}
</div>
);
})}
</div>
);
}

View file

@ -1,58 +0,0 @@
/**
* SplitStage input and output given EQUAL prominence, side by side. Left =
* Manifold (input), right = OutputStage (output field). Ported from `SplitStage.jsx`.
*/
import { Manifold } from './Manifold';
import { OutputStage } from './OutputStage';
import type { MFParam } from './model';
import type { FeedbackMarker, Pin } from './types';
export interface SplitStageProps {
pos: [number, number];
onMove: (x: number, y: number) => void;
noiseCap: number;
pins: Pin[];
markers?: FeedbackMarker[];
variant?: 'rectangular' | 'circular';
follow: boolean;
onLongPress: (p: [number, number]) => void;
params: MFParam[];
values: number[];
onChange: (i: number, patch: Partial<MFParam>) => void;
}
export function SplitStage({
pos,
onMove,
noiseCap,
pins,
markers = [],
variant = 'rectangular',
follow,
onLongPress,
params,
values,
onChange,
}: SplitStageProps) {
return (
<div style={{ position: 'absolute', inset: 0, display: 'flex' }}>
<div
style={{ flex: 1, position: 'relative', borderRight: '1px solid var(--line)', minWidth: 0 }}
>
<Manifold
pos={pos}
onMove={onMove}
noiseCap={noiseCap}
pins={pins}
markers={markers}
variant={variant}
follow={follow}
onLongPress={onLongPress}
/>
</div>
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
<OutputStage params={params} values={values} onChange={onChange} compact />
</div>
</div>
);
}

View file

@ -1,6 +1,6 @@
/** /**
* VerdictCluster floating bottom-centre control, the app's main verdict. * VerdictCluster floating bottom-centre control, the app's main verdict.
* perturb (thumbs-down) · undo · commit (thumbs-up), + A/B toggle. * perturb (thumbs-down) · undo · commit (thumbs-up).
* Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`. * Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`.
* *
* The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0): * The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0):
@ -50,9 +50,6 @@ export interface VerdictClusterProps {
/** Full re-roll of the current net (right half of the pill). */ /** Full re-roll of the current net (right half of the pill). */
onRandomise: () => void; onRandomise: () => void;
canUndo: boolean; canUndo: boolean;
ab: 'A' | 'B';
onToggleAB: () => void;
onHoldA: (holding: boolean) => void;
firstSession: boolean; firstSession: boolean;
/** Active feedback mode — drives the cluster's labels/tones (rl-feedback §0). */ /** Active feedback mode — drives the cluster's labels/tones (rl-feedback §0). */
feedbackMode: FeedbackModeUI; feedbackMode: FeedbackModeUI;
@ -70,9 +67,6 @@ export function VerdictCluster({
onNudge, onNudge,
onRandomise, onRandomise,
canUndo, canUndo,
ab,
onToggleAB,
onHoldA,
firstSession, firstSession,
feedbackMode, feedbackMode,
exploring, exploring,

View file

@ -2,17 +2,13 @@
* Console barrel the convertible Console shell, wired to the real engine. * Console barrel the convertible Console shell, wired to the real engine.
*/ */
export { ConsoleApp } from './ConsoleApp'; export { ConsoleApp } from './ConsoleApp';
export type { ConsoleAppProps } from './ConsoleApp';
export { CompositeStage } from './CompositeStage'; export { CompositeStage } from './CompositeStage';
export { SplitStage } from './SplitStage';
export { OutputStage } from './OutputStage'; export { OutputStage } from './OutputStage';
export { ReadoutStrip } from './ReadoutStrip';
export { Manifold } from './Manifold'; export { Manifold } from './Manifold';
export { InputMini } from './InputMini';
export { VerdictCluster } from './VerdictCluster'; export { VerdictCluster } from './VerdictCluster';
export { Dock } from './Dock'; export { Dock } from './Dock';
export { DRAWERS } from './Drawers'; export { DRAWERS } from './Drawers';
export { AltitudeNav, MiniMeters, CompactAxis } from './shared-ui'; export { MiniMeters } from './shared-ui';
export { MF_MODES, shapeValues, applyCurve, seededGradient, modeEngineId } from './model'; export { MF_MODES, shapeValues, applyCurve, modeEngineId } from './model';
export type { MFMode, MFParam, ParamStatus } from './model'; export type { MFMode, MFParam, ParamStatus } from './model';
export type { Focus, ConsoleCtx } from './types'; export type { ConsoleCtx } from './types';

View file

@ -69,7 +69,7 @@ export const DEFAULT_MODE_ML: ModeML = {
/** /**
* Per-output control row the unified store used by both the stage * Per-output control row the unified store used by both the stage
* (OutputStage / ReadoutStrip) and the Outputs/Routing dock. `status` is the * (OutputStage) and the Outputs/Routing dock. `status` is the
* model-control tri-state; `muted` and `armed` are ORTHOGONAL modifiers * model-control tri-state; `muted` and `armed` are ORTHOGONAL modifiers
* (dock-spec §3.2 the deliberate split of the deployed conflated * (dock-spec §3.2 the deliberate split of the deployed conflated
* frozenmuted field). Backend-specific specs are populated by the active * frozenmuted field). Backend-specific specs are populated by the active
@ -302,22 +302,6 @@ export function shapeValues(params: MFParam[], engineOut: Float32Array | null):
}); });
} }
/** Deterministic per-revision gradient-flow stub (visual only; ported as-is). */
export function seededGradient(rev: number): {
norms: number[];
status: string[];
} {
const n = 4;
const norms: number[] = [];
const status: string[] = [];
for (let i = 0; i < n; i++) {
const r = Math.abs((Math.sin((rev + 1) * (i + 1) * 12.9898) * 43758.5453) % 1);
norms.push(0.2 + r * 0.8);
status.push(r > 0.85 ? 'exploding' : r < 0.18 ? 'vanishing' : r < 0.3 ? 'converged' : 'healthy');
}
return { norms, status };
}
/** Map a mode id the audio-engine backend id. Mode ids align with engine ids /** Map a mode id the audio-engine backend id. Mode ids align with engine ids
* except `slp_workshop` (runs the memlcelium engine), the analysis controller, * except `slp_workshop` (runs the memlcelium engine), the analysis controller,
* and the relabelled `c15`. */ * and the relabelled `c15`. */

View file

@ -18,13 +18,6 @@
*/ */
import type { OutputMode } from './types'; import type { OutputMode } from './types';
import type { BackendId } from '../dock/output-state'; import type { BackendId } from '../dock/output-state';
import type {
ParticleIcon,
MidiIcon,
OscIcon,
SynthIcon,
EditorIcon,
} from './icons';
export interface OutputModeDescriptor { export interface OutputModeDescriptor {
id: OutputMode; id: OutputMode;
@ -87,11 +80,3 @@ export const DEFAULT_OUTPUT_MODE: OutputMode = OUTPUT_MODES[0].id;
export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor { export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor {
return OUTPUT_MODES.find((m) => m.id === id) ?? OUTPUT_MODES[0]; return OUTPUT_MODES.find((m) => m.id === id) ?? OUTPUT_MODES[0];
} }
/** The monochrome icon component for a Mode (resolved by the dock). */
export type ModeIconComponent =
| typeof ParticleIcon
| typeof MidiIcon
| typeof OscIcon
| typeof SynthIcon
| typeof EditorIcon;

View file

@ -1,85 +1,13 @@
/** /**
* Console shared chrome for the simpler altitudes. Ported from `shared-ui.jsx`. * Console shared chrome. Ported from `shared-ui.jsx`.
* *
* AltitudeNav no longer navigates to separate HTML files (the JSX's href model); * AltitudeNav and CompactAxis were deleted 2026-07 (simplification audit S15)
* the focus switch is driven by React state via `onFocus`. The altitude pills * both were part of the dead focus/altitude system (setFocus was never called;
* (Console / Perform / Zen) are inert here Manifold ships a single altitude. * Manifold ships a single "composite" altitude). MiniMeters survives it is
* the live glanceable output readout used by CompositeStage's minimap.
*/ */
import type { CSSProperties } from 'react';
import type { Focus } from './types';
import type { MFParam } from './model'; import type { MFParam } from './model';
const FOCI: [Focus, string, string][] = [
['in', 'IN', 'Input-first'],
['split', 'DUAL', 'Input + output equal'],
['out', 'OUT', 'Output-first'],
['composite', 'FLEX', 'Composite — drag to rebalance'],
];
export interface AltitudeNavProps {
current?: string;
focus?: Focus;
onFocus?: (f: Focus) => void;
style?: CSSProperties;
}
export function AltitudeNav({ current = 'console', focus = 'in', onFocus, style }: AltitudeNavProps) {
const items = [
{ id: 'console', dots: '◆◆◆', label: 'Console' },
{ id: 'perform', dots: '◆◆', label: 'Perform' },
{ id: 'zen', dots: '◆', label: 'Zen' },
];
const pill = (on: boolean): CSSProperties => ({
textDecoration: 'none',
fontSize: 11,
padding: '2px 8px',
borderRadius: 'var(--r-pill)',
color: on ? 'var(--accent)' : 'var(--fg-dim)',
background: on ? 'rgba(255,106,0,0.14)' : 'transparent',
border: 'none',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
});
return (
<div
style={{
position: 'absolute',
top: 12,
right: 14,
zIndex: 70,
display: 'flex',
gap: 6,
alignItems: 'center',
background: 'var(--glass)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid var(--glass-line)',
borderRadius: 'var(--r-pill)',
padding: '4px 6px',
...style,
}}
>
{items.map((it) => (
<span key={it.id} title={`${it.label} · ${focus}`} style={pill(it.id === current)}>
{it.dots}
</span>
))}
<span style={{ width: 1, height: 16, background: 'var(--glass-line)' }} />
{FOCI.map(([f, label, title]) => (
<button
key={f}
type="button"
title={title}
onClick={() => onFocus?.(f)}
style={{ ...pill(focus === f), fontSize: 9, letterSpacing: '0.08em' }}
>
{label}
</button>
))}
</div>
);
}
const MM_GROUP_COLOR: Record<string, string> = { const MM_GROUP_COLOR: Record<string, string> = {
formant: '--accent', formant: '--accent',
pitch: '--accent-2', pitch: '--accent-2',
@ -122,66 +50,3 @@ export function MiniMeters({ params, values }: { params: MFParam[]; values: numb
</div> </div>
); );
} }
/** CompactAxis — slim labelled feel slider (Perform bar; kept for parity). */
export function CompactAxis({
label,
value,
onChange,
accent = 'var(--accent)',
}: {
label: string;
value: number;
onChange: (v: number) => void;
accent?: string;
}) {
return (
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-2)',
fontFamily: 'var(--font-mono)',
}}
>
<span
style={{
width: 64,
fontSize: 10,
color: 'var(--fg-mute)',
textTransform: 'uppercase',
letterSpacing: '0.08em',
}}
>
{label}
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="mf-slider-input"
style={
{
width: 120,
['--mf-axis-accent' as string]: accent,
['--mf-pct' as string]: `${Math.max(0, Math.min(1, value))}`,
} as CSSProperties
}
/>
<span
style={{
width: '3ch',
fontSize: 10,
color: 'var(--fg-dim)',
fontVariantNumeric: 'tabular-nums',
textAlign: 'right',
}}
>
{value.toFixed(2)}
</span>
</label>
);
}

View file

@ -2,7 +2,6 @@
* Console shared prop/context types used across the stage + dock components. * Console shared prop/context types used across the stage + dock components.
*/ */
import type { MFMode, MFParam } from './model'; import type { MFMode, MFParam } from './model';
import type { BackendId } from '../dock/output-state';
import type { FeedbackMode } from '../engine/types'; import type { FeedbackMode } from '../engine/types';
import type { BackendStatus } from '../backends/backend'; import type { BackendStatus } from '../backends/backend';
import type { UseInputLayer } from '../inputs'; import type { UseInputLayer } from '../inputs';
@ -35,37 +34,34 @@ export interface FeedbackMarker {
polarity: 'positive' | 'negative'; polarity: 'positive' | 'negative';
} }
export interface Snapshot {
id: number;
tag: string;
noise: number;
seed: number;
}
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help'; export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
export type DrawerDepth = 'condensed' | 'expanded'; export type DrawerDepth = 'condensed' | 'expanded';
export type Focus = 'in' | 'split' | 'out' | 'composite';
export interface Axes { /**
boldness: number; * The flat context the Dock + drawers read. Pruned 2026-07 (simplification
memory: number; * audit S19) to the fields Dock/Drawers/OutputsBackendConfig actually consume.
precision: number; * Deleted outright: the Axes type + axes/setAxis state, the
} * preset/setPreset/offsetActive chain (permanently 'Sculpt'/false), the
* markers/outputBackend/setOutputBackend/cycleStatus fields (each had a zero-
/** The flat context the Dock + drawers read. */ * consumer duplicate elsewhere see ConsoleApp.tsx), and everything S16/S18/
* L1/L20 removed (decorative sliders, snapshots, health/gradient visuals,
* BackendAdvanced's catalogue).
*
* `modes` / `setModeId` are KEPT despite having no renderer today this is
* the exact plumbing the Phase-5 instrument-mode picker is built on (A7/A3),
* not dead code. `busy` / `addingExample` / `onAddExample` / `onTrain` / `loss`
* are ALSO kept even though no drawer reads them either: unlike the fields
* above they drive real engine calls (engine.addExample / engine.train /
* engine.evalLoss), so pending confirmation either way they read as
* unfinished plumbing rather than confirmed-dead decoration; deleting them
* was out of this pass's authorized scope.
*/
export interface ConsoleCtx { export interface ConsoleCtx {
modes: MFMode[]; modes: MFMode[];
modeId: string; modeId: string;
setModeId: (id: string) => void; setModeId: (id: string) => void;
mode: MFMode; mode: MFMode;
axes: Axes;
setAxis: (k: keyof Axes, v: number) => void;
preset: string;
setPreset: (p: string) => void;
offsetActive: boolean;
datasetCount: number; datasetCount: number;
loss: number[]; loss: number[];
busy: boolean; busy: boolean;
@ -74,15 +70,9 @@ export interface ConsoleCtx {
onTrain: () => void; onTrain: () => void;
onClear: () => void; onClear: () => void;
snapshots: Snapshot[];
onJump: (id: number) => void;
params: MFParam[]; params: MFParam[];
cycleStatus: (i: number) => void;
/** Patch one output row in the shared store (drives stage + dock in sync). */ /** Patch one output row in the shared store (drives stage + dock in sync). */
setParam: (i: number, patch: Partial<MFParam>) => void; setParam: (i: number, patch: Partial<MFParam>) => void;
outputBackend: BackendId;
setOutputBackend: (v: BackendId) => void;
// ---- Output backend transport (backends-spec §1§5) ---- // ---- Output backend transport (backends-spec §1§5) ----
/** Live status of the active output backend (MIDI/OSC connect state, etc.). */ /** Live status of the active output backend (MIDI/OSC connect state, etc.). */
@ -117,23 +107,12 @@ export interface ConsoleCtx {
outputMode: OutputMode; outputMode: OutputMode;
setOutputMode: (m: OutputMode) => void; setOutputMode: (m: OutputMode) => void;
// ---- Feedback markers on the 2D map (both polarities) ----
/** Markers plotted at the input location where each feedback was given. */
markers: FeedbackMarker[];
// ---- Modular input layer (workstream F; inputs-spec) ---- // ---- Modular input layer (workstream F; inputs-spec) ----
/** The composed input layer: source enable/config/status + channel layout. */ /** The composed input layer: source enable/config/status + channel layout. */
inputs: UseInputLayer; inputs: UseInputLayer;
health: number;
gradient: number[];
gradientStatus: string[];
weightsRevision: number;
spread: boolean; spread: boolean;
setSpread: (v: boolean) => void; setSpread: (v: boolean) => void;
tame: number;
setTame: (v: number) => void;
noiseCap: number; noiseCap: number;
setNoiseCap: (v: number) => void; setNoiseCap: (v: number) => void;
@ -151,14 +130,6 @@ export interface ConsoleCtx {
/** Clear all arm flags ("Arm all"). */ /** Clear all arm flags ("Arm all"). */
clearArmed: () => void; clearArmed: () => void;
// ---- Live training params (dock-spec §1.3) ----
learningRate: number;
setLearningRate: (v: number) => void;
decay: number;
setDecay: (v: number) => void;
spreadLevel: number;
setSpreadLevel: (v: number) => void;
// ---- Exploration gestures (one-core-engine §P1; interim TS shells) ---- // ---- Exploration gestures (one-core-engine §P1; interim TS shells) ----
/** True while the Jolt press-and-hold weight-morph is engaged. */ /** True while the Jolt press-and-hold weight-morph is engaged. */
joltActive: boolean; joltActive: boolean;
@ -173,10 +144,6 @@ export interface ConsoleCtx {
// ---- Synth engine (dock-spec §5) ---- // ---- Synth engine (dock-spec §5) ----
audioStarted: boolean; audioStarted: boolean;
onToggleAudio: () => void; onToggleAudio: () => void;
volume: number;
setVolume: (v: number) => void;
bpm: number;
setBpm: (v: number) => void;
// ---- Explore-and-place scratchpad session (workstream B; rl-feedback §2.2) ---- // ---- Explore-and-place scratchpad session (workstream B; rl-feedback §2.2) ----
/** True while awaiting a manifold location pick after pressing "place". */ /** True while awaiting a manifold location pick after pressing "place". */

View file

@ -1,354 +0,0 @@
/**
* BackendAdvanced the FULL-depth advanced backend modal bodies (dock-spec §4).
* One editor per backend. All backends share the §3.1 baseline (rendered as
* OutputControlRow elsewhere); these add the backend-specific fields.
*
* The backend transport (backends-spec workstream E) is now LIVE: editing these
* fields writes the shared MFParam store, which the BackendManager reads to send
* real Web MIDI CC / OSC-over-WS. This modal is the full-depth duplicate of the
* inline config in OutputsBackendConfig; both write the same store.
*/
import type { MFParam } from '../console/model';
import type { BackendId, CvChannelId } from './output-state';
import { CV_CHANNELS, defaultCvSpec, defaultMidiSpec, defaultOscSpec } from './output-state';
function num(s: string, fallback: number): number {
const v = parseFloat(s);
return Number.isFinite(v) ? v : fallback;
}
const cellInput: React.CSSProperties = {
width: '100%',
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
color: 'var(--fg)',
fontFamily: 'var(--font-mono)',
fontSize: 'var(--fs-xs)',
padding: '3px 6px',
};
function Th({ children }: { children: React.ReactNode }) {
return (
<th
style={{
textAlign: 'left',
fontSize: 9,
textTransform: 'uppercase',
letterSpacing: '0.06em',
color: 'var(--fg-dim)',
padding: '4px 6px',
borderBottom: '1px solid var(--line)',
}}
>
{children}
</th>
);
}
export interface BackendAdvancedProps {
backend: BackendId;
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}
export function BackendAdvanced({ backend, params, setParam }: BackendAdvancedProps) {
switch (backend) {
case 'midi':
return <MidiCcEditor params={params} setParam={setParam} />;
case 'osc':
return <OscPathEditor params={params} setParam={setParam} />;
case 'vcv':
return <VcvChannelEditor params={params} setParam={setParam} />;
case 'cvgate':
return <CvChannelEditor params={params} setParam={setParam} />;
default:
return <SynthGroupNote params={params} />;
}
}
// ---- MIDI (dock-spec §4.1) -------------------------------------------------
function MidiCcEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
{params.length} CCs · live Web MIDI out (backends-spec §2.3). Editing the CC map here sends in
real time once a MIDI port is selected in the Outputs panel.
</p>
<div style={{ maxHeight: 360, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<Th>Name</Th>
<Th>CC#</Th>
<Th>Ch</Th>
<Th>State</Th>
</tr>
</thead>
<tbody>
{params.map((p, i) => {
const m = p.midi ?? defaultMidiSpec(i);
return (
<tr key={i}>
<td style={{ padding: '3px 6px' }}>
<input
style={cellInput}
value={m.name}
onChange={(e) => setParam(i, { midi: { ...m, name: e.target.value } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 70 }}>
<input
type="number"
min={0}
max={127}
style={cellInput}
value={m.cc}
onChange={(e) =>
setParam(i, {
midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) },
})
}
/>
</td>
<td style={{ padding: '3px 6px', width: 60 }}>
<input
type="number"
min={1}
max={16}
style={cellInput}
value={m.channel}
onChange={(e) =>
setParam(i, {
midi: {
...m,
channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))),
},
})
}
/>
</td>
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
{p.status}
{p.muted ? ' · muted' : ''}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
// ---- OSC (dock-spec §4.2) --------------------------------------------------
function OscPathEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
Live OSC over the WebSocket bridge (backends-spec §2.4). Set the bridge URL + per-output paths in
the Outputs panel; emits only while the bridge process is connected.
</p>
<div style={{ maxHeight: 360, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<Th>OSC path</Th>
<Th>range min</Th>
<Th>range max</Th>
</tr>
</thead>
<tbody>
{params.map((p, i) => {
const o = p.osc ?? defaultOscSpec(p.name);
return (
<tr key={i}>
<td style={{ padding: '3px 6px' }}>
<input
style={cellInput}
value={o.path}
onChange={(e) => setParam(i, { osc: { ...o, path: e.target.value } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 90 }}>
<input
type="number"
style={cellInput}
value={o.rangeMin}
onChange={(e) => setParam(i, { osc: { ...o, rangeMin: num(e.target.value, o.rangeMin) } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 90 }}>
<input
type="number"
style={cellInput}
value={o.rangeMax}
onChange={(e) => setParam(i, { osc: { ...o, rangeMax: num(e.target.value, o.rangeMax) } })}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
// ---- VCV / CV (dock-spec §4.3) ---------------------------------------------
function VcvChannelEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
VCV adds nothing beyond the baseline (min/max = range, fixed = freeze) plus per-channel polarity.
{/* TODO(backends-spec §2.6): the VCV browser↔module bridge transport is not yet wired here. */}
</p>
<div style={{ maxHeight: 360, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
{params.map((p, i) => {
const bipolar = p.vcv?.bipolar ?? false;
return (
<div
key={i}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
padding: '4px 8px',
}}
>
<span style={{ flex: 1, fontSize: 'var(--fs-xs)', color: 'var(--fg)' }}>{p.name}</span>
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>
{p.min.toFixed(2)}{p.max.toFixed(2)} · {p.status === 'fixed' ? 'frozen' : 'live'}
</span>
<button
type="button"
onClick={() => setParam(i, { vcv: { bipolar: !bipolar } })}
style={{
fontSize: 9,
fontFamily: 'var(--font-mono)',
padding: '2px 8px',
cursor: 'pointer',
borderRadius: 'var(--r-pill)',
border: `1px solid ${bipolar ? 'var(--danger)' : 'var(--line)'}`,
background: 'transparent',
color: bipolar ? 'var(--danger)' : 'var(--fg-mute)',
}}
>
{bipolar ? '±5 V' : '010 V'}
</button>
</div>
);
})}
</div>
</div>
);
}
// ---- uSEQ CV / gate (docs/specs/useq-cv-protocol.md) -------------------------------------
function CvChannelEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
Live CV/gate over USB serial to a uSEQ module + expander (docs/specs/useq-cv-protocol.md). Assign each model output to a
CV jack or gate; gates threshold the mapped 01 value. Connect the device in the Outputs panel.
</p>
<div style={{ maxHeight: 360, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<Th>Output</Th>
<Th>uSEQ channel</Th>
<Th>Gate </Th>
</tr>
</thead>
<tbody>
{params.map((p, i) => {
const c = (p.cv as { channel: CvChannelId; gateThreshold: number } | undefined) ?? defaultCvSpec(i);
const isGate = c.channel.startsWith('gate');
return (
<tr key={i}>
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>{p.name}</td>
<td style={{ padding: '3px 6px', width: 150 }}>
<select
value={c.channel}
onChange={(e) => setParam(i, { cv: { ...c, channel: e.target.value as CvChannelId } })}
style={{ ...cellInput, cursor: 'pointer' }}
>
<option value="none"> none </option>
{CV_CHANNELS.map((ch) => (
<option key={ch.id} value={ch.id}>
{ch.label}
</option>
))}
</select>
</td>
<td style={{ padding: '3px 6px', width: 80 }}>
<input
type="number"
min={0}
max={1}
step={0.05}
disabled={!isGate}
style={{ ...cellInput, opacity: isGate ? 1 : 0.4 }}
value={c.gateThreshold}
onChange={(e) =>
setParam(i, {
cv: { ...c, gateThreshold: Math.max(0, Math.min(1, num(e.target.value, c.gateThreshold))) },
})
}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
function SynthGroupNote({ params }: { params: MFParam[] }) {
const groups = Array.from(new Set(params.map((p) => p.group)));
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px', lineHeight: 1.6 }}>
The synth backend's advanced surface is the group-override matrix see the Powerful Synth Engine
drawer's full depth (dock-spec §4.4 / §5). Groups: {groups.join(' · ')}.
</p>
</div>
);
}

View file

@ -12,8 +12,10 @@
* - Synth/Particle/Editor handled by ModeConfig in Drawers (no extra config here). * - Synth/Particle/Editor handled by ModeConfig in Drawers (no extra config here).
* *
* Everything is editable inline; writes go through the shared MFParam store * Everything is editable inline; writes go through the shared MFParam store
* (ctx.setParam) never a second data path. The full-depth modal reuses the * (ctx.setParam) never a second data path. This is the ONLY per-backend
* same sections via BackendAdvanced. * config editor (BackendAdvanced.tsx, a full-depth duplicate that rendered
* alongside this one in the same expanded drawer, was deleted 2026-07
* simplification audit S18).
*/ */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { ConsoleCtx } from '../console/types'; import type { ConsoleCtx } from '../console/types';

View file

@ -14,8 +14,8 @@
* They compose freely (e.g. an output can be `off` AND `muted` AND `armed`). * They compose freely (e.g. an output can be `off` AND `muted` AND `armed`).
* Recorded in ALIGNMENT.md. * Recorded in ALIGNMENT.md.
* *
* To keep the dock tri-state and the existing OutputStage / ReadoutStrip * To keep the dock tri-state and the existing OutputStage tri-state in sync
* tri-state in sync WITHOUT a second data path, this model is folded onto the * WITHOUT a second data path, this model is folded onto the
* existing `MFParam` (model.ts) `MFParam.status` carries `state`, and the new * existing `MFParam` (model.ts) `MFParam.status` carries `state`, and the new
* `muted` / `armed` / backend fields live alongside it. ConsoleApp owns the * `muted` / `armed` / backend fields live alongside it. ConsoleApp owns the
* single `MFParam[]` store; the dock and the stage both read/write it. * single `MFParam[]` store; the dock and the stage both read/write it.
@ -29,23 +29,6 @@ export type OutputState = ParamStatus; // 'off' | 'fixed' | 'live'
/** The selectable output backend (dock-spec §3.4; backends-spec §1). */ /** The selectable output backend (dock-spec §3.4; backends-spec §1). */
export type BackendId = 'synth' | 'particles' | 'midi' | 'osc' | 'cvgate' | 'vcv'; export type BackendId = 'synth' | 'particles' | 'midi' | 'osc' | 'cvgate' | 'vcv';
export interface BackendDescriptor {
id: BackendId;
/** Dock label — NEVER "C15" (backends-spec naming guard). */
label: string;
description: string;
}
/** The backend roster surfaced in the dock's backend selector. */
export const BACKENDS: readonly BackendDescriptor[] = [
{ id: 'synth', label: 'Powerful Synth Engine', description: 'Firmware-parity built-in audio engine.' },
{ id: 'midi', label: 'MIDI', description: 'Web MIDI CC out — per-output CC#/channel.' },
{ id: 'osc', label: 'OSC', description: 'OSC bridge — named paths + physical ranges.' },
{ id: 'cvgate', label: 'CV', description: 'uSEQ CV/gate over USB serial — 11 CV + 3 gate.' },
{ id: 'vcv', label: 'VCV', description: 'VCV Rack module — 16 CV outs with LED rings.' },
{ id: 'particles', label: 'Particle', description: 'Flow-field visualiser (no audio).' },
] as const;
// ---- Backend-specific per-output specs (dock-spec §4) ---------------------- // ---- Backend-specific per-output specs (dock-spec §4) ----------------------
/** MIDI CC backend per-output extras (dock-spec §4.1). */ /** MIDI CC backend per-output extras (dock-spec §4.1). */
@ -111,47 +94,6 @@ export interface CvSpec {
gateThreshold: number; gateThreshold: number;
} }
/**
* The full per-output control. This is the spec's `OutputControl` (dock-spec
* §3.2). It is represented on `MFParam` for the shared store; this interface
* documents the complete contract and is what {@link toOutputControl} yields.
*/
export interface OutputControl {
index: number;
name: string;
group: string;
state: OutputState; // off | fixed | live
muted: boolean; // downstream silence; still computed
armed: boolean; // solo / focus-training (=arm)
min: number; // [0,1]
max: number; // [0,1], min<=max
curve: number; // [0,1], 0.5 linear
fixedValue: number; // held value when state==='fixed'
// backend-specific, populated by the active backend adapter:
midi?: MidiCcSpec;
osc?: OscSpec;
vcv?: VcvSpec;
}
/** Project an MFParam (the shared store row) into the full OutputControl view. */
export function toOutputControl(p: MFParam, index: number): OutputControl {
return {
index,
name: p.name,
group: p.group,
state: p.status,
muted: p.muted ?? false,
armed: p.armed ?? false,
min: p.min,
max: p.max,
curve: p.curve,
fixedValue: p.val,
midi: p.midi,
osc: p.osc,
vcv: p.vcv,
};
}
/** /**
* Build the focus / solo mask from the per-row armed flags (dock-spec §1.2). * Build the focus / solo mask from the per-row armed flags (dock-spec §1.2).
* Returns null when nothing is armed ( all outputs active / no focus). * Returns null when nothing is armed ( all outputs active / no focus).

View file

@ -15,7 +15,7 @@
* process() re-run last input after a weight change * process() re-run last input after a weight change
* addExample([x,y], outVec) append a training example * addExample([x,y], outVec) append a training example
* train() SGD over the dataset * train() SGD over the dataset
* feedback.{setFocus,thumbsUp,dislikeGeometric,storePositive,} * feedback.{setFocus,thumbsUp,dislikeGeometric,}
* the SHARED C++ core's RL primitives * the SHARED C++ core's RL primitives
* *
* As of one-core-engine P3 the geometric push-away (Mode 1) is a C++ core * As of one-core-engine P3 the geometric push-away (Mode 1) is a C++ core
@ -55,9 +55,6 @@ export interface Anchor {
/** The minimal engine surface the controller needs (decoupled from EngineApi). */ /** The minimal engine surface the controller needs (decoupled from EngineApi). */
export interface ControllerEngine { export interface ControllerEngine {
getWeights(): Float32Array;
setWeights(w: Float32Array): void;
randomise(spread?: number): void;
setInput(x: number, y: number): void; setInput(x: number, y: number): void;
getOutputs(): Float32Array; getOutputs(): Float32Array;
process(): void; process(): void;
@ -70,8 +67,6 @@ export interface ControllerEngine {
// `heardVec` is the post-pipeline (HEARD) output; returns the FeedbackAction // `heardVec` is the post-pipeline (HEARD) output; returns the FeedbackAction
// int (14=GeometricPush, 15=GeometricColdStart). // int (14=GeometricPush, 15=GeometricColdStart).
dislikeGeometric(heardVec?: Float32Array, lr?: number): number; dislikeGeometric(heardVec?: Float32Array, lr?: number): number;
/** Feed a positive into the k-NN centroid (null → live MLP output). */
storePositive(vec?: Float32Array): void;
positiveCount(): number; positiveCount(): number;
negativeCount(): number; negativeCount(): number;
// ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place'). // ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place').
@ -87,8 +82,6 @@ export interface ControllerEngine {
like(): void; like(): void;
commitPlace(): void; commitPlace(): void;
cancelPlace(): void; cancelPlace(): void;
placing(): boolean;
exploreState(): number; // 0=Idle 1=Exploring 2=Placing
undoDepth(): number; undoDepth(): number;
placedOutput(): Float32Array | null; placedOutput(): Float32Array | null;
}; };
@ -115,24 +108,16 @@ export interface FeedbackControllerState {
} }
export interface FeedbackControllerOptions { export interface FeedbackControllerOptions {
/** Seed for the deterministic nudge RNG (NOT Math.random — task constraint). */
seed?: number;
/** Master spread for randomise / nudge (mirrors the engine spread knob). */ /** Master spread for randomise / nudge (mirrors the engine spread knob). */
spread?: number; spread?: number;
/** Nudge perturbation standard deviation (small bounded weight jitter). */ /** Nudge perturbation standard deviation (small bounded weight jitter). */
nudgeStddev?: number; nudgeStddev?: number;
/**
* Undo-stack depth. WASM D=4, firmware D=2 per rl-feedback-design §2.2; the
* prototype defaults to the WASM depth.
*/
undoDepth?: number;
} }
export class FeedbackController { export class FeedbackController {
private engine: ControllerEngine; private engine: ControllerEngine;
private spread: number; private spread: number;
private nudgeStddev: number; private nudgeStddev: number;
private maxUndo: number;
private mode: ProtoFeedbackMode = 'explore-and-place'; private mode: ProtoFeedbackMode = 'explore-and-place';
private soloMode: ProtoSoloMode = 'mask-gradients'; private soloMode: ProtoSoloMode = 'mask-gradients';
@ -162,7 +147,6 @@ export class FeedbackController {
this.engine = engine; this.engine = engine;
this.spread = opts.spread ?? 0.6; this.spread = opts.spread ?? 0.6;
this.nudgeStddev = opts.nudgeStddev ?? 0.05; this.nudgeStddev = opts.nudgeStddev ?? 0.05;
this.maxUndo = Math.max(1, opts.undoDepth ?? 4);
} }
// =================================================================== // ===================================================================
@ -367,23 +351,12 @@ export class FeedbackController {
* 5. cold-start fallback (negative-LR) when there are no positives yet. * 5. cold-start fallback (negative-LR) when there are no positives yet.
* Soloed/active dims come from the core's focus mask (set via setArmMask). * Soloed/active dims come from the core's focus mask (set via setArmMask).
* *
* @param input the control input the disliked sound was heard at (unused by
* the core it reads the MLP's live input kept for the marker /
* call-site symmetry with like()).
* @param output the HEARD (post-pipeline) output vector a_neg. MUST be the * @param output the HEARD (post-pipeline) output vector a_neg. MUST be the
* heard vector, not the raw MLP output, or the cold-start MSE * heard vector, not the raw MLP output, or the cold-start MSE
* derivative is zero (see engine.feedback.dislikeGeometric). * derivative is zero (see engine.feedback.dislikeGeometric).
* @param _speed legacy move_weights speed ignored (geometric path).
* @param _spread legacy move_weights spread ignored (geometric path).
* @returns the FeedbackAction int (14=GeometricPush, 15=GeometricColdStart). * @returns the FeedbackAction int (14=GeometricPush, 15=GeometricColdStart).
*/ */
dislike( dislike(output: Float32Array): number {
input: readonly [number, number],
output: Float32Array,
_speed: number,
_spread: number,
): number {
void input;
const action = this.engine.feedback.dislikeGeometric(output); const action = this.engine.feedback.dislikeGeometric(output);
this.engine.process(); this.engine.process();
return action; return action;

View file

@ -1,131 +0,0 @@
import type { CSSProperties, ReactNode } from 'react';
export interface ControlAxisProps {
label?: ReactNode;
/** Bipolar endpoint labels, e.g. ['Caution', 'Bold']. */
endpoints?: [ReactNode, ReactNode];
value?: number;
onChange?: (value: number) => void;
/** Live preset tag shown next to the label. */
preset?: ReactNode;
/** Per-axis track/thumb accent colour (any CSS colour or var()). */
accent?: string;
disabled?: boolean;
style?: CSSProperties;
}
/**
* Manifold ControlAxis a named macro slider with bipolar endpoint labels
* (e.g. Boldness: Caution Bold). Shows a live preset tag and value. The
* track accent can be themed per-axis via `accent`.
*
* Relies on the `.mf-axis-input` rules in `styles/primitives.css`; the accent
* is passed via the inline `--mf-axis-accent` custom property.
*/
export function ControlAxis({
label,
endpoints = ['', ''],
value = 0.5,
onChange,
preset,
accent = 'var(--accent)',
disabled = false,
style,
}: ControlAxisProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--sp-1)',
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-2)',
padding: 'var(--sp-2) var(--sp-3)',
fontFamily: 'var(--font-mono)',
opacity: disabled ? 0.5 : 1,
pointerEvents: disabled ? 'none' : 'auto',
...style,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-2)',
fontSize: 'var(--fs-sm)',
}}
>
<span
style={{
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.08em',
color: 'var(--fg)',
flex: 1,
}}
>
{label}
</span>
{preset && (
<span
style={{
color: accent,
fontSize: 'var(--fs-xs)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
}}
>
{preset}
</span>
)}
<span
style={{
fontVariantNumeric: 'tabular-nums',
color: 'var(--fg-mute)',
fontSize: 'var(--fs-xs)',
minWidth: '4ch',
textAlign: 'right',
}}
>
{value.toFixed(2)}
</span>
</div>
<input
type="range"
min={0}
max={1}
step={0.01}
value={value}
disabled={disabled}
onChange={(e) => onChange?.(parseFloat(e.target.value))}
className="mf-axis-input"
style={
{
WebkitAppearance: 'none',
appearance: 'none',
width: '100%',
height: 24,
background: 'transparent',
margin: 0,
cursor: 'pointer',
'--mf-axis-accent': accent,
} as CSSProperties
}
/>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: '0.08em',
color: 'var(--fg-dim)',
}}
>
<span>{endpoints[0]}</span>
<span>{endpoints[1]}</span>
</div>
</div>
);
}

View file

@ -1,131 +0,0 @@
import { useEffect, useRef } from 'react';
import type { CSSProperties } from 'react';
export type CurveName =
| 'linear'
| 'exp'
| 'log'
| 'square'
| 'sqrt'
| 'sigmoid'
| 'cubic'
| 'centered_power';
export interface CurvePlotProps {
/** One of the named response curves. Ignored when `fn` is provided. */
curve?: CurveName;
/** Custom response function f:[0,1]→[0,1]. Overrides `curve`. */
fn?: (x: number) => number;
width?: number;
height?: number;
/** Stroke colour (any CSS colour or var()). */
color?: string;
showAxes?: boolean;
ariaLabel?: string;
style?: CSSProperties;
}
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
const CURVES: Record<CurveName, (x: number) => number> = {
linear: (x) => x,
exp: (x) => (Math.exp(4 * x) - 1) / (Math.exp(4) - 1),
log: (x) => Math.log(1 + x * (Math.exp(4) - 1)) / 4,
square: (x) => x * x,
sqrt: (x) => Math.sqrt(clamp01(x)),
sigmoid: (x) => {
const s = (v: number) => 1 / (1 + Math.exp(-(v - 0.5) * 8));
const lo = s(0);
const hi = s(1);
return (s(x) - lo) / (hi - lo);
},
cubic: (x) => {
const v = clamp01(x);
return v * v * (3 - 2 * v);
},
centered_power: (x) => {
const o = x - 0.5;
const sg = o < 0 ? -1 : 1;
return clamp01((sg * Math.pow(Math.abs(o) * 2, 0.5)) / 2 + 0.5);
},
};
/**
* Manifold CurvePlot renders one of the named response curves (or a custom
* function f:[0,1][0,1]) on the dark grid. The brand's straight-line &
* parabolic/bézier motif.
*/
export function CurvePlot({
curve = 'cubic',
fn,
width = 200,
height = 120,
color = 'var(--accent)',
showAxes = true,
ariaLabel,
style,
}: CurvePlotProps) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const cv = ref.current;
if (!cv) return;
const dpr = window.devicePixelRatio || 1;
const w = width * dpr;
const h = height * dpr;
cv.width = w;
cv.height = h;
const ctx = cv.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, w, h);
const cs = getComputedStyle(cv);
const stroke = color.startsWith('var(')
? cs.getPropertyValue(color.slice(4, -1).trim()).trim() || '#ff6a00'
: color;
const pad = 6 * dpr;
if (showAxes) {
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
ctx.beginPath();
ctx.moveTo(0, h / 2);
ctx.lineTo(w, h / 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(w / 2, 0);
ctx.lineTo(w / 2, h);
ctx.stroke();
}
const f = fn || CURVES[curve] || CURVES.linear;
ctx.strokeStyle = stroke;
ctx.lineWidth = 2 * dpr;
ctx.beginPath();
for (let p = 0; p <= 120; p++) {
const x = p / 120;
const y = clamp01(f(x));
const px = pad + x * (w - 2 * pad);
const py = h - pad - y * (h - 2 * pad);
if (p === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.stroke();
}, [curve, fn, width, height, color, showAxes]);
return (
<canvas
ref={ref}
role="img"
aria-label={ariaLabel || `${curve} curve`}
style={{
display: 'block',
width,
height,
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-2)',
...style,
}}
/>
);
}

View file

@ -1,82 +0,0 @@
import type { CSSProperties, ReactNode } from 'react';
export interface PanelProps {
title?: ReactNode;
/** Small uppercase eyebrow shown before the title. */
label?: ReactNode;
/** Right-aligned header actions. */
actions?: ReactNode;
children?: ReactNode;
padding?: string;
style?: CSSProperties;
}
/**
* Manifold Panel the house surface: bg-1 fill, 1px hairline border, 8px
* radius, no shadow. Optional header row with an uppercase title + actions,
* separated by a hairline.
*/
export function Panel({
title,
label,
actions,
children,
padding = 'var(--sp-3)',
style,
}: PanelProps) {
return (
<section
style={{
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-2)',
fontFamily: 'var(--font-mono)',
color: 'var(--fg)',
...style,
}}
>
{(title || label || actions) && (
<header
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-2)',
padding: 'var(--sp-2) var(--sp-3)',
borderBottom: '1px solid var(--line)',
}}
>
{label && (
<span
style={{
fontSize: 'var(--fs-xs)',
color: 'var(--fg-dim)',
textTransform: 'uppercase',
letterSpacing: '0.08em',
}}
>
{label}
</span>
)}
{title && (
<h3
style={{
margin: 0,
fontSize: 'var(--fs-sm)',
fontWeight: 600,
color: 'var(--fg)',
}}
>
{title}
</h3>
)}
{actions && (
<div style={{ marginLeft: 'auto', display: 'flex', gap: 'var(--sp-2)' }}>
{actions}
</div>
)}
</header>
)}
<div style={{ padding }}>{children}</div>
</section>
);
}

View file

@ -1,116 +0,0 @@
import { useEffect, useRef } from 'react';
import type { CSSProperties } from 'react';
export interface SparklineProps {
data?: number[];
width?: number;
height?: number;
/** Stroke colour (any CSS colour or var()). */
color?: string;
/** Plot on a log scale (log(max(1e-10, v) + 1)). */
log?: boolean;
/** Render the last-value readout in the top-right. */
showLast?: boolean;
/** Custom formatter for the last-value readout. */
format?: (value: number) => string;
ariaLabel?: string;
style?: CSSProperties;
}
/**
* Manifold Sparkline a compact time-series trace (training loss, a feature
* envelope). Cyan line on a faint grid, with an optional last-value readout.
*/
export function Sparkline({
data = [],
width = 320,
height = 70,
color = 'var(--accent-2)',
log = false,
showLast = true,
format,
ariaLabel = 'time series',
style,
}: SparklineProps) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const cv = ref.current;
if (!cv) return;
const dpr = window.devicePixelRatio || 1;
const w = width * dpr;
const h = height * dpr;
cv.width = w;
cv.height = h;
const ctx = cv.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, w, h);
if (!data.length) return;
const cs = getComputedStyle(cv);
const stroke = color.startsWith('var(')
? cs.getPropertyValue(color.slice(4, -1).trim()).trim() || '#00ccff'
: color;
const ys = data.map((v) => (log ? Math.log(Math.max(1e-10, v) + 1) : v));
let lo = Infinity;
let hi = -Infinity;
for (const y of ys) {
if (y < lo) lo = y;
if (y > hi) hi = y;
}
if (hi === lo) hi = lo + 1e-6;
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 1;
for (let i = 1; i < 4; i++) {
const y = (i / 4) * h;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
ctx.strokeStyle = stroke;
ctx.lineWidth = 1.5 * dpr;
ctx.beginPath();
for (let i = 0; i < ys.length; i++) {
const x = (i / Math.max(1, ys.length - 1)) * w;
const norm = (ys[i] - lo) / (hi - lo);
const y = h - norm * h;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
if (showLast) {
const last = data[data.length - 1];
const txt = format
? format(last)
: typeof last === 'number'
? last.toExponential(2)
: String(last);
ctx.fillStyle = '#9a9a9a';
ctx.font = `${10 * dpr}px ui-monospace, monospace`;
ctx.textAlign = 'right';
ctx.fillText(txt, w - 4 * dpr, 12 * dpr);
}
}, [data, width, height, color, log, showLast, format]);
return (
<canvas
ref={ref}
role="img"
aria-label={ariaLabel}
style={{
display: 'block',
width,
height,
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-2)',
...style,
}}
/>
);
}

View file

@ -1,74 +0,0 @@
import { Fragment } from 'react';
import type { CSSProperties, ReactNode } from 'react';
export type StatusTone = 'accent' | 'cyan' | 'good' | 'warn' | 'bad';
export interface StatusItemObject {
label?: ReactNode;
value: ReactNode;
tone?: StatusTone;
}
export type StatusItem = string | StatusItemObject;
export interface StatusLineProps {
items?: StatusItem[];
style?: CSSProperties;
}
const TONE_COLORS: Record<StatusTone, string> = {
accent: 'var(--accent)',
cyan: 'var(--accent-2)',
good: 'var(--good)',
warn: 'var(--warn)',
bad: 'var(--bad)',
};
/**
* Manifold StatusLine the dim mono readout strip at the bottom of a mode.
* Pass an array of items; strings render plain, {label,value,tone} render a
* labelled readout. Items are joined with the house middle-dot separator.
*/
export function StatusLine({ items = [], style }: StatusLineProps) {
return (
<p
style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 'var(--sp-2)',
margin: 0,
fontFamily: 'var(--font-mono)',
fontSize: 'var(--fs-xs)',
color: 'var(--fg-dim)',
...style,
}}
>
{items.map((it, i) => {
const isObj = it !== null && typeof it === 'object';
const toneColor =
isObj && it.tone ? (TONE_COLORS[it.tone] ?? null) : null;
return (
<Fragment key={i}>
{i > 0 && <span aria-hidden="true">·</span>}
{isObj ? (
<span style={{ color: toneColor || 'var(--fg-dim)' }}>
{it.label && <span style={{ color: 'var(--fg-dim)' }}>{it.label} </span>}
<span
style={{
fontVariantNumeric: 'tabular-nums',
color: toneColor || 'var(--fg-mute)',
}}
>
{it.value}
</span>
</span>
) : (
<span>{it}</span>
)}
</Fragment>
);
})}
</p>
);
}

View file

@ -3,9 +3,13 @@
* on the Manifold design tokens. Ported from the window-global JSX reference * on the Manifold design tokens. Ported from the window-global JSX reference
* implementations in docs/redesign/manifold-export/components/. * implementations in docs/redesign/manifold-export/components/.
* *
* Side-effect import: pulls in the `.mf-slider-input` / `.mf-axis-input` * Side-effect import: pulls in the `.mf-slider-input` range-input styling that
* range-input styling that Slider and ControlAxis depend on. Importing this * Slider depends on. Importing this barrel anywhere in the app is enough to
* barrel anywhere in the app is enough to register those rules. * register those rules.
*
* Panel / StatusLine / ControlAxis / CurvePlot / Sparkline were deleted
* 2026-07 (simplification audit L22) zero consumers repo-wide. Sparkline/
* CurvePlot may return with the deferred training-health diagnostics suite.
*/ */
import '../styles/primitives.css'; import '../styles/primitives.css';
@ -18,34 +22,14 @@ export type { SliderProps } from './Slider';
export { PillToggle } from './PillToggle'; export { PillToggle } from './PillToggle';
export type { PillToggleProps, PillOption } from './PillToggle'; export type { PillToggleProps, PillOption } from './PillToggle';
export { Panel } from './Panel';
export type { PanelProps } from './Panel';
export { Badge } from './Badge'; export { Badge } from './Badge';
export type { BadgeProps, BadgeTone } from './Badge'; export type { BadgeProps, BadgeTone } from './Badge';
export { Switch } from './Switch'; export { Switch } from './Switch';
export type { SwitchProps } from './Switch'; export type { SwitchProps } from './Switch';
export { StatusLine } from './StatusLine';
export type {
StatusLineProps,
StatusItem,
StatusItemObject,
StatusTone,
} from './StatusLine';
export { XYPad } from './XYPad'; export { XYPad } from './XYPad';
export type { XYPadProps } from './XYPad'; export type { XYPadProps } from './XYPad';
export { VirtualJoystick } from './VirtualJoystick'; export { VirtualJoystick } from './VirtualJoystick';
export type { VirtualJoystickProps } from './VirtualJoystick'; export type { VirtualJoystickProps } from './VirtualJoystick';
export { ControlAxis } from './ControlAxis';
export type { ControlAxisProps } from './ControlAxis';
export { CurvePlot } from './CurvePlot';
export type { CurvePlotProps, CurveName } from './CurvePlot';
export { Sparkline } from './Sparkline';
export type { SparklineProps } from './Sparkline';

View file

@ -70,37 +70,3 @@
.mf-slider-input:focus::-webkit-slider-thumb { .mf-slider-input:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 3px var(--glow-focus); box-shadow: 0 0 0 3px var(--glow-focus);
} }
/* ---- ControlAxis (.mf-axis-input) ---- */
.mf-axis-input::-webkit-slider-runnable-track {
height: 6px;
border-radius: 999px;
background: var(--bg-3);
}
.mf-axis-input::-moz-range-track {
height: 6px;
border-radius: 999px;
background: var(--bg-3);
}
.mf-axis-input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--mf-axis-accent, var(--accent));
margin-top: -6px;
box-shadow: 0 0 10px var(--mf-axis-accent, var(--accent));
cursor: pointer;
}
.mf-axis-input::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--mf-axis-accent, var(--accent));
border: none;
box-shadow: 0 0 10px var(--mf-axis-accent, var(--accent));
}
.mf-axis-input:focus {
outline: none;
}