Files
ComfyAssets-ComfyUI_PromptM…/web/prompt_manager.js
T
Vito Sansevero 2774ed921b fix: forward partial execution targets in queuePrompt wrapper
The api.queuePrompt wrapper introduced in PR #91 only forwarded 2 of 3
arguments, dropping the options object that carries
partialExecutionTargets. This caused the server to execute all output
nodes instead of just the targeted branch.

Also removes debug console.log and print statements added during
development.

Fixes #94
2026-02-03 13:36:51 -08:00

1137 lines
40 KiB
JavaScript

// PromptManager/web/prompt_manager.js
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// ============================================================================
// Top Bar Button Integration (similar to rgthree-comfy approach)
// ============================================================================
let pmButtonGroup = null;
/**
* 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("/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("/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 (node.type === "PromptManager" || node.type === "PromptManagerText") {
if (node.widgets && node.widgets.length > 0) {
if (!node.widgets_values) {
node.widgets_values = [];
}
node.widgets.forEach((widget, idx) => {
if (widget) {
// CRITICAL: For multiline widgets, inputEl.value is the true source
const actualValue = widget.inputEl ? widget.inputEl.value : widget.value;
if (actualValue !== undefined) {
const oldValue = node.widgets_values[idx];
node.widgets_values[idx] = actualValue;
// Also sync widget.value
if (widget.inputEl && widget.value !== actualValue) {
widget.value = actualValue;
}
// text widget value synced from DOM
}
}
});
}
}
}
}
app.registerExtension({
name: "PromptManager.UI",
async setup() {
// 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 ((node.type === "PromptManager" || node.type === "PromptManagerText") && output[node.id]) {
// Find the text widget and get its current DOM value
const textWidget = node.widgets?.find(w => w.name === "text");
if (textWidget && textWidget.inputEl) {
const currentValue = textWidget.inputEl.value;
const outputNode = output[node.id];
// The output structure has "inputs" with widget values
if (outputNode.inputs && outputNode.inputs.text !== currentValue) {
outputNode.inputs.text = currentValue;
}
}
}
}
}
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 === "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;
setTimeout(() => {
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
textWidget.inputEl.addEventListener('input', syncValue);
// Sync on change (for paste, autocomplete, etc.)
textWidget.inputEl.addEventListener('change', syncValue);
// Sync when focus leaves the field
textWidget.inputEl.addEventListener('blur', syncValue);
// Sync on keyup as backup
textWidget.inputEl.addEventListener('keyup', syncValue);
// 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: #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 () {
// CRITICAL: Sync widget values BEFORE serialization
// This ensures graphToPrompt reads current values, not cached ones
// For multiline widgets, the value is stored in inputEl.value
if (this.widgets && this.widgets.length > 0) {
if (!this.widgets_values) {
this.widgets_values = [];
}
this.widgets.forEach((widget, idx) => {
if (widget) {
// Get the actual value - inputEl.value for DOM widgets, otherwise widget.value
const actualValue = widget.inputEl ? widget.inputEl.value : widget.value;
if (actualValue !== undefined) {
this.widgets_values[idx] = actualValue;
// Sync widget.value with inputEl.value
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 =
'<div style="color: #999; text-align: center; padding: 20px;">💡 Click Search or Recent to find prompts</div>';
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 =
'<div style="color: #999; text-align: center;">🔍 Searching database...</div>';
// Get search criteria from node widgets (only search_text available now)
const searchText =
this.widgets?.find((w) => w.name === "search_text")?.value || "";
// Validate search criteria
if (!searchText.trim()) {
this.resultsSection.innerHTML =
'<div style="color: #f9a825; text-align: center;">⚠️ Please enter search text</div>';
return;
}
// Call the backend search function
const response = await this.callNodeMethod("search_prompts", {
search_text: searchText,
});
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 = `<div style="color: #f88; text-align: center;">❌ Search error: ${error.message}</div>`;
this.showNotification(`Search failed: ${error.message}`, "error");
}
};
// Method to load recent prompts
nodeType.prototype.loadRecentPrompts = async function () {
try {
this.resultsSection.innerHTML =
'<div style="color: #999; text-align: center;">📋 Loading recent prompts...</div>';
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 = `<div style="color: #f88; text-align: center;">❌ Error loading recent prompts: ${error.message}</div>`;
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("/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<br><small>Click to restore</small>";
// 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 =
'<div style="color: #f9a825; text-align: center; padding: 20px;">📭 No prompts found</div>';
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;
resultItem.innerHTML = `
<div style="color: #e8e8e8; font-weight: bold; margin-bottom: 4px; line-height: 1.3;">
${promptPreview}
</div>
<div style="color: #aaa; font-size: 10px; line-height: 1.4;">
<div style="margin-bottom: 2px;">
<span style="color: #4facfe;">📁 ${category}</span> •
<span style="color: #ffd700;">${rating}</span> •
<span style="color: #8cc8ff;">📅 ${created}</span>
</div>
<div style="color: #9c9c9c;">
🏷️ ${tags}
</div>
</div>
`;
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") {
// Build query string for search (simplified - only text search)
const queryParams = new URLSearchParams();
if (params.search_text)
queryParams.append("text", params.search_text);
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}`;
} else {
throw new Error(`Unknown method: ${method}`);
}
const response = await fetch(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) {
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("/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(`/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 =
'<div style="color: #999; text-align: center;">🔧 Testing API connection...</div>';
const response = await fetch("/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 = `
<div style="color: #4caf50; text-align: center; padding: 10px;">
✅ API Connection Successful!<br>
<small>${data.message}</small><br>
<small>Time: ${data.timestamp}</small>
</div>
`;
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 = `
<div style="color: #f44336; text-align: center; padding: 10px;">
❌ API Connection Failed<br>
<small>${error.message}</small>
</div>
`;
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);
};
}
},
});