From 4aab4830e0f40bbe82bece514b5f15bf9b06e436 Mon Sep 17 00:00:00 2001 From: moonwhaler Date: Tue, 11 Aug 2026 12:39:20 +0200 Subject: [PATCH] feat: add Prompt Library node Managed multiline text box backed by a shared JSON library: save/update/ delete named prompts (with description/tags) and reload them via a searchable dropdown picker on the node. --- __init__.py | 7 + prompt_library_node.py | 44 +++++ prompt_library_routes.py | 67 +++++++ prompt_library_store.py | 83 +++++++++ web/lib/prompt_chooser.js | 374 ++++++++++++++++++++++++++++++++++++++ web/prompt_library.js | 224 +++++++++++++++++++++++ 6 files changed, 799 insertions(+) create mode 100644 prompt_library_node.py create mode 100644 prompt_library_routes.py create mode 100644 prompt_library_store.py create mode 100644 web/lib/prompt_chooser.js create mode 100644 web/prompt_library.js diff --git a/__init__.py b/__init__.py index c9d73ac..cea64f9 100644 --- a/__init__.py +++ b/__init__.py @@ -44,7 +44,12 @@ from .text_builder_node import ( NODE_CLASS_MAPPINGS as tb_class_mappings, NODE_DISPLAY_NAME_MAPPINGS as tb_display_mappings, ) +from .prompt_library_node import ( + NODE_CLASS_MAPPINGS as pl_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as pl_display_mappings, +) from . import server_routes # noqa: F401 - registers /moonpack/* HTTP routes +from . import prompt_library_routes # noqa: F401 - registers /moonpack/prompt_library routes log = logging.getLogger("MoonPack") @@ -60,6 +65,7 @@ NODE_CLASS_MAPPINGS = { **csi_class_mappings, **mll_class_mappings, **tb_class_mappings, + **pl_class_mappings, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -74,6 +80,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { **csi_display_mappings, **mll_display_mappings, **tb_display_mappings, + **pl_display_mappings, } # Legacy aliases keep workflows saved with v0.1 keys loadable. Remove in 1.0. diff --git a/prompt_library_node.py b/prompt_library_node.py new file mode 100644 index 0000000..094aacc --- /dev/null +++ b/prompt_library_node.py @@ -0,0 +1,44 @@ +class PromptLibrary: + """A managed text box: save, update and recall named prompts from a shared library.""" + + DESCRIPTION = ( + "A multiline text box that doubles as a searchable prompt library. Save the " + "current text under a name (with an optional description/tags), reload any " + "saved prompt from the dropdown, or update/delete it. The STRING output is " + "always whatever is currently in the text box." + ) + SEARCH_ALIASES = ["prompt", "library", "preset", "text", "multiline"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", { + "default": "", "multiline": True, + "tooltip": "The prompt text. This is exactly what gets output.", + }), + }, + "optional": { + # Set by the frontend when a library entry is loaded/saved, so + # reopening the workflow shows which entry is active. Not read + # by the backend: the output is always `text`, verbatim. + "entry_name": ("STRING", {"default": "", "forceInput": False}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "get_text" + CATEGORY = "MoonPack/string" + + def get_text(self, text="", entry_name=""): + return (text,) + + +NODE_CLASS_MAPPINGS = { + "MoonPack_PromptLibrary": PromptLibrary, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MoonPack_PromptLibrary": "Prompt Library", +} diff --git a/prompt_library_routes.py b/prompt_library_routes.py new file mode 100644 index 0000000..fb8bc7b --- /dev/null +++ b/prompt_library_routes.py @@ -0,0 +1,67 @@ +"""HTTP routes backing the Prompt Library node's dropdown and save/update/delete buttons. + +Import-guarded: outside a running ComfyUI (tests, linting) this module is a +no-op instead of an ImportError. +""" + +import logging + +from . import prompt_library_store as store + +log = logging.getLogger("MoonPack") + + +def _register(): + from aiohttp import web + from server import PromptServer + + routes = PromptServer.instance.routes + + @routes.get("/moonpack/prompt_library") + async def moonpack_list_prompts(_request): + return web.json_response({"entries": store.load_library()}) + + @routes.post("/moonpack/prompt_library") + async def moonpack_add_prompt(request): + body = await request.json() + name = str(body.get("name", "")).strip() + if not name: + return web.json_response({"error": "Name is required."}, status=400) + try: + entries = store.add_entry( + name, str(body.get("description", "")), str(body.get("tags", "")), + str(body.get("text", "")), + ) + except ValueError as exc: + return web.json_response({"error": str(exc)}, status=400) + return web.json_response({"entries": entries}) + + @routes.put("/moonpack/prompt_library/{name}") + async def moonpack_update_prompt(request): + body = await request.json() + try: + entries = store.update_entry( + request.match_info["name"], str(body.get("description", "")), + str(body.get("tags", "")), str(body.get("text", "")), + ) + except KeyError as exc: + return web.json_response({"error": str(exc)}, status=404) + return web.json_response({"entries": entries}) + + @routes.delete("/moonpack/prompt_library/{name}") + async def moonpack_delete_prompt(request): + try: + entries = store.delete_entry(request.match_info["name"]) + except KeyError as exc: + return web.json_response({"error": str(exc)}, status=404) + return web.json_response({"entries": entries}) + + return True + + +try: + REGISTERED = _register() +except Exception: # noqa: BLE001 - a broken route must not stop node loading + REGISTERED = False + log.warning("MoonPack: could not register HTTP routes; the Prompt Library " + "node's dropdown/save buttons will not work.", exc_info=True) diff --git a/prompt_library_store.py b/prompt_library_store.py new file mode 100644 index 0000000..4af0d71 --- /dev/null +++ b/prompt_library_store.py @@ -0,0 +1,83 @@ +"""Pure JSON-backed CRUD for the Prompt Library node's saved entries. + +Kept import-free of ComfyUI (except folder_paths, imported lazily inside +_library_path) so the CRUD logic is testable without a running server. +""" + +import json +import logging +import os + +log = logging.getLogger("MoonPack") + +LIBRARY_FILENAME = "moonpack_prompt_library.json" + + +def _library_path() -> str: + import folder_paths + return os.path.join(folder_paths.get_user_directory(), LIBRARY_FILENAME) + + +def load_library() -> list: + """Returns every saved entry, or [] if the file is missing or unreadable.""" + path = _library_path() + if not os.path.isfile(path): + return [] + try: + with open(path, "r", encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, json.JSONDecodeError): + log.warning("MoonPack Prompt Library: could not read %s; treating as empty.", path) + return [] + entries = data.get("entries") if isinstance(data, dict) else None + return entries if isinstance(entries, list) else [] + + +def save_library(entries: list) -> None: + path = _library_path() + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as handle: + json.dump({"entries": entries}, handle, indent=2, ensure_ascii=False) + + +def find_entry(entries: list, name: str): + for entry in entries: + if entry.get("name") == name: + return entry + return None + + +def add_entry(name: str, description: str, tags: str, text: str) -> list: + """Appends a new entry. Raises ValueError if the name is already taken.""" + entries = load_library() + if find_entry(entries, name) is not None: + raise ValueError(f"An entry named '{name}' already exists.") + entries.append({"name": name, "description": description, "tags": tags, "text": text}) + save_library(entries) + return entries + + +def update_entry(name: str, description: str, tags: str, text: str) -> list: + """Updates description/tags/text of an existing entry. The name stays fixed. + + Raises KeyError if no entry with that name exists. + """ + entries = load_library() + entry = find_entry(entries, name) + if entry is None: + raise KeyError(f"No entry named '{name}' exists.") + entry["description"] = description + entry["tags"] = tags + entry["text"] = text + save_library(entries) + return entries + + +def delete_entry(name: str) -> list: + """Removes an entry by name. Raises KeyError if no entry with that name exists.""" + entries = load_library() + if find_entry(entries, name) is None: + raise KeyError(f"No entry named '{name}' exists.") + entries = [entry for entry in entries if entry.get("name") != name] + save_library(entries) + return entries diff --git a/web/lib/prompt_chooser.js b/web/lib/prompt_chooser.js new file mode 100644 index 0000000..dae7178 --- /dev/null +++ b/web/lib/prompt_chooser.js @@ -0,0 +1,374 @@ +/** + * Fetch/cache and DOM overlays for the Prompt Library node, backed by + * MoonPack's /moonpack/prompt_library routes. + */ + +import { api } from "../../../scripts/api.js"; + +const STYLE_ID = "moonpack-prompt-chooser-style"; +const CSS = ` +.moonpack-prompt-chooser { + position: absolute; + z-index: 10000; + display: flex; + flex-direction: column; + width: 420px; + max-height: 60vh; + background: var(--comfy-menu-bg, #353535); + color: var(--fg-color, #fff); + border: 1px solid var(--border-color, #4e4e4e); + border-radius: 6px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5); + font-family: sans-serif; + font-size: 13px; + overflow: hidden; +} +.moonpack-prompt-chooser input { + flex: 0 0 auto; + margin: 6px; + padding: 6px 8px; + background: var(--comfy-input-bg, #222); + color: var(--input-text, #ddd); + border: 1px solid var(--border-color, #4e4e4e); + border-radius: 4px; + outline: none; +} +.moonpack-prompt-chooser ul { + flex: 1 1 auto; + margin: 0; + padding: 0 0 4px 0; + list-style: none; + overflow-y: auto; +} +.moonpack-prompt-chooser li { + padding: 5px 10px; + cursor: pointer; +} +.moonpack-prompt-chooser li:hover, +.moonpack-prompt-chooser li.selected { + background: var(--comfy-menu-secondary-bg, #4a4a4a); +} +.moonpack-prompt-chooser .moonpack-entry-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.moonpack-prompt-chooser .moonpack-entry-desc { + font-size: 11px; + opacity: 0.65; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.moonpack-prompt-chooser .moonpack-empty { + padding: 10px; + opacity: 0.6; + cursor: default; +} +.moonpack-prompt-dialog-backdrop { + position: fixed; + inset: 0; + z-index: 10001; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; +} +.moonpack-prompt-dialog { + width: 380px; + background: var(--comfy-menu-bg, #353535); + color: var(--fg-color, #fff); + border: 1px solid var(--border-color, #4e4e4e); + border-radius: 6px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5); + font-family: sans-serif; + font-size: 13px; + padding: 14px; +} +.moonpack-prompt-dialog h3 { + margin: 0 0 10px 0; + font-size: 14px; +} +.moonpack-prompt-dialog label { + display: block; + margin: 8px 0 3px 0; + opacity: 0.8; +} +.moonpack-prompt-dialog input { + width: 100%; + box-sizing: border-box; + padding: 6px 8px; + background: var(--comfy-input-bg, #222); + color: var(--input-text, #ddd); + border: 1px solid var(--border-color, #4e4e4e); + border-radius: 4px; + outline: none; +} +.moonpack-prompt-dialog input:disabled { + opacity: 0.6; +} +.moonpack-prompt-dialog .moonpack-error { + color: #e06868; + margin-top: 8px; + min-height: 1em; +} +.moonpack-prompt-dialog .moonpack-dialog-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 14px; +} +.moonpack-prompt-dialog button { + padding: 6px 14px; + background: var(--comfy-input-bg, #222); + color: var(--input-text, #ddd); + border: 1px solid var(--border-color, #4e4e4e); + border-radius: 4px; + cursor: pointer; +} +.moonpack-prompt-dialog button:hover { + background: var(--comfy-menu-secondary-bg, #4a4a4a); +} +`; + +function ensureStyle() { + if (document.getElementById(STYLE_ID)) return; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = CSS; + document.head.appendChild(style); +} + +let cache = null; +let inflight = null; + +/** Returns every saved entry ({name, description, tags, text}), cached for the session. */ +export function fetchPromptEntries(force = false) { + if (force) { + cache = null; + inflight = null; + } + if (cache) return Promise.resolve(cache); + if (!inflight) { + inflight = api + .fetchApi("/moonpack/prompt_library") + .then((response) => response.json()) + .then((data) => { + cache = Array.isArray(data?.entries) ? data.entries : []; + return cache; + }) + .catch((error) => { + console.error("[MoonPack] could not load the prompt library:", error); + inflight = null; + return []; + }); + } + return inflight; +} + +export function clearPromptCache() { + cache = null; + inflight = null; +} + +async function request(method, path, body) { + const response = await api.fetchApi(path, { + method, + headers: body ? { "Content-Type": "application/json" } : undefined, + body: body ? JSON.stringify(body) : undefined, + }); + const data = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(data?.error || `Request failed (${response.status}).`); + } + clearPromptCache(); + return data.entries; +} + +export const createPromptEntry = (entry) => request("POST", "/moonpack/prompt_library", entry); +export const updatePromptEntry = (name, entry) => + request("PUT", `/moonpack/prompt_library/${encodeURIComponent(name)}`, entry); +export const deletePromptEntry = (name) => + request("DELETE", `/moonpack/prompt_library/${encodeURIComponent(name)}`); + +/** + * Opens the searchable entry picker near the given mouse event. + * `onPick` is called with the chosen entry; cancelling calls nothing. + */ +export function showPromptChooser(event, entries, onPick) { + ensureStyle(); + + const root = document.createElement("div"); + root.className = "moonpack-prompt-chooser"; + + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Filter prompts…"; + input.spellcheck = false; + + const list = document.createElement("ul"); + root.append(input, list); + document.body.appendChild(root); + + const left = Math.min(event?.clientX ?? 100, window.innerWidth - 440); + const top = Math.min(event?.clientY ?? 100, window.innerHeight - 260); + root.style.left = `${Math.max(8, left)}px`; + root.style.top = `${Math.max(8, top)}px`; + + let shown = []; + let selected = 0; + + function close() { + document.removeEventListener("pointerdown", onOutside, true); + root.remove(); + } + + function pick(entry) { + close(); + if (entry != null) onPick(entry); + } + + function onOutside(e) { + if (!root.contains(e.target)) close(); + } + + function render() { + const filter = input.value.trim().toLowerCase(); + shown = filter + ? entries.filter((entry) => (entry.name || "").toLowerCase().includes(filter)) + : entries.slice(); + selected = 0; + list.replaceChildren(); + + if (!shown.length) { + const empty = document.createElement("li"); + empty.className = "moonpack-empty"; + empty.textContent = entries.length ? "No match" : "Library is empty"; + list.appendChild(empty); + return; + } + + shown.forEach((entry, index) => { + const item = document.createElement("li"); + const nameLine = document.createElement("div"); + nameLine.className = "moonpack-entry-name"; + nameLine.textContent = entry.name; + item.appendChild(nameLine); + if (entry.description) { + const descLine = document.createElement("div"); + descLine.className = "moonpack-entry-desc"; + descLine.textContent = entry.description; + item.appendChild(descLine); + } + item.title = [entry.description, entry.tags].filter(Boolean).join(" · "); + if (index === selected) item.classList.add("selected"); + item.addEventListener("click", () => pick(entry)); + list.appendChild(item); + }); + } + + function moveSelection(delta) { + if (!shown.length) return; + const items = list.querySelectorAll("li"); + items[selected]?.classList.remove("selected"); + selected = Math.max(0, Math.min(shown.length - 1, selected + delta)); + items[selected]?.classList.add("selected"); + items[selected]?.scrollIntoView({ block: "nearest" }); + } + + input.addEventListener("input", render); + input.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + e.preventDefault(); + close(); + } else if (e.key === "ArrowDown") { + e.preventDefault(); + moveSelection(1); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + moveSelection(-1); + } else if (e.key === "Enter") { + e.preventDefault(); + if (shown.length) pick(shown[selected]); + } + e.stopPropagation(); + }); + + render(); + setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0); + input.focus(); +} + +/** + * Opens a small modal with name/description/tags fields. + * `onSubmit` receives {name, description, tags} and may return a Promise; + * a rejection's message is shown inline and the dialog stays open. + */ +export function showPromptEditDialog({ title, name = "", description = "", tags = "", lockName = false }, onSubmit) { + ensureStyle(); + + const backdrop = document.createElement("div"); + backdrop.className = "moonpack-prompt-dialog-backdrop"; + + const dialog = document.createElement("div"); + dialog.className = "moonpack-prompt-dialog"; + backdrop.appendChild(dialog); + document.body.appendChild(backdrop); + + const heading = document.createElement("h3"); + heading.textContent = title; + dialog.appendChild(heading); + + function field(labelText, value, disabled) { + const label = document.createElement("label"); + label.textContent = labelText; + const box = document.createElement("input"); + box.type = "text"; + box.value = value; + box.disabled = !!disabled; + dialog.append(label, box); + return box; + } + + const nameInput = field("Name", name, lockName); + const descInput = field("Description", description); + const tagsInput = field("Tags (comma-separated)", tags); + + const error = document.createElement("div"); + error.className = "moonpack-error"; + dialog.appendChild(error); + + const buttons = document.createElement("div"); + buttons.className = "moonpack-dialog-buttons"; + const cancelBtn = document.createElement("button"); + cancelBtn.textContent = "Cancel"; + const submitBtn = document.createElement("button"); + submitBtn.textContent = "Save"; + buttons.append(cancelBtn, submitBtn); + dialog.appendChild(buttons); + + function close() { + backdrop.remove(); + } + + cancelBtn.addEventListener("click", close); + backdrop.addEventListener("pointerdown", (e) => { + if (e.target === backdrop) close(); + }); + + submitBtn.addEventListener("click", () => { + error.textContent = ""; + const result = onSubmit({ + name: nameInput.value.trim(), + description: descInput.value.trim(), + tags: tagsInput.value.trim(), + }); + Promise.resolve(result) + .then(close) + .catch((err) => { + error.textContent = err?.message || String(err); + }); + }); + + (lockName ? descInput : nameInput).focus(); +} diff --git a/web/prompt_library.js b/web/prompt_library.js new file mode 100644 index 0000000..096a213 --- /dev/null +++ b/web/prompt_library.js @@ -0,0 +1,224 @@ +/** + * Prompt Library - a managed multiline text box backed by a shared, + * server-side JSON library (see prompt_library_routes.py). + * + * The node's native `text` widget IS the output; `entry_name` is a hidden + * native widget that just remembers which library entry is loaded, so + * reopening the workflow shows the right picker label. Everything else + * (description, tags) lives server-side and is fetched fresh when needed. + */ + +import { app } from "../../scripts/app.js"; +import { drawButton } from "./lib/canvas_draw.js"; +import { + clearPromptCache, + createPromptEntry, + deletePromptEntry, + fetchPromptEntries, + showPromptChooser, + showPromptEditDialog, + updatePromptEntry, +} from "./lib/prompt_chooser.js"; + +const NODE_TYPE = "MoonPack_PromptLibrary"; +const MARGIN = 10; + +const lineHeight = () => LiteGraph.NODE_WIDGET_HEIGHT; + +let lastPointerEvent = null; +document.addEventListener("pointerdown", (e) => (lastPointerEvent = e), true); + +/** A full-width clickable button row; fires `onPress` on pointer-up inside it. */ +class ClickWidget { + constructor(name, getLabel, onPress) { + this.name = name; + this.type = "custom"; + this.value = null; + this.options = { serialize: false }; + this.getLabel = getLabel; + this.onPress = onPress; + this._bounds = [0, 0, 0, 0]; + this._down = false; + // Not part of INPUT_TYPES: must be stripped before LiteGraph's default + // configure() restores widgets_values by raw index, and re-added after. + this._moonExtra = true; + } + + computeSize(width) { + return [width, lineHeight() + 4]; + } + + draw(ctx, node, width, posY, height) { + const x = MARGIN; + const w = node.size[0] - MARGIN * 2; + drawButton(ctx, x, posY + 2, w, height - 4, this.getLabel(node)); + this._bounds = [x, posY + 2, w, height - 4]; + } + + _hit(pos) { + const localY = pos[1] - (this.last_y ?? 0); + const [x, y, w, h] = this._bounds; + return pos[0] >= x && pos[0] <= x + w && localY >= y && localY <= y + h; + } + + mouse(event, pos, node) { + const type = String(event?.type || ""); + if (type.endsWith("down")) { + this._down = this._hit(pos); + return this._down; + } + if (type.endsWith("up")) { + const wasDown = this._down; + this._down = false; + if (wasDown && this._hit(pos)) this.onPress(event, node); + return wasDown; + } + return false; + } +} + +/** Hides a native widget from layout/drawing while keeping it serialized. */ +function hideWidget(widget) { + widget.computeSize = () => [0, -4]; + widget.draw = () => {}; +} + +/** Inserts the picker before `text` and appends the action buttons at the end. */ +function addExtraWidgets(node) { + const picker = new ClickWidget( + "moonPicker", + (n) => n.moonEntryName() || "📋 Pick a Saved Prompt…", + (event, n) => n.moonOpenChooser(event ?? lastPointerEvent), + ); + const textIndex = node.widgets.findIndex((w) => w.name === "text"); + node.widgets.splice(textIndex === -1 ? 0 : textIndex, 0, picker); + + node.widgets.push( + new ClickWidget("moonSaveAsNew", () => "💾 Save as New", + (event, n) => n.moonSaveAsNew()), + new ClickWidget("moonUpdateSelected", () => "✏️ Update Selected", + (event, n) => n.moonUpdateSelected()), + new ClickWidget("moonDeleteSelected", () => "🗑️ Delete Selected", + (event, n) => n.moonDeleteSelected()), + ); +} + +function setupPromptLibrary(nodeType) { + nodeType.prototype.moonEntryName = function () { + const widget = this.widgets?.find((w) => w.name === "entry_name"); + return widget ? String(widget.value ?? "") : ""; + }; + + nodeType.prototype.moonSetEntryName = function (name) { + const widget = this.widgets?.find((w) => w.name === "entry_name"); + if (widget) widget.value = name; + }; + + nodeType.prototype.moonTextWidget = function () { + return this.widgets?.find((w) => w.name === "text"); + }; + + nodeType.prototype.moonOpenChooser = function (event) { + fetchPromptEntries().then((entries) => { + showPromptChooser(event, entries, (entry) => { + this.moonTextWidget().value = entry.text ?? ""; + this.moonSetEntryName(entry.name ?? ""); + this.setDirtyCanvas(true, true); + }); + }); + }; + + nodeType.prototype.moonSaveAsNew = function () { + showPromptEditDialog( + { title: "Save as New Prompt" }, + ({ name, description, tags }) => { + if (!name) return Promise.reject(new Error("Name is required.")); + return createPromptEntry({ + name, description, tags, text: this.moonTextWidget().value ?? "", + }).then(() => { + this.moonSetEntryName(name); + this.setDirtyCanvas(true, true); + }); + }, + ); + }; + + nodeType.prototype.moonUpdateSelected = function () { + const name = this.moonEntryName(); + if (!name) { + alert("No prompt is loaded. Use 'Save as New' first."); + return; + } + fetchPromptEntries(true).then((entries) => { + const entry = entries.find((e) => e.name === name); + if (!entry) { + alert(`'${name}' no longer exists in the library. Use 'Save as New' instead.`); + return; + } + showPromptEditDialog( + { + title: "Update Prompt", + name: entry.name, + description: entry.description, + tags: entry.tags, + lockName: true, + }, + ({ description, tags }) => + updatePromptEntry(name, { + description, tags, text: this.moonTextWidget().value ?? "", + }), + ); + }); + }; + + nodeType.prototype.moonDeleteSelected = function () { + const name = this.moonEntryName(); + if (!name) { + alert("No prompt is loaded."); + return; + } + if (!confirm(`Delete '${name}' from the prompt library? This cannot be undone.`)) return; + deletePromptEntry(name) + .then(() => { + this.moonSetEntryName(""); + this.setDirtyCanvas(true, true); + }) + .catch((err) => alert(err?.message || String(err))); + }; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + this.serialize_widgets = true; + + const entryNameWidget = this.widgets?.find((w) => w.name === "entry_name"); + if (entryNameWidget) hideWidget(entryNameWidget); + + addExtraWidgets(this); + + clearPromptCache(); + this.computeSize(); + this.setDirtyCanvas(true, true); + }; + + // Default configure() restores widgets_values onto this.widgets by raw + // index; the picker/button widgets aren't in that saved array, so they + // must be pulled out first and reinserted after the real values land. + const configure = nodeType.prototype.configure; + nodeType.prototype.configure = function (info) { + this.widgets = (this.widgets || []).filter((w) => !w._moonExtra); + configure?.apply(this, arguments); + addExtraWidgets(this); + this.computeSize(); + this.setDirtyCanvas(true, true); + }; +} + +app.registerExtension({ + name: "comfyui.moonpack.prompt-library", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === NODE_TYPE) { + setupPromptLibrary(nodeType); + } + }, +});