diff --git a/web/rayko_system_prompt.js b/web/rayko_system_prompt.js index 12a1ee3..10ff6e7 100644 --- a/web/rayko_system_prompt.js +++ b/web/rayko_system_prompt.js @@ -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); } } -} \ No newline at end of file +}