/** * Showcase - Left panel for displaying example images * Features: * - Main image display with navigation * - Thumbnail rail for quick switching * - Params panel for image metadata * - Keyboard navigation support (← →) */ import { escapeHtml } from '../shared/utils.js'; import { translate } from '../../utils/i18nHelpers.js'; export class Showcase { constructor(container) { this.element = container; this.images = []; this.currentIndex = 0; this.modelHash = ''; this.filePath = ''; this.paramsVisible = false; } /** * Render the showcase */ render({ images, modelHash, filePath }) { this.images = images || []; this.modelHash = modelHash || ''; this.filePath = filePath || ''; this.currentIndex = 0; this.paramsVisible = false; this.element.innerHTML = this.getTemplate(); this.bindEvents(); if (this.images.length > 0) { this.loadImage(0); } } /** * Get the HTML template */ getTemplate() { const hasImages = this.images.length > 0; return `
${hasImages ? `
${translate('modals.model.examples.title', {}, 'Example')}
${translate('modals.model.params.title', {}, 'Generation Parameters')}
` : `

${translate('modals.model.examples.empty', {}, 'No example images available')}

`}
${this.renderThumbnailRail()} `; } /** * Render the thumbnail rail */ renderThumbnailRail() { if (this.images.length === 0) { return `
`; } const thumbnails = this.images.map((img, index) => { const url = img.url || img; const isNsfw = img.nsfw || false; return `
${isNsfw ? 'NSFW' : ''}
`; }).join(''); return `
${thumbnails}
`; } /** * Bind event listeners */ bindEvents() { this.element.addEventListener('click', (e) => { const target = e.target.closest('[data-action]'); if (!target) return; const action = target.dataset.action; switch (action) { case 'prev-image': this.prevImage(); break; case 'next-image': this.nextImage(); break; case 'select-image': const index = parseInt(target.dataset.index, 10); if (!isNaN(index)) { this.loadImage(index); } break; case 'toggle-params': this.toggleParams(); break; case 'close-params': this.hideParams(); break; case 'set-preview': this.setAsPreview(); break; case 'delete-example': this.deleteExample(); break; case 'add-example': this.showUploadArea(); break; case 'copy-prompt': this.copyPrompt(); break; } }); } /** * Load and display an image by index */ loadImage(index) { if (index < 0 || index >= this.images.length) return; this.currentIndex = index; const image = this.images[index]; const url = image.url || image; // Update main image const imgElement = this.element.querySelector('.showcase__image'); if (imgElement) { imgElement.classList.add('loading'); imgElement.src = url; imgElement.onload = () => { imgElement.classList.remove('loading'); }; } // Update thumbnail rail active state this.element.querySelectorAll('.thumbnail-rail__item').forEach((item, i) => { item.classList.toggle('active', i === index); }); // Update params this.updateParams(image); } /** * Navigate to previous image */ prevImage() { if (this.images.length === 0) return; const newIndex = this.currentIndex > 0 ? this.currentIndex - 1 : this.images.length - 1; this.loadImage(newIndex); } /** * Navigate to next image */ nextImage() { if (this.images.length === 0) return; const newIndex = this.currentIndex < this.images.length - 1 ? this.currentIndex + 1 : 0; this.loadImage(newIndex); } /** * Toggle params panel visibility */ toggleParams() { this.paramsVisible = !this.paramsVisible; const panel = this.element.querySelector('.showcase__params'); if (panel) { panel.classList.toggle('visible', this.paramsVisible); } } /** * Hide params panel */ hideParams() { this.paramsVisible = false; const panel = this.element.querySelector('.showcase__params'); if (panel) { panel.classList.remove('visible'); } } /** * Update params panel content */ updateParams(image) { const content = this.element.querySelector('.showcase__params-content'); if (!content) return; const meta = image.meta || {}; const prompt = meta.prompt || ''; const negativePrompt = meta.negativePrompt || ''; // Build params display let html = ''; if (prompt) { html += this.renderPromptSection( translate('modals.model.params.prompt', {}, 'Prompt'), prompt, 'prompt' ); } if (negativePrompt) { html += this.renderPromptSection( translate('modals.model.params.negativePrompt', {}, 'Negative Prompt'), negativePrompt, 'negative' ); } // Add parameter tags const params = []; if (meta.sampler) params.push({ name: 'Sampler', value: meta.sampler }); if (meta.steps) params.push({ name: 'Steps', value: meta.steps }); if (meta.cfgScale) params.push({ name: 'CFG', value: meta.cfgScale }); if (meta.seed) params.push({ name: 'Seed', value: meta.seed }); if (meta.size) params.push({ name: 'Size', value: meta.size }); if (params.length > 0) { html += '
'; params.forEach(param => { html += ` ${escapeHtml(param.name)}: ${escapeHtml(String(param.value))} `; }); html += '
'; } if (!prompt && !negativePrompt && params.length === 0) { html = `
${translate('modals.model.params.noData', {}, 'No generation data available')}
`; } content.innerHTML = html; } /** * Render a prompt section */ renderPromptSection(label, text, type) { return `
${escapeHtml(label)}
${escapeHtml(text)}
`; } /** * Copy current prompt to clipboard */ async copyPrompt() { const image = this.images[this.currentIndex]; if (!image) return; const meta = image.meta || {}; const prompt = meta.prompt || ''; if (!prompt) return; try { await navigator.clipboard.writeText(prompt); const { showToast } = await import('../../utils/uiHelpers.js'); showToast('modals.model.params.promptCopied', {}, 'success'); } catch (err) { console.error('Failed to copy prompt:', err); } } /** * Set current image as model preview */ async setAsPreview() { const image = this.images[this.currentIndex]; if (!image || !this.filePath) return; const url = image.url || image; try { const { getModelApiClient } = await import('../../api/modelApiFactory.js'); await getModelApiClient().setModelPreview(this.filePath, url); const { showToast } = await import('../../utils/uiHelpers.js'); showToast('modals.model.actions.previewSet', {}, 'success'); } catch (err) { console.error('Failed to set preview:', err); const { showToast } = await import('../../utils/uiHelpers.js'); showToast('modals.model.actions.previewFailed', {}, 'error'); } } /** * Delete current example */ async deleteExample() { const image = this.images[this.currentIndex]; if (!image || !this.filePath) return; // TODO: Implement delete confirmation and API call console.log('Delete example:', image); } /** * Show upload area for adding new examples */ showUploadArea() { // TODO: Implement upload area expansion console.log('Show upload area'); } }