import { app } from "../../../scripts/app.js"; import { PROMPT_LIBRARY } from "./promptLibrary.data.js"; // ── 适用节点 ────────────────────────────────────────────────────────────── const TARGET_NODES = ["K3Video", "K3MotionControl"]; const STYLE_ID = "o1key-plib-styles"; const DIALOG_ID = "o1key-plib-dialog"; // 记录每个节点最近聚焦的提示词框 widget 名 const lastFocused = new WeakMap(); let dialogRoot = null; let activeNode = null; // 单选分类的当前选择:{ [分类名]: 词条label };多选分类:Set const selected = {}; // 单选 const multiSelected = {}; // 多选 → Set const CSS = ` #o1key-plib-dialog{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.52);color:#ddd;font-family:inherit} #o1key-plib-modal{width:min(460px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px));display:flex;flex-direction:column;overflow:hidden;border:1px solid rgba(255,255,255,.12);border-radius:10px;background:var(--comfy-menu-bg,#1a1a1a);box-shadow:0 24px 80px rgba(0,0,0,.5)} #o1key-plib-header{height:50px;padding:0 16px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} #o1key-plib-title{font-size:15px;font-weight:700;color:#eee} .o1pl-icon-btn{width:30px;height:30px;border:1px solid rgba(255,255,255,.12);border-radius:6px;background:rgba(255,255,255,.04);color:#aaa;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s} .o1pl-icon-btn:hover{color:#eee;background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.2)} #o1pl-body{flex:1;min-height:0;overflow:auto;padding:14px 16px} #o1pl-body::-webkit-scrollbar{width:6px} #o1pl-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:3px} .o1pl-cat{margin-bottom:18px} .o1pl-cat-title{font-size:13px;font-weight:700;color:#cfcfcf;margin-bottom:10px;display:flex;align-items:center;gap:6px} .o1pl-cat-multi{font-size:11px;font-weight:400;color:#777} .o1pl-tags{display:flex;flex-wrap:wrap;gap:8px} .o1pl-tag{padding:7px 14px;border:1px solid rgba(255,255,255,.16);border-radius:18px;background:rgba(255,255,255,.03);color:#ccc;font-size:13px;cursor:pointer;transition:all .12s;font-family:inherit} .o1pl-tag:hover{background:rgba(255,255,255,.08);color:#fff} .o1pl-tag.active{border-color:#4ade80;color:#4ade80;background:rgba(74,222,128,.08)} #o1pl-footer{flex-shrink:0;border-top:1px solid rgba(255,255,255,.08);padding:10px 16px;display:flex;flex-direction:column;gap:8px} #o1pl-preview{font-size:12px;color:#9a9a9a;line-height:1.5;max-height:54px;overflow:auto;min-height:18px} #o1pl-preview::-webkit-scrollbar{width:5px} #o1pl-preview::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:3px} #o1pl-footer-row{display:flex;gap:8px;align-items:center} #o1pl-target{flex:1;font-size:11px;color:#777;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .o1pl-btn{padding:8px 18px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid transparent;font-family:inherit;font-weight:600} .o1pl-btn-ghost{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.14);color:#bbb} .o1pl-btn-ghost:hover{background:rgba(255,255,255,.1);color:#eee} .o1pl-btn-primary{background:#fff;color:#111} .o1pl-btn-primary:hover{background:#e6e6e6} `; function injectStyles() { if (document.getElementById(STYLE_ID)) return; const el = document.createElement("style"); el.id = STYLE_ID; el.textContent = CSS; document.head.appendChild(el); } function escapeHtml(v) { return String(v ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } function iconClose() { return ``; } // ── 提示词框定位 ────────────────────────────────────────────────────────── // 匹配任意「…提示词…」框,但排除「负向提示词 / negative」。 // 用包含匹配(而非前缀),以兼容 DynamicCombo 可能给子输入加的前缀名。 function isPromptWidget(w) { if (!w || typeof w.name !== "string") return false; const n = w.name; if (!n.includes("提示词") && !/prompt/i.test(n)) return false; if (n.includes("负向") || /negative/i.test(n)) return false; return true; } function listPromptWidgets(node) { return (node?.widgets || []).filter(isPromptWidget); } // 取目标提示词框:优先最近聚焦,否则首个「正向/主提示词」,再退首个可用 function resolveTargetWidget(node) { const widgets = listPromptWidgets(node); if (!widgets.length) return null; const remembered = lastFocused.get(node); if (remembered) { const hit = widgets.find(w => w.name === remembered); if (hit) return hit; } return widgets.find(w => w.name.includes("正向") || w.name === "提示词") || widgets[0]; } // 监听节点内提示词框聚焦,记录最近编辑的那个 function trackFocus(node) { for (const w of listPromptWidgets(node)) { const el = w.element || w.inputEl; if (!el || el.dataset?.o1plFocusBound) continue; el.dataset.o1plFocusBound = "1"; el.addEventListener("focus", () => lastFocused.set(node, w.name)); } } function setWidgetValue(node, widget, value) { widget.value = value; const el = widget.element || widget.inputEl; if (el && "value" in el) el.value = value; widget.callback?.(value); node.setDirtyCanvas?.(true, true); } // 智能逗号拼接:自动在已有文本后补「,」 function appendToPrompt(node, widget, addition) { const cur = String(widget.value || "").trim(); let next; if (!cur) { next = addition; } else { const sep = /[,,。.\s]$/.test(cur) ? "" : ","; next = cur + sep + addition; } setWidgetValue(node, widget, next); } // ── 选择状态 → 组装短句 ─────────────────────────────────────────────────── function buildAssembled() { const parts = []; for (const cat of PROMPT_LIBRARY) { if (cat.multi) { const set = multiSelected[cat.name]; if (!set || !set.size) continue; for (const tag of cat.tags) if (set.has(tag.label)) parts.push(tag.prompt); } else { const lbl = selected[cat.name]; if (!lbl) continue; const tag = cat.tags.find(t => t.label === lbl); if (tag) parts.push(tag.prompt); } } return parts.join(","); } function refreshPreview() { const el = dialogRoot?.querySelector("#o1pl-preview"); if (!el) return; const text = buildAssembled(); el.textContent = text || "点击词条组合提示词,预览将显示在这里…"; el.style.color = text ? "#cfcfcf" : "#666"; const tgt = dialogRoot?.querySelector("#o1pl-target"); if (tgt && activeNode) { const w = resolveTargetWidget(activeNode); tgt.textContent = w ? `→ 插入到「${w.name}」` : "(未找到提示词框)"; } } // ── 渲染:灵感词库 tab ──────────────────────────────────────────────────── function renderLibraryTab() { const body = dialogRoot.querySelector("#o1pl-body"); body.innerHTML = PROMPT_LIBRARY.map(cat => { const tags = cat.tags.map(tag => { let active; if (cat.multi) active = multiSelected[cat.name]?.has(tag.label); else active = selected[cat.name] === tag.label; return ``; }).join(""); const multiHint = cat.multi ? `(多选)` : ""; return `
${escapeHtml(cat.name)}${multiHint}
${tags}
`; }).join(""); body.querySelectorAll(".o1pl-tag").forEach(btn => { btn.addEventListener("click", () => toggleTag(btn.dataset.cat, btn.dataset.label)); }); } function toggleTag(catName, label) { const cat = PROMPT_LIBRARY.find(c => c.name === catName); if (!cat) return; if (cat.multi) { if (!multiSelected[catName]) multiSelected[catName] = new Set(); const set = multiSelected[catName]; set.has(label) ? set.delete(label) : set.add(label); } else { // 单选:再次点击同一词条则取消,否则替换 selected[catName] = selected[catName] === label ? null : label; } renderLibraryTab(); refreshPreview(); } // ── 插入到节点 ──────────────────────────────────────────────────────────── function insertText(text) { if (!text || !activeNode) return closePanel(); const widget = resolveTargetWidget(activeNode); if (!widget) { alert("未找到可注入的提示词框。"); return; } appendToPrompt(activeNode, widget, text); closePanel(); } function insertAssembled() { const text = buildAssembled(); if (!text) { closePanel(); return; } insertText(text); } function resetSelection() { for (const k of Object.keys(selected)) delete selected[k]; for (const k of Object.keys(multiSelected)) delete multiSelected[k]; } // ── 面板开关 ────────────────────────────────────────────────────────────── function closePanel() { document.removeEventListener("keydown", handleKeydown); dialogRoot?.remove(); dialogRoot = null; activeNode = null; } function handleKeydown(e) { if (e.key === "Escape") closePanel(); } function openPanel(node) { injectStyles(); if (dialogRoot) closePanel(); activeNode = node; resetSelection(); trackFocus(node); dialogRoot = document.createElement("div"); dialogRoot.id = DIALOG_ID; dialogRoot.innerHTML = ` `; document.body.appendChild(dialogRoot); dialogRoot.addEventListener("click", (e) => { if (e.target === dialogRoot) closePanel(); }); dialogRoot.querySelector("[data-close]").addEventListener("click", closePanel); dialogRoot.querySelector("[data-reset]").addEventListener("click", () => { resetSelection(); renderLibraryTab(); refreshPreview(); }); dialogRoot.querySelector("[data-insert]").addEventListener("click", insertAssembled); document.addEventListener("keydown", handleKeydown); renderLibraryTab(); refreshPreview(); } // ── 扩展注册:在目标节点上添加常驻「词库」按钮 widget ────────────────────── app.registerExtension({ name: "o1key.promptLibrary", beforeRegisterNodeDef(nodeType, nodeData) { if (!TARGET_NODES.includes(nodeData.name)) return; const origCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = origCreated?.apply(this, arguments); // 常驻按钮控件:点击打开词库面板 this.addWidget("button", "📖 提示词词库", null, () => openPanel(this), { serialize: false }); // 绑定提示词框聚焦追踪(widget 此时可能尚未建好,延迟一拍) setTimeout(() => trackFocus(this), 0); return r; }; // 右键菜单兜底入口 const origMenu = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (canvasRef, options) { origMenu?.call(this, canvasRef, options); options.unshift({ content: "📖 提示词词库", callback: () => openPanel(this) }); }; }, });