feat(manifold): verdict-cluster + dock polish
- thumbs-up → green; down/explore → orange (dice icon in explore mode, thumb-down in dislike mode); undo button shrunk - add nudge | randomise two-segment pill under the verdict cluster - dock drawers closed by default; collapse to two depths (condensed side-panel / expanded ~80% centred modal) with a left-edge expand tab - Mode button → 'M' (white border, orange letter) - smoke test asserts rail button title (drawers now closed by default)
This commit is contained in:
parent
453047612a
commit
cb28facc65
7 changed files with 329 additions and 172 deletions
|
|
@ -106,8 +106,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
|||
const [tame, setTame] = useState(0.85);
|
||||
const [health, setHealth] = useState(0.8);
|
||||
const [rev, setRev] = useState(1);
|
||||
const [active, setActive] = useState<DrawerKey | null>('learn');
|
||||
const [depth, setDepth] = useState<DrawerDepth>('peek');
|
||||
const [active, setActive] = useState<DrawerKey | null>(null);
|
||||
const [depth, setDepth] = useState<DrawerDepth>('condensed');
|
||||
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
|
||||
const [sandwich, setSandwich] = useState(false);
|
||||
|
||||
|
|
@ -222,8 +222,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
|||
setFollow(false);
|
||||
setPins([]);
|
||||
setMarkers([]);
|
||||
setActive('learn');
|
||||
setDepth('peek');
|
||||
setActive(null);
|
||||
setDepth('condensed');
|
||||
if (engine) engine.setInput(0.5, 0.5);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modeId]);
|
||||
|
|
@ -411,6 +411,23 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
|||
setRev((r) => r + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Nudge — a small bounded weight perturbation of the current net (the left
|
||||
* half of the secondary pill). Routes to the scratchpad while exploring,
|
||||
* otherwise nudges the real net directly.
|
||||
*/
|
||||
const nudgeNet = () => {
|
||||
const c = controllerRef.current;
|
||||
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
||||
c.nudge();
|
||||
} else {
|
||||
engine?.feedback.nudge(0.05);
|
||||
engine?.process();
|
||||
}
|
||||
syncController();
|
||||
setRev((r) => r + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo. While exploring (Mode 2) this pops the scratchpad undo ring (reroll /
|
||||
* nudge). Otherwise it falls back to the UI snapshot stack (visual A/B seed).
|
||||
|
|
@ -526,8 +543,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
|||
};
|
||||
if (map[e.key]) {
|
||||
setActive((a) => (a === map[e.key] ? null : map[e.key]));
|
||||
setDepth('peek');
|
||||
} else if (e.key === '\\') setDepth((d) => (d === 'full' ? 'peek' : 'full'));
|
||||
setDepth('condensed');
|
||||
} else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded'));
|
||||
else if (focus === 'composite' && e.key === '[') {
|
||||
e.preventDefault();
|
||||
setSplit((s) => Math.max(0, s - 0.04));
|
||||
|
|
@ -903,6 +920,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
|||
onUndo={undo}
|
||||
onCommit={commit}
|
||||
onReroll={reroll}
|
||||
onNudge={nudgeNet}
|
||||
onRandomise={reroll}
|
||||
canUndo={feedbackMode === 'explore-and-place' && exploring ? undoDepth > 0 : snapshots.length > 0}
|
||||
ab={ab}
|
||||
onToggleAB={toggleAB}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* The Console dock — right-edge 48px rail + one mutually-exclusive drawer with
|
||||
* three depth states (Peek 320 / Expand 520 / Full modal).
|
||||
* two depth states: condensed (360px side panel) and expanded (centred modal,
|
||||
* ~80% of the viewport). A vertically-centred tab on the panel's left edge
|
||||
* toggles between them; drawers are closed by default.
|
||||
*
|
||||
* RESTRUCTURED (operator dock restructure):
|
||||
* - TOP, pinned: the **Mode** selector — the active OUTPUT BACKEND/target
|
||||
|
|
@ -22,7 +24,6 @@ import type { ConsoleCtx, DrawerDepth, DrawerKey, OutputMode } from './types';
|
|||
import { OUTPUT_MODES } from './output-mode';
|
||||
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
||||
import {
|
||||
ModeIcon,
|
||||
ParticleIcon,
|
||||
MidiIcon,
|
||||
OscIcon,
|
||||
|
|
@ -80,17 +81,20 @@ function ModeSelector({
|
|||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 'var(--r-2)',
|
||||
border: '1px solid var(--accent)',
|
||||
border: '1px solid #ffffff',
|
||||
background: 'rgba(255,106,0,0.12)',
|
||||
color: 'var(--accent)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 18,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{mono ? <ModeIcon /> : '⊞'}
|
||||
M
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
|
|
@ -196,7 +200,7 @@ export function Dock({ ctx, active, setActive, depth, setDepth, sandwich, setSan
|
|||
title={s.label}
|
||||
onClick={() => {
|
||||
setActive(on ? null : key);
|
||||
setDepth('peek');
|
||||
setDepth('condensed');
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
|
|
@ -234,107 +238,146 @@ export function Dock({ ctx, active, setActive, depth, setDepth, sandwich, setSan
|
|||
);
|
||||
};
|
||||
|
||||
const width = depth === 'expand' ? 520 : 320;
|
||||
const full = depth === 'full';
|
||||
const expanded = depth === 'expanded';
|
||||
const section = active ? DRAWERS[active] : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Dim backdrop behind the centred modal; click to collapse back. */}
|
||||
{section && expanded && (
|
||||
<div
|
||||
onClick={() => setDepth('condensed')}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 85,
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
WebkitBackdropFilter: 'blur(2px)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{section && (
|
||||
// Shell: positions/sizes the drawer and is the anchor for the left-edge
|
||||
// tab. overflow:visible so the tab (at left:-22) is NOT clipped; the
|
||||
// inner wrapper handles scrolling.
|
||||
<aside
|
||||
style={
|
||||
full
|
||||
expanded
|
||||
? {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '80vw',
|
||||
height: '80vh',
|
||||
zIndex: 90,
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
padding: 'var(--sp-6)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--sp-3)',
|
||||
border: '1px solid var(--glass-line)',
|
||||
borderRadius: 'var(--r-3)',
|
||||
boxShadow: 'var(--shadow-2)',
|
||||
overflow: 'visible',
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 48,
|
||||
bottom: 0,
|
||||
width,
|
||||
width: 360,
|
||||
zIndex: 35,
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
borderLeft: '1px solid var(--glass-line)',
|
||||
boxShadow: '-8px 0 24px rgba(0,0,0,0.4)',
|
||||
padding: 'var(--sp-4)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--sp-2)',
|
||||
overflow: 'auto',
|
||||
overflow: 'visible',
|
||||
animation: 'mfDrawerIn var(--dur-med) var(--ease-console)',
|
||||
}
|
||||
}
|
||||
>
|
||||
<header
|
||||
{/* Expand/collapse tab — vertically centred on the panel's left edge. */}
|
||||
<button
|
||||
type="button"
|
||||
title={expanded ? 'Condense' : 'Expand'}
|
||||
onClick={() => setDepth(expanded ? 'condensed' : 'expanded')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: -22,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
width: 22,
|
||||
height: 56,
|
||||
borderRadius: 'var(--r-2) 0 0 var(--r-2)',
|
||||
border: '1px solid var(--glass-line)',
|
||||
borderRight: 'none',
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
color: 'var(--fg-mute)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--sp-2)',
|
||||
borderBottom: '1px solid var(--glass-line)',
|
||||
paddingBottom: 'var(--sp-2)',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}>
|
||||
{mono ? section.icon : <span style={{ fontSize: 'var(--fs-md)' }}>{section.glyph}</span>}
|
||||
</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'var(--fs-md)', color: 'var(--fg)' }}>{section.label}</h3>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--fg-dim)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
{depth}
|
||||
</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 4 }}>
|
||||
{!full && (
|
||||
<button
|
||||
type="button"
|
||||
title={depth === 'peek' ? 'More' : 'Peek'}
|
||||
onClick={() => setDepth(depth === 'peek' ? 'expand' : 'peek')}
|
||||
style={dockMini}
|
||||
>
|
||||
<ExpandIcon size={12} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
title="Open full"
|
||||
onClick={() => setDepth(full ? 'peek' : 'full')}
|
||||
style={dockMini}
|
||||
>
|
||||
<ExpandIcon size={12} />
|
||||
</button>
|
||||
<button type="button" title="Close" onClick={() => setActive(null)} style={dockMini}>
|
||||
<CloseIcon size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<ExpandIcon size={12} />
|
||||
</button>
|
||||
{/* Inner scroll wrapper — holds header + content, scrolls independently. */}
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
padding: expanded ? 'var(--sp-6)' : 'var(--sp-4)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--sp-2)',
|
||||
flex: 1,
|
||||
...(full ? { maxWidth: 720 } : {}),
|
||||
gap: expanded ? 'var(--sp-3)' : 'var(--sp-2)',
|
||||
borderRadius: 'inherit',
|
||||
}}
|
||||
>
|
||||
{section.render(ctx, depth)}
|
||||
<header
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--sp-2)',
|
||||
borderBottom: '1px solid var(--glass-line)',
|
||||
paddingBottom: 'var(--sp-2)',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}>
|
||||
{mono ? section.icon : <span style={{ fontSize: 'var(--fs-md)' }}>{section.glyph}</span>}
|
||||
</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'var(--fs-md)', color: 'var(--fg)' }}>{section.label}</h3>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--fg-dim)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
{depth}
|
||||
</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 4 }}>
|
||||
<button type="button" title="Close" onClick={() => setActive(null)} style={dockMini}>
|
||||
<CloseIcon size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--sp-2)',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{section.render(ctx, depth)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Console — the FIVE real dock drawers (operator dock restructure). Each renderer
|
||||
* takes (ctx, depth); what shows is gated by depth (peek | expand | full):
|
||||
* takes (ctx, depth); what shows is gated by depth (condensed | expanded):
|
||||
*
|
||||
* learn — Learning : feedback-mode selector, solo/arm chooser, live params
|
||||
* inputs — Inputs : input source
|
||||
|
|
@ -162,13 +162,13 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
|
||||
<SectionLabel>Down action · feedback mode</SectionLabel>
|
||||
<Segmented value={ctx.feedbackMode} onChange={ctx.setFeedbackMode} options={FEEDBACK_OPTS} />
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
{FEEDBACK_DESC[ctx.feedbackMode]}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Solo / arm scope</SectionLabel>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
|
|
@ -204,7 +204,7 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{depth === 'full' && (
|
||||
{depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Feedback lab</SectionLabel>
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
|
|
@ -339,21 +339,21 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
|
||||
<SectionLabel>Input source</SectionLabel>
|
||||
<Segmented value={inp.inputMode} onChange={inp.setInputMode} options={INPUT_MODE_OPTS} />
|
||||
{active && depth !== 'peek' && (
|
||||
{active && depth === 'expanded' && (
|
||||
<span style={{ fontSize: 9, color: STATUS_TONE[active.status.state] ?? 'var(--fg-dim)' }}>
|
||||
{active.status.message}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* ---- Internal (XY pad / manifold) ---- */}
|
||||
{inp.inputMode === 'internal' && depth !== 'peek' && (
|
||||
{inp.inputMode === 'internal' && depth === 'expanded' && (
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
Drag the on-screen manifold / XY pad. Two axes feed the net directly — this is the default.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ---- Game Controller ---- */}
|
||||
{inp.inputMode === 'gamepad' && depth !== 'peek' && (
|
||||
{inp.inputMode === 'gamepad' && depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Sticks</SectionLabel>
|
||||
<Segmented
|
||||
|
|
@ -384,7 +384,7 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
)}
|
||||
|
||||
{/* ---- MIDI ---- */}
|
||||
{inp.inputMode === 'midi' && depth !== 'peek' && (
|
||||
{inp.inputMode === 'midi' && depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Device</SectionLabel>
|
||||
{inp.midiInputs.length === 0 ? (
|
||||
|
|
@ -466,7 +466,7 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
)}
|
||||
|
||||
{/* ---- Reshape note (only when >2 axes feed the fixed WASM head) ---- */}
|
||||
{reshaping && depth !== 'peek' && (
|
||||
{reshaping && depth === 'expanded' && (
|
||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
{/* TODO(workstream F, docs/redesign/inputs-spec.md — "multiple WASM modules +
|
||||
warm-start"): give every axis its own genuine input dimension by (re)loading a
|
||||
|
|
@ -509,7 +509,7 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
|
||||
</div>
|
||||
<Slider label="master volume" value={ctx.volume} min={0} max={1} step={0.01} onChange={ctx.setVolume} />
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Tempo</SectionLabel>
|
||||
<Slider label="bpm" value={ctx.bpm} min={40} max={220} step={1} onChange={ctx.setBpm} />
|
||||
|
|
@ -530,7 +530,7 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
</>
|
||||
);
|
||||
case 'particles':
|
||||
return depth !== 'peek' ? (
|
||||
return depth === 'expanded' ? (
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
Flow-field visualiser driven by the first {Math.min(20, ctx.params.length)} outputs (no audio).
|
||||
{/* TODO(backends-spec §4): port FlowFieldVisualizer + visual preset chips. */}
|
||||
|
|
@ -539,13 +539,13 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
case 'midi':
|
||||
// The full MIDI config (port picker, CC count, per-output CC/channel/name)
|
||||
// + preset bar render via OutputsBackendConfig in RoutingDrawer below.
|
||||
return depth !== 'peek' ? (
|
||||
return depth === 'expanded' ? (
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
Each output sends a real Web MIDI CC. Pick a port and set CC# / channel per output.
|
||||
</p>
|
||||
) : null;
|
||||
case 'osc':
|
||||
return depth !== 'peek' ? (
|
||||
return depth === 'expanded' ? (
|
||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
Each output sends to an OSC path with a physical range, over the WebSocket bridge.
|
||||
</p>
|
||||
|
|
@ -568,23 +568,8 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
const nameFor = (idx: number, fallback: string) =>
|
||||
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
|
||||
|
||||
if (depth === 'full') {
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Chip tone="var(--accent)">{modeDesc.label}</Chip>
|
||||
<BackendStatusChip ctx={ctx} />
|
||||
</div>
|
||||
{ModeConfig(ctx, depth)}
|
||||
{/* Specialised, editable per-backend config (MIDI/OSC) + named-preset bar. */}
|
||||
<OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />
|
||||
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
|
||||
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const rows = depth === 'peek' ? ctx.params.slice(0, 6) : ctx.params;
|
||||
const expanded = depth === 'expanded';
|
||||
const rows = expanded ? ctx.params : ctx.params.slice(0, 6);
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
|
|
@ -596,15 +581,15 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
<BackendStatusChip ctx={ctx} />
|
||||
</div>
|
||||
{ModeConfig(ctx, depth)}
|
||||
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden at peek. */}
|
||||
{depth !== 'peek' && <OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />}
|
||||
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
|
||||
{expanded && <OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />}
|
||||
<SectionLabel>Outputs · M mute · S arm · off/fixed/live</SectionLabel>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
maxHeight: depth === 'peek' ? 220 : 460,
|
||||
maxHeight: expanded ? 460 : 220,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
|
|
@ -617,14 +602,20 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
param={labelled}
|
||||
value={values[i] ?? 0}
|
||||
onChange={(patch) => ctx.setParam(i, patch)}
|
||||
showCurve={depth !== 'peek'}
|
||||
showCurve={expanded}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{depth === 'peek' && 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>
|
||||
)}
|
||||
{expanded && (
|
||||
<>
|
||||
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
|
||||
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -654,7 +645,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
|||
onChange={(v) => set('monochromeIcons', v)}
|
||||
label="Monochrome icons"
|
||||
/>
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<>
|
||||
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>Unfocused icon colour</div>
|
||||
<PillToggle
|
||||
|
|
@ -683,7 +674,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
|||
onChange={(v) => set('inputMap', v as InputMapMode)}
|
||||
options={INPUT_MAP_OPTS}
|
||||
/>
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
The 2D input surface: a rectangular XY map or a circular joystick-style disc. "Follow mode"
|
||||
uses the active mode's declared input (joystick → circular, else rectangular).
|
||||
|
|
@ -700,7 +691,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
|||
unit="px"
|
||||
onChange={(v) => set('cornerRadius', Math.round(v))}
|
||||
/>
|
||||
{depth !== 'peek' && (
|
||||
{depth === 'expanded' && (
|
||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||
Roundness of buttons, control rows, dock icons and panels. Pills and the circular verdict
|
||||
buttons are intentionally exempt. Default 2px.
|
||||
|
|
@ -716,7 +707,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
|||
|
||||
const KEYS: [string, string][] = [
|
||||
['1–5', 'open drawers'],
|
||||
['\\', 'full depth'],
|
||||
['\\', 'expand drawer'],
|
||||
['space / ↑', 'commit +'],
|
||||
['↓', 'perturb / down −'],
|
||||
['z', 'undo'],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { FeedbackModeUI } from './types';
|
||||
import { DiceIcon } from './icons';
|
||||
|
||||
function ThumbIcon({ size = 24, down = false }: { size?: number; down?: boolean }) {
|
||||
return (
|
||||
|
|
@ -44,6 +45,10 @@ export interface VerdictClusterProps {
|
|||
onUndo: () => void;
|
||||
onCommit: () => void;
|
||||
onReroll: () => void;
|
||||
/** Small bounded weight perturbation of the current net (left half of the pill). */
|
||||
onNudge: () => void;
|
||||
/** Full re-roll of the current net (right half of the pill). */
|
||||
onRandomise: () => void;
|
||||
canUndo: boolean;
|
||||
ab: 'A' | 'B';
|
||||
onToggleAB: () => void;
|
||||
|
|
@ -62,6 +67,8 @@ export function VerdictCluster({
|
|||
onUndo,
|
||||
onCommit,
|
||||
onReroll,
|
||||
onNudge,
|
||||
onRandomise,
|
||||
canUndo,
|
||||
ab,
|
||||
onToggleAB,
|
||||
|
|
@ -116,6 +123,22 @@ export function VerdictCluster({
|
|||
...extra,
|
||||
});
|
||||
|
||||
// Two halves of the secondary pill (nudge | randomise).
|
||||
const pillHalf = (extra: CSSProperties): CSSProperties => ({
|
||||
flex: 1,
|
||||
height: 30,
|
||||
border: 0,
|
||||
background: 'transparent',
|
||||
color: 'var(--fg-mute)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--fs-xs)',
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
transition: 'background var(--dur-fast), color var(--dur-fast)',
|
||||
...extra,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerEnter={() => setHover(true)}
|
||||
|
|
@ -126,75 +149,138 @@ export function VerdictCluster({
|
|||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--sp-3)',
|
||||
padding: 'var(--sp-2) var(--sp-3)',
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
border: '1px solid var(--glass-line)',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
boxShadow: 'var(--shadow-2)',
|
||||
gap: 'var(--sp-2)',
|
||||
opacity: hover || firstSession ? 1 : 0.55,
|
||||
transition: 'opacity var(--dur-med) var(--ease-console)',
|
||||
zIndex: 40,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
title={downTitle}
|
||||
onPointerDown={perturbDown}
|
||||
onPointerUp={perturbUp}
|
||||
onPointerLeave={() => {
|
||||
if (lp.current) clearTimeout(lp.current);
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--sp-3)',
|
||||
padding: 'var(--sp-2) var(--sp-3)',
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
border: '1px solid var(--glass-line)',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
boxShadow: 'var(--shadow-2)',
|
||||
}}
|
||||
style={big(
|
||||
explore
|
||||
? exploring
|
||||
? { background: 'rgba(0,204,255,0.16)', color: 'var(--accent-2)' }
|
||||
: { background: 'rgba(0,204,255,0.10)', color: 'var(--accent-2)' }
|
||||
: { background: 'rgba(255,68,102,0.16)', color: 'var(--danger)' },
|
||||
)}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
>
|
||||
{/* Explore-mode down is a re-roll/explore (↻), not a dislike thumb. */}
|
||||
{explore ? <span style={{ fontSize: 24, lineHeight: 1 }}>↻</span> : <ThumbIcon down />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title={downTitle}
|
||||
onPointerDown={perturbDown}
|
||||
onPointerUp={perturbUp}
|
||||
onPointerLeave={() => {
|
||||
if (lp.current) clearTimeout(lp.current);
|
||||
}}
|
||||
style={big({
|
||||
background: explore && exploring ? 'rgba(255,106,0,0.22)' : 'rgba(255,106,0,0.16)',
|
||||
color: 'var(--accent)',
|
||||
})}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
>
|
||||
{/* Explore mode = dice (re-roll into a scratchpad); dislike mode = thumb-down. */}
|
||||
{explore ? <DiceIcon size={24} /> : <ThumbIcon down />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Undo (z)"
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
style={big({
|
||||
width: 48,
|
||||
height: 48,
|
||||
fontSize: 20,
|
||||
background: 'var(--bg-2)',
|
||||
color: 'var(--fg-mute)',
|
||||
opacity: canUndo ? 1 : 0.4,
|
||||
cursor: canUndo ? 'pointer' : 'not-allowed',
|
||||
})}
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Undo (z)"
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
style={big({
|
||||
width: 36,
|
||||
height: 36,
|
||||
fontSize: 16,
|
||||
background: 'var(--bg-2)',
|
||||
color: 'var(--fg-mute)',
|
||||
opacity: canUndo ? 1 : 0.4,
|
||||
cursor: canUndo ? 'pointer' : 'not-allowed',
|
||||
})}
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={upTitle}
|
||||
onClick={onCommit}
|
||||
style={big({
|
||||
background: picking ? 'rgba(0,204,255,0.22)' : 'rgba(255,106,0,0.18)',
|
||||
color: picking ? 'var(--accent-2)' : 'var(--accent)',
|
||||
boxShadow: picking ? '0 0 16px var(--accent-2)' : '0 0 16px var(--glow-accent)',
|
||||
})}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
<button
|
||||
type="button"
|
||||
title={upTitle}
|
||||
onClick={onCommit}
|
||||
style={big(
|
||||
picking
|
||||
? {
|
||||
background: 'rgba(0,204,255,0.22)',
|
||||
color: 'var(--accent-2)',
|
||||
boxShadow: '0 0 16px var(--accent-2)',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(107,194,107,0.20)',
|
||||
color: 'var(--good)',
|
||||
boxShadow: '0 0 16px rgba(107,194,107,0.45)',
|
||||
},
|
||||
)}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
>
|
||||
{/* Explore-mode up is "place" (a pin glyph) once exploring. */}
|
||||
{explore && exploring ? <span style={{ fontSize: 22, lineHeight: 1 }}>⌖</span> : <ThumbIcon />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Secondary pill — two halves: nudge (small perturbation) | randomise (full re-roll). */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
width: 220,
|
||||
background: 'var(--glass)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
border: '1px solid var(--glass-line)',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
boxShadow: 'var(--shadow-2)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Explore-mode up is "place" (a pin glyph) once exploring. */}
|
||||
{explore && exploring ? <span style={{ fontSize: 22, lineHeight: 1 }}>⌖</span> : <ThumbIcon />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Nudge — small bounded perturbation of the current net"
|
||||
onClick={onNudge}
|
||||
style={pillHalf({ borderRight: '1px solid var(--glass-line)' })}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,106,0,0.12)';
|
||||
e.currentTarget.style.color = 'var(--accent)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = 'var(--fg-mute)';
|
||||
}}
|
||||
>
|
||||
nudge
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Randomise — full re-roll of the current net"
|
||||
onClick={onRandomise}
|
||||
style={pillHalf({})}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,106,0,0.12)';
|
||||
e.currentTarget.style.color = 'var(--accent)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = 'var(--fg-mute)';
|
||||
}}
|
||||
>
|
||||
randomise
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,22 @@ export function HelpIcon({ size = 18, style }: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
/** Dice — explore / re-roll into a scratchpad (a die showing five pips). */
|
||||
export function DiceIcon({ size = 22, style }: IconProps) {
|
||||
return svg(
|
||||
size,
|
||||
style,
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="16" rx="3" />
|
||||
<circle cx="8.5" cy="8.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
<circle cx="15.5" cy="8.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
<circle cx="12" cy="12" r="1.1" fill="currentColor" stroke="none" />
|
||||
<circle cx="8.5" cy="15.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
<circle cx="15.5" cy="15.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
/** Close (✕). */
|
||||
export function CloseIcon({ size = 14, style }: IconProps) {
|
||||
return svg(size, style, <path d="M6 6l12 12M18 6 6 18" />);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface Snapshot {
|
|||
}
|
||||
|
||||
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
|
||||
export type DrawerDepth = 'peek' | 'expand' | 'full';
|
||||
export type DrawerDepth = 'condensed' | 'expanded';
|
||||
export type Focus = 'in' | 'split' | 'out' | 'composite';
|
||||
|
||||
export interface Axes {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ test('engine loads, spine propagates, console renders', async ({ page }) => {
|
|||
// 4. The convertible Console rendered (assert on the dock drawer rail, which
|
||||
// is always present — the old "MEMLNaut" wordmark only shows outside
|
||||
// Particle mode now that the particle top bar is a heatmap strip).
|
||||
await expect(page.getByText('Learning')).toBeVisible();
|
||||
// Drawers are closed by default, so assert on the rail button's title
|
||||
// rather than the (hidden) drawer-header text.
|
||||
await expect(page.getByTitle('Learning')).toBeVisible();
|
||||
|
||||
// 5. No "C15" anywhere in the rendered UI.
|
||||
const body = await page.evaluate(() => document.body.innerText);
|
||||
|
|
|
|||
Loading…
Reference in a new issue