| |
|
|
| import React, { useState, useEffect } from "react"; |
| import { useNavigate, useSearchParams } from "react-router-dom"; |
| 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"; |
|
|
| |
| 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 navigate = useNavigate(); |
| const [searchParams, setSearchParams] = useSearchParams(); |
| 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); |
|
|
| |
| useEffect(() => { |
| const fetchHistory = async () => { |
| setIsLoading(true); |
| setError(null); |
| try { |
| const data = await getHistory(); |
| setHistory(data); |
| |
| |
| const extractionId = searchParams.get("extractionId"); |
| if (extractionId) { |
| |
| setSearchParams({}); |
| |
| setTimeout(() => { |
| navigate(`/?extractionId=${extractionId}`); |
| }, 100); |
| } |
| } catch (err) { |
| console.error("Failed to fetch history:", err); |
| setError(err.message || "Failed to load history"); |
| setHistory([]); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| fetchHistory(); |
| }, [searchParams, setSearchParams, navigate]); |
|
|
| 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 > 0 |
| ? (typeof item.confidence === 'number' |
| ? item.confidence.toFixed(2) |
| : parseFloat(item.confidence).toFixed(2)) |
| : "0.00", |
| 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 > 0 |
| ? (typeof item.confidence === 'number' |
| ? item.confidence.toFixed(2) |
| : parseFloat(item.confidence).toFixed(2)) |
| : "0.00", |
| 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 h-16"> |
| <div className="px-8 h-full flex items-center"> |
| <div> |
| <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight"> |
| Extraction History |
| </h1> |
| <p className="text-sm text-slate-500 leading-tight"> |
| View detailed reports and performance metrics for all extractions |
| </p> |
| </div> |
| </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" title={item.fileName}> |
| {item.fileName && item.fileName.length > 50 |
| ? `${item.fileName.substring(0, 50)}...` |
| : 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 |
| ? `${typeof item.confidence === 'number' |
| ? item.confidence.toFixed(2) |
| : parseFloat(item.confidence).toFixed(2)}%` |
| : "-"} |
| </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" |
| onClick={(e) => { |
| e.stopPropagation(); |
| navigate(`/?extractionId=${item.id}`); |
| }} |
| > |
| <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> |
| ); |
| } |
|
|