// PromptManager/web/prompt_manager.js import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const { installClassicControls } = await import(api.fileURL('/prompt_manager/js/classic-node.js')); const { installFocusedNode, installSplitNode, installPairNode } = await import(api.fileURL('/prompt_manager/js/focused-node.js')); const { installPromptBridge } = await import(api.fileURL('/prompt_manager/js/prompt-bridge-host.js')); const promptManagerTypes = new Set(['PromptManager', 'PromptManagerText', 'PromptManagerFocused', 'PromptManagerSplit', 'PromptManagerPair']); // ============================================================================ // Top Bar Button Integration (similar to rgthree-comfy approach) // ============================================================================ let pmButtonGroup = null; /** * Escape HTML special characters to prevent XSS in innerHTML contexts. */ function escapeHtml(text) { if (text == null) return ''; const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } /** * Split node search input into free text and tag filters (#150). * `tag:portrait tag:"film grain" sunset` -> { text: "sunset", tags: ["portrait", "film grain"] } * @param {string} input - Raw search_text widget value * @returns {{text: string, tags: string[]}} */ function parseSearchQuery(input) { const tags = []; const text = (input || "") .replace(/\btag:(?:"([^"]+)"|(\S+))/gi, (_, quoted, bare) => { tags.push((quoted || bare).trim()); return " "; }) .replace(/\s+/g, " ") .trim(); return { text, tags: tags.filter(Boolean) }; } /** * Create a DOM element with optional attributes and children * @param {string} tag - HTML tag name * @param {Object} attrs - Attributes and properties * @returns {HTMLElement} */ function $el(tag, attrs = {}) { const element = document.createElement(tag); for (const [key, value] of Object.entries(attrs)) { if (key === "children") { if (Array.isArray(value)) { value.forEach(child => child && element.appendChild(child)); } else if (value) { element.appendChild(value); } } else if (key === "child") { if (value) element.appendChild(value); } else if (key === "text") { element.textContent = value; } else if (key === "html") { element.innerHTML = value; } else if (key === "style" && typeof value === "object") { Object.assign(element.style, value); } else if (key === "classList") { element.className = value; } else if (key.startsWith("on") && typeof value === "function") { element.addEventListener(key.slice(2).toLowerCase(), value); } else { element.setAttribute(key, value); } } return element; } /** * Add PromptManager button to the top bar */ function addPMTopBarButton() { // Remove existing button group if present if (pmButtonGroup?.parentElement) { pmButtonGroup.parentElement.removeChild(pmButtonGroup); pmButtonGroup = null; } // Check if app.menu exists (new frontend) if (!app.menu?.settingsGroup?.element) { // New frontend menu not found, try legacy approach addPMTopBarButtonLegacy(); return; } // Create button group container pmButtonGroup = $el("div", { classList: "comfyui-button-group promptmanager-top-button-group", style: { display: "flex", gap: "4px", marginRight: "8px", } }); // Create PM button const pmButton = $el("button", { classList: "comfyui-button comfyui-menu-mobile-collapse primary", title: "PromptManager - Open Admin Dashboard", style: { display: "flex", alignItems: "center", justifyContent: "center", padding: "4px 10px", background: "#14b8a6", color: "white", border: "none", borderRadius: "4px", fontWeight: "bold", fontSize: "12px", cursor: "pointer", transition: "background 0.2s", }, onclick: () => { window.open(api.fileURL("/prompt_manager/admin"), "_blank"); }, onmouseenter: (e) => { e.target.style.background = "#0d9488"; }, onmouseleave: (e) => { e.target.style.background = "#14b8a6"; }, text: "PM" }); pmButtonGroup.appendChild(pmButton); // Insert before the settings group app.menu.settingsGroup.element.before(pmButtonGroup); } /** * Legacy approach for older ComfyUI versions */ function addPMTopBarButtonLegacy() { // Try multiple selectors for different ComfyUI versions const topBar = document.querySelector(".comfyui-menu") || document.querySelector(".comfy-menu") || document.querySelector("#comfy-main-menu"); if (!topBar) { // Retry after a short delay if UI not loaded yet setTimeout(addPMTopBarButtonLegacy, 500); return; } // Check if button already exists if (document.getElementById("pm-toolbar-btn")) { return; } const btn = document.createElement("button"); btn.id = "pm-toolbar-btn"; btn.innerText = "PM"; btn.title = "Open PromptManager Admin Dashboard"; // Teal button styling btn.style.cssText = ` margin-left: 8px; padding: 4px 12px; background: #14b8a6; color: white; border: none; border-radius: 6px; font-weight: bold; font-size: 12px; cursor: pointer; transition: background 0.2s; `; btn.onmouseenter = () => btn.style.background = "#0d9488"; btn.onmouseleave = () => btn.style.background = "#14b8a6"; btn.onclick = () => { window.open(api.fileURL("/prompt_manager/admin"), "_blank"); }; topBar.appendChild(btn); } // ============================================================================ // Extension Registration // ============================================================================ /** * CRITICAL FIX: Sync all PromptManager widget values from DOM * This must be called before any serialization happens */ function syncPromptManagerWidgets() { const graph = app.graph; if (!graph) return; const nodes = graph._nodes || graph.nodes || []; for (const node of nodes) { if (promptManagerTypes.has(node.type)) { if (node.widgets && node.widgets.length > 0) { if (!node.widgets_values) { node.widgets_values = []; } node.widgets.forEach((widget, idx) => { if (widget) { // Skip widgets whose input is connected to another node — // the connected value takes precedence over the DOM widget value const isConnected = node.inputs?.some( i => i.name === widget.name && i.link != null ); if (isConnected) return; // For non-connected widgets, inputEl.value is the true source const actualValue = widget.inputEl ? widget.inputEl.value : widget.value; if (actualValue !== undefined) { node.widgets_values[idx] = actualValue; // Also sync widget.value if (widget.inputEl && widget.value !== actualValue) { widget.value = actualValue; } } } }); } } } } app.registerExtension({ name: "PromptManager.UI", async setup() { installPromptBridge(app, { baseUrl: api.fileURL('/prompt_manager/') }); // Add the top bar button after a short delay to ensure UI is ready setTimeout(() => { addPMTopBarButton(); }, 100); // CRITICAL: Wrap app.graphToPrompt to sync widget values BEFORE serialization // This runs before any other extension's wrapper can access stale data const originalGraphToPrompt = app.graphToPrompt.bind(app); app.graphToPrompt = async function(...args) { syncPromptManagerWidgets(); return originalGraphToPrompt(...args); }; // CRITICAL FIX: Intercept api.queuePrompt to fix prompt data at the LAST moment // This catches any cached/stale data that other extensions might have passed through // NOTE: The third parameter (...rest) carries partialExecutionTargets and previewMethod // which ComfyUI uses for branch execution. Dropping it breaks partial execution. const originalQueuePrompt = api.queuePrompt.bind(api); api.queuePrompt = async function(number, { output, workflow }, ...rest) { // Find PromptManager nodes in the output and fix their text values const graph = app.graph; if (graph && output) { const nodes = graph._nodes || graph.nodes || []; for (const node of nodes) { if (promptManagerTypes.has(node.type) && output[node.id]) { const outputNode = output[node.id]; for (const name of node.type === 'PromptManagerPair' ? ['text','negative_text'] : ['text']) { if (!outputNode.inputs || Array.isArray(outputNode.inputs[name]) || node.inputs?.some(input => input.name === name && input.link != null)) continue; const widget = node.widgets?.find(widget => widget.name === name); if (widget?.inputEl) outputNode.inputs[name] = widget.inputEl.value; } } } } return originalQueuePrompt(number, { output, workflow }, ...rest); }; }, /** * CRITICAL: Also hook beforeQueuePrompt as a backup sync point */ async beforeQueuePrompt(promptInfo) { syncPromptManagerWidgets(); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === 'PromptManagerFocused' || nodeData.name === 'PromptManagerSplit' || nodeData.name === 'PromptManagerPair') { const install = nodeData.name === 'PromptManagerPair' ? installPairNode : nodeData.name === 'PromptManagerSplit' ? installSplitNode : installFocusedNode; install(nodeType, app, api, parseSearchQuery); return; } if (nodeData.name === "PromptManager" || nodeData.name === "PromptManagerText") { // Store original methods const onNodeCreated = nodeType.prototype.onNodeCreated; // Override onNodeCreated to add custom UI nodeType.prototype.onNodeCreated = function () { if (onNodeCreated) { onNodeCreated.apply(this, arguments); } // CRITICAL FIX: Ensure text widget always returns current value during serialization // This fixes the issue where graphToPrompt reads stale cached values const self = this; this._textSyncTimer = setTimeout(() => { if (self._classicScope?.signal.aborted) return; const textWidget = self.widgets?.find((w) => w.name === "text"); if (textWidget) { // Store reference to the widget for the closure const widgetRef = textWidget; const originalSerializeValue = textWidget.serializeValue; // Override serializeValue - use arrow function to capture widgetRef textWidget.serializeValue = (nodeId, widgetIndex) => { // Always return the current DOM value using our stored reference if (widgetRef.inputEl) { const currentValue = widgetRef.inputEl.value; // Sync widget.value as well widgetRef.value = currentValue; return currentValue; } return widgetRef.value; }; // Add aggressive sync listeners for all value change events if (textWidget.inputEl) { const syncValue = function() { const currentVal = textWidget.inputEl.value; textWidget.value = currentVal; // Also update widgets_values array const widgetIndex = self.widgets?.indexOf(textWidget); if (self.widgets_values && widgetIndex >= 0) { self.widgets_values[widgetIndex] = currentVal; } }; // Sync on every keystroke const listen = self._classicScope ? (event) => self._classicScope.listen(textWidget.inputEl, event, syncValue) : (event) => textWidget.inputEl.addEventListener(event, syncValue); listen('input'); // Sync on change (for paste, autocomplete, etc.) listen('change'); // Sync when focus leaves the field listen('blur'); // Sync on keyup as backup listen('keyup'); // Initial sync syncValue(); } } }, 50); // Shorter delay to beat other extensions // Initialize properties for search state (non-serialized runtime state) this.properties = this.properties || {}; this.properties.resultTimeout = 3; // Default 3 seconds this.properties.showTestButton = false; // Default hide test button this.properties.webuiDisplayMode = "popup"; // Default popup mode // Runtime-only state (not serialized to workflow) this._searchResults = []; this._selectedPromptIndex = -1; this.resultHideTimer = null; // Store node type for conditional behavior this.nodeTypeName = nodeData.name; // Load settings from API this.loadSettings(); // Create DOM widget container with unique ID for scoped styling const container = document.createElement("div"); const uniqueId = `pm-${Math.random().toString(36).substring(2, 9)}`; container.id = uniqueId; // Add inline styles for perfect sizing like KikoLocalImageLoader const styleSheet = document.createElement("style"); styleSheet.textContent = ` #${uniqueId} { width: 100%; height: 100%; padding: 8px; background-color: ${nodeData.name === 'PromptManager' ? 'var(--ca-surface)' : '#2a2a2a'}; border-radius: 4px; box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; } #${uniqueId} > div { width: 100%; height: 100%; display: flex; flex-direction: column; gap: 8px; } `; document.head.appendChild(styleSheet); // Store style reference for cleanup this._styleSheet = styleSheet; this.createSearchUI(container); // Add as DOM widget with proper configuration this.searchWidget = this.addDOMWidget( "prompt_manager_search_ui", "div", container, { serialize: false, // Don't save UI state } ); // Set initial node size, but preserve user resizes if (!this._userHasResized) { // For nodes with text widgets, we need more height const baseHeight = 400; const widgetHeight = this.widgets ? this.widgets.length * 30 : 0; this.size = [600, baseHeight + widgetHeight]; // Dynamic size based on widgets this.setSize(this.size); // IMPORTANT: Actually apply the size } // Hook into resize to track user changes const originalOnResize = this.onResize; this.onResize = function (size) { this._userHasResized = true; if (originalOnResize) { originalOnResize.call(this, size); } }; // Hook into serialization to preserve resize flag, sync widget values, and prevent runtime data from being saved const originalSerialize = this.serialize; this.serialize = function () { // Sync widget values BEFORE serialization, but only for non-connected widgets if (this.widgets && this.widgets.length > 0) { if (!this.widgets_values) { this.widgets_values = []; } this.widgets.forEach((widget, idx) => { if (widget) { // Skip connected widgets — their value comes from the link const isConnected = self.inputs?.some( i => i.name === widget.name && i.link != null ); if (isConnected) return; const actualValue = widget.inputEl ? widget.inputEl.value : widget.value; if (actualValue !== undefined) { this.widgets_values[idx] = actualValue; if (widget.inputEl && widget.value !== actualValue) { widget.value = actualValue; } } } }); } const data = originalSerialize ? originalSerialize.call(this) : {}; data._userHasResized = this._userHasResized; // Ensure widgets_values is included in serialization if (this.widgets_values) { data.widgets_values = [...this.widgets_values]; } // Ensure search results are never saved to workflow if (data.properties && data.properties.searchResults) { delete data.properties.searchResults; } if (data.properties && data.properties.selectedPromptIndex) { delete data.properties.selectedPromptIndex; } return data; }; // Hook into configure to restore resize flag const originalConfigure = this.configure; this.configure = function (data) { if (originalConfigure) { originalConfigure.call(this, data); } if (data._userHasResized) { this._userHasResized = data._userHasResized; } }; this.setDirtyCanvas(true, true); }; // Override onRemoved to cleanup styles const onRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function () { // Cleanup styles when node is removed if (this._styleSheet && this._styleSheet.parentNode) { this._styleSheet.parentNode.removeChild(this._styleSheet); } // Call original onRemoved if it exists if (onRemoved) { onRemoved.apply(this, arguments); } }; // Method to create search UI elements nodeType.prototype.createSearchUI = function (container) { container.innerHTML = ""; // Main wrapper to match KikoLocalImageLoader structure const wrapper = document.createElement("div"); wrapper.style.width = "100%"; wrapper.style.height = "100%"; wrapper.style.display = "flex"; wrapper.style.flexDirection = "column"; wrapper.style.gap = "8px"; // Search controls section const searchSection = document.createElement("div"); searchSection.style.display = "flex"; searchSection.style.flexDirection = "column"; searchSection.style.height = "100%"; searchSection.style.gap = "8px"; // Search buttons row const buttonRow = document.createElement("div"); buttonRow.style.display = "flex"; buttonRow.style.gap = "8px"; buttonRow.style.flexShrink = "0"; // Don't shrink buttons // Search button const searchButton = document.createElement("button"); searchButton.textContent = "🔍 Search"; searchButton.style.flex = "1"; searchButton.style.padding = "6px 12px"; searchButton.style.backgroundColor = "#4a90e2"; searchButton.style.color = "white"; searchButton.style.border = "none"; searchButton.style.borderRadius = "4px"; searchButton.style.cursor = "pointer"; searchButton.style.fontSize = "12px"; searchButton.addEventListener("click", () => this.performSearch()); buttonRow.appendChild(searchButton); // Recent prompts button const recentButton = document.createElement("button"); recentButton.textContent = "📋 Recent"; recentButton.style.flex = "1"; recentButton.style.padding = "6px 12px"; recentButton.style.backgroundColor = "#7b68ee"; recentButton.style.color = "white"; recentButton.style.border = "none"; recentButton.style.borderRadius = "4px"; recentButton.style.cursor = "pointer"; recentButton.style.fontSize = "12px"; recentButton.addEventListener("click", () => this.loadRecentPrompts()); buttonRow.appendChild(recentButton); // Web UI button const webUIButton = document.createElement("button"); webUIButton.textContent = "🌐 Web UI"; webUIButton.style.flex = "1"; webUIButton.style.padding = "6px 12px"; webUIButton.style.backgroundColor = "#50c878"; webUIButton.style.color = "white"; webUIButton.style.border = "none"; webUIButton.style.borderRadius = "4px"; webUIButton.style.cursor = "pointer"; webUIButton.style.fontSize = "12px"; webUIButton.addEventListener("click", () => this.openWebInterface()); buttonRow.appendChild(webUIButton); searchSection.appendChild(buttonRow); // Add test API button for debugging (only if enabled in settings) if (this.properties.showTestButton) { const testRow = document.createElement("div"); testRow.style.marginBottom = "8px"; const testButton = document.createElement("button"); testButton.textContent = "🔧 Test API"; testButton.style.width = "100%"; testButton.style.padding = "4px 8px"; testButton.style.backgroundColor = "#666"; testButton.style.color = "white"; testButton.style.border = "none"; testButton.style.borderRadius = "3px"; testButton.style.cursor = "pointer"; testButton.style.fontSize = "10px"; testButton.addEventListener("click", () => this.testApiConnection()); testRow.appendChild(testButton); searchSection.appendChild(testRow); } // Results section const resultsSection = document.createElement("div"); resultsSection.style.flex = "1"; // Take remaining space resultsSection.style.minHeight = "100px"; // Minimum height resultsSection.style.overflowY = "auto"; resultsSection.style.border = "1px solid #444"; resultsSection.style.borderRadius = "4px"; resultsSection.style.backgroundColor = "#333"; resultsSection.style.padding = "8px"; resultsSection.style.fontSize = "11px"; resultsSection.style.fontFamily = "monospace"; resultsSection.innerHTML = '
💡 Click Search or Recent to find prompts
'; this.resultsSection = resultsSection; searchSection.appendChild(resultsSection); // Add search section to wrapper, then wrapper to container wrapper.appendChild(searchSection); container.appendChild(wrapper); }; // Method to perform search nodeType.prototype.performSearch = async function () { try { this.resultsSection.innerHTML = '
🔍 Searching database...
'; // search_text supports free text plus tag:name filters const { text, tags } = parseSearchQuery( this.widgets?.find((w) => w.name === "search_text")?.value || "", ); // Validate search criteria if (!text && tags.length === 0) { this.resultsSection.innerHTML = '
⚠️ Enter search text or tag:name
'; return; } // Call the backend search function const response = await this.callNodeMethod("search_prompts", { search_text: text, tags, }); this.displayResults(response.results || []); // Show success notification if (response.results && response.results.length > 0) { this.showNotification( `Found ${response.results.length} prompts`, "success", ); } } catch (error) { console.error("[PromptManager] Search error:", error); this.resultsSection.innerHTML = `
❌ Search error: ${escapeHtml(error.message)}
`; this.showNotification(`Search failed: ${error.message}`, "error"); } }; // Method to load recent prompts nodeType.prototype.loadRecentPrompts = async function () { try { this.resultsSection.innerHTML = '
📋 Loading recent prompts...
'; const response = await this.callNodeMethod("get_recent_prompts", { limit: 20, }); this.displayResults(response.results || []); // Show success notification if (response.results && response.results.length > 0) { this.showNotification( `Loaded ${response.results.length} recent prompts`, "success", ); } else { this.showNotification("No recent prompts found", "info"); } } catch (error) { console.error("[PromptManager] Recent prompts error:", error); this.resultsSection.innerHTML = `
❌ Error loading recent prompts: ${escapeHtml(error.message)}
`; this.showNotification( `Failed to load recent prompts: ${error.message}`, "error", ); } }; // Method to load settings from API nodeType.prototype.loadSettings = async function () { try { const response = await fetch(api.fileURL("/prompt_manager/settings")); if (response.ok) { const data = await response.json(); if (data.success && data.settings) { this.properties.resultTimeout = data.settings.result_timeout ?? 3; this.properties.showTestButton = data.settings.show_test_button || false; this.properties.webuiDisplayMode = data.settings.webui_display_mode || "popup"; } } } catch (error) { // Settings not available, use defaults } }; // Method to start auto-hide timer nodeType.prototype.startAutoHideTimer = function () { // Clear existing timer if (this.resultHideTimer) { clearTimeout(this.resultHideTimer); } // Only set timer if timeout > 0 if (this.properties.resultTimeout > 0) { this.resultHideTimer = setTimeout(() => { this.hideResults(); }, this.properties.resultTimeout * 1000); } }; // Method to hide results nodeType.prototype.hideResults = function () { if (this.resultsSection) { // Completely hide the results section this.resultsSection.style.display = "none"; // Create a restore button in place of the results const restoreButton = document.createElement("div"); restoreButton.style.padding = "20px"; restoreButton.style.backgroundColor = "#333"; restoreButton.style.border = "1px solid #444"; restoreButton.style.borderRadius = "4px"; restoreButton.style.textAlign = "center"; restoreButton.style.cursor = "pointer"; restoreButton.style.color = "#999"; restoreButton.style.fontSize = "12px"; restoreButton.innerHTML = "🔄 Results auto-hidden
Click to restore"; // Add hover effect restoreButton.addEventListener("mouseenter", () => { restoreButton.style.backgroundColor = "#444"; restoreButton.style.color = "#ccc"; }); restoreButton.addEventListener("mouseleave", () => { restoreButton.style.backgroundColor = "#333"; restoreButton.style.color = "#999"; }); // Click to restore restoreButton.addEventListener("click", () => { this.showResults(); }); // Store reference and insert after results section this.restoreButton = restoreButton; this.resultsSection.parentNode.insertBefore( restoreButton, this.resultsSection.nextSibling, ); } }; // Method to show results nodeType.prototype.showResults = function () { if (this.resultsSection) { // Show the results section this.resultsSection.style.display = "block"; // Remove restore button if it exists if (this.restoreButton && this.restoreButton.parentNode) { this.restoreButton.parentNode.removeChild(this.restoreButton); this.restoreButton = null; } // Restart timer this.startAutoHideTimer(); } }; // Method to display search results nodeType.prototype.displayResults = function (results) { if (!results || results.length === 0) { this.resultsSection.innerHTML = '
📭 No prompts found
'; return; } this._searchResults = results; this.resultsSection.innerHTML = ""; // Ensure results are visible this.resultsSection.style.opacity = "1"; this.resultsSection.style.pointerEvents = "auto"; // Header const header = document.createElement("div"); header.style.marginBottom = "10px"; header.style.fontWeight = "bold"; header.style.color = "#4facfe"; header.style.textAlign = "center"; header.style.padding = "8px"; header.style.backgroundColor = "rgba(79, 172, 254, 0.1)"; header.style.borderRadius = "4px"; header.innerHTML = `📚 Found ${results.length} prompt${results.length === 1 ? "" : "s"}`; this.resultsSection.appendChild(header); // Start auto-hide timer this.startAutoHideTimer(); // Results list results.forEach((result, index) => { const resultItem = document.createElement("div"); resultItem.style.padding = "6px"; resultItem.style.marginBottom = "4px"; resultItem.style.backgroundColor = "#444"; resultItem.style.borderRadius = "3px"; resultItem.style.cursor = "pointer"; resultItem.style.border = "1px solid transparent"; // Hover effect resultItem.addEventListener("mouseenter", () => { resultItem.style.backgroundColor = "#555"; resultItem.style.border = "1px solid #666"; }); resultItem.addEventListener("mouseleave", () => { resultItem.style.backgroundColor = "#444"; resultItem.style.border = "1px solid transparent"; }); // Click to use prompt resultItem.addEventListener("click", () => { this.usePrompt(result, index); }); // Format display const tags = Array.isArray(result.tags) ? result.tags.join(", ") : "No tags"; const rating = result.rating ? `⭐${result.rating}` : "No rating"; const category = result.category || "No category"; const created = result.created_at ? new Date(result.created_at).toLocaleDateString() : "Unknown"; const promptPreview = result.text.length > 80 ? result.text.substring(0, 80) + "..." : result.text; // All user-controlled values are escaped to prevent XSS resultItem.innerHTML = `
${escapeHtml(promptPreview)}
📁 ${escapeHtml(category)} • ${escapeHtml(rating)} • 📅 ${escapeHtml(created)}
🏷️ ${escapeHtml(tags)}
`; this.resultsSection.appendChild(resultItem); }); }; // Method to use a selected prompt nodeType.prototype.usePrompt = function (prompt, index) { // Find the text widget and set its value const textWidget = this.widgets?.find((w) => w.name === "text"); if (textWidget) { const textWidgetIndex = this.widgets.indexOf(textWidget); // Set the widget value textWidget.value = prompt.text; // CRITICAL: For multiline widgets, the value is stored in the DOM element (inputEl) // graphToPrompt calls serializeValue() which reads from inputEl.value if (textWidget.inputEl) { textWidget.inputEl.value = prompt.text; } // CRITICAL: Also update widgets_values array if it exists // ComfyUI's graphToPrompt reads from this array for execution if (this.widgets_values && textWidgetIndex >= 0) { this.widgets_values[textWidgetIndex] = prompt.text; } // Trigger widget callback to notify ComfyUI of value change if (textWidget.callback) { textWidget.callback(prompt.text, app.canvas, this, null, null); } // Also set metadata if available const categoryWidget = this.widgets?.find( (w) => w.name === "category", ); if (categoryWidget && prompt.category) { const categoryIdx = this.widgets.indexOf(categoryWidget); categoryWidget.value = prompt.category; if (categoryWidget.inputEl) { categoryWidget.inputEl.value = prompt.category; } if (this.widgets_values && categoryIdx >= 0) { this.widgets_values[categoryIdx] = prompt.category; } if (categoryWidget.callback) { categoryWidget.callback(prompt.category, app.canvas, this, null, null); } } const tagsWidget = this.widgets?.find((w) => w.name === "tags"); if (tagsWidget && prompt.tags) { const tagsIdx = this.widgets.indexOf(tagsWidget); const tagsValue = Array.isArray(prompt.tags) ? prompt.tags.join(", ") : prompt.tags; tagsWidget.value = tagsValue; if (tagsWidget.inputEl) { tagsWidget.inputEl.value = tagsValue; } if (this.widgets_values && tagsIdx >= 0) { this.widgets_values[tagsIdx] = tagsValue; } if (tagsWidget.callback) { tagsWidget.callback(tagsValue, app.canvas, this, null, null); } } // Visual feedback this.highlightSelectedResult(index); // Trigger widget change events and mark graph as needing re-execution this.setDirtyCanvas(true, true); // Force graph to recognize value change for execution cache invalidation if (app.graph) { app.graph.setDirtyCanvas(true, true); } } }; // Method to highlight selected result nodeType.prototype.highlightSelectedResult = function (index) { const resultItems = this.resultsSection.querySelectorAll( "div[style*='cursor: pointer']", ); resultItems.forEach((item, i) => { if (i === index) { item.style.backgroundColor = "#2a5d31"; item.style.border = "1px solid #4a8c57"; } else { item.style.backgroundColor = "#444"; item.style.border = "1px solid transparent"; } }); }; // Method to call backend API endpoints nodeType.prototype.callNodeMethod = async function (method, params) { try { let url, options = { method: "GET" }; if (method === "search_prompts") { const queryParams = new URLSearchParams(); if (params.search_text) queryParams.append("text", params.search_text); if (params.tags?.length) queryParams.append("tags", params.tags.join(",")); queryParams.append("limit", "50"); url = `/prompt_manager/search?${queryParams.toString()}`; } else if (method === "get_recent_prompts") { const limit = params.limit || 20; url = `/prompt_manager/recent?limit=${limit}&sort=last_used_desc`; } else { throw new Error(`Unknown method: ${method}`); } if (this._classicScope) options.signal = this._classicScope.signal; const response = await fetch(api.fileURL(url), options); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || "API call failed"); } return { results: data.results || [] }; } catch (error) { if (!this._classicScope?.signal.aborted) { console.error( `[PromptManager] API call failed for ${method}:`, error, ); } throw error; } }; // Method to save a prompt via API nodeType.prototype.savePromptToDatabase = async function (promptData) { try { const response = await fetch(api.fileURL("/prompt_manager/save"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(promptData), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || "Save failed"); } return data; } catch (error) { console.error("[PromptManager] Save prompt failed:", error); throw error; } }; // Method to delete a prompt via API nodeType.prototype.deletePrompt = async function (promptId) { try { const response = await fetch(api.fileURL(`/prompt_manager/delete/${promptId}`), { method: "DELETE", }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || "Delete failed"); } return data; } catch (error) { console.error("[PromptManager] Delete prompt failed:", error); throw error; } }; // Method to test API connection nodeType.prototype.testApiConnection = async function () { try { this.resultsSection.innerHTML = '
🔧 Testing API connection...
'; const response = await fetch(api.fileURL("/prompt_manager/test")); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success) { this.resultsSection.innerHTML = `
✅ API Connection Successful!
${data.message}
Time: ${data.timestamp}
`; this.showNotification("API connection test passed!", "success"); } else { throw new Error(data.message || "Test failed"); } } catch (error) { console.error("[PromptManager] API test failed:", error); this.resultsSection.innerHTML = `
❌ API Connection Failed
${error.message}
`; this.showNotification(`API test failed: ${error.message}`, "error"); } }; // Method to open web interface nodeType.prototype.openWebInterface = function () { try { // Open the web interface served via our API endpoint const currentOrigin = window.location.origin; const webUrl = `${currentOrigin}/prompt_manager/web`; if (this.properties.webuiDisplayMode === "newtab") { // Open in new tab without window options const newWindow = window.open(webUrl, "_blank"); if (newWindow) { this.showNotification( "Web interface opened in new tab", "success", ); } else { window.location.href = webUrl; } } else { // Default popup mode with specific dimensions const newWindow = window.open( webUrl, "_blank", "width=1200,height=800,scrollbars=yes,resizable=yes", ); if (newWindow) { this.showNotification("Web interface opened in popup", "success"); } else { window.location.href = webUrl; } } } catch (error) { console.error("[PromptManager] Error opening web interface:", error); this.showNotification( `Web interface error: ${error.message}`, "error", ); } }; // Method to show notifications nodeType.prototype.showNotification = function (message, type = "info") { // Create a temporary notification element const notification = document.createElement("div"); notification.style.position = "fixed"; notification.style.top = "20px"; notification.style.right = "20px"; notification.style.padding = "10px 15px"; notification.style.borderRadius = "4px"; notification.style.color = "white"; notification.style.fontSize = "12px"; notification.style.zIndex = "10000"; notification.style.maxWidth = "300px"; notification.textContent = message; // Set color based on type switch (type) { case "success": notification.style.backgroundColor = "#4caf50"; break; case "error": notification.style.backgroundColor = "#f44336"; break; case "warning": notification.style.backgroundColor = "#ff9800"; break; default: notification.style.backgroundColor = "#2196f3"; } document.body.appendChild(notification); // Remove after 3 seconds setTimeout(() => { document.body.removeChild(notification); }, 3000); }; if (nodeData.name === 'PromptManager') installClassicControls(nodeType, app, parseSearchQuery, api); } }, });