Spaces:
Running
Running
Update frontend/src/components/ocr/ExtractionOutput.jsx
Browse files
frontend/src/components/ocr/ExtractionOutput.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { useState } from "react";
|
| 2 |
import { motion, AnimatePresence } from "framer-motion";
|
| 3 |
import {
|
| 4 |
Code2,
|
|
@@ -110,22 +110,110 @@ Support Package 1 $500.00 $500.00
|
|
| 110 |
Payment Terms: Net 30
|
| 111 |
Thank you for your business!`;
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
const [copied, setCopied] = useState(false);
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
const handleCopy = () => {
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
navigator.clipboard.writeText(content);
|
| 126 |
setCopied(true);
|
| 127 |
setTimeout(() => setCopied(false), 2000);
|
| 128 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
const toggleSection = (section) => {
|
| 131 |
setExpandedSections((prev) =>
|
|
@@ -224,7 +312,9 @@ export default function ExtractionOutput({ hasFile, isProcessing, isComplete })
|
|
| 224 |
<div>
|
| 225 |
<h3 className="font-semibold text-slate-800 text-sm">Extracted Data</h3>
|
| 226 |
<p className="text-xs text-slate-400">
|
| 227 |
-
{isComplete
|
|
|
|
|
|
|
| 228 |
</p>
|
| 229 |
</div>
|
| 230 |
</div>
|
|
@@ -309,23 +399,37 @@ export default function ExtractionOutput({ hasFile, isProcessing, isComplete })
|
|
| 309 |
</div>
|
| 310 |
</div>
|
| 311 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
) : (
|
| 313 |
<div className="p-4 font-mono text-sm">
|
| 314 |
{activeTab === "text" ? (
|
| 315 |
<pre className="text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">
|
| 316 |
-
{
|
| 317 |
</pre>
|
| 318 |
) : activeTab === "json" ? (
|
| 319 |
<div className="space-y-1">
|
| 320 |
<span className="text-slate-400">{"{"}</span>
|
| 321 |
-
{Object.
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
)}
|
| 324 |
<span className="text-slate-400">{"}"}</span>
|
| 325 |
</div>
|
| 326 |
) : (
|
| 327 |
<pre className="text-sm text-slate-600 whitespace-pre-wrap">
|
| 328 |
-
{
|
| 329 |
<div key={i} className="hover:bg-slate-50 px-2 -mx-2 rounded">
|
| 330 |
{line.includes("<") ? (
|
| 331 |
<>
|
|
@@ -363,21 +467,28 @@ export default function ExtractionOutput({ hasFile, isProcessing, isComplete })
|
|
| 363 |
</div>
|
| 364 |
|
| 365 |
{/* Confidence Footer */}
|
| 366 |
-
{isComplete && (
|
| 367 |
<div className="px-5 py-3 border-t border-slate-100 bg-slate-50/50">
|
| 368 |
<div className="flex items-center justify-between text-xs">
|
| 369 |
<div className="flex items-center gap-4">
|
| 370 |
<div className="flex items-center gap-1.5">
|
| 371 |
-
<div className=
|
|
|
|
|
|
|
|
|
|
| 372 |
<span className="text-slate-500">Confidence:</span>
|
| 373 |
-
<span className="font-semibold text-slate-700">
|
|
|
|
|
|
|
| 374 |
</div>
|
| 375 |
<div className="flex items-center gap-1.5">
|
| 376 |
<span className="text-slate-500">Fields:</span>
|
| 377 |
-
<span className="font-semibold text-slate-700">
|
| 378 |
</div>
|
| 379 |
</div>
|
| 380 |
-
<span className="text-slate-400">
|
|
|
|
|
|
|
| 381 |
</div>
|
| 382 |
</div>
|
| 383 |
)}
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
import { motion, AnimatePresence } from "framer-motion";
|
| 3 |
import {
|
| 4 |
Code2,
|
|
|
|
| 110 |
Payment Terms: Net 30
|
| 111 |
Thank you for your business!`;
|
| 112 |
|
| 113 |
+
// Helper function to convert object to XML
|
| 114 |
+
function objectToXML(obj, rootName = "extraction") {
|
| 115 |
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}>\n`;
|
| 116 |
+
|
| 117 |
+
const convert = (obj, indent = " ") => {
|
| 118 |
+
for (const [key, value] of Object.entries(obj)) {
|
| 119 |
+
if (value === null || value === undefined) continue;
|
| 120 |
+
|
| 121 |
+
if (Array.isArray(value)) {
|
| 122 |
+
value.forEach((item) => {
|
| 123 |
+
xml += `${indent}<${key}>\n`;
|
| 124 |
+
if (typeof item === "object") {
|
| 125 |
+
convert(item, indent + " ");
|
| 126 |
+
} else {
|
| 127 |
+
xml += `${indent} ${item}\n`;
|
| 128 |
+
}
|
| 129 |
+
xml += `${indent}</${key}>\n`;
|
| 130 |
+
});
|
| 131 |
+
} else if (typeof value === "object") {
|
| 132 |
+
xml += `${indent}<${key}>\n`;
|
| 133 |
+
convert(value, indent + " ");
|
| 134 |
+
xml += `${indent}</${key}>\n`;
|
| 135 |
+
} else {
|
| 136 |
+
xml += `${indent}<${key}>${value}</${key}>\n`;
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
convert(obj);
|
| 142 |
+
xml += `</${rootName}>`;
|
| 143 |
+
return xml;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Helper function to format fields as readable text
|
| 147 |
+
function fieldsToText(fields) {
|
| 148 |
+
if (!fields || typeof fields !== "object") {
|
| 149 |
+
return "No data extracted.";
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
let text = "";
|
| 153 |
+
|
| 154 |
+
const formatValue = (key, value, indent = "") => {
|
| 155 |
+
if (Array.isArray(value)) {
|
| 156 |
+
text += `${indent}${key}:\n`;
|
| 157 |
+
value.forEach((item, idx) => {
|
| 158 |
+
if (typeof item === "object") {
|
| 159 |
+
text += `${indent} Item ${idx + 1}:\n`;
|
| 160 |
+
Object.entries(item).forEach(([k, v]) => formatValue(k, v, indent + " "));
|
| 161 |
+
} else {
|
| 162 |
+
text += `${indent} - ${item}\n`;
|
| 163 |
+
}
|
| 164 |
+
});
|
| 165 |
+
} else if (typeof value === "object" && value !== null) {
|
| 166 |
+
text += `${indent}${key}:\n`;
|
| 167 |
+
Object.entries(value).forEach(([k, v]) => formatValue(k, v, indent + " "));
|
| 168 |
+
} else {
|
| 169 |
+
text += `${indent}${key}: ${value}\n`;
|
| 170 |
+
}
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
Object.entries(fields).forEach(([key, value]) => {
|
| 174 |
+
formatValue(key, value);
|
| 175 |
+
text += "\n";
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
return text.trim() || "No data extracted.";
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export default function ExtractionOutput({ hasFile, isProcessing, isComplete, extractionResult }) {
|
| 182 |
+
const [activeTab, setActiveTab] = useState("json");
|
| 183 |
const [copied, setCopied] = useState(false);
|
| 184 |
+
|
| 185 |
+
// Get fields from extraction result, default to empty object
|
| 186 |
+
const fields = extractionResult?.fields || {};
|
| 187 |
+
const confidence = extractionResult?.confidence || 0;
|
| 188 |
+
const fieldsExtracted = extractionResult?.fieldsExtracted || 0;
|
| 189 |
+
const totalTime = extractionResult?.totalTime || 0;
|
| 190 |
+
|
| 191 |
+
// Initialize expanded sections based on available fields
|
| 192 |
+
const [expandedSections, setExpandedSections] = useState(() =>
|
| 193 |
+
Object.keys(fields).slice(0, 5) // Expand first 5 sections by default
|
| 194 |
+
);
|
| 195 |
|
| 196 |
const handleCopy = () => {
|
| 197 |
+
let content = "";
|
| 198 |
+
if (activeTab === "json") {
|
| 199 |
+
content = JSON.stringify(fields, null, 2);
|
| 200 |
+
} else if (activeTab === "xml") {
|
| 201 |
+
content = objectToXML(fields);
|
| 202 |
+
} else {
|
| 203 |
+
content = fieldsToText(fields);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
navigator.clipboard.writeText(content);
|
| 207 |
setCopied(true);
|
| 208 |
setTimeout(() => setCopied(false), 2000);
|
| 209 |
};
|
| 210 |
+
|
| 211 |
+
// Update expanded sections when fields change
|
| 212 |
+
React.useEffect(() => {
|
| 213 |
+
if (extractionResult?.fields) {
|
| 214 |
+
setExpandedSections(Object.keys(extractionResult.fields).slice(0, 5));
|
| 215 |
+
}
|
| 216 |
+
}, [extractionResult]);
|
| 217 |
|
| 218 |
const toggleSection = (section) => {
|
| 219 |
setExpandedSections((prev) =>
|
|
|
|
| 312 |
<div>
|
| 313 |
<h3 className="font-semibold text-slate-800 text-sm">Extracted Data</h3>
|
| 314 |
<p className="text-xs text-slate-400">
|
| 315 |
+
{isComplete
|
| 316 |
+
? `${fieldsExtracted} field${fieldsExtracted !== 1 ? 's' : ''} extracted`
|
| 317 |
+
: "Waiting for extraction"}
|
| 318 |
</p>
|
| 319 |
</div>
|
| 320 |
</div>
|
|
|
|
| 399 |
</div>
|
| 400 |
</div>
|
| 401 |
</div>
|
| 402 |
+
) : isComplete && Object.keys(fields).length === 0 ? (
|
| 403 |
+
<div className="h-full flex items-center justify-center p-6">
|
| 404 |
+
<div className="text-center">
|
| 405 |
+
<div className="h-20 w-20 mx-auto rounded-2xl bg-amber-100 flex items-center justify-center mb-4">
|
| 406 |
+
<Code2 className="h-10 w-10 text-amber-600" />
|
| 407 |
+
</div>
|
| 408 |
+
<p className="text-slate-600 font-medium mb-1">No data extracted</p>
|
| 409 |
+
<p className="text-slate-400 text-sm">The document may not contain extractable fields</p>
|
| 410 |
+
</div>
|
| 411 |
+
</div>
|
| 412 |
) : (
|
| 413 |
<div className="p-4 font-mono text-sm">
|
| 414 |
{activeTab === "text" ? (
|
| 415 |
<pre className="text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">
|
| 416 |
+
{fieldsToText(fields)}
|
| 417 |
</pre>
|
| 418 |
) : activeTab === "json" ? (
|
| 419 |
<div className="space-y-1">
|
| 420 |
<span className="text-slate-400">{"{"}</span>
|
| 421 |
+
{Object.keys(fields).length > 0 ? (
|
| 422 |
+
Object.entries(fields).map(([key, value]) =>
|
| 423 |
+
renderSection(key, value, 1)
|
| 424 |
+
)
|
| 425 |
+
) : (
|
| 426 |
+
<div className="pl-4 text-slate-400 italic">No fields extracted</div>
|
| 427 |
)}
|
| 428 |
<span className="text-slate-400">{"}"}</span>
|
| 429 |
</div>
|
| 430 |
) : (
|
| 431 |
<pre className="text-sm text-slate-600 whitespace-pre-wrap">
|
| 432 |
+
{objectToXML(fields).split("\n").map((line, i) => (
|
| 433 |
<div key={i} className="hover:bg-slate-50 px-2 -mx-2 rounded">
|
| 434 |
{line.includes("<") ? (
|
| 435 |
<>
|
|
|
|
| 467 |
</div>
|
| 468 |
|
| 469 |
{/* Confidence Footer */}
|
| 470 |
+
{isComplete && extractionResult && (
|
| 471 |
<div className="px-5 py-3 border-t border-slate-100 bg-slate-50/50">
|
| 472 |
<div className="flex items-center justify-between text-xs">
|
| 473 |
<div className="flex items-center gap-4">
|
| 474 |
<div className="flex items-center gap-1.5">
|
| 475 |
+
<div className={cn(
|
| 476 |
+
"h-2 w-2 rounded-full",
|
| 477 |
+
confidence >= 90 ? "bg-emerald-500" : confidence >= 70 ? "bg-amber-500" : "bg-red-500"
|
| 478 |
+
)} />
|
| 479 |
<span className="text-slate-500">Confidence:</span>
|
| 480 |
+
<span className="font-semibold text-slate-700">
|
| 481 |
+
{confidence > 0 ? `${confidence.toFixed(1)}%` : "N/A"}
|
| 482 |
+
</span>
|
| 483 |
</div>
|
| 484 |
<div className="flex items-center gap-1.5">
|
| 485 |
<span className="text-slate-500">Fields:</span>
|
| 486 |
+
<span className="font-semibold text-slate-700">{fieldsExtracted}</span>
|
| 487 |
</div>
|
| 488 |
</div>
|
| 489 |
+
<span className="text-slate-400">
|
| 490 |
+
Processed in {totalTime >= 1000 ? `${(totalTime / 1000).toFixed(1)}s` : `${totalTime}ms`}
|
| 491 |
+
</span>
|
| 492 |
</div>
|
| 493 |
</div>
|
| 494 |
)}
|