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 } from '@/services'; import { TaskStatus, type TaskResult } from '@/types'; import React, { useEffect, useState } from 'react'; import AlertError from './AlertError'; import { Spinner } from './Spinner'; import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'; import { Progress } from './ui/progress'; import { Button } from './ui/button'; import { X } from 'lucide-react'; interface ActiveQueryProps { taskID: string | null; onTaskCancelled?: () => void; } const ActiveQuery: React.FC = ({ taskID, onTaskCancelled }) => { const [user, setUser] = useState(null); useEffect(() => { const passkeyUser = getStoredPasskeyUser(); if (passkeyUser) { setUser(passkeyUser); } else { getUser().then((oidcUser) => { if (oidcUser) setUser(fromOidcUser(oidcUser)); }); } }, []); const [progressPercentage, setProgressPercentage] = useState(0); const [taskStatus, setTaskStatus] = useState(TaskStatus.PENDING); const [lastUpdateTime, setLastUpdateTime] = useState(new Date()); const [fetchStatusError, setFetchStatusError] = useState(null); const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const handleCancelTask = async () => { if (!user || !taskID || isCancelling) return; setIsCancelling(true); try { const result = await cancelTask(user, taskID); if (result.success) { setTaskStatus(TaskStatus.REVOKED); onTaskCancelled?.(); } else { setFetchStatusError(result.message); setAlertDialogIsOpen(true); } } catch (error) { setFetchStatusError(error instanceof Error ? error.message : 'Failed to cancel task'); setAlertDialogIsOpen(true); } finally { setIsCancelling(false); } }; const pollTaskStatus = async (interval: NodeJS.Timeout) => { if (!user || !taskID) { return; } try { const data = await fetchTaskStatus(user, taskID); setLastUpdateTime(new Date()); const status = data.status as TaskStatus; setTaskStatus(status); if (status === TaskStatus.FAILURE || status === TaskStatus.REVOKED) { clearInterval(interval); setFetchStatusError('Task failed with status: ' + status); setAlertDialogIsOpen(true); return; } if (status === TaskStatus.SUCCESS) { clearInterval(interval); setProgressPercentage(100); return; } // Only parse result for in-progress tasks if (data.result) { try { const parsedResult: TaskResult = JSON.parse(data.result); setProgressPercentage(parsedResult.progress * 100); } catch { // Result parsing failed, but task is still running - ignore } } } catch (error) { clearInterval(interval); setTaskStatus(TaskStatus.FAILURE); setAlertDialogIsOpen(true); if (error instanceof Error) { setFetchStatusError(error.message); } else { setFetchStatusError('Failed to update task status: ' + String(error)); } } }; useEffect(() => { const interval = setInterval( () => pollTaskStatus(interval), POLLING_INTERVALS.TASK_STATUS_MS ); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, [taskID, user]); if (!taskID) { return null; } const isInProgress = taskStatus && taskStatus !== TaskStatus.SUCCESS && taskStatus !== TaskStatus.FAILURE && taskStatus !== TaskStatus.REVOKED; return ( <>
{taskStatus && Task: {taskStatus}} {isInProgress && }
Task ID: {taskID}
Last updated: {lastUpdateTime.toLocaleString()}
{isInProgress && ( )}
); }; export default ActiveQuery;