diff --git a/static/css/components/lora-modal.css b/static/css/components/lora-modal.css index c4278a29..3fa5d344 100644 --- a/static/css/components/lora-modal.css +++ b/static/css/components/lora-modal.css @@ -99,6 +99,7 @@ width: 100%; background: var(--lora-surface); margin-bottom: var(--space-2); + overflow: hidden; /* Ensure metadata panel is contained */ } .media-wrapper:last-child { @@ -1083,4 +1084,172 @@ /* Make sure media wrapper maintains position: relative for absolute positioning of children */ .carousel .media-wrapper { position: relative; +} + +/* Image Metadata Panel Styles */ +.image-metadata-panel { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-color); + border-top: 1px solid var(--border-color); + padding: var(--space-2); + transform: translateY(100%); + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.25s ease; + z-index: 5; + max-height: 50%; /* Reduced to take less space */ + overflow-y: auto; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); + opacity: 0; + pointer-events: none; +} + +/* Show metadata panel only on hover */ +.media-wrapper:hover .image-metadata-panel { + transform: translateY(0); + opacity: 0.98; + pointer-events: auto; +} + +/* Adjust to dark theme */ +[data-theme="dark"] .image-metadata-panel { + background: var(--card-bg); + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3); +} + +.metadata-content { + display: flex; + flex-direction: column; + gap: 10px; +} + +/* Styling for parameters tags */ +.params-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: var(--space-1); + padding-bottom: var(--space-1); + border-bottom: 1px solid var(--lora-border); +} + +.param-tag { + display: inline-flex; + align-items: center; + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + padding: 2px 6px; + font-size: 0.8em; + line-height: 1.2; + white-space: nowrap; +} + +.param-tag .param-name { + font-weight: 600; + color: var(--text-color); + margin-right: 4px; + opacity: 0.8; +} + +.param-tag .param-value { + color: var(--lora-accent); +} + +/* Special styling for prompt row */ +.metadata-row.prompt-row { + flex-direction: column; + padding-top: 0; +} + +.metadata-row.prompt-row + .metadata-row.prompt-row { + margin-top: var(--space-2); +} + +.metadata-label { + font-weight: 600; + color: var(--text-color); + opacity: 0.8; + font-size: 0.85em; + display: block; + margin-bottom: 4px; +} + +.metadata-prompt-wrapper { + position: relative; + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + padding: 6px 30px 6px 8px; + margin-top: 2px; + max-height: 80px; /* Reduced from 120px */ + overflow-y: auto; + word-break: break-word; + width: 100%; + box-sizing: border-box; +} + +.metadata-prompt { + color: var(--text-color); + font-family: monospace; + font-size: 0.85em; + white-space: pre-wrap; +} + +.copy-prompt-btn { + position: absolute; + top: 6px; + right: 6px; + background: transparent; + border: none; + color: var(--text-color); + opacity: 0.6; + cursor: pointer; + padding: 3px; + transition: all 0.2s ease; +} + +.copy-prompt-btn:hover { + opacity: 1; + color: var(--lora-accent); +} + +/* Scrollbar styling for metadata panel */ +.image-metadata-panel::-webkit-scrollbar { + width: 6px; +} + +.image-metadata-panel::-webkit-scrollbar-track { + background: transparent; +} + +.image-metadata-panel::-webkit-scrollbar-thumb { + background-color: var(--border-color); + border-radius: 3px; +} + +/* For Firefox */ +.image-metadata-panel { + scrollbar-width: thin; + scrollbar-color: var(--border-color) transparent; +} + +/* No metadata message styling */ +.no-metadata-message { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2); + color: var(--text-color); + opacity: 0.7; + text-align: center; + font-style: italic; + gap: 8px; +} + +.no-metadata-message i { + font-size: 1.1em; + color: var(--lora-accent); + opacity: 0.8; } \ No newline at end of file diff --git a/static/js/components/LoraModal.js b/static/js/components/LoraModal.js index 607e90d1..54454f69 100644 --- a/static/js/components/LoraModal.js +++ b/static/js/components/LoraModal.js @@ -208,61 +208,165 @@ function renderShowcaseContent(images) { nsfwText = "R-rated Content"; } - if (img.type === 'video') { - return ` -
- ${shouldBlur ? ` - - ` : ''} - - ${shouldBlur ? ` -
-
-

${nsfwText}

- -
-
- ` : ''} -
- `; - } - return ` -
- ${shouldBlur ? ` - - ` : ''} - Preview - ${shouldBlur ? ` -
-
-

${nsfwText}

