diff --git a/web/gemini_prompt_ui.js b/web/gemini_prompt_ui.js index 444dac0..29227ed 100644 --- a/web/gemini_prompt_ui.js +++ b/web/gemini_prompt_ui.js @@ -138,8 +138,16 @@ app.registerExtension({ const result = onExecuted?.apply(this, arguments); if (this.status) { + // Remember the user's color only if no flash is already pending, + // otherwise we would capture our own temporary flash color + if (this.geminiFlashTimeout) { + clearTimeout(this.geminiFlashTimeout); + } else { + this.geminiPrevBgcolor = this.bgcolor; + } + // Check if there was an error in the output - const outputs = message.output; + const outputs = message?.output; if (outputs && outputs.prompt && outputs.prompt[0] && outputs.prompt[0].startsWith("Error:")) { this.status.value = "Error - Check output"; this.bgcolor = "#552222"; @@ -148,17 +156,30 @@ app.registerExtension({ this.bgcolor = "#225522"; } - // Reset color after delay - setTimeout(() => { - this.bgcolor = ""; + // Restore the previous color after delay + this.geminiFlashTimeout = setTimeout(() => { + this.geminiFlashTimeout = null; + this.bgcolor = this.geminiPrevBgcolor; + this.geminiPrevBgcolor = undefined; if (this.status) { this.status.value = "Ready"; } + this.setDirtyCanvas?.(true, true); }, 3000); } return result; }; + + // Clear any pending color-restore timer when the node is removed + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function() { + if (this.geminiFlashTimeout) { + clearTimeout(this.geminiFlashTimeout); + this.geminiFlashTimeout = null; + } + return onRemoved?.apply(this, arguments); + }; } }, diff --git a/web/kiko_local_image_loader.js b/web/kiko_local_image_loader.js index 1259e30..75e9349 100644 --- a/web/kiko_local_image_loader.js +++ b/web/kiko_local_image_loader.js @@ -1,6 +1,34 @@ import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; +// Escape a value for safe interpolation into HTML text or attribute contexts +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// Build a readable error message from a non-OK fetch response +async function describeHttpError(response) { + let detail = ''; + try { + const body = await response.json(); + detail = body?.error || body?.message || ''; + } catch (e) { + // Non-JSON body; fall back to the status code + } + if (response.status === 403) { + return detail || 'Access denied: path is outside the allowed folders'; + } + if (response.status === 400) { + return detail || 'Invalid path or request'; + } + return detail || `Request failed (HTTP ${response.status})`; +} + // Setup global lightbox for image preview function setupGlobalLightbox() { if (document.getElementById('kiko-image-lightbox')) return; @@ -511,7 +539,22 @@ app.registerExtension({ const debouncedLayout = debounce(applyMasonryLayout, 20); // Set up ResizeObserver for responsive layout - new ResizeObserver(debouncedLayout).observe(cardHolder); + this.kikoResizeObserver = new ResizeObserver(debouncedLayout); + this.kikoResizeObserver.observe(cardHolder); + + const originalOnRemoved = this.onRemoved; + this.onRemoved = function () { + if (this.kikoResizeObserver) { + this.kikoResizeObserver.disconnect(); + this.kikoResizeObserver = null; + } + return originalOnRemoved?.apply(this, arguments); + }; + + const showStatus = (message) => { + statusMessage.style.display = 'block'; + statusMessage.textContent = message; + }; // Autocomplete functionality let autocompleteSelectedIndex = -1; @@ -521,6 +564,10 @@ app.registerExtension({ async function fetchDirectorySuggestions(path) { try { const response = await api.fetchApi(`/kiko_local_image_loader/list_directories?path=${encodeURIComponent(path)}`); + if (!response.ok) { + // Disallowed or invalid paths simply yield no suggestions + return []; + } const data = await response.json(); return data.directories || []; } catch (error) { @@ -545,11 +592,11 @@ app.registerExtension({ const parentPath = parts.length > 0 ? '/' + parts.join('/') : ''; return ` -
+
${smallFolderSVG}
- ${folderName} - ${parentPath ? ` — ${parentPath}` : ''} + ${escapeHtml(folderName)} + ${parentPath ? ` — ${escapeHtml(parentPath)}` : ''}
`; @@ -668,6 +715,9 @@ app.registerExtension({ async function loadSavedPaths() { try { const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths'); + if (!response.ok) { + throw new Error(await describeHttpError(response)); + } const data = await response.json(); pathPresets.innerHTML = ''; @@ -689,20 +739,27 @@ app.registerExtension({ try { const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths'); + if (!response.ok) { + throw new Error(await describeHttpError(response)); + } const data = await response.json(); const savedPaths = data.saved_paths || []; if (!savedPaths.includes(currentDirectory)) { savedPaths.push(currentDirectory); - await api.fetchApi('/kiko_local_image_loader/save_paths', { + const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths: savedPaths }) }); + if (!saveResponse.ok) { + throw new Error(await describeHttpError(saveResponse)); + } await loadSavedPaths(); } } catch (error) { console.error('Error saving path:', error); + showStatus(`Error saving path: ${error.message}`); } }; @@ -713,20 +770,27 @@ app.registerExtension({ try { const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths'); + if (!response.ok) { + throw new Error(await describeHttpError(response)); + } const data = await response.json(); const savedPaths = data.saved_paths || []; const updatedPaths = savedPaths.filter(p => p !== selectedPath); if (updatedPaths.length !== savedPaths.length) { - await api.fetchApi('/kiko_local_image_loader/save_paths', { + const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths: updatedPaths }) }); + if (!saveResponse.ok) { + throw new Error(await describeHttpError(saveResponse)); + } await loadSavedPaths(); } } catch (error) { console.error('Error deleting path:', error); + showStatus(`Error deleting path: ${error.message}`); } }; @@ -757,6 +821,10 @@ app.registerExtension({ try { const response = await api.fetchApi(`/kiko_local_image_loader/images?${params}`); + if (!response.ok) { + statusMessage.textContent = await describeHttpError(response); + return; + } const data = await response.json(); if (data.error) { @@ -820,7 +888,7 @@ app.registerExtension({
${folderSVG}
-
${item.name}
+
${escapeHtml(item.name)}
`; @@ -831,9 +899,9 @@ app.registerExtension({ const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`; card.innerHTML = `
- ${item.name} + ${escapeHtml(item.name)}
-
${item.name}
+
${escapeHtml(item.name)}
`; // Trigger layout when image loads @@ -856,10 +924,10 @@ app.registerExtension({ const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`; card.innerHTML = `
- ${item.name} + ${escapeHtml(item.name)}
${videoSVG}
-
${item.name}
+
${escapeHtml(item.name)}
`; const img = card.querySelector('img'); @@ -882,7 +950,7 @@ app.registerExtension({
${audioSVG}
-
${item.name}
+
${escapeHtml(item.name)}
`; @@ -924,7 +992,7 @@ app.registerExtension({ // Send to backend try { - await api.fetchApi('/kiko_local_image_loader/set_node_selection', { + const response = await api.fetchApi('/kiko_local_image_loader/set_node_selection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -933,11 +1001,16 @@ app.registerExtension({ type: type }) }); + if (!response.ok) { + showStatus(`Selection rejected: ${await describeHttpError(response)}`); + return; + } // Trigger node update node.setDirtyCanvas(true); } catch (error) { console.error('Error setting selection:', error); + showStatus('Error setting selection'); } } @@ -997,10 +1070,13 @@ app.registerExtension({ // Try to load last path api.fetchApi('/kiko_local_image_loader/get_last_path').then(async response => { + if (!response.ok) return; const data = await response.json(); if (data.last_path) { loadImages(data.last_path); } + }).catch(error => { + console.error('Error loading last path:', error); }); return r; diff --git a/web/kiko_save_image_ui.js b/web/kiko_save_image_ui.js index 25d1f88..8bc45ac 100644 --- a/web/kiko_save_image_ui.js +++ b/web/kiko_save_image_ui.js @@ -377,6 +377,26 @@ function injectStyles() { } } +// Escape a value for safe interpolation into HTML text or attribute contexts +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// Build a ComfyUI /api/view URL with every parameter encoded +function buildViewUrl(filename, subfolder, type) { + const params = new URLSearchParams({ filename: filename ?? '' }); + if (subfolder && subfolder.trim()) { + params.set('subfolder', subfolder); + } + params.set('type', type || 'output'); + return `${window.location.origin}/api/view?${params.toString()}`; +} + // Format file size for display function formatFileSize(bytes) { if (bytes === 0) return '0 B'; @@ -388,8 +408,6 @@ function formatFileSize(bytes) { // Open image in new tab function openImageInTab(imagePath, subfolder = '', enablePopup = true) { - console.log(`KikoSaveImage: Attempting to open image: ${imagePath}, subfolder: ${subfolder}`); - // Note: enablePopup parameter is kept for compatibility but not used // since popup now controls viewer visibility, not individual image clicks @@ -403,16 +421,12 @@ function openImageInTab(imagePath, subfolder = '', enablePopup = true) { fullPath = `${basePath}/api/view?filename=${encodeURIComponent(imagePath)}&type=output`; } - console.log(`KikoSaveImage: Opening URL: ${fullPath}`); - // Extract clean filename for window name (remove timestamp and batch number) const cleanName = imagePath.split('_').slice(0, -2).join('_') || 'KikoSaveImage'; // Try to open the image with a clean window name const newWindow = window.open(fullPath, cleanName.replace(/[^a-zA-Z0-9]/g, '_')); - if (newWindow) { - console.log('KikoSaveImage: Successfully opened new window'); - } else { + if (!newWindow) { console.error('KikoSaveImage: Failed to open new window - popup blocked?'); // Fallback: copy URL to clipboard and alert user navigator.clipboard.writeText(fullPath).then(() => { @@ -435,6 +449,10 @@ class KikoImageViewer extends HTMLElement { this.dragOffset = { x: 0, y: 0 }; this.lastHeaderClick = 0; + // Bind document-level drag handlers once so they can be removed later + this.boundHandleDrag = this.handleDrag.bind(this); + this.boundEndDrag = this.endDrag.bind(this); + // Always start at default position } @@ -442,6 +460,16 @@ class KikoImageViewer extends HTMLElement { return ['data']; } + connectedCallback() { + document.addEventListener('mousemove', this.boundHandleDrag); + document.addEventListener('mouseup', this.boundEndDrag); + } + + disconnectedCallback() { + document.removeEventListener('mousemove', this.boundHandleDrag); + document.removeEventListener('mouseup', this.boundEndDrag); + } + attributeChangedCallback(name, oldValue, newValue) { if (name === 'data' && newValue) { try { @@ -455,8 +483,6 @@ class KikoImageViewer extends HTMLElement { } setImageData(data) { - console.log('KikoImageViewer: setImageData called with:', data); - console.log('KikoImageViewer: First image data:', data[0]); this.imageData = data; this.autoUnrollOnNewImages(); this.render(); @@ -513,9 +539,7 @@ class KikoImageViewer extends HTMLElement { }); }); - // Global mouse events for dragging - document.addEventListener('mousemove', this.handleDrag.bind(this)); - document.addEventListener('mouseup', this.endDrag.bind(this)); + // Document-level drag listeners are managed in connectedCallback/disconnectedCallback } startDrag(e) { @@ -697,12 +721,9 @@ class KikoImageViewer extends HTMLElement { openAllImagesWithDelay() { const selectedIndices = Array.from(this.selectedImages); if (selectedIndices.length === 0) { - console.log('KikoSaveImage: No images selected for opening'); return; } - console.log(`KikoSaveImage: Opening ${selectedIndices.length} images with delay`); - // Open images with 150ms delay between each to prevent popup blocking selectedIndices.forEach((index, i) => { setTimeout(() => { @@ -716,12 +737,7 @@ class KikoImageViewer extends HTMLElement { downloadImage(imageData) { // Construct proper image URL handling subfolder - let imageUrl; - if (imageData.subfolder && imageData.subfolder.trim()) { - imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&subfolder=${encodeURIComponent(imageData.subfolder)}&type=${imageData.type}`; - } else { - imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&type=${imageData.type}`; - } + const imageUrl = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type); // Create temporary link and trigger download const link = document.createElement('a'); @@ -748,7 +764,7 @@ class KikoImageViewer extends HTMLElement { } const header = this.imageData.length === 1 - ? `Saved Image (${this.imageData[0].format})` + ? `Saved Image (${escapeHtml(this.imageData[0].format)})` : `Saved Images (${this.imageData.length} files)`; const containerStyle = this.isMinimized ? 'style="display: none;"' : ''; @@ -791,15 +807,10 @@ class KikoImageViewer extends HTMLElement { createImageItem(data, index) { // Construct proper image URL handling subfolder - let imageUrl; - if (data.subfolder && data.subfolder.trim()) { - // ComfyUI expects subfolder to be passed separately - imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&subfolder=${encodeURIComponent(data.subfolder)}&type=${data.type}`; - } else { - imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}`; - } + // ComfyUI expects subfolder to be passed separately + const imageUrl = buildViewUrl(data.filename, data.subfolder, data.type); - const formatClass = `kiko-format-${data.format.toLowerCase()}`; + const formatClass = `kiko-format-${String(data.format ?? '').toLowerCase().replace(/[^a-z0-9_-]/g, '')}`; // Format file size const fileSize = data.file_size ? formatFileSize(data.file_size) : 'Unknown'; @@ -818,7 +829,7 @@ class KikoImageViewer extends HTMLElement { return `
- ${data.filename} + ${escapeHtml(data.filename)} ${this.imageData.length > 1 ? `
@@ -829,14 +840,14 @@ class KikoImageViewer extends HTMLElement {
-
- ${data.filename} +
+ ${escapeHtml(data.filename)}
- ${data.format} - ${data.dimensions || 'Unknown'} • ${fileSize} - ${qualityInfo ? ` • ${qualityInfo}` : ''} + ${escapeHtml(data.format)} + ${escapeHtml(data.dimensions || 'Unknown')} • ${escapeHtml(fileSize)} + ${qualityInfo ? ` • ${escapeHtml(qualityInfo)}` : ''}
@@ -852,7 +863,6 @@ class KikoImageViewer extends HTMLElement { item.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - console.log(`KikoImageViewer: Clicking image ${index}: ${data.filename}`); openImageInTab(data.filename, data.subfolder || ''); }); } @@ -872,8 +882,7 @@ function createImagePreview(imageData) { // Create image element const img = document.createElement('img'); - const imagePath = `/api/view?filename=${imageData.filename}&type=${imageData.type}`; - img.src = imagePath; + img.src = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type); img.alt = imageData.filename; // Create info overlay @@ -1012,17 +1021,13 @@ app.registerExtension({ // Replace the standard image viewer with our custom web component const originalOnExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function(message) { - console.log('KikoSaveImage: onExecuted called with message:', message); - // Skip the original ComfyUI preview system // Don't call originalOnExecuted to prevent default image display // Use our custom web component instead if (message && message.kiko_enhanced && message.kiko_enhanced.length > 0) { - console.log('KikoSaveImage: Creating custom image viewer'); this.createCustomImageViewer(message.kiko_enhanced); } else { - console.log('KikoSaveImage: No enhanced data found, falling back to standard preview'); // Fall back to standard behavior if no enhanced data if (originalOnExecuted) { originalOnExecuted.apply(this, arguments); @@ -1032,14 +1037,10 @@ app.registerExtension({ // Add method to create custom image viewer nodeType.prototype.createCustomImageViewer = function(imageData) { - console.log('KikoSaveImage: Creating custom viewer for', imageData.length, 'images'); - // Check if popup is enabled for any image (use first image's popup setting) const popupEnabled = imageData.length > 0 ? imageData[0].popup : true; - console.log('KikoSaveImage: Popup enabled:', popupEnabled); if (!popupEnabled) { - console.log('KikoSaveImage: Popup disabled, not showing custom viewer'); return; } @@ -1049,7 +1050,6 @@ app.registerExtension({ // Update existing viewer with new data existingViewer.setImageData(imageData); existingViewer.show(); // Make sure it's visible - console.log('KikoSaveImage: Updated existing viewer'); return; } @@ -1072,8 +1072,6 @@ app.registerExtension({ // Append to body (floating window) - always visible document.body.appendChild(viewer); - - console.log('KikoSaveImage: Custom viewer created successfully'); }; // Add method to create toggle button @@ -1137,50 +1135,16 @@ app.registerExtension({ nodeElement.appendChild(toggleButton); this.kikoToggleButton = toggleButton; - - console.log('KikoSaveImage: Toggle button created'); }; // Legacy method kept for compatibility (not used with web component) nodeType.prototype.enhanceImagePreviews = function(imageData) { - console.log('KikoSaveImage: Legacy enhanceImagePreviews called (should use web component instead)'); + // No-op: superseded by the kiko-image-viewer web component }; } } }); -// Export helper functions to global scope for debugging -window.kikoSaveImageTest = function() { - console.log('KikoSaveImage: Testing click functionality...'); - - // Find all images in the document - const allImages = document.querySelectorAll('img'); - console.log(`Found ${allImages.length} images in document`); - - // Try to find images that look like our saved images - allImages.forEach((img, index) => { - console.log(`Image ${index}: src = ${img.src}`); - - // Add test click handler to all images - img.style.border = '2px solid red'; - img.style.cursor = 'pointer'; - img.title = 'TEST: Click to open in new tab'; - - // Remove old handlers and add new one - const newImg = img.cloneNode(true); - img.parentNode.replaceChild(newImg, img); - - newImg.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log('TEST CLICK WORKED!', newImg.src); - window.open(newImg.src, '_blank'); - }, true); - }); - - console.log('KikoSaveImage: Test setup complete. All images should now be clickable with red borders.'); -}; - // 🎉 RESET VIEWER WINDOWS 🎉 window.kikoResetViewer = function() { console.log('🎉 Resetting KikoSaveImage viewer windows...'); @@ -1219,4 +1183,3 @@ console.log('🎉 KikoSaveImage: Extension loaded!'); console.log('💡 Helper functions available:'); console.log(' 📞 kikoResetViewer() - Remove existing viewer windows'); console.log(' 🚀 kikoForceViewer() - Show demo viewer'); -console.log(' 🧪 kikoSaveImageTest() - Test image click functionality'); diff --git a/web/kiko_workflow_timer_ui.js b/web/kiko_workflow_timer_ui.js index 8ae736f..cb049bd 100644 --- a/web/kiko_workflow_timer_ui.js +++ b/web/kiko_workflow_timer_ui.js @@ -448,8 +448,10 @@ const KikoWorkflowTimerExtension = { KikoGlobalTimer.registerNode(this); }; + const origOnRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function() { KikoGlobalTimer.unregisterNode(this); + return origOnRemoved?.apply(this, arguments); }; const origOnSerialize = nodeType.prototype.onSerialize; @@ -461,7 +463,7 @@ const KikoWorkflowTimerExtension = { const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(info) { origOnConfigure?.apply(this, arguments); - this.properties = info.properties || {}; + this.properties = { ...(this.properties || {}), ...(info?.properties || {}) }; if (this.timerDisplay) { this.timerDisplay.textContent = this.properties.elapsed_time_str || "00:00:000"; diff --git a/web/seed_history_ui.js b/web/seed_history_ui.js index 2ebf6f8..e92cd79 100644 --- a/web/seed_history_ui.js +++ b/web/seed_history_ui.js @@ -371,14 +371,15 @@ app.registerExtension({ }); const timeAgo = this.formatTimeAgo(item.timestamp); - entryDiv.innerHTML = ` -
- 🎲 ${item.seed} -
-
- ⏰ ${timeAgo} -
- `; + // Build with textContent: history comes from localStorage and is untrusted + const seedDiv = document.createElement("div"); + seedDiv.style.cssText = "color: #fff; font-weight: bold; margin-bottom: 1px;"; + seedDiv.textContent = `🎲 ${Number(item.seed)}`; + const timeDiv = document.createElement("div"); + timeDiv.style.cssText = "color: #999; font-size: 8px;"; + timeDiv.textContent = `⏰ ${timeAgo}`; + entryDiv.appendChild(seedDiv); + entryDiv.appendChild(timeDiv); this.historyDisplay.appendChild(entryDiv); });