Files
ComfyAssets-ComfyUI_PromptM…/web/prompt_manager.js
T

727 lines
26 KiB
JavaScript

// PromptManager/web/prompt_manager.js
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "PromptManager.UI",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "PromptManager" || nodeData.name === "PromptManagerText") {
console.log(`[PromptManager] Patching node type for custom UI: ${nodeData.name}`);
// Store original methods
const onNodeCreated = nodeType.prototype.onNodeCreated;
// Override onNodeCreated to add custom UI
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// 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
const container = document.createElement("div");
container.style.padding = "8px";
container.style.backgroundColor = "#2a2a2a";
container.style.borderRadius = "4px";
container.style.marginTop = "5px";
this.createSearchUI(container);
// Add as DOM widget
this.searchWidget = this.addDOMWidget(
"prompt_manager_search_ui",
"div",
container,
);
// Set initial node size, but preserve user resizes
if (!this._userHasResized) {
this.size = [400, 400]; // width=400, height=300 pixels
}
// Hook into resize to track user changes
const originalOnResize = this.onResize;
this.onResize = function (size) {
this._userHasResized = true;
console.log("[PromptManager] User resized to:", size);
if (originalOnResize) {
originalOnResize.call(this, size);
}
};
// Hook into serialization to preserve resize flag and prevent runtime data from being saved
const originalSerialize = this.serialize;
this.serialize = function () {
const data = originalSerialize ? originalSerialize.call(this) : {};
data._userHasResized = this._userHasResized;
// 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);
};
// Method to create search UI elements
nodeType.prototype.createSearchUI = function (container) {
container.innerHTML = "";
// Search controls section
const searchSection = document.createElement("div");
searchSection.style.marginBottom = "10px";
// Search buttons row
const buttonRow = document.createElement("div");
buttonRow.style.display = "flex";
buttonRow.style.gap = "8px";
buttonRow.style.marginBottom = "8px";
// 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.maxHeight = "200px";
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);
container.appendChild(searchSection);
};
// 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) {
console.log(
"[PromptManager] Could not load settings, using 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) {
textWidget.value = prompt.text;
// Also set metadata if available
const categoryWidget = this.widgets?.find(
(w) => w.name === "category",
);
if (categoryWidget && prompt.category) {
categoryWidget.value = prompt.category;
}
const tagsWidget = this.widgets?.find((w) => w.name === "tags");
if (tagsWidget && prompt.tags) {
tagsWidget.value = Array.isArray(prompt.tags)
? prompt.tags.join(", ")
: prompt.tags;
}
// Visual feedback
this.highlightSelectedResult(index);
// Trigger widget change events
this.setDirtyCanvas(true, true);
console.log(
`[PromptManager] Loaded prompt: ${prompt.text.substring(0, 50)}...`,
);
}
};
// 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) {
console.log("[PromptManager] Web interface opened in new tab");
this.showNotification(
"Web interface opened in new tab",
"success",
);
} else {
// Fallback if popup was blocked
console.log(
"[PromptManager] Popup blocked, trying alternative method",
);
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) {
console.log("[PromptManager] Web interface opened in popup");
this.showNotification("Web interface opened in popup", "success");
} else {
// Fallback if popup was blocked
console.log(
"[PromptManager] Popup blocked, trying alternative method",
);
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);
};
}
},
});