Seth0330's picture
Update frontend/src/pages/History.jsx
239572f verified
// frontend/src/pages/History.jsx
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
FileText,
Clock,
CheckCircle2,
ChevronRight,
Download,
Eye,
Trash2,
Search,
Filter,
Calendar,
Upload,
Cpu,
TableProperties,
MonitorPlay,
TrendingUp,
TrendingDown,
Minus,
AlertCircle,
X,
FileSpreadsheet,
Table2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { getHistory } from "@/services/api";
// minimal "toast"
const toastSuccess = (msg) => {
console.log(msg);
};
const stageConfig = {
uploading: { label: "Uploading", icon: Upload, color: "blue" },
aiAnalysis: { label: "AI Analysis", icon: Cpu, color: "violet" },
dataExtraction: { label: "Data Extraction", icon: TableProperties, color: "emerald" },
outputRendering: { label: "Output Rendering", icon: MonitorPlay, color: "amber" },
};
const variationConfig = {
fast: { icon: TrendingDown, color: "text-emerald-500", label: "Faster than avg" },
normal: { icon: Minus, color: "text-slate-400", label: "Normal" },
slow: { icon: TrendingUp, color: "text-amber-500", label: "Slower than avg" },
error: { icon: AlertCircle, color: "text-red-500", label: "Error" },
skipped: { icon: Minus, color: "text-slate-300", label: "Skipped" },
};
export default function History() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedStatus, setSelectedStatus] = useState("all");
const [expandedReport, setExpandedReport] = useState(null);
const [isExporting, setIsExporting] = useState(false);
const [history, setHistory] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch history on component mount
useEffect(() => {
const fetchHistory = async () => {
setIsLoading(true);
setError(null);
try {
const data = await getHistory();
setHistory(data);
} catch (err) {
console.error("Failed to fetch history:", err);
setError(err.message || "Failed to load history");
setHistory([]); // Fallback to empty array
} finally {
setIsLoading(false);
}
};
fetchHistory();
}, []);
const filteredHistory = history.filter((item) => {
const matchesSearch = item.fileName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false;
const matchesStatus = selectedStatus === "all" || item.status === selectedStatus;
return matchesSearch && matchesStatus;
});
const formatTime = (ms) => {
if (ms >= 1000) {
return `${(ms / 1000).toFixed(2)}s`;
}
return `${ms}ms`;
};
const formatTimeForExport = (ms) => {
return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatDateForExport = (dateString) => {
const date = new Date(dateString);
return date.toISOString().replace("T", " ").slice(0, 19);
};
const generateCSV = (data) => {
const headers = [
"File Name",
"File Type",
"File Size",
"Extracted At",
"Status",
"Confidence (%)",
"Fields Extracted",
"Total Time (ms)",
"Upload Time (ms)",
"Upload Status",
"Upload Variation",
"AI Analysis Time (ms)",
"AI Analysis Status",
"AI Analysis Variation",
"Data Extraction Time (ms)",
"Data Extraction Status",
"Data Extraction Variation",
"Output Rendering Time (ms)",
"Output Rendering Status",
"Output Rendering Variation",
"Error Message",
];
const rows = data.map((item) => [
item.fileName,
item.fileType,
item.fileSize,
formatDateForExport(item.extractedAt),
item.status,
item.confidence,
item.fieldsExtracted,
item.totalTime,
item.stages.uploading.time,
item.stages.uploading.status,
item.stages.uploading.variation,
item.stages.aiAnalysis.time,
item.stages.aiAnalysis.status,
item.stages.aiAnalysis.variation,
item.stages.dataExtraction.time,
item.stages.dataExtraction.status,
item.stages.dataExtraction.variation,
item.stages.outputRendering.time,
item.stages.outputRendering.status,
item.stages.outputRendering.variation,
item.errorMessage || "",
]);
const csvContent = [
headers.join(","),
...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
].join("\n");
return csvContent;
};
const downloadFile = (content, fileName, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleExportCSV = () => {
setIsExporting(true);
setTimeout(() => {
const csvContent = generateCSV(filteredHistory);
downloadFile(
csvContent,
`extraction_history_${new Date().toISOString().slice(0, 10)}.csv`,
"text/csv;charset=utf-8;"
);
toastSuccess("CSV exported successfully");
setIsExporting(false);
}, 500);
};
const generateExcelXML = (data) => {
const headers = [
"File Name",
"File Type",
"File Size",
"Extracted At",
"Status",
"Confidence (%)",
"Fields Extracted",
"Total Time (ms)",
"Upload Time (ms)",
"Upload Status",
"Upload Variation",
"AI Analysis Time (ms)",
"AI Analysis Status",
"AI Analysis Variation",
"Data Extraction Time (ms)",
"Data Extraction Status",
"Data Extraction Variation",
"Output Rendering Time (ms)",
"Output Rendering Status",
"Output Rendering Variation",
"Error Message",
];
const rows = data.map((item) => [
item.fileName,
item.fileType,
item.fileSize,
formatDateForExport(item.extractedAt),
item.status,
item.confidence,
item.fieldsExtracted,
item.totalTime,
item.stages.uploading.time,
item.stages.uploading.status,
item.stages.uploading.variation,
item.stages.aiAnalysis.time,
item.stages.aiAnalysis.status,
item.stages.aiAnalysis.variation,
item.stages.dataExtraction.time,
item.stages.dataExtraction.status,
item.stages.dataExtraction.variation,
item.stages.outputRendering.time,
item.stages.outputRendering.status,
item.stages.outputRendering.variation,
item.errorMessage || "",
]);
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
<Worksheet ss:Name="Extraction History">
<Table>
<Row>`;
headers.forEach((header) => {
xml += `<Cell><Data ss:Type="String">${header}</Data></Cell>`;
});
xml += `</Row>`;
rows.forEach((row) => {
xml += `<Row>`;
row.forEach((cell) => {
const type = typeof cell === "number" ? "Number" : "String";
xml += `<Cell><Data ss:Type="${type}">${cell}</Data></Cell>`;
});
xml += `</Row>`;
});
xml += `</Table></Worksheet></Workbook>`;
return xml;
};
const handleExportExcel = () => {
setIsExporting(true);
setTimeout(() => {
const excelContent = generateExcelXML(filteredHistory);
downloadFile(
excelContent,
`extraction_history_${new Date().toISOString().slice(0, 10)}.xls`,
"application/vnd.ms-excel"
);
toastSuccess("Excel file exported successfully");
setIsExporting(false);
}, 500);
};
const handleExportSingleReport = (item, format) => {
if (format === "csv") {
const csvContent = generateCSV([item]);
downloadFile(
csvContent,
`${item.fileName.replace(/\.[^/.]+$/, "")}_report.csv`,
"text/csv;charset=utf-8;"
);
toastSuccess("Report exported as CSV");
} else {
const excelContent = generateExcelXML([item]);
downloadFile(
excelContent,
`${item.fileName.replace(/\.[^/.]+$/, "")}_report.xls`,
"application/vnd.ms-excel"
);
toastSuccess("Report exported as Excel");
}
};
return (
<div className="min-h-screen bg-[#FAFAFA]">
{/* Header */}
<header className="bg-white border-b border-slate-200/80 sticky top-0 z-40">
<div className="px-8 py-4">
<h1 className="text-xl font-bold text-slate-900 tracking-tight">
Extraction History
</h1>
<p className="text-sm text-slate-500 mt-0.5">
View detailed reports and performance metrics for all extractions
</p>
</div>
</header>
{/* Content */}
<div className="p-8">
{/* Filters */}
<div className="flex items-center gap-4 mb-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Search by file name..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-11 rounded-xl border-slate-200"
/>
</div>
<Select
value={selectedStatus}
onValueChange={(value) => setSelectedStatus(value)}
>
<SelectTrigger className="w-40 h-11 rounded-xl border-slate-200">
<Filter className="h-4 w-4 mr-2 text-slate-400" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
</SelectContent>
</Select>
{/* Export All Button */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="h-11 px-4 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-lg shadow-indigo-500/25"
disabled={isExporting || filteredHistory.length === 0}
>
{isExporting ? (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
className="mr-2"
>
<Download className="h-4 w-4" />
</motion.div>
) : (
<Download className="h-4 w-4 mr-2" />
)}
Export All
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-48 rounded-xl p-2"
>
<DropdownMenuItem
className="rounded-lg cursor-pointer"
onClick={handleExportCSV}
>
<Table2 className="h-4 w-4 mr-2 text-emerald-600" />
Export as CSV
</DropdownMenuItem>
<DropdownMenuItem
className="rounded-lg cursor-pointer"
onClick={handleExportExcel}
>
<FileSpreadsheet className="h-4 w-4 mr-2 text-green-600" />
Export as Excel
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="px-2 py-1.5 text-xs text-slate-500">
{filteredHistory.length} records will be exported
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
{(() => {
const total = history.length;
const completed = history.filter((h) => h.status === "completed").length;
const successRate = total > 0 ? ((completed / total) * 100).toFixed(1) : 0;
const avgTime = history.length > 0
? history.reduce((sum, h) => sum + (h.totalTime || 0), 0) / history.length
: 0;
const totalFields = history.reduce((sum, h) => sum + (h.fieldsExtracted || 0), 0);
return [
{
label: "Total Extractions",
value: total.toString(),
change: "",
color: "indigo",
},
{
label: "Success Rate",
value: `${successRate}%`,
change: total > 0 ? `${completed}/${total} successful` : "No data",
color: "emerald",
},
{
label: "Avg. Processing Time",
value: avgTime >= 1000 ? `${(avgTime / 1000).toFixed(1)}s` : `${Math.round(avgTime)}ms`,
change: "",
color: "violet",
},
{
label: "Fields Extracted",
value: totalFields.toLocaleString(),
change: "",
color: "amber",
},
].map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white rounded-2xl border border-slate-200 p-5"
>
<p className="text-sm text-slate-500 mb-1">{stat.label}</p>
<p className="text-2xl font-bold text-slate-900">{stat.value}</p>
<p className={`text-xs text-${stat.color}-600 mt-1`}>
{stat.change}
</p>
</motion.div>
));
})()}
</div>
{/* Loading State */}
{isLoading && (
<div className="text-center py-16">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="h-16 w-16 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4"
>
<Cpu className="h-8 w-8 text-indigo-600" />
</motion.div>
<p className="text-slate-500">Loading extraction history...</p>
</div>
)}
{/* History List */}
{!isLoading && (
<div className="space-y-4">
{filteredHistory.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="bg-white rounded-2xl border border-slate-200 overflow-hidden"
>
{/* Main Row */}
<div
className="p-5 cursor-pointer hover:bg-slate-50/50 transition-colors"
onClick={() =>
setExpandedReport(
expandedReport === item.id ? null : item.id
)
}
>
<div className="flex items-center gap-4">
{/* File Icon */}
<div
className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center",
item.status === "completed" ? "bg-indigo-50" : "bg-red-50"
)}
>
<FileText
className={cn(
"h-6 w-6",
item.status === "completed"
? "text-indigo-600"
: "text-red-500"
)}
/>
</div>
{/* File Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-slate-900 truncate">
{item.fileName}
</h3>
<Badge variant="secondary" className="text-xs">
{item.fileType}
</Badge>
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-slate-500">
<span>{item.fileSize}</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(item.extractedAt)}
</span>
</div>
</div>
{/* Stats */}
<div className="hidden md:flex items-center gap-6">
<div className="text-center">
<p className="text-xs text-slate-400">Time</p>
<p className="font-semibold text-slate-700">
{formatTime(item.totalTime)}
</p>
</div>
<div className="text-center">
<p className="text-xs text-slate-400">Fields</p>
<p className="font-semibold text-slate-700">
{item.fieldsExtracted}
</p>
</div>
<div className="text-center">
<p className="text-xs text-slate-400">Confidence</p>
<p
className={cn(
"font-semibold",
item.confidence >= 95
? "text-emerald-600"
: item.confidence >= 90
? "text-amber-600"
: "text-red-600"
)}
>
{item.confidence > 0 ? `${item.confidence}%` : "-"}
</p>
</div>
</div>
{/* Status & Actions */}
<div className="flex items-center gap-3">
<Badge
className={cn(
"capitalize",
item.status === "completed"
? "bg-emerald-50 text-emerald-700 border-emerald-200"
: "bg-red-50 text-red-700 border-red-200"
)}
>
{item.status === "completed" ? (
<CheckCircle2 className="h-3 w-3 mr-1" />
) : (
<AlertCircle className="h-3 w-3 mr-1" />
)}
{item.status}
</Badge>
<ChevronRight
className={cn(
"h-5 w-5 text-slate-400 transition-transform",
expandedReport === item.id && "rotate-90"
)}
/>
</div>
</div>
</div>
{/* Expanded Report */}
<AnimatePresence>
{expandedReport === item.id && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-5 pb-5 pt-2 border-t border-slate-100">
{/* Error Message */}
{item.errorMessage && (
<div className="mb-4 p-4 bg-red-50 border border-red-100 rounded-xl">
<div className="flex items-center gap-2 text-red-700">
<AlertCircle className="h-4 w-4" />
<span className="font-medium">Error Details</span>
</div>
<p className="text-sm text-red-600 mt-1">
{item.errorMessage}
</p>
</div>
)}
{/* Performance Report Header */}
<div className="flex items-center justify-between mb-4">
<h4 className="font-semibold text-slate-800">
Performance Report
</h4>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-8 text-xs"
>
<Eye className="h-3 w-3 mr-1" />
View Output
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 text-xs"
>
<Download className="h-3 w-3 mr-1" />
Export Report
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-44 rounded-xl p-2"
>
<DropdownMenuItem
className="rounded-lg cursor-pointer text-xs"
onClick={(e) => {
e.stopPropagation();
handleExportSingleReport(item, "csv");
}}
>
<Table2 className="h-3 w-3 mr-2 text-emerald-600" />
Download CSV
</DropdownMenuItem>
<DropdownMenuItem
className="rounded-lg cursor-pointer text-xs"
onClick={(e) => {
e.stopPropagation();
handleExportSingleReport(item, "excel");
}}
>
<FileSpreadsheet className="h-3 w-3 mr-2 text-green-600" />
Download Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Stage Timing Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{Object.entries(item.stages).map(
([stageKey, stageData]) => {
const config = stageConfig[stageKey];
const variationInfo =
variationConfig[stageData.variation];
const Icon = config.icon;
const VariationIcon = variationInfo.icon;
return (
<div
key={stageKey}
className={cn(
"relative p-4 rounded-xl border",
stageData.status === "completed"
? "bg-slate-50 border-slate-200"
: stageData.status === "failed"
? "bg-red-50 border-red-200"
: "bg-slate-50/50 border-slate-100"
)}
>
<div className="flex items-center gap-2 mb-3">
<div
className={cn(
"h-8 w-8 rounded-lg flex items-center justify-center",
`bg-${config.color}-100`
)}
>
<Icon
className={cn(
"h-4 w-4",
`text-${config.color}-600`
)}
/>
</div>
<span className="text-sm font-medium text-slate-700">
{config.label}
</span>
</div>
<div className="flex items-end justify-between">
<div>
<p
className={cn(
"text-2xl font-bold",
stageData.status === "skipped"
? "text-slate-300"
: stageData.status === "failed"
? "text-red-600"
: "text-slate-900"
)}
>
{stageData.status === "skipped"
? "-"
: formatTime(stageData.time)}
</p>
{stageData.status !== "skipped" && (
<div className="flex items-center gap-1 mt-1">
<VariationIcon
className={cn(
"h-3 w-3",
variationInfo.color
)}
/>
<span
className={cn(
"text-xs",
variationInfo.color
)}
>
{variationInfo.label}
</span>
</div>
)}
</div>
{stageData.status === "completed" && (
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
)}
{stageData.status === "failed" && (
<X className="h-5 w-5 text-red-500" />
)}
</div>
{/* Progress bar */}
<div className="mt-3 h-1.5 bg-slate-200 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{
width:
stageData.status === "completed"
? "100%"
: stageData.status === "failed"
? "60%"
: "0%",
}}
transition={{ duration: 0.5, delay: 0.2 }}
className={cn(
"h-full rounded-full",
stageData.status === "failed"
? "bg-red-500"
: `bg-${config.color}-500`
)}
/>
</div>
</div>
);
}
)}
</div>
{/* Total Time Summary */}
<div className="mt-4 flex items-center justify-between p-4 bg-gradient-to-r from-indigo-50 to-violet-50 rounded-xl border border-indigo-100">
<div className="flex items-center gap-3">
<Clock className="h-5 w-5 text-indigo-600" />
<div>
<p className="text-sm font-medium text-slate-700">
Total Processing Time
</p>
<p className="text-xs text-slate-500">
From upload to output ready
</p>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-indigo-600">
{formatTime(item.totalTime)}
</p>
<p className="text-xs text-slate-500">
{item.status === "completed"
? "Completed successfully"
: "Process failed"}
</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
{filteredHistory.length === 0 && !error && (
<div className="text-center py-16">
<div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
<FileText className="h-10 w-10 text-slate-300" />
</div>
<p className="text-slate-500 mb-2">
{history.length === 0
? "No extraction history yet"
: "No extractions match your filters"}
</p>
{history.length === 0 && (
<p className="text-sm text-slate-400">
Upload a document to get started
</p>
)}
</div>
)}
</div>
)}
</div>
</div>
);
}