// Configuration and state let uploadedImages = []; let imageDimensions = []; let generationHistory = []; let currentGeneration = null; let activeTab = 'current'; // Initialize local storage const HISTORY_KEY = 'seedream_generation_history'; // Load history from localStorage on startup function loadHistory() { try { const saved = localStorage.getItem(HISTORY_KEY); if (saved) { generationHistory = JSON.parse(saved); updateHistoryCount(); } } catch (error) { console.error('Error loading history:', error); generationHistory = []; } } // Save history to localStorage function saveHistory() { try { // Keep only last 100 generations to avoid storage limits if (generationHistory.length > 100) { generationHistory = generationHistory.slice(-100); } localStorage.setItem(HISTORY_KEY, JSON.stringify(generationHistory)); updateHistoryCount(); } catch (error) { console.error('Error saving history:', error); } } // DOM Elements const fileInput = document.getElementById('fileInput'); const imagePreview = document.getElementById('imagePreview'); const imageUrls = document.getElementById('imageUrls'); const generateBtn = document.getElementById('generateBtn'); const statusMessage = document.getElementById('statusMessage'); const progressLogs = document.getElementById('progressLogs'); const currentResults = document.getElementById('currentResults'); const currentInfo = document.getElementById('currentInfo'); const historyGrid = document.getElementById('historyGrid'); const imageSizeSelect = document.getElementById('imageSize'); const customSizeElements = document.querySelectorAll('.custom-size'); const modelSelect = document.getElementById('modelSelect'); const promptTitle = document.getElementById('promptTitle'); const promptLabel = document.getElementById('promptLabel'); const imageInputCard = document.getElementById('imageInputCard'); const settingsCard = document.getElementById('settingsCard'); // Event Listeners fileInput.addEventListener('change', handleFileUpload); generateBtn.addEventListener('click', generateEdit); imageSizeSelect.addEventListener('change', handleImageSizeChange); modelSelect.addEventListener('change', handleModelChange); // Tab switching function switchTab(tabName) { activeTab = tabName; // Update tab buttons document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); event.target.classList.add('active'); // Update tab content document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); if (tabName === 'current') { document.getElementById('currentTab').classList.add('active'); } else { document.getElementById('historyTab').classList.add('active'); displayHistory(); } } // Toggle settings panel function toggleSettings() { settingsCard.classList.toggle('collapsed'); } // Handle image size dropdown change function handleImageSizeChange() { if (imageSizeSelect.value === 'custom') { customSizeElements.forEach(el => el.style.display = 'block'); } else { customSizeElements.forEach(el => el.style.display = 'none'); } } // Handle model dropdown change function handleModelChange() { const value = modelSelect.value; const isTextToImage = value === 'fal-ai/bytedance/seedream/v4/text-to-image' || value === 'fal-ai/bytedance/seedream/v4.5/text-to-image' || value === 'fal-ai/hunyuan-image/v3/text-to-image'; const isTextToVideo = value === 'fal-ai/bytedance/seedance/v1/pro/fast/text-to-video'; const isImageToVideo = value === 'fal-ai/bytedance/seedance/v1/pro/fast/image-to-video'; // Toggle prompt and input image area if (isTextToImage || isTextToVideo) { promptTitle.textContent = 'Generation Prompt'; promptLabel.textContent = 'Generation Prompt'; document.getElementById('prompt').placeholder = 'e.g., A cinematic shot of a subject, trending film look'; imageInputCard.style.display = isTextToVideo ? 'none' : 'none'; // text-to-image also hides inputs by design here uploadedImages = []; imageDimensions = []; renderImagePreviews(); } else if (isImageToVideo) { promptTitle.textContent = 'Generation Prompt'; promptLabel.textContent = 'Generation Prompt'; document.getElementById('prompt').placeholder = 'e.g., Animate this image with smooth camera motion'; imageInputCard.style.display = 'block'; } else { promptTitle.textContent = 'Edit Instructions'; promptLabel.textContent = 'Editing Prompt'; document.getElementById('prompt').placeholder = 'e.g., Dress the model in the clothes and shoes.'; imageInputCard.style.display = 'block'; } // Toggle Settings sections const imageSettings = document.getElementById('imageSettings'); const videoSettings = document.getElementById('videoSettings'); if (imageSettings && videoSettings) { if (isTextToVideo || isImageToVideo) { imageSettings.style.display = 'none'; videoSettings.style.display = 'block'; } else { imageSettings.style.display = 'block'; videoSettings.style.display = 'none'; } } } // Initialize on page load window.addEventListener('DOMContentLoaded', () => { // Load saved API key const savedKey = localStorage.getItem('fal_api_key'); if (savedKey) { document.getElementById('apiKey').value = savedKey; } // Initialize UI state handleImageSizeChange(); handleModelChange(); loadHistory(); displayHistory(); // Collapse settings by default settingsCard.classList.add('collapsed'); }); // Handle file upload with immediate preview async function handleFileUpload(event) { const files = Array.from(event.target.files); if (files.length === 0) return; showStatus(`Processing ${files.length} image(s)...`, 'info'); let processedCount = 0; let errorCount = 0; for (const file of files) { if (uploadedImages.length >= 10) { showStatus('Maximum 10 images allowed. Some images were not added.', 'error'); break; } if (file.type.startsWith('image/')) { try { const tempIndex = uploadedImages.length; const loadingId = `loading-${Date.now()}-${tempIndex}`; // Add loading placeholder const loadingPreview = document.createElement('div'); loadingPreview.className = 'image-preview-item loading-preview'; loadingPreview.id = loadingId; loadingPreview.innerHTML = `

