import { getUser } from '@/auth/authService'; import { getStoredPasskeyUser } from '@/auth/passkeyService'; import { fromOidcUser, type AuthUser } from '@/auth/types'; import { POLLING_INTERVALS } from '@/constants'; import { fetchTaskStatus, cancelTask, clearAllTasks } from '@/services'; import { TaskStatus, type TaskResult } from '@/types'; import { useEffect, useState, useRef } 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 { taskID: string | null; onTaskCancelled?: () => void; onTaskCompleted?: () => void; } export function TaskIndicator({ taskID, onTaskCancelled, onTaskCompleted }: TaskIndicatorProps) { const [user, setUser] = useState(null); const [progressPercentage, setProgressPercentage] = useState(0); const [processed, setProcessed] = useState(null); const [total, setTotal] = useState(null); const [taskStatus, setTaskStatus] = useState(null); const [taskResult, setTaskResult] = useState(null); const [isCancelling, setIsCancelling] = useState(false); const [isClearing, setIsClearing] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const onTaskCompletedRef = useRef(onTaskCompleted); useEffect(() => { onTaskCompletedRef.current = onTaskCompleted; }, [onTaskCompleted]); useEffect(() => { const passkeyUser = getStoredPasskeyUser(); if (passkeyUser) { setUser(passkeyUser); } else { getUser().then((oidcUser) => { if (oidcUser) setUser(fromOidcUser(oidcUser)); }); } }, []); useEffect(() => { if (!user || !taskID) { setTaskStatus(null); setTaskResult(null); return; } // Reset state for new task setTaskStatus(TaskStatus.PENDING); setProgressPercentage(0); setProcessed(null); setTotal(null); setTaskResult(null); const pollTaskStatus = async () => { try { const data = await fetchTaskStatus(user, taskID); const status = data.status as TaskStatus; setTaskStatus(status); if (status === TaskStatus.SUCCESS) { setProgressPercentage(100); // Parse final result for the drawer to show completed state. // Only update taskResult if the new result has phase info; // otherwise keep the last in-progress result which has richer data // than the bare SUCCESS return value. if (data.result) { try { const parsedResult: TaskResult = JSON.parse(data.result); if (parsedResult.phase) { setTaskResult(parsedResult); } } catch { // Ignore parsing errors } } onTaskCompletedRef.current?.(); return true; // Stop polling } if (status === TaskStatus.FAILURE || status === TaskStatus.REVOKED) { return true; // Stop polling } // Parse progress for in-progress tasks if (data.result) { try { const parsedResult: TaskResult = JSON.parse(data.result); // Only update taskResult if the parsed data has a phase field. // This prevents blanking the drawer when the backend sends a // state update without phase info (e.g. during brief transitions). if (parsedResult.phase) { setTaskResult(parsedResult); } // Only update progress/processed/total when the fields are // actually present — otherwise keep the previous values so // the UI doesn't flash back to 0 during phase transitions. if (parsedResult.progress !== undefined) { setProgressPercentage(parsedResult.progress * 100); } if (parsedResult.processed !== undefined) { setProcessed(parsedResult.processed); } if (parsedResult.total !== undefined) { setTotal(parsedResult.total); } } catch { // Ignore parsing errors } } return false; // Continue polling } catch { setTaskStatus(TaskStatus.FAILURE); return true; // Stop polling on error } }; // Initial poll pollTaskStatus(); const interval = setInterval(async () => { const shouldStop = await pollTaskStatus(); if (shouldStop) { clearInterval(interval); } }, POLLING_INTERVALS.TASK_STATUS_MS); return () => clearInterval(interval); }, [taskID, user]); const handleCancel = async () => { if (!user || !taskID || isCancelling) return; setIsCancelling(true); try { const result = await cancelTask(user, taskID); if (result.success) { setTaskStatus(TaskStatus.REVOKED); onTaskCancelled?.(); } } catch { // Ignore cancel errors } finally { setIsCancelling(false); } }; const handleClearAll = async () => { if (!user || isClearing) return; setIsClearing(true); try { const result = await clearAllTasks(user); if (result.success) { setTaskStatus(null); setTaskResult(null); onTaskCancelled?.(); } } catch { // Ignore clear errors } finally { setIsClearing(false); } }; if (!taskID || !taskStatus) { return null; } const isInProgress = taskStatus !== TaskStatus.SUCCESS && taskStatus !== TaskStatus.FAILURE && taskStatus !== TaskStatus.REVOKED; 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} )}

{getTooltipContent()}

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

{isInProgress && (

Cancel task

)}

Clear all tasks

); }