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)}
`;
}
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 ? '#fbbf24' : '#9ca3af'; // 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 = '#fcd34d'; // bright yellow on hover
} else {
btn.textContent = '☆';
btn.style.color = '#9ca3af'; // gray
}
});
});
starBtn.addEventListener('mouseleave', () => {
// Reset to current rating
starButtons.forEach((btn, i) => {
if (i < currentRating) {
btn.textContent = '⭐';
btn.style.color = '#fbbf24'; // yellow
} else {
btn.textContent = '☆';
btn.style.color = '#9ca3af'; // 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-6 py-4 rounded-lg shadow-lg ${zIndex} transition-all duration-300 transform translate-x-full`;
const colors = {
success: "bg-green-600 text-white",
error: "bg-red-600 text-white",
warning: "bg-yellow-600 text-white",
info: "bg-blue-600 text-white"
};
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-gray-700", "border", "border-blue-500", "rounded-lg", "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-gray-700", "border", "border-blue-500", "rounded-lg", "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-gray-700", "border", "border-blue-500", "rounded-lg", "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) => `
${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-green-900/20 border-green-600' : 'bg-red-900/20 border-red-600';
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 += `
`;
result.issues.forEach(issue => {
html += `- ${issue}
`;
});
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 = `
`;
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-green-400' : (result.status === 'warning' ? 'text-yellow-400' : 'text-red-400');
html += `
${this.escapeHtml(category)}
${status}
`;
}
html += '
';
// Detailed results
for (const [category, result] of Object.entries(diagnostics)) {
const bgColor = result.status === 'ok' ? 'bg-green-900/20 border-green-600' :
(result.status === 'warning' ? 'bg-yellow-900/20 border-yellow-600' : 'bg-red-900/20 border-red-600');
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-gray-400',
INFO: 'text-blue-400',
WARNING: 'text-yellow-400',
ERROR: 'text-red-400',
CRITICAL: 'text-red-600'
};
const levelColor = levelColors[log.level] || 'text-gray-400';
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 = `
Resources used
${checkpoint}
ComfyUI Generated
CHECKPOINT
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
${positivePrompt.length > 200 ? '
' : ''}
${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
${negativePrompt.length > 200 ? '
' : ''}
Other metadata
CFG SCALE: ${cfgScale}
STEPS: ${steps}
SAMPLER: ${sampler}
SEED: ${seed}
`;
}
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 safePrompt = this.escapeHtml(prompt);
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 = `
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 = `
`;
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
${metadata.positivePrompt.substring(0, 200)}${metadata.positivePrompt.length > 200 ? '...' : ''}
${metadata.positivePrompt.length > 200 ? '
' : ''}
${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}
`;
// 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-yellow-400');
star.classList.remove('text-gray-500');
} else {
star.textContent = '☆';
star.classList.remove('text-yellow-400');
star.classList.add('text-gray-500');
}
});
}
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-blue-600 text-white text-xs rounded cursor-pointer hover:bg-blue-700';
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 => `