"use client"; import { ChevronDownIcon, ChevronRightIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import React, { useEffect, useMemo, useState } from "react"; import toast from "react-hot-toast"; import { del, get } from "../../lib/api"; import ConfirmationDialog from "./ConfirmationDialog"; import LoadingCard from "./Loading"; import { useNotifications } from "./NotificationContext"; import Pagination from "./Pagination"; import TaskCrud from "./TaskCrud"; export default function TaskList({ limit = 10, context = "homepage" }: { limit?: number; context?: "homepage" | "tasks"; }) { const queryClient = useQueryClient(); const { addNotification } = useNotifications(); const { data, isLoading, error } = useQuery({ queryKey: ["tasks"], queryFn: () => get("/tasks") }); // Listen for WebSocket events useEffect(() => { const handleTaskUpdate = (event: CustomEvent) => { const taskData = event.detail; console.log("[TaskList] Received task update:", taskData); // For progress updates, update the cache directly instead of refetching if (taskData.type === "progress" && taskData.taskId !== undefined) { console.log( `[TaskList] Updating progress for task ${taskData.taskId} to ${taskData.progress}%` ); queryClient.setQueryData(["tasks"], (oldData: any) => { if (!oldData) { console.log("[TaskList] No cached task data available"); return oldData; } const updated = oldData.map((task: any) => task.id === taskData.taskId ? { ...task, progress: taskData.progress } : task ); console.log( "[TaskList] Updated task cache:", updated.find((t: any) => t.id === taskData.taskId) ); return updated; }); } else { console.log("[TaskList] Non-progress update, refetching tasks"); // For other task updates (created, completed, failed), do a full refetch queryClient.refetchQueries({ queryKey: ["tasks"] }); } }; const handleFileUpdate = (event: CustomEvent) => { const _fileData = event.detail; // Refetch tasks when file updates occur (e.g., new files detected) queryClient.refetchQueries({ queryKey: ["tasks"] }); }; window.addEventListener("taskUpdate", handleTaskUpdate as EventListener); window.addEventListener("fileUpdate", handleFileUpdate as EventListener); return () => { window.removeEventListener( "taskUpdate", handleTaskUpdate as EventListener ); window.removeEventListener( "fileUpdate", handleFileUpdate as EventListener ); }; }, [queryClient]); const [editTask, setEditTask] = useState(null); // Confirmation dialog and batch selection state const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; task?: any; isBatch: boolean; selectedTasks?: any[]; }>({ isOpen: false, isBatch: false }); const [selectedTasks, setSelectedTasks] = useState>(new Set()); // Expanded rows state const [expandedRows, setExpandedRows] = useState>(new Set()); // Dataset and status filter state - initialize from localStorage const [enabledDatasets, setEnabledDatasets] = useState>(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("taskList:enabledDatasets"); return saved ? new Set(JSON.parse(saved)) : new Set(); } return new Set(); }); const [enabledStatuses, setEnabledStatuses] = useState>(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("taskList:enabledStatuses"); return saved ? new Set(JSON.parse(saved)) : new Set(["pending", "processing", "failed", "skipped"]); } return new Set(["pending", "processing", "failed", "skipped"]); }); // State for filters and search - initialize from localStorage const [searchTerm, setSearchTerm] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("taskList:searchTerm") || ""; } return ""; }); const [sortField, setSortField] = useState< "id" | "status" | "progress" | "dataset" | "updated_at" >(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("taskList:sortField"); return ( (saved as "id" | "status" | "progress" | "dataset" | "updated_at") || "updated_at" ); } return "updated_at"; }); const [sortDirection, setSortDirection] = useState<"asc" | "desc">(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("taskList:sortDirection"); return (saved as "asc" | "desc") || "desc"; } return "desc"; }); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("taskList:pageSize"); return saved ? parseInt(saved) : 25; } return 25; }); // Save filters to localStorage useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem( "taskList:enabledDatasets", JSON.stringify(Array.from(enabledDatasets)) ); } }, [enabledDatasets]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem( "taskList:enabledStatuses", JSON.stringify(Array.from(enabledStatuses)) ); } }, [enabledStatuses]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("taskList:searchTerm", searchTerm); } }, [searchTerm]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("taskList:sortField", sortField); } }, [sortField]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("taskList:sortDirection", sortDirection); } }, [sortDirection]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("taskList:pageSize", pageSize.toString()); } }, [pageSize]); const deleteMutation = useMutation({ mutationFn: (params: { task?: string; tasks?: any[] }) => { if (params.tasks) { // Batch delete return Promise.all( params.tasks.map((task) => del(`/tasks/${task.id}`)) ); } else if (params.task) { // Single delete return del(`/tasks/${params.task}`); } throw new Error("Invalid delete parameters"); }, onSuccess: (_, params) => { queryClient.refetchQueries({ queryKey: ["tasks"] }); if (params.tasks) { toast.success(`${params.tasks.length} tasks deleted successfully`); addNotification({ type: "success", title: "Tasks Deleted", message: `${params.tasks.length} tasks have been deleted successfully.` }); setSelectedTasks(new Set()); // Clear selection after batch delete } else { toast.success("Task deleted successfully"); addNotification({ type: "success", title: "Task Deleted", message: `Task with ID "${params.task}" has been deleted successfully.` }); } }, onError: (error, _params) => { console.error("Failed to delete task(s):", error); toast.error("Failed to delete task(s)"); addNotification({ type: "error", title: "Delete Failed", message: `Failed to delete task(s). Please try again.` }); } }); const handleEdit = (task: any) => { setEditTask(task); }; const handleEditClose = () => { setEditTask(null); }; // Initialize enabled datasets when data is loaded useEffect(() => { if (data && data.length > 0 && enabledDatasets.size === 0) { const datasets = data .map((task: any) => task.dataset) .filter((dataset: any): dataset is string => Boolean(dataset)); setEnabledDatasets(new Set(datasets)); } }, [data, enabledDatasets.size]); // Confirmation dialog handlers const handleDeleteClick = (task: any) => { setDeleteConfirm({ isOpen: true, task, isBatch: false }); }; const handleBatchDeleteClick = () => { const tasksToDelete = displayTasks.filter((task) => selectedTasks.has(task.id.toString()) ); setDeleteConfirm({ isOpen: true, isBatch: true, selectedTasks: tasksToDelete }); }; const handleDeleteConfirm = () => { if (deleteConfirm.isBatch && deleteConfirm.selectedTasks) { deleteMutation.mutate({ tasks: deleteConfirm.selectedTasks }); } else if (deleteConfirm.task) { deleteMutation.mutate({ task: deleteConfirm.task.id.toString() }); } setDeleteConfirm({ isOpen: false, isBatch: false }); }; const handleDeleteCancel = () => { setDeleteConfirm({ isOpen: false, isBatch: false }); }; // Batch selection handlers const handleSelectAll = () => { if (selectedTasks.size === displayTasks.length) { setSelectedTasks(new Set()); } else { const allIds = new Set(displayTasks.map((task) => task.id.toString())); setSelectedTasks(allIds); } }; const handleSelectTask = (task: any) => { const taskId = task.id.toString(); const newSelected = new Set(selectedTasks); if (newSelected.has(taskId)) { newSelected.delete(taskId); } else { newSelected.add(taskId); } setSelectedTasks(newSelected); }; // Expanded row handlers const handleToggleExpanded = (taskId: string) => { const newExpanded = new Set(expandedRows); if (newExpanded.has(taskId)) { newExpanded.delete(taskId); } else { newExpanded.add(taskId); } setExpandedRows(newExpanded); }; // Dataset and status filter handlers const toggleDataset = (datasetName: string) => { const newEnabled = new Set(enabledDatasets); if (newEnabled.has(datasetName)) { newEnabled.delete(datasetName); } else { newEnabled.add(datasetName); } setEnabledDatasets(newEnabled); }; const toggleStatus = (statusName: string) => { const newEnabled = new Set(enabledStatuses); if (newEnabled.has(statusName)) { newEnabled.delete(statusName); } else { newEnabled.add(statusName); } setEnabledStatuses(newEnabled); }; const truncateText = (text: string, maxLength: number = 30) => { return text && text.length > maxLength ? text.substring(0, maxLength) + "..." : text; }; const handleSort = ( field: "id" | "status" | "progress" | "dataset" | "updated_at" ) => { if (sortField === field) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDirection("asc"); } }; // Filter and sort data const filteredAndSortedData = useMemo(() => { if (!data || !Array.isArray(data)) return []; const filtered = data.filter( (task: any) => enabledDatasets.has(task.dataset || "") && enabledStatuses.has(task.status || "") && (task.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) || (task.status && task.status.toLowerCase().includes(searchTerm.toLowerCase())) || (task.progress && task.progress .toString() .toLowerCase() .includes(searchTerm.toLowerCase())) || (task.dataset && task.dataset.toLowerCase().includes(searchTerm.toLowerCase())) || (task.input && task.input.toLowerCase().includes(searchTerm.toLowerCase())) || (task.output && task.output.toLowerCase().includes(searchTerm.toLowerCase())) || (task.error_message && task.error_message .toLowerCase() .includes(searchTerm.toLowerCase()))) ); filtered.sort((a: any, b: any) => { let aValue = a[sortField]; let bValue = b[sortField]; if (typeof aValue === "string") { aValue = aValue.toLowerCase(); bValue = bValue.toLowerCase(); } if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; return 0; }); return filtered; }, [ data, enabledDatasets, enabledStatuses, searchTerm, sortField, sortDirection ]); // Handle pagination const paginatedData = useMemo(() => { if (context === "homepage") { // For homepage, just apply limit return limit ? filteredAndSortedData.slice(0, limit) : filteredAndSortedData; } else { // For tasks page, apply pagination const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; return filteredAndSortedData.slice(startIndex, endIndex); } }, [filteredAndSortedData, context, limit, currentPage, pageSize]); // Reset to page 1 when filters change useEffect(() => { setCurrentPage(1); }, [enabledDatasets, enabledStatuses, searchTerm, sortField, sortDirection]); const displayTasks = paginatedData; // Show loading state if data is still being fetched or if data is undefined if (isLoading || !data) return ; if (error) { return (

Failed to load tasks

There was an error loading the tasks data.

); } return ( <>

Tasks

Total: {filteredAndSortedData.length} tasks {context === "homepage" ? ( View All Tasks ) : null}
{/* Filter Controls - Only show on tasks page */} {context === "tasks" && (
{/* Search */}
setSearchTerm(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" />
{/* Dataset Toggles */}
Datasets: {Array.from(enabledDatasets).map((datasetName) => ( ))}
{/* Status Toggles */}
Status: {["pending", "processing", "completed", "failed", "skipped"].map( (statusName) => ( ) )}
)} {/* Batch Actions */} {selectedTasks.size > 0 && (
{selectedTasks.size} task{selectedTasks.size !== 1 ? "s" : ""}{" "} selected
)}
{context === "tasks" && ( )} {context === "tasks" && ( )} {context === "tasks" && ( <> )} {context === "tasks" && ( )} {displayTasks.length > 0 ? ( displayTasks.map((task: any) => { const taskId = task.id.toString(); const isExpanded = expandedRows.has(taskId); return ( {context === "tasks" && ( )} {context === "tasks" && ( )} {context === "tasks" && ( <> )} {context === "tasks" && ( )} {context === "tasks" && isExpanded && ( )} ); }) ) : ( )}
{/* Expand/collapse column */} 0 } onChange={handleSelectAll} className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" /> handleSort("id") : undefined } > ID{" "} {context === "tasks" && sortField === "id" && (sortDirection === "asc" ? "↑" : "↓")} handleSort("status") : undefined } > Status{" "} {context === "tasks" && sortField === "status" && (sortDirection === "asc" ? "↑" : "↓")} handleSort("progress") : undefined } > Progress{" "} {context === "tasks" && sortField === "progress" && (sortDirection === "asc" ? "↑" : "↓")} handleSort("dataset") : undefined } > Dataset{" "} {context === "tasks" && sortField === "dataset" && (sortDirection === "asc" ? "↑" : "↓")} Preset Input Actions
handleSelectTask(task)} className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" /> {task.id} {truncateText(task.status)} {task.progress !== null && task.progress !== undefined ? (
{task.progress}%
) : ( "-" )}
{truncateText(task.dataset ?? "-")} {truncateText(task.preset ?? "-")}
{task.input ? truncateText(task.input, 30) : "-"}
Output
{task.output || "No output available"}
Created
{task.created_at ? new Date(task.created_at).toLocaleString() : "N/A"}
Error
{task.error_message || "No error"}
No tasks found matching your filters.
{context === "tasks" && ( { setPageSize(newPageSize); setCurrentPage(1); }} /> )} {context === "tasks" && editTask && ( )} ); }