AIEXTRACT1 / frontend /src /components /ocr /ExtractionOutput.jsx
Seth0330's picture
Create frontend/src/components/ocr/ExtractionOutput.jsx
c697880 verified
raw
history blame
13.5 kB
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Code2,
Copy,
Check,
Braces,
FileCode2,
FileText,
Sparkles,
ChevronDown,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
// Mock extracted data
const mockData = {
document: {
type: "Invoice",
confidence: 0.98,
},
vendor: {
name: "Acme Corporation",
address: "123 Business Ave, Suite 400",
city: "San Francisco",
state: "CA",
zip: "94102",
phone: "+1 (555) 123-4567",
},
invoice: {
number: "INV-2024-0847",
date: "2024-01-15",
due_date: "2024-02-14",
po_number: "PO-9823",
},
items: [
{ description: "Professional Services", quantity: 40, unit_price: 150.0, total: 6000.0 },
{ description: "Software License", quantity: 5, unit_price: 299.99, total: 1499.95 },
{ description: "Support Package", quantity: 1, unit_price: 500.0, total: 500.0 },
],
totals: {
subtotal: 7999.95,
tax_rate: 0.0875,
tax_amount: 699.99,
total: 8699.94,
},
};
const mockXML = `<?xml version="1.0" encoding="UTF-8"?>
<extraction>
<document type="Invoice" confidence="0.98"/>
<vendor>
<name>Acme Corporation</name>
<address>123 Business Ave, Suite 400</address>
<city>San Francisco</city>
<state>CA</state>
<zip>94102</zip>
</vendor>
<invoice>
<number>INV-2024-0847</number>
<date>2024-01-15</date>
<due_date>2024-02-14</due_date>
</invoice>
<items>
<item>
<description>Professional Services</description>
<quantity>40</quantity>
<total>6000.00</total>
</item>
</items>
<totals>
<subtotal>7999.95</subtotal>
<tax>699.99</tax>
<total>8699.94</total>
</totals>
</extraction>`;
const mockText = `INVOICE
ACME CORPORATION
123 Business Ave, Suite 400
San Francisco, CA 94102
Phone: +1 (555) 123-4567
Invoice Number: INV-2024-0847
Invoice Date: January 15, 2024
Due Date: February 14, 2024
PO Number: PO-9823
BILL TO:
Customer Name
456 Client Street
New York, NY 10001
ITEMS:
─────────────────────────────────────────────────────────
Description Qty Unit Price Total
─────────────────────────────────────────────────────────
Professional Services 40 $150.00 $6,000.00
Software License 5 $299.99 $1,499.95
Support Package 1 $500.00 $500.00
─────────────────────────────────────────────────────────
Subtotal: $7,999.95
Tax (8.75%): $699.99
─────────────────────────
TOTAL: $8,699.94
Payment Terms: Net 30
Thank you for your business!`;
export default function ExtractionOutput({ hasFile, isProcessing, isComplete }) {
const [activeTab, setActiveTab] = useState("text");
const [copied, setCopied] = useState(false);
const [expandedSections, setExpandedSections] = useState(["vendor", "invoice", "items", "totals"]);
const handleCopy = () => {
const content =
activeTab === "json"
? JSON.stringify(mockData, null, 2)
: activeTab === "xml"
? mockXML
: mockText;
navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const toggleSection = (section) => {
setExpandedSections((prev) =>
prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
);
};
const renderValue = (value) => {
if (typeof value === "number") {
return <span className="text-amber-600">{value}</span>;
}
if (typeof value === "string") {
return <span className="text-emerald-600">"{value}"</span>;
}
return String(value);
};
const renderSection = (key, value, level = 0) => {
const isExpanded = expandedSections.includes(key);
const isObject = typeof value === "object" && value !== null;
const isArray = Array.isArray(value);
if (!isObject) {
return (
<div
key={key}
className="flex items-start gap-2 py-1"
style={{ paddingLeft: level * 16 }}
>
<span className="text-violet-500">"{key}"</span>
<span className="text-slate-400">:</span>
{renderValue(value)}
</div>
);
}
return (
<div key={key}>
<button
onClick={() => toggleSection(key)}
className="flex items-center gap-2 py-1 hover:bg-slate-50 w-full text-left rounded"
style={{ paddingLeft: level * 16 }}
>
<ChevronDown
className={cn(
"h-3 w-3 text-slate-400 transition-transform",
!isExpanded && "-rotate-90"
)}
/>
<span className="text-violet-500">"{key}"</span>
<span className="text-slate-400">:</span>
<span className="text-slate-400">{isArray ? "[" : "{"}</span>
{!isExpanded && (
<span className="text-slate-300 text-xs">
{isArray ? `${value.length} items` : `${Object.keys(value).length} fields`}
</span>
)}
</button>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
{isArray ? (
value.map((item, idx) => (
<div key={idx} className="border-l border-slate-100 ml-4">
{Object.entries(item).map(([k, v]) => renderSection(k, v, level + 2))}
{idx < value.length - 1 && <div className="h-2" />}
</div>
))
) : (
Object.entries(value).map(([k, v]) => renderSection(k, v, level + 1))
)}
<div style={{ paddingLeft: level * 16 }} className="text-slate-400">
{isArray ? "]" : "}"}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
return (
<div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-lg bg-emerald-50 flex items-center justify-center">
<Code2 className="h-4 w-4 text-emerald-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 text-sm">Extracted Data</h3>
<p className="text-xs text-slate-400">
{isComplete ? "25 fields extracted" : "Waiting for extraction"}
</p>
</div>
</div>
{isComplete && (
<div className="flex items-center gap-2">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="h-8 bg-slate-100 p-0.5">
<TabsTrigger value="text" className="h-7 text-xs gap-1.5">
<FileText className="h-3 w-3" />
Text
</TabsTrigger>
<TabsTrigger value="json" className="h-7 text-xs gap-1.5">
<Braces className="h-3 w-3" />
JSON
</TabsTrigger>
<TabsTrigger value="xml" className="h-7 text-xs gap-1.5">
<FileCode2 className="h-3 w-3" />
XML
</TabsTrigger>
</TabsList>
</Tabs>
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="h-8 text-xs gap-1.5"
>
{copied ? (
<>
<Check className="h-3 w-3 text-emerald-500" />
Copied
</>
) : (
<>
<Copy className="h-3 w-3" />
Copy
</>
)}
</Button>
</div>
)}
</div>
{/* Output Area */}
<div className="flex-1 overflow-auto">
{!hasFile ? (
<div className="h-full flex items-center justify-center p-6">
<div className="text-center">
<div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
<Code2 className="h-10 w-10 text-slate-300" />
</div>
<p className="text-slate-400 text-sm">Extracted data will appear here</p>
</div>
</div>
) : isProcessing ? (
<div className="h-full flex items-center justify-center p-6">
<div className="text-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
className="h-16 w-16 mx-auto rounded-2xl bg-gradient-to-br from-indigo-100 to-violet-100 flex items-center justify-center mb-4"
>
<Sparkles className="h-8 w-8 text-indigo-500" />
</motion.div>
<p className="text-slate-700 font-medium mb-1">AI is extracting data...</p>
<p className="text-slate-400 text-sm">Analyzing document structure</p>
<div className="mt-6 flex items-center justify-center gap-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.2, 1] }}
transition={{
duration: 0.6,
repeat: Infinity,
delay: i * 0.2,
}}
className="h-2 w-2 rounded-full bg-indigo-400"
/>
))}
</div>
</div>
</div>
) : (
<div className="p-4 font-mono text-sm">
{activeTab === "text" ? (
<pre className="text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">
{mockText}
</pre>
) : activeTab === "json" ? (
<div className="space-y-1">
<span className="text-slate-400">{"{"}</span>
{Object.entries(mockData).map(([key, value]) =>
renderSection(key, value, 1)
)}
<span className="text-slate-400">{"}"}</span>
</div>
) : (
<pre className="text-sm text-slate-600 whitespace-pre-wrap">
{mockXML.split("\n").map((line, i) => (
<div key={i} className="hover:bg-slate-50 px-2 -mx-2 rounded">
{line.includes("<") ? (
<>
{line.split(/(<\/?[\w\s=".-]+>)/g).map((part, j) => {
if (part.startsWith("</")) {
return (
<span key={j} className="text-rose-500">
{part}
</span>
);
}
if (part.startsWith("<")) {
return (
<span key={j} className="text-indigo-500">
{part}
</span>
);
}
return (
<span key={j} className="text-slate-700">
{part}
</span>
);
})}
</>
) : (
line
)}
</div>
))}
</pre>
)}
</div>
)}
</div>
{/* Confidence Footer */}
{isComplete && (
<div className="px-5 py-3 border-t border-slate-100 bg-slate-50/50">
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<div className="h-2 w-2 rounded-full bg-emerald-500" />
<span className="text-slate-500">Confidence:</span>
<span className="font-semibold text-slate-700">98%</span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-slate-500">Fields:</span>
<span className="font-semibold text-slate-700">25</span>
</div>
</div>
<span className="text-slate-400">Processed in 2.3s</span>
</div>
</div>
)}
</div>
);
}