${file.name}

`; imagePreview.appendChild(loadingPreview); const reader = new FileReader(); reader.onerror = (error) => { console.error('Error reading file:', file.name, error); errorCount++; document.getElementById(loadingId)?.remove(); showStatus(`Failed to read file: ${file.name}`, 'error'); }; reader.onload = (e) => { const dataUrl = e.target.result; document.getElementById(loadingId)?.remove(); uploadedImages.push(dataUrl); processedCount++; // Get image dimensions const img = new Image(); img.onload = function() { imageDimensions.push({ width: this.width, height: this.height }); addImagePreview(dataUrl, uploadedImages.length - 1); updateCustomSizeFromLastImage(); if (processedCount + errorCount === files.length) { if (errorCount === 0) { showStatus(`Successfully added ${processedCount} image(s) (${uploadedImages.length}/10 slots used)`, 'success'); } else { showStatus(`Added ${processedCount} image(s), ${errorCount} failed (${uploadedImages.length}/10 slots used)`, 'warning'); } } }; img.onerror = () => { console.error('Error loading image dimensions for:', file.name); imageDimensions.push({ width: 1280, height: 1280 }); addImagePreview(dataUrl, uploadedImages.length - 1); updateCustomSizeFromLastImage(); }; img.src = dataUrl; }; reader.readAsDataURL(file); } catch (error) { console.error('Error processing file:', file.name, error); errorCount++; showStatus(`Error processing ${file.name}`, 'error'); } } else { errorCount++; showStatus(`${file.name} is not an image file`, 'error'); } } event.target.value = ''; } // Add image preview with upload status indicator function addImagePreview(src, index) { const previewItem = document.createElement('div'); previewItem.className = 'image-preview-item'; previewItem.dataset.imageIndex = index; const imageId = `upload-img-${Date.now()}-${index}`; previewItem.innerHTML = ` Upload ${index + 1} `; imagePreview.appendChild(previewItem); } // Remove image function removeImage(index) { uploadedImages.splice(index, 1); imageDimensions.splice(index, 1); renderImagePreviews(); updateCustomSizeFromLastImage(); } // Update custom size based on last image function updateCustomSizeFromLastImage() { if (imageDimensions.length > 0) { const lastDims = imageDimensions[imageDimensions.length - 1]; let width = lastDims.width; let height = lastDims.height; // Calculate aspect ratio const aspectRatio = width / height; // If both dimensions are less than 1024 (minimum allowed), scale up proportionally if (width < 1024 && height < 1024) { // Scale based on which dimension needs more scaling to reach 1024 if (width < height) { // Height is larger, scale based on width minimum width = 1024; height = Math.round(1024 / aspectRatio); } else { // Width is larger or equal, scale based on height minimum height = 1024; width = Math.round(1024 * aspectRatio); } } // If any dimension exceeds 4096, scale down proportionally if (width > 4096 || height > 4096) { if (width > height) { // Width exceeds more, scale based on width maximum const scaleFactor = 4096 / width; width = 4096; height = Math.round(height * scaleFactor); } else { // Height exceeds more, scale based on height maximum const scaleFactor = 4096 / height; height = 4096; width = Math.round(width * scaleFactor); } } // Final bounds check while maintaining aspect ratio // If width is still below minimum after height-based scaling if (width < 1024) { const scaleFactor = 1024 / width; width = 1024; height = Math.round(height * scaleFactor); } // If height is still below minimum after width-based scaling if (height < 1024) { const scaleFactor = 1024 / height; height = 1024; width = Math.round(width * scaleFactor); } // Final check to ensure we don't exceed maximums width = Math.min(4096, width); height = Math.min(4096, height); document.getElementById('customWidth').value = width; document.getElementById('customHeight').value = height; if (imageSizeSelect.value !== 'custom') { imageSizeSelect.value = 'custom'; handleImageSizeChange(); } } } // Re-render all image previews function renderImagePreviews() { imagePreview.innerHTML = ''; uploadedImages.forEach((src, index) => { addImagePreview(src, index); }); } // Show status message function showStatus(message, type = 'info') { statusMessage.className = `status-message ${type}`; statusMessage.textContent = message; statusMessage.style.display = 'block'; if (type === 'success' || type === 'error') { setTimeout(() => { statusMessage.style.display = 'none'; }, 5000); } } // Add log entry function addLog(message) { const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`; progressLogs.appendChild(logEntry); progressLogs.scrollTop = progressLogs.scrollHeight; } // Clear logs function clearLogs() { progressLogs.innerHTML = ''; progressLogs.classList.remove('active'); } // Get image size configuration function getImageSize() { const size = imageSizeSelect.value; if (size === 'custom') { return { width: parseInt(document.getElementById('customWidth').value), height: parseInt(document.getElementById('customHeight').value) }; } return size; } // Upload image to FAL storage with progress tracking async function uploadImageToFal(imageData, apiKey, imageIndex, totalImages, actualIndex) { try { // Show individual image upload status if preview exists const previewItem = document.querySelector(`.image-preview-item[data-image-index="${actualIndex}"]`); if (previewItem) { const statusDiv = previewItem.querySelector('.image-upload-status'); const statusText = previewItem.querySelector('.upload-status-text'); const progressBar = previewItem.querySelector('.upload-progress-mini-bar'); if (statusDiv) { statusDiv.style.display = 'block'; statusText.textContent = 'Uploading...'; progressBar.style.width = '30%'; previewItem.classList.add('uploading'); } } // Update main progress bar updateUploadProgress(imageIndex - 1, totalImages, `Uploading image ${imageIndex}/${totalImages}...`); // Show upload start message addLog(`Uploading image ${imageIndex}/${totalImages} to FAL storage...`); // Calculate approximate size for logging const sizeInMB = (imageData.length * 0.75 / 1024 / 1024).toFixed(2); addLog(`Image ${imageIndex} size: ~${sizeInMB} MB`); const response = await fetch('/api/upload-to-fal', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ image_data: imageData }) }); if (!response.ok) { const error = await response.text(); throw new Error(error || 'Failed to upload image to FAL'); } const data = await response.json(); addLog(`✓ Image ${imageIndex}/${totalImages} uploaded successfully`); // Update progress after successful upload updateUploadProgress(imageIndex, totalImages, `Completed ${imageIndex}/${totalImages}`); // Update individual image status for success if (previewItem) { const statusText = previewItem.querySelector('.upload-status-text'); const progressBar = previewItem.querySelector('.upload-progress-mini-bar'); if (progressBar && statusText) { progressBar.style.width = '100%'; statusText.textContent = 'Uploaded ✓'; previewItem.classList.remove('uploading'); previewItem.classList.add('uploaded'); // Fade out status after 3 seconds setTimeout(() => { const statusDiv = previewItem.querySelector('.image-upload-status'); if (statusDiv) { statusDiv.style.opacity = '0'; setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.style.opacity = '1'; previewItem.classList.remove('uploaded'); }, 300); } }, 3000); } } return data.url; } catch (error) { console.error('Error uploading to FAL:', error); addLog(`✗ Failed to upload image ${imageIndex}: ${error.message}`); // Update individual image status for error const previewItem = document.querySelector(`.image-preview-item[data-image-index="${actualIndex}"]`); if (previewItem) { const statusText = previewItem.querySelector('.upload-status-text'); const progressBar = previewItem.querySelector('.upload-progress-mini-bar'); if (progressBar && statusText) { progressBar.style.width = '100%'; progressBar.style.backgroundColor = '#dc3545'; statusText.textContent = 'Upload failed ✗'; previewItem.classList.remove('uploading'); previewItem.classList.add('upload-failed'); } } throw error; } } // Update upload progress bar function updateUploadProgress(completed, total, message) { const progressFill = document.getElementById('uploadProgressFill'); const progressText = document.getElementById('uploadProgressText'); const container = document.getElementById('uploadProgressContainer'); if (progressFill && progressText && container) { const percentage = Math.round((completed / total) * 100); progressFill.style.width = `${percentage}%`; progressText.textContent = `${message} (${percentage}%)`; // Add animation class on completion if (percentage === 100) { progressFill.classList.add('complete'); // Wait briefly to show completion state, then fade out setTimeout(() => { // Add transition for smooth fade container.style.transition = 'opacity 0.5s ease'; container.style.opacity = '0'; // Remove from DOM after fade completes setTimeout(() => { if (container.parentNode) { container.parentNode.removeChild(container); } }, 500); }, 1500); } else { // Ensure visible if not complete container.style.opacity = '1'; } } } // Prepare image URLs for API with detailed progress tracking async function getImageUrlsForAPI() { const urls = []; const apiKey = getAPIKey(); // Count total images to upload const base64Images = uploadedImages.filter(img => img.startsWith('data:')); const urlImages = uploadedImages.filter(img => !img.startsWith('data:')); const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim()); const totalUploads = base64Images.length; const totalImages = uploadedImages.length + textUrls.length; if (totalUploads > 0) { addLog(`Preparing to upload ${totalUploads} image(s) to FAL storage...`); showStatus(`Uploading ${totalUploads} image(s) to FAL storage...`, 'info'); } // Process uploaded base64 images - upload to FAL first let uploadCount = 0; for (let i = 0; i < uploadedImages.length; i++) { const imageData = uploadedImages[i]; // If it's a base64 data URL, upload to FAL if (imageData.startsWith('data:')) { uploadCount++; try { const falUrl = await uploadImageToFal(imageData, apiKey, uploadCount, totalUploads, i); urls.push(falUrl); // Update progress status if (uploadCount < totalUploads) { const percentage = Math.round((uploadCount / totalUploads) * 100); showStatus(`Upload progress: ${uploadCount}/${totalUploads} (${percentage}%)`, 'info'); } } catch (error) { showStatus(`Upload failed for image ${uploadCount}: ${error.message}`, 'error'); throw error; } } else { // Already a URL, use as-is urls.push(imageData); addLog(`Using existing URL for image ${i + 1}`); } } // Add text URLs directly if (textUrls.length > 0) { addLog(`Processing ${textUrls.length} URL(s) from text input...`); } for (const url of textUrls) { urls.push(url); addLog(`Added URL: ${url.substring(0, 50)}...`); await getImageDimensionsFromUrl(url); } if (totalUploads > 0) { showStatus(`All ${totalUploads} image(s) uploaded successfully!`, 'success'); addLog(`Upload complete: ${totalImages} total image(s) ready for generation`); } return urls.slice(0, 10); } // Get image dimensions from URL async function getImageDimensionsFromUrl(url) { return new Promise((resolve) => { const img = new Image(); img.onload = function() { imageDimensions.push({ width: this.width, height: this.height }); updateCustomSizeFromLastImage(); resolve(); }; img.onerror = function() { resolve(); }; img.src = url; }); } // Generate edit async function generateEdit() { const prompt = document.getElementById('prompt').value.trim(); if (!prompt) { showStatus('Please enter a prompt', 'error'); return; } const selectedModel = modelSelect.value; const isTextToImage = selectedModel === 'fal-ai/bytedance/seedream/v4/text-to-image' || selectedModel === 'fal-ai/bytedance/seedream/v4.5/text-to-image' || selectedModel === 'fal-ai/hunyuan-image/v3/text-to-image'; const isTextToVideo = selectedModel === 'fal-ai/bytedance/seedance/v1/pro/fast/text-to-video'; const isImageToVideo = selectedModel === 'fal-ai/bytedance/seedance/v1/pro/fast/image-to-video'; // Determine if current model requires image inputs const needsImageInputs = (!isTextToImage && !isTextToVideo); let imageUrlsArray = []; if (needsImageInputs) { // Prepare upload progress UI early if there are base64 uploads const base64Images = uploadedImages.filter(img => img.startsWith('data:')); const totalUploads = base64Images.length; // Remove any existing progress container const existingProgress = document.getElementById('uploadProgressContainer'); if (existingProgress && existingProgress.parentNode) { existingProgress.parentNode.removeChild(existingProgress); } if (totalUploads > 0) { const progressContainer = document.createElement('div'); progressContainer.id = 'uploadProgressContainer'; progressContainer.className = 'upload-progress-container'; progressContainer.innerHTML = `
Initializing...
`; // Insert progress container after status message if (statusMessage.parentNode) { statusMessage.parentNode.insertBefore(progressContainer, statusMessage.nextSibling); } } // Now resolve image URLs (this may trigger uploads and progress updates) try { imageUrlsArray = await getImageUrlsForAPI(); } catch (error) { // Ensure progress UI is removed on upload failure const pc = document.getElementById('uploadProgressContainer'); if (pc && pc.parentNode) { pc.parentNode.removeChild(pc); } addLog(`Upload error: ${error.message || error}`); showStatus(`Upload error: ${error.message || error}`, 'error'); return; } // Validate inputs for specific modes if (isImageToVideo && imageUrlsArray.length === 0) { showStatus('Please upload an image or provide an image URL for image-to-video', 'error'); const pc = document.getElementById('uploadProgressContainer'); if (pc && pc.parentNode) pc.parentNode.removeChild(pc); return; } else if (!isImageToVideo && imageUrlsArray.length === 0) { showStatus('Please upload images or provide image URLs for image editing', 'error'); const pc = document.getElementById('uploadProgressContainer'); if (pc && pc.parentNode) pc.parentNode.removeChild(pc); return; } } generateBtn.disabled = true; generateBtn.querySelector('.btn-text').textContent = 'Generating...'; generateBtn.querySelector('.spinner').style.display = 'block'; // Clear current results currentResults.innerHTML = '

Preparing generation...

'; currentInfo.innerHTML = ''; clearLogs(); showStatus('Starting generation process...', 'info'); progressLogs.classList.add('active'); // Show initial status if (!isTextToImage && imageUrlsArray.length > 0) { addLog(`Processing ${imageUrlsArray.length} input image(s)...`); } let requestData; if (isTextToVideo) { requestData = { prompt: prompt, aspect_ratio: (document.getElementById('videoAspectRatio')?.value) || 'auto', resolution: (document.getElementById('videoResolution')?.value) || '1080p', duration: (document.getElementById('videoDuration')?.value) || '5', camera_fixed: !!(document.getElementById('cameraFixed')?.checked), enable_safety_checker: false }; } else if (isImageToVideo) { requestData = { prompt: prompt, image_url: imageUrlsArray[0], aspect_ratio: (document.getElementById('videoAspectRatio')?.value) || 'auto', resolution: (document.getElementById('videoResolution')?.value) || '1080p', duration: (document.getElementById('videoDuration')?.value) || '5', camera_fixed: !!(document.getElementById('cameraFixed')?.checked), enable_safety_checker: false }; } else { requestData = { prompt: prompt, image_size: getImageSize(), num_images: parseInt(document.getElementById('numImages').value), enable_safety_checker: false }; if (!isTextToImage) { // Note: imageUrlsArray will now contain FAL URLs after upload requestData.image_urls = imageUrlsArray; requestData.max_images = parseInt(document.getElementById('maxImages').value); } } const seed = document.getElementById('seed').value; if (seed) { requestData.seed = parseInt(seed); } // Store generation metadata currentGeneration = { id: Date.now(), timestamp: new Date().toISOString(), prompt: prompt, model: selectedModel, settings: { image_size: requestData.image_size, num_images: requestData.num_images, seed: requestData.seed } }; try { const apiKey = getAPIKey(); if (!apiKey) { showStatus('Please enter your FAL API key', 'error'); addLog('API key not found'); document.getElementById('apiKey').focus(); return; } addLog('Submitting request to FAL API...'); addLog(`Model: ${selectedModel}`); addLog(`Prompt: ${prompt}`); if (!isTextToImage && !isTextToVideo) { addLog(`Number of input images: ${imageUrlsArray.length}`); } const response = await callFalAPI(apiKey, requestData, selectedModel); // Store results in current generation currentGeneration.results = response; // Display results displayCurrentResults(response); // Add to history generationHistory.push(currentGeneration); saveHistory(); showStatus('Generation completed successfully!', 'success'); } catch (error) { console.error('Error:', error); showStatus(`Error: ${error.message}`, 'error'); addLog(`Error: ${error.message}`); } finally { generateBtn.disabled = false; generateBtn.querySelector('.btn-text').textContent = 'Generate'; generateBtn.querySelector('.spinner').style.display = 'none'; // Ensure any lingering upload progress UI is removed const pc2 = document.getElementById('uploadProgressContainer'); if (pc2 && pc2.parentNode) { pc2.parentNode.removeChild(pc2); } } } // Get API key function getAPIKey() { const apiKeyInput = document.getElementById('apiKey'); const apiKey = apiKeyInput.value.trim(); if (apiKey) { localStorage.setItem('fal_api_key', apiKey); } return apiKey || localStorage.getItem('fal_api_key'); } // Call FAL API (non-blocking) async function callFalAPI(apiKey, requestData, model) { const submitResponse = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'X-Model-Endpoint': model }, body: JSON.stringify(requestData) }); if (!submitResponse.ok) { const error = await submitResponse.text(); throw new Error(error || 'API request failed'); } const submitData = await submitResponse.json(); const { request_id } = submitData; addLog(`Request submitted with ID: ${request_id}`); // Poll for results let attempts = 0; const isVideoModel = model.includes('text-to-video') || model.includes('image-to-video'); const maxAttempts = isVideoModel ? 900 : 120; const pollInterval = 1000; let previousLogCount = 0; while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, pollInterval)); const statusUrl = `/api/status/${request_id}`; const statusResponse = await fetch(statusUrl); if (!statusResponse.ok) { throw new Error('Failed to check request status'); } const statusData = await statusResponse.json(); // Add any new logs if (statusData.logs && statusData.logs.length > previousLogCount) { const newLogs = statusData.logs.slice(previousLogCount); newLogs.forEach(log => { if (log && !log.includes('Request submitted')) { addLog(log); } }); previousLogCount = statusData.logs.length; } if (statusData.status === 'completed') { return statusData.result; } else if (statusData.status === 'error') { throw new Error(statusData.error || 'Generation failed'); } attempts++; if (attempts % 5 === 0) { addLog(`Processing... (${attempts}s elapsed)`); } } throw new Error('Request timed out'); } // Display current results function displayCurrentResults(response) { currentResults.innerHTML = ''; let displayed = false; // Images if (response && response.images && response.images.length > 0) { response.images.forEach((image, index) => { const imgSrc = image.url || image.file_data || ''; const imageId = `current-img-${Date.now()}-${index}`; const item = document.createElement('div'); item.className = 'generation-item'; item.innerHTML = ` Result ${index + 1} `; currentResults.appendChild(item); }); displayed = true; } // Video if (response && response.video) { const vidSrc = response.video.url || response.video.file_data || ''; if (vidSrc) { const item = document.createElement('div'); item.className = 'generation-item'; item.innerHTML = ` `; currentResults.appendChild(item); displayed = true; } } // Display generation info if (response && response.seed) { currentInfo.innerHTML = `Seed: ${response.seed}`; addLog(`Seed used: ${response.seed}`); } if (!displayed) { currentResults.innerHTML = '

No results

'; } else { if (response.images && response.images.length) { addLog(`Generated ${response.images.length} image(s)`); } if (response.video) { addLog('Generated 1 video'); } } } // Display history function displayHistory() { if (generationHistory.length === 0) { historyGrid.innerHTML = `

