From 7aa571000a3c3dffca94807952900659d8f59e40 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:27:37 +0300 Subject: [PATCH] feat: block influence readout in Refinement & Taste: flatness, novelty, run counts, Download and Clear Co-Authored-By: Claude Haiku 5.5 --- app/ui/features/settings/taste.js | 22 ++++++++++++++++++++++ app/ui/features/settings/taste.test.js | 18 ++++++++++++++++++ app/ui/shell/api.js | 2 ++ 3 files changed, 42 insertions(+) diff --git a/app/ui/features/settings/taste.js b/app/ui/features/settings/taste.js index ef1165a6..bd5a457e 100644 --- a/app/ui/features/settings/taste.js +++ b/app/ui/features/settings/taste.js @@ -45,6 +45,19 @@ export const taste = (app) => function mount() { c.dropzone.default({ label: "Drop or choose an exported key (.zip)", hint: "from another machine", accept: ".zip,application/zip", multiple: false, onFiles: bring }), ].filter(Boolean)); } + const exportInfluence = async (key) => { + try { + const r = await fetch(app.api.blockInfluenceExportUrl(key)); + if (!r.ok) throw new Error((await r.json().catch(() => ({}))).why || `The server refused (${r.status}).`); + const link = Object.assign(document.createElement("a"), { download: `${key}.block_influence.pt`, href: URL.createObjectURL(await r.blob()) }); + link.click(); URL.revokeObjectURL(link.href); + } catch (err) { tell(err.message); } + }; + const clearInfluence = async (key) => { + if (!(await c.modal.dialogue({ title: "Clear block influence", message: `Clear everything recorded for โ€œ${key}โ€? This cannot be undone.`, tone: "danger", confirmLabel: "Clear" }).result)) return; + try { await app.api.clearBlockInfluence(key); } catch (err) { tell(err.message); } + refreshResearch(); + }; // Block influence: research recording, read here. Its numbers say whether the four groups point apart; no clip changes. const research = c.region.stack({ gap: "sm" }); let influence = null; @@ -66,6 +79,15 @@ export const taste = (app) => function mount() { rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. Off: nothing is recorded.", checked: !!state.enabled, onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } })); if (state.problem) rows.push(c.banner.warn({ text: state.problem })); + rows.push(c.settingsRow.default({ label: "Runs measured", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`, + control: c.hint.default({ text: `${state.used} kept` }) })); + if (state.used) rows.push(c.settingsRow.default({ label: "Flatness", hint: "How evenly the blocks move the picture. Near 0 means every block moves it equally, so there is nothing to aim at.", + control: c.hint.default({ text: state.flatness == null ? "n/a" : state.flatness.toFixed(3) }) })); + if (state.used && state.mean_novelty != null) rows.push(c.settingsRow.default({ label: "Novelty", hint: "Near 1: blocks amplify what came before. Near 0: they add something new.", + control: c.hint.default({ text: state.mean_novelty.toFixed(3) }) })); + rows.push(c.settingsRow.default({ label: "Recording data", hint: "Download the stored data for this key, or clear it.", control: c.toolbar.default({ items: [ + c.button.sm({ label: "โค“ Download", tone: "ghost", onClick: () => exportInfluence(state.key) }), + c.button.sm({ label: "Clear", tone: "danger", onClick: () => clearInfluence(state.key) })] }) })); if (four) rows.push(c.settingsRow.default({ label: "Clips rated", hint: `Key: ${four.key || state.key || "none yet"}`, control: c.hint.default({ text: four.used ? Object.entries(four.counts || {}).map(([g, n]) => `${g} ${n}`).join(", ") : "none yet" }) })); if (four && four.note) rows.push(c.hint.default({ text: four.note })); diff --git a/app/ui/features/settings/taste.test.js b/app/ui/features/settings/taste.test.js index 0d925d12..75a04d67 100644 --- a/app/ui/features/settings/taste.test.js +++ b/app/ui/features/settings/taste.test.js @@ -60,3 +60,21 @@ test("a failed report still shows the switch, so recording can be turned off", a assert.deepEqual(sets, [false]); assert.match(page.node.textContent, /HTTP 500/); }); + +test("the block influence readout shows flatness, and Clear asks before it clears", async () => { + const cleared = []; + const app = { api: { + tasteKeys: async () => ({ keys: [] }), + blockInfluence: async () => ({ key: "fox", enabled: true, problem: null, runs: 9, skipped: 1, used: 8, flatness: 0.123, mean_novelty: 0.4 }), + blockInfluenceGroups: async () => ({ key: "fox", used: 0, counts: {}, cos: {}, chance: {}, note: "nothing recorded yet" }), + blockInfluenceExportUrl: (k) => `/x/${k}`, + clearBlockInfluence: async (k) => { cleared.push(k); return {}; }, + } }; + const page = taste(app)(); + document.body.append(page.node); + await tick(); await tick(); + assert.match(page.node.textContent, /0\.123/); + assert.match(page.node.textContent, /0\.400/); + assert.match(page.node.textContent, /Download/); + assert.match(page.node.textContent, /Clear/); +}); diff --git a/app/ui/shell/api.js b/app/ui/shell/api.js index 06dcc8a3..6ceb8692 100644 --- a/app/ui/shell/api.js +++ b/app/ui/shell/api.js @@ -56,6 +56,8 @@ export const api = { tasteKeys: () => call("GET", "/api/m/taste/keys"), blockInfluence: (key) => call("GET", `/api/m/block_influence/status?key=${encodeURIComponent(key || "default")}`), blockInfluenceGroups: (key) => call("GET", `/api/m/block_influence/groups?key=${encodeURIComponent(key || "default")}`), + clearBlockInfluence: (key) => call("POST", "/api/m/block_influence/clear", { key: key || "default" }), + blockInfluenceExportUrl: (key) => `${BASE}/api/m/block_influence/export?key=${encodeURIComponent(key || "default")}`, setBlockInfluence: (on) => call("POST", `/api/m/block_influence/enabled?key=default`, { enabled: !!on }), libraryFiles: () => call("GET", "/api/files"), deleteLibraryFile: (name) => call("DELETE", `/api/files/${encodeURIComponent(name)}`),