- + // Extract metadata from the image + const meta = img.meta || {}; + const prompt = meta.prompt || ''; + const negativePrompt = meta.negative_prompt || meta.negativePrompt || ''; + const size = meta.Size || `${img.width}x${img.height}`; + const seed = meta.seed || ''; + const model = meta.Model || ''; + const steps = meta.steps || ''; + const sampler = meta.sampler || ''; + const cfgScale = meta.cfgScale || ''; + const clipSkip = meta.clipSkip || ''; + + // Check if we have any meaningful generation parameters + const hasParams = seed || model || steps || sampler || cfgScale || clipSkip; + const hasPrompts = prompt || negativePrompt; + + // If no metadata available, show a message + if (!hasParams && !hasPrompts) { + const metadataPanel = ` + + `; + + if (img.type === 'video') { + return generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel); + } + return generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel); + } + + // Create a data attribute with the prompt for copying instead of trying to handle it in the onclick + // This avoids issues with quotes and special characters + const promptIndex = Math.random().toString(36).substring(2, 15); + const negPromptIndex = Math.random().toString(36).substring(2, 15); + + // Create parameter tags HTML + const paramTags = ` +
+ ${size ? `
Size:${size}
` : ''} + ${seed ? `
Seed:${seed}
` : ''} + ${model ? `
Model:${model}
` : ''} + ${steps ? `
Steps:${steps}
` : ''} + ${sampler ? `
Sampler:${sampler}
` : ''} + ${cfgScale ? `
CFG:${cfgScale}
` : ''} + ${clipSkip ? `
Clip Skip:${clipSkip}
` : ''}
`; + + // Metadata panel HTML + const metadataPanel = ` + + `; + + if (img.type === 'video') { + return generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel); + } + return generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel); }).join('')}
`; } +// Helper function to generate video wrapper HTML +function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { + return ` +
+ ${shouldBlur ? ` + + ` : ''} + + ${shouldBlur ? ` +
+
+

${nsfwText}

+ +
+
+ ` : ''} + ${metadataPanel} +
+ `; +} + +// Helper function to generate image wrapper HTML +function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { + return ` +
+ ${shouldBlur ? ` + + ` : ''} + Preview + ${shouldBlur ? ` +
+
+

${nsfwText}

+ +
+
+ ` : ''} + ${metadataPanel} +
+ `; +} + // New function to handle tab switching function setupTabSwitching() { const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn'); @@ -626,13 +730,74 @@ export function toggleShowcase(element) { // Initialize NSFW content blur toggle handlers initNsfwBlurHandlers(carousel); + + // Initialize metadata panel interaction handlers + initMetadataPanelHandlers(carousel); } else { const count = carousel.querySelectorAll('.media-wrapper').length; indicator.textContent = `Scroll or click to show ${count} examples`; icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); + + // Make sure any open metadata panels get closed + const carouselContainer = carousel.querySelector('.carousel-container'); + if (carouselContainer) { + carouselContainer.style.height = '0'; + setTimeout(() => { + carouselContainer.style.height = ''; + }, 300); + } } } +// Function to initialize metadata panel interactions +function initMetadataPanelHandlers(container) { + // Find all media wrappers + const mediaWrappers = container.querySelectorAll('.media-wrapper'); + + mediaWrappers.forEach(wrapper => { + // Get the metadata panel + const metadataPanel = wrapper.querySelector('.image-metadata-panel'); + if (!metadataPanel) return; + + // Prevent events from the metadata panel from bubbling + metadataPanel.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Handle copy prompt button clicks + const copyBtns = metadataPanel.querySelectorAll('.copy-prompt-btn'); + copyBtns.forEach(copyBtn => { + const promptIndex = copyBtn.dataset.promptIndex; + const promptElement = wrapper.querySelector(`#prompt-${promptIndex}`); + + copyBtn.addEventListener('click', async (e) => { + e.stopPropagation(); // Prevent bubbling + + if (!promptElement) return; + + try { + await navigator.clipboard.writeText(promptElement.textContent); + showToast('Prompt copied to clipboard', 'success'); + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); + } + }); + }); + + // Prevent scrolling in the metadata panel from scrolling the whole modal + metadataPanel.addEventListener('wheel', (e) => { + const isAtTop = metadataPanel.scrollTop === 0; + const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight; + + // Only prevent default if scrolling would cause the panel to scroll + if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) { + e.stopPropagation(); + } + }, { passive: true }); + }); +} + // New function to initialize blur toggle handlers for showcase images/videos function initNsfwBlurHandlers(container) { // Handle toggle blur buttons