diff --git a/Rayko_Prompts.py b/Rayko_Prompts.py index f828a6f..fe7e0be 100644 --- a/Rayko_Prompts.py +++ b/Rayko_Prompts.py @@ -3,6 +3,8 @@ import os import server import torch from aiohttp import web +from concurrent.futures import Future +import time CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROMPTS_DIR = os.path.join(CURRENT_DIR, "prompts") @@ -10,14 +12,22 @@ PROMPTS_DIR = os.path.join(CURRENT_DIR, "prompts") if not os.path.exists(PROMPTS_DIR): os.makedirs(PROMPTS_DIR) +PENDING_PROMPTS = {} + class RSPrompts: @classmethod def INPUT_TYPES(cls): return { "required": { - "clip": ("CLIP", {"tooltip": "CLIP model to encode prompts with."}), - "Positive prompt": ("STRING", {"default": "", "multiline": True}), - "Negative prompt": ("STRING", {"default": "", "multiline": True}), + "clip": ("CLIP",), + "text": ("STRING", {"multiline": True, "default": "", "hidden": True}), + }, + "optional": { + "text_input": ("STRING", {"forceInput": True}), + "pause_for_edit": ("BOOLEAN", {"default": False, "label": "⏸️ Pause for manual edit"}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", } } @@ -25,37 +35,79 @@ class RSPrompts: RETURN_NAMES = ("POSITIVE", "NEGATIVE") FUNCTION = "encode_prompts" CATEGORY = "🦊 RaykoStudio" - DESCRIPTION = "Combined positive and negative prompt encoder with preset management." + DESCRIPTION = "Text encoder with visual prompt controls and pause-for-edit mode." - def encode_prompts(self, clip, **kwargs): - positive = kwargs.get("Positive prompt", "") - negative = kwargs.get("Negative prompt", "") - - tokens_pos = clip.tokenize(positive) + def encode_prompts(self, clip, pause_for_edit=False, text="", text_input=None, unique_id=None): + current_text = text_input if text_input is not None else text + + if pause_for_edit and text_input is not None and current_text and unique_id: + print(f"[RS Prompts 🦊] Pause mode enabled for node {unique_id}") + + PENDING_PROMPTS[unique_id] = { + "original": current_text, + "edited": current_text, + "status": "pending", + "timestamp": time.time(), + "clip": clip + } + + from server import PromptServer + PromptServer.instance.send_sync("rs.prompt.pause", { + "node_id": unique_id, + "prompt": current_text + }) + + future = Future() + PENDING_PROMPTS[unique_id]["future"] = future + + try: + result = future.result(timeout=600) + if result["status"] == "approved": + final_text = result["prompt"] + print(f"[RS Prompts 🦊] Using edited prompt: {final_text[:50]}...") + else: + final_text = current_text + print(f"[RS Prompts 🦊] Using original prompt") + except TimeoutError: + print(f"[RS Prompts 🦊] Timeout! Using original prompt") + final_text = current_text + except Exception as e: + print(f"[RS Prompts 🦊] Error: {e}, using original") + final_text = current_text + finally: + if unique_id in PENDING_PROMPTS: + del PENDING_PROMPTS[unique_id] + + current_text = final_text + + tokens_pos = clip.tokenize(current_text) pos_cond = clip.encode_from_tokens_scheduled(tokens_pos) - tokens_neg = clip.tokenize(negative) + tokens_neg = clip.tokenize("") neg_cond = clip.encode_from_tokens_scheduled(tokens_neg) - return (pos_cond, neg_cond) + return { + "ui": {"text": [current_text]}, + "result": (pos_cond, neg_cond) + } + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("nan") -NODE_CLASS_MAPPINGS = {"RSPrompts": RSPrompts} -NODE_DISPLAY_NAME_MAPPINGS = {"RSPrompts": "🦊 RS Prompts"} @server.PromptServer.instance.routes.post("/rs_prompts/save_prompt") async def rs_prompts_save_prompt(request): try: data = await request.json() name = data.get("name", "").strip() - if not name: return web.Response(status=400, text="Name required") + if not name: + return web.Response(status=400, text="Name required") name = "".join(c for c in name if c.isalnum() or c in " _-").strip() - if not name: return web.Response(status=400, text="Invalid name") - + if not name: + return web.Response(status=400, text="Invalid name") filepath = os.path.join(PROMPTS_DIR, f"{name}.json") - prompt_data = { - "positive": data.get("positive", ""), - "negative": data.get("negative", "") - } + prompt_data = {"text": data.get("text", "")} with open(filepath, 'w', encoding='utf-8') as f: json.dump(prompt_data, f, indent=2) return web.Response(status=200, text="OK") @@ -90,11 +142,70 @@ async def rs_prompts_delete_prompt(request): try: data = await request.json() name = data.get("name") - if not name: return web.Response(status=400, text="Name required") + if not name: + return web.Response(status=400, text="Name required") filepath = os.path.join(PROMPTS_DIR, f"{name}.json") if os.path.exists(filepath): os.remove(filepath) return web.Response(status=200, text="OK") return web.Response(status=404, text="Prompt not found") except Exception as e: - return web.Response(status=500, text=str(e)) \ No newline at end of file + return web.Response(status=500, text=str(e)) + + +@server.PromptServer.instance.routes.post("/rs_prompts/approve_edit") +async def rs_prompts_approve_edit(request): + try: + data = await request.json() + node_id = data.get("node_id") + edited_prompt = data.get("prompt", "") + + if node_id in PENDING_PROMPTS: + pending = PENDING_PROMPTS[node_id] + pending["status"] = "approved" + pending["edited"] = edited_prompt + + if "future" in pending: + pending["future"].set_result({ + "status": "approved", + "prompt": edited_prompt + }) + + print(f"[RS Prompts 🦊] Node {node_id} approved with new prompt") + return web.Response(status=200, text="Approved") + else: + return web.Response(status=404, text=f"Node {node_id} not waiting") + + except Exception as e: + print(f"[RS Prompts 🦊] Error in approve_edit: {e}") + return web.Response(status=500, text=str(e)) + + +@server.PromptServer.instance.routes.post("/rs_prompts/reject_edit") +async def rs_prompts_reject_edit(request): + try: + data = await request.json() + node_id = data.get("node_id") + + if node_id in PENDING_PROMPTS: + pending = PENDING_PROMPTS[node_id] + pending["status"] = "rejected" + + if "future" in pending: + pending["future"].set_result({ + "status": "rejected", + "prompt": pending["original"] + }) + + print(f"[RS Prompts 🦊] Node {node_id} rejected, using original") + return web.Response(status=200, text="Rejected") + else: + return web.Response(status=404, text=f"Node {node_id} not waiting") + + except Exception as e: + print(f"[RS Prompts 🦊] Error in reject_edit: {e}") + return web.Response(status=500, text=str(e)) + + +NODE_CLASS_MAPPINGS = {"RSPrompts": RSPrompts} +NODE_DISPLAY_NAME_MAPPINGS = {"RSPrompts": "🦊 RS Prompts"} \ No newline at end of file diff --git a/__init__.py b/__init__.py index f5acb72..c04297f 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.17.0" +__version__ = "0.17.1" import importlib import logging from pathlib import Path diff --git a/package.json b/package.json index afd0b1b..2f5813d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.17.0", + "version": "0.17.1", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "MIT", diff --git a/prompts/1.json b/prompts/1.json new file mode 100644 index 0000000..2aa8c8c --- /dev/null +++ b/prompts/1.json @@ -0,0 +1,3 @@ +{ + "text": "Roshii art style, a rugged, middle-aged man with a thick beard and spiky short hair stands confidently on a gritty urban street at dusk; he wears dark sunglasses, smokes a cigar emitting smoke into the air, and has tattoos covering his left arm while wearing a torn leather jacket over a white t-shirt revealing part of his chest; his right hand grips a silver pistol holstered at hip level, adorned with various pouches and gear strapped to his waistband; background features towering buildings under a hazy teal sky with neon signs glowing faintly \u00e2 one red sign displays Chinese characters \u00e2 evoking cyberpunk meets post-apocalyptic aesthetics; detailed brushwork mimics ink wash textures with sharp contrasts between shadowed forms and illuminated surfaces; intense gaze directed off-camera as if surveying danger ahead; overall composition emphasizes raw masculinity, solitude, and edgy survivalist vibe through dynamic lighting and atmospheric depth." +} \ No newline at end of file diff --git a/prompts/2.json b/prompts/2.json new file mode 100644 index 0000000..85c90c8 --- /dev/null +++ b/prompts/2.json @@ -0,0 +1,3 @@ +{ + "text": "A digital illustration shoot from the side about a muscular young man standing confidently in a dimly lit library, holding a knife in his right hand. the image also shows the man, who appears to be in his mid-twenties, standing in the middle of the image, with a serious expression on his face. he has short, black hair and is wearing a black jacket over a white shirt, paired with dark jeans. the man is holding a knife in his left hand, and his right hand is clenched into fists. he is standing in a dimly lit library, with tall wooden shelves filled with books, and the lighting is dim, creating a moody atmosphere. the image has a realistic style with detailed shading and highlights, giving it a polished look. the man is positioned in the center of the image, with his full body visible, and his posture conveys a sense of strength and determination. he is wearing a black jacket over a white shirt, and his black jeans are tucked into his pants, completing the look." +} \ No newline at end of file diff --git a/prompts/3.json b/prompts/3.json new file mode 100644 index 0000000..548c187 --- /dev/null +++ b/prompts/3.json @@ -0,0 +1,3 @@ +{ + "text": "A photo-realistic portrait shoot from a portrait angle about a young woman with wavy brown hair and freckles, wearing a blue and gold embroidered vest and white blouse. in the middle of the image, a young woman appears to be in her early twenties, with fair skin, brown hair, and green eyes, looking directly at the viewer with a serious expression. she is wearing a blue vest with intricate gold embroidery and white blouse with long sleeves, and a brown belt with a gold buckle. her hair is blowing in the wind, and she is wearing dangling earrings. the background is blurred, with a soft, muted color palette. the lighting is soft and natural, highlighting her delicate features and the intricate details of the vest and blouse. the image has a serene and contemplative atmosphere, with the woman's expression conveying a sense of calmness and introspection." +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6127ca2..bcd9bf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.17.0" +version = "0.17.1" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/rs_prompts.js b/web/rs_prompts.js index 1016463..6444cd2 100644 --- a/web/rs_prompts.js +++ b/web/rs_prompts.js @@ -4,7 +4,88 @@ import { api } from "../../scripts/api.js"; const NODE_CLASS = "RSPrompts"; let pendingDeleteName = null; -function mkEl(tag, css) { const el = document.createElement(tag); if (css) el.style.cssText = css; return el; } +const styleBlock = document.createElement('style'); +styleBlock.innerHTML = ` + textarea.comfy-multiline-input { opacity: 1 !important; color: #fff !important; text-shadow: none !important; } + textarea.comfy-multiline-input:disabled { opacity: 0.5 !important; color: #888 !important; } + .rs-waiting-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + border-radius: 8px; + } + .rs-waiting-message { + background: #2a2a2a; + padding: 20px; + border-radius: 8px; + border: 1px solid #5090cc; + text-align: center; + } + /* Toggle switch styles */ + .rs-toggle-switch { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + gap: 8px; + } + .rs-toggle-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; + } + .rs-toggle-slider { + position: relative; + display: inline-block; + width: 40px; + height: 20px; + background-color: #444; + border-radius: 20px; + transition: 0.3s; + cursor: pointer; + } + .rs-toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + border-radius: 50%; + transition: 0.3s; + } + input:checked + .rs-toggle-slider { + background-color: #28a745; + } + input:checked + .rs-toggle-slider:before { + transform: translateX(20px); + } + .rs-toggle-label { + font-size: 11px; + color: #ccc; + user-select: none; + } + .rs-toggle-active .rs-toggle-label { + color: #28a745; + font-weight: bold; + } +`; +document.head.appendChild(styleBlock); + +function mkEl(tag, css) { + const el = document.createElement(tag); + if (css) el.style.cssText = css; + return el; +} app.registerExtension({ name: "RSPrompts", @@ -16,43 +97,147 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { const result = origOnNodeCreated?.apply(this, arguments); const node = this; + + const hidePhantomSlot = () => { + if (node.inputs) { + const textInput = node.inputs.find(i => i.name === "text"); + if (textInput) { + textInput.disabled = true; + textInput.color_on = "transparent"; + textInput.color_off = "transparent"; + textInput.pos = [-1000, -1000]; + } + } + }; + setTimeout(hidePhantomSlot, 0); + + const textWidget = node.widgets?.find(w => w.name === "text"); + const pauseWidget = node.widgets?.find(w => w.name === "pause_for_edit"); - const widgets = { - positive: node.widgets.find(w => w.name === "Positive prompt"), - negative: node.widgets.find(w => w.name === "Negative prompt") + if (pauseWidget) { + pauseWidget.hidden = true; + pauseWidget.serializeValue = () => toggleInput.checked; + } + + if (textWidget) textWidget.hidden = true; + + const enableWidgets = () => { + if (textWidget) { + textWidget.disabled = false; + textWidget.serializeValue = () => textWidget.value; + } + }; + setTimeout(enableWidgets, 50); + + const origOnConnectionsChange = node.onConnectionsChange; + node.onConnectionsChange = function(slotType, slotIndex, isConnected, link, linkInfo) { + if (origOnConnectionsChange) origOnConnectionsChange.apply(this, arguments); + setTimeout(enableWidgets, 50); }; - const root = mkEl("div", "display:flex;flex-direction:column;gap:2px;padding:0px 4px 4px 4px;width:100%;box-sizing:border-box;position:relative;margin-top:0px;"); + let waitingOverlay = null; + let pauseModeEnabled = false; + + const removeWaitingOverlay = () => { + if (waitingOverlay && waitingOverlay.parentNode) { + waitingOverlay.remove(); + waitingOverlay = null; + } + }; + + const showWaitingOverlay = () => { + removeWaitingOverlay(); + waitingOverlay = mkEl("div", "rs-waiting-overlay"); + const messageDiv = mkEl("div", "rs-waiting-message"); + messageDiv.innerHTML = ` +
✏️ EDITING MODE
+
Edit the prompt below and click APPROVE
+
⏳ Waiting for your decision...
+ `; + waitingOverlay.appendChild(messageDiv); + const domWidget = node.domWidgets?.find(w => w.name === "prompt_ui"); + if (domWidget && domWidget.element) { + domWidget.element.style.position = "relative"; + domWidget.element.appendChild(waitingOverlay); + } + }; + + const root = mkEl("div", "display:flex;flex-direction:column;height:100%;padding:0;margin:0;box-sizing:border-box;overflow:hidden;position:relative;"); + + const controlBar = mkEl("div", "display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:#1a1a1a;border-bottom:1px solid #333;gap:8px;"); + + const toggleContainer = mkEl("label", "rs-toggle-switch"); + const toggleInput = document.createElement("input"); + toggleInput.type = "checkbox"; + const toggleSlider = mkEl("span", "rs-toggle-slider"); + const toggleLabel = mkEl("span", "rs-toggle-label"); + toggleLabel.textContent = "⏸️ Pause for edit"; + + toggleContainer.appendChild(toggleInput); + toggleContainer.appendChild(toggleSlider); + toggleContainer.appendChild(toggleLabel); + + const statusIndicator = mkEl("div", "font-size:10px;padding:2px 6px;border-radius:4px;background:#2a2a2a;color:#888;"); + statusIndicator.textContent = "⚡ Normal mode"; + + controlBar.appendChild(toggleContainer); + controlBar.appendChild(statusIndicator); + root.appendChild(controlBar); + + const customTextarea = document.createElement("textarea"); + customTextarea.className = "rs-custom-textarea"; + customTextarea.style.cssText = "flex:1;width:100%;min-height:0;border:none;border-radius:4px;padding:8px;background:#111;color:#fff;font-family:system-ui,sans-serif;font-size:12px;resize:none;outline:none;box-sizing:border-box;"; + customTextarea.placeholder = "Enter your prompt here..."; + if (textWidget) customTextarea.value = textWidget.value; + + customTextarea.addEventListener("input", () => { + if (textWidget) { + textWidget.value = customTextarea.value; + node.graph?.setDirtyCanvas(true, true); + } + }); + root.appendChild(customTextarea); + + const buttonsWrapper = mkEl("div", "width:100%;display:flex;flex-direction:column;gap:4px;padding:4px;box-sizing:border-box;"); const clearRow = mkEl("div", "display:flex;gap:4px;width:100%;"); - const clearPosBtn = mkEl("button", "flex:1;padding:4px 2px;font-size:10px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;"); - clearPosBtn.textContent = "❌ Clear Positive"; - const clearNegBtn = mkEl("button", "flex:1;padding:4px 2px;font-size:10px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;"); - clearNegBtn.textContent = "❌ Clear Negative"; - clearRow.append(clearPosBtn, clearNegBtn); + const clearBtn = mkEl("button", "flex:1;padding:6px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;"); + clearBtn.textContent = "❌ Clear prompt"; + clearRow.append(clearBtn); const btnRow = mkEl("div", "display:flex;gap:4px;width:100%;"); const saveBtn = mkEl("button", "flex:1;padding:6px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;"); saveBtn.textContent = "💾 Save prompt"; - const selectBtn = mkEl("button", "flex:1;padding:6px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;"); selectBtn.textContent = "📂 Select prompt"; - btnRow.append(saveBtn, selectBtn); - root.append(clearRow, btnRow); + + const actionRow = mkEl("div", "display:flex;gap:4px;width:100%;margin-top:2px;"); + const acceptEditBtn = mkEl("button", "flex:1;padding:6px 2px;font-size:11px;border:1px solid #28a745;border-radius:5px;background:#1a3a1a;color:#aaffaa;cursor:pointer;transition:all 0.2s;"); + acceptEditBtn.textContent = "✔️ APPROVE & CONTINUE"; + const rejectEditBtn = mkEl("button", "flex:1;padding:6px 2px;font-size:11px;border:1px solid #dc3545;border-radius:5px;background:#3a1a1a;color:#ffaaaa;cursor:pointer;transition:all 0.2s;"); + rejectEditBtn.textContent = "❌ REJECT (use original)"; + actionRow.append(acceptEditBtn, rejectEditBtn); + + acceptEditBtn.disabled = true; + acceptEditBtn.style.opacity = "0.5"; + acceptEditBtn.style.cursor = "not-allowed"; + rejectEditBtn.disabled = true; + rejectEditBtn.style.opacity = "0.5"; + rejectEditBtn.style.cursor = "not-allowed"; + + buttonsWrapper.append(clearRow, btnRow, actionRow); + root.appendChild(buttonsWrapper); const presetListOverlay = mkEl("div", "position:absolute;display:none;top:50%;left:50%;transform:translate(-50%, -50%);flex-direction:column;max-height:200px;overflow-y:auto;background:#2a2a2a;border:1px solid #5090cc;border-radius:6px;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.8);min-width:180px;padding:5px;"); const presetNameInput = mkEl("div", "position:absolute;display:none;top:50%;left:50%;transform:translate(-50%, -50%);background:#2a2a2a;padding:10px;border:1px solid #5090cc;border-radius:6px;z-index:9999;box-shadow:0 4px 15px rgba(0,0,0,0.7);width:220px;text-align:center;"); - const inputLabel = mkEl("div", "color:#999;font-size:11px;margin-bottom:4px;text-align:left;"); inputLabel.textContent = "Prompt name:"; - const inputField = mkEl("input", "width:100%;padding:5px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;margin-bottom:5px;font-size:12px;box-sizing:border-box;"); const inputBtns = mkEl("div", "display:flex;gap:5px;"); const inputOk = mkEl("button", "flex:1;padding:4px;background:#1a3a5a;color:#aadaff;border:1px solid #5090cc;border-radius:3px;cursor:pointer;font-size:11px;"); inputOk.textContent = "OK"; const inputCancel = mkEl("button", "flex:1;padding:4px;background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:11px;"); inputCancel.textContent = "Cancel"; inputBtns.append(inputOk, inputCancel); - presetNameInput.append(inputLabel, inputField, inputBtns); const deleteConfirmOverlay = mkEl("div", "position:absolute;display:none;top:50%;left:50%;transform:translate(-50%, -50%);background:#2a2a2a;padding:10px;border:1px solid #5090cc;border-radius:6px;z-index:9999;box-shadow:0 4px 15px rgba(0,0,0,0.7);width:220px;text-align:center;"); @@ -68,20 +253,107 @@ app.registerExtension({ root.appendChild(deleteConfirmOverlay); node.addDOMWidget("prompt_ui", "custom", root); - node.setSize([370, 280]); - node.min_height = 280; + node.setSize([370, 350]); + node.min_height = 350; node.min_width = 370; - clearPosBtn.addEventListener("click", () => { - if(widgets.positive) { - widgets.positive.value = ""; - node.graph?.setDirtyCanvas(true, true); + const origOnResize = node.onResize; + node.onResize = function(size) { + if (origOnResize) origOnResize.apply(this, [size]); + if (size[0] < 370) size[0] = 370; + if (size[1] < 350) size[1] = 350; + }; + + const updateUIForPauseMode = (isPaused) => { + if (isPaused) { + clearBtn.disabled = true; + clearBtn.style.opacity = "0.5"; + clearBtn.style.cursor = "not-allowed"; + saveBtn.disabled = true; + saveBtn.style.opacity = "0.5"; + saveBtn.style.cursor = "not-allowed"; + selectBtn.disabled = true; + selectBtn.style.opacity = "0.5"; + selectBtn.style.cursor = "not-allowed"; + + acceptEditBtn.disabled = false; + acceptEditBtn.style.opacity = "1"; + acceptEditBtn.style.cursor = "pointer"; + rejectEditBtn.disabled = false; + rejectEditBtn.style.opacity = "1"; + rejectEditBtn.style.cursor = "pointer"; + + customTextarea.style.border = "2px solid #28a745"; + statusIndicator.innerHTML = "⏸️ WAITING FOR EDIT"; + statusIndicator.style.background = "#1a3a1a"; + + showWaitingOverlay(); + } else { + clearBtn.disabled = false; + clearBtn.style.opacity = "1"; + clearBtn.style.cursor = "pointer"; + saveBtn.disabled = false; + saveBtn.style.opacity = "1"; + saveBtn.style.cursor = "pointer"; + selectBtn.disabled = false; + selectBtn.style.opacity = "1"; + selectBtn.style.cursor = "pointer"; + + acceptEditBtn.disabled = true; + acceptEditBtn.style.opacity = "0.5"; + acceptEditBtn.style.cursor = "not-allowed"; + rejectEditBtn.disabled = true; + rejectEditBtn.style.opacity = "0.5"; + rejectEditBtn.style.cursor = "not-allowed"; + + customTextarea.style.border = "1px solid #444"; + statusIndicator.innerHTML = "⚡ Active"; + statusIndicator.style.background = "#2a2a2a"; + removeWaitingOverlay(); + } + }; + + toggleInput.addEventListener("change", (e) => { + pauseModeEnabled = e.target.checked; + if (pauseModeEnabled) { + statusIndicator.innerHTML = "🔘 Pause ON"; + statusIndicator.style.background = "#3a2a1a"; + } else { + statusIndicator.innerHTML = "⚡ Active"; + statusIndicator.style.background = "#2a2a2a"; } }); - clearNegBtn.addEventListener("click", () => { - if(widgets.negative) { - widgets.negative.value = ""; + acceptEditBtn.addEventListener("click", async () => { + const currentPrompt = customTextarea.value; + await fetch("/rs_prompts/approve_edit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + node_id: node.id.toString(), + prompt: currentPrompt + }) + }); + updateUIForPauseMode(false); + node.graph?.setDirtyCanvas(true, true); + }); + + rejectEditBtn.addEventListener("click", async () => { + await fetch("/rs_prompts/reject_edit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + node_id: node.id.toString() + }) + }); + updateUIForPauseMode(false); + node.graph?.setDirtyCanvas(true, true); + }); + + clearBtn.addEventListener("click", () => { + if(textWidget) { + textWidget.value = ""; + customTextarea.value = ""; node.graph?.setDirtyCanvas(true, true); } }); @@ -91,22 +363,17 @@ app.registerExtension({ deleteConfirmOverlay.style.display = "none"; presetNameInput.style.display = "block"; inputField.value = ""; - inputField.focus(); + setTimeout(() => inputField.focus(), 50); }); const performSave = () => { const name = inputField.value.trim(); if (!name) return; presetNameInput.style.display = "none"; - fetch("/rs_prompts/save_prompt", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name, - positive: widgets.positive ? widgets.positive.value : "", - negative: widgets.negative ? widgets.negative.value : "" - }) + body: JSON.stringify({ name, text: textWidget ? textWidget.value : "" }) }); }; inputOk.addEventListener("click", performSave); @@ -129,19 +396,23 @@ app.registerExtension({ row.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid #333;"; const nameSpan = document.createElement("span"); nameSpan.textContent = name; nameSpan.style.cssText = "flex:1;cursor:pointer;color:#ccc;font-size:12px;"; - nameSpan.onmouseenter = () => nameSpan.style.background = "#3a3a3a"; nameSpan.onmouseleave = () => nameSpan.style.background = "transparent"; + nameSpan.onmouseenter = () => nameSpan.style.background = "#3a3a3a"; + nameSpan.onmouseleave = () => nameSpan.style.background = "transparent"; nameSpan.onclick = async () => { presetListOverlay.style.display = "none"; const res2 = await fetch("/rs_prompts/load_prompt", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ name }) }); if(res2.ok) { const data = await res2.json(); - if(widgets.positive) widgets.positive.value = data.positive || ""; - if(widgets.negative) widgets.negative.value = data.negative || ""; + if(textWidget) { + textWidget.value = data.text || ""; + customTextarea.value = data.text || ""; + } node.graph?.setDirtyCanvas(true, true); } }; const deleteBtn = document.createElement("span"); - deleteBtn.textContent = "❌"; deleteBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;"; + deleteBtn.textContent = "❌"; + deleteBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;"; deleteBtn.onmouseenter = () => { deleteBtn.style.opacity = "1"; deleteBtn.style.transform = "scale(1.2)"; }; deleteBtn.onmouseleave = () => { deleteBtn.style.opacity = "0.7"; deleteBtn.style.transform = "scale(1)"; }; deleteBtn.onclick = async (e) => { @@ -150,7 +421,8 @@ app.registerExtension({ deleteText.textContent = `Delete "${name}"?`; deleteConfirmOverlay.style.display = "block"; }; - row.appendChild(nameSpan); row.appendChild(deleteBtn); + row.appendChild(nameSpan); + row.appendChild(deleteBtn); presetListOverlay.appendChild(row); }); } catch(e) { presetListOverlay.textContent = "Error loading"; } @@ -180,6 +452,28 @@ app.registerExtension({ if (!deleteConfirmOverlay?.contains(e.target)) deleteConfirmOverlay.style.display = "none"; }); + api.addEventListener("rs.prompt.pause", (event) => { + if (event.detail.node_id == node.id) { + customTextarea.value = event.detail.prompt; + if (textWidget) textWidget.value = event.detail.prompt; + updateUIForPauseMode(true); + node.graph?.setDirtyCanvas(true, true); + } + }); + + updateUIForPauseMode(false); + + if (pauseWidget) { + const savedValue = pauseWidget.value; + if (savedValue !== undefined) { + toggleInput.checked = savedValue; + if (savedValue) { + statusIndicator.innerHTML = "🔘 Pause ON"; + statusIndicator.style.background = "#3a2a1a"; + } + } + } + return result; }; }