fix(manifold): start push-away feedback on press
This commit is contained in:
parent
079720a572
commit
a6b8f87d32
4 changed files with 45 additions and 11 deletions
|
|
@ -80,9 +80,10 @@ The `+` (up) verdict is always "keep this" (`addExample` + train, `findings-feed
|
|||
|
||||
| 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). |
|
||||
| **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). |
|
||||
| **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) | `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.)**
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -183,7 +183,9 @@ sweep (L22, zero consumers) — don't cite them.
|
|||
### Other shared UI files
|
||||
- `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).
|
||||
- `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.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* 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`.
|
||||
* 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):
|
||||
*
|
||||
|
|
@ -12,8 +13,9 @@
|
|||
* 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.
|
||||
* Wiring (in ConsoleApp): onCommit / onPerturb dispatch on the mode; onReroll
|
||||
* is the Explore-mode long-press path; onRandomise is the explicit secondary
|
||||
* control.
|
||||
*/
|
||||
import { useRef, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
|
@ -78,7 +80,7 @@ export function VerdictCluster({
|
|||
? 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)';
|
||||
: 'Dislike — push the sound away';
|
||||
const upTitle = explore
|
||||
? exploring
|
||||
? picking
|
||||
|
|
@ -90,15 +92,28 @@ export function VerdictCluster({
|
|||
const lp = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const firedReroll = useRef(false);
|
||||
|
||||
const cancelLongPress = () => {
|
||||
if (lp.current) {
|
||||
clearTimeout(lp.current);
|
||||
lp.current = null;
|
||||
}
|
||||
};
|
||||
const perturbDown = () => {
|
||||
cancelLongPress();
|
||||
if (!explore) {
|
||||
onPerturb();
|
||||
return;
|
||||
}
|
||||
firedReroll.current = false;
|
||||
lp.current = setTimeout(() => {
|
||||
lp.current = null;
|
||||
firedReroll.current = true;
|
||||
onReroll();
|
||||
}, 600);
|
||||
};
|
||||
const perturbUp = () => {
|
||||
if (lp.current) clearTimeout(lp.current);
|
||||
if (!explore) return;
|
||||
cancelLongPress();
|
||||
if (!firedReroll.current) onPerturb();
|
||||
};
|
||||
|
||||
|
|
@ -170,9 +185,8 @@ export function VerdictCluster({
|
|||
title={downTitle}
|
||||
onPointerDown={perturbDown}
|
||||
onPointerUp={perturbUp}
|
||||
onPointerLeave={() => {
|
||||
if (lp.current) clearTimeout(lp.current);
|
||||
}}
|
||||
onPointerLeave={cancelLongPress}
|
||||
onPointerCancel={cancelLongPress}
|
||||
style={big({
|
||||
background: explore && exploring ? 'rgba(255,106,0,0.22)' : 'rgba(255,106,0,0.16)',
|
||||
color: 'var(--accent)',
|
||||
|
|
|
|||
|
|
@ -89,6 +89,23 @@ test.describe('geometric dislike (Mode 1) — core-backed', () => {
|
|||
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 }) => {
|
||||
await page.getByTitle('Learning', { exact: true }).click();
|
||||
await page.getByTitle('Expand', { exact: true }).click();
|
||||
|
|
|
|||
Loading…
Reference in a new issue