/* Tab: Generar Imágenes (workspace) */ function timeAgoShort(iso) { const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); const h = Math.floor(diff / 3600000); const d = Math.floor(diff / 86400000); if (d > 0) return `hace ${d}d`; if (h > 0) return `hace ${h}h`; if (m > 0) return `hace ${m}m`; return "ahora"; } function engineBadge(img) { if (img.provider === "gptimage") return "GPT Image · " + (img.mode === "edit" ? "i2i" : "t2i"); return img.model === "pro" ? "Nano Pro" : img.model === "v2" ? "Nano v2" : "Nano v1"; } function expiresIn(iso) { const expires = new Date(iso).getTime() + 48 * 3600000; const diff = expires - Date.now(); if (diff <= 0) return null; const h = Math.floor(diff / 3600000); if (h < 6) return `expira en ${h}h`; return null; } function GenerateImagesTab() { const [prompt, setPrompt] = React.useState(""); const [provider, setProvider] = React.useState("nanobanana"); // "nanobanana" | "gptimage" const [model, setModel] = React.useState("v2"); const [mode, setMode] = React.useState("generate"); // "generate" (t2i) | "edit" (i2i) const isGpt = provider === "gptimage"; const [aspectRatio, setAspectRatio] = React.useState("16:9"); // "1:1" | "16:9" | "4:3" const [copies, setCopies] = React.useState(1); // 1-4 const [referenceImageUrl, setReferenceImageUrl] = React.useState(null); const [uploadingRef, setUploadingRef] = React.useState(false); const [refError, setRefError] = React.useState(""); const [generating, setGenerating] = React.useState(false); const [images, setImages] = React.useState([]); const [loading, setLoading] = React.useState(true); const [preview, setPreview] = React.useState(null); const [genError, setGenError] = React.useState(""); function selectMode(m) { setMode(m); if (m === "edit" && model === "pro") setModel("v2"); } async function handleReferenceUpload(e) { const file = e.target.files && e.target.files[0]; e.target.value = ""; if (!file) return; setRefError(""); setUploadingRef(true); try { const fd = new FormData(); fd.append("file", file); const r = await fetch("/api/workspace/images/upload-reference", { method: "POST", credentials: "same-origin", body: fd, }); const d = await r.json(); if (r.ok && d.success) setReferenceImageUrl(d.url); else setRefError(d.detail || "Error al subir la imagen"); } catch { setRefError("Error de conexión"); } finally { setUploadingRef(false); } } React.useEffect(() => { fetchImages(); }, []); React.useEffect(() => { const hasProcessing = images.some((img) => img.status === "processing"); if (!hasProcessing) return; const id = setInterval(fetchImages, 3500); return () => clearInterval(id); }, [images]); async function fetchImages() { try { const r = await fetch("/api/workspace/images", { credentials: "same-origin" }); if (r.ok) { const d = await r.json(); setImages(d.images || []); } } finally { setLoading(false); } } async function handleGenerate() { if (!prompt.trim() || generating || uploadingRef) return; if (mode === "edit" && !referenceImageUrl) { setGenError("Sube una imagen de referencia para editar"); return; } setGenerating(true); setGenError(""); try { const r = await fetch("/api/workspace/images/generate", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ prompt: prompt.trim(), provider, model, mode, aspect_ratio: aspectRatio, reference_image_url: mode === "edit" ? referenceImageUrl : null, copies, }), }); const d = await r.json(); if (r.ok && d.success) { setImages((prev) => [...d.images, ...prev]); } else { setGenError(d.detail || "Error al iniciar generación"); } } catch { setGenError("Error de conexión"); } finally { setGenerating(false); } } async function handleDelete(id) { const r = await fetch(`/api/workspace/images/${id}`, { method: "DELETE", credentials: "same-origin" }); if (r.ok) { setImages((prev) => prev.filter((img) => img.id !== id)); if (preview && preview.id === id) setPreview(null); } } function handleKeyDown(e) { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") handleGenerate(); } const processing = images.filter((i) => i.status === "processing").length; return (