From 092f80bb05045a873846d2905f68794e68abcb78 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sat, 24 May 2025 12:26:21 -0700 Subject: [PATCH] feat(web): Add admin and index UI for PromptManager --- web/admin.html | 1458 +++++++++++++++++++++++++++++++++++++++++ web/index.html | 61 ++ web/prompt_manager.js | 673 +++++++++++++++++++ 3 files changed, 2192 insertions(+) create mode 100644 web/admin.html create mode 100644 web/index.html create mode 100644 web/prompt_manager.js diff --git a/web/admin.html b/web/admin.html new file mode 100644 index 0000000..8cfea6e --- /dev/null +++ b/web/admin.html @@ -0,0 +1,1458 @@ + + + + + + PromptManager - Admin Dashboard + + + + + +
+ +
+
+
+
+
+ ๐Ÿ” +
+
+

+ PromptManager Admin Dashboard +

+

Manage and organize your AI prompts with powerful search and editing tools

+
+
+ +
+
+
+ + +
+
+
+
+
+
+

Total Prompts

+

-

+
+
+ ๐Ÿ“ +
+
+
+
+
+
+

Categories

+

-

+
+
+ ๐Ÿ“ +
+
+
+
+
+
+

Unique Tags

+

-

+
+
+ ๐Ÿท๏ธ +
+
+
+
+
+
+

Avg Rating

+

-

+
+
+ โญ +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + + + + + + + +
+
+
+
+ + +
+
+
+ +
+

Recent Prompts

+ Loading... +
+ + +
+
+
+

Loading prompts...

+
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f1bbb39 --- /dev/null +++ b/web/index.html @@ -0,0 +1,61 @@ + + + + + + PromptManager - Redirecting to Admin + + + +
+
+

๐Ÿ” PromptManager

+

Redirecting to Admin Dashboard...

+

Loading the full-featured prompt management interface

+
+ + + + \ No newline at end of file diff --git a/web/prompt_manager.js b/web/prompt_manager.js new file mode 100644 index 0000000..75d7af7 --- /dev/null +++ b/web/prompt_manager.js @@ -0,0 +1,673 @@ +// 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") { + console.log("[PromptManager] Patching node type for custom UI"); + + // 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 + this.properties = this.properties || {}; + this.properties.searchResults = []; + this.properties.selectedPromptIndex = -1; + this.properties.resultTimeout = 3; // Default 3 seconds + this.properties.showTestButton = false; // Default hide test button + this.properties.webuiDisplayMode = 'popup'; // Default popup mode + this.resultHideTimer = null; + + // 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, + ); + + // Recompute size + this.size = this.computeSize(); + 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 = + '
๐Ÿ’ก Click Search or Recent to find prompts
'; + + this.resultsSection = resultsSection; + searchSection.appendChild(resultsSection); + + container.appendChild(searchSection); + }; + + // Method to perform search + nodeType.prototype.performSearch = async function () { + try { + this.resultsSection.innerHTML = + '
๐Ÿ” Searching database...
'; + + // 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 = + '
โš ๏ธ Please enter search text
'; + 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 = `
โŒ Search error: ${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: ${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("/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
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.properties.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 = ` +
+ ${promptPreview} +
+
+
+ ๐Ÿ“ ${category} โ€ข + ${rating} โ€ข + ๐Ÿ“… ${created} +
+
+ ๐Ÿท๏ธ ${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) { + 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 = + '
๐Ÿ”ง Testing API connection...
'; + + 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 = ` +
+ โœ… 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) { + 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); + }; + } + }, +});