import React, { useState, useEffect } from 'react'; import { Download, Loader2, X, Clock, CheckCircle, AlertCircle, FileVideo, Image as ImageIcon, FolderOpen } from 'lucide-react'; interface RenderJob { id: string; status: 'queued' | 'rendering' | 'done' | 'error'; progress: number; format: string; width: number; height: number; downloadUrl?: string; targetPath?: string; error?: string; createdAt: number; completedAt?: number; fileSizeBytes?: number; } interface RenderHistoryPanelProps { isOpen: boolean; onClose: () => void; onDownload?: (job: RenderJob) => void; } /** * RenderHistoryPanel — Shows past and active render jobs with progress, * download links, and job status information. */ export const RenderHistoryPanel: React.FC = ({ isOpen, onClose, onDownload = () => {} }) => { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!isOpen) return; setLoading(true); fetch('/api/render/jobs') .then(res => res.json()) .then(data => setJobs(data.jobs ?? [])) .catch(() => setJobs([])) .finally(() => setLoading(false)); // SSE for real-time updates const es = new EventSource('/api/render/events'); es.onmessage = (event) => { try { const { type, job } = JSON.parse(event.data); if (type === 'job-update' && job) { setJobs(prev => { const idx = prev.findIndex(j => j.id === job.id); if (idx >= 0) { const updated = [...prev]; updated[idx] = job; return updated; } return [job, ...prev]; }); } } catch {} }; return () => es.close(); }, [isOpen]); if (!isOpen) return null; const formatSize = (bytes?: number) => { if (!bytes) return '—'; if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`; return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; }; const formatTime = (ms: number) => { const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; return `${Math.floor(s / 60)}m ${s % 60}s`; }; const statusIcon = (status: string) => { switch (status) { case 'queued': return ; case 'rendering': return ; case 'done': return ; case 'error': return ; default: return null; } }; return (
e.stopPropagation()}> {/* Header */}

Historial de Renders

{/* Content */}
{loading && (
Cargando...
)} {!loading && jobs.length === 0 && (
No hay renders aún. Haz clic en "Renderizar" para comenzar.
)} {jobs.map(job => (
{statusIcon(job.status)} {job.format} {job.width}×{job.height}
{new Date(job.createdAt).toLocaleTimeString()}
{/* Progress bar */} {job.status === 'rendering' && (
)} {/* Status info */}
{job.status === 'rendering' && ( {job.progress}% )} {job.status === 'done' && ( ✅ Completado en {formatTime((job.completedAt ?? 0) - job.createdAt)} — {formatSize(job.fileSizeBytes)} )} {job.status === 'error' && ( {job.error} )} {job.status === 'queued' && ( En cola... )} {/* Download button */} {job.status === 'done' && job.downloadUrl && ( )}
))}
); };