/* VoiceHistoryTab — Historial de generaciones de voz */ function VoiceHistoryTab() { const [tasks, setTasks] = React.useState([]); const [loading, setLoading] = React.useState(false); const pollRef = React.useRef(null); React.useEffect(() => { loadHistory(); return () => stopPolling(); }, []); // Auto-poll if any task is "doing" React.useEffect(() => { stopPolling(); const hasDoing = tasks.some(t => t.status === "doing" || t.status === "processing"); if (!hasDoing) return; pollRef.current = setInterval(pollDoingTasks, 5000); return () => stopPolling(); }, [tasks]); function stopPolling() { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } } async function loadHistory() { setLoading(true); try { const r = await fetch("/api/voice/history?limit=100", { credentials: "same-origin" }); const d = await r.json(); setTasks(d.tasks || []); } catch (e) { console.error("Error cargando historial", e); } finally { setLoading(false); } } async function deleteTask(taskId) { if (!confirm("¿Borrar este registro del historial?")) return; try { const r = await fetch(`/api/voice/history/${taskId}`, { method: "DELETE", credentials: "same-origin" }); if (r.ok) setTasks(prev => prev.filter(t => t.task_id !== taskId)); } catch (e) { console.error("Error borrando registro", e); } } async function pollDoingTasks() { const doing = tasks.filter(t => t.status === "doing" || t.status === "processing"); if (!doing.length) return; const updates = await Promise.all(doing.map(async t => { try { const r = await fetch(`/api/voice/task/${t.task_id}`, { credentials: "same-origin" }); const d = await r.json(); return { task_id: t.task_id, remote: d }; } catch (_) { return null; } })); setTasks(prev => prev.map(t => { const upd = updates.find(u => u && u.task_id === t.task_id); if (!upd) return t; const rd = upd.remote; const done = rd.status === "done" || rd.status === "Success" || rd.status === "success" || rd.status === "completed"; const err = rd.status === "error" || rd.status === "failed"; if (done) { const url = (rd.metadata && rd.metadata.audio_url) || rd.audio_url || (rd.data && rd.data.audio_url) || t.audio_url; return { ...t, status: "done", audio_url: url }; } if (err) { return { ...t, status: "error", error_message: rd.error_message || rd.message || "Error" }; } return t; })); } function relativeTime(isoStr) { if (!isoStr) return "—"; const diff = Date.now() - new Date(isoStr + (isoStr.includes("Z") ? "" : "Z")).getTime(); if (isNaN(diff)) return "—"; const secs = Math.floor(diff / 1000); const mins = Math.floor(secs / 60); const hours = Math.floor(mins / 60); const days = Math.floor(hours / 24); if (days > 0) return `hace ${days}d`; if (hours > 0) return `hace ${hours}h`; if (mins > 0) return `hace ${mins}m`; return "ahora"; } function StatusBadge({ status }) { const map = { doing: { label: "Procesando", color: "#fbbf24", bg: "rgba(251,191,36,0.12)" }, processing: { label: "Procesando", color: "#fbbf24", bg: "rgba(251,191,36,0.12)" }, done: { label: "Completado", color: "#4ade80", bg: "rgba(74,222,128,0.12)" }, Success: { label: "Completado", color: "#4ade80", bg: "rgba(74,222,128,0.12)" }, success: { label: "Completado", color: "#4ade80", bg: "rgba(74,222,128,0.12)" }, completed: { label: "Completado", color: "#4ade80", bg: "rgba(74,222,128,0.12)" }, error: { label: "Error", color: "#f87171", bg: "rgba(248,113,113,0.12)" }, failed: { label: "Error", color: "#f87171", bg: "rgba(248,113,113,0.12)" }, }; const s = map[status] || { label: status || "—", color: "var(--text-3)", bg: "var(--bg-3)" }; return ( {s.label} ); } const isDone = (status) => ["done", "Success", "success", "completed"].includes(status); return (