import { TaskStatus, type TaskResult, type TaskState } from '@/types'; import { useEffect, useState, useRef, useMemo } from 'react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; import { Button } from './ui/button'; import { Loader2, CheckCircle2, XCircle, X, Trash2 } from 'lucide-react'; import { TaskProgressDrawer } from './TaskProgressDrawer'; interface TaskIndicatorProps { tasks: Record; activeTaskId: string | null; isConnected: boolean; onCancelTask: (taskId: string) => Promise; onClearAllTasks: () => Promise; onTaskCompleted?: () => void; } /** Convert a TaskState into a TaskResult (for the drawer). */ function taskStateToResult(ts: TaskState): TaskResult { return { progress: ts.progress ?? 0, processed: ts.processed, total: ts.total, phase: ts.phase, message: ts.message, subqueries_probed: ts.subqueries_probed, subqueries_initial: ts.subqueries_initial, estimated_results: ts.estimated_results, subqueries_total: ts.subqueries_total, subqueries_completed: ts.subqueries_completed, ids_collected: ts.ids_collected, pages_fetched: ts.pages_fetched, fetching_done: ts.fetching_done, details_fetched: ts.details_fetched, images_downloaded: ts.images_downloaded, ocr_completed: ts.ocr_completed, failed: ts.failed, elapsed_seconds: ts.elapsed_seconds, rate_per_second: ts.rate_per_second, eta_seconds: ts.eta_seconds, logs: ts.logs, }; } function isTerminalStatus(status: string): boolean { return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED'; } export function TaskIndicator({ tasks, activeTaskId, isConnected: _isConnected, onCancelTask, onClearAllTasks, onTaskCompleted, }: TaskIndicatorProps) { const [isCancelling, setIsCancelling] = useState(false); const [isClearing, setIsClearing] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const [selectedTaskId, setSelectedTaskId] = useState(null); const onTaskCompletedRef = useRef(onTaskCompleted); useEffect(() => { onTaskCompletedRef.current = onTaskCompleted; }, [onTaskCompleted]); // Track the currently-viewed task in the drawer; default to the externally-provided activeTaskId useEffect(() => { if (activeTaskId) { setSelectedTaskId(activeTaskId); } }, [activeTaskId]); // Fire onTaskCompleted when the active task transitions to SUCCESS const prevStatusRef = useRef(null); useEffect(() => { if (!activeTaskId) { prevStatusRef.current = null; return; } const task = tasks[activeTaskId]; const currentStatus = task?.status ?? null; if ( currentStatus === 'SUCCESS' && prevStatusRef.current !== null && prevStatusRef.current !== 'SUCCESS' ) { onTaskCompletedRef.current?.(); } prevStatusRef.current = currentStatus; }, [activeTaskId, tasks]); // Derive display data from the active task const activeTask = activeTaskId ? tasks[activeTaskId] : undefined; const taskStatus = activeTask ? (activeTask.status as TaskStatus) : null; const taskResult = activeTask?.phase ? taskStateToResult(activeTask) : null; const progressPercentage = (activeTask?.progress ?? 0) * 100; const processed = activeTask?.processed ?? null; const total = activeTask?.total ?? null; // Count active (non-terminal) tasks const activeTaskCount = useMemo(() => { return Object.values(tasks).filter( (t) => !isTerminalStatus(t.status), ).length; }, [tasks]); const handleCancel = async () => { if (!activeTaskId || isCancelling) return; setIsCancelling(true); try { await onCancelTask(activeTaskId); } finally { setIsCancelling(false); } }; const handleClearAll = async () => { if (isClearing) return; setIsClearing(true); try { await onClearAllTasks(); } finally { setIsClearing(false); } }; if (!activeTaskId || !taskStatus) { return null; } const isInProgress = !isTerminalStatus(taskStatus); const getStatusIcon = () => { if (isInProgress) { return ; } if (taskStatus === TaskStatus.SUCCESS) { return ; } return ; }; const getProgressText = () => { if (processed !== null && total !== null && total > 0) { return `${processed} / ${total}`; } if (taskResult?.phase && taskResult.phase !== 'processing') { const phaseLabels: Record = { splitting: 'Splitting', splitting_complete: 'Split done', fetching: 'Fetching', }; return phaseLabels[taskResult.phase] ?? `${Math.round(progressPercentage)}%`; } return `${Math.round(progressPercentage)}%`; }; const getTooltipContent = () => { if (isInProgress) { if (processed !== null && total !== null && total > 0) { return `Processing: ${processed} / ${total} listings (${Math.round(progressPercentage)}%) — click for details`; } return `Task running: ${getProgressText()} — click for details`; } if (taskStatus === TaskStatus.SUCCESS) { return 'Task completed successfully — click for details'; } if (taskStatus === TaskStatus.REVOKED) { return 'Task was cancelled'; } return 'Task failed'; }; return (
setDrawerOpen(true)} > {getStatusIcon()} {isInProgress && (
{getProgressText()}
)} {!isInProgress && ( {taskStatus} )} {activeTaskCount > 1 && ( {activeTaskCount} )}

{getTooltipContent()}

ID: {activeTaskId.slice(0, 8)}...

{isInProgress && (

Cancel task

)}

Clear all tasks

); }