From 136ac6df929fb4d31bbec66f311880a1c2cfceb9 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:57:32 +0300 Subject: [PATCH] feat(ui): Refinement & Taste section lists, exports, deletes and imports refinement keys Co-Authored-By: Claude Sonnet 5.5 --- app/ui/features/settings/index.js | 3 ++- app/ui/features/settings/taste.js | 42 +++++++++++++++++++++++++++++++ app/ui/shell/api.js | 11 ++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 app/ui/features/settings/taste.js diff --git a/app/ui/features/settings/index.js b/app/ui/features/settings/index.js index e7df26e2..08b5b20c 100644 --- a/app/ui/features/settings/index.js +++ b/app/ui/features/settings/index.js @@ -7,6 +7,7 @@ import { engine } from "./engine.js"; import { modules } from "./modules.js"; import { system } from "./system.js"; import { packs } from "./packs.js"; +import { taste } from "./taste.js"; const later = (what) => function mount() { return c.emptyState.default({ icon: "◌", title: "Not built yet", hint: what }); @@ -29,7 +30,7 @@ export default { add("modules", "Modules", "Switch a module off for this project; see and re-enable ones that failed.", "☷", "neutral", modules(app), "Generation", "modules enable disable quarantine"); add("refinement", "Refinement & Taste", "Learned-taste state: refinement keys and the Absolute global-taste store.", "✦", "danger", - later("Refinement keys: export, delete; the taste store."), "Learning", "refinement taste keys rating"); + taste(app), "Learning", "refinement taste keys rating"); add("system", "Updates & ComfyUI", "Server connection, FunPack code updates, pipeline health.", "⟳", "good", system(app), "System", "update git branch restart rollback"); add("customnodes", "Custom Nodes", "Install, update and remove ComfyUI node packs.", "⧉", "neutral", diff --git a/app/ui/features/settings/taste.js b/app/ui/features/settings/taste.js new file mode 100644 index 00000000..ee7b4dd4 --- /dev/null +++ b/app/ui/features/settings/taste.js @@ -0,0 +1,42 @@ +// Refinement & Taste: the refinement keys FunPack has learned from your ratings. Keys are files: carry one to another machine +// as a zip, or delete one to start that taste over. +import { composer as c } from "../../composer/composer.js"; + +export const taste = (app) => function mount() { + const page = c.region.stack({ gap: "md" }); + const tell = (text) => c.toast.warn({ text }); + let keys = null, error = ""; + + async function refresh() { + try { keys = (await app.api.tasteKeys()).keys || []; error = ""; } catch (err) { keys = []; error = err.message; } + draw(); + } + const remove = async (name) => { + if (!(await c.modal.dialogue({ title: "Delete refinement key", message: `Delete “${name}” and everything it learned? This cannot be undone.`, tone: "danger", confirmLabel: "Delete" }).result)) return; + try { await app.api.deleteTasteKey(name); } catch (err) { tell(err.message); } + refresh(); + }; + async function bring([file]) { + const name = await c.modal.prompt({ title: "Import refinement key", label: "Name", value: file.name.replace(/\.zip$/i, ""), confirmLabel: "Import" }).result; + if (!name || !name.trim()) return; + try { + let r = await app.api.importTasteKey(name.trim(), file, false); + if (r.exists && await c.modal.dialogue({ title: "Replace key", message: `A key named “${name.trim()}” already exists. Replace it?`, tone: "danger", confirmLabel: "Replace" }).result) r = await app.api.importTasteKey(name.trim(), file, true); + if (!r.exists) c.toast.good({ text: `Imported “${name.trim()}”.` }); + } catch (err) { tell(err.message); } + refresh(); + } + function draw() { + page.set([ + error ? c.banner.warn({ text: `Could not read the keys: ${error}` }) : null, + keys && !keys.length && !error ? c.emptyState.default({ icon: "✦", title: "No keys yet", hint: "A key appears once you rate a render with learning on." }) : null, + ...(keys || []).map((name) => c.settingsRow.default({ label: name, hint: "Refinement key", control: c.toolbar.default({ items: [ + c.button.sm({ label: "⤓ Export", tone: "ghost", onClick: () => c.toast.good({ text: "Preparing the download…" }) || Object.assign(document.createElement("a"), { href: app.api.tasteKeyUrl(name) }).click() }), + c.button.sm({ label: "Delete", tone: "danger", onClick: () => remove(name) })] }) })), + c.label.section({ text: "Bring a key here" }), + 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)); + } + draw(); refresh(); + return { node: page.node, destroy: () => page.node.remove() }; +}; diff --git a/app/ui/shell/api.js b/app/ui/shell/api.js index 1df25b5d..74e73c92 100644 --- a/app/ui/shell/api.js +++ b/app/ui/shell/api.js @@ -37,6 +37,17 @@ export const api = { enhancerRuns: () => call("GET", "/api/m/conditioning_prompt_enhancer/runs"), rateTaste: (promptId, rating, axis) => call("POST", "/api/m/taste/rate", { prompt_id: promptId, rating, axis: axis || null }), newTasteGeneration: () => call("POST", "/api/m/taste/generation", {}), + tasteKeys: () => call("GET", "/api/m/taste/keys"), + deleteTasteKey: (name) => call("DELETE", `/api/m/taste/keys/${encodeURIComponent(name)}`), + tasteKeyUrl: (name) => `${BASE}/api/m/taste/keys/${encodeURIComponent(name)}/export`, + /** The zip as the raw body; `exists: true` comes back (not thrown) when the name is taken and `overwrite` was not asked. */ + async importTasteKey(name, file, overwrite) { + const res = await fetch(`${BASE}/api/m/taste/keys/import?name=${encodeURIComponent(name)}${overwrite ? "&overwrite=1" : ""}`, { method: "POST", body: file }); + const payload = await res.json().catch(() => null); + if (res.status === 409) return { exists: true }; + if (!res.ok) throw new Error((payload && payload.why) || `HTTP ${res.status}`); + return payload; + }, packs: () => call("GET", "/api/packs"), pack: (action, body) => call("POST", `/api/packs/${action}`, body || {}), git: (action, body) => call("POST", `/api/git/${action}`, body || {}),