import { TaskStatus, type TaskPhase, type TaskResult } from '@/types';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
SheetFooter,
} from './ui/sheet';
import { Button } from './ui/button';
import { CheckCircle2, Circle, Loader2, XCircle } from 'lucide-react';
import { useEffect, useRef } from 'react';
interface TaskProgressDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
taskResult: TaskResult | null;
taskStatus: TaskStatus | null;
taskID: string | null;
onCancel: () => void;
isCancelling: boolean;
}
const PHASES: { key: TaskPhase; label: string }[] = [
{ key: 'splitting', label: 'Splitting queries' },
{ key: 'fetching', label: 'Fetching listings' },
{ key: 'filtering', label: 'Filtering results' },
{ key: 'processing', label: 'Processing listings' },
];
function getPhaseIndex(phase: TaskPhase | undefined): number {
if (!phase) return -1;
if (phase === 'splitting_complete') return 1; // splitting done, fetching is next
if (phase === 'completed') return PHASES.length;
return PHASES.findIndex((p) => p.key === phase);
}
function formatEta(seconds: number | undefined): string {
if (seconds === undefined || seconds <= 0) return '';
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
if (mins > 0) {
return `~${mins}m ${secs}s remaining`;
}
return `~${secs}s remaining`;
}
function StatusBadge({ status }: { status: TaskStatus | null }) {
if (!status) return null;
const isInProgress =
status !== TaskStatus.SUCCESS &&
status !== TaskStatus.FAILURE &&
status !== TaskStatus.REVOKED;
if (isInProgress) {
return (
Running
);
}
if (status === TaskStatus.SUCCESS) {
return (
Complete
);
}
if (status === TaskStatus.REVOKED) {
return (
Cancelled
);
}
return (
Failed
);
}
function PhaseTimeline({
currentPhase,
taskStatus,
}: {
currentPhase: TaskPhase | undefined;
taskStatus: TaskStatus | null;
}) {
const isTerminal =
taskStatus === TaskStatus.SUCCESS ||
taskStatus === TaskStatus.FAILURE ||
taskStatus === TaskStatus.REVOKED;
const activeIdx = isTerminal ? PHASES.length : getPhaseIndex(currentPhase);
return (
{PHASES.map((phase, idx) => {
const isCompleted = idx < activeIdx;
const isActive = idx === activeIdx && !isTerminal;
const isFuture = idx > activeIdx;
return (
{isCompleted && (
)}
{isActive && (
)}
{isFuture && (
)}
{phase.label}
);
})}
);
}
function CounterRow({ label, value, total }: { label: string; value?: number; total?: number }) {
if (value === undefined) return null;
return (
{label}
{value}
{total !== undefined && ` / ${total}`}
);
}
function PhaseDetails({ result }: { result: TaskResult }) {
const phase = result.phase;
if (phase === 'splitting' || phase === 'splitting_complete') {
return (
Splitting
{result.subqueries_total !== undefined && (
)}
{result.estimated_results !== undefined && (
)}
);
}
if (phase === 'fetching') {
return (
);
}
if (phase === 'filtering') {
return (
);
}
if (phase === 'processing') {
return (
Processing
{(result.failed ?? 0) > 0 && (
Failed
{result.failed}
)}
);
}
return null;
}
function LogViewer({ logs }: { logs: string[] }) {
const scrollRef = useRef(null);
const isAutoScrolling = useRef(true);
const handleScroll = () => {
const el = scrollRef.current;
if (!el) return;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
isAutoScrolling.current = atBottom;
};
useEffect(() => {
if (isAutoScrolling.current && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
return (
{logs.length === 0 ? (
Waiting for logs...
) : (
logs.map((line, i) => (
{line}
))
)}
);
}
export function TaskProgressDrawer({
open,
onOpenChange,
taskResult,
taskStatus,
taskID,
onCancel,
isCancelling,
}: TaskProgressDrawerProps) {
const isInProgress =
taskStatus !== null &&
taskStatus !== TaskStatus.SUCCESS &&
taskStatus !== TaskStatus.FAILURE &&
taskStatus !== TaskStatus.REVOKED;
const progressPercent = taskResult
? Math.min((taskResult.progress ?? 0) * 100, 100)
: 0;
return (
Crawl Job Progress
{taskID && (
Task ID: {taskID.slice(0, 8)}...
)}
{/* Fixed top section: timeline + counters + progress */}
{taskResult &&
}
{taskResult && taskResult.phase === 'processing' && (
{taskResult.processed ?? 0} / {taskResult.total ?? '?'}
{formatEta(taskResult.eta_seconds)}
)}
{taskResult?.message && (
{taskResult.message}
)}
{/* Log viewer fills remaining space */}
{isInProgress && (
)}
);
}