class PromptAdmin { constructor() { this.prompts = []; this.selectedPrompts = new Set(); this.settings = { resultTimeout: 5, webuiDisplayMode: 'popup', }; this.categories = []; this.tags = []; this.imageViewMode = 'fit'; // 'fit' or 'full' this.naturalImageSize = { width: 0, height: 0 }; // Pagination state this.pagination = { currentPage: 1, limit: 50, total: 0, totalPages: 1 }; this.tagsPage = null; this.init(); } init() { this.bindEvents(); this.initRouter(); this.loadInitialData(); } initRouter() { window.addEventListener('hashchange', () => this.handleRoute()); this.handleRoute(); } handleRoute() { const hash = window.location.hash; if (hash === '#/tags' || hash.startsWith('#/tags/')) { this.showTagsPage(); } else { this.showDashboard(); } } showDashboard() { const dashboard = document.getElementById('dashboardView'); const tagsPage = document.getElementById('tagsPageView'); if (dashboard) dashboard.classList.remove('hidden'); if (tagsPage) tagsPage.classList.add('hidden'); if (this.tagsPage) { this.tagsPage.destroy(); this.tagsPage = null; } } showTagsPage() { const dashboard = document.getElementById('dashboardView'); const tagsPage = document.getElementById('tagsPageView'); if (dashboard) dashboard.classList.add('hidden'); if (tagsPage) tagsPage.classList.remove('hidden'); // Lazy-initialize TagsPageManager if (!this.tagsPage && typeof TagsPageManager !== 'undefined') { this.tagsPage = new TagsPageManager(this); } else if (this.tagsPage) { this.tagsPage.loadTagsList(); } } bindEvents() { // Search document.getElementById("searchBtn").addEventListener("click", () => this.search()); document.getElementById("searchText").addEventListener("keyup", (e) => { if (e.key === "Enter") this.search(); }); // Bulk actions document.getElementById("selectAll").addEventListener("change", (e) => this.toggleSelectAll(e.target.checked) ); document.getElementById("bulkDeleteBtn").addEventListener("click", () => this.bulkDelete()); document.getElementById("bulkTagBtn").addEventListener("click", () => this.showBulkTagModal()); document.getElementById("bulkCategoryBtn").addEventListener("click", () => this.showBulkCategoryModal()); document.getElementById("exportBtn").addEventListener("click", () => this.exportPrompts()); document.getElementById("settingsBtn").addEventListener("click", () => this.showSettingsModal()); document.getElementById("statsBtn").addEventListener("click", () => this.showStats()); document.getElementById("diagnosticsBtn").addEventListener("click", () => this.showDiagnosticsModal()); document.getElementById("maintenanceBtn").addEventListener("click", () => this.showMaintenanceModal()); document.getElementById("backupBtn").addEventListener("click", () => this.backupDatabase()); document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal()); document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal()); document.getElementById("autoTagBtn").addEventListener("click", () => this.showAutoTagModal()); document.getElementById("addPromptBtn").addEventListener("click", () => this.showAddPromptModal()); document.getElementById("logsBtn").addEventListener("click", () => this.showLogsModal()); document.getElementById("metadataBtn").addEventListener("click", () => this.openMetadataViewer()); document.getElementById("galleryBtn").addEventListener("click", () => this.openGallery()); // Pagination controls document.getElementById("limitSelector").addEventListener("change", (e) => this.changeLimit(parseInt(e.target.value))); document.getElementById("firstPageBtn").addEventListener("click", () => this.goToPage(1)); document.getElementById("prevPageBtn").addEventListener("click", () => this.goToPage(this.pagination.currentPage - 1)); document.getElementById("nextPageBtn").addEventListener("click", () => this.goToPage(this.pagination.currentPage + 1)); document.getElementById("lastPageBtn").addEventListener("click", () => this.goToPage(this.pagination.totalPages)); // Modals this.bindModalEvents(); // Auto-search on filter changes ["searchCategory"].forEach((id) => { document.getElementById(id).addEventListener("change", () => this.search()); }); // Sort dropdown change document.getElementById("sortBy").addEventListener("change", () => this.renderPrompts()); } bindModalEvents() { // Settings modal document.getElementById("saveSettings").addEventListener("click", () => this.saveSettings()); document.getElementById("cancelSettings").addEventListener("click", () => this.hideModal("settingsModal")); document.getElementById("refreshMonitoringStatus").addEventListener("click", () => this.updateMonitoringStatus()); // Bulk tag modal document.getElementById("confirmBulkTag").addEventListener("click", () => this.confirmBulkTag()); document.getElementById("cancelBulkTag").addEventListener("click", () => this.hideModal("bulkTagModal")); // Individual tag modal document.getElementById("confirmIndividualTag").addEventListener("click", () => this.confirmIndividualTag()); document.getElementById("cancelIndividualTag").addEventListener("click", () => this.hideModal("individualTagModal")); // Bulk category modal document.getElementById("confirmBulkCategory").addEventListener("click", () => this.confirmBulkCategory()); document.getElementById("cancelBulkCategory").addEventListener("click", () => this.hideModal("bulkCategoryModal")); // Restore modal document.getElementById("confirmRestore").addEventListener("click", () => this.confirmRestore()); document.getElementById("cancelRestore").addEventListener("click", () => this.hideModal("restoreModal")); // Diagnostics modal document.getElementById("runDiagnosticsBtn").addEventListener("click", () => this.runDiagnostics()); document.getElementById("testImageLinkBtn").addEventListener("click", () => this.testImageLink()); // Maintenance modal document.getElementById("runMaintenanceBtn").addEventListener("click", () => this.runMaintenance()); document.getElementById("selectAllMaintenanceBtn").addEventListener("click", () => this.selectAllMaintenance()); document.getElementById("clearAllMaintenanceBtn").addEventListener("click", () => this.clearAllMaintenance()); // Scan modal document.getElementById("startScan").addEventListener("click", () => this.startScan()); document.getElementById("cancelScan").addEventListener("click", () => this.hideModal("scanModal")); document.getElementById("quickBackupBtn").addEventListener("click", () => this.quickBackup()); // Logs modal document.getElementById("refreshLogsBtn").addEventListener("click", () => this.refreshLogs()); document.getElementById("downloadLogsBtn").addEventListener("click", () => this.downloadLogs()); document.getElementById("clearLogsBtn").addEventListener("click", () => this.clearLogs()); document.getElementById("updateLogConfigBtn").addEventListener("click", () => this.updateLogConfig()); // Auto Tag modals document.getElementById("startAutoTagBtn").addEventListener("click", () => this.startAutoTag()); document.getElementById("startReviewBtn").addEventListener("click", () => this.startReview()); document.getElementById("skipReviewBtn").addEventListener("click", () => this.skipReviewImage()); document.getElementById("applyReviewBtn").addEventListener("click", () => this.applyReviewTags()); document.getElementById("cancelAutoTagBtn").addEventListener("click", () => this.cancelAutoTag()); document.getElementById("cancelDownloadBtn").addEventListener("click", () => this.cancelDownload()); // Re-tag confirmation modal buttons document.getElementById("retagSkipBtn").addEventListener("click", () => this.handleRetagChoice('skip')); document.getElementById("retagSkipAllBtn").addEventListener("click", () => this.handleRetagChoice('skipAll')); document.getElementById("retagConfirmBtn").addEventListener("click", () => this.handleRetagChoice('retag')); // Tags accordion toggle document.getElementById("reviewTagsToggle").addEventListener("click", () => this.toggleReviewTags()); // Add Prompt modal document.getElementById("saveNewPromptBtn").addEventListener("click", () => this.saveNewPrompt()); this.setupAddPromptModal(); // Close modals on backdrop click document.querySelectorAll("[id$='Modal']").forEach((modal) => { modal.addEventListener("click", (e) => { if (e.target === modal) { modal.classList.add("hidden"); modal.classList.remove("flex"); document.body.style.overflow = ""; } }); }); } async loadInitialData() { try { await Promise.all([ this.loadStatistics(), this.loadCategories(), this.loadTags(), this.loadRecentPrompts(), this.loadSettings(), ]); } catch (error) { this.showNotification("Failed to load initial data", "error"); console.error("Load error:", error); } } async loadSettings() { try { const response = await fetch("/prompt_manager/settings"); if (response.ok) { const data = await response.json(); if (data.success && data.settings) { this.settings.resultTimeout = data.settings.result_timeout || 5; this.settings.webuiDisplayMode = data.settings.webui_display_mode || 'popup'; this.settings.galleryRootPath = data.settings.gallery_root_path || ''; this.settings.monitoredDirectories = data.settings.monitored_directories || []; } } } catch (error) { console.error("Settings load error:", error); } } async loadStatistics() { try { const response = await fetch("/prompt_manager/stats"); if (response.ok) { const data = await response.json(); console.log("Stats response:", data); // Debug log if (data.success) { // Try different possible response structures const stats = data.stats || data; document.getElementById("totalPrompts").textContent = stats.total_prompts || data.total_prompts || 0; document.getElementById("totalCategories").textContent = stats.total_categories || data.total_categories || 0; document.getElementById("totalTags").textContent = stats.total_tags || data.total_tags || 0; document.getElementById("avgRating").textContent = (stats.avg_rating || data.avg_rating) ? (stats.avg_rating || data.avg_rating).toFixed(1) : "N/A"; } else { console.error("Stats API returned success=false:", data); } } else { console.error("Stats API HTTP error:", response.status, response.statusText); } } catch (error) { console.error("Stats error:", error); // Set some default values if API fails document.getElementById("totalPrompts").textContent = this.prompts.length || 0; document.getElementById("totalCategories").textContent = this.categories.length || 0; document.getElementById("totalTags").textContent = this.tags.length || 0; document.getElementById("avgRating").textContent = "N/A"; } } async loadCategories() { try { const response = await fetch("/prompt_manager/categories"); if (response.ok) { const data = await response.json(); if (data.success) { this.categories = data.categories; this.populateCategoryDropdown(); } } } catch (error) { console.error("Categories error:", error); } } async loadTags() { try { const response = await fetch("/prompt_manager/tags"); if (response.ok) { const data = await response.json(); if (data.success) { this.tags = data.tags; } } } catch (error) { console.error("Tags error:", error); } } populateCategoryDropdown() { const select = document.getElementById("searchCategory"); select.innerHTML = ''; this.categories.forEach((category) => { const option = document.createElement("option"); option.value = category; option.textContent = category; select.appendChild(option); }); } async loadRecentPrompts(page = 1) { try { this.pagination.currentPage = page; const offset = (page - 1) * this.pagination.limit; const response = await fetch(`/prompt_manager/recent?limit=${this.pagination.limit}&offset=${offset}&page=${page}`); if (response.ok) { const data = await response.json(); if (data.success) { this.prompts = data.results; this.pagination = { ...this.pagination, total: data.pagination.total, totalPages: data.pagination.total_pages, currentPage: data.pagination.page }; this.renderPrompts(); this.updatePaginationControls(); // Don't update stats from local data as it's only a subset of prompts } } } catch (error) { console.error("Recent prompts error:", error); } finally { document.getElementById("loadingState").classList.add("hidden"); document.getElementById("resultsList").classList.remove("hidden"); document.getElementById("paginationControls").classList.remove("hidden"); } } updateLocalStats() { // Only update local stats if we haven't loaded proper statistics yet // This prevents overwriting the correct total counts from the API const currentTotal = document.getElementById("totalPrompts").textContent; // Only update if the current total is still the default "-" if (currentTotal === "-") { // Show local counts as fallback only when API hasn't loaded yet document.getElementById("totalPrompts").textContent = this.prompts.length; const uniqueCategories = new Set(this.prompts.map(p => p.category).filter(Boolean)); document.getElementById("totalCategories").textContent = uniqueCategories.size; const allTags = this.prompts.flatMap(p => Array.isArray(p.tags) ? p.tags : []); const uniqueTags = new Set(allTags.filter(Boolean)); document.getElementById("totalTags").textContent = uniqueTags.size; const ratings = this.prompts.map(p => p.rating).filter(r => r && r > 0); if (ratings.length > 0) { const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length; document.getElementById("avgRating").textContent = avgRating.toFixed(1); } } } async search() { const searchText = document.getElementById("searchText").value; const category = document.getElementById("searchCategory").value; const tags = document.getElementById("searchTags").value; document.getElementById("loadingState").classList.remove("hidden"); document.getElementById("resultsList").classList.add("hidden"); try { const params = new URLSearchParams(); if (searchText) params.append("text", searchText); if (category) params.append("category", category); if (tags) params.append("tags", tags); params.append("limit", "100"); const response = await fetch(`/prompt_manager/search?${params}`); if (response.ok) { const data = await response.json(); if (data.success) { this.prompts = data.results; this.renderPrompts(); document.getElementById("resultsTitle").textContent = "Search Results"; } } } catch (error) { this.showNotification("Search failed", "error"); console.error("Search error:", error); } finally { document.getElementById("loadingState").classList.add("hidden"); document.getElementById("resultsList").classList.remove("hidden"); } } sortPrompts(prompts) { const sortBy = document.getElementById("sortBy")?.value || "created_desc"; const [field, direction] = sortBy.split("_"); return [...prompts].sort((a, b) => { let aVal, bVal; switch (field) { case "rating": aVal = a.rating || 0; bVal = b.rating || 0; break; case "created": aVal = new Date(a.created_at); bVal = new Date(b.created_at); break; case "text": aVal = a.text.toLowerCase(); bVal = b.text.toLowerCase(); break; default: return 0; } if (direction === "asc") { return aVal > bVal ? 1 : aVal < bVal ? -1 : 0; } else { return aVal < bVal ? 1 : aVal > bVal ? -1 : 0; } }); } renderPrompts() { const container = document.getElementById("resultsList"); const count = document.getElementById("resultsCount"); count.textContent = `${this.prompts.length} prompts`; if (this.prompts.length === 0) { container.innerHTML = `
📭

No prompts found

Try adjusting your search criteria or create some prompts in ComfyUI

`; return; } // Sort prompts before rendering const sortedPrompts = this.sortPrompts(this.prompts); container.innerHTML = sortedPrompts.map((prompt) => this.renderPromptItem(prompt)).join(""); this.selectedPrompts.clear(); this.updateBulkActionButtons(); // Add hover behavior to all star ratings sortedPrompts.forEach(prompt => { this.addStarHoverBehavior(prompt.id); }); // Delegated click handler for remove-tag buttons (avoids inline onclick XSS) container.querySelectorAll('.remove-tag-btn').forEach(btn => { btn.addEventListener('click', (e) => { const promptId = parseInt(e.target.dataset.promptId); const tag = e.target.dataset.tag; window.admin.removeTag(promptId, tag); }); }); // Load film strips for each prompt (async, non-blocking) this.loadAllFilmStrips(sortedPrompts); } async loadAllFilmStrips(prompts) { for (const prompt of prompts) { const container = document.getElementById(`filmStrip-${prompt.id}`); if (!container) continue; // Use pre-loaded images from API response if available if (prompt.images && prompt.images.length > 0) { container.innerHTML = this.createFilmStrip(prompt.images, prompt.id); // trusted internal HTML } else if (prompt.image_count > 0) { // Images exist but weren't pre-loaded — fetch individually this.loadFilmStripForPrompt(prompt.id); } } } async loadFilmStripForPrompt(promptId) { const container = document.getElementById(`filmStrip-${promptId}`); if (!container) return; try { const images = await this.loadFilmStripImages(promptId); container.innerHTML = this.createFilmStrip(images, promptId); // trusted internal HTML } catch (error) { console.error(`Error loading filmstrip for prompt ${promptId}:`, error); container.innerHTML = `
Failed to load images
`; // static HTML } } renderPromptItem(prompt) { const tags = Array.isArray(prompt.tags) ? prompt.tags : []; const category = prompt.category || "No category"; const rating = prompt.rating || 0; const created = new Date(prompt.created_at).toLocaleDateString(); return `
${this.escapeHtml(prompt.text)}
📁 ${this.escapeHtml(category)}
📅 ${created}
${this.renderStars(rating, prompt.id)}
${tags.slice(0, 10).map(tag => ` ${this.escapeHtml(tag)} `).join("")}
${tags.length > 10 ? ` ` : ''}
Loading images...
`; } renderStars(rating, promptId) { return Array.from({ length: 5 }, (_, i) => { const starNum = i + 1; const isActive = starNum <= rating; const starIcon = isActive ? '⭐' : '☆'; // filled star vs outline star const color = isActive ? 'var(--pm-warning)' : 'var(--pm-text-secondary)'; // yellow-400 : gray-400 return ``; }).join(""); } addStarHoverBehavior(promptId) { const starButtons = document.querySelectorAll(`[data-prompt-id="${promptId}"].star-btn`); const ratingElement = document.querySelector(`[data-id="${promptId}"][data-rating]`); const currentRating = parseInt(ratingElement?.dataset.rating || 0); starButtons.forEach((starBtn, index) => { const starNum = index + 1; starBtn.addEventListener('mouseenter', () => { // Light up all stars up to this one on hover starButtons.forEach((btn, i) => { if (i < starNum) { btn.textContent = '⭐'; btn.style.color = 'var(--pm-warning)'; // bright yellow on hover } else { btn.textContent = '☆'; btn.style.color = 'var(--pm-text-secondary)'; // gray } }); }); starBtn.addEventListener('mouseleave', () => { // Reset to current rating starButtons.forEach((btn, i) => { if (i < currentRating) { btn.textContent = '⭐'; btn.style.color = 'var(--pm-warning)'; // yellow } else { btn.textContent = '☆'; btn.style.color = 'var(--pm-text-secondary)'; // gray } }); }); }); } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } showNotification(message, type = "info") { const notification = document.createElement("div"); // Check if ViewerJS is open and use higher z-index const viewerContainer = document.querySelector('.viewer-container'); const isViewerOpen = viewerContainer && viewerContainer.style.display !== 'none'; const zIndex = isViewerOpen ? 'z-[50000]' : 'z-50'; notification.className = `fixed top-4 right-4 px-4 py-2 rounded-pm-sm shadow-pm text-sm ${zIndex} transition-all duration-300 transform translate-x-full`; const colors = { success: "bg-pm-success text-pm", error: "bg-pm-error text-pm", warning: "bg-pm-warning text-pm", info: "bg-pm-accent text-pm" }; notification.className += ` ${colors[type] || colors.info}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => notification.classList.remove("translate-x-full"), 100); setTimeout(() => { notification.classList.add("translate-x-full"); setTimeout(() => { if (notification.parentNode) { document.body.removeChild(notification); } }, 300); }, 3000); } showModal(modalId) { document.getElementById(modalId).classList.remove("hidden"); document.getElementById(modalId).classList.add("flex"); document.body.style.overflow = "hidden"; } hideModal(modalId) { document.getElementById(modalId).classList.add("hidden"); document.getElementById(modalId).classList.remove("flex"); document.body.style.overflow = ""; } showSettingsModal() { document.getElementById("resultTimeout").value = this.settings.resultTimeout; document.getElementById("webuiDisplayMode").value = this.settings.webuiDisplayMode; document.getElementById("galleryRootPath").value = this.settings.galleryRootPath || ''; this.updateMonitoringStatus(); this.showModal("settingsModal"); } async updateMonitoringStatus() { const statusEl = document.getElementById("monitoringStatus"); try { const response = await fetch("/prompt_manager/settings"); if (response.ok) { const data = await response.json(); const dirs = data.settings?.monitored_directories || []; if (dirs.length > 0) { statusEl.innerHTML = dirs.map(d => `
✓ ${d}
`).join(''); } else { statusEl.textContent = 'No directories being monitored (auto-detect on restart)'; } } } catch (error) { statusEl.textContent = 'Unable to fetch status'; } } async saveSettings() { const timeout = parseInt(document.getElementById("resultTimeout").value); const displayMode = document.getElementById("webuiDisplayMode").value; const galleryPath = document.getElementById("galleryRootPath").value.trim(); this.settings.resultTimeout = timeout; this.settings.webuiDisplayMode = displayMode; this.settings.galleryRootPath = galleryPath; try { const response = await fetch("/prompt_manager/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ result_timeout: timeout, webui_display_mode: displayMode, gallery_root_path: galleryPath }), }); if (response.ok) { const data = await response.json(); if (data.restart_required) { this.showNotification("Settings saved. Restart ComfyUI for gallery path changes to take effect.", "warning"); } else { this.showNotification("Settings saved successfully", "success"); } this.hideModal("settingsModal"); } else { throw new Error("Failed to save settings"); } } catch (error) { this.showNotification("Failed to save settings", "error"); } } toggleSelectAll(checked) { document.querySelectorAll(".prompt-checkbox").forEach((checkbox) => { checkbox.checked = checked; if (checked) { this.selectedPrompts.add(parseInt(checkbox.dataset.id)); } else { this.selectedPrompts.clear(); } }); this.updateBulkActionButtons(); } updateBulkActionButtons() { const hasSelected = this.selectedPrompts.size > 0; document.getElementById("bulkDeleteBtn").disabled = !hasSelected; document.getElementById("bulkTagBtn").disabled = !hasSelected; document.getElementById("bulkCategoryBtn").disabled = !hasSelected; } async setRating(promptId, rating) { console.log(`Setting rating for prompt ${promptId} to ${rating}`); // Update UI immediately for better UX const ratingElement = document.querySelector(`[data-id="${promptId}"][data-rating]`); if (ratingElement) { ratingElement.dataset.rating = rating; ratingElement.innerHTML = this.renderStars(rating, promptId); // Add hover behavior to new stars this.addStarHoverBehavior(promptId); } try { const response = await fetch(`/prompt_manager/prompts/${promptId}/rating`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rating }), }); if (response.ok) { this.showNotification("Rating updated", "success"); // Update the prompt data in memory const promptIndex = this.prompts.findIndex(p => p.id === promptId); if (promptIndex !== -1) { this.prompts[promptIndex].rating = rating; } // Refresh stats setTimeout(() => { this.loadStatistics(); }, 500); } else { // Revert UI change on API failure const originalPrompt = this.prompts.find(p => p.id === promptId); const originalRating = originalPrompt ? originalPrompt.rating : 0; if (ratingElement) { ratingElement.dataset.rating = originalRating; ratingElement.innerHTML = this.renderStars(originalRating, promptId); } this.showNotification("Failed to update rating", "error"); } } catch (error) { console.error("Rating update error:", error); // Revert UI change on error const originalPrompt = this.prompts.find(p => p.id === promptId); const originalRating = originalPrompt ? originalPrompt.rating : 0; if (ratingElement) { ratingElement.dataset.rating = originalRating; ratingElement.innerHTML = this.renderStars(originalRating, promptId); } this.showNotification("Failed to update rating", "error"); } } addTag(promptId) { // Store the prompt ID for later use this.currentTagPromptId = promptId; // Clear the input and show the modal document.getElementById("individualTagInput").value = ""; this.showModal("individualTagModal"); } async confirmIndividualTag() { const tags = document.getElementById("individualTagInput").value.split(",") .map((tag) => tag.trim()).filter((tag) => tag); if (tags.length === 0) return; try { const response = await fetch("/prompt_manager/prompts/tags", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt_id: this.currentTagPromptId, tags: tags, }), }); if (response.ok) { this.showNotification(`Tags added to prompt`, "success"); this.hideModal("individualTagModal"); // Refresh both stats and results await this.loadStatistics(); this.search(); } } catch (error) { this.showNotification("Failed to add tags", "error"); } } async removeTag(promptId, tag) { try { const response = await fetch(`/prompt_manager/prompts/${promptId}/tags`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tag }), }); if (response.ok) { this.showNotification("Tag removed", "success"); // Refresh both stats and results await this.loadStatistics(); this.search(); } } catch (error) { this.showNotification("Failed to remove tag", "error"); } } async editPrompt(promptId) { const promptElement = document.querySelector(`[data-id="${promptId}"].prompt-text`); if (!promptElement) return; const originalText = promptElement.textContent; promptElement.contentEditable = true; promptElement.focus(); promptElement.classList.add("bg-pm-surface", "border", "border-pm-accent", "rounded-pm-sm", "p-3"); const saveEdit = async () => { const newText = promptElement.textContent.trim(); if (newText !== originalText && newText) { try { const response = await fetch(`/prompt_manager/prompts/${promptId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: newText }), }); if (response.ok) { this.showNotification("Prompt updated", "success"); // Refresh the page to show updated content setTimeout(() => { this.loadStatistics(); setTimeout(() => window.location.reload(), 500); }, 1000); } else { throw new Error("Update failed"); } } catch (error) { this.showNotification("Failed to update prompt", "error"); promptElement.textContent = originalText; } } promptElement.contentEditable = false; promptElement.classList.remove("bg-pm-surface", "border", "border-pm-accent", "rounded-pm-sm", "p-3"); }; promptElement.addEventListener("blur", saveEdit, { once: true }); promptElement.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); saveEdit(); } if (e.key === "Escape") { promptElement.textContent = originalText; promptElement.contentEditable = false; promptElement.classList.remove("bg-pm-surface", "border", "border-pm-accent", "rounded-pm-sm", "p-3"); } }); } async deletePrompt(promptId) { if (!confirm("Are you sure you want to delete this prompt?")) return; try { const response = await fetch(`/prompt_manager/delete/${promptId}`, { method: "DELETE", }); if (response.ok) { this.showNotification("Prompt deleted", "success"); // Refresh stats and reload page setTimeout(() => { this.loadStatistics(); setTimeout(() => window.location.reload(), 500); }, 1000); } } catch (error) { this.showNotification("Failed to delete prompt", "error"); } } showBulkTagModal() { this.showModal("bulkTagModal"); } showBulkCategoryModal() { this.showModal("bulkCategoryModal"); } async confirmBulkTag() { const tags = document.getElementById("bulkTagInput").value.split(",") .map((tag) => tag.trim()).filter((tag) => tag); if (tags.length === 0) return; try { const response = await fetch("/prompt_manager/bulk/tags", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt_ids: Array.from(this.selectedPrompts), tags, }), }); if (response.ok) { this.showNotification(`Tags added to ${this.selectedPrompts.size} prompts`, "success"); this.hideModal("bulkTagModal"); // Refresh both stats and results await this.loadStatistics(); this.search(); } } catch (error) { this.showNotification("Failed to add tags", "error"); } } async confirmBulkCategory() { const category = document.getElementById("bulkCategoryInput").value.trim(); if (!category) return; try { const response = await fetch("/prompt_manager/bulk/category", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt_ids: Array.from(this.selectedPrompts), category, }), }); if (response.ok) { this.showNotification(`Category set for ${this.selectedPrompts.size} prompts`, "success"); this.hideModal("bulkCategoryModal"); // Refresh both stats and results await this.loadStatistics(); this.search(); } } catch (error) { this.showNotification("Failed to set category", "error"); } } async bulkDelete() { if (!confirm(`Are you sure you want to delete ${this.selectedPrompts.size} prompts?`)) return; try { const response = await fetch("/prompt_manager/bulk/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt_ids: Array.from(this.selectedPrompts), }), }); if (response.ok) { this.showNotification(`${this.selectedPrompts.size} prompts deleted`, "success"); // Refresh stats and reload page setTimeout(() => { this.loadStatistics(); setTimeout(() => window.location.reload(), 500); }, 1000); } } catch (error) { this.showNotification("Failed to delete prompts", "error"); } } async exportPrompts() { try { const response = await fetch("/prompt_manager/export"); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `prompts-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); this.showNotification("Prompts exported", "success"); } } catch (error) { this.showNotification("Failed to export prompts", "error"); } } async showStats() { await this.loadStatistics(); this.showNotification("📊 Stats refreshed successfully!", "success"); } // Gallery functionality async viewGallery(promptId) { this.showModal("galleryModal"); // Show loading state document.getElementById("galleryLoading").classList.remove("hidden"); document.getElementById("galleryContent").classList.add("hidden"); document.getElementById("galleryEmpty").classList.add("hidden"); try { const response = await fetch(`/prompt_manager/prompts/${promptId}/images`); if (response.ok) { const data = await response.json(); if (data.success) { this.renderGallery(data.images, promptId); } else { throw new Error(data.error || 'Failed to load images'); } } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Gallery load error:', error); this.showNotification('Failed to load gallery', 'error'); this.closeGallery(); } finally { document.getElementById("galleryLoading").classList.add("hidden"); } } renderGallery(images, promptId) { const content = document.getElementById("galleryContent"); const empty = document.getElementById("galleryEmpty"); // Update title const prompt = this.prompts.find(p => p.id === promptId); const promptText = prompt ? prompt.text.substring(0, 50) + (prompt.text.length > 50 ? '...' : '') : 'Unknown Prompt'; document.getElementById("galleryTitle").textContent = `Gallery: ${promptText}`; if (!images || images.length === 0) { content.classList.add("hidden"); empty.classList.remove("hidden"); return; } content.classList.remove("hidden"); empty.classList.add("hidden"); content.innerHTML = images.map((image, index) => `
Generated image ${index + 1}
${new Date(image.generation_time).toLocaleDateString()} ${new Date(image.generation_time).toLocaleTimeString()}
${image.width && image.height ? `${image.width}×${image.height}` : 'Unknown size'} ${image.file_size ? ` • ${this.formatFileSize(image.file_size)}` : ''}
`).join(''); // Store images for navigation this.currentGalleryImages = images; // Initialize ViewerJS for the gallery after DOM is updated setTimeout(() => { if (this.galleryViewer) { this.galleryViewer.destroy(); } this.galleryViewer = new Viewer(content, { toolbar: { zoomIn: 1, zoomOut: 1, oneToOne: 1, reset: 1, prev: 1, play: { show: 1, size: 'large', }, next: 1, rotateLeft: 1, rotateRight: 1, flipHorizontal: 1, flipVertical: 1, }, navbar: true, title: true, transition: true, keyboard: true, backdrop: 'static', loading: true, loop: true, tooltip: true, zoomRatio: 0.1, minZoomRatio: 0.1, maxZoomRatio: 4, zoomOnTouch: true, zoomOnWheel: true, slideOnTouch: true, toggleOnDblclick: false, className: 'viewer-dark-theme', shown: () => { this.addMetadataSidebar(); }, viewed: (event) => { this.loadMetadataForImage(event.detail.image); } }); }, 100); } closeGallery() { this.hideModal("galleryModal"); } formatFileSize(bytes) { if (!bytes) return 'Unknown'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } // Diagnostics functionality showDiagnosticsModal() { this.showModal("diagnosticsModal"); } closeDiagnostics() { this.hideModal("diagnosticsModal"); } // Maintenance functionality showMaintenanceModal() { this.showModal("maintenanceModal"); } closeMaintenance() { this.hideModal("maintenanceModal"); } selectAllMaintenance() { document.querySelectorAll('.maintenance-option').forEach(checkbox => { checkbox.checked = true; }); } clearAllMaintenance() { document.querySelectorAll('.maintenance-option').forEach(checkbox => { checkbox.checked = false; }); } async runMaintenance() { const resultsContainer = document.getElementById("maintenanceResults"); const runButton = document.getElementById("runMaintenanceBtn"); // Get selected operations const selectedOps = Array.from(document.querySelectorAll('.maintenance-option:checked')) .map(checkbox => checkbox.id); if (selectedOps.length === 0) { this.showNotification('Please select at least one maintenance operation', 'warning'); return; } // Show loading state runButton.disabled = true; runButton.innerHTML = '🔄 Running...'; resultsContainer.innerHTML = `

Running maintenance operations...

Operations: ${selectedOps.map(op => op.replace(/_/g, ' ')).join(', ')}

`; try { const response = await fetch('/prompt_manager/maintenance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operations: selectedOps }) }); if (response.ok) { const data = await response.json(); if (data.success) { this.renderMaintenanceResults(data); // Show overall success notification if (data.all_successful) { this.showNotification('✅ All maintenance operations completed successfully!', 'success'); } else { this.showNotification('⚠️ Some maintenance operations had issues', 'warning'); } // Refresh stats after maintenance setTimeout(() => this.loadStatistics(), 1000); } else { throw new Error(data.error || 'Maintenance failed'); } } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Maintenance error:', error); resultsContainer.innerHTML = `

❌ Maintenance Failed

${error.message}

`; this.showNotification('❌ Maintenance failed', 'error'); } finally { runButton.disabled = false; runButton.innerHTML = '🔧 Run Maintenance'; } } renderMaintenanceResults(data) { const resultsContainer = document.getElementById("maintenanceResults"); let html = '
'; // Summary html += '
'; html += '

📋 Maintenance Summary

'; html += '
'; html += `
Operations Completed ${data.operations_completed}
`; html += `
Overall Success ${data.all_successful ? '✅ ALL PASSED' : '⚠️ SOME ISSUES'}
`; html += '
'; // Detailed results for (const [operation, result] of Object.entries(data.results)) { const bgColor = result.success ? 'bg-pm-success-tint border-pm-success' : 'bg-pm-error-tint border-pm-error'; const statusIcon = result.success ? '✅' : '❌'; const statusText = result.success ? 'SUCCESS' : 'FAILED'; html += `
`; html += `

`; html += `${statusIcon}`; html += `${operation.replace(/_/g, ' ')}`; html += `${statusText}`; html += `

`; if (result.message) { html += `

${result.message}

`; } // Show specific details if (result.removed_count !== undefined) { html += `
Items removed: ${result.removed_count}
`; } if (result.duplicate_hashes !== undefined) { html += `
Duplicate hash groups found: ${result.duplicate_hashes}
`; } if (result.issues_found !== undefined) { html += `
Issues found: ${result.issues_found}
`; if (result.issues && result.issues.length > 0) { html += ``; } } if (result.info) { html += `
`; html += `

Total prompts: ${result.info.total_prompts || 'N/A'}

`; html += `

Database size: ${result.info.database_size_bytes ? this.formatFileSize(result.info.database_size_bytes) : 'N/A'}

`; html += `
`; } if (result.error) { html += `
`; html += `Error: ${result.error}`; html += `
`; } html += '
'; } html += '
'; resultsContainer.innerHTML = html; } async runDiagnostics() { const content = document.getElementById("diagnosticsContent"); content.innerHTML = `

Running diagnostics...

`; try { const response = await fetch('/prompt_manager/diagnostics'); if (response.ok) { const data = await response.json(); if (data.success) { this.renderDiagnostics(data.diagnostics); } else { throw new Error(data.error || 'Diagnostics failed'); } } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Diagnostics error:', error); content.innerHTML = `

❌ Diagnostics Failed

${error.message}

`; } } renderDiagnostics(diagnostics) { const content = document.getElementById("diagnosticsContent"); let html = '
'; // Summary html += '
'; html += '

📋 Diagnostic Summary

'; html += '
'; for (const [category, result] of Object.entries(diagnostics)) { const status = result.status === 'ok' ? '✅ PASS' : (result.status === 'warning' ? '⚠️ WARNING' : '❌ FAIL'); const statusColor = result.status === 'ok' ? 'text-pm-success' : (result.status === 'warning' ? 'text-pm-warning' : 'text-pm-error'); html += `
${this.escapeHtml(category)} ${status}
`; } html += '
'; // Detailed results for (const [category, result] of Object.entries(diagnostics)) { const bgColor = result.status === 'ok' ? 'bg-pm-success-tint border-pm-success' : (result.status === 'warning' ? 'bg-pm-warning/20 border-pm-warning' : 'bg-pm-error-tint border-pm-error'); html += `
`; html += `

${this.escapeHtml(category)}

`; if (result.message) { html += `

${result.message}

`; } // Show specific details based on category if (category === 'database' && result.status === 'ok') { html += `
`; html += `

Prompts: ${result.prompt_count || 0}

`; html += `

Images table: ${result.has_images_table ? 'Yes' : 'No'}

`; html += `
`; } if (category === 'images_table' && result.status === 'ok') { html += `
`; html += `

Images: ${result.image_count || 0}

`; if (result.recent_images && result.recent_images.length > 0) { html += `

Recent images:

`; html += `
    `; result.recent_images.slice(0, 3).forEach(img => { html += `
  • ${img.filename} → Prompt ${img.prompt_id}
  • `; }); html += `
`; } html += `
`; } if (category === 'comfyui_output' && result.output_dirs) { html += `
`; html += `

Output directories found:

`; html += `
    `; result.output_dirs.forEach(dir => { html += `
  • ${dir}
  • `; }); html += `
`; html += `
`; } if (category === 'dependencies' && result.dependencies) { html += `
`; for (const [dep, available] of Object.entries(result.dependencies)) { const status = available ? '✅' : '❌'; html += `

${status} ${dep}

`; } html += `
`; } html += '
'; } html += '
'; content.innerHTML = html; } async testImageLink() { if (this.prompts.length === 0) { this.showNotification('No prompts available for testing', 'warning'); return; } // Use the first prompt for testing const testPrompt = this.prompts[0]; try { const response = await fetch('/prompt_manager/diagnostics/test-link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt_id: testPrompt.id, image_path: '/test/fake/image.png' }) }); if (response.ok) { const data = await response.json(); if (data.success) { this.showNotification('✅ Test image link created successfully!', 'success'); // Refresh the gallery to show the test image setTimeout(() => this.search(), 1000); } else { throw new Error(data.result?.message || 'Test failed'); } } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Test link error:', error); this.showNotification(`❌ Test failed: ${error.message}`, 'error'); } } // Database backup and restore functionality async backupDatabase() { try { const response = await fetch('/prompt_manager/backup'); if (response.ok) { // Get the filename from the Content-Disposition header const contentDisposition = response.headers.get('Content-Disposition'); let filename = 'prompts_backup.db'; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="(.+)"/); if (filenameMatch) { filename = filenameMatch[1]; } } // Create blob and download const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); this.showNotification('💾 Database backup downloaded successfully!', 'success'); } else { const errorData = await response.json(); throw new Error(errorData.error || 'Backup failed'); } } catch (error) { console.error('Backup error:', error); this.showNotification(`❌ Backup failed: ${error.message}`, 'error'); } } showRestoreModal() { // Clear previous file selection document.getElementById('restoreFileInput').value = ''; this.showModal('restoreModal'); } async confirmRestore() { const fileInput = document.getElementById('restoreFileInput'); const file = fileInput.files[0]; if (!file) { this.showNotification('Please select a database file to restore', 'warning'); return; } if (!file.name.endsWith('.db')) { this.showNotification('Please select a valid .db file', 'warning'); return; } // Show confirmation const confirmed = confirm( `Are you sure you want to restore from "${file.name}"?\n\n` + 'This will replace your current database. A backup will be created automatically.\n\n' + 'This action cannot be undone.' ); if (!confirmed) { return; } try { // Disable the restore button and show loading const confirmBtn = document.getElementById('confirmRestore'); confirmBtn.disabled = true; confirmBtn.innerHTML = '🔄 Restoring...'; // Create FormData for file upload const formData = new FormData(); formData.append('database_file', file); const response = await fetch('/prompt_manager/restore', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok && data.success) { this.showNotification( `✅ Database restored successfully! Found ${data.prompt_count} prompts.`, 'success' ); this.hideModal('restoreModal'); // Refresh the interface to show new data setTimeout(() => { window.location.reload(); }, 2000); } else { throw new Error(data.error || 'Restore failed'); } } catch (error) { console.error('Restore error:', error); this.showNotification(`❌ Restore failed: ${error.message}`, 'error'); } finally { // Re-enable the restore button const confirmBtn = document.getElementById('confirmRestore'); confirmBtn.disabled = false; confirmBtn.innerHTML = 'Restore Database'; } } // Copy prompt to clipboard functionality async copyPromptToClipboard(promptId) { // Find the prompt text from the prompts array const prompt = this.prompts.find(p => p.id === promptId); if (!prompt) { this.showNotification('❌ Prompt not found', 'error'); return; } const promptText = prompt.text; try { // Use the modern Clipboard API if available if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(promptText); this.showNotification('📋 Prompt copied to clipboard!', 'success'); } else { // Fallback for older browsers or non-secure contexts const textArea = document.createElement('textarea'); textArea.value = promptText; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); if (document.execCommand('copy')) { this.showNotification('📋 Prompt copied to clipboard!', 'success'); } else { throw new Error('Copy command failed'); } document.body.removeChild(textArea); } } catch (error) { console.error('Copy to clipboard failed:', error); this.showNotification('❌ Failed to copy prompt to clipboard', 'error'); // Show a fallback modal with the text for manual copying this.showCopyFallbackModal(promptText); } } showCopyFallbackModal(text) { // Create a temporary modal for manual copy const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; modal.innerHTML = `

Copy Prompt Text

Please manually copy the text below:

`; document.body.appendChild(modal); document.body.style.overflow = "hidden"; // Auto-select the text in the textarea const textarea = modal.querySelector('textarea'); textarea.focus(); textarea.select(); // Close modal when clicking outside modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); document.body.style.overflow = ""; } }); // Also handle the close button modal.querySelector('button').addEventListener('click', () => { document.body.style.overflow = ""; }); } // Scan functionality showScanModal() { // Reset progress display document.getElementById("scanProgress").classList.add("hidden"); document.getElementById("startScan").disabled = false; document.getElementById("startScan").textContent = "Start Scan"; this.showModal("scanModal"); } async quickBackup() { try { const response = await fetch("/prompt_manager/backup"); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `prompts-backup-${new Date().toISOString().split("T")[0]}.db`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); this.showNotification("Database backup created successfully!", "success"); } else { throw new Error("Backup failed"); } } catch (error) { this.showNotification("Failed to create backup", "error"); } } async startScan() { // Show progress section document.getElementById("scanProgress").classList.remove("hidden"); document.getElementById("startScan").disabled = true; document.getElementById("startScan").textContent = "Scanning..."; // Reset progress this.updateScanProgress(0, "Initializing scan...", 0, 0); try { const response = await fetch("/prompt_manager/scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Handle streaming response const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.trim().startsWith('data: ')) { try { const data = JSON.parse(line.substring(6)); if (data.type === 'progress') { this.updateScanProgress( data.progress, data.status, data.processed, data.found ); } else if (data.type === 'complete') { this.completeScan(data.processed, data.found, data.added, data.linked); return; } else if (data.type === 'error') { throw new Error(data.message); } } catch (e) { console.log('Non-JSON line:', line); } } } } } catch (error) { this.showNotification(`Scan failed: ${error.message}`, "error"); document.getElementById("startScan").disabled = false; document.getElementById("startScan").textContent = "Start Scan"; } } updateScanProgress(progress, status, processed, found) { document.getElementById("scanProgressBar").style.width = `${progress}%`; document.getElementById("scanStatusText").textContent = status; document.getElementById("scanCount").textContent = `${processed} files processed`; document.getElementById("scanFound").textContent = `${found} prompts found`; } completeScan(processed, found, added, linked = 0) { this.updateScanProgress(100, "Scan completed!", processed, found); document.getElementById("startScan").disabled = false; document.getElementById("startScan").textContent = "Start New Scan"; // Show detailed notification with all counts const linkedText = linked > 0 ? `, linked ${linked} images to existing prompts` : ''; this.showNotification( `Scan completed! Processed ${processed} files, found ${found} prompts, added ${added} new prompts to database${linkedText}.`, "success" ); // Auto-close modal after a short delay to let user see the completion message setTimeout(() => { this.hideModal("scanModal"); // Refresh the statistics immediately this.loadStatistics(); // Also reload the page after a short delay to ensure everything is fully updated setTimeout(() => { window.location.reload(); }, 1000); }, 2000); } // Logs functionality showLogsModal() { this.showModal("logsModal"); this.loadLogStats(); this.loadLogFiles(); this.loadLogConfig(); } closeLogs() { this.hideModal("logsModal"); } async loadLogStats() { try { const response = await fetch('/prompt_manager/logs/stats'); const data = await response.json(); if (data.success) { const stats = data.stats; document.getElementById("logStatsTotal").textContent = stats.buffer_count || 0; document.getElementById("logStatsErrors").textContent = stats.level_counts?.ERROR || 0; document.getElementById("logStatsWarnings").textContent = stats.level_counts?.WARNING || 0; document.getElementById("logStatsLevel").textContent = stats.current_level || "INFO"; document.getElementById("logStatsSize").textContent = this.formatBytes(stats.total_log_size || 0); } } catch (error) { console.error("Failed to load log stats:", error); } } async loadLogConfig() { try { const response = await fetch('/prompt_manager/logs/config'); const data = await response.json(); if (data.success) { const config = data.config; document.getElementById("setLogLevel").value = config.level || "INFO"; document.getElementById("consoleLogging").checked = config.console_logging !== false; } } catch (error) { console.error("Failed to load log config:", error); } } async loadLogFiles() { try { const response = await fetch('/prompt_manager/logs/files'); const data = await response.json(); if (data.success) { const container = document.getElementById("logFilesList"); if (data.files.length === 0) { container.innerHTML = '
No log files found
'; return; } container.innerHTML = data.files.map(file => `
${file.filename} ${file.is_main ? 'Active' : ''}
Size: ${this.formatBytes(file.size)} | Modified: ${new Date(file.modified).toLocaleString()}
`).join(''); } } catch (error) { console.error("Failed to load log files:", error); } } async refreshLogs() { const level = document.getElementById("logLevel").value; const limit = parseInt(document.getElementById("logLimit").value); try { const url = new URL('/prompt_manager/logs', window.location.origin); if (level) url.searchParams.set('level', level); url.searchParams.set('limit', limit.toString()); const response = await fetch(url); const data = await response.json(); if (data.success) { this.displayLogs(data.logs); this.showNotification(`Loaded ${data.logs.length} log entries`, "success"); } else { this.showNotification(`Failed to load logs: ${data.error}`, "error"); } } catch (error) { console.error("Failed to refresh logs:", error); this.showNotification("Failed to refresh logs", "error"); } } displayLogs(logs) { const container = document.getElementById("logsContainer"); if (logs.length === 0) { container.innerHTML = '
No logs found
'; return; } container.innerHTML = logs.map(log => { const levelColors = { DEBUG: 'text-pm-secondary', INFO: 'text-pm-accent', WARNING: 'text-pm-warning', ERROR: 'text-pm-error', CRITICAL: 'text-pm-error' }; const levelColor = levelColors[log.level] || 'text-pm-secondary'; const timestamp = new Date(log.timestamp).toLocaleString(); return `
${timestamp.split(' ')[1]} ${log.level} ${log.logger} ${log.message}
${log.filename}:${log.lineno}
`; }).join(''); // Auto-scroll to bottom container.scrollTop = container.scrollHeight; } async downloadLogs() { const level = document.getElementById("logLevel").value; const limit = parseInt(document.getElementById("logLimit").value); try { const url = new URL('/prompt_manager/logs', window.location.origin); if (level) url.searchParams.set('level', level); url.searchParams.set('limit', limit.toString()); const response = await fetch(url); const data = await response.json(); if (data.success) { const logText = data.logs.map(log => `${log.timestamp} [${log.level}] ${log.logger}: ${log.message} (${log.filename}:${log.lineno})` ).join('\n'); const blob = new Blob([logText], { type: 'text/plain' }); const url2 = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url2; a.download = `prompt_manager_logs_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url2); this.showNotification("Logs downloaded successfully", "success"); } else { this.showNotification(`Failed to download logs: ${data.error}`, "error"); } } catch (error) { console.error("Failed to download logs:", error); this.showNotification("Failed to download logs", "error"); } } async downloadLogFile(filename) { try { const response = await fetch(`/prompt_manager/logs/download/${encodeURIComponent(filename)}`); if (response.ok) { const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showNotification(`Downloaded ${filename}`, "success"); } else { this.showNotification(`Failed to download ${filename}`, "error"); } } catch (error) { console.error("Failed to download log file:", error); this.showNotification("Failed to download log file", "error"); } } async clearLogs() { if (!confirm("Are you sure you want to clear all log files? This cannot be undone.")) { return; } try { const response = await fetch('/prompt_manager/logs/truncate', { method: 'POST' }); const data = await response.json(); if (data.success) { this.showNotification(data.message, "success"); this.refreshLogs(); this.loadLogStats(); this.loadLogFiles(); } else { this.showNotification(`Failed to clear logs: ${data.error}`, "error"); } } catch (error) { console.error("Failed to clear logs:", error); this.showNotification("Failed to clear logs", "error"); } } async updateLogConfig() { const level = document.getElementById("setLogLevel").value; const consoleLogging = document.getElementById("consoleLogging").checked; try { const response = await fetch('/prompt_manager/logs/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level: level, console_logging: consoleLogging }) }); const data = await response.json(); if (data.success) { this.showNotification("Log configuration updated", "success"); this.loadLogStats(); } else { this.showNotification(`Failed to update config: ${data.error}`, "error"); } } catch (error) { console.error("Failed to update log config:", error); this.showNotification("Failed to update log configuration", "error"); } } formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Pagination methods updatePaginationControls() { const { currentPage, totalPages, total, limit } = this.pagination; // Update pagination info document.getElementById("currentPage").textContent = currentPage; document.getElementById("totalPages").textContent = totalPages; document.getElementById("totalResults").textContent = total; // Calculate showing range const start = ((currentPage - 1) * limit) + 1; const end = Math.min(currentPage * limit, total); document.getElementById("showingStart").textContent = start; document.getElementById("showingEnd").textContent = end; // Update results count document.getElementById("resultsCount").textContent = `${this.prompts.length} of ${total}`; // Update button states document.getElementById("firstPageBtn").disabled = currentPage === 1; document.getElementById("prevPageBtn").disabled = currentPage === 1; document.getElementById("nextPageBtn").disabled = currentPage === totalPages; document.getElementById("lastPageBtn").disabled = currentPage === totalPages; } async goToPage(page) { if (page < 1 || page > this.pagination.totalPages || page === this.pagination.currentPage) { return; } await this.loadRecentPrompts(page); // Scroll to top of results after page loads window.scrollTo({ top: 0, behavior: 'smooth' }); } async changeLimit(newLimit) { this.pagination.limit = newLimit; this.pagination.currentPage = 1; // Reset to first page await this.loadRecentPrompts(1); } // Metadata functionality openMetadataViewer() { window.open('/prompt_manager/gallery.html', '_blank'); } // Gallery functionality openGallery() { window.open('/prompt_manager/gallery', '_blank'); } async parsePNGMetadata(arrayBuffer) { const dataView = new DataView(arrayBuffer); let offset = 8; // Skip PNG signature const metadata = {}; let chunkCount = 0; console.log('Starting PNG metadata parsing...'); while (offset < arrayBuffer.byteLength - 8) { const length = dataView.getUint32(offset); const type = new TextDecoder().decode(arrayBuffer.slice(offset + 4, offset + 8)); chunkCount++; console.log(`Chunk ${chunkCount}: type=${type}, length=${length}`); if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') { const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + length); let text; if (type === 'tEXt') { text = new TextDecoder().decode(chunkData); } else if (type === 'iTXt') { // iTXt format: keyword\0compression\0language\0translated_keyword\0text const textData = new TextDecoder().decode(chunkData); const parts = textData.split('\0'); console.log(`iTXt parts count: ${parts.length}, first part: ${parts[0]}`); if (parts.length >= 5) { metadata[parts[0]] = parts[4]; } text = textData; } else if (type === 'zTXt') { // zTXt is compressed - basic parsing (might need proper decompression) text = new TextDecoder().decode(chunkData); } // Parse the text chunk for key-value pairs const nullIndex = text.indexOf('\0'); if (nullIndex !== -1) { const key = text.substring(0, nullIndex); const value = text.substring(nullIndex + 1); console.log(`Found metadata: ${key} = ${value.substring(0, 100)}...`); metadata[key] = value; } } offset += 8 + length + 4; // Move to next chunk (8 = length + type, 4 = CRC) } console.log(`Parsed ${chunkCount} chunks, found ${Object.keys(metadata).length} metadata items`); return metadata; } extractComfyUIData(metadata) { // Look for ComfyUI workflow data in various possible fields let workflowData = null; let promptData = null; // Common ComfyUI metadata field names const workflowFields = ['workflow', 'Workflow', 'comfy', 'ComfyUI']; const promptFields = ['prompt', 'Prompt', 'parameters', 'Parameters']; for (const field of workflowFields) { if (metadata[field]) { try { // Clean NaN values from JSON string before parsing let cleanedJson = metadata[field]; cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null'); cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null'); workflowData = JSON.parse(cleanedJson); console.log(`Successfully parsed workflow field: ${field}`); break; } catch (e) { console.log('Failed to parse workflow field:', field, e.message); } } } for (const field of promptFields) { if (metadata[field]) { try { // Clean NaN values from JSON string before parsing let cleanedJson = metadata[field]; cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null'); cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null'); promptData = JSON.parse(cleanedJson); console.log(`Successfully parsed prompt field: ${field}`); break; } catch (e) { console.log('Failed to parse prompt field:', field, e.message); console.log('Raw data:', metadata[field].substring(0, 200) + '...'); } } } return { workflow: workflowData, prompt: promptData }; } updateMetadataPanel(comfyData, imageSrc) { const metadataContent = document.getElementById('metadataContent'); if (!metadataContent) return; // Get the actual file path from current image data let filePath = imageSrc; // fallback to URL if (this.currentGalleryImages && this.currentImageIndex !== null && this.currentGalleryImages[this.currentImageIndex]) { const currentImage = this.currentGalleryImages[this.currentImageIndex]; filePath = currentImage.image_path || currentImage.filename || imageSrc; } // Extract information from ComfyUI workflow let checkpoint = 'Unknown'; let positivePrompt = 'No prompt found'; let negativePrompt = 'No negative prompt found'; let steps = 'Unknown'; let cfgScale = 'Unknown'; let sampler = 'Unknown'; let seed = 'Unknown'; // Parse prompt data first (more reliable for actual generation parameters) if (comfyData.prompt) { console.log('Parsing prompt data...', Object.keys(comfyData.prompt).length, 'nodes'); const promptNodes = comfyData.prompt; for (const nodeId in promptNodes) { const node = promptNodes[nodeId]; console.log(`Node ${nodeId}:`, node.class_type, node); // Checkpoint if (node.class_type === 'CheckpointLoaderSimple' && node.inputs) { checkpoint = node.inputs.ckpt_name || checkpoint; console.log('Found checkpoint:', checkpoint); } // Prompts - need to identify which is positive vs negative if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) { // PromptManager typically contains the positive prompt const promptValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); positivePrompt = promptValue; console.log('Found PromptManager with text:', promptValue.substring(0, 100)); } if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) { // Check if this looks like a negative prompt const textValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); console.log('Found CLIPTextEncode:', textValue.substring(0, 50)); const text = textValue.toLowerCase(); if (text.includes('bad anatomy') || text.includes('unfinished') || text.includes('censored') || text.includes('weird anatomy') || text.includes('negative') || text.includes('embedding:')) { negativePrompt = textValue; console.log('Found negative prompt:', textValue.substring(0, 100)); } else if (positivePrompt === 'No prompt found') { // If we haven't found a positive prompt yet, this might be it positivePrompt = textValue; console.log('Found potential positive prompt:', textValue.substring(0, 100)); } } // Sampling parameters if (node.class_type === 'KSampler' && node.inputs) { seed = node.inputs.seed || seed; steps = node.inputs.steps || steps; cfgScale = node.inputs.cfg || cfgScale; sampler = node.inputs.sampler_name || sampler; console.log('Found sampler params:', {seed, steps, cfgScale, sampler}); } } } else { console.log('No prompt data found, will try workflow fallback'); } // Store the current metadata for copying this.currentMetadata = { positivePrompt, negativePrompt, checkpoint, steps, cfgScale, sampler, seed, workflow: comfyData.workflow, prompt: comfyData.prompt }; // Update the HTML metadataContent.innerHTML = `

File Path

${filePath}

Resources used

${checkpoint}
ComfyUI Generated
CHECKPOINT

Prompt

COMFYUI
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
${positivePrompt.length > 200 ? '' : ''}

Negative prompt

${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
${negativePrompt.length > 200 ? '' : ''}

Other metadata

CFG SCALE: ${cfgScale} STEPS: ${steps} SAMPLER: ${sampler}
SEED: ${seed}

ComfyUI Workflow

`; } showMetadataError() { const metadataContent = document.getElementById('metadataContent'); if (!metadataContent) return; metadataContent.innerHTML = `

Error Loading Metadata

Could not extract ComfyUI metadata from this image

`; } async copyPrompt(type) { if (!this.currentMetadata) { this.showNotification('❌ No metadata available for copying', 'error'); return; } const text = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt; if (!text || text === 'No prompt found' || text === 'No negative prompt found') { this.showNotification(`❌ No ${type} prompt available`, 'error'); return; } await this.copyToClipboard(text); } async tryFallbackMetadata(imageSrc) { console.log('Trying fallback metadata extraction for:', imageSrc); try { // Try to get prompt from current gallery image data let fallbackPrompt = 'No prompt found'; if (this.currentGalleryImages && this.currentImageIndex !== null && this.currentGalleryImages[this.currentImageIndex]) { const currentImage = this.currentGalleryImages[this.currentImageIndex]; console.log('Current image data:', currentImage); // If we have prompt_id, try to get the prompt from our local prompts array if (currentImage.prompt_id && this.prompts) { const prompt = this.prompts.find(p => p.id === currentImage.prompt_id); if (prompt) { fallbackPrompt = prompt.text; console.log('Found prompt from database:', fallbackPrompt.substring(0, 100)); } } } // Set fallback metadata this.currentMetadata = { positivePrompt: fallbackPrompt, negativePrompt: 'No negative prompt found', checkpoint: 'Unknown', steps: 'Unknown', cfgScale: 'Unknown', sampler: 'Unknown', seed: 'Unknown', workflow: null, prompt: null }; // Update metadata panel with fallback data this.updateMetadataPanel({}, imageSrc); } catch (error) { console.error('Fallback metadata extraction failed:', error); this.showMetadataError(); // Set empty metadata as last resort this.currentMetadata = { positivePrompt: 'No prompt found', negativePrompt: 'No negative prompt found', checkpoint: 'Unknown', steps: 'Unknown', cfgScale: 'Unknown', sampler: 'Unknown', seed: 'Unknown', workflow: null, prompt: null }; } } async copyAllMetadata() { if (!this.currentMetadata) return; const allData = `Checkpoint: ${this.currentMetadata.checkpoint || 'Unknown'} Positive Prompt: ${this.currentMetadata.positivePrompt} Negative Prompt: ${this.currentMetadata.negativePrompt} Steps: ${this.currentMetadata.steps || 'Unknown'} CFG Scale: ${this.currentMetadata.cfgScale || 'Unknown'} Sampler: ${this.currentMetadata.sampler || 'Unknown'} Seed: ${this.currentMetadata.seed || 'Unknown'}`; await this.copyToClipboard(allData); } showFullPrompt(type) { if (!this.currentMetadata) return; const prompt = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt; const safeType = this.escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); const newWindow = window.open('', '_blank'); const doc = newWindow.document; doc.open(); doc.write('' + safeType + ' Prompt'); doc.close(); doc.body.style.cssText = 'background:#111;color:#fff;font-family:monospace;padding:20px;'; const h2 = doc.createElement('h2'); h2.textContent = type.charAt(0).toUpperCase() + type.slice(1) + ' Prompt'; doc.body.appendChild(h2); const pre = doc.createElement('pre'); pre.style.cssText = 'background:#222;padding:15px;border-radius:5px;white-space:pre-wrap;line-height:1.5;'; pre.textContent = prompt; doc.body.appendChild(pre); const btn = doc.createElement('button'); btn.textContent = 'Copy to Clipboard'; btn.style.cssText = 'margin-top:20px;padding:10px 20px;background:#444;color:#fff;border:none;border-radius:5px;cursor:pointer;'; btn.addEventListener('click', () => { navigator.clipboard.writeText(pre.textContent).then(() => alert('Copied!')); }); doc.body.appendChild(btn); } showWorkflowData() { if (!this.currentMetadata || !this.currentMetadata.workflow) return; const newWindow = window.open('', '_blank'); newWindow.document.write(` ComfyUI Workflow Data

ComfyUI Workflow JSON

${JSON.stringify(this.currentMetadata.workflow, null, 2)}
`); } downloadWorkflowJSON() { if (!this.currentMetadata || !this.currentMetadata.workflow) return; const dataStr = JSON.stringify(this.currentMetadata.workflow, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'comfyui_workflow.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.showNotification('Copied to clipboard!', 'success'); } catch (err) { console.error('Copy failed:', err); this.showNotification('Copy failed', 'error'); } } async parsePNGMetadata(arrayBuffer) { const dataView = new DataView(arrayBuffer); let offset = 8; // Skip PNG signature const metadata = {}; while (offset < arrayBuffer.byteLength - 8) { const length = dataView.getUint32(offset); const type = new TextDecoder().decode(arrayBuffer.slice(offset + 4, offset + 8)); if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') { const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + length); let text; if (type === 'tEXt') { text = new TextDecoder().decode(chunkData); } else if (type === 'iTXt') { // iTXt format: keyword\0compression\0language\0translated_keyword\0text const textData = new TextDecoder().decode(chunkData); const parts = textData.split('\0'); if (parts.length >= 5) { metadata[parts[0]] = parts[4]; } text = textData; } else if (type === 'zTXt') { // zTXt is compressed - basic parsing (might need proper decompression) text = new TextDecoder().decode(chunkData); } // Parse the text chunk for key-value pairs const nullIndex = text.indexOf('\0'); if (nullIndex !== -1) { const key = text.substring(0, nullIndex); const value = text.substring(nullIndex + 1); metadata[key] = value; } } offset += 8 + length + 4; // Move to next chunk (8 = length + type, 4 = CRC) } return metadata; } extractComfyUIData(metadata) { // Look for ComfyUI workflow data in various possible fields let workflowData = null; let promptData = null; // Common ComfyUI metadata field names const workflowFields = ['workflow', 'Workflow', 'comfy', 'ComfyUI']; const promptFields = ['prompt', 'Prompt', 'parameters', 'Parameters']; for (const field of workflowFields) { if (metadata[field]) { try { // Clean NaN values from JSON string before parsing let cleanedJson = metadata[field]; cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null'); cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null'); workflowData = JSON.parse(cleanedJson); break; } catch (e) { console.log('Failed to parse workflow field:', field); } } } for (const field of promptFields) { if (metadata[field]) { try { // Clean NaN values from JSON string before parsing let cleanedJson = metadata[field]; cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null'); cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null'); promptData = JSON.parse(cleanedJson); break; } catch (e) { console.log('Failed to parse prompt field:', field); } } } return { workflow: workflowData, prompt: promptData }; } async extractImageMetadata(imageUrl) { try { // Fetch the image as an array buffer const response = await fetch(imageUrl); const arrayBuffer = await response.arrayBuffer(); // Parse PNG metadata const metadata = await this.parsePNGMetadata(arrayBuffer); const comfyData = this.extractComfyUIData(metadata); return this.parseWorkflowData(comfyData, imageUrl); } catch (error) { console.error('Error extracting metadata:', error); return null; } } parseWorkflowData(comfyData, imagePath) { // Extract information from ComfyUI workflow let checkpoint = 'Unknown'; let positivePrompt = 'No prompt found'; let negativePrompt = 'No negative prompt found'; let steps = 'Unknown'; let cfgScale = 'Unknown'; let sampler = 'Unknown'; let seed = 'Unknown'; // Parse prompt data first (more reliable for actual generation parameters) if (comfyData.prompt) { const promptNodes = comfyData.prompt; for (const nodeId in promptNodes) { const node = promptNodes[nodeId]; // Checkpoint if (node.class_type === 'CheckpointLoaderSimple' && node.inputs) { checkpoint = node.inputs.ckpt_name || checkpoint; } // Prompts - need to identify which is positive vs negative if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) { // PromptManager typically contains the positive prompt const promptValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); positivePrompt = promptValue; } if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) { // Check if this looks like a negative prompt const textValue = typeof node.inputs.text === 'string' ? node.inputs.text : (Array.isArray(node.inputs.text) ? node.inputs.text[0] : String(node.inputs.text)); const text = textValue.toLowerCase(); if (text.includes('bad anatomy') || text.includes('unfinished') || text.includes('censored') || text.includes('weird anatomy') || text.includes('negative') || text.includes('embedding:')) { negativePrompt = textValue; } else if (positivePrompt === 'No prompt found') { // If we haven't found a positive prompt yet, this might be it positivePrompt = textValue; } } // Sampling parameters if (node.class_type === 'KSampler' && node.inputs) { seed = node.inputs.seed || seed; steps = node.inputs.steps || steps; cfgScale = node.inputs.cfg || cfgScale; sampler = node.inputs.sampler_name || sampler; } } } // Parse workflow data (this is what we actually have in your case) if (comfyData.workflow && comfyData.workflow.nodes) { const nodes = comfyData.workflow.nodes; console.log('DEBUG: Total nodes found:', nodes.length); // Debug: Log all nodes to understand the structure nodes.forEach((node, index) => { console.log(`Node ${index} (ID: ${node.id}):`, { type: node.type, title: node.title, widgets_values: node.widgets_values }); }); // Look for checkpoint loader const checkpointNode = nodes.find(node => node.type === 'CheckpointLoaderSimple' || node.type === 'CheckpointLoader' ); if (checkpointNode && checkpointNode.widgets_values && checkpointNode.widgets_values[0]) { checkpoint = checkpointNode.widgets_values[0]; } // Look for prompts in workflow nodes const textEncodeNodes = nodes.filter(node => node.type === 'CLIPTextEncode'); const promptManagerNodes = nodes.filter(node => node.type === 'PromptManager'); // Check PromptManager first for positive prompt if (promptManagerNodes.length > 0 && promptManagerNodes[0].widgets_values && promptManagerNodes[0].widgets_values[0]) { positivePrompt = promptManagerNodes[0].widgets_values[0]; } // For negative prompt, look for CLIPTextEncode that contains negative keywords for (const node of textEncodeNodes) { if (node.widgets_values && node.widgets_values[0]) { const text = node.widgets_values[0].toLowerCase(); if (text.includes('bad anatomy') || text.includes('unfinished') || text.includes('censored') || text.includes('weird anatomy') || text.includes('negative') || text.includes('embedding:')) { negativePrompt = node.widgets_values[0]; break; } } } // Extract generation parameters from their source nodes // Look for specific node types with better fallback logic // CFG Scale - prioritize Float nodes with title 'CFG', then other sources console.log('DEBUG: Looking for CFG scale...'); // First priority: Float node with title 'CFG' const cfgFloatNode = nodes.find(node => node.type === 'Float' && node.title === 'CFG'); if (cfgFloatNode && cfgFloatNode.widgets_values && cfgFloatNode.widgets_values[0]) { cfgScale = cfgFloatNode.widgets_values[0]; console.log(`DEBUG: Found CFG in Float node: ${cfgScale}`); } // If not found, look for specific value of 7 (your target CFG) if (cfgScale === 'Unknown') { for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (value === 7) { // Target the specific value you mentioned cfgScale = value; console.log(`DEBUG: Found target CFG value 7 in node ${node.id} (${node.type}) at index ${i}`); break; } } if (cfgScale !== 'Unknown') break; } } } // Fallback: any reasonable CFG value if (cfgScale === 'Unknown') { for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (typeof value === 'number' && value > 1 && value <= 30) { console.log(`DEBUG: Found potential CFG ${value} in node ${node.id} (${node.type}) at index ${i}`); cfgScale = value; console.log(`DEBUG: Set CFG scale to ${value}`); break; } } if (cfgScale !== 'Unknown') break; } } } // Steps - prioritize Int nodes with title 'Steps', then target value 30 console.log('DEBUG: Looking for steps...'); // First priority: Int node with title 'Steps' const stepsIntNode = nodes.find(node => node.type === 'Int' && node.title === 'Steps'); if (stepsIntNode && stepsIntNode.widgets_values && stepsIntNode.widgets_values[0]) { steps = stepsIntNode.widgets_values[0]; console.log(`DEBUG: Found steps in Int node: ${steps}`); } // If not found, look for specific value of 30 (your target steps) if (steps === 'Unknown') { for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (value === 30) { // Target the specific value you mentioned steps = value; console.log(`DEBUG: Found target steps value 30 in node ${node.id} (${node.type}) at index ${i}`); break; } } if (steps !== 'Unknown') break; } } } // Fallback: reasonable step values, prefer 10-150 range if (steps === 'Unknown') { for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (typeof value === 'number' && value >= 10 && value <= 150) { console.log(`DEBUG: Found good steps value ${value} in node ${node.id} (${node.type}) at index ${i}`); steps = value; console.log(`DEBUG: Set steps to ${value}`); break; } } if (steps !== 'Unknown') break; } } } // Sampler - look for valid ComfyUI samplers console.log('DEBUG: Looking for sampler...'); const validSamplers = [ 'euler', 'euler_ancestral', 'heun', 'dpm_2', 'dpm_2_ancestral', 'lms', 'dpm_fast', 'dpm_adaptive', 'dpmpp_2s_ancestral', 'dpmpp_sde', 'dpmpp_sde_gpu', 'dpmpp_2m', 'dpmpp_2m_sde', 'dpmpp_2m_sde_gpu', 'dpmpp_3m_sde', 'dpmpp_3m_sde_gpu', 'ddim', 'uni_pc', 'uni_pc_bh2' ]; for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (typeof value === 'string') { const samplerValue = value.toLowerCase(); if (validSamplers.some(validSampler => samplerValue.includes(validSampler))) { console.log(`DEBUG: Found sampler ${value} in node ${node.id} (${node.type}) at index ${i}`); if (sampler === 'Unknown') { sampler = value; console.log(`DEBUG: Set sampler to ${value}`); } } } } } } // Seed - look for large numbers that could be seeds console.log('DEBUG: Looking for seed...'); for (const node of nodes) { if (node.widgets_values && Array.isArray(node.widgets_values)) { for (let i = 0; i < node.widgets_values.length; i++) { const value = node.widgets_values[i]; if (typeof value === 'number' && value > 100000) { console.log(`DEBUG: Found potential seed ${value} in node ${node.id} (${node.type}) at index ${i}`); if (seed === 'Unknown') { seed = value; console.log(`DEBUG: Set seed to ${value}`); } } } } } // If we still don't have sampling params, look at KSampler widgets_values directly const ksamplerNode = nodes.find(node => node.type === 'KSampler' || node.type === 'KSamplerAdvanced' ); if (ksamplerNode && ksamplerNode.widgets_values) { const widgets = ksamplerNode.widgets_values; // KSampler widgets_values order: [seed, control_mode, steps, cfg, sampler_name, scheduler, denoise, ...] if (seed === 'Unknown' && widgets[0]) seed = widgets[0]; if (steps === 'Unknown' && widgets[2]) steps = widgets[2]; if (cfgScale === 'Unknown' && widgets[3]) cfgScale = widgets[3]; if (sampler === 'Unknown' && widgets[4]) sampler = widgets[4]; } } return { positivePrompt, negativePrompt, checkpoint, steps, cfgScale, sampler, seed, workflow: comfyData.workflow, prompt: comfyData.prompt, imagePath }; } addMetadataSidebar() { const viewerContainer = document.querySelector('.viewer-container'); if (!viewerContainer || document.getElementById('metadata-sidebar')) return; const sidebar = document.createElement('div'); sidebar.id = 'metadata-sidebar'; sidebar.className = 'metadata-sidebar'; sidebar.innerHTML = `

Generation data

Loading Metadata...

Extracting ComfyUI workflow data

`; viewerContainer.appendChild(sidebar); this.attachMetadataEventListeners(); } async loadMetadataForImage(imgElement) { const imageUrl = imgElement.getAttribute('data-original') || imgElement.src; if (!imageUrl) return; const metadataContent = document.getElementById('metadata-content'); if (!metadataContent) return; // Show loading state metadataContent.innerHTML = `

Loading Metadata...

Extracting ComfyUI workflow data

`; try { const metadata = await this.extractImageMetadata(imageUrl); this.currentMetadata = metadata; this.updateMetadataSidebar(metadata); } catch (error) { console.error('Failed to load metadata:', error); metadataContent.innerHTML = `

Failed to Load Metadata

Could not extract ComfyUI workflow data

`; } } updateMetadataSidebar(metadata) { const metadataContent = document.getElementById('metadata-content'); if (!metadataContent || !metadata) return; metadataContent.innerHTML = `

File Path

${metadata.imagePath}

Resources used

${metadata.checkpoint}
ComfyUI Generated
CHECKPOINT

Prompt

COMFYUI
${metadata.positivePrompt.substring(0, 200)}${metadata.positivePrompt.length > 200 ? '...' : ''}
${metadata.positivePrompt.length > 200 ? '' : ''}

Negative prompt

${metadata.negativePrompt.substring(0, 200)}${metadata.negativePrompt.length > 200 ? '...' : ''}
${metadata.negativePrompt.length > 200 ? '' : ''}

Other metadata

CFG SCALE: ${metadata.cfgScale} STEPS: ${metadata.steps} SAMPLER: ${metadata.sampler}
SEED: ${metadata.seed}

ComfyUI Workflow

`; // Re-attach event listeners after updating content this.attachMetadataEventListeners(); } attachMetadataEventListeners() { const sidebar = document.getElementById('metadata-sidebar'); if (!sidebar) return; // Copy buttons const copyBtns = sidebar.querySelectorAll('[data-copy-type]'); copyBtns.forEach(btn => { btn.addEventListener('click', (e) => { const type = btn.getAttribute('data-copy-type'); if (this.currentMetadata) { const text = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt; this.copyToClipboard(text); } }); }); // File path copy const pathEl = sidebar.querySelector('[data-copy-path]'); if (pathEl) { pathEl.addEventListener('click', () => { if (this.currentMetadata) { this.copyToClipboard(this.currentMetadata.imagePath); } }); } // Show more buttons const showMoreBtns = sidebar.querySelectorAll('[data-show-type]'); showMoreBtns.forEach(btn => { btn.addEventListener('click', (e) => { const type = btn.getAttribute('data-show-type'); this.showFullPrompt(type); }); }); // Workflow action buttons const actionBtns = sidebar.querySelectorAll('[data-action]'); actionBtns.forEach(btn => { btn.addEventListener('click', (e) => { const action = btn.getAttribute('data-action'); if (action === 'show-workflow') { this.showWorkflowData(); } else if (action === 'download-workflow') { this.downloadWorkflowJSON(); } }); }); // Copy all button const copyAllBtn = sidebar.querySelector('.metadata-copy-all'); if (copyAllBtn) { copyAllBtn.addEventListener('click', () => { this.copyAllMetadata(); }); } } showFullPrompt(type) { if (this.currentMetadata) { const prompt = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt; const safeType = this.escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); const newWindow = window.open('', '_blank'); const doc = newWindow.document; doc.open(); doc.write('' + safeType + ' Prompt'); doc.close(); doc.body.style.cssText = 'background:#111;color:#fff;font-family:monospace;padding:20px;'; const h2 = doc.createElement('h2'); h2.textContent = type.charAt(0).toUpperCase() + type.slice(1) + ' Prompt'; doc.body.appendChild(h2); const pre = doc.createElement('pre'); pre.style.cssText = 'background:#222;padding:15px;border-radius:5px;white-space:pre-wrap;line-height:1.5;'; pre.textContent = prompt; doc.body.appendChild(pre); const btn = doc.createElement('button'); btn.textContent = 'Copy to Clipboard'; btn.style.cssText = 'margin-top:20px;padding:10px 20px;background:#444;color:#fff;border:none;border-radius:5px;cursor:pointer;'; btn.addEventListener('click', () => { navigator.clipboard.writeText(pre.textContent).then(() => alert('Copied!')); }); doc.body.appendChild(btn); } } showWorkflowData() { if (this.currentMetadata && this.currentMetadata.workflow) { const newWindow = window.open('', '_blank'); newWindow.document.write(` ComfyUI Workflow Data

ComfyUI Workflow JSON

${JSON.stringify(this.currentMetadata.workflow, null, 2)}
`); } } downloadWorkflowJSON() { if (this.currentMetadata && this.currentMetadata.workflow) { const dataStr = JSON.stringify(this.currentMetadata.workflow, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'comfyui_workflow.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } } copyAllMetadata() { if (!this.currentMetadata) return; const allData = `Checkpoint: ${this.currentMetadata.checkpoint || 'Unknown'} Positive Prompt: ${this.currentMetadata.positivePrompt} Negative Prompt: ${this.currentMetadata.negativePrompt} Steps: ${this.currentMetadata.steps || 'Unknown'} CFG Scale: ${this.currentMetadata.cfgScale || 'Unknown'} Sampler: ${this.currentMetadata.sampler || 'Unknown'} Seed: ${this.currentMetadata.seed || 'Unknown'}`; this.copyToClipboard(allData); } // ========================================== // Auto Tag Feature Methods // ========================================== // State for AutoTag feature autoTagState = { eventSource: null, downloadEventSource: null, reviewImages: [], reviewIndex: 0, currentTags: [], cancelled: false, skipAllTagged: false, retagResolve: null, // Promise resolver for retag confirmation tagsExpanded: false // Track accordion state }; // ==================== Add Prompt Modal ==================== showAddPromptModal() { // Clear form document.getElementById('addPromptText').value = ''; document.getElementById('addPromptCategory').value = ''; document.getElementById('addPromptRatingValue').value = ''; document.getElementById('addPromptNotes').value = ''; document.getElementById('addPromptProtected').checked = true; // Clear tags this.addPromptTags = []; this.renderAddPromptTags(); // Reset rating stars this.updateAddPromptRatingStars(0); // Populate categories datalist this.populateAddPromptCategories(); this.showModal('addPromptModal'); } setupAddPromptModal() { this.addPromptTags = []; // Rating stars const ratingContainer = document.getElementById('addPromptRating'); const ratingStars = ratingContainer.querySelectorAll('.rating-star'); ratingStars.forEach(star => { star.addEventListener('click', () => { const rating = parseInt(star.dataset.rating); document.getElementById('addPromptRatingValue').value = rating; this.updateAddPromptRatingStars(rating); }); }); // Clear rating button document.getElementById('clearAddPromptRatingBtn').addEventListener('click', () => { document.getElementById('addPromptRatingValue').value = ''; this.updateAddPromptRatingStars(0); }); // Tag input const tagInput = document.getElementById('addPromptTagInput'); tagInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); const tag = tagInput.value.trim().replace(/,/g, ''); if (tag && !this.addPromptTags.includes(tag)) { this.addPromptTags.push(tag); this.renderAddPromptTags(); } tagInput.value = ''; } }); // Tag suggestions tagInput.addEventListener('input', () => { this.showAddPromptTagSuggestions(tagInput.value); }); } updateAddPromptRatingStars(rating) { const stars = document.querySelectorAll('#addPromptRating .rating-star'); stars.forEach((star, index) => { if (index < rating) { star.textContent = '★'; star.classList.add('text-pm-warning'); star.classList.remove('text-pm-muted'); } else { star.textContent = '☆'; star.classList.remove('text-pm-warning'); star.classList.add('text-pm-muted'); } }); } renderAddPromptTags() { const container = document.getElementById('addPromptTagsContainer'); const input = document.getElementById('addPromptTagInput'); // Remove existing tag chips container.querySelectorAll('.tag-chip').forEach(chip => chip.remove()); // Add tag chips this.addPromptTags.forEach(tag => { const chip = document.createElement('span'); chip.className = 'tag-chip inline-flex items-center px-2 py-1 bg-pm-accent text-pm text-xs rounded cursor-pointer hover:bg-pm-accent-hover'; chip.innerHTML = `${tag} ×`; chip.addEventListener('click', () => { this.addPromptTags = this.addPromptTags.filter(t => t !== tag); this.renderAddPromptTags(); }); container.insertBefore(chip, input); }); } showAddPromptTagSuggestions(query) { const suggestionsContainer = document.getElementById('addPromptTagSuggestions'); if (!query) { suggestionsContainer.classList.add('hidden'); return; } const matchingTags = this.tags.filter(tag => tag.toLowerCase().includes(query.toLowerCase()) && !this.addPromptTags.includes(tag) ).slice(0, 10); if (matchingTags.length === 0) { suggestionsContainer.classList.add('hidden'); return; } suggestionsContainer.innerHTML = matchingTags.map(tag => `
${tag}
`).join(''); suggestionsContainer.querySelectorAll('[data-tag]').forEach(el => { el.addEventListener('click', () => { const tag = el.dataset.tag; if (!this.addPromptTags.includes(tag)) { this.addPromptTags.push(tag); this.renderAddPromptTags(); } document.getElementById('addPromptTagInput').value = ''; suggestionsContainer.classList.add('hidden'); }); }); suggestionsContainer.classList.remove('hidden'); } populateAddPromptCategories() { const datalist = document.getElementById('addPromptCategoryList'); datalist.innerHTML = this.categories.map(cat => `