Modification of the RS Prompts node to work with LLM
This commit is contained in:
+133
-22
@@ -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))
|
||||
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"}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
__version__ = "0.17.0"
|
||||
__version__ = "0.17.1"
|
||||
import importlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
|
||||
+331
-37
@@ -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 = `
|
||||
<div style="color:#aadaff; font-size:14px; margin-bottom:10px;">✏️ EDITING MODE</div>
|
||||
<div style="color:#ccc; font-size:12px;">Edit the prompt below and click APPROVE</div>
|
||||
<div style="color:#666; font-size:10px; margin-top:8px;">⏳ Waiting for your decision...</div>
|
||||
`;
|
||||
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 = "⏸️ <span style='color:#28a745'>WAITING FOR EDIT</span>";
|
||||
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 = "⚡ <span style='color:#aadaff'>Active</span>";
|
||||
statusIndicator.style.background = "#2a2a2a";
|
||||
removeWaitingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
toggleInput.addEventListener("change", (e) => {
|
||||
pauseModeEnabled = e.target.checked;
|
||||
if (pauseModeEnabled) {
|
||||
statusIndicator.innerHTML = "🔘 <span style='color:#ffaa44'>Pause ON</span>";
|
||||
statusIndicator.style.background = "#3a2a1a";
|
||||
} else {
|
||||
statusIndicator.innerHTML = "⚡ <span style='color:#aadaff'>Active</span>";
|
||||
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 = "🔘 <span style='color:#ffaa44'>Pause ON</span>";
|
||||
statusIndicator.style.background = "#3a2a1a";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user