"use client"; import { useEffect, useState } from "react"; import { Bell, CheckCircle, XCircle, Clock, Loader2, ChevronDown, ChevronUp, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { useTask, Task } from "@/contexts/task-context"; export function TaskNotificationMenu() { const { tasks, isFetching, isMenuOpen, isRecentTasksExpanded, cancelTask } = useTask(); const [isExpanded, setIsExpanded] = useState(false); // Sync local state with context state useEffect(() => { if (isRecentTasksExpanded) { setIsExpanded(true); } }, [isRecentTasksExpanded]); // Don't render if menu is closed if (!isMenuOpen) return null; const activeTasks = tasks.filter( (task) => task.status === "pending" || task.status === "running" || task.status === "processing", ); const recentTasks = tasks .filter( (task) => task.status === "completed" || task.status === "failed" || task.status === "error", ) .slice(0, 5); // Show last 5 completed/failed tasks const getTaskIcon = (status: Task["status"]) => { switch (status) { case "completed": return ; case "failed": case "error": return ; case "pending": return ; case "running": case "processing": return ; default: return ; } }; const getStatusBadge = (status: Task["status"]) => { switch (status) { case "completed": return ( Completed ); case "failed": case "error": return ( Failed ); case "pending": return ( Pending ); case "running": case "processing": return ( Processing ); default: return ( Unknown ); } }; const formatTaskProgress = (task: Task) => { const total = task.total_files || 0; const processed = task.processed_files || 0; const successful = task.successful_files || 0; const failed = task.failed_files || 0; const running = task.running_files || 0; const pending = task.pending_files || 0; if (total > 0) { return { basic: `${processed}/${total} files`, detailed: { total, processed, successful, failed, running, pending, remaining: total - processed, }, }; } return null; }; const formatDuration = (seconds?: number) => { if (!seconds || seconds < 0) return null; if (seconds < 60) { return `${Math.round(seconds)}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`; } else { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; } }; const formatRelativeTime = (dateString: string) => { // Handle different timestamp formats let date: Date; // If it's a number (Unix timestamp), convert it if (/^\d+$/.test(dateString)) { const timestamp = parseInt(dateString); // If it looks like seconds (less than 10^13), convert to milliseconds date = new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp); } // If it's a decimal number (Unix timestamp with decimals) else if (/^\d+\.\d+$/.test(dateString)) { const timestamp = parseFloat(dateString); // Convert seconds to milliseconds date = new Date(timestamp * 1000); } // Otherwise, try to parse as ISO string or other date format else { date = new Date(dateString); } // Check if date is valid if (isNaN(date.getTime())) { console.warn("Invalid date format:", dateString); return "Unknown time"; } const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMinutes = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMinutes < 1) return "Just now"; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; return `${diffDays}d ago`; }; return (
{/* Header */}

Tasks

{isFetching && ( )}
{activeTasks.length > 0 && ( {activeTasks.length} )}
{/* Content */}
{/* Active Tasks */} {activeTasks.length > 0 && (

Active Tasks

{activeTasks.map((task) => { const progress = formatTaskProgress(task); const showCancel = task.status === "pending" || task.status === "running" || task.status === "processing"; return (
{getTaskIcon(task.status)} Task {task.task_id.substring(0, 8)}...
Started {formatRelativeTime(task.created_at)} {formatDuration(task.duration_seconds) && ( • {formatDuration(task.duration_seconds)} )}
{(progress || showCancel) && ( {progress && (
Progress: {progress.basic}
{progress.detailed && (
{progress.detailed.successful} success
{progress.detailed.failed} failed
{progress.detailed.running} running
{progress.detailed.pending} pending
)}
)} {showCancel && (
)}
)}
); })}
)} {/* Recent Tasks */} {recentTasks.length > 0 && (

Recent Tasks

{isExpanded && (
{recentTasks.map((task) => { const progress = formatTaskProgress(task); return (
{getTaskIcon(task.status)}
Task {task.task_id.substring(0, 8)}...
{formatRelativeTime(task.updated_at)} {formatDuration(task.duration_seconds) && ( • {formatDuration(task.duration_seconds)} )}
{/* Show final results for completed tasks */} {task.status === "completed" && progress?.detailed && (
{progress.detailed.successful} success,{" "} {progress.detailed.failed} failed {(progress.detailed.running || 0) > 0 && ( , {progress.detailed.running} running )}
)} {task.status === "failed" && task.error && (
{task.error}
)}
{getStatusBadge(task.status)}
); })}
)}
)} {/* Empty State */} {activeTasks.length === 0 && recentTasks.length === 0 && (

No tasks yet

Task notifications will appear here when you upload files or sync connectors.

)}
); }