diff --git a/README.md b/README.md index 9a5ccbb..6a79391 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ Load shows the request error (for example `HTTP 500`) when the store cannot be r - Ensure the ComfyUI user directory is writable - Try a shorter name (max 64 characters) +Saving asks before it replaces a character, object, or scene that already uses that name. Saving again to the preset already linked to the region does not ask. If the preset list cannot be loaded, the save still goes ahead. + ### Connected import JSON did not change the prompt The `import_json` input overrides the editor only when it is a JSON object. A leading UTF-8 byte-order mark is ignored. If the value is empty, not valid JSON, or a JSON array, the node keeps the editor caption and prints a line starting with `[ByAE Ideogram]`. diff --git a/tests/store_client.test.js b/tests/store_client.test.js index a79d5ad..46e3fa5 100644 --- a/tests/store_client.test.js +++ b/tests/store_client.test.js @@ -8,7 +8,9 @@ globalThis.window = { }, }; -const { loadStores, deleteStoreEntry, saveStoreEntry, normalizeStoreName } = await import("../web/js/ideogram4_shared.js"); +const { + loadStores, deleteStoreEntry, saveStoreEntry, normalizeStoreName, savedPresetCollision, +} = await import("../web/js/ideogram4_shared.js"); describe("loadStores", () => { const originalFetch = globalThis.fetch; @@ -54,6 +56,26 @@ describe("loadStores", () => { }); }); +describe("savedPresetCollision", () => { + it("names a different preset that would be replaced", () => { + assert.equal(savedPresetCollision(["Ada", "Bea"], "Bea"), "Bea"); + assert.equal(savedPresetCollision(["Ada"], "Cara"), null); + assert.equal(savedPresetCollision(["Ada"], " "), null); + assert.equal(savedPresetCollision([], "Ada"), null); + }); + + it("does not treat an update of the linked preset as a replacement", () => { + assert.equal(savedPresetCollision(["Ada"], "Ada", "Ada"), null); + assert.equal(savedPresetCollision(["Ada", "Bea"], "Bea", "Ada"), "Bea"); + }); + + it("matches a composed name to the stored spelling", () => { + const nfd = "Émilie".normalize("NFD"); + assert.equal(savedPresetCollision(["Émilie"], nfd), "Émilie"); + assert.equal(savedPresetCollision(["Émilie"], ` ${nfd} `, "Émilie"), null); + }); +}); + describe("saveStoreEntry", () => { const originalFetch = globalThis.fetch; diff --git a/web/js/ideogram4_prompt_builder_enhanced.js b/web/js/ideogram4_prompt_builder_enhanced.js index 670b249..fb19a2e 100644 --- a/web/js/ideogram4_prompt_builder_enhanced.js +++ b/web/js/ideogram4_prompt_builder_enhanced.js @@ -5,7 +5,7 @@ import { } from "./utility.js"; import { loadStores, saveStoreEntry, deleteStoreEntry, installBuilderQueueHooks, installBgUpdateListener, - normalizeStoreName, + normalizeStoreName, savedPresetCollision, } from "./ideogram4_shared.js"; import { captionText, cleanPalette, dimsFromAspect, elementToRegion, ideogramResolutionWarning, @@ -1959,9 +1959,11 @@ app.registerExtension({ sceneSaveBtn.addEventListener("mousedown", (e) => e.stopPropagation()); sceneSaveBtn.addEventListener("click", () => { showModal("Save Scene", [{ key: "name", label: "Scene name", placeholder: "my-scene" }], async (r) => { - const name = (r.name || "").trim(); + const name = normalizeStoreName(r.name || ""); if (!name) return; try { + const collision = savedPresetCollision(await existingPresetNames("scenes"), name); + if (collision && !confirm(`Replace saved scene "${collision}"? This cannot be undone.`)) return; await saveStoreEntry("scenes", name, captureSceneState()); console.log("ByAE Ideogram: saved scene:", name); } catch (e) { @@ -2056,13 +2058,30 @@ app.registerExtension({ return ta; } + async function existingPresetNames(bucket) { + try { + const stores = await loadStores(); + return Object.keys(stores[bucket] || {}); + } catch (e) { + console.warn("ByAE Ideogram: could not check existing presets before save", e); + return []; + } + } + async function persistBoxToStore(b, type, name) { const bucket = type === "object" ? "objects" : "characters"; + const safeName = normalizeStoreName(name); + if (!safeName) return; + const linked = b.storeRef && b.storeRef.type === type ? b.storeRef.name : ""; try { - const savedName = await saveStoreEntry(bucket, name, boxPayload(b)); + if (normalizeStoreName(linked) !== safeName) { + const collision = savedPresetCollision(await existingPresetNames(bucket), safeName, linked); + if (collision && !confirm(`Replace saved ${type} "${collision}"? This cannot be undone.`)) return; + } + const savedName = await saveStoreEntry(bucket, safeName, boxPayload(b)); b.storeRef = { type, name: savedName }; commit(); - console.log("ByAE Ideogram: saved", type, name); + console.log("ByAE Ideogram: saved", type, savedName); } catch (e) { console.error("ByAE Ideogram: failed to save:", e); alert("Failed to save: " + e.message); diff --git a/web/js/ideogram4_shared.js b/web/js/ideogram4_shared.js index 5015902..07366fe 100644 --- a/web/js/ideogram4_shared.js +++ b/web/js/ideogram4_shared.js @@ -13,6 +13,19 @@ export function normalizeStoreName(name) { return String(name ?? "").trim().normalize("NFC"); } +/** + * Stored name this save would replace, or null. + * Saving the preset already linked to the region is an update, not a replacement. + */ +export function savedPresetCollision(existingNames, name, linkedName) { + const target = normalizeStoreName(name); + if (!target) return null; + const match = (existingNames || []).find((entry) => normalizeStoreName(entry) === target); + if (!match) return null; + if (linkedName && normalizeStoreName(linkedName) === target) return null; + return match; +} + export async function loadStores() { try { const resp = await fetch(STORE_API, {