No generation history

Your generated content will be saved here
`; return; } historyGrid.innerHTML = ''; // Display history in reverse order (newest first) [...generationHistory].reverse().forEach((generation) => { if (!generation.results) return; // Images if (generation.results.images) { generation.results.images.forEach((image, imgIndex) => { const imgSrc = image.url || image.file_data || ''; const imageId = `history-img-${generation.id}-${imgIndex}`; const item = document.createElement('div'); item.className = 'generation-item'; item.innerHTML = ` Generation
${new Date(generation.timestamp).toLocaleString()} ${generation.prompt}
`; historyGrid.appendChild(item); }); } // Video if (generation.results.video) { const vidSrc = generation.results.video.url || generation.results.video.file_data || ''; if (vidSrc) { const item = document.createElement('div'); item.className = 'generation-item'; item.innerHTML = `
${new Date(generation.timestamp).toLocaleString()} ${generation.prompt}
`; historyGrid.appendChild(item); } } }); } // Use image as input async function useAsInput(imageId, imageSrc) { try { // Switch to edit mode if in text-to-image mode const currentModel = modelSelect.value; if (currentModel === 'fal-ai/bytedance/seedream/v4/text-to-image' || currentModel === 'fal-ai/bytedance/seedream/v4.5/text-to-image' || currentModel === 'fal-ai/hunyuan-image/v3/text-to-image') { modelSelect.value = 'fal-ai/bytedance/seedream/v4.5/edit'; handleModelChange(); showStatus('Switched to Image Edit mode', 'info'); } if (uploadedImages.length >= 10) { showStatus('Maximum 10 images allowed. Please remove some images first.', 'error'); return; } // If the image is already a FAL URL (from history), use it directly // Otherwise, it's a base64 image that will be uploaded when generating uploadedImages.push(imageSrc); // Get dimensions const imgElement = document.getElementById(imageId); if (imgElement) { if (!imgElement.complete) { await new Promise((resolve) => { imgElement.onload = resolve; imgElement.onerror = resolve; }); } imageDimensions.push({ width: imgElement.naturalWidth || imgElement.width, height: imgElement.naturalHeight || imgElement.height }); updateCustomSizeFromLastImage(); } else { const img = new Image(); img.onload = function() { imageDimensions.push({ width: this.width, height: this.height }); updateCustomSizeFromLastImage(); }; img.onerror = function() { imageDimensions.push({ width: 1280, height: 1280 }); updateCustomSizeFromLastImage(); }; img.src = imageSrc; } renderImagePreviews(); const totalImages = uploadedImages.length; showStatus(`Image added as input (${totalImages}/10 slots used)`, 'success'); addLog(`Added image as input (${totalImages}/10 images)`); // Flash animation imagePreview.style.animation = 'flash 0.5s'; setTimeout(() => { imagePreview.style.animation = ''; }, 500); } catch (error) { console.error('Error using image as input:', error); showStatus('Failed to add image as input', 'error'); } } // Clear all input images function clearAllInputImages() { uploadedImages = []; imageDimensions = []; renderImagePreviews(); showStatus('All input images cleared', 'info'); } // Clear history function clearHistory() { if (confirm('Are you sure you want to clear all generation history? This cannot be undone.')) { generationHistory = []; localStorage.removeItem(HISTORY_KEY); displayHistory(); updateHistoryCount(); showStatus('History cleared', 'info'); } } // Download all history function downloadAllHistory() { if (generationHistory.length === 0) { showStatus('No history to download', 'error'); return; } // Create a zip file or download each asset generationHistory.forEach((generation, genIndex) => { if (generation.results) { if (generation.results.images) { generation.results.images.forEach((image, imgIndex) => { const imgSrc = image.url || image.file_data || ''; if (imgSrc) { const link = document.createElement('a'); link.href = imgSrc; link.download = `seedream-${generation.id}-${imgIndex}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }); } if (generation.results.video) { const vidSrc = generation.results.video.url || generation.results.video.file_data || ''; if (vidSrc) { const link = document.createElement('a'); link.href = vidSrc; link.download = `seedance-${generation.id}.mp4`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } } }); showStatus('Downloading all assets...', 'info'); } // Update history count function updateHistoryCount() { const countElement = document.getElementById('historyCount'); if (countElement) { let totalItems = 0; generationHistory.forEach(gen => { if (gen.results && gen.results.images) { totalItems += gen.results.images.length; } if (gen.results && gen.results.video) { totalItems += 1; } }); countElement.textContent = totalItems; } } // Modal functionality for viewing large images let currentModalImage = null; function openImageModal(imageId, imageSrc, prompt, timestamp) { const modal = document.getElementById('imageModal'); const modalImg = document.getElementById('modalImage'); const modalCaption = document.getElementById('modalCaption'); // Store current image info for use in "Use as Input" currentModalImage = { id: imageId, src: imageSrc }; // Set modal content modalImg.src = imageSrc; modalCaption.innerHTML = ` Generated: ${timestamp}
Prompt: ${prompt} `; // Show modal modal.classList.add('show'); // Prevent body scroll when modal is open document.body.style.overflow = 'hidden'; // Close modal on escape key document.addEventListener('keydown', handleModalEscape); // Close modal on clicking outside the image modal.onclick = function(event) { if (event.target === modal || event.target === modalImg.parentElement) { closeImageModal(); } }; } function closeImageModal() { const modal = document.getElementById('imageModal'); modal.classList.remove('show'); document.body.style.overflow = ''; document.removeEventListener('keydown', handleModalEscape); currentModalImage = null; } function handleModalEscape(event) { if (event.key === 'Escape') { closeImageModal(); } } function useModalImageAsInput() { if (currentModalImage) { useAsInput(currentModalImage.id, currentModalImage.src); closeImageModal(); } }