import { useEffect, useState } from 'react' import JSZip from 'jszip' import ImageLightbox from './ImageLightbox' const API_BASE = '/api' const AUTH_TOKEN_KEY = 'amalfa_auth_token' function authHeaders() { const token = typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_KEY) : null if (!token) return {} return { Authorization: `Bearer ${token}` } } function proxyImageUrl(url) { return url ? `${API_BASE}/proxy-image?url=${encodeURIComponent(url)}` : '' } function downloadImage(url, filename) { const a = document.createElement('a') a.href = proxyImageUrl(url) || url a.download = filename document.body.appendChild(a) a.click() a.remove() } async function fetchImageBlob(url) { if (!url) throw new Error('Missing image URL') try { const proxied = await fetch(proxyImageUrl(url)) if (proxied.ok) return await proxied.blob() } catch (_) {} const direct = await fetch(url) if (!direct.ok) throw new Error('Failed to fetch image') return await direct.blob() } export default function Variations({ onLogout }) { const [imageModels, setImageModels] = useState([]) const [selectedImageModel, setSelectedImageModel] = useState('nano-banana-2') const [aspectRatio, setAspectRatio] = useState('1:1') const [variationCount, setVariationCount] = useState(10) const [userPrompt, setUserPrompt] = useState('') const [winningImageUrl, setWinningImageUrl] = useState('') const [winningPreviewUrl, setWinningPreviewUrl] = useState('') const [uploading, setUploading] = useState(false) const [generating, setGenerating] = useState(false) const [progress, setProgress] = useState({ done: 0, total: 0 }) const [results, setResults] = useState([]) const [selected, setSelected] = useState([]) const [downloading, setDownloading] = useState(false) const [error, setError] = useState(null) const [lightboxImage, setLightboxImage] = useState(null) useEffect(() => { fetch(`${API_BASE}/image-models`, { headers: authHeaders() }) .then((r) => { if (r.status === 401) { localStorage.removeItem(AUTH_TOKEN_KEY) onLogout?.() return null } return r.ok ? r.json() : null }) .then((data) => { if (!data) return setImageModels(data.models || []) if (data.default) setSelectedImageModel(data.default) }) .catch(() => {}) }, [onLogout]) useEffect(() => { return () => { if (winningPreviewUrl) URL.revokeObjectURL(winningPreviewUrl) } }, [winningPreviewUrl]) async function handleUpload(file) { if (!file) return if (winningPreviewUrl) URL.revokeObjectURL(winningPreviewUrl) setWinningPreviewUrl(URL.createObjectURL(file)) setUploading(true) setError(null) try { const form = new FormData() form.append('file', file) const res = await fetch(`${API_BASE}/upload-reference`, { method: 'POST', headers: authHeaders(), body: form, }) if (res.status === 401) { localStorage.removeItem(AUTH_TOKEN_KEY) onLogout?.() return } const data = await res.json().catch(() => ({})) if (!res.ok) throw new Error(data.detail || 'Upload failed') setWinningImageUrl(data.url || '') } catch (e) { setError(e.message || 'Upload failed') } finally { setUploading(false) } } async function handleGenerate() { if (!winningImageUrl) { setError('Upload a winning creative first.') return } setGenerating(true) setError(null) setResults([]) setSelected([]) setProgress({ done: 0, total: variationCount }) try { const res = await fetch(`${API_BASE}/generate-variations/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify({ image_url: winningImageUrl, n: variationCount, model_key: selectedImageModel, aspect_ratio: aspectRatio, user_prompt: userPrompt || undefined, }), }) if (res.status === 401) { localStorage.removeItem(AUTH_TOKEN_KEY) onLogout?.() return } if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.detail || 'Variation generation failed') } const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const chunks = buffer.split('\n\n') buffer = chunks.pop() || '' for (const chunk of chunks) { const dataLine = chunk.split('\n').find((l) => l.startsWith('data: ')) if (!dataLine) continue try { const payload = JSON.parse(dataLine.slice(6)) if (payload.event === 'result' && payload.data) { const rowId = `${payload.data.variation_id ?? 'v'}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` setResults((prev) => [...prev, { ...payload.data, rowId }]) setProgress((prev) => ({ ...prev, done: prev.done + 1 })) } else if (payload.event === 'error') { setError(payload.message || 'Variation generation failed') } } catch (_) {} } } } catch (e) { setError(e.message || 'Variation generation failed') } finally { setGenerating(false) } } function toggleSelect(rowId) { setSelected((prev) => (prev.includes(rowId) ? prev.filter((id) => id !== rowId) : [...prev, rowId])) } async function handleDownloadSelected() { const toDownload = results.filter((r) => r.image_url && selected.includes(r.rowId)) if (!toDownload.length) return setDownloading(true) try { const zip = new JSZip() await Promise.all( toDownload.map(async (item, idx) => { const blob = await fetchImageBlob(item.image_url) const id = String(item.variation_id ?? idx + 1).padStart(2, '0') const slug = (item.concept_name || 'variation') .replace(/[^a-z0-9]+/gi, '-') .toLowerCase() .replace(/^-|-$/g, '') || 'variation' zip.file(`Amalfa-variation-${id}-${slug}.png`, blob) }) ) const blob = await zip.generateAsync({ type: 'blob' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = 'Amalfa-variations-selected.zip' a.click() URL.revokeObjectURL(a.href) } catch (e) { setError(e.message || 'Failed to prepare ZIP') } finally { setDownloading(false) } } return (
{lightboxImage && ( setLightboxImage(null)} /> )}

Winning Creative Variations

Upload your best-performing creative and generate multiple fresh variations.

{error &&

{error}

}
{winningImageUrl && (
Winning creative
)}
{generating && Progress: {progress.done}/{progress.total}}
{results.length > 0 && ( <>
{results.map((item, idx) => (
{item.image_url ? ( {item.concept_name setLightboxImage({ src: proxyImageUrl(item.image_url), alt: item.concept_name || `Variation ${idx + 1}`, }) } /> ) : (
Failed
)}

#{item.variation_id || idx + 1} {item.concept_name || 'Variation'}

{item.error ? (

{item.error}

) : ( )}
))}
)}
) }