/* Tab: Generar Clips (workspace) — kie.ai Gemini Omni */
function GenerateClipsTab() {
const [prompt, setPrompt] = React.useState("");
const [duration, setDuration] = React.useState(8);
const [aspectRatio, setAspectRatio] = React.useState("16:9");
const [generateAudio, setGenAudio] = React.useState(true);
const [generating, setGenerating] = React.useState(false);
const [clips, setClips] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [preview, setPreview] = React.useState(null);
const [genError, setGenError] = React.useState("");
React.useEffect(() => { fetchClips(); }, []);
React.useEffect(() => {
const hasProcessing = clips.some((c) => c.status === "processing");
if (!hasProcessing) return;
const id = setInterval(fetchClips, 4000);
return () => clearInterval(id);
}, [clips]);
async function fetchClips() {
try {
const r = await fetch("/api/workspace/clips", { credentials: "same-origin" });
if (r.ok) { const d = await r.json(); setClips(d.clips || []); }
} finally { setLoading(false); }
}
async function handleGenerate() {
if (!prompt.trim() || generating) return;
setGenerating(true);
setGenError("");
try {
const r = await fetch("/api/workspace/clips/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ prompt: prompt.trim(), duration, aspect_ratio: aspectRatio, generate_audio: generateAudio }),
});
const d = await r.json();
if (r.ok && d.success) {
setClips((prev) => [d.clip, ...prev]);
} else {
setGenError(d.detail || "Error al iniciar la generación");
}
} catch { setGenError("Error de conexión"); }
finally { setGenerating(false); }
}
async function handleDelete(id) {
const r = await fetch(`/api/workspace/clips/${id}`, { method: "DELETE", credentials: "same-origin" });
if (r.ok) {
setClips((prev) => prev.filter((c) => c.id !== id));
if (preview && preview.id === id) setPreview(null);
}
}
function handleKeyDown(e) {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") handleGenerate();
}
const processing = clips.filter((c) => c.status === "processing").length;
return (
{/* ── Left panel ── */}
{[6, 8, 10].map((d) => (
))}
{genError && (
{genError}
)}
Modelo: kie.ai · Gemini Omni · 720p
La generación tarda 1–3 minutos.
{/* ── Right panel ── */}
Historial
{clips.length > 0 && (
{clips.length}
)}
{processing > 0 && (
{processing} generando
)}
Eliminación automática a las 48h
{loading ? (
Cargando…
) : clips.length === 0 ? (
Sin clips generados
Escribe un prompt y pulsa Generar clip para empezar.
) : (
{clips.map((clip) => (
handleDelete(clip.id)}
onPreview={() => setPreview(clip)}
/>
))}
)}
{preview && (
setPreview(null)} onDelete={() => handleDelete(preview.id)}/>
)}
);
}
function ClipSpinner({ size = 16 }) {
return (
);
}
function ClipCard({ clip, onDelete, onPreview }) {
const videoRef = React.useRef(null);
const [hovered, setHovered] = React.useState(false);
const warn = (() => {
const expires = new Date(clip.created_at).getTime() + 48 * 3600000;
const h = Math.floor((expires - Date.now()) / 3600000);
return h > 0 && h < 6 ? `expira en ${h}h` : null;
})();
const ago = (() => {
const diff = Date.now() - new Date(clip.created_at).getTime();
const m = Math.floor(diff / 60000), h = Math.floor(diff / 3600000), d = Math.floor(diff / 86400000);
return d > 0 ? `hace ${d}d` : h > 0 ? `hace ${h}h` : m > 0 ? `hace ${m}m` : "ahora";
})();
function onMouseEnter() {
setHovered(true);
if (videoRef.current && clip.status === "done") videoRef.current.play().catch(() => {});
}
function onMouseLeave() {
setHovered(false);
if (videoRef.current) { videoRef.current.pause(); videoRef.current.currentTime = 0; }
}
function handleDownload(e) {
e.stopPropagation();
const a = document.createElement("a");
a.href = `/workspace-clips/${clip.filename}`;
a.download = `${clip.id}.mp4`;
a.click();
}
return (
clip.status === "done" && onPreview()}
>
{/* Thumbnail / video area */}
{clip.status === "processing" && (
Generando · {clip.duration}s
)}
{clip.status === "failed" && (
{(clip.error || "Error desconocido").slice(0, 80)}
)}
{clip.status === "done" && (
<>
{!hovered && (
)}
>
)}
{/* Hover actions */}
{hovered && clip.status === "done" && (
)}
{/* Delete for failed */}
{clip.status === "failed" && (
)}
{/* Duration badge */}
{clip.status === "done" && (
{clip.duration}s
)}
{/* Footer */}
{clip.prompt}
{clip.aspect_ratio} · LTX
{warn && {warn}}
{ago}
);
}
function ClipLightbox({ clip, onClose, onDelete }) {
React.useEffect(() => {
function onKey(e) { if (e.key === "Escape") onClose(); }
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
function handleDownload() {
const a = document.createElement("a");
a.href = `/workspace-clips/${clip.filename}`;
a.download = `${clip.id}.mp4`;
a.click();
}
const isVertical = clip.aspect_ratio === "9:16";
return (
e.target === e.currentTarget && onClose()}
>
{/* Top bar */}
{clip.prompt}
{clip.aspect_ratio} · {clip.duration}s · LTX
{/* Video player */}
Pulsa Esc para cerrar · Expira 48h desde la creación
);
}
Object.assign(window, { GenerateClipsTab });