'use client'; import { useState, useEffect, useCallback } from 'react'; import { useStore } from '@/lib/store'; import api from '@/lib/api'; import { toast } from 'react-hot-toast'; import { Loader2, CheckCircle2, XCircle, Clock, ChevronDown, ChevronUp, Image as ImageIcon, Video, Mic, FileText, X, } from 'lucide-react'; import { clsx } from 'clsx'; const MODULE_ICONS: Record = { image_generator: ImageIcon, image_upscaler: ImageIcon, background_remover: ImageIcon, video_generator: Video, video_upscaler: Video, subtitle_processor: Video, text_to_speech: Mic, voice_to_text: Mic, speech_to_speech: Mic, alt_text_generator: FileText, prompt_studio: FileText, }; const MODULE_LABELS: Record = { image_generator: 'Image Generation', image_upscaler: 'Image Upscale', background_remover: 'BG Removal', video_generator: 'Video Generation', video_upscaler: 'Video Upscale', subtitle_processor: 'Subtitles', text_to_speech: 'Text to Speech', voice_to_text: 'Voice to Text', speech_to_speech: 'Voice Clone', alt_text_generator: 'Alt Text', prompt_studio: 'Prompt Studio', }; interface JobTrackerProps { className?: string; } export default function JobTracker({ className }: JobTrackerProps) { const { activeJobs, updateJob, removeJob } = useStore(); const [expanded, setExpanded] = useState(false); const [polling, setPolling] = useState(false); // Poll for job updates const pollJobs = useCallback(async () => { const pendingJobs = activeJobs.filter( (job) => job.status === 'queued' || job.status === 'processing' ); if (pendingJobs.length === 0) { setPolling(false); return; } setPolling(true); for (const job of pendingJobs) { try { const response = await api.get(`/jobs/${job.id}`); const updatedJob = response.data; if (updatedJob.status !== job.status || updatedJob.progress !== job.progress) { updateJob(job.id, { status: updatedJob.status, progress: updatedJob.progress, completed_at: updatedJob.completed_at, output_asset_ids: updatedJob.output_asset_ids, error_message: updatedJob.error_message, }); // Show toast on completion if (updatedJob.status === 'completed' && job.status !== 'completed') { toast.success(`${MODULE_LABELS[job.module] || job.module} completed!`, { duration: 5000, }); } else if (updatedJob.status === 'failed' && job.status !== 'failed') { toast.error(`${MODULE_LABELS[job.module] || job.module} failed`, { duration: 5000, }); } } } catch (error) { console.error(`Failed to poll job ${job.id}:`, error); } } }, [activeJobs, updateJob]); // Set up polling interval useEffect(() => { const hasPendingJobs = activeJobs.some( (job) => job.status === 'queued' || job.status === 'processing' ); if (!hasPendingJobs) return; // Poll immediately pollJobs(); // Then poll every 2 seconds const interval = setInterval(pollJobs, 2000); return () => clearInterval(interval); }, [activeJobs.length, pollJobs]); const pendingCount = activeJobs.filter( (job) => job.status === 'queued' || job.status === 'processing' ).length; const getStatusIcon = (status: string) => { switch (status) { case 'completed': return ; case 'failed': return ; case 'processing': return ; default: return ; } }; const formatTime = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); if (diffMin < 1) return 'Just now'; if (diffMin < 60) return `${diffMin}m ago`; return `${Math.floor(diffMin / 60)}h ago`; }; if (activeJobs.length === 0) return null; return (
{/* Trigger Button */} {/* Dropdown Panel */} {expanded && (

Active Jobs

{polling && 'Updating...'}
{activeJobs.slice(0, 10).map((job) => { const Icon = MODULE_ICONS[job.module] || FileText; return (
{MODULE_LABELS[job.module] || job.module}
{getStatusIcon(job.status)}
{/* Progress bar for processing jobs */} {(job.status === 'processing' || job.status === 'queued') && (
{job.status === 'queued' ? 'Queued' : 'Processing'} {job.progress}%
)} {/* Completed/Failed status */} {job.status === 'completed' && (

Completed {job.completed_at && formatTime(job.completed_at)}

)} {job.status === 'failed' && (

{job.error_message || 'Failed'}

)}

Started {formatTime(job.created_at)}

); })}
{activeJobs.length > 10 && ( )}
)}
); }