19 Commits
Author SHA1 Message Date
Rayko db28fc5a7e Update README.md 2026-10-21 13:52:24 +03:00
Rayko 4ea7928657 Update pyproject.toml 2026-10-21 13:51:38 +03:00
Rayko c04fa2fb57 Update package.json 2026-10-21 13:51:23 +03:00
Rayko 07ba5d2bb6 Update __init__.py 2026-10-21 13:51:06 +03:00
Rayko a9cfa35bd5 Update Rayko_Models_Loader_Pro.py 2026-10-21 13:50:33 +03:00
Rayko f8b75b2a29 Update Rayko_Models_Loader.py 2026-10-21 13:50:10 +03:00
Raykosan 952bdb34c7 clip_type - fibo2 has been added to the RS Models Loader and RS Models Loader Pro nodes 2026-10-21 13:45:12 +03:00
Raykosan 9afdeb6674 Add Rayko_System_Prompt.py 2026-10-20 01:10:26 +03:00
Raykosan 0fd1348c43 Added the RS Load System Prompt node 2026-10-20 01:05:40 +03:00
Raykosan 8bc15a1716 Added the RS Load System Prompt node 2026-10-20 00:54:52 +03:00
Rayko 9914c3ded0 Update .gitignore 2026-10-20 00:53:40 +03:00
Rayko c7d208714c Update pyproject.toml 2026-10-20 00:50:28 +03:00
Rayko f26c48ea5b Update package.json 2026-10-20 00:50:12 +03:00
Rayko b1d52082da Update __init__.py 2026-10-20 00:48:39 +03:00
Rayko 2700fd3695 Update README.md 2026-10-20 00:48:06 +03:00
Raykosan 518e818c16 clip_type - moss_image has been added to the RS Models Loader and RS Models Loader Pro nodes. 2026-10-17 19:51:47 +03:00
Rayko c391de705d Update pyproject.toml 2026-10-16 03:57:36 +03:00
Rayko 0eb0c62412 Update package.json 2026-10-16 03:57:22 +03:00
Rayko 03d41d51a7 Update __init__.py 2026-10-16 03:57:08 +03:00
10 changed files with 713 additions and 11 deletions
+1
View File
@@ -9,6 +9,7 @@ presets/
prompts/
rayko_lora_data/
styles/
system_prompts/
ComfyUI-Custom-Node-Manager/
python
+46 -4
View File
@@ -48,6 +48,9 @@ git clone https://github.com/Raykosan/ComfyUI_RaykoStudio.git
- v0.49.1 - New clip_types have been added to the RS Models Loader and RS Models Loader Pro nodes: stable_audio and yue2
- 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.1 - Added fibo2
</details>
@@ -88,9 +91,9 @@ If the input video has no audio track, the `audio_only` output returns a silent
**The fast path looks for ffmpeg in this order**:
RS_FFMPEG_PATH environment variable — set this to point at a specific binary.
System PATH — scanned manually (with .exe/.cmd/.bat suffixes on Windows).
imageio-ffmpeg — if the package is installed, its bundled binary is used.
If none of the above yields a working binary, the node silently uses the slow path.
@@ -98,9 +101,9 @@ If none of the above yields a working binary, the node silently uses the slow pa
**To install a system binary**:
Windows: winget install Gyan.FFmpeg or download from ffmpeg.org and add to PATH.
Linux: sudo apt install ffmpeg
macOS: brew install ffmpeg
**Alternatively, in the ComfyUI venv**:
@@ -326,6 +329,45 @@ In any case, you can always paste the path using the keyboard shortcut Ctrl+V.
| `number_of_files` | INT | The total number of files found matching the filter in the directory. |
</details>
<details>
<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.**
<img width="1012" height="588" alt="Screenshot_2" src="https://github.com/user-attachments/assets/a6a97e78-4212-4a43-9025-475d21aed1a1" />
### 🔥 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.
**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.
### 🪛 Usage
- Add the node 🦊 RS Load System Prompt from the 🦊 RaykoStudio category.
- Click LOAD SYSTEM PROMPT.
- 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).
- Connect the outputs:
- system_prompt — contents of the selected .txt file.
- prompt — text from the textarea.
### 📝 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.
### 💡 Note
If the dropdown is already open and you add a file from outside ComfyUI, click 🔄 Refresh inside the dropdown to re-scan.
</details>
<details>
<summary>🦊 RS Image-Prompt</summary>
+2 -2
View File
@@ -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", "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"}
+2 -2
View File
@@ -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", "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"}
+130
View File
@@ -0,0 +1,130 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Raykosan (RaykoStudio)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import server
from aiohttp import web
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
PROMPT_DIR = os.path.join(NODE_DIR, "system_prompts")
def ensure_prompt_dir():
try:
os.makedirs(PROMPT_DIR, exist_ok=True)
return os.path.isdir(PROMPT_DIR)
except OSError as error:
print(f"[RSLoadSystemPrompt] Cannot create prompt dir '{PROMPT_DIR}': {error}")
return False
def _scan_prompts():
if not ensure_prompt_dir():
return []
files = []
for root, _, filenames in os.walk(PROMPT_DIR):
for filename in filenames:
if filename.lower().endswith(".txt"):
full_path = os.path.join(root, filename)
relative_path = os.path.relpath(full_path, PROMPT_DIR)
files.append(relative_path.replace(os.sep, "/"))
files.sort(key=str.casefold)
return files
def resolve_prompt_path(prompt_file):
if not prompt_file:
return None
try:
file_path = os.path.abspath(
os.path.join(PROMPT_DIR, prompt_file.replace("/", os.sep))
)
except (OSError, ValueError):
return None
prompt_root = os.path.abspath(PROMPT_DIR)
if not file_path.startswith(prompt_root + os.sep):
return None
return file_path
@server.PromptServer.instance.routes.get("/rs_system_prompt_dropdown/get_prompts")
async def get_prompts_handler(request):
return web.json_response({"files": _scan_prompts()})
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 ""}),
"prompt": (
"STRING",
{
"default": "",
"multiline": True,
"placeholder": "Additional prompt text (combined with system prompt)",
},
),
},
"hidden": {
"selected_file": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("system_prompt", "prompt")
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 "")
NODE_CLASS_MAPPINGS = {
"RSLoadSystemPrompt": RSLoadSystemPrompt,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RSLoadSystemPrompt": "🦊 RS Load System Prompt",
}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.50.1"
__version__ = "0.51.1"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.50.1",
"version": "0.51.1",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
@@ -44,6 +44,7 @@
"Rayko_Save_Image_Pair.py"
"Rayko_Spline_Mask.py"
"Rayko_Styles_Loader.py"
"Rayko_System_Prompt.py"
"Rayko_Text_Overlay_Pro.py"
"Rayko_Tile_Adjustments.py"
"Rayko_Tile_Assemble.py"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.50.1"
version = "0.51.1"
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"]
+19
View File
@@ -0,0 +1,19 @@
You are an expert prompt engineer for text-to-image models. Your task is to expand the user's prompt into a highly effective image-generation prompt.
Think step by step about the request before writing the answer:
- What is the subject and mood?
- What visual styles, mediums, and lighting options would fit? Consider two or three alternatives and pick the one that best serves the caption.
- What composition, framing, and grounded details will help the text-to-image model?
Then output a single expanded prompt paragraph.
Follow these rules strictly:
1. Faithfulness First: Preserve all original subjects, actions, colors, and spatial relationships. Do not add new objects, props, characters, or animals unless the user clearly implies them.
2. Practical T2I Structure: Write a prompt that a text-to-image model can parse cleanly. Group subjects with their own attributes and actions. Use grounded phrasing for poses, interactions, and spatial layout.
3. Style Planning Stays Internal: Use your internal reasoning to choose style, medium, framing, and lighting. Do not emit planning tags or wrappers in the visible answer body.
4. Text Rendering: If the user requests visible text, quotes, labels, or typography, specify the exact text clearly and wrap requested words in quotes.
5. Avoid Over-Specification: Do not invent highly specific clothing, colors, materials, or scene details unless the input supports them.
6. Structure: Write one cohesive paragraph after the thinking block. No bullets, JSON, or markdown.
7. Respect Existing Detail: If the user's prompt is already detailed, lightly polish and finalize rather than heavily expanding — preserve their phrasing and direction.
8. Respect the Human Form: Treat depictions of people with dignity. Assume clothing covers genitals and intimate anatomy.
9. Preserve User Medium: When the user explicitly requests a medium (e.g. "photo of", "photograph of", "illustration of", "painting of", "sketch of", "3D render of"), honor it. Do not pivot to a different medium to avoid difficulty — match the user's stated intent.
+509
View File
@@ -0,0 +1,509 @@
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 existing = document.querySelector(".rs-spd-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.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;
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 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)";
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateY(-20px)";
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function buildTree(list) {
const tree = {};
for (const item of list) {
if (!item) continue;
const parts = item.replace(/\\/g, "/").split("/");
let cur = tree;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const last = i === parts.length - 1;
if (!cur[part]) cur[part] = last ? null : {};
if (!last) cur = cur[part];
}
}
return tree;
}
function collectPaths(tree, prefix = "") {
const out = [];
for (const name of Object.keys(tree)) {
const sub = tree[name];
const full = prefix ? `${prefix}/${name}` : name;
const isFolder = sub !== null;
out.push({ path: full, isFolder });
if (isFolder) out.push(...collectPaths(sub, full));
}
return out;
}
app.registerExtension({
name: "RaykoStudio.SystemPromptDropdown",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
const origOnConfigure = nodeType.prototype.onConfigure;
const origOnRemoved = nodeType.prototype.onRemoved;
nodeType.prototype._btnLabel = function () {
return this.selectedFile
? `📄 ${this.selectedFile}`
: "📄 Select system prompt…";
};
nodeType.prototype._updateBtnLabel = function () {
if (this._promptBtnEl) {
this._promptBtnEl.textContent = this._btnLabel();
this._promptBtnEl.title = this.selectedFile || "";
}
};
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" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
this.filesCache = Array.isArray(data.files) ? data.files : [];
this.treeCache = buildTree(this.filesCache);
} catch (err) {
console.error("[RSLoadSystemPrompt] loadFiles error:", err);
this.filesCache = [];
this.treeCache = {};
if (showToastOnError) {
showToast(`Failed to load prompts: ${err.message}`, "error", this);
}
}
};
nodeType.prototype.onNodeCreated = function () {
const result = origOnNodeCreated
? origOnNodeCreated.apply(this, arguments)
: undefined;
const self = this;
this.selectedFile = "";
this.filesCache = [];
this.treeCache = {};
this._promptBtnEl = 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];
}
const selWidget = this.widgets?.find((w) => w.name === "selected_file");
if (selWidget) {
selWidget.hidden = true;
if (selWidget.element) selWidget.element.style.display = "none";
}
if (this.properties?.selected_file) {
this.selectedFile = this.properties.selected_file;
} else if (comboWidget?.value) {
this.selectedFile = comboWidget.value;
}
const btnContainer = document.createElement("div");
btnContainer.style.cssText =
"display:flex;flex-direction:column;gap:2px;" +
"width:100%;box-sizing:border-box;";
const label = document.createElement("div");
label.textContent = "LOAD SYSTEM PROMPT";
label.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);
const btn = document.createElement("button");
btn.textContent = this._btnLabel();
btn.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) => {
e.preventDefault();
e.stopPropagation();
await self.loadFiles(true);
self.showTreeMenu();
});
this._promptBtnEl = btn;
btnContainer.appendChild(btn);
const domWidget = this.addDOMWidget("rs_spd_btn", "custom", btnContainer);
domWidget.computeSize = () => [this.width || 200, 50];
const origOnResize = this.onResize;
this.onResize = function (size) {
if (size[0] < 320) size[0] = 320;
const minH = self.computeSize()[1];
if (size[1] < minH) size[1] = minH;
return origOnResize
? origOnResize.apply(this, arguments)
: undefined;
};
const startH = this.computeSize()[1] + 8;
this.setSize([Math.max(this.size[0], 320), startH]);
this.loadFiles(false).then(() => {
if (!this.selectedFile && this.filesCache.length) {
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;
}
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;
if (this.size[1] < minHeight) {
this.size[1] = minHeight;
this.graph?.setDirtyCanvas(true, true);
}
};
nodeType.prototype.onConfigure = function (info) {
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;
}
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 = "";
}
self._syncSelected();
});
return origOnConfigure
? origOnConfigure.apply(this, arguments)
: undefined;
};
nodeType.prototype.onRemoved = function () {
const existing = document.getElementById(MENU_ID);
if (existing) existing.remove();
return origOnRemoved
? origOnRemoved.apply(this, arguments)
: undefined;
};
nodeType.prototype.showTreeMenu = function () {
const self = this;
const old = document.getElementById(MENU_ID);
if (old) old.remove();
const mw = 460;
const mh = 600;
const expanded = {};
const menu = document.createElement("div");
menu.id = MENU_ID;
menu.style.cssText =
`position:fixed;background:#1a1a1a;border:1px solid #444;` +
`border-radius:6px;height:${mh}px;width:${mw}px;` +
`overflow-y:auto;overflow-x:hidden;z-index:10000;` +
`box-shadow:0 4px 20px rgba(0,0,0,0.8);display:flex;` +
`flex-direction:column;`;
const hc = document.createElement("div");
hc.style.cssText =
"padding:10px;background:#252525;border-bottom:1px solid #333;" +
"display:flex;flex-direction:column;gap:8px;flex-shrink:0;";
const titleRow = document.createElement("div");
titleRow.style.cssText =
"display:flex;justify-content:space-between;align-items:center;";
const title = document.createElement("div");
title.textContent = "System Prompt";
title.style.cssText = "color:#fff;font-weight:bold;font-size:14px;";
const refreshBtn = document.createElement("button");
refreshBtn.textContent = "🔄 Refresh";
refreshBtn.title = "Rescan system_prompts folder";
refreshBtn.style.cssText =
"padding:4px 10px;background:#2a2a2a;color:#ddd;" +
"border:1px solid #555;border-radius:4px;cursor:pointer;" +
"font-size:12px;";
refreshBtn.onmouseenter = () => (refreshBtn.style.background = "#3a3a3a");
refreshBtn.onmouseleave = () => (refreshBtn.style.background = "#2a2a2a");
titleRow.append(title, refreshBtn);
const si = document.createElement("input");
si.type = "text";
si.placeholder = "Type to search...";
si.style.cssText =
"width:100%;padding:8px;background:#333;border:1px solid #555;" +
"color:#fff;border-radius:4px;outline:none;font-size:13px;" +
"box-sizing:border-box;";
hc.append(titleRow, si);
menu.appendChild(hc);
const lc = document.createElement("div");
lc.style.cssText = "padding:5px 0;overflow-y:auto;flex-grow:1;";
menu.appendChild(lc);
let rafId = null;
const closeMenu = () => {
if (menu.parentNode) menu.remove();
document.removeEventListener("pointerdown", onOutside, true);
document.removeEventListener("keydown", onKey, true);
if (rafId) cancelAnimationFrame(rafId);
};
const pick = (path) => {
self.selectedFile = path;
self._syncSelected();
closeMenu();
};
const render = (filter = "") => {
lc.innerHTML = "";
const lf = filter.trim().toLowerCase();
if (!self.filesCache.length) {
const em = document.createElement("div");
em.textContent = "No .txt files in system_prompts/";
em.style.cssText =
"padding:20px;color:#f44336;text-align:center;font-style:italic;";
lc.appendChild(em);
return;
}
if (lf.length > 0) {
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;";
lc.appendChild(nr);
return;
}
all.forEach((it) => {
const active = self.selectedFile === it.path;
const el = document.createElement("div");
el.textContent = (active ? "✓ " : " ") + it.path;
el.style.cssText =
`padding:8px 12px;cursor:pointer;font-size:12px;` +
`border-bottom:1px solid #2a2a2a;` +
`color:${active ? "#4CAF50" : "#ddd"};`;
el.onmouseenter = () => (el.style.background = "#333");
el.onmouseleave = () => (el.style.background = "transparent");
el.onclick = (e) => {
e.stopPropagation();
pick(it.path);
};
lc.appendChild(el);
});
return;
}
renderTree("", self.treeCache, 0, lc, expanded, self, pick);
};
render("");
document.body.appendChild(menu);
const anchor = self._promptBtnEl;
const positionMenu = () => {
if (!anchor || !menu.parentNode || !anchor.isConnected) return false;
const r = anchor.getBoundingClientRect();
let left = r.left;
let top = r.bottom + 4;
if (left + mw > window.innerWidth - 10) left = window.innerWidth - mw - 10;
if (left < 10) left = 10;
if (top + mh > window.innerHeight - 10) top = r.top - mh - 4;
if (top < 10) top = 10;
menu.style.left = left + "px";
menu.style.top = top + "px";
return true;
};
positionMenu();
rafId = requestAnimationFrame(function tick() {
if (!menu.parentNode) {
rafId = null;
return;
}
if (!positionMenu()) {
closeMenu();
return;
}
rafId = requestAnimationFrame(tick);
});
let tid = null;
si.addEventListener("input", (e) => {
if (tid) clearTimeout(tid);
tid = setTimeout(() => render(e.target.value), 50);
});
refreshBtn.addEventListener("click", async (e) => {
e.stopPropagation();
refreshBtn.disabled = true;
refreshBtn.textContent = "⏳ ...";
const before = self.filesCache.length;
await self.loadFiles(true);
const after = self.filesCache.length;
refreshBtn.disabled = false;
refreshBtn.textContent = "🔄 Refresh";
showToast(`Reloaded: ${after} file(s) (was ${before})`, "ok", self);
render(si.value || "");
});
const onKey = (ev) => {
if (ev.key === "Escape") closeMenu();
};
const onOutside = (ev) => {
if (!menu.contains(ev.target)) closeMenu();
};
setTimeout(() => {
document.addEventListener("pointerdown", onOutside, true);
document.addEventListener("keydown", onKey, true);
si.focus();
}, 50);
};
},
});
function renderTree(prefix, tree, level, container, expanded, self, onPick) {
const keys = Object.keys(tree).sort((a, b) => {
const af = tree[a] !== null;
const bf = tree[b] !== null;
if (af && !bf) return -1;
if (!af && bf) return 1;
return a.toLowerCase().localeCompare(b.toLowerCase());
});
for (const name of keys) {
const sub = tree[name];
const isFolder = sub !== null;
const full = prefix ? `${prefix}/${name}` : name;
if (isFolder) {
const fh = document.createElement("div");
fh.style.cssText =
`padding:8px 12px;cursor:pointer;color:#ffd700;font-size:13px;` +
`background:#252525;display:flex;align-items:center;` +
`padding-left:${12 + level * 16}px;`;
fh.innerHTML =
`<span style="margin-right:8px;">${expanded[full] ? "▼" : "▶"}</span>📁 ${name}`;
fh.onclick = (e) => {
e.stopPropagation();
expanded[full] = !expanded[full];
container.innerHTML = "";
renderTree("", self.treeCache, 0, container, expanded, self, onPick);
};
container.appendChild(fh);
if (expanded[full]) {
renderTree(full, sub, level + 1, 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) => {
e.stopPropagation();
onPick(full);
};
container.appendChild(fi);
}
}
}