fix(manifold): start push-away feedback on press
Some checks failed
CI / C++ tests + WASM + parity + lint (push) Has been cancelled
CI / Manifold typecheck + unit + e2e (push) Has been cancelled
CI / Firmware (RP2350, PlatformIO) (push) Has been cancelled

This commit is contained in:
monkey-w1n5t0n 2026-07-25 17:43:52 +02:00
parent 079720a572
commit a6b8f87d32
4 changed files with 45 additions and 11 deletions

View file

@ -80,9 +80,10 @@ The `+` (up) verdict is always "keep this" (`addExample` + train, `findings-feed
| Selector label (UI copy) | Engine `FeedbackMode` | Behaviour | | Selector label (UI copy) | Engine `FeedbackMode` | Behaviour |
|---|---|---| |---|---|---|
| **Push away** (Mode 1) | `Avoid` (`feedback.hpp` enum, `findings-feedback-behaviour.md:90`) | down → geometric-dislike: perturbs the mapping away from what you disliked. In the deployed core this routes to `move_weights(speed, spread, pinMask)` (`plans/feedback-modes-port-spec.md` §2.5 — the true k-NN centroid push is firmware-only, out of scope). | | **Push away** (Mode 1) | `Avoid` (`nisps/ml/feedback.hpp`) | down → geometric dislike: stores the rejection and immediately pushes the mapping away from what you disliked; controller-owned elapsed-time replay then continues optimising live rejections (`docs/adr/rl-feedback-design.md`, 2026-07-25 amendment). |
| **Explore & place** (Mode 2) | `RandomiseMlp` (`findings-feedback-behaviour.md:122`) | down → snapshot + `draw_weights(spread)` re-rolls the whole net into a scratchpad you audition by moving the joystick; `+`/drag commits a `+1` example at the chosen input and **restores the real net** (`findings-feedback-behaviour.md:135-146`); down-again **cancels** (restore snapshot). | | **Explore & place** (Mode 2) | `ExploreAndPlace` (`nisps/ml/feedback.hpp`) | down → snapshot + `draw_weights(spread)` re-rolls the whole net into a scratchpad you audition by moving the joystick; `+`/drag commits a positive anchor at the chosen input and **restores the real net**; re-roll and cancel remain distinct Explore-and-place actions (`docs/adr/rl-feedback-design.md` §2.2). |
- **Verdict pointer timing is mode-specific:** in **Push away**, pointer-down MUST dispatch exactly one geometric dislike immediately; holding, pointer-up, and release MUST NOT dispatch another dislike, re-roll/randomise, or cross-route into Explore-and-place. Controller-owned elapsed-time replay MAY continue after that single UI dispatch and is not a repeated held-pointer dispatch. **Explore & place alone** MAY interpret its existing long press as a re-roll.
- A third engine mode `RandomiseOutputs` (bypass MLP, hold a static random vector, `findings-feedback-behaviour.md:111`) exists in the core but is **not** surfaced as a product mode in v1 — expose it only behind `?debug=1` as "Static roll". **(Open choice 1.)** - A third engine mode `RandomiseOutputs` (bypass MLP, hold a static random vector, `findings-feedback-behaviour.md:111`) exists in the core but is **not** surfaced as a product mode in v1 — expose it only behind `?debug=1` as "Static roll". **(Open choice 1.)**
- **Peek**: a 2-segment pill (`Push away` / `Explore & place`), the current mode highlighted in `--accent`. This pill is *also* mirrored next to the Verdict cluster (`VerdictCluster`, `findings-design-and-manifold.md:50`) so it is reachable during live play without opening the drawer — matching how a-immersive puts the `rl-label` above the RL buttons (`aimmersive-clone-spec.md:283`). - **Peek**: a 2-segment pill (`Push away` / `Explore & place`), the current mode highlighted in `--accent`. This pill is *also* mirrored next to the Verdict cluster (`VerdictCluster`, `findings-design-and-manifold.md:50`) so it is reachable during live play without opening the drawer — matching how a-immersive puts the `rl-label` above the RL buttons (`aimmersive-clone-spec.md:283`).
- **Expand**: the pill + a one-line plain-English description of the active mode + an **"exploring…" indicator** that lights when `engine.feedback.exploring()` is true (`findings-feedback-behaviour.md:237`), reusing the `<NoiseRing>` colour ramp (off/active/high, `aimmersive-clone-spec.md:98`). While exploring in Mode 2, training is paused (`learning_paused()`, `findings-feedback-behaviour.md:48`) — show a small "learning paused" badge. - **Expand**: the pill + a one-line plain-English description of the active mode + an **"exploring…" indicator** that lights when `engine.feedback.exploring()` is true (`findings-feedback-behaviour.md:237`), reusing the `<NoiseRing>` colour ramp (off/active/high, `aimmersive-clone-spec.md:98`). While exploring in Mode 2, training is paused (`learning_paused()`, `findings-feedback-behaviour.md:48`) — show a small "learning paused" badge.

View file

@ -183,7 +183,9 @@ sweep (L22, zero consumers) — don't cite them.
### Other shared UI files ### Other shared UI files
- `shared-ui.tsx``MiniMeters` (read-only output bars). `AltitudeNav`/`CompactAxis` were deleted with the focus system. - `shared-ui.tsx``MiniMeters` (read-only output bars). `AltitudeNav`/`CompactAxis` were deleted with the focus system.
- `icons.tsx` — monochrome inline-SVG icons (mode icons + drawer icons + `GLYPH_FALLBACK` for when monochrome is off). - `icons.tsx` — monochrome inline-SVG icons (mode icons + drawer icons + `GLYPH_FALLBACK` for when monochrome is off).
- `VerdictCluster.tsx` — floating bottom-centre feedback UI (perturb ▽ / undo ↺ / commit △ + A/B); labels adapt to feedback mode. - `VerdictCluster.tsx` — floating bottom-centre feedback UI (perturb ▽ / undo ↺ / commit △ + A/B);
labels and press timing adapt to feedback mode. Push away fires once on pointer-down and
never turns a hold into randomisation; the 600 ms long-press re-roll remains Explore-only.
- `CurvePad.tsx` — square canvas curve editor (vertical drag reshapes [0,1]; ~0.43 ≈ linear). Used in OutputEditor + OutputControlRow. - `CurvePad.tsx` — square canvas curve editor (vertical drag reshapes [0,1]; ~0.43 ≈ linear). Used in OutputEditor + OutputControlRow.
- `OutputEditor.tsx` — inline range/curve popup for a single output (hover/click on a bar), using - `OutputEditor.tsx` — inline range/curve popup for a single output (hover/click on a bar), using
`DualRange.tsx` for the shared dual-thumb min/max control. `DualRange.tsx` for the shared dual-thumb min/max control.

View file

@ -1,7 +1,8 @@
/** /**
* 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). * perturb (thumbs-down) · undo · commit (thumbs-up).
* Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`. * Explore mode retains the ported long-press re-roll gesture. Push away fires
* once on pointer-down and never reinterprets a hold as randomisation.
* *
* The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0): * The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0):
* *
@ -12,8 +13,9 @@
* thumbs-DOWN = dislike (push away); * thumbs-DOWN = dislike (push away);
* thumbs-UP = like + train. * thumbs-UP = like + train.
* *
* Wiring (in ConsoleApp): onCommit / onPerturb dispatch on the mode; onReroll = * Wiring (in ConsoleApp): onCommit / onPerturb dispatch on the mode; onReroll
* re-roll the scratchpad (Mode 2) or the real net. * is the Explore-mode long-press path; onRandomise is the explicit secondary
* control.
*/ */
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
@ -78,7 +80,7 @@ export function VerdictCluster({
? exploring ? exploring
? 'Cancel explore — restore the real net' ? 'Cancel explore — restore the real net'
: 'Explore — re-roll into a scratchpad (hold to re-roll again)' : 'Explore — re-roll into a scratchpad (hold to re-roll again)'
: 'Dislike — push the sound away (hold to re-roll)'; : 'Dislike — push the sound away';
const upTitle = explore const upTitle = explore
? exploring ? exploring
? picking ? picking
@ -90,15 +92,28 @@ export function VerdictCluster({
const lp = useRef<ReturnType<typeof setTimeout> | null>(null); const lp = useRef<ReturnType<typeof setTimeout> | null>(null);
const firedReroll = useRef(false); const firedReroll = useRef(false);
const cancelLongPress = () => {
if (lp.current) {
clearTimeout(lp.current);
lp.current = null;
}
};
const perturbDown = () => { const perturbDown = () => {
cancelLongPress();
if (!explore) {
onPerturb();
return;
}
firedReroll.current = false; firedReroll.current = false;
lp.current = setTimeout(() => { lp.current = setTimeout(() => {
lp.current = null;
firedReroll.current = true; firedReroll.current = true;
onReroll(); onReroll();
}, 600); }, 600);
}; };
const perturbUp = () => { const perturbUp = () => {
if (lp.current) clearTimeout(lp.current); if (!explore) return;
cancelLongPress();
if (!firedReroll.current) onPerturb(); if (!firedReroll.current) onPerturb();
}; };
@ -170,9 +185,8 @@ export function VerdictCluster({
title={downTitle} title={downTitle}
onPointerDown={perturbDown} onPointerDown={perturbDown}
onPointerUp={perturbUp} onPointerUp={perturbUp}
onPointerLeave={() => { onPointerLeave={cancelLongPress}
if (lp.current) clearTimeout(lp.current); onPointerCancel={cancelLongPress}
}}
style={big({ style={big({
background: explore && exploring ? 'rgba(255,106,0,0.22)' : 'rgba(255,106,0,0.16)', background: explore && exploring ? 'rgba(255,106,0,0.22)' : 'rgba(255,106,0,0.16)',
color: 'var(--accent)', color: 'var(--accent)',

View file

@ -89,6 +89,23 @@ test.describe('geometric dislike (Mode 1) — core-backed', () => {
expect(result).toEqual({ first: 1, live: 1, second: 1, expired: 0 }); expect(result).toEqual({ first: 1, live: 1, second: 1, expired: 0 });
}); });
test('Push away starts once on pointer-down and holding never rerolls', async ({ page }) => {
const button = page.getByTitle(/Dislike — push the sound away/);
const box = await button.boundingBox();
if (!box) throw new Error('negative-feedback button has no bounds');
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
expect(await page.evaluate(() => window.__nisps!.feedbackCounts().negative)).toBe(1);
await page.waitForTimeout(700);
expect(await page.evaluate(() => window.__nisps!.feedbackCounts().negative)).toBe(1);
await page.mouse.up();
expect(await page.evaluate(() => window.__nisps!.feedbackCounts().negative)).toBe(1);
await expect(button).not.toHaveAttribute('title', /hold|re-roll/);
});
test('expanded Learning panel starts calibrated and can restore upstream defaults', async ({ page }) => { test('expanded Learning panel starts calibrated and can restore upstream defaults', async ({ page }) => {
await page.getByTitle('Learning', { exact: true }).click(); await page.getByTitle('Learning', { exact: true }).click();
await page.getByTitle('Expand', { exact: true }).click(); await page.getByTitle('Expand', { exact: true }).click();