Spaces:
Running
Running
| // State Management | |
| const AppState = { | |
| currentImage: null, | |
| currentVideo: null, | |
| isGenerating: false, | |
| history: JSON.parse(localStorage.getItem('generationHistory') || '[]'), | |
| settings: { | |
| width: 1024, | |
| height: 1024, | |
| steps: 30, | |
| cfgScale: 7.5, | |
| seed: -1, | |
| motionStrength: 5, | |
| videoDuration: 4 | |
| } | |
| }; | |
| // Utility Functions | |
| const Utils = { | |
| generateId: () => Math.random().toString(36).substr(2, 9), | |
| async hashString(str) { | |
| const encoder = new TextEncoder(); | |
| const data = encoder.encode(str); | |
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); | |
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | |
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | |
| }, | |
| getRandomSeed() { | |
| return Math.floor(Math.random() * 2147483647); | |
| }, | |
| formatDate(date) { | |
| return new Intl.DateTimeFormat('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }).format(date); | |
| }, | |
| debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| }; | |
| // API Simulation (Mock) | |
| const APIService = { | |
| async generateImage(prompt, negativePrompt, settings) { | |
| // Simulate API delay | |
| await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 2000)); | |
| // Generate deterministic seed from prompt for consistent demo results | |
| const seed = settings.seed > 0 ? settings.seed : Math.abs(Utils.hashString(prompt).split('').reduce((a,b)=>a+b.charCodeAt(0),0)); | |
| // Use static.photos with random category based on prompt content | |
| const categories = ['technology', 'abstract', 'nature', 'people', 'cityscape', 'minimal']; | |
| const category = categories[seed % categories.length]; | |
| const dimensions = settings.width > settings.height ? '1200x630' : settings.width < settings.height ? '640x360' : '1024x576'; | |
| return { | |
| id: Utils.generateId(), | |
| url: `http://static.photos/${category}/${dimensions}/${seed % 999}`, | |
| prompt: prompt, | |
| negativePrompt: negativePrompt, | |
| settings: {...settings}, | |
| timestamp: new Date(), | |
| seed: seed, | |
| type: 'image' | |
| }; | |
| }, | |
| async generateVideo(imageUrl, prompt, settings) { | |
| await new Promise(resolve => setTimeout(resolve, 3000 + Math.random() * 3000)); | |
| const seed = Utils.getRandomSeed(); | |
| return { | |
| id: Utils.generateId(), | |
| imageUrl: imageUrl, | |
| videoUrl: `http://static.photos/technology/640x360/${seed % 999}`, // Simulated video thumbnail | |
| prompt: prompt, | |
| settings: {...settings}, | |
| timestamp: new Date(), | |
| duration: settings.videoDuration || 4, | |
| type: 'video' | |
| }; | |
| } | |
| }; | |
| // UI Components Logic | |
| const UI = { | |
| showLoading(element, text = 'Generating...') { | |
| element.innerHTML = ` | |
| <div class="flex flex-col items-center justify-center space-y-4 p-8"> | |
| <div class="relative w-16 h-16"> | |
| <div class="absolute inset-0 border-4 border-primary-500/30 rounded-full"></div> | |
| <div class="absolute inset-0 border-4 border-t-primary-500 border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div> | |
| </div> | |
| <p class="text-primary-400 animate-pulse font-mono text-sm">${text}</p> | |
| <div class="w-48 h-1 bg-slate-800 rounded-full overflow-hidden"> | |
| <div class="h-full bg-gradient-to-r from-primary-500 to-secondary-500 animate-[shimmer_2s_infinite]" style="width: 0%; animation: loadingProgress 3s ease-out forwards;"></div> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes loadingProgress { | |
| 0% { width: 0%; } | |
| 100% { width: 100%; } | |
| } | |
| </style> | |
| `; | |
| }, | |
| showError(element, message) { | |
| element.innerHTML = ` | |
| <div class="flex flex-col items-center justify-center space-y-3 p-8 text-red-400"> | |
| <i data-feather="alert-circle" class="w-12 h-12"></i> | |
| <p class="text-center">${message}</p> | |
| <button onclick="this.closest('.error-container').remove()" class="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 rounded-lg text-sm transition-colors"> | |
| Dismiss | |
| </button> | |
| </div> | |
| `; | |
| // Fade in effect for image | |
| const img = container.querySelector('img'); | |
| if (img) { | |
| img.onload = () => { | |
| img.style.opacity = '1'; | |
| }; | |
| // Force load if cached | |
| if (img.complete) { | |
| img.style.opacity = '1'; | |
| } | |
| } | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| }, | |
| createImageCard(item) { | |
| return ` | |
| <div class="group relative bg-slate-900/50 rounded-xl overflow-hidden border border-slate-700/50 hover:border-primary-500/50 transition-all duration-300 hover:glow-primary" data-id="${item.id}"> | |
| <div class="aspect-video relative overflow-hidden bg-slate-800"> | |
| <img src="${item.url}" alt="${item.prompt}" class="w-full h-full object-cover img-zoom" loading="lazy"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-60"></div> | |
| <!-- Hover Actions --> | |
| <div class="absolute inset-0 bg-slate-950/80 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3"> | |
| <button onclick="app.animateImage('${item.id}')" class="p-3 bg-primary-600 hover:bg-primary-500 rounded-full text-white transition-all hover:scale-110" title="Generate Video"> | |
| <i data-feather="film" class="w-5 h-5"></i> | |
| </button> | |
| <button onclick="app.downloadImage('${item.url}')" class="p-3 bg-slate-700 hover:bg-slate-600 rounded-full text-white transition-all hover:scale-110" title="Download"> | |
| <i data-feather="download" class="w-5 h-5"></i> | |
| </button> | |
| <button onclick="app.deleteItem('${item.id}')" class="p-3 bg-red-500/20 hover:bg-red-500/40 rounded-full text-red-400 transition-all hover:scale-110" title="Delete"> | |
| <i data-feather="trash-2" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| ${item.type === 'video' ? ` | |
| <div class="absolute top-3 right-3 bg-secondary-500/90 text-white px-2 py-1 rounded text-xs font-bold flex items-center gap-1"> | |
| <i data-feather="video" class="w-3 h-3"></i> VIDEO | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="p-4 space-y-2"> | |
| <p class="text-sm text-slate-300 line-clamp-2 font-medium">${item.prompt}</p> | |
| <div class="flex items-center justify-between text-xs text-slate-500"> | |
| <span>${Utils.formatDate(new Date(item.timestamp))}</span> | |
| <span class="font-mono">Seed: ${item.seed || 'N/A'}</span> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| }; | |
| // Main Application Logic | |
| class DreamMachineApp { | |
| constructor() { | |
| this.init(); | |
| } | |
| init() { | |
| this.bindEvents(); | |
| this.loadHistory(); | |
| this.setupRealtimeValidation(); | |
| } | |
| bindEvents() { | |
| // Generate Image Button - handle both shadow DOM and regular DOM | |
| document.addEventListener('click', (e) => { | |
| const generateBtn = e.target.closest('#generate-btn'); | |
| if (generateBtn) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| this.generateImage(); | |
| } | |
| if (e.target.closest('#generate-video-btn')) { | |
| this.generateVideoFromCurrent(); | |
| } | |
| }); | |
| // Also bind to shadow DOM buttons directly | |
| this.bindShadowDomevents(); | |
| } | |
| bindShadowDomevents() { | |
| // Wait for components to be defined | |
| const controls = document.querySelector('generator-controls'); | |
| if (controls && controls.shadowRoot) { | |
| const btn = controls.shadowRoot.getElementById('generate-btn'); | |
| if (btn) { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.generateImage(); | |
| }); | |
| } | |
| } | |
| } | |
| // Settings changes | |
| document.addEventListener('change', (e) => { | |
| if (e.target.hasAttribute('data-setting')) { | |
| const key = e.target.getAttribute('data-setting'); | |
| AppState.settings[key] = parseFloat(e.target.value) || e.target.value; | |
| } | |
| }); | |
| // Aspect ratio presets | |
| document.addEventListener('click', (e) => { | |
| if (e.target.closest('[data-ratio]')) { | |
| const ratio = e.target.closest('[data-ratio]').getAttribute('data-ratio'); | |
| this.setAspectRatio(ratio); | |
| } | |
| }); | |
| // Prompt enhancement | |
| document.addEventListener('click', (e) => { | |
| if (e.target.closest('.enhance-btn')) { | |
| this.enhancePrompt(); | |
| } | |
| }); | |
| } | |
| setupRealtimeValidation() { | |
| const promptInput = document.getElementById('prompt-input'); | |
| if (promptInput) { | |
| promptInput.addEventListener('input', Utils.debounce(() => { | |
| const length = promptInput.value.length; | |
| const indicator = document.getElementById('prompt-length'); | |
| if (indicator) { | |
| indicator.textContent = `${length} chars`; | |
| indicator.className = `text-xs ${length > 200 ? 'text-yellow-400' : 'text-slate-500'}`; | |
| } | |
| }, 300)); | |
| } | |
| } | |
| setAspectRatio(ratio) { | |
| const ratios = { | |
| '1:1': [1024, 1024], | |
| '16:9': [1024, 576], | |
| '9:16': [576, 1024], | |
| '4:3': [1024, 768], | |
| '3:4': [768, 1024] | |
| }; | |
| const [w, h] = ratios[ratio] || ratios['1:1']; | |
| AppState.settings.width = w; | |
| AppState.settings.height = h; | |
| // Update UI indicators | |
| document.querySelectorAll('[data-ratio]').forEach(el => { | |
| el.classList.remove('bg-primary-600', 'text-white'); | |
| el.classList.add('bg-slate-800', 'text-slate-400'); | |
| }); | |
| const active = document.querySelector(`[data-ratio="${ratio}"]`); | |
| if (active) { | |
| active.classList.remove('bg-slate-800', 'text-slate-400'); | |
| active.classList.add('bg-primary-600', 'text-white'); | |
| } | |
| } | |
| async generateImage() { | |
| // Get elements from shadow DOM if needed | |
| const controls = document.querySelector('generator-controls'); | |
| let promptInput = document.getElementById('prompt-input'); | |
| let negativeInput = document.getElementById('negative-prompt'); | |
| // Use shadow DOM if regular DOM not found | |
| const shadowRoot = controls?.shadowRoot; | |
| if (shadowRoot) { | |
| if (!promptInput) promptInput = shadowRoot.getElementById('prompt-input'); | |
| if (!negativeInput) negativeInput = shadowRoot.getElementById('negative-prompt'); | |
| } | |
| const preview = document.querySelector('generator-preview'); | |
| const previewContainer = preview?.shadowRoot?.getElementById('preview-container') || document.getElementById('preview-container'); | |
| const promptValue = promptInput?.value?.trim(); | |
| if (!promptValue) { | |
| if (promptInput) { | |
| promptInput.style.borderColor = '#ef4444'; | |
| setTimeout(() => { | |
| promptInput.style.borderColor = ''; | |
| }, 2000); | |
| } | |
| return; | |
| } | |
| const prompt = promptValue; | |
| const negativePrompt = negativeInput ? negativeInput.value.trim() : ''; | |
| AppState.isGenerating = true; | |
| UI.showLoading(previewContainer, 'Generating Image...'); | |
| try { | |
| const result = await APIService.generateImage(prompt, negativePrompt, AppState.settings); | |
| AppState.currentImage = result; | |
| // Save to history | |
| AppState.history.unshift(result); | |
| localStorage.setItem('generationHistory', JSON.stringify(AppState.history)); | |
| this.renderPreview(result); | |
| this.updateGallery(); | |
| } catch (error) { | |
| console.error('Generation error:', error); | |
| this.showErrorInPreview(preview || previewContainer, 'Generation failed. Please try again.'); | |
| } finally { | |
| AppState.isGenerating = false; | |
| } | |
| } | |
| showErrorInPreview(previewElement, message) { | |
| const container = previewElement?.shadowRoot?.getElementById('preview-container') || previewElement; | |
| if (!container) return; | |
| const errorDiv = document.createElement('div'); | |
| errorDiv.className = 'error-container flex flex-col items-center justify-center space-y-3 p-8 text-red-400'; | |
| errorDiv.innerHTML = ` | |
| <i data-feather="alert-circle" class="w-12 h-12"></i> | |
| <p class="text-center">${message}</p> | |
| <button class="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 rounded-lg text-sm transition-colors dismiss-error"> | |
| Dismiss | |
| </button> | |
| `; | |
| container.innerHTML = ''; | |
| container.appendChild(errorDiv); | |
| // Bind dismiss button | |
| errorDiv.querySelector('.dismiss-error')?.addEventListener('click', () => { | |
| errorDiv.remove(); | |
| }); | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| } | |
| renderPreview(item) { | |
| const preview = document.querySelector('generator-preview'); | |
| const container = preview?.shadowRoot?.getElementById('preview-container') || document.getElementById('preview-container'); | |
| if (!container) return; | |
| container.innerHTML = ` | |
| <div class="relative w-full h-full min-h-[400px] bg-slate-900 rounded-2xl overflow-hidden border border-slate-700"> | |
| <img src="${item.url}" class="w-full h-full object-contain max-h-[500px]" alt="Generated" onload="this.classList.add('loaded')" style="opacity: 0; transition: opacity 0.5s;" onload="this.style.opacity=1"> | |
| <!-- Action Bar --> | |
| <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-slate-950 to-transparent"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex gap-2"> | |
| <button onclick="app.generateVideoFromCurrent()" class="px-4 py-2 bg-secondary-600 hover:bg-secondary-500 text-white rounded-lg text-sm font-medium transition-all flex items-center gap-2 btn-glow"> | |
| <i data-feather="film" class="w-4 h-4"></i> | |
| Animate to Video | |
| </button> | |
| <button onclick="app.downloadImage('${item.url}')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm font-medium transition-all"> | |
| <i data-feather="download" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="text-xs text-slate-400 font-mono"> | |
| ${item.width || 1024}x${item.height || 1024} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Overlay --> | |
| <div class="absolute top-4 left-4 max-w-md"> | |
| <div class="glass-panel px-3 py-2 rounded-lg text-xs text-slate-300"> | |
| <span class="text-slate-500">Prompt:</span> ${item.prompt.substring(0, 100)}${item.prompt.length > 100 ? '...' : ''} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| } | |
| async generateVideoFromCurrent() { | |
| if (!AppState.currentImage) return; | |
| const preview = document.querySelector('generator-preview'); | |
| const container = preview?.shadowRoot?.getElementById('preview-container') || document.getElementById('preview-container'); | |
| this.showLoadingInContainer(container, 'Generating Video... This may take a minute'); | |
| try { | |
| const result = await APIService.generateVideo( | |
| AppState.currentImage.url, | |
| AppState.currentImage.prompt + ', cinematic motion, smooth animation', | |
| AppState.settings | |
| ); | |
| AppState.currentVideo = result; | |
| // Update history | |
| AppState.history.unshift(result); | |
| localStorage.setItem('generationHistory', JSON.stringify(AppState.history)); | |
| this.renderVideoPreview(result); | |
| this.updateGallery(); | |
| } catch (error) { | |
| console.error('Video generation error:', error); | |
| this.showErrorInPreview(preview, 'Video generation failed.'); | |
| } | |
| } | |
| showLoadingInContainer(container, text = 'Generating...') { | |
| if (!container) return; | |
| container.innerHTML = ` | |
| <div class="flex flex-col items-center justify-center space-y-4 p-8"> | |
| <div class="relative w-16 h-16"> | |
| <div class="absolute inset-0 border-4 border-purple-500/30 rounded-full"></div> | |
| <div class="absolute inset-0 border-4 border-t-purple-500 border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div> | |
| </div> | |
| <p class="text-purple-400 animate-pulse font-mono text-sm">${text}</p> | |
| <div class="w-48 h-1 bg-slate-800 rounded-full overflow-hidden"> | |
| <div class="loading-bar h-full bg-gradient-to-r from-purple-500 to-cyan-500" style="width: 0%;"></div> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes loadingProgress { | |
| 0% { width: 0%; } | |
| 100% { width: 100%; } | |
| } | |
| .loading-bar { | |
| animation: loadingProgress 3s ease-out forwards; | |
| } | |
| </style> | |
| `; | |
| } | |
| renderVideoPreview(item) { | |
| const preview = document.querySelector('generator-preview'); | |
| const container = preview?.shadowRoot?.getElementById('preview-container') || document.getElementById('preview-container'); | |
| if (!container) return; | |
| container.innerHTML = ` | |
| <div class="relative w-full h-full min-h-[400px] bg-black rounded-2xl overflow-hidden border border-secondary-500/30"> | |
| <div class="video-container w-full h-full flex items-center justify-center"> | |
| <img src="${item.videoUrl}" class="w-full h-full object-contain" alt="Video thumbnail"> | |
| <!-- Play Button Overlay --> | |
| <div class="absolute inset-0 flex items-center justify-center bg-black/40"> | |
| <button class="w-20 h-20 bg-secondary-500/90 hover:bg-secondary-400 rounded-full flex items-center justify-center text-white transition-all hover:scale-110 group"> | |
| <i data-feather="play" class="w-8 h-8 fill-current group-hover:translate-x-1 transition-transform"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black to-transparent"> | |
| <div class="flex items-center justify-between text-white"> | |
| <div class="flex items-center gap-3"> | |
| <span class="px-2 py-1 bg-secondary-500/20 rounded text-xs font-mono text-secondary-400">MP4</span> | |
| <span class="text-sm">${item.duration}s @ 24fps</span> | |
| </div> | |
| <button onclick="app.downloadImage('${item.videoUrl}')" class="p-2 hover:bg-white/10 rounded-lg transition-colors"> | |
| <i data-feather="download" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } | |
| animateImage(id) { | |
| const item = AppState.history.find(h => h.id === id); | |
| if (item && item.type === 'image') { | |
| AppState.currentImage = item; | |
| this.generateVideoFromCurrent(); | |
| } | |
| } | |
| downloadImage(url) { | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `dream-machine-${Date.now()}.png`; | |
| a.target = '_blank'; | |
| a.click(); | |
| } | |
| deleteItem(id) { | |
| AppState.history = AppState.history.filter(h => h.id !== id); | |
| localStorage.setItem('generationHistory', JSON.stringify(AppState.history)); | |
| this.updateGallery(); | |
| } | |
| updateGallery() { | |
| const gallery = document.querySelector('gallery-grid'); | |
| const grid = gallery?.shadowRoot?.getElementById('gallery-grid') || document.getElementById('gallery-grid'); | |
| const countIndicator = gallery?.shadowRoot?.getElementById('gallery-count'); | |
| if (countIndicator) { | |
| countIndicator.textContent = `${AppState.history.length} items`; | |
| } | |
| if (!grid) return; | |
| if (AppState.history.length === 0) { | |
| grid.innerHTML = ` | |
| <div class="empty-state"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="margin: 0 auto 1rem; opacity: 0.3;"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> | |
| <p>No generations yet</p> | |
| <p style="font-size: 0.875rem; margin-top: 0.5rem; opacity: 0.7;">Your creations will be saved here automatically</p> | |
| </div> | |
| `; | |
| } else { | |
| grid.innerHTML = AppState.history.map(item => this.createImageCard(item)).join(''); | |
| } | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| } | |
| createImageCard(item) { | |
| const isVideo = item.type === 'video'; | |
| return ` | |
| <div class="group relative bg-slate-900/50 rounded-xl overflow-hidden border border-slate-700/50 hover:border-purple-500/50 transition-all duration-300" style="box-shadow: 0 0 20px rgba(139, 92, 246, 0.1);" data-id="${item.id}"> | |
| <div class="aspect-video relative overflow-hidden bg-slate-800"> | |
| <img src="${item.url || item.videoUrl}" alt="${item.prompt}" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-60"></div> | |
| <div class="absolute inset-0 bg-slate-950/80 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3"> | |
| ${!isVideo ? `<button class="animate-image-btn p-3 bg-purple-600 hover:bg-purple-500 rounded-full text-white transition-all hover:scale-110" data-id="${item.id}" title="Generate Video"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg> | |
| </button>` : ''} | |
| <button class="download-btn p-3 bg-slate-700 hover:bg-slate-600 rounded-full text-white transition-all hover:scale-110" data-url="${item.url || item.videoUrl}" title="Download"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| </button> | |
| <button class="delete-btn p-3 bg-red-500/20 hover:bg-red-500/40 rounded-full text-red-400 transition-all hover:scale-110" data-id="${item.id}" title="Delete"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> | |
| </button> | |
| </div> | |
| ${isVideo ? ` | |
| <div class="absolute top-3 right-3 bg-cyan-500/90 text-white px-2 py-1 rounded text-xs font-bold flex items-center gap-1"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg> | |
| VIDEO | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="p-4 space-y-2"> | |
| <p class="text-sm text-slate-300 line-clamp-2 font-medium">${item.prompt}</p> | |
| <div class="flex items-center justify-between text-xs text-slate-500"> | |
| <span>${Utils.formatDate(new Date(item.timestamp))}</span> | |
| <span class="font-mono">Seed: ${item.seed || 'N/A'}</span> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| loadHistory() { | |
| this.updateGallery(); | |
| } | |
| enhancePrompt() { | |
| const controls = document.querySelector('generator-controls'); | |
| const input = document.getElementById('prompt-input') || controls?.shadowRoot?.getElementById('prompt-input'); | |
| if (!input) return; | |
| const current = input.value; | |
| const enhancers = [ | |
| '8k resolution, photorealistic, highly detailed, professional photography', | |
| 'cinematic lighting, unreal engine 5, octane render', | |
| 'masterpiece, best quality, intricate details', | |
| 'trending on artstation, sharp focus, vivid colors' | |
| ]; | |
| const randomEnhancer = enhancers[Math.floor(Math.random() * enhancers.length)]; | |
| input.value = current ? `${current}, ${randomEnhancer}` : randomEnhancer; | |
| // Visual feedback | |
| input.classList.add('ring-2', 'ring-secondary-500'); | |
| setTimeout(() => input.classList.remove('ring-2', 'ring-secondary-500'), 500); | |
| } | |
| } | |
| // Gallery button event delegation | |
| document.addEventListener('click', (e) => { | |
| const animateBtn = e.target.closest('.animate-image-btn'); | |
| const downloadBtn = e.target.closest('.download-btn'); | |
| const deleteBtn = e.target.closest('.delete-btn'); | |
| if (animateBtn) { | |
| e.stopPropagation(); | |
| const id = animateBtn.getAttribute('data-id'); | |
| app.animateImage(id); | |
| } | |
| if (downloadBtn) { | |
| e.stopPropagation(); | |
| const url = downloadBtn.getAttribute('data-url'); | |
| app.downloadImage(url); | |
| } | |
| if (deleteBtn) { | |
| e.stopPropagation(); | |
| const id = deleteBtn.getAttribute('data-id'); | |
| app.deleteItem(id); | |
| } | |
| }); | |
| // Initialize | |
| const app = new DreamMachineApp(); | |
| // Expose to window for onclick handlers | |
| window.app = app; | |