diff --git a/__init__.py b/__init__.py index d0463cc..242ff0b 100644 --- a/__init__.py +++ b/__init__.py @@ -522,6 +522,7 @@ if PromptServer is not None and web is not None: payload = await request.json() result = accumulator_delete_entries( store_key=str(payload.get("store_key") or ""), + preview_key=str(payload.get("preview_key") or ""), entry_id=str(payload.get("entry_id") or ""), index=int(payload.get("index") or 0), clear=bool(payload.get("clear")), @@ -537,6 +538,7 @@ if PromptServer is not None and web is not None: payload = await request.json() result = accumulator_move_entry( store_key=str(payload.get("store_key") or ""), + preview_key=str(payload.get("preview_key") or ""), entry_id=str(payload.get("entry_id") or ""), index=int(payload.get("index") or 0), direction=str(payload.get("direction") or "up"), diff --git a/loop_nodes.py b/loop_nodes.py index 7bdd131..b6cef0f 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -236,6 +236,18 @@ def _entry_infos(store: list[dict[str, Any]]) -> list[dict[str, Any]]: return entries +def _attach_preview_images(entries: list[dict[str, Any]], images: list[dict[str, str]]) -> None: + by_key = { + str(image.get("preview_key") or ""): image + for image in images + if str(image.get("preview_key") or "") + } + for entry in entries: + image = by_key.get(str(entry.get("preview_key") or "")) + if image: + entry["preview_image"] = image + + def _accumulator_status(key: str, store: list[dict[str, Any]]) -> str: images = [entry.get("image") for entry in store if entry.get("image") is not None] shapes = [] @@ -252,19 +264,23 @@ def accumulator_list_entries(store_key: str, preview_limit: int = 0) -> dict[str if not key: raise ValueError("store_key is required for accumulator preview actions") store = _ACCUMULATOR_STORES.setdefault(key, []) + entries = _entry_infos(store) result = { "store_key": key, - "entries": _entry_infos(store), + "entries": entries, "count": len(store), "status": _accumulator_status(key, store), } if int(preview_limit) > 0: - result["images"] = _preview_image_results(store, preview_limit, None, None) + images = _preview_image_results(store, preview_limit, None, None) + _attach_preview_images(entries, images) + result["images"] = images return result def accumulator_delete_entries( store_key: str, + preview_key: str = "", entry_id: str = "", index: int = 0, clear: bool = False, @@ -279,8 +295,13 @@ def accumulator_delete_entries( removed = len(store) store.clear() else: + preview_key = str(preview_key or "").strip() entry_id = str(entry_id or "").strip() - if entry_id: + if preview_key: + before = len(store) + store[:] = [entry for entry in store if _entry_preview_key(entry) != preview_key] + removed = before - len(store) + elif entry_id: before = len(store) store[:] = [entry for entry in store if str(entry.get("id") or "") != entry_id] removed = before - len(store) @@ -298,6 +319,7 @@ def accumulator_delete_entries( def accumulator_move_entry( store_key: str, + preview_key: str = "", entry_id: str = "", index: int = 0, direction: str = "up", @@ -313,8 +335,14 @@ def accumulator_move_entry( result["moved"] = False return result zero_index = -1 + preview_key = str(preview_key or "").strip() entry_id = str(entry_id or "").strip() - if entry_id: + if preview_key: + for current_index, entry in enumerate(store): + if _entry_preview_key(entry) == preview_key: + zero_index = current_index + break + elif entry_id: for current_index, entry in enumerate(store): if str(entry.get("id") or "") == entry_id: zero_index = current_index @@ -1121,8 +1149,9 @@ class SxCPAccumulatorPreview: images = [] save_status += "; cleared_after_save" - preview_images = _preview_image_results(store, preview_limit, prompt, extra_pnginfo) entries = _entry_infos(store) + preview_images = _preview_image_results(store, preview_limit, prompt, extra_pnginfo) + _attach_preview_images(entries, preview_images) status = _accumulator_status(key, store) if removed: status += f"; removed={removed}" diff --git a/web/accumulator_preview.js b/web/accumulator_preview.js index 0bd4480..223e5b1 100644 --- a/web/accumulator_preview.js +++ b/web/accumulator_preview.js @@ -4,6 +4,7 @@ import { api } from "../../scripts/api.js"; const EXTENSION = "ethanfel.prompt_builder.accumulator_preview"; const NODE_NAME = "SxCPAccumulatorPreview"; const STYLE_ID = "sxcp-accumulator-preview-styles"; +const DEBUG_STORAGE_KEY = "sxcpAccumulatorPreviewDebug"; const MIN_CELL_W = 180; const GAP = 6; @@ -33,6 +34,62 @@ function getNodeById(id) { return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)]; } +function debugEnabled() { + try { + return window.SXCP_ACCUMULATOR_PREVIEW_DEBUG === true || localStorage.getItem(DEBUG_STORAGE_KEY) === "1"; + } catch (_err) { + return window.SXCP_ACCUMULATOR_PREVIEW_DEBUG === true; + } +} + +function debugLog(...args) { + if (debugEnabled()) console.log(`[${EXTENSION}]`, ...args); +} + +function debugWarn(...args) { + console.warn(`[${EXTENSION}]`, ...args); +} + +function nodeSummaries() { + return (app.graph?._nodes || []) + .filter(isAccumulatorPreviewNode) + .map((node) => ({ + id: node.id, + store_key: storeKey(node), + entries: (node._sxapEntries || []).map((entry, index) => ({ + index: entry.index, + id: entry.id, + preview_key: entry.preview_key, + has_image: entry.has_image, + has_preview_image: !!entry.preview_image, + image_ref: imageParamsForEntry(node, entry, index), + })), + images: node._sxapImages || [], + status: node._sxapStatus || "", + })); +} + +function installDebugHelpers() { + if (window.sxcpAccumulatorPreviewDebug) return; + window.sxcpAccumulatorPreviewDebug = { + enable() { + localStorage.setItem(DEBUG_STORAGE_KEY, "1"); + window.SXCP_ACCUMULATOR_PREVIEW_DEBUG = true; + console.log(`[${EXTENSION}] debug enabled`); + }, + disable() { + localStorage.removeItem(DEBUG_STORAGE_KEY); + window.SXCP_ACCUMULATOR_PREVIEW_DEBUG = false; + console.log(`[${EXTENSION}] debug disabled`); + }, + dump() { + const data = nodeSummaries(); + console.log(`[${EXTENSION}] dump`, data); + return data; + }, + }; +} + function asArray(value) { if (!value) return []; return Array.isArray(value) ? value : [value]; @@ -93,6 +150,9 @@ function entryKey(entry) { function buildImageMap(entries, images, previous = new Map()) { const next = new Map(previous); const imageEntries = asArray(entries).filter((entry) => entry?.has_image); + imageEntries.forEach((entry) => { + if (entry.preview_image) next.set(entryKey(entry), entry.preview_image); + }); asArray(images).filter(Boolean).forEach((image, index) => { const directKey = String(image?.preview_key || "").trim(); if (directKey) { @@ -111,6 +171,7 @@ function buildImageMap(entries, images, previous = new Map()) { } function imageParamsForEntry(node, entry, fallbackIndex) { + if (entry?.preview_image) return entry.preview_image; const keyed = node._sxapImageByKey?.get(entryKey(entry)); if (keyed) return keyed; return (node._sxapImages || [])[fallbackIndex]; @@ -137,12 +198,14 @@ function entryTitle(entry) { } async function postJson(path, payload) { + debugLog("POST", path, payload); const response = await api.fetchApi(path, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(payload), }); const data = await response.json(); + debugLog("POST response", path, data); if (!response.ok) throw new Error(data?.error || response.statusText); return data; } @@ -299,17 +362,37 @@ function renderCell(node, entry, imageParams, displayIndex) { const source = node._sxapDragEntry; node._sxapDragEntry = null; if (source.index === entry.index) return; + debugLog("drop", { + source_index: source.index, + source_key: entryKey(source), + target_index: entry.index, + target_key: entryKey(entry), + }); await moveEntryToIndex(node, source, entry.index); }; const thumb = document.createElement("img"); thumb.className = "sxap-thumb"; thumb.style.height = `${Math.max(48, Math.round(cellW * entryAspect(entry)))}px`; - if (imageParams) thumb.src = imageUrl(imageParams); + if (imageParams) { + const url = imageUrl(imageParams); + thumb.src = url; + thumb.onerror = () => debugWarn("image load failed", { + entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key}, + imageParams, + url, + }); + } else { + debugWarn("missing image params for entry", { + entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key}, + displayIndex, + }); + } thumb.draggable = true; thumb.onclick = () => markSelected(node, entry.index); thumb.ondragstart = (event) => { node._sxapDragEntry = entry; + debugLog("dragstart", {index: entry.index, key: entryKey(entry), id: entry.id}); if (event.dataTransfer) { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(entry.id || entry.index || "")); @@ -364,6 +447,12 @@ function renderGrid(node) { empty.textContent = storeKey(node) ? "No accumulator images." : "Run once or set an explicit store_key."; grid.appendChild(empty); } else { + debugLog("renderGrid", entries.map((entry, index) => ({ + index: entry.index, + key: entryKey(entry), + has_preview_image: !!entry.preview_image, + has_image_params: !!imageParamsForEntry(node, entry, index), + }))); entries.forEach((entry, index) => { grid.appendChild(renderCell(node, entry, imageParamsForEntry(node, entry, index), index)); }); @@ -428,8 +517,9 @@ async function deleteEntry(node, entry) { try { const data = await postJson("/sxcp/accumulator/delete", actionPayload(node, { store_key: key, + preview_key: entry.preview_key || "", entry_id: entry.id || "", - index: entry.id ? 0 : entry.index, + index: entry.preview_key || entry.id ? 0 : entry.index, clear: false, })); applyData(node, data, `${data.status || ""}; deleted=${data.removed || 0}`); @@ -447,12 +537,33 @@ async function moveEntryToIndex(node, entry, targetIndex) { } if (!entry) return; try { + debugLog("move request", { + entry: {index: entry.index, id: entry.id, preview_key: entry.preview_key}, + targetIndex, + }); const data = await postJson("/sxcp/accumulator/move", actionPayload(node, { store_key: key, + preview_key: entry.preview_key || "", entry_id: entry.id || "", - index: entry.id ? 0 : entry.index, + index: entry.preview_key || entry.id ? 0 : entry.index, target_index: targetIndex, })); + debugLog("move response summary", { + moved: data.moved, + from_index: data.from_index, + to_index: data.to_index, + entries: asArray(data.entries).map((item) => ({ + index: item.index, + id: item.id, + preview_key: item.preview_key, + has_preview_image: !!item.preview_image, + })), + images: asArray(data.images).map((item) => ({ + filename: item.filename, + preview_key: item.preview_key, + entry_id: item.entry_id, + })), + }); applyData(node, data, `${data.status || ""}; moved=${data.moved ? "yes" : "no"}`); } catch (err) { console.error(`[${EXTENSION}] drag move failed`, err); @@ -599,6 +710,7 @@ app.registerExtension({ name: EXTENSION, async setup() { + installDebugHelpers(); api.addEventListener("executed", ({detail}) => { const node = getNodeById(detail?.display_node ?? detail?.node); if (!isAccumulatorPreviewNode(node)) return;