"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.
)}
);
}