Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9862906fae | ||
|
|
5cc28c6fb2 | ||
|
|
4bb484f9a9 | ||
|
|
be2a373c31 | ||
|
|
6408e014a3 | ||
|
|
6b0b6c16d7 | ||
|
|
db28fc5a7e | ||
|
|
4ea7928657 | ||
|
|
c04fa2fb57 | ||
|
|
07ba5d2bb6 | ||
|
|
a9cfa35bd5 | ||
|
|
f8b75b2a29 | ||
|
|
952bdb34c7 |
@@ -49,7 +49,9 @@ git clone https://github.com/Raykosan/ComfyUI_RaykoStudio.git
|
||||
- v0.50.1 - Added a new RS Split Video/Audio node
|
||||
- v0.50.2 - Added flux3
|
||||
- v0.50.3 - Added moss_image
|
||||
- v0.51.0 - Added the RS Load System Prompt node
|
||||
- v0.51.0 - Added the RS Load System Prompt node
|
||||
- v0.51.1 - Added fibo2
|
||||
- v0.51.2 - Modification of the RS Load System Prompt node
|
||||
|
||||
</details>
|
||||
|
||||
@@ -333,31 +335,39 @@ In any case, you can always paste the path using the keyboard shortcut Ctrl+V.
|
||||
<summary>🦊 RS Load System Prompt</summary>
|
||||
|
||||
# 🦊 RS Load System Prompt
|
||||
**A ComfyUI custom node that loads system prompts from `.txt` files with a searchable tree dropdown, folder hierarchy support, and live file list refresh — no page reload required.**
|
||||
**A ComfyUI custom node that loads, edits, and saves system prompts directly from `.txt` files — with a searchable tree dropdown, folder hierarchy, live file list refresh, and built-in save/delete functionality.**
|
||||
|
||||
<img width="1012" height="588" alt="Screenshot_2" src="https://github.com/user-attachments/assets/a6a97e78-4212-4a43-9025-475d21aed1a1" />
|
||||
<img width="630" height="865" alt="Screenshot_1" src="https://github.com/user-attachments/assets/5430770e-5809-4605-b389-fa29a259c9bb" />
|
||||
|
||||
### 🔥 Features
|
||||
**Folder-based prompts** — every `.txt` file inside `system_prompts/` becomes a selectable prompt.
|
||||
**Tree dropdown with subfolders** — nested directories are shown as expandable folders.
|
||||
**Search** — filter by any part of the path (file name or folder).
|
||||
**Live refresh** — the file list is re-scanned every time the dropdown opens. No need to reload ComfyUI after adding a new `.txt`.
|
||||
**Extra prompt textarea** — a multiline input for additional prompt text, passed through as a separate output.
|
||||
**Editable system prompt** — select a file and its contents load into a large textarea where you can edit them.
|
||||
**Save edited prompts** — save your changes back to the existing file or create a new one. `.txt` extension is added automatically.
|
||||
**Delete files** — remove prompt files directly from the dropdown (with a confirmation dialog).
|
||||
**Reload from file** — revert your textarea to the on-disk version if you made a mistake.
|
||||
**User prompt textarea** — a separate multiline input for additional prompt text, passed through as a distinct output.
|
||||
**Unsaved changes indicator** — a `*` appears in the selection button when your textarea differs from the saved file.
|
||||
**Safe by default** — path traversal protection, `PermissionError` / `UnicodeDecodeError` handling, BOM-aware UTF-8 reading.
|
||||
**Smart caching** — `IS_CHANGED` uses the file's mtime, so editing a prompt re-triggers execution without reloading the workflow.
|
||||
**Compact UI** — no hidden standard combo widget clutter; a clean labeled button with the selected file name.
|
||||
**Compact, polished UI** — dark-themed modals for save/confirm, centered labels, and a clean layout.
|
||||
|
||||
### 🪛 Usage
|
||||
- Add the node 🦊 RS Load System Prompt from the 🦊 RaykoStudio category.
|
||||
- Click LOAD SYSTEM PROMPT.
|
||||
- Click LOAD SYSTEM PROMPT (below the system prompt textarea).
|
||||
- Browse the tree or type into the search field.
|
||||
- Select a .txt file — its path is saved in the workflow.
|
||||
- Optionally fill the prompt textarea with extra text (passed through unchanged).
|
||||
- Select a .txt file — its contents load into the SYSTEM PROMPT textarea.
|
||||
- Edit the text if needed.
|
||||
- Click 💾 to save:
|
||||
- Keep the same name to overwrite the current file.
|
||||
- Change the name to save as a new file (the old one stays untouched).
|
||||
- Optionally fill the USER PROMPT textarea with extra text.
|
||||
- Connect the outputs:
|
||||
- system_prompt — contents of the selected .txt file.
|
||||
- prompt — text from the textarea.
|
||||
- system_prompt — the contents of the SYSTEM PROMPT textarea.
|
||||
- prompt — text from the USER PROMPT textarea.
|
||||
|
||||
### 📝 Adding new prompts
|
||||
### 📝 Adding new prompts
|
||||
Drop any .txt file into:
|
||||
`ComfyUI/custom_nodes/ComfyUI_RaykoStudio/system_prompts/`
|
||||
Subfolders are supported. The file list is refreshed automatically every time you open the dropdown — no ComfyUI restart or page reload needed.
|
||||
@@ -365,6 +375,12 @@ Subfolders are supported. The file list is refreshed automatically every time yo
|
||||
### 💡 Note
|
||||
If the dropdown is already open and you add a file from outside ComfyUI, click 🔄 Refresh inside the dropdown to re-scan.
|
||||
|
||||
### 🔘 Buttons
|
||||
⟳ Reload the current file from disk. Asks for confirmation if there are unsaved changes.
|
||||
💾 Open the save dialog. Pre-filled with the current file name (selected). .txt is added automatically if missing.
|
||||
🔄 Refresh (in dropdown) Re-scan the system_prompts/ folder without closing the dropdown.
|
||||
❌ (in dropdown) Delete a prompt file (with confirmation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
@@ -30,7 +30,7 @@ class RaykoModelsLoader:
|
||||
vae_files_with_pixel = vae_files + ["pixel_space"]
|
||||
weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]
|
||||
clip_type_opts = [
|
||||
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
|
||||
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "fibo2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
|
||||
]
|
||||
device_opts = ["default", "cpu", "cuda"]
|
||||
|
||||
@@ -103,4 +103,4 @@ class RaykoModelsLoader:
|
||||
return (model, clip, vae)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RaykoModelsLoader": RaykoModelsLoader}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"}
|
||||
|
||||
@@ -29,7 +29,7 @@ class RaykoModelsLoaderPro:
|
||||
|
||||
weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]
|
||||
clip_type_opts = [
|
||||
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
|
||||
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "fibo2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
|
||||
]
|
||||
device_opts = ["default", "cpu", "cuda"]
|
||||
|
||||
@@ -106,4 +106,4 @@ class RaykoModelsLoaderPro:
|
||||
return (model1, model2, clip, vae1, vae2)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RaykoModelsLoaderPro": RaykoModelsLoaderPro}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoaderPro": "🦊 RS Models Loader Pro"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoaderPro": "🦊 RS Models Loader Pro"}
|
||||
|
||||
+152
-34
@@ -21,6 +21,10 @@ from aiohttp import web
|
||||
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROMPT_DIR = os.path.join(NODE_DIR, "system_prompts")
|
||||
|
||||
ROUTE_GET_LIST = "/rs_system_prompt_dropdown/get_prompts"
|
||||
ROUTE_GET_CONTENT = "/rs_system_prompt_dropdown/get_prompt_content"
|
||||
ROUTE_SAVE = "/rs_system_prompt_dropdown/save_prompt"
|
||||
|
||||
|
||||
def ensure_prompt_dir():
|
||||
try:
|
||||
@@ -47,45 +51,181 @@ def _scan_prompts():
|
||||
return files
|
||||
|
||||
|
||||
def resolve_prompt_path(prompt_file):
|
||||
def resolve_prompt_path(prompt_file, must_exist=False):
|
||||
if not prompt_file:
|
||||
return None
|
||||
try:
|
||||
file_path = os.path.abspath(
|
||||
os.path.join(PROMPT_DIR, prompt_file.replace("/", os.sep))
|
||||
)
|
||||
rel = prompt_file.replace("\\", "/").replace("/", os.sep)
|
||||
file_path = os.path.abspath(os.path.join(PROMPT_DIR, rel))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
prompt_root = os.path.abspath(PROMPT_DIR)
|
||||
if not file_path.startswith(prompt_root + os.sep):
|
||||
return None
|
||||
if file_path == prompt_root:
|
||||
return None
|
||||
if must_exist and not os.path.isfile(file_path):
|
||||
return None
|
||||
return file_path
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/rs_system_prompt_dropdown/get_prompts")
|
||||
@server.PromptServer.instance.routes.get(ROUTE_GET_LIST)
|
||||
async def get_prompts_handler(request):
|
||||
return web.json_response({"files": _scan_prompts()})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get(ROUTE_GET_CONTENT)
|
||||
async def get_prompt_content_handler(request):
|
||||
filename = request.query.get("file", "")
|
||||
file_path = resolve_prompt_path(filename, must_exist=True)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "File not found or invalid path"},
|
||||
status=404,
|
||||
)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8-sig") as file:
|
||||
content = file.read()
|
||||
return web.json_response({"ok": True, "content": content})
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error reading file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post(ROUTE_SAVE)
|
||||
async def save_prompt_handler(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid JSON body"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = (data or {}).get("filename", "")
|
||||
content = (data or {}).get("content", "")
|
||||
|
||||
if not isinstance(filename, str) or not isinstance(content, str):
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "filename and content must be strings"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = filename.strip().replace("\\", "/")
|
||||
if not filename:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Empty file name"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if not filename.lower().endswith(".txt"):
|
||||
filename += ".txt"
|
||||
|
||||
if not ensure_prompt_dir():
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Cannot access system_prompts directory"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
file_path = resolve_prompt_path(filename)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid path"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
created = not os.path.exists(file_path)
|
||||
|
||||
try:
|
||||
parent = os.path.dirname(file_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Cannot create folder: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(content)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error writing file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
rel_path = os.path.relpath(file_path, PROMPT_DIR).replace(os.sep, "/")
|
||||
return web.json_response(
|
||||
{"ok": True, "path": rel_path, "created": created}
|
||||
)
|
||||
|
||||
|
||||
ROUTE_DELETE = "/rs_system_prompt_dropdown/delete_prompt"
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post(ROUTE_DELETE)
|
||||
async def delete_prompt_handler(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid JSON body"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = (data or {}).get("filename", "")
|
||||
if not isinstance(filename, str) or not filename.strip():
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Empty file name"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
file_path = resolve_prompt_path(filename.strip(), must_exist=True)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "File not found or invalid path"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error deleting file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
return web.json_response({"ok": True, "path": filename.strip()})
|
||||
|
||||
|
||||
class RSLoadSystemPrompt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
files = _scan_prompts()
|
||||
values = files if files else [""]
|
||||
return {
|
||||
"required": {
|
||||
"prompt_file": ("STRING", {"default": values[0] if values else ""}),
|
||||
"system_prompt": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Pick a file above or type a system prompt here",
|
||||
},
|
||||
),
|
||||
"prompt": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Additional prompt text (combined with system prompt)",
|
||||
"placeholder": "Additional user prompt",
|
||||
},
|
||||
),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt_file": ("STRING", {"default": ""}),
|
||||
"selected_file": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
@@ -95,30 +235,8 @@ class RSLoadSystemPrompt:
|
||||
FUNCTION = "load_system_prompt"
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, prompt_file, prompt, selected_file=""):
|
||||
target = selected_file or prompt_file
|
||||
path = resolve_prompt_path(target)
|
||||
if path is None:
|
||||
return ""
|
||||
try:
|
||||
return os.path.getmtime(path)
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
def load_system_prompt(self, prompt_file, prompt, selected_file=""):
|
||||
target = selected_file or prompt_file
|
||||
file_path = resolve_prompt_path(target)
|
||||
|
||||
if file_path is None:
|
||||
return ("", prompt or "")
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8-sig") as file:
|
||||
text = file.read()
|
||||
return (text, prompt or "")
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
return (f"Error reading file: {error}", prompt or "")
|
||||
def load_system_prompt(self, system_prompt, prompt, prompt_file="", selected_file=""):
|
||||
return (system_prompt or "", prompt or "")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -127,4 +245,4 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RSLoadSystemPrompt": "🦊 RS Load System Prompt",
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.51.0"
|
||||
__version__ = "0.51.2"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.51.0",
|
||||
"version": "0.51.2",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.51.0"
|
||||
version = "0.51.2"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "opencv-python", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Write ONE detailed paragraph (6–10 sentences). Describe only what is visible: subject(s) and actions; people details if present (approx age group, gender expression if clear, hair, facial expression, pose, clothing, accessories); environment (location type, background elements, time cues); lighting (source, direction, softness/hardness, color temperature, shadows); camera viewpoint (eye-level/low/high, distance) and composition (framing, focal emphasis). No preface, no reasoning, no <think>.
|
||||
+659
-123
@@ -1,44 +1,141 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const API_PATH = "/rs_system_prompt_dropdown/get_prompts";
|
||||
const NODE_NAME = "RSLoadSystemPrompt";
|
||||
const MENU_ID = "rs-spd-tree-menu";
|
||||
|
||||
function showToast(message, type = "info", node = null) {
|
||||
const API_LIST = "/rs_system_prompt_dropdown/get_prompts";
|
||||
const API_CONTENT = "/rs_system_prompt_dropdown/get_prompt_content";
|
||||
const API_SAVE = "/rs_system_prompt_dropdown/save_prompt";
|
||||
const API_DELETE = "/rs_system_prompt_dropdown/delete_prompt";
|
||||
|
||||
const MENU_ID = "rs-spd-tree-menu";
|
||||
const SAVE_MODAL_ID = "rs-spd-save-modal";
|
||||
const CONFIRM_MODAL_ID = "rs-spd-confirm";
|
||||
|
||||
|
||||
function toast(message, type = "info", node = null) {
|
||||
const existing = document.querySelector(".rs-spd-toast");
|
||||
if (existing) existing.remove();
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "rs-spd-toast";
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "rs-spd-toast";
|
||||
const bg = type === "error" ? "#f44336" : type === "ok" ? "#4CAF50" : "#2196F3";
|
||||
toast.style.cssText = `position:fixed;background:${bg};color:white;padding:12px 20px;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.4);z-index:100000;font-size:14px;font-family:sans-serif;opacity:0;transition:opacity 0.3s,transform 0.3s;transform:translateY(-20px);pointer-events:none;white-space:nowrap;`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
const r = toast.getBoundingClientRect();
|
||||
let left = window.innerWidth - r.width - 20, top = 20;
|
||||
el.style.cssText =
|
||||
`position:fixed;background:${bg};color:white;padding:12px 20px;` +
|
||||
`border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.4);` +
|
||||
`z-index:100001;font-size:14px;font-family:sans-serif;opacity:0;` +
|
||||
`transition:opacity 0.3s,transform 0.3s;transform:translateY(-20px);` +
|
||||
`pointer-events:none;white-space:nowrap;`;
|
||||
el.textContent = message;
|
||||
document.body.appendChild(el);
|
||||
|
||||
const r = el.getBoundingClientRect();
|
||||
let left = window.innerWidth - r.width - 20;
|
||||
let top = 20;
|
||||
if (node && app?.canvas) {
|
||||
try {
|
||||
const cr = app.canvas.canvas.getBoundingClientRect();
|
||||
const s = app.canvas.ds.scale;
|
||||
const ox = app.canvas.ds.offset[0], oy = app.canvas.ds.offset[1];
|
||||
const ox = app.canvas.ds.offset[0];
|
||||
const oy = app.canvas.ds.offset[1];
|
||||
const nx = cr.left + (node.pos[0] + node.size[0] / 2 + ox) * s;
|
||||
const ny = cr.top + (node.pos[1] + node.size[1] / 2 + oy) * s;
|
||||
left = Math.max(10, Math.min(nx - r.width / 2, window.innerWidth - r.width - 10));
|
||||
top = Math.max(10, Math.min(ny - r.height / 2, window.innerHeight - r.height - 10));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
toast.style.left = left + "px";
|
||||
toast.style.top = top + "px";
|
||||
void toast.offsetWidth;
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateY(0)";
|
||||
el.style.left = left + "px";
|
||||
el.style.top = top + "px";
|
||||
void el.offsetWidth;
|
||||
el.style.opacity = "1";
|
||||
el.style.transform = "translateY(0)";
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateY(-20px)";
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
el.style.opacity = "0";
|
||||
el.style.transform = "translateY(-20px)";
|
||||
setTimeout(() => el.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
function showConfirm(message, node = null) {
|
||||
return new Promise((resolve) => {
|
||||
const old = document.getElementById(CONFIRM_MODAL_ID);
|
||||
if (old) old.remove();
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = CONFIRM_MODAL_ID;
|
||||
overlay.style.cssText =
|
||||
"position:fixed;inset:0;background:rgba(0,0,0,0.55);" +
|
||||
"z-index:100003;display:flex;align-items:center;" +
|
||||
"justify-content:center;";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.style.cssText =
|
||||
"background:#1f1f1f;border:1px solid #444;border-radius:8px;" +
|
||||
"width:380px;padding:16px;display:flex;flex-direction:column;" +
|
||||
"gap:14px;box-shadow:0 8px 30px rgba(0,0,0,0.8);" +
|
||||
"font-family:sans-serif;color:#ddd;";
|
||||
|
||||
const text = document.createElement("div");
|
||||
text.textContent = message;
|
||||
text.style.cssText = "font-size:13px;line-height:1.5;color:#eee;white-space:pre-wrap;";
|
||||
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.style.cssText =
|
||||
"padding:6px 16px;background:#2a2a2a;color:#ddd;" +
|
||||
"border:1px solid #555;border-radius:4px;cursor:pointer;font-size:13px;";
|
||||
|
||||
const okBtn = document.createElement("button");
|
||||
okBtn.textContent = "OK";
|
||||
okBtn.style.cssText =
|
||||
"padding:6px 16px;background:#1a3a1a;color:#aaffaa;" +
|
||||
"border:1px solid #4CAF50;border-radius:4px;cursor:pointer;" +
|
||||
"font-size:13px;font-weight:bold;";
|
||||
|
||||
btnRow.append(cancelBtn, okBtn);
|
||||
modal.append(text, btnRow);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const close = (result) => {
|
||||
overlay.remove();
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const onKey = (ev) => {
|
||||
if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
close(false);
|
||||
} else if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
close(true);
|
||||
}
|
||||
};
|
||||
|
||||
okBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
close(true);
|
||||
});
|
||||
cancelBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
close(false);
|
||||
});
|
||||
overlay.addEventListener("pointerdown", (e) => {
|
||||
if (e.target === overlay) close(false);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
|
||||
requestAnimationFrame(() => okBtn.focus());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function buildTree(list) {
|
||||
const tree = {};
|
||||
for (const item of list) {
|
||||
@@ -67,6 +164,7 @@ function collectPaths(tree, prefix = "") {
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "RaykoStudio.SystemPromptDropdown",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
@@ -76,10 +174,48 @@ app.registerExtension({
|
||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||
const origOnRemoved = nodeType.prototype.onRemoved;
|
||||
|
||||
nodeType.prototype._getWidget = function (name) {
|
||||
return this.widgets?.find((w) => w.name === name);
|
||||
};
|
||||
|
||||
nodeType.prototype._getSystemPrompt = function () {
|
||||
const w = this._getWidget("system_prompt");
|
||||
return w ? (w.value ?? "") : "";
|
||||
};
|
||||
|
||||
nodeType.prototype._setSystemPrompt = function (text) {
|
||||
const w = this._getWidget("system_prompt");
|
||||
if (!w) return;
|
||||
w.value = text ?? "";
|
||||
if (typeof w.callback === "function") {
|
||||
try { w.callback(w.value); } catch (e) { /* ignore */ }
|
||||
}
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype._syncSelected = function () {
|
||||
const sel = this._getWidget("selected_file");
|
||||
if (sel) sel.value = this.selectedFile || "";
|
||||
|
||||
const pf = this._getWidget("prompt_file");
|
||||
if (pf) pf.value = this.selectedFile || "";
|
||||
|
||||
if (!this.properties) this.properties = {};
|
||||
this.properties.selected_file = this.selectedFile || "";
|
||||
|
||||
this._updateBtnLabel();
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype._isDirty = function () {
|
||||
const current = this._getSystemPrompt();
|
||||
return current !== (this.lastLoadedContent ?? "");
|
||||
};
|
||||
|
||||
nodeType.prototype._btnLabel = function () {
|
||||
return this.selectedFile
|
||||
? `📄 ${this.selectedFile}`
|
||||
: "📄 Select system prompt…";
|
||||
const name = this.selectedFile || "SELECT SYSTEM PROMPT…";
|
||||
const star = this._isDirty() ? " *" : "";
|
||||
return `📄 ${name}${star}`;
|
||||
};
|
||||
|
||||
nodeType.prototype._updateBtnLabel = function () {
|
||||
@@ -89,35 +225,9 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype._syncSelected = function () {
|
||||
const selWidget = this.widgets?.find((w) => w.name === "selected_file");
|
||||
if (selWidget) selWidget.value = this.selectedFile || "";
|
||||
|
||||
const pf = this.widgets?.find((w) => w.name === "prompt_file");
|
||||
if (pf) {
|
||||
const validList = Array.isArray(pf.options?.values)
|
||||
? pf.options.values
|
||||
: [];
|
||||
if (this.selectedFile && validList.includes(this.selectedFile)) {
|
||||
pf.value = this.selectedFile;
|
||||
} else if (validList.length) {
|
||||
pf.value = validList[0];
|
||||
this.selectedFile = validList[0];
|
||||
} else {
|
||||
pf.value = this.selectedFile || "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.properties) this.properties = {};
|
||||
this.properties.selected_file = this.selectedFile || "";
|
||||
|
||||
this._updateBtnLabel();
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype.loadFiles = async function (showToastOnError = false) {
|
||||
try {
|
||||
const res = await api.fetchApi(API_PATH, { cache: "no-store" });
|
||||
const res = await api.fetchApi(API_LIST, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
this.filesCache = Array.isArray(data.files) ? data.files : [];
|
||||
@@ -127,11 +237,73 @@ app.registerExtension({
|
||||
this.filesCache = [];
|
||||
this.treeCache = {};
|
||||
if (showToastOnError) {
|
||||
showToast(`Failed to load prompts: ${err.message}`, "error", this);
|
||||
toast(`Failed to load prompts: ${err.message}`, "error", this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.fetchPromptContent = async function (filename) {
|
||||
const url = `${API_CONTENT}?file=${encodeURIComponent(filename)}`;
|
||||
const res = await api.fetchApi(url, { cache: "no-store" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return data.content ?? "";
|
||||
};
|
||||
|
||||
nodeType.prototype.savePromptContent = async function (filename, content) {
|
||||
const res = await api.fetchApi(API_SAVE, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, content }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
nodeType.prototype.deletePromptFile = async function (filename) {
|
||||
const res = await api.fetchApi(API_DELETE, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
nodeType.prototype.handleDeleteFile = async function (filename, refreshCallback) {
|
||||
const ok = await showConfirm(
|
||||
`Delete file "${filename}"?\nThis action cannot be undone.`,
|
||||
this
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await this.deletePromptFile(filename);
|
||||
toast(`Deleted: ${filename}`, "ok", this);
|
||||
|
||||
if (this.selectedFile === filename) {
|
||||
this.selectedFile = "";
|
||||
this._setSystemPrompt("");
|
||||
this.lastLoadedContent = "";
|
||||
this._syncSelected();
|
||||
this._updateBtnLabel();
|
||||
}
|
||||
|
||||
await this.loadFiles(false);
|
||||
if (refreshCallback) refreshCallback();
|
||||
} catch (err) {
|
||||
toast(`Delete failed: ${err.message}`, "error", this);
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = origOnNodeCreated
|
||||
? origOnNodeCreated.apply(this, arguments)
|
||||
@@ -141,69 +313,166 @@ app.registerExtension({
|
||||
this.selectedFile = "";
|
||||
this.filesCache = [];
|
||||
this.treeCache = {};
|
||||
this.lastLoadedContent = "";
|
||||
this._promptBtnEl = null;
|
||||
this._reloadBtnEl = null;
|
||||
this._saveBtnEl = null;
|
||||
|
||||
const comboWidget = this.widgets?.find((w) => w.name === "prompt_file");
|
||||
if (comboWidget) {
|
||||
comboWidget.hidden = true;
|
||||
if (comboWidget.element) comboWidget.element.style.display = "none";
|
||||
comboWidget.computeSize = () => [0, 0];
|
||||
for (const name of ["prompt_file", "selected_file"]) {
|
||||
const w = this._getWidget(name);
|
||||
if (w) {
|
||||
w.hidden = true;
|
||||
if (w.element) w.element.style.display = "none";
|
||||
w.computeSize = () => [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
const selWidget = this.widgets?.find((w) => w.name === "selected_file");
|
||||
if (selWidget) {
|
||||
selWidget.hidden = true;
|
||||
if (selWidget.element) selWidget.element.style.display = "none";
|
||||
}
|
||||
const sysHeader = document.createElement("div");
|
||||
sysHeader.style.cssText =
|
||||
"position:relative;display:flex;align-items:center;" +
|
||||
"justify-content:center;width:100%;box-sizing:border-box;" +
|
||||
"padding:6px 2px 0 2px;";
|
||||
|
||||
if (this.properties?.selected_file) {
|
||||
this.selectedFile = this.properties.selected_file;
|
||||
} else if (comboWidget?.value) {
|
||||
this.selectedFile = comboWidget.value;
|
||||
}
|
||||
const sysLabel = document.createElement("div");
|
||||
sysLabel.textContent = "SYSTEM PROMPT";
|
||||
sysLabel.style.cssText =
|
||||
"font-size:10px;font-weight:bold;letter-spacing:1px;" +
|
||||
"color:#888;text-transform:uppercase;user-select:none;" +
|
||||
"line-height:12px;text-align:center;";
|
||||
sysHeader.appendChild(sysLabel);
|
||||
|
||||
const btnContainer = document.createElement("div");
|
||||
btnContainer.style.cssText =
|
||||
const btnGroup = document.createElement("div");
|
||||
btnGroup.style.cssText = "position:absolute;right:2px;display:flex;gap:4px;";
|
||||
|
||||
const reloadBtn = document.createElement("button");
|
||||
reloadBtn.textContent = "⟳";
|
||||
reloadBtn.title = "Reload from file";
|
||||
reloadBtn.style.cssText =
|
||||
"width:36px;height:20px;padding:0;font-size:13px;" +
|
||||
"border:1px solid #555;border-radius:4px;background:#2a2a2a;" +
|
||||
"color:#ddd;cursor:pointer;line-height:1;box-sizing:border-box;" +
|
||||
"display:flex;align-items:center;justify-content:center;";
|
||||
reloadBtn.onmouseenter = () => (reloadBtn.style.background = "#3a3a3a");
|
||||
reloadBtn.onmouseleave = () => (reloadBtn.style.background = "#2a2a2a");
|
||||
reloadBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.reloadFromFile();
|
||||
});
|
||||
this._reloadBtnEl = reloadBtn;
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.textContent = "💾";
|
||||
saveBtn.title = "Save system prompt to file";
|
||||
saveBtn.style.cssText =
|
||||
"width:36px;height:20px;padding:0;font-size:13px;" +
|
||||
"border:1px solid #555;border-radius:4px;background:#2a2a2a;" +
|
||||
"color:#ddd;cursor:pointer;line-height:1;box-sizing:border-box;" +
|
||||
"display:flex;align-items:center;justify-content:center;";
|
||||
saveBtn.onmouseenter = () => (saveBtn.style.background = "#3a3a3a");
|
||||
saveBtn.onmouseleave = () => (saveBtn.style.background = "#2a2a2a");
|
||||
saveBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.openSaveModal();
|
||||
});
|
||||
this._saveBtnEl = saveBtn;
|
||||
|
||||
btnGroup.append(reloadBtn, saveBtn);
|
||||
sysHeader.appendChild(btnGroup);
|
||||
|
||||
const sysHeaderWidget = this.addDOMWidget(
|
||||
"rs_spd_sys_header", "custom", sysHeader
|
||||
);
|
||||
sysHeaderWidget.computeSize = () => [this.width || 200, 22];
|
||||
|
||||
const topContainer = document.createElement("div");
|
||||
topContainer.style.cssText =
|
||||
"display:flex;flex-direction:column;gap:2px;" +
|
||||
"width:100%;box-sizing:border-box;";
|
||||
"width:100%;box-sizing:border-box;padding-top:8px;";
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.textContent = "LOAD SYSTEM PROMPT";
|
||||
label.style.cssText =
|
||||
const topLabel = document.createElement("div");
|
||||
topLabel.textContent = "LOAD SYSTEM PROMPT";
|
||||
topLabel.style.cssText =
|
||||
"font-size:10px;font-weight:bold;letter-spacing:1px;" +
|
||||
"color:#888;text-transform:uppercase;text-align:center;" +
|
||||
"padding:0 2px;user-select:none;line-height:12px;";
|
||||
btnContainer.appendChild(label);
|
||||
topContainer.appendChild(topLabel);
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = this._btnLabel();
|
||||
btn.style.cssText =
|
||||
const loadBtn = document.createElement("button");
|
||||
loadBtn.textContent = this._btnLabel();
|
||||
loadBtn.style.cssText =
|
||||
"width:100%;height:28px;padding:0 10px;font-size:12px;" +
|
||||
"border:1px solid #4CAF50;border-radius:5px;background:#1a3a1a;" +
|
||||
"color:#aaffaa;cursor:pointer;box-sizing:border-box;" +
|
||||
"text-align:center;text-transform:uppercase;letter-spacing:0.5px;" +
|
||||
"overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" +
|
||||
"font-weight:bold;display:block;";
|
||||
btn.onmouseenter = () => (btn.style.background = "#2a4a2a");
|
||||
btn.onmouseleave = () => (btn.style.background = "#1a3a1a");
|
||||
|
||||
btn.addEventListener("click", async (e) => {
|
||||
loadBtn.onmouseenter = () => (loadBtn.style.background = "#2a4a2a");
|
||||
loadBtn.onmouseleave = () => (loadBtn.style.background = "#1a3a1a");
|
||||
loadBtn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await self.loadFiles(true);
|
||||
self.showTreeMenu();
|
||||
});
|
||||
this._promptBtnEl = loadBtn;
|
||||
topContainer.appendChild(loadBtn);
|
||||
|
||||
this._promptBtnEl = btn;
|
||||
btnContainer.appendChild(btn);
|
||||
const topWidget = this.addDOMWidget("rs_spd_btn", "custom", topContainer);
|
||||
topWidget.computeSize = () => [this.width || 200, 58];
|
||||
|
||||
const domWidget = this.addDOMWidget("rs_spd_btn", "custom", btnContainer);
|
||||
domWidget.computeSize = () => [this.width || 200, 50];
|
||||
const userHeader = document.createElement("div");
|
||||
userHeader.style.cssText =
|
||||
"display:flex;align-items:center;justify-content:center;" +
|
||||
"width:100%;box-sizing:border-box;padding:6px 2px 0 2px;";
|
||||
|
||||
const userLabel = document.createElement("div");
|
||||
userLabel.textContent = "USER PROMPT";
|
||||
userLabel.style.cssText =
|
||||
"font-size:10px;font-weight:bold;letter-spacing:1px;" +
|
||||
"color:#888;text-transform:uppercase;user-select:none;" +
|
||||
"line-height:12px;text-align:center;";
|
||||
userHeader.appendChild(userLabel);
|
||||
|
||||
const userHeaderWidget = this.addDOMWidget(
|
||||
"rs_spd_user_header", "custom", userHeader
|
||||
);
|
||||
userHeaderWidget.computeSize = () => [this.width || 200, 22];
|
||||
|
||||
const sysPromptWidget = this._getWidget("system_prompt");
|
||||
const promptWidget = this._getWidget("prompt");
|
||||
|
||||
if (sysPromptWidget && promptWidget) {
|
||||
this.widgets = [
|
||||
sysHeaderWidget,
|
||||
sysPromptWidget,
|
||||
topWidget,
|
||||
userHeaderWidget,
|
||||
promptWidget
|
||||
];
|
||||
}
|
||||
|
||||
if (sysPromptWidget) {
|
||||
if (sysPromptWidget.element) {
|
||||
sysPromptWidget.element.addEventListener("input", () => {
|
||||
self._updateBtnLabel();
|
||||
});
|
||||
}
|
||||
const origCallback = sysPromptWidget.callback;
|
||||
sysPromptWidget.callback = (...args) => {
|
||||
const r = typeof origCallback === "function"
|
||||
? origCallback.apply(sysPromptWidget, args)
|
||||
: undefined;
|
||||
self._updateBtnLabel();
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
const origOnResize = this.onResize;
|
||||
this.onResize = function (size) {
|
||||
if (size[0] < 320) size[0] = 320;
|
||||
const minH = self.computeSize()[1];
|
||||
if (size[0] < 360) size[0] = 360;
|
||||
const minH = self.computeSize()[1] + 8;
|
||||
if (size[1] < minH) size[1] = minH;
|
||||
return origOnResize
|
||||
? origOnResize.apply(this, arguments)
|
||||
@@ -211,27 +480,28 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
const startH = this.computeSize()[1] + 8;
|
||||
this.setSize([Math.max(this.size[0], 320), startH]);
|
||||
this.setSize([Math.max(this.size[0], 360), startH]);
|
||||
|
||||
this.loadFiles(false).then(() => {
|
||||
if (!this.selectedFile && this.filesCache.length) {
|
||||
const pf = this._getWidget("prompt_file");
|
||||
const sel = this._getWidget("selected_file");
|
||||
const candidate = (sel?.value || pf?.value || "").trim();
|
||||
if (candidate && this.filesCache.includes(candidate)) {
|
||||
this.selectedFile = candidate;
|
||||
} else if (this.filesCache.length && !this._getSystemPrompt()) {
|
||||
this.selectedFile = this.filesCache[0];
|
||||
}
|
||||
const pf = this.widgets?.find((w) => w.name === "prompt_file");
|
||||
if (pf && !pf.value && this.selectedFile) {
|
||||
pf.value = this.selectedFile;
|
||||
} else {
|
||||
this.selectedFile = candidate;
|
||||
}
|
||||
this._syncSelected();
|
||||
this._updateBtnLabel();
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const padding = 8;
|
||||
const minHeight = this.computeSize()[1] + padding;
|
||||
const minHeight = this.computeSize()[1] + 8;
|
||||
if (this.size[1] < minHeight) {
|
||||
this.size[1] = minHeight;
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
@@ -239,24 +509,33 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
nodeType.prototype.onConfigure = function (info) {
|
||||
const self = this;
|
||||
|
||||
let saved = "";
|
||||
if (info?.properties?.selected_file) {
|
||||
saved = info.properties.selected_file;
|
||||
} else if (this.widgets) {
|
||||
const selWidget = this.widgets.find((w) => w.name === "selected_file");
|
||||
if (selWidget?.value) saved = selWidget.value;
|
||||
} else {
|
||||
const sel = this._getWidget("selected_file");
|
||||
if (sel?.value) saved = sel.value;
|
||||
}
|
||||
this.selectedFile = saved || "";
|
||||
|
||||
const self = this;
|
||||
this.loadFiles(false).then(() => {
|
||||
if (saved && self.filesCache.includes(saved)) {
|
||||
self.selectedFile = saved;
|
||||
} else if (self.filesCache.length) {
|
||||
self.selectedFile = self.filesCache[0];
|
||||
} else {
|
||||
self.selectedFile = "";
|
||||
const currentText = this._getSystemPrompt();
|
||||
|
||||
this.loadFiles(false).then(async () => {
|
||||
if (!self.selectedFile) {
|
||||
self.lastLoadedContent = currentText;
|
||||
self._syncSelected();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const disk = await self.fetchPromptContent(self.selectedFile);
|
||||
self.lastLoadedContent = disk;
|
||||
} catch (err) {
|
||||
self.lastLoadedContent = "\u0000__none__\u0000";
|
||||
}
|
||||
self._syncSelected();
|
||||
self._updateBtnLabel();
|
||||
});
|
||||
|
||||
return origOnConfigure
|
||||
@@ -265,13 +544,40 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
const existing = document.getElementById(MENU_ID);
|
||||
if (existing) existing.remove();
|
||||
const menu = document.getElementById(MENU_ID);
|
||||
if (menu) menu.remove();
|
||||
const modal = document.getElementById(SAVE_MODAL_ID);
|
||||
if (modal) modal.remove();
|
||||
const confirmModal = document.getElementById(CONFIRM_MODAL_ID);
|
||||
if (confirmModal) confirmModal.remove();
|
||||
return origOnRemoved
|
||||
? origOnRemoved.apply(this, arguments)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
nodeType.prototype.reloadFromFile = async function () {
|
||||
if (!this.selectedFile) {
|
||||
toast("No file selected", "error", this);
|
||||
return;
|
||||
}
|
||||
if (this._isDirty()) {
|
||||
const ok = await showConfirm(
|
||||
"Discard unsaved changes and reload from file?",
|
||||
this
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
const content = await this.fetchPromptContent(this.selectedFile);
|
||||
this._setSystemPrompt(content);
|
||||
this.lastLoadedContent = content;
|
||||
this._updateBtnLabel();
|
||||
toast(`Reloaded: ${this.selectedFile}`, "ok", this);
|
||||
} catch (err) {
|
||||
toast(`Reload failed: ${err.message}`, "error", this);
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.showTreeMenu = function () {
|
||||
const self = this;
|
||||
const old = document.getElementById(MENU_ID);
|
||||
@@ -338,10 +644,18 @@ app.registerExtension({
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
|
||||
const pick = (path) => {
|
||||
self.selectedFile = path;
|
||||
self._syncSelected();
|
||||
const pick = async (path) => {
|
||||
closeMenu();
|
||||
try {
|
||||
const content = await self.fetchPromptContent(path);
|
||||
self.selectedFile = path;
|
||||
self._setSystemPrompt(content);
|
||||
self.lastLoadedContent = content;
|
||||
self._syncSelected();
|
||||
self._updateBtnLabel();
|
||||
} catch (err) {
|
||||
toast(`Failed to load: ${err.message}`, "error", self);
|
||||
}
|
||||
};
|
||||
|
||||
const render = (filter = "") => {
|
||||
@@ -388,7 +702,7 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
renderTree("", self.treeCache, 0, lc, expanded, self, pick);
|
||||
renderTree("", self.treeCache, 0, lc, expanded, self, (path) => pick(path));
|
||||
};
|
||||
|
||||
render("");
|
||||
@@ -438,7 +752,7 @@ app.registerExtension({
|
||||
const after = self.filesCache.length;
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.textContent = "🔄 Refresh";
|
||||
showToast(`Reloaded: ${after} file(s) (was ${before})`, "ok", self);
|
||||
toast(`Reloaded: ${after} file(s) (was ${before})`, "ok", self);
|
||||
render(si.value || "");
|
||||
});
|
||||
|
||||
@@ -454,9 +768,155 @@ app.registerExtension({
|
||||
si.focus();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
nodeType.prototype.openSaveModal = function () {
|
||||
const self = this;
|
||||
const old = document.getElementById(SAVE_MODAL_ID);
|
||||
if (old) old.remove();
|
||||
|
||||
const currentText = this._getSystemPrompt();
|
||||
const defaultName = this.selectedFile || "";
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = SAVE_MODAL_ID;
|
||||
overlay.style.cssText =
|
||||
"position:fixed;inset:0;background:rgba(0,0,0,0.55);" +
|
||||
"z-index:100002;display:flex;align-items:center;" +
|
||||
"justify-content:center;";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.style.cssText =
|
||||
"background:#1f1f1f;border:1px solid #444;border-radius:8px;" +
|
||||
"width:440px;padding:16px;display:flex;flex-direction:column;" +
|
||||
"gap:12px;box-shadow:0 8px 30px rgba(0,0,0,0.8);" +
|
||||
"font-family:sans-serif;color:#ddd;";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Save System Prompt";
|
||||
title.style.cssText = "font-size:15px;font-weight:bold;color:#fff;";
|
||||
|
||||
const hint = document.createElement("div");
|
||||
hint.textContent =
|
||||
"Enter a file name (relative to system_prompts/). " +
|
||||
"Subfolders are allowed, .txt is added automatically.";
|
||||
hint.style.cssText = "font-size:12px;color:#888;";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = defaultName;
|
||||
input.placeholder = "prompts/new_prompt.txt";
|
||||
input.style.cssText =
|
||||
"width:100%;padding:8px 10px;background:#111;color:#fff;" +
|
||||
"border:1px solid #555;border-radius:4px;font-size:13px;" +
|
||||
"box-sizing:border-box;outline:none;";
|
||||
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.style.cssText =
|
||||
"padding:6px 16px;background:#2a2a2a;color:#ddd;" +
|
||||
"border:1px solid #555;border-radius:4px;cursor:pointer;font-size:13px;";
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.textContent = "Save";
|
||||
saveBtn.style.cssText =
|
||||
"padding:6px 16px;background:#1a3a1a;color:#aaffaa;" +
|
||||
"border:1px solid #4CAF50;border-radius:4px;cursor:pointer;" +
|
||||
"font-size:13px;font-weight:bold;";
|
||||
|
||||
btnRow.append(cancelBtn, saveBtn);
|
||||
modal.append(title, hint, input, btnRow);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const close = () => {
|
||||
overlay.remove();
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
};
|
||||
|
||||
const normalizeName = (raw) => {
|
||||
let name = (raw || "").trim().replace(/\\/g, "/");
|
||||
if (!name) return "";
|
||||
if (!name.toLowerCase().endsWith(".txt")) name += ".txt";
|
||||
return name;
|
||||
};
|
||||
|
||||
const doSave = async () => {
|
||||
const name = normalizeName(input.value);
|
||||
if (!name) {
|
||||
toast("Enter a file name", "error", self);
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const nameChanged = name !== (self.selectedFile || "");
|
||||
if (nameChanged && self.filesCache.includes(name)) {
|
||||
const ok = await showConfirm(
|
||||
`File "${name}" already exists. Overwrite?`,
|
||||
self
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = "⏳ ...";
|
||||
try {
|
||||
const data = await self.savePromptContent(name, currentText);
|
||||
self.selectedFile = data.path;
|
||||
self.lastLoadedContent = currentText;
|
||||
await self.loadFiles(false);
|
||||
self._syncSelected();
|
||||
self._updateBtnLabel();
|
||||
toast(
|
||||
data.created
|
||||
? `Saved as: ${data.path}`
|
||||
: `Saved: ${data.path}`,
|
||||
"ok",
|
||||
self
|
||||
);
|
||||
close();
|
||||
} catch (err) {
|
||||
toast(`Save failed: ${err.message}`, "error", self);
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = "Save";
|
||||
}
|
||||
};
|
||||
|
||||
const onKey = (ev) => {
|
||||
if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
close();
|
||||
} else if (ev.key === "Enter" && document.activeElement === input) {
|
||||
ev.preventDefault();
|
||||
doSave();
|
||||
}
|
||||
};
|
||||
|
||||
saveBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
doSave();
|
||||
});
|
||||
cancelBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
});
|
||||
overlay.addEventListener("pointerdown", (e) => {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function renderTree(prefix, tree, level, container, expanded, self, onPick) {
|
||||
const keys = Object.keys(tree).sort((a, b) => {
|
||||
const af = tree[a] !== null;
|
||||
@@ -491,19 +951,95 @@ function renderTree(prefix, tree, level, container, expanded, self, onPick) {
|
||||
}
|
||||
} else {
|
||||
const active = self.selectedFile === full;
|
||||
const fi = document.createElement("div");
|
||||
fi.textContent = (active ? "✓ " : " ") + "📄 " + name;
|
||||
fi.style.cssText =
|
||||
`padding:8px 12px;cursor:pointer;font-size:12px;` +
|
||||
`padding-left:${12 + level * 16}px;` +
|
||||
`color:${active ? "#4CAF50" : "#ddd"};`;
|
||||
fi.onmouseenter = () => (fi.style.background = "#333");
|
||||
fi.onmouseleave = () => (fi.style.background = "transparent");
|
||||
fi.onclick = (e) => {
|
||||
const row = document.createElement("div");
|
||||
row.style.cssText =
|
||||
`display:flex;align-items:center;justify-content:space-between;` +
|
||||
`padding:8px 12px;padding-left:${12 + level * 16}px;` +
|
||||
`cursor:pointer;border-bottom:1px solid #2a2a2a;` +
|
||||
`color:${active ? "#4CAF50" : "#ddd"};font-size:12px;`;
|
||||
|
||||
row.onmouseenter = () => (row.style.background = "#333");
|
||||
row.onmouseleave = () => (row.style.background = "transparent");
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = (active ? "✓ " : " ") + "📄 " + name;
|
||||
nameSpan.style.cssText = "flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
||||
|
||||
const delBtn = document.createElement("div");
|
||||
delBtn.textContent = "❌";
|
||||
delBtn.title = "Delete file";
|
||||
delBtn.style.cssText =
|
||||
"margin-left:8px;padding:0 4px;opacity:0.6;cursor:pointer;" +
|
||||
"transition:opacity 0.2s;font-size:14px;line-height:1;";
|
||||
|
||||
delBtn.onmouseenter = (e) => {
|
||||
e.stopPropagation();
|
||||
delBtn.style.opacity = "1";
|
||||
delBtn.style.color = "#f44336";
|
||||
};
|
||||
delBtn.onmouseleave = (e) => {
|
||||
e.stopPropagation();
|
||||
delBtn.style.opacity = "0.6";
|
||||
delBtn.style.color = "inherit";
|
||||
};
|
||||
|
||||
delBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
self.handleDeleteFile(full, () => {
|
||||
const input = container.closest(`#${MENU_ID}`)?.querySelector("input");
|
||||
const filter = input ? input.value : "";
|
||||
|
||||
container.innerHTML = "";
|
||||
if (filter && filter.trim()) {
|
||||
const lf = filter.trim().toLowerCase();
|
||||
const all = collectPaths(self.treeCache)
|
||||
.filter((p) => !p.isFolder && p.path.toLowerCase().includes(lf))
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
if (!all.length) {
|
||||
const nr = document.createElement("div");
|
||||
nr.textContent = `No results for "${filter}"`;
|
||||
nr.style.cssText = "padding:15px;color:#777;text-align:center;font-style:italic;";
|
||||
container.appendChild(nr);
|
||||
} else {
|
||||
all.forEach((it) => {
|
||||
const isAct = self.selectedFile === it.path;
|
||||
const r = document.createElement("div");
|
||||
r.style.cssText = `display:flex;align-items:center;justify-content:space-between;padding:8px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid #2a2a2a;color:${isAct ? "#4CAF50" : "#ddd"};`;
|
||||
r.onmouseenter = () => (r.style.background = "#333");
|
||||
r.onmouseleave = () => (r.style.background = "transparent");
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.textContent = (isAct ? "✓ " : " ") + it.path;
|
||||
span.style.cssText = "flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
||||
|
||||
const d = document.createElement("div");
|
||||
d.textContent = "❌";
|
||||
d.title = "Delete file";
|
||||
d.style.cssText = "margin-left:8px;padding:0 4px;opacity:0.6;cursor:pointer;transition:opacity 0.2s;font-size:14px;line-height:1;";
|
||||
d.onmouseenter = (e) => { e.stopPropagation(); d.style.opacity = "1"; d.style.color = "#f44336"; };
|
||||
d.onmouseleave = (e) => { e.stopPropagation(); d.style.opacity = "0.6"; d.style.color = "inherit"; };
|
||||
d.onclick = (e) => { e.stopPropagation(); self.handleDeleteFile(it.path, () => { /* nested? rare */ }); };
|
||||
|
||||
r.appendChild(span);
|
||||
r.appendChild(d);
|
||||
r.onclick = (e) => { e.stopPropagation(); onPick(it.path); };
|
||||
container.appendChild(r);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
renderTree("", self.treeCache, 0, container, expanded, self, onPick);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(delBtn);
|
||||
row.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
onPick(full);
|
||||
};
|
||||
container.appendChild(fi);
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user