/** * VerdictCluster — floating bottom-centre control, the app's main verdict. * ▽ perturb (thumbs-down) · ↺ undo · △ commit (thumbs-up). * Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`. * * The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0): * * Explore & place (Mode 2, default): * thumbs-DOWN = enter explore / cancel explore (NEVER a dislike); * thumbs-UP = place (when exploring) / commit a like (when not). * Geometric dislike (Mode 1): * thumbs-DOWN = dislike (push away); * thumbs-UP = like + train. * * Wiring (in ConsoleApp): onCommit / onPerturb dispatch on the mode; onReroll = * re-roll the scratchpad (Mode 2) or the real net. */ 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 ( ); } export interface VerdictClusterProps { onPerturb: () => void; 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; firstSession: boolean; /** Active feedback mode — drives the cluster's labels/tones (rl-feedback §0). */ feedbackMode: FeedbackModeUI; /** True while a Mode-2 scratchpad session is active. */ exploring: boolean; /** True while awaiting a manifold location pick after "place". */ picking: boolean; } export function VerdictCluster({ onPerturb, onUndo, onCommit, onReroll, onNudge, onRandomise, canUndo, firstSession, feedbackMode, exploring, picking, }: VerdictClusterProps) { const explore = feedbackMode === 'explore-and-place'; // Labels per mode + session state. const downTitle = explore ? exploring ? 'Cancel explore — restore the real net' : 'Explore — re-roll into a scratchpad (hold to re-roll again)' : 'Dislike — push the sound away (hold to re-roll)'; const upTitle = explore ? exploring ? picking ? 'Tap the manifold to place this sound' : 'Place — pick a manifold location for this sound' : 'Commit — keep the current sound' : 'Like — reinforce + train'; const [hover, setHover] = useState(false); const lp = useRef | null>(null); const firedReroll = useRef(false); const perturbDown = () => { firedReroll.current = false; lp.current = setTimeout(() => { firedReroll.current = true; onReroll(); }, 600); }; const perturbUp = () => { if (lp.current) clearTimeout(lp.current); if (!firedReroll.current) onPerturb(); }; const big = (extra: CSSProperties): CSSProperties => ({ width: 64, height: 64, borderRadius: '50%', fontSize: 26, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', border: '1px solid var(--glass-line)', transition: 'transform var(--dur-fast) var(--ease-console), background var(--dur-fast)', ...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 (
setHover(true)} onPointerLeave={() => setHover(false)} style={{ position: 'absolute', bottom: 28, left: '50%', transform: 'translateX(-50%)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--sp-2)', opacity: hover || firstSession ? 1 : 0.55, transition: 'opacity var(--dur-med) var(--ease-console)', zIndex: 40, }} >
{/* Secondary pill — two halves: nudge (small perturbation) | randomise (full re-roll). */}
); }