Seth commited on
Commit
c847cac
·
1 Parent(s): f21322e
frontend/src/components/ExportButtons.jsx CHANGED
@@ -1,697 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import {
5
- Download,
6
- Braces,
7
- FileCode2,
8
- Check,
9
- Share2,
10
- FileText,
11
- Link2,
12
- Mail,
13
- } from "lucide-react";
14
- import { Button } from "@/components/ui/button";
15
- import {
16
- DropdownMenu,
17
- DropdownMenuContent,
18
- DropdownMenuItem,
19
- DropdownMenuSeparator,
20
- DropdownMenuTrigger,
21
- } from "@/components/ui/dropdown-menu";
22
- import { cn } from "@/lib/utils";
23
- import ShareModal from "@/components/ShareModal";
24
- import ShareLinkModal from "@/components/ShareLinkModal";
25
- import { shareExtraction, createShareLink } from "@/services/api";
26
-
27
- // Helper functions from ExtractionOutput
28
- function prepareFieldsForOutput(fields, format = "json") {
29
- if (!fields || typeof fields !== "object") {
30
- return fields;
31
- }
32
-
33
- const output = { ...fields };
34
-
35
- // Extract Fields from root level if it exists
36
- const rootFields = output.Fields;
37
- // Remove Fields from output temporarily (will be added back at top)
38
- delete output.Fields;
39
-
40
- // Remove full_text from top-level if pages array exists (to avoid duplication)
41
- if (output.pages && Array.isArray(output.pages) && output.pages.length > 0) {
42
- delete output.full_text;
43
-
44
- // Clean up each page: remove full_text from page.fields (it duplicates page.text)
45
- output.pages = output.pages.map(page => {
46
- const cleanedPage = { ...page };
47
- if (cleanedPage.fields && typeof cleanedPage.fields === "object") {
48
- const cleanedFields = { ...cleanedPage.fields };
49
- // Remove full_text from page fields (duplicates page.text)
50
- delete cleanedFields.full_text;
51
- cleanedPage.fields = cleanedFields;
52
- }
53
- return cleanedPage;
54
- });
55
- }
56
-
57
- // For JSON and XML: restructure pages into separate top-level fields (page_1, page_2, etc.)
58
- if ((format === "json" || format === "xml") && output.pages && Array.isArray(output.pages)) {
59
- // Get top-level field keys (these are merged from all pages - avoid duplicating in page fields)
60
- const topLevelKeys = new Set(Object.keys(output).filter(k => k !== "pages" && k !== "full_text" && k !== "Fields"));
61
-
62
- output.pages.forEach((page, idx) => {
63
- const pageNum = page.page_number || idx + 1;
64
- const pageFields = page.fields || {};
65
-
66
- // Remove duplicate fields from page.fields:
67
- // 1. Remove full_text (duplicates page.text)
68
- // 2. Remove fields that match top-level fields (already shown at root)
69
- const cleanedPageFields = {};
70
- for (const [key, value] of Object.entries(pageFields)) {
71
- // Skip full_text and fields that match top-level exactly
72
- if (key !== "full_text" && (!topLevelKeys.has(key) || (value !== output[key]))) {
73
- cleanedPageFields[key] = value;
74
- }
75
- }
76
-
77
- const pageObj = {
78
- text: page.text || "",
79
- confidence: page.confidence || 0,
80
- doc_type: page.doc_type || "other"
81
- };
82
-
83
- // Add table and footer_notes if they exist
84
- if (page.table && Array.isArray(page.table) && page.table.length > 0) {
85
- pageObj.table = page.table;
86
- }
87
- if (page.footer_notes && Array.isArray(page.footer_notes) && page.footer_notes.length > 0) {
88
- pageObj.footer_notes = page.footer_notes;
89
- }
90
-
91
- // Only add fields if there are unique page-specific fields
92
- if (Object.keys(cleanedPageFields).length > 0) {
93
- pageObj.fields = cleanedPageFields;
94
- }
95
-
96
- output[`page_${pageNum}`] = pageObj;
97
- });
98
- // Remove pages array - we now have page_1, page_2, etc. as separate fields
99
- delete output.pages;
100
- }
101
-
102
- // Handle page_X structure (from backend) - remove Fields from page objects if they exist
103
- if (output && typeof output === "object") {
104
- const pageKeys = Object.keys(output).filter(k => k.startsWith("page_"));
105
- for (const pageKey of pageKeys) {
106
- const pageData = output[pageKey];
107
- if (pageData && typeof pageData === "object") {
108
- // Remove Fields from page objects (it's now at root level)
109
- delete pageData.Fields;
110
- delete pageData.metadata;
111
- }
112
- }
113
- }
114
-
115
- // Rebuild output with Fields at the top (only if it exists and is not empty)
116
- const finalOutput = {};
117
- if (rootFields && typeof rootFields === "object" && Object.keys(rootFields).length > 0) {
118
- finalOutput.Fields = rootFields;
119
- }
120
-
121
- // Add all other keys
122
- Object.keys(output).forEach(key => {
123
- finalOutput[key] = output[key];
124
- });
125
-
126
- return finalOutput;
127
- }
128
-
129
- function escapeXML(str) {
130
- return str
131
- .replace(/&/g, "&amp;")
132
- .replace(/</g, "&lt;")
133
- .replace(/>/g, "&gt;")
134
- .replace(/"/g, "&quot;")
135
- .replace(/'/g, "&apos;");
136
- }
137
-
138
- function objectToXML(obj, rootName = "extraction") {
139
- // Prepare fields - remove full_text if pages exist
140
- const preparedObj = prepareFieldsForOutput(obj, "xml");
141
-
142
- let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}>\n`;
143
-
144
- const convert = (obj, indent = " ") => {
145
- for (const [key, value] of Object.entries(obj)) {
146
- if (value === null || value === undefined) continue;
147
-
148
- // Skip full_text if pages exist (already handled in prepareFieldsForOutput)
149
- if (key === "full_text" && obj.pages && Array.isArray(obj.pages) && obj.pages.length > 0) {
150
- continue;
151
- }
152
-
153
- if (Array.isArray(value)) {
154
- value.forEach((item) => {
155
- xml += `${indent}<${key}>\n`;
156
- if (typeof item === "object") {
157
- convert(item, indent + " ");
158
- } else {
159
- xml += `${indent} ${escapeXML(String(item))}\n`;
160
- }
161
- xml += `${indent}</${key}>\n`;
162
- });
163
- } else if (typeof value === "object") {
164
- xml += `${indent}<${key}>\n`;
165
- convert(value, indent + " ");
166
- xml += `${indent}</${key}>\n`;
167
- } else {
168
- xml += `${indent}<${key}>${escapeXML(String(value))}</${key}>\n`;
169
- }
170
- }
171
- };
172
-
173
- convert(preparedObj);
174
- xml += `</${rootName}>`;
175
- return xml;
176
- }
177
-
178
- export default function ExportButtons({ isComplete, extractionResult }) {
179
- const [downloading, setDownloading] = useState(null);
180
- const [copied, setCopied] = useState(false);
181
- const [isShareModalOpen, setIsShareModalOpen] = useState(false);
182
- const [isShareLinkModalOpen, setIsShareLinkModalOpen] = useState(false);
183
- const [shareLink, setShareLink] = useState("");
184
- const [isGeneratingLink, setIsGeneratingLink] = useState(false);
185
-
186
- // Helper function to extract text from fields (same as in ExtractionOutput)
187
- const extractTextFromFields = (fields) => {
188
- if (!fields || typeof fields !== "object") {
189
- return "";
190
- }
191
-
192
- // Check for page_X structure first (preferred format)
193
- const pageKeys = Object.keys(fields).filter(key => key.startsWith("page_"));
194
- if (pageKeys.length > 0) {
195
- // Get text from first page (or combine all pages)
196
- const pageTexts = pageKeys.map(key => {
197
- const page = fields[key];
198
- if (page && page.text) {
199
- return page.text;
200
- }
201
- return "";
202
- }).filter(text => text);
203
-
204
- if (pageTexts.length > 0) {
205
- return pageTexts.join("\n\n");
206
- }
207
- }
208
-
209
- // Fallback to full_text
210
- if (fields.full_text) {
211
- return fields.full_text;
212
- }
213
-
214
- return "";
215
- };
216
-
217
- // Helper function to escape HTML
218
- const escapeHtml = (text) => {
219
- if (!text) return '';
220
- const div = document.createElement('div');
221
- div.textContent = text;
222
- return div.innerHTML;
223
- };
224
-
225
- // Helper function to convert pipe-separated tables to HTML tables
226
- const convertPipeTablesToHTML = (text) => {
227
- if (!text) return text;
228
-
229
- const lines = text.split('\n');
230
- const result = [];
231
- let i = 0;
232
-
233
- while (i < lines.length) {
234
- const line = lines[i];
235
-
236
- // Check if this line looks like a table row (has multiple pipes)
237
- if (line.includes('|') && line.split('|').length >= 3) {
238
- // Check if it's a separator line (only |, -, :, spaces)
239
- const isSeparator = /^[\s|\-:]+$/.test(line.trim());
240
-
241
- if (!isSeparator) {
242
- // Start of a table - collect all table rows
243
- const tableRows = [];
244
- let j = i;
245
-
246
- // Collect header row
247
- const headerLine = lines[j];
248
- const headerCells = headerLine.split('|').map(cell => cell.trim()).filter(cell => cell || cell === '');
249
- // Remove empty cells at start/end
250
- if (headerCells.length > 0 && !headerCells[0]) headerCells.shift();
251
- if (headerCells.length > 0 && !headerCells[headerCells.length - 1]) headerCells.pop();
252
-
253
- if (headerCells.length >= 2) {
254
- tableRows.push(headerCells);
255
- j++;
256
-
257
- // Skip separator line if present
258
- if (j < lines.length && /^[\s|\-:]+$/.test(lines[j].trim())) {
259
- j++;
260
- }
261
-
262
- // Collect data rows
263
- while (j < lines.length) {
264
- const rowLine = lines[j];
265
- if (!rowLine.trim()) break; // Empty line ends table
266
-
267
- // Check if it's still a table row
268
- if (rowLine.includes('|') && rowLine.split('|').length >= 2) {
269
- const isRowSeparator = /^[\s|\-:]+$/.test(rowLine.trim());
270
- if (!isRowSeparator) {
271
- const rowCells = rowLine.split('|').map(cell => cell.trim());
272
- // Remove empty cells at start/end
273
- if (rowCells.length > 0 && !rowCells[0]) rowCells.shift();
274
- if (rowCells.length > 0 && !rowCells[rowCells.length - 1]) rowCells.pop();
275
- tableRows.push(rowCells);
276
- j++;
277
- } else {
278
- j++;
279
- }
280
- } else {
281
- break; // Not a table row anymore
282
- }
283
- }
284
-
285
- // Convert to HTML table
286
- if (tableRows.length > 0) {
287
- let htmlTable = '<table class="border-collapse border border-gray-300 w-full my-4">\n<thead>\n<tr>';
288
-
289
- // Header row
290
- tableRows[0].forEach(cell => {
291
- htmlTable += `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${escapeHtml(cell)}</th>`;
292
- });
293
- htmlTable += '</tr>\n</thead>\n<tbody>\n';
294
-
295
- // Data rows
296
- for (let rowIdx = 1; rowIdx < tableRows.length; rowIdx++) {
297
- htmlTable += '<tr>';
298
- tableRows[rowIdx].forEach((cell, colIdx) => {
299
- // Use header cell count to ensure alignment
300
- const cellContent = cell || '';
301
- htmlTable += `<td class="border border-gray-300 px-4 py-2">${escapeHtml(cellContent)}</td>`;
302
- });
303
- htmlTable += '</tr>\n';
304
- }
305
-
306
- htmlTable += '</tbody>\n</table>';
307
- result.push(htmlTable);
308
- i = j;
309
- continue;
310
- }
311
- }
312
- }
313
- }
314
-
315
- // Not a table row, add as-is
316
- result.push(line);
317
- i++;
318
- }
319
-
320
- return result.join('\n');
321
- };
322
-
323
- // Helper function to render markdown to HTML (same as in ExtractionOutput)
324
- const renderMarkdownToHTML = (text) => {
325
- if (!text) return "";
326
-
327
- let html = text;
328
-
329
- // FIRST: Convert pipe-separated tables to HTML tables
330
- html = convertPipeTablesToHTML(html);
331
-
332
- // Convert LaTeX-style superscripts/subscripts FIRST
333
- html = html.replace(/\$\s*\^\s*\{([^}]+)\}\s*\$/g, '<sup>$1</sup>');
334
- html = html.replace(/\$\s*\^\s*([^\s$<>]+)\s*\$/g, '<sup>$1</sup>');
335
- html = html.replace(/\$\s*_\s*\{([^}]+)\}\s*\$/g, '<sub>$1</sub>');
336
- html = html.replace(/\$\s*_\s*([^\s$<>]+)\s*\$/g, '<sub>$1</sub>');
337
-
338
- // Protect HTML table blocks
339
- const htmlBlocks = [];
340
- let htmlBlockIndex = 0;
341
-
342
- html = html.replace(/<table[\s\S]*?<\/table>/gi, (match) => {
343
- const placeholder = `__HTML_BLOCK_${htmlBlockIndex}__`;
344
- htmlBlocks[htmlBlockIndex] = match;
345
- htmlBlockIndex++;
346
- return placeholder;
347
- });
348
-
349
- // Convert markdown headers
350
- html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
351
- html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
352
- html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
353
-
354
- // Convert markdown bold/italic
355
- html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
356
- html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
357
-
358
- // Convert markdown links
359
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
360
-
361
- // Process line breaks
362
- const parts = html.split(/(__HTML_BLOCK_\d+__)/);
363
- const processedParts = parts.map((part) => {
364
- if (part.match(/^__HTML_BLOCK_\d+__$/)) {
365
- const blockIndex = parseInt(part.match(/\d+/)[0]);
366
- return htmlBlocks[blockIndex];
367
- } else {
368
- let processed = part;
369
- processed = processed.replace(/\n\n+/g, '</p><p>');
370
- processed = processed.replace(/([^\n>])\n([^\n<])/g, '$1<br>$2');
371
- if (processed.trim() && !processed.trim().startsWith('<')) {
372
- processed = '<p>' + processed + '</p>';
373
- }
374
- return processed;
375
- }
376
- });
377
-
378
- html = processedParts.join('');
379
- html = html.replace(/<p><\/p>/g, '');
380
- html = html.replace(/<p>\s*<br>\s*<\/p>/g, '');
381
- html = html.replace(/<p>\s*<\/p>/g, '');
382
-
383
- return html;
384
- };
385
-
386
- const handleDownload = async (format) => {
387
- if (!extractionResult || !extractionResult.fields) {
388
- console.error("No extraction data available");
389
- return;
390
- }
391
-
392
- setDownloading(format);
393
-
394
- try {
395
- const fields = extractionResult.fields;
396
- let content = "";
397
- let filename = "";
398
- let mimeType = "";
399
-
400
- if (format === "json") {
401
- const preparedFields = prepareFieldsForOutput(fields, "json");
402
- content = JSON.stringify(preparedFields, null, 2);
403
- filename = `extraction_${new Date().toISOString().split('T')[0]}.json`;
404
- mimeType = "application/json";
405
- } else if (format === "xml") {
406
- content = objectToXML(fields);
407
- filename = `extraction_${new Date().toISOString().split('T')[0]}.xml`;
408
- mimeType = "application/xml";
409
- } else if (format === "docx") {
410
- // For DOCX, create a Word-compatible HTML document that preserves layout
411
- // Extract text and convert to HTML (same as text viewer)
412
- const textContent = extractTextFromFields(fields);
413
- const htmlContent = renderMarkdownToHTML(textContent);
414
-
415
- // Create a Word-compatible HTML document with proper MIME type
416
- // Word can open HTML files with .docx extension if we use the right MIME type
417
- const wordHTML = `<!DOCTYPE html>
418
- <html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
419
- <head>
420
- <meta charset="UTF-8">
421
- <meta name="ProgId" content="Word.Document">
422
- <meta name="Generator" content="Microsoft Word">
423
- <meta name="Originator" content="Microsoft Word">
424
- <!--[if gte mso 9]><xml>
425
- <w:WordDocument>
426
- <w:View>Print</w:View>
427
- <w:Zoom>100</w:Zoom>
428
- <w:DoNotOptimizeForBrowser/>
429
- </w:WordDocument>
430
- </xml><![endif]-->
431
- <title>Document Extraction</title>
432
- <style>
433
- @page {
434
- size: 8.5in 11in;
435
- margin: 1in;
436
- }
437
- body {
438
- font-family: 'Calibri', 'Arial', sans-serif;
439
- font-size: 11pt;
440
- line-height: 1.6;
441
- margin: 0;
442
- color: #333;
443
- }
444
- h1 {
445
- font-size: 18pt;
446
- font-weight: bold;
447
- color: #0f172a;
448
- margin-top: 24pt;
449
- margin-bottom: 12pt;
450
- page-break-after: avoid;
451
- }
452
- h2 {
453
- font-size: 16pt;
454
- font-weight: 600;
455
- color: #0f172a;
456
- margin-top: 20pt;
457
- margin-bottom: 10pt;
458
- page-break-after: avoid;
459
- }
460
- h3 {
461
- font-size: 14pt;
462
- font-weight: 600;
463
- color: #1e293b;
464
- margin-top: 16pt;
465
- margin-bottom: 8pt;
466
- page-break-after: avoid;
467
- }
468
- p {
469
- margin-top: 6pt;
470
- margin-bottom: 6pt;
471
- }
472
- table {
473
- width: 100%;
474
- border-collapse: collapse;
475
- margin: 12pt 0;
476
- font-size: 10pt;
477
- page-break-inside: avoid;
478
- }
479
- table th {
480
- background-color: #f8fafc;
481
- border: 1pt solid #cbd5e1;
482
- padding: 6pt;
483
- text-align: left;
484
- font-weight: 600;
485
- color: #0f172a;
486
- }
487
- table td {
488
- border: 1pt solid #cbd5e1;
489
- padding: 6pt;
490
- color: #334155;
491
- }
492
- table tr:nth-child(even) {
493
- background-color: #f8fafc;
494
- }
495
- sup {
496
- font-size: 0.75em;
497
- vertical-align: super;
498
- line-height: 0;
499
- }
500
- sub {
501
- font-size: 0.75em;
502
- vertical-align: sub;
503
- line-height: 0;
504
- }
505
- strong {
506
- font-weight: 600;
507
- }
508
- em {
509
- font-style: italic;
510
- }
511
- a {
512
- color: #4f46e5;
513
- text-decoration: underline;
514
- }
515
- </style>
516
- </head>
517
- <body>
518
- ${htmlContent}
519
- </body>
520
- </html>`;
521
-
522
- content = wordHTML;
523
- filename = `extraction_${new Date().toISOString().split('T')[0]}.doc`;
524
- mimeType = "application/msword";
525
- }
526
-
527
- // Create blob and download
528
- const blob = new Blob([content], { type: mimeType });
529
- const url = URL.createObjectURL(blob);
530
- const link = document.createElement("a");
531
- link.href = url;
532
- link.download = filename;
533
- document.body.appendChild(link);
534
- link.click();
535
- document.body.removeChild(link);
536
- URL.revokeObjectURL(url);
537
-
538
- setDownloading(null);
539
- } catch (error) {
540
- console.error("Download error:", error);
541
- setDownloading(null);
542
- }
543
- };
544
-
545
- const handleCopyLink = async () => {
546
- if (!extractionResult?.id) return;
547
-
548
- setIsGeneratingLink(true);
549
- setIsShareLinkModalOpen(true);
550
- setShareLink("");
551
-
552
- try {
553
- const result = await createShareLink(extractionResult.id);
554
- if (result.success && result.share_link) {
555
- setShareLink(result.share_link);
556
- } else {
557
- throw new Error("Failed to generate share link");
558
- }
559
- } catch (err) {
560
- console.error("Failed to create share link:", err);
561
- setShareLink("");
562
- // Still show modal but with error state
563
- } finally {
564
- setIsGeneratingLink(false);
565
- }
566
- };
567
-
568
- const handleShare = async (extractionId, recipientEmail) => {
569
- await shareExtraction(extractionId, recipientEmail);
570
- };
571
-
572
- if (!isComplete) return null;
573
-
574
- return (
575
- <motion.div
576
- initial={{ opacity: 0, y: 20 }}
577
- animate={{ opacity: 1, y: 0 }}
578
- className="flex items-center gap-3"
579
- >
580
- {/* Export Options Dropdown */}
581
- <DropdownMenu>
582
- <DropdownMenuTrigger asChild>
583
- <Button
584
- variant="ghost"
585
- className="h-11 w-11 rounded-xl hover:bg-slate-100"
586
- disabled={downloading !== null}
587
- >
588
- {downloading ? (
589
- <motion.div
590
- animate={{ rotate: 360 }}
591
- transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
592
- >
593
- <Download className="h-4 w-4" />
594
- </motion.div>
595
- ) : (
596
- <Share2 className="h-4 w-4" />
597
- )}
598
- </Button>
599
- </DropdownMenuTrigger>
600
- <DropdownMenuContent align="end" className="w-56 rounded-xl p-2">
601
- <DropdownMenuItem
602
- className="rounded-lg cursor-pointer"
603
- onClick={() => setIsShareModalOpen(true)}
604
- >
605
- <Mail className="h-4 w-4 mr-2 text-indigo-600" />
606
- Share output
607
- </DropdownMenuItem>
608
- <DropdownMenuItem
609
- className="rounded-lg cursor-pointer"
610
- onClick={handleCopyLink}
611
- >
612
- <Link2 className="h-4 w-4 mr-2 text-indigo-600" />
613
- Copy share link
614
- </DropdownMenuItem>
615
- <DropdownMenuSeparator />
616
- <DropdownMenuItem
617
- className="rounded-lg cursor-pointer"
618
- onClick={() => handleDownload("docx")}
619
- disabled={downloading === "docx"}
620
- >
621
- {downloading === "docx" ? (
622
- <motion.div
623
- animate={{ rotate: 360 }}
624
- transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
625
- className="h-4 w-4 mr-2"
626
- >
627
- <Download className="h-4 w-4" />
628
- </motion.div>
629
- ) : (
630
- <FileText className="h-4 w-4 mr-2 text-blue-600" />
631
- )}
632
- Download Docx
633
- </DropdownMenuItem>
634
- <DropdownMenuItem
635
- className="rounded-lg cursor-pointer"
636
- onClick={() => handleDownload("json")}
637
- disabled={downloading === "json"}
638
- >
639
- {downloading === "json" ? (
640
- <motion.div
641
- animate={{ rotate: 360 }}
642
- transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
643
- className="h-4 w-4 mr-2"
644
- >
645
- <Download className="h-4 w-4" />
646
- </motion.div>
647
- ) : (
648
- <Braces className="h-4 w-4 mr-2 text-indigo-600" />
649
- )}
650
- Download JSON
651
- </DropdownMenuItem>
652
- <DropdownMenuItem
653
- className="rounded-lg cursor-pointer"
654
- onClick={() => handleDownload("xml")}
655
- disabled={downloading === "xml"}
656
- >
657
- {downloading === "xml" ? (
658
- <motion.div
659
- animate={{ rotate: 360 }}
660
- transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
661
- className="h-4 w-4 mr-2"
662
- >
663
- <Download className="h-4 w-4" />
664
- </motion.div>
665
- ) : (
666
- <FileCode2 className="h-4 w-4 mr-2 text-slate-600" />
667
- )}
668
- Download XML
669
- </DropdownMenuItem>
670
- </DropdownMenuContent>
671
- </DropdownMenu>
672
-
673
- {/* Share Modal */}
674
- <ShareModal
675
- isOpen={isShareModalOpen}
676
- onClose={() => setIsShareModalOpen(false)}
677
- onShare={handleShare}
678
- extractionId={extractionResult?.id}
679
- />
680
-
681
- {/* Share Link Modal */}
682
- <ShareLinkModal
683
- isOpen={isShareLinkModalOpen}
684
- onClose={() => {
685
- setIsShareLinkModalOpen(false);
686
- setShareLink("");
687
- }}
688
- shareLink={shareLink}
689
- isLoading={isGeneratingLink}
690
- />
691
- </motion.div>
692
- );
693
- }
694
- =======
695
  import React, { useState } from "react";
696
  import { motion, AnimatePresence } from "framer-motion";
697
  import {
@@ -1384,4 +690,694 @@ ${htmlContent}
1384
  </motion.div>
1385
  );
1386
  }
1387
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState } from "react";
2
  import { motion, AnimatePresence } from "framer-motion";
3
  import {
 
690
  </motion.div>
691
  );
692
  }
693
+ import { motion, AnimatePresence } from "framer-motion";
694
+ import {
695
+ Download,
696
+ Braces,
697
+ FileCode2,
698
+ Check,
699
+ Share2,
700
+ FileText,
701
+ Link2,
702
+ Mail,
703
+ } from "lucide-react";
704
+ import { Button } from "@/components/ui/button";
705
+ import {
706
+ DropdownMenu,
707
+ DropdownMenuContent,
708
+ DropdownMenuItem,
709
+ DropdownMenuSeparator,
710
+ DropdownMenuTrigger,
711
+ } from "@/components/ui/dropdown-menu";
712
+ import { cn } from "@/lib/utils";
713
+ import ShareModal from "@/components/ShareModal";
714
+ import ShareLinkModal from "@/components/ShareLinkModal";
715
+ import { shareExtraction, createShareLink } from "@/services/api";
716
+
717
+ // Helper functions from ExtractionOutput
718
+ function prepareFieldsForOutput(fields, format = "json") {
719
+ if (!fields || typeof fields !== "object") {
720
+ return fields;
721
+ }
722
+
723
+ const output = { ...fields };
724
+
725
+ // Extract Fields from root level if it exists
726
+ const rootFields = output.Fields;
727
+ // Remove Fields from output temporarily (will be added back at top)
728
+ delete output.Fields;
729
+
730
+ // Remove full_text from top-level if pages array exists (to avoid duplication)
731
+ if (output.pages && Array.isArray(output.pages) && output.pages.length > 0) {
732
+ delete output.full_text;
733
+
734
+ // Clean up each page: remove full_text from page.fields (it duplicates page.text)
735
+ output.pages = output.pages.map(page => {
736
+ const cleanedPage = { ...page };
737
+ if (cleanedPage.fields && typeof cleanedPage.fields === "object") {
738
+ const cleanedFields = { ...cleanedPage.fields };
739
+ // Remove full_text from page fields (duplicates page.text)
740
+ delete cleanedFields.full_text;
741
+ cleanedPage.fields = cleanedFields;
742
+ }
743
+ return cleanedPage;
744
+ });
745
+ }
746
+
747
+ // For JSON and XML: restructure pages into separate top-level fields (page_1, page_2, etc.)
748
+ if ((format === "json" || format === "xml") && output.pages && Array.isArray(output.pages)) {
749
+ // Get top-level field keys (these are merged from all pages - avoid duplicating in page fields)
750
+ const topLevelKeys = new Set(Object.keys(output).filter(k => k !== "pages" && k !== "full_text" && k !== "Fields"));
751
+
752
+ output.pages.forEach((page, idx) => {
753
+ const pageNum = page.page_number || idx + 1;
754
+ const pageFields = page.fields || {};
755
+
756
+ // Remove duplicate fields from page.fields:
757
+ // 1. Remove full_text (duplicates page.text)
758
+ // 2. Remove fields that match top-level fields (already shown at root)
759
+ const cleanedPageFields = {};
760
+ for (const [key, value] of Object.entries(pageFields)) {
761
+ // Skip full_text and fields that match top-level exactly
762
+ if (key !== "full_text" && (!topLevelKeys.has(key) || (value !== output[key]))) {
763
+ cleanedPageFields[key] = value;
764
+ }
765
+ }
766
+
767
+ const pageObj = {
768
+ text: page.text || "",
769
+ confidence: page.confidence || 0,
770
+ doc_type: page.doc_type || "other"
771
+ };
772
+
773
+ // Add table and footer_notes if they exist
774
+ if (page.table && Array.isArray(page.table) && page.table.length > 0) {
775
+ pageObj.table = page.table;
776
+ }
777
+ if (page.footer_notes && Array.isArray(page.footer_notes) && page.footer_notes.length > 0) {
778
+ pageObj.footer_notes = page.footer_notes;
779
+ }
780
+
781
+ // Only add fields if there are unique page-specific fields
782
+ if (Object.keys(cleanedPageFields).length > 0) {
783
+ pageObj.fields = cleanedPageFields;
784
+ }
785
+
786
+ output[`page_${pageNum}`] = pageObj;
787
+ });
788
+ // Remove pages array - we now have page_1, page_2, etc. as separate fields
789
+ delete output.pages;
790
+ }
791
+
792
+ // Handle page_X structure (from backend) - remove Fields from page objects if they exist
793
+ if (output && typeof output === "object") {
794
+ const pageKeys = Object.keys(output).filter(k => k.startsWith("page_"));
795
+ for (const pageKey of pageKeys) {
796
+ const pageData = output[pageKey];
797
+ if (pageData && typeof pageData === "object") {
798
+ // Remove Fields from page objects (it's now at root level)
799
+ delete pageData.Fields;
800
+ delete pageData.metadata;
801
+ }
802
+ }
803
+ }
804
+
805
+ // Rebuild output with Fields at the top (only if it exists and is not empty)
806
+ const finalOutput = {};
807
+ if (rootFields && typeof rootFields === "object" && Object.keys(rootFields).length > 0) {
808
+ finalOutput.Fields = rootFields;
809
+ }
810
+
811
+ // Add all other keys
812
+ Object.keys(output).forEach(key => {
813
+ finalOutput[key] = output[key];
814
+ });
815
+
816
+ return finalOutput;
817
+ }
818
+
819
+ function escapeXML(str) {
820
+ return str
821
+ .replace(/&/g, "&amp;")
822
+ .replace(/</g, "&lt;")
823
+ .replace(/>/g, "&gt;")
824
+ .replace(/"/g, "&quot;")
825
+ .replace(/'/g, "&apos;");
826
+ }
827
+
828
+ function objectToXML(obj, rootName = "extraction") {
829
+ // Prepare fields - remove full_text if pages exist
830
+ const preparedObj = prepareFieldsForOutput(obj, "xml");
831
+
832
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}>\n`;
833
+
834
+ const convert = (obj, indent = " ") => {
835
+ for (const [key, value] of Object.entries(obj)) {
836
+ if (value === null || value === undefined) continue;
837
+
838
+ // Skip full_text if pages exist (already handled in prepareFieldsForOutput)
839
+ if (key === "full_text" && obj.pages && Array.isArray(obj.pages) && obj.pages.length > 0) {
840
+ continue;
841
+ }
842
+
843
+ if (Array.isArray(value)) {
844
+ value.forEach((item) => {
845
+ xml += `${indent}<${key}>\n`;
846
+ if (typeof item === "object") {
847
+ convert(item, indent + " ");
848
+ } else {
849
+ xml += `${indent} ${escapeXML(String(item))}\n`;
850
+ }
851
+ xml += `${indent}</${key}>\n`;
852
+ });
853
+ } else if (typeof value === "object") {
854
+ xml += `${indent}<${key}>\n`;
855
+ convert(value, indent + " ");
856
+ xml += `${indent}</${key}>\n`;
857
+ } else {
858
+ xml += `${indent}<${key}>${escapeXML(String(value))}</${key}>\n`;
859
+ }
860
+ }
861
+ };
862
+
863
+ convert(preparedObj);
864
+ xml += `</${rootName}>`;
865
+ return xml;
866
+ }
867
+
868
+ export default function ExportButtons({ isComplete, extractionResult }) {
869
+ const [downloading, setDownloading] = useState(null);
870
+ const [copied, setCopied] = useState(false);
871
+ const [isShareModalOpen, setIsShareModalOpen] = useState(false);
872
+ const [isShareLinkModalOpen, setIsShareLinkModalOpen] = useState(false);
873
+ const [shareLink, setShareLink] = useState("");
874
+ const [isGeneratingLink, setIsGeneratingLink] = useState(false);
875
+
876
+ // Helper function to extract text from fields (same as in ExtractionOutput)
877
+ const extractTextFromFields = (fields) => {
878
+ if (!fields || typeof fields !== "object") {
879
+ return "";
880
+ }
881
+
882
+ // Check for page_X structure first (preferred format)
883
+ const pageKeys = Object.keys(fields).filter(key => key.startsWith("page_"));
884
+ if (pageKeys.length > 0) {
885
+ // Get text from first page (or combine all pages)
886
+ const pageTexts = pageKeys.map(key => {
887
+ const page = fields[key];
888
+ if (page && page.text) {
889
+ return page.text;
890
+ }
891
+ return "";
892
+ }).filter(text => text);
893
+
894
+ if (pageTexts.length > 0) {
895
+ return pageTexts.join("\n\n");
896
+ }
897
+ }
898
+
899
+ // Fallback to full_text
900
+ if (fields.full_text) {
901
+ return fields.full_text;
902
+ }
903
+
904
+ return "";
905
+ };
906
+
907
+ // Helper function to escape HTML
908
+ const escapeHtml = (text) => {
909
+ if (!text) return '';
910
+ const div = document.createElement('div');
911
+ div.textContent = text;
912
+ return div.innerHTML;
913
+ };
914
+
915
+ // Helper function to convert pipe-separated tables to HTML tables
916
+ const convertPipeTablesToHTML = (text) => {
917
+ if (!text) return text;
918
+
919
+ const lines = text.split('\n');
920
+ const result = [];
921
+ let i = 0;
922
+
923
+ while (i < lines.length) {
924
+ const line = lines[i];
925
+
926
+ // Check if this line looks like a table row (has multiple pipes)
927
+ if (line.includes('|') && line.split('|').length >= 3) {
928
+ // Check if it's a separator line (only |, -, :, spaces)
929
+ const isSeparator = /^[\s|\-:]+$/.test(line.trim());
930
+
931
+ if (!isSeparator) {
932
+ // Start of a table - collect all table rows
933
+ const tableRows = [];
934
+ let j = i;
935
+
936
+ // Collect header row
937
+ const headerLine = lines[j];
938
+ const headerCells = headerLine.split('|').map(cell => cell.trim()).filter(cell => cell || cell === '');
939
+ // Remove empty cells at start/end
940
+ if (headerCells.length > 0 && !headerCells[0]) headerCells.shift();
941
+ if (headerCells.length > 0 && !headerCells[headerCells.length - 1]) headerCells.pop();
942
+
943
+ if (headerCells.length >= 2) {
944
+ tableRows.push(headerCells);
945
+ j++;
946
+
947
+ // Skip separator line if present
948
+ if (j < lines.length && /^[\s|\-:]+$/.test(lines[j].trim())) {
949
+ j++;
950
+ }
951
+
952
+ // Collect data rows
953
+ while (j < lines.length) {
954
+ const rowLine = lines[j];
955
+ if (!rowLine.trim()) break; // Empty line ends table
956
+
957
+ // Check if it's still a table row
958
+ if (rowLine.includes('|') && rowLine.split('|').length >= 2) {
959
+ const isRowSeparator = /^[\s|\-:]+$/.test(rowLine.trim());
960
+ if (!isRowSeparator) {
961
+ const rowCells = rowLine.split('|').map(cell => cell.trim());
962
+ // Remove empty cells at start/end
963
+ if (rowCells.length > 0 && !rowCells[0]) rowCells.shift();
964
+ if (rowCells.length > 0 && !rowCells[rowCells.length - 1]) rowCells.pop();
965
+ tableRows.push(rowCells);
966
+ j++;
967
+ } else {
968
+ j++;
969
+ }
970
+ } else {
971
+ break; // Not a table row anymore
972
+ }
973
+ }
974
+
975
+ // Convert to HTML table
976
+ if (tableRows.length > 0) {
977
+ let htmlTable = '<table class="border-collapse border border-gray-300 w-full my-4">\n<thead>\n<tr>';
978
+
979
+ // Header row
980
+ tableRows[0].forEach(cell => {
981
+ htmlTable += `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${escapeHtml(cell)}</th>`;
982
+ });
983
+ htmlTable += '</tr>\n</thead>\n<tbody>\n';
984
+
985
+ // Data rows
986
+ for (let rowIdx = 1; rowIdx < tableRows.length; rowIdx++) {
987
+ htmlTable += '<tr>';
988
+ tableRows[rowIdx].forEach((cell, colIdx) => {
989
+ // Use header cell count to ensure alignment
990
+ const cellContent = cell || '';
991
+ htmlTable += `<td class="border border-gray-300 px-4 py-2">${escapeHtml(cellContent)}</td>`;
992
+ });
993
+ htmlTable += '</tr>\n';
994
+ }
995
+
996
+ htmlTable += '</tbody>\n</table>';
997
+ result.push(htmlTable);
998
+ i = j;
999
+ continue;
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ // Not a table row, add as-is
1006
+ result.push(line);
1007
+ i++;
1008
+ }
1009
+
1010
+ return result.join('\n');
1011
+ };
1012
+
1013
+ // Helper function to render markdown to HTML (same as in ExtractionOutput)
1014
+ const renderMarkdownToHTML = (text) => {
1015
+ if (!text) return "";
1016
+
1017
+ let html = text;
1018
+
1019
+ // FIRST: Convert pipe-separated tables to HTML tables
1020
+ html = convertPipeTablesToHTML(html);
1021
+
1022
+ // Convert LaTeX-style superscripts/subscripts FIRST
1023
+ html = html.replace(/\$\s*\^\s*\{([^}]+)\}\s*\$/g, '<sup>$1</sup>');
1024
+ html = html.replace(/\$\s*\^\s*([^\s$<>]+)\s*\$/g, '<sup>$1</sup>');
1025
+ html = html.replace(/\$\s*_\s*\{([^}]+)\}\s*\$/g, '<sub>$1</sub>');
1026
+ html = html.replace(/\$\s*_\s*([^\s$<>]+)\s*\$/g, '<sub>$1</sub>');
1027
+
1028
+ // Protect HTML table blocks
1029
+ const htmlBlocks = [];
1030
+ let htmlBlockIndex = 0;
1031
+
1032
+ html = html.replace(/<table[\s\S]*?<\/table>/gi, (match) => {
1033
+ const placeholder = `__HTML_BLOCK_${htmlBlockIndex}__`;
1034
+ htmlBlocks[htmlBlockIndex] = match;
1035
+ htmlBlockIndex++;
1036
+ return placeholder;
1037
+ });
1038
+
1039
+ // Convert markdown headers
1040
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
1041
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
1042
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
1043
+
1044
+ // Convert markdown bold/italic
1045
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1046
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
1047
+
1048
+ // Convert markdown links
1049
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
1050
+
1051
+ // Process line breaks
1052
+ const parts = html.split(/(__HTML_BLOCK_\d+__)/);
1053
+ const processedParts = parts.map((part) => {
1054
+ if (part.match(/^__HTML_BLOCK_\d+__$/)) {
1055
+ const blockIndex = parseInt(part.match(/\d+/)[0]);
1056
+ return htmlBlocks[blockIndex];
1057
+ } else {
1058
+ let processed = part;
1059
+ processed = processed.replace(/\n\n+/g, '</p><p>');
1060
+ processed = processed.replace(/([^\n>])\n([^\n<])/g, '$1<br>$2');
1061
+ if (processed.trim() && !processed.trim().startsWith('<')) {
1062
+ processed = '<p>' + processed + '</p>';
1063
+ }
1064
+ return processed;
1065
+ }
1066
+ });
1067
+
1068
+ html = processedParts.join('');
1069
+ html = html.replace(/<p><\/p>/g, '');
1070
+ html = html.replace(/<p>\s*<br>\s*<\/p>/g, '');
1071
+ html = html.replace(/<p>\s*<\/p>/g, '');
1072
+
1073
+ return html;
1074
+ };
1075
+
1076
+ const handleDownload = async (format) => {
1077
+ if (!extractionResult || !extractionResult.fields) {
1078
+ console.error("No extraction data available");
1079
+ return;
1080
+ }
1081
+
1082
+ setDownloading(format);
1083
+
1084
+ try {
1085
+ const fields = extractionResult.fields;
1086
+ let content = "";
1087
+ let filename = "";
1088
+ let mimeType = "";
1089
+
1090
+ if (format === "json") {
1091
+ const preparedFields = prepareFieldsForOutput(fields, "json");
1092
+ content = JSON.stringify(preparedFields, null, 2);
1093
+ filename = `extraction_${new Date().toISOString().split('T')[0]}.json`;
1094
+ mimeType = "application/json";
1095
+ } else if (format === "xml") {
1096
+ content = objectToXML(fields);
1097
+ filename = `extraction_${new Date().toISOString().split('T')[0]}.xml`;
1098
+ mimeType = "application/xml";
1099
+ } else if (format === "docx") {
1100
+ // For DOCX, create a Word-compatible HTML document that preserves layout
1101
+ // Extract text and convert to HTML (same as text viewer)
1102
+ const textContent = extractTextFromFields(fields);
1103
+ const htmlContent = renderMarkdownToHTML(textContent);
1104
+
1105
+ // Create a Word-compatible HTML document with proper MIME type
1106
+ // Word can open HTML files with .docx extension if we use the right MIME type
1107
+ const wordHTML = `<!DOCTYPE html>
1108
+ <html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
1109
+ <head>
1110
+ <meta charset="UTF-8">
1111
+ <meta name="ProgId" content="Word.Document">
1112
+ <meta name="Generator" content="Microsoft Word">
1113
+ <meta name="Originator" content="Microsoft Word">
1114
+ <!--[if gte mso 9]><xml>
1115
+ <w:WordDocument>
1116
+ <w:View>Print</w:View>
1117
+ <w:Zoom>100</w:Zoom>
1118
+ <w:DoNotOptimizeForBrowser/>
1119
+ </w:WordDocument>
1120
+ </xml><![endif]-->
1121
+ <title>Document Extraction</title>
1122
+ <style>
1123
+ @page {
1124
+ size: 8.5in 11in;
1125
+ margin: 1in;
1126
+ }
1127
+ body {
1128
+ font-family: 'Calibri', 'Arial', sans-serif;
1129
+ font-size: 11pt;
1130
+ line-height: 1.6;
1131
+ margin: 0;
1132
+ color: #333;
1133
+ }
1134
+ h1 {
1135
+ font-size: 18pt;
1136
+ font-weight: bold;
1137
+ color: #0f172a;
1138
+ margin-top: 24pt;
1139
+ margin-bottom: 12pt;
1140
+ page-break-after: avoid;
1141
+ }
1142
+ h2 {
1143
+ font-size: 16pt;
1144
+ font-weight: 600;
1145
+ color: #0f172a;
1146
+ margin-top: 20pt;
1147
+ margin-bottom: 10pt;
1148
+ page-break-after: avoid;
1149
+ }
1150
+ h3 {
1151
+ font-size: 14pt;
1152
+ font-weight: 600;
1153
+ color: #1e293b;
1154
+ margin-top: 16pt;
1155
+ margin-bottom: 8pt;
1156
+ page-break-after: avoid;
1157
+ }
1158
+ p {
1159
+ margin-top: 6pt;
1160
+ margin-bottom: 6pt;
1161
+ }
1162
+ table {
1163
+ width: 100%;
1164
+ border-collapse: collapse;
1165
+ margin: 12pt 0;
1166
+ font-size: 10pt;
1167
+ page-break-inside: avoid;
1168
+ }
1169
+ table th {
1170
+ background-color: #f8fafc;
1171
+ border: 1pt solid #cbd5e1;
1172
+ padding: 6pt;
1173
+ text-align: left;
1174
+ font-weight: 600;
1175
+ color: #0f172a;
1176
+ }
1177
+ table td {
1178
+ border: 1pt solid #cbd5e1;
1179
+ padding: 6pt;
1180
+ color: #334155;
1181
+ }
1182
+ table tr:nth-child(even) {
1183
+ background-color: #f8fafc;
1184
+ }
1185
+ sup {
1186
+ font-size: 0.75em;
1187
+ vertical-align: super;
1188
+ line-height: 0;
1189
+ }
1190
+ sub {
1191
+ font-size: 0.75em;
1192
+ vertical-align: sub;
1193
+ line-height: 0;
1194
+ }
1195
+ strong {
1196
+ font-weight: 600;
1197
+ }
1198
+ em {
1199
+ font-style: italic;
1200
+ }
1201
+ a {
1202
+ color: #4f46e5;
1203
+ text-decoration: underline;
1204
+ }
1205
+ </style>
1206
+ </head>
1207
+ <body>
1208
+ ${htmlContent}
1209
+ </body>
1210
+ </html>`;
1211
+
1212
+ content = wordHTML;
1213
+ filename = `extraction_${new Date().toISOString().split('T')[0]}.doc`;
1214
+ mimeType = "application/msword";
1215
+ }
1216
+
1217
+ // Create blob and download
1218
+ const blob = new Blob([content], { type: mimeType });
1219
+ const url = URL.createObjectURL(blob);
1220
+ const link = document.createElement("a");
1221
+ link.href = url;
1222
+ link.download = filename;
1223
+ document.body.appendChild(link);
1224
+ link.click();
1225
+ document.body.removeChild(link);
1226
+ URL.revokeObjectURL(url);
1227
+
1228
+ setDownloading(null);
1229
+ } catch (error) {
1230
+ console.error("Download error:", error);
1231
+ setDownloading(null);
1232
+ }
1233
+ };
1234
+
1235
+ const handleCopyLink = async () => {
1236
+ if (!extractionResult?.id) return;
1237
+
1238
+ setIsGeneratingLink(true);
1239
+ setIsShareLinkModalOpen(true);
1240
+ setShareLink("");
1241
+
1242
+ try {
1243
+ const result = await createShareLink(extractionResult.id);
1244
+ if (result.success && result.share_link) {
1245
+ setShareLink(result.share_link);
1246
+ } else {
1247
+ throw new Error("Failed to generate share link");
1248
+ }
1249
+ } catch (err) {
1250
+ console.error("Failed to create share link:", err);
1251
+ setShareLink("");
1252
+ // Still show modal but with error state
1253
+ } finally {
1254
+ setIsGeneratingLink(false);
1255
+ }
1256
+ };
1257
+
1258
+ const handleShare = async (extractionId, recipientEmail) => {
1259
+ await shareExtraction(extractionId, recipientEmail);
1260
+ };
1261
+
1262
+ if (!isComplete) return null;
1263
+
1264
+ return (
1265
+ <motion.div
1266
+ initial={{ opacity: 0, y: 20 }}
1267
+ animate={{ opacity: 1, y: 0 }}
1268
+ className="flex items-center gap-3"
1269
+ >
1270
+ {/* Export Options Dropdown */}
1271
+ <DropdownMenu>
1272
+ <DropdownMenuTrigger asChild>
1273
+ <Button
1274
+ variant="ghost"
1275
+ className="h-11 w-11 rounded-xl hover:bg-slate-100"
1276
+ disabled={downloading !== null}
1277
+ >
1278
+ {downloading ? (
1279
+ <motion.div
1280
+ animate={{ rotate: 360 }}
1281
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
1282
+ >
1283
+ <Download className="h-4 w-4" />
1284
+ </motion.div>
1285
+ ) : (
1286
+ <Share2 className="h-4 w-4" />
1287
+ )}
1288
+ </Button>
1289
+ </DropdownMenuTrigger>
1290
+ <DropdownMenuContent align="end" className="w-56 rounded-xl p-2">
1291
+ <DropdownMenuItem
1292
+ className="rounded-lg cursor-pointer"
1293
+ onClick={() => setIsShareModalOpen(true)}
1294
+ >
1295
+ <Mail className="h-4 w-4 mr-2 text-indigo-600" />
1296
+ Share output
1297
+ </DropdownMenuItem>
1298
+ <DropdownMenuItem
1299
+ className="rounded-lg cursor-pointer"
1300
+ onClick={handleCopyLink}
1301
+ >
1302
+ <Link2 className="h-4 w-4 mr-2 text-indigo-600" />
1303
+ Copy share link
1304
+ </DropdownMenuItem>
1305
+ <DropdownMenuSeparator />
1306
+ <DropdownMenuItem
1307
+ className="rounded-lg cursor-pointer"
1308
+ onClick={() => handleDownload("docx")}
1309
+ disabled={downloading === "docx"}
1310
+ >
1311
+ {downloading === "docx" ? (
1312
+ <motion.div
1313
+ animate={{ rotate: 360 }}
1314
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
1315
+ className="h-4 w-4 mr-2"
1316
+ >
1317
+ <Download className="h-4 w-4" />
1318
+ </motion.div>
1319
+ ) : (
1320
+ <FileText className="h-4 w-4 mr-2 text-blue-600" />
1321
+ )}
1322
+ Download Docx
1323
+ </DropdownMenuItem>
1324
+ <DropdownMenuItem
1325
+ className="rounded-lg cursor-pointer"
1326
+ onClick={() => handleDownload("json")}
1327
+ disabled={downloading === "json"}
1328
+ >
1329
+ {downloading === "json" ? (
1330
+ <motion.div
1331
+ animate={{ rotate: 360 }}
1332
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
1333
+ className="h-4 w-4 mr-2"
1334
+ >
1335
+ <Download className="h-4 w-4" />
1336
+ </motion.div>
1337
+ ) : (
1338
+ <Braces className="h-4 w-4 mr-2 text-indigo-600" />
1339
+ )}
1340
+ Download JSON
1341
+ </DropdownMenuItem>
1342
+ <DropdownMenuItem
1343
+ className="rounded-lg cursor-pointer"
1344
+ onClick={() => handleDownload("xml")}
1345
+ disabled={downloading === "xml"}
1346
+ >
1347
+ {downloading === "xml" ? (
1348
+ <motion.div
1349
+ animate={{ rotate: 360 }}
1350
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
1351
+ className="h-4 w-4 mr-2"
1352
+ >
1353
+ <Download className="h-4 w-4" />
1354
+ </motion.div>
1355
+ ) : (
1356
+ <FileCode2 className="h-4 w-4 mr-2 text-slate-600" />
1357
+ )}
1358
+ Download XML
1359
+ </DropdownMenuItem>
1360
+ </DropdownMenuContent>
1361
+ </DropdownMenu>
1362
+
1363
+ {/* Share Modal */}
1364
+ <ShareModal
1365
+ isOpen={isShareModalOpen}
1366
+ onClose={() => setIsShareModalOpen(false)}
1367
+ onShare={handleShare}
1368
+ extractionId={extractionResult?.id}
1369
+ />
1370
+
1371
+ {/* Share Link Modal */}
1372
+ <ShareLinkModal
1373
+ isOpen={isShareLinkModalOpen}
1374
+ onClose={() => {
1375
+ setIsShareLinkModalOpen(false);
1376
+ setShareLink("");
1377
+ }}
1378
+ shareLink={shareLink}
1379
+ isLoading={isGeneratingLink}
1380
+ />
1381
+ </motion.div>
1382
+ );
1383
+ }
frontend/src/components/ShareLinkModal.jsx CHANGED
@@ -1,146 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState, useEffect } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import { X, Copy, Check, Loader2 } from "lucide-react";
5
- import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
7
-
8
- export default function ShareLinkModal({ isOpen, onClose, shareLink, isLoading }) {
9
- const [copied, setCopied] = useState(false);
10
-
11
- useEffect(() => {
12
- if (!isOpen) {
13
- setCopied(false);
14
- }
15
- }, [isOpen]);
16
-
17
- const handleCopy = async () => {
18
- if (!shareLink) return;
19
-
20
- try {
21
- await navigator.clipboard.writeText(shareLink);
22
- setCopied(true);
23
- setTimeout(() => setCopied(false), 2000);
24
- } catch (err) {
25
- // Fallback for older browsers
26
- const textArea = document.createElement("textarea");
27
- textArea.value = shareLink;
28
- textArea.style.position = "fixed";
29
- textArea.style.opacity = "0";
30
- document.body.appendChild(textArea);
31
- textArea.select();
32
- try {
33
- document.execCommand("copy");
34
- setCopied(true);
35
- setTimeout(() => setCopied(false), 2000);
36
- } catch (fallbackErr) {
37
- console.error("Failed to copy:", fallbackErr);
38
- }
39
- document.body.removeChild(textArea);
40
- }
41
- };
42
-
43
- if (!isOpen) return null;
44
-
45
- return (
46
- <AnimatePresence>
47
- <div className="fixed inset-0 z-50 flex items-center justify-center">
48
- {/* Backdrop */}
49
- <motion.div
50
- initial={{ opacity: 0 }}
51
- animate={{ opacity: 1 }}
52
- exit={{ opacity: 0 }}
53
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
54
- onClick={onClose}
55
- />
56
-
57
- {/* Modal */}
58
- <motion.div
59
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
60
- animate={{ opacity: 1, scale: 1, y: 0 }}
61
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
62
- className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden"
63
- onClick={(e) => e.stopPropagation()}
64
- >
65
- {/* Header */}
66
- <div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
67
- <h2 className="text-xl font-semibold text-slate-900">Copy Share Link</h2>
68
- <button
69
- onClick={onClose}
70
- disabled={isLoading}
71
- className="p-2 rounded-lg hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
72
- >
73
- <X className="h-5 w-5 text-slate-500" />
74
- </button>
75
- </div>
76
-
77
- {/* Content */}
78
- <div className="px-6 py-6">
79
- {isLoading ? (
80
- <div className="text-center py-8">
81
- <Loader2 className="h-8 w-8 mx-auto mb-4 text-indigo-600 animate-spin" />
82
- <p className="text-sm text-slate-600">Generating share link...</p>
83
- </div>
84
- ) : shareLink ? (
85
- <div className="space-y-4">
86
- <div>
87
- <label className="block text-sm font-medium text-slate-700 mb-2">
88
- Share Link
89
- </label>
90
- <div className="flex gap-2">
91
- <Input
92
- type="text"
93
- value={shareLink}
94
- readOnly
95
- className="flex-1 h-12 rounded-xl border-slate-200 bg-slate-50 text-sm font-mono"
96
- />
97
- <Button
98
- onClick={handleCopy}
99
- className="h-12 px-4 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
100
- >
101
- {copied ? (
102
- <>
103
- <Check className="h-4 w-4 mr-2" />
104
- Copied!
105
- </>
106
- ) : (
107
- <>
108
- <Copy className="h-4 w-4 mr-2" />
109
- Copy
110
- </>
111
- )}
112
- </Button>
113
- </div>
114
- </div>
115
- <p className="text-xs text-slate-500">
116
- Share this link with anyone you want to give access to this extraction. They'll need to sign in to view it.
117
- </p>
118
- </div>
119
- ) : (
120
- <div className="text-center py-8">
121
- <p className="text-sm text-slate-600">No share link available</p>
122
- </div>
123
- )}
124
-
125
- <div className="pt-4 mt-6 border-t border-slate-200">
126
- <Button
127
- type="button"
128
- variant="outline"
129
- onClick={onClose}
130
- disabled={isLoading}
131
- className="w-full h-11 rounded-xl"
132
- >
133
- Close
134
- </Button>
135
- </div>
136
- </div>
137
- </motion.div>
138
- </div>
139
- </AnimatePresence>
140
- );
141
- }
142
-
143
- =======
144
  import React, { useState, useEffect } from "react";
145
  import { motion, AnimatePresence } from "framer-motion";
146
  import { X, Copy, Check, Loader2 } from "lucide-react";
@@ -281,5 +138,142 @@ export default function ShareLinkModal({ isOpen, onClose, shareLink, isLoading }
281
  </AnimatePresence>
282
  );
283
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useEffect } from "react";
2
  import { motion, AnimatePresence } from "framer-motion";
3
  import { X, Copy, Check, Loader2 } from "lucide-react";
 
138
  </AnimatePresence>
139
  );
140
  }
141
+ import { motion, AnimatePresence } from "framer-motion";
142
+ import { X, Copy, Check, Loader2 } from "lucide-react";
143
+ import { Button } from "@/components/ui/button";
144
+ import { Input } from "@/components/ui/input";
145
+
146
+ export default function ShareLinkModal({ isOpen, onClose, shareLink, isLoading }) {
147
+ const [copied, setCopied] = useState(false);
148
+
149
+ useEffect(() => {
150
+ if (!isOpen) {
151
+ setCopied(false);
152
+ }
153
+ }, [isOpen]);
154
+
155
+ const handleCopy = async () => {
156
+ if (!shareLink) return;
157
+
158
+ try {
159
+ await navigator.clipboard.writeText(shareLink);
160
+ setCopied(true);
161
+ setTimeout(() => setCopied(false), 2000);
162
+ } catch (err) {
163
+ // Fallback for older browsers
164
+ const textArea = document.createElement("textarea");
165
+ textArea.value = shareLink;
166
+ textArea.style.position = "fixed";
167
+ textArea.style.opacity = "0";
168
+ document.body.appendChild(textArea);
169
+ textArea.select();
170
+ try {
171
+ document.execCommand("copy");
172
+ setCopied(true);
173
+ setTimeout(() => setCopied(false), 2000);
174
+ } catch (fallbackErr) {
175
+ console.error("Failed to copy:", fallbackErr);
176
+ }
177
+ document.body.removeChild(textArea);
178
+ }
179
+ };
180
+
181
+ if (!isOpen) return null;
182
+
183
+ return (
184
+ <AnimatePresence>
185
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
186
+ {/* Backdrop */}
187
+ <motion.div
188
+ initial={{ opacity: 0 }}
189
+ animate={{ opacity: 1 }}
190
+ exit={{ opacity: 0 }}
191
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
192
+ onClick={onClose}
193
+ />
194
+
195
+ {/* Modal */}
196
+ <motion.div
197
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
198
+ animate={{ opacity: 1, scale: 1, y: 0 }}
199
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
200
+ className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden"
201
+ onClick={(e) => e.stopPropagation()}
202
+ >
203
+ {/* Header */}
204
+ <div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
205
+ <h2 className="text-xl font-semibold text-slate-900">Copy Share Link</h2>
206
+ <button
207
+ onClick={onClose}
208
+ disabled={isLoading}
209
+ className="p-2 rounded-lg hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
210
+ >
211
+ <X className="h-5 w-5 text-slate-500" />
212
+ </button>
213
+ </div>
214
 
215
+ {/* Content */}
216
+ <div className="px-6 py-6">
217
+ {isLoading ? (
218
+ <div className="text-center py-8">
219
+ <Loader2 className="h-8 w-8 mx-auto mb-4 text-indigo-600 animate-spin" />
220
+ <p className="text-sm text-slate-600">Generating share link...</p>
221
+ </div>
222
+ ) : shareLink ? (
223
+ <div className="space-y-4">
224
+ <div>
225
+ <label className="block text-sm font-medium text-slate-700 mb-2">
226
+ Share Link
227
+ </label>
228
+ <div className="flex gap-2">
229
+ <Input
230
+ type="text"
231
+ value={shareLink}
232
+ readOnly
233
+ className="flex-1 h-12 rounded-xl border-slate-200 bg-slate-50 text-sm font-mono"
234
+ />
235
+ <Button
236
+ onClick={handleCopy}
237
+ className="h-12 px-4 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
238
+ >
239
+ {copied ? (
240
+ <>
241
+ <Check className="h-4 w-4 mr-2" />
242
+ Copied!
243
+ </>
244
+ ) : (
245
+ <>
246
+ <Copy className="h-4 w-4 mr-2" />
247
+ Copy
248
+ </>
249
+ )}
250
+ </Button>
251
+ </div>
252
+ </div>
253
+ <p className="text-xs text-slate-500">
254
+ Share this link with anyone you want to give access to this extraction. They'll need to sign in to view it.
255
+ </p>
256
+ </div>
257
+ ) : (
258
+ <div className="text-center py-8">
259
+ <p className="text-sm text-slate-600">No share link available</p>
260
+ </div>
261
+ )}
262
+
263
+ <div className="pt-4 mt-6 border-t border-slate-200">
264
+ <Button
265
+ type="button"
266
+ variant="outline"
267
+ onClick={onClose}
268
+ disabled={isLoading}
269
+ className="w-full h-11 rounded-xl"
270
+ >
271
+ Close
272
+ </Button>
273
+ </div>
274
+ </div>
275
+ </motion.div>
276
+ </div>
277
+ </AnimatePresence>
278
+ );
279
+ }
frontend/src/components/auth/LoginForm.jsx CHANGED
@@ -1,517 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState } from "react";
3
- import { motion } from "framer-motion";
4
- import { Button } from "@/components/ui/button";
5
- import { Input } from "@/components/ui/input";
6
- import { Separator } from "@/components/ui/separator";
7
- import {
8
- Zap,
9
- Target,
10
- Upload,
11
- CheckCircle2,
12
- ArrowRight,
13
- Mail,
14
- Sparkles,
15
- Shield,
16
- Globe,
17
- AlertCircle,
18
- Loader2,
19
- } from "lucide-react";
20
- import { useAuth } from "@/contexts/AuthContext";
21
-
22
- export default function LoginForm() {
23
- const { firebaseLogin, requestOTP, verifyOTP } = useAuth();
24
- const [email, setEmail] = useState("");
25
- const [showOtp, setShowOtp] = useState(false);
26
- const [otp, setOtp] = useState(["", "", "", "", "", ""]);
27
- const [loading, setLoading] = useState(false);
28
- const [error, setError] = useState("");
29
-
30
- // Business email validation
31
- const PERSONAL_EMAIL_DOMAINS = [
32
- "gmail.com",
33
- "yahoo.com",
34
- "hotmail.com",
35
- "outlook.com",
36
- "aol.com",
37
- "icloud.com",
38
- "mail.com",
39
- "protonmail.com",
40
- "yandex.com",
41
- "zoho.com",
42
- "gmx.com",
43
- "live.com",
44
- "msn.com",
45
- ];
46
-
47
- const isBusinessEmail = (email) => {
48
- if (!email || !email.includes("@")) return false;
49
- const domain = email.split("@")[1].toLowerCase();
50
- return !PERSONAL_EMAIL_DOMAINS.includes(domain);
51
- };
52
-
53
- const handleGoogleLogin = async () => {
54
- setLoading(true);
55
- setError("");
56
- try {
57
- await firebaseLogin();
58
- } catch (err) {
59
- setError(err.message || "Failed to sign in with Google");
60
- } finally {
61
- setLoading(false);
62
- }
63
- };
64
-
65
- const handleEmailSubmit = async (e) => {
66
- e.preventDefault();
67
- setLoading(true);
68
- setError("");
69
-
70
- if (!email) {
71
- setError("Please enter your email address");
72
- setLoading(false);
73
- return;
74
- }
75
-
76
- if (!isBusinessEmail(email)) {
77
- setError("Only business email addresses are allowed. Personal email accounts (Gmail, Yahoo, etc.) are not permitted.");
78
- setLoading(false);
79
- return;
80
- }
81
-
82
- try {
83
- await requestOTP(email);
84
- setShowOtp(true);
85
- } catch (err) {
86
- setError(err.message || "Failed to send OTP");
87
- } finally {
88
- setLoading(false);
89
- }
90
- };
91
-
92
- const handleOtpChange = (index, value) => {
93
- if (value.length <= 1 && /^\d*$/.test(value)) {
94
- const newOtp = [...otp];
95
- newOtp[index] = value;
96
- setOtp(newOtp);
97
- setError("");
98
-
99
- // Auto-focus next input
100
- if (value && index < 5) {
101
- const nextInput = document.getElementById(`otp-${index + 1}`);
102
- nextInput?.focus();
103
- }
104
- }
105
- };
106
-
107
- const handleOtpPaste = (e, startIndex = 0) => {
108
- e.preventDefault();
109
- const pastedData = e.clipboardData.getData("text");
110
- // Extract only digits from pasted content
111
- const digits = pastedData.replace(/\D/g, "").slice(0, 6);
112
-
113
- if (digits.length > 0) {
114
- const newOtp = [...otp];
115
- // Fill the OTP array with pasted digits starting from the current field
116
- for (let i = 0; i < digits.length && (startIndex + i) < 6; i++) {
117
- newOtp[startIndex + i] = digits[i];
118
- }
119
- setOtp(newOtp);
120
- setError("");
121
-
122
- // Focus on the next empty input or the last input if all are filled
123
- const nextEmptyIndex = Math.min(startIndex + digits.length, 5);
124
- const nextInput = document.getElementById(`otp-${nextEmptyIndex}`);
125
- nextInput?.focus();
126
- }
127
- };
128
-
129
- const handleOtpKeyDown = (index, e) => {
130
- if (e.key === "Backspace" && !otp[index] && index > 0) {
131
- const prevInput = document.getElementById(`otp-${index - 1}`);
132
- prevInput?.focus();
133
- }
134
- };
135
-
136
- const handleOtpVerify = async (e) => {
137
- e.preventDefault();
138
- setLoading(true);
139
- setError("");
140
-
141
- const otpString = otp.join("");
142
- if (otpString.length !== 6) {
143
- setError("Please enter a valid 6-digit OTP");
144
- setLoading(false);
145
- return;
146
- }
147
-
148
- try {
149
- await verifyOTP(email, otpString);
150
- // Success - user will be redirected by AuthContext
151
- } catch (err) {
152
- setError(err.message || "Invalid OTP. Please try again.");
153
- setOtp(["", "", "", "", "", ""]);
154
- } finally {
155
- setLoading(false);
156
- }
157
- };
158
-
159
- const features = [
160
- {
161
- icon: Zap,
162
- title: "Lightning Fast",
163
- description: "Process documents in seconds and get outputs for ERP ingestion",
164
- color: "text-amber-500",
165
- bg: "bg-amber-50",
166
- },
167
- {
168
- icon: Target,
169
- title: "100% Accuracy",
170
- description: "Industry-leading extraction with Visual Reasoning Processor",
171
- color: "text-emerald-500",
172
- bg: "bg-emerald-50",
173
- },
174
- {
175
- icon: Globe,
176
- title: "Any Format, Any Language",
177
- description: "PDF, images, scanned docs — multi-lingual support included",
178
- color: "text-blue-500",
179
- bg: "bg-blue-50",
180
- },
181
- ];
182
-
183
- const supportedFormats = [
184
- { ext: "PDF", color: "bg-red-500" },
185
- { ext: "PNG", color: "bg-blue-500" },
186
- { ext: "JPG", color: "bg-green-500" },
187
- { ext: "TIFF", color: "bg-purple-500" },
188
- ];
189
-
190
- return (
191
- <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex">
192
- {/* Left Side - Product Showcase */}
193
- <div className="hidden lg:flex lg:w-[56%] flex-col justify-between p-8 relative overflow-hidden">
194
- {/* Background Elements */}
195
- <div className="absolute top-0 right-0 w-96 h-96 bg-blue-100/40 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
196
- <div className="absolute bottom-0 left-0 w-80 h-80 bg-emerald-100/40 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
197
-
198
- {/* Logo & Brand */}
199
- <motion.div
200
- initial={{ opacity: 0, y: -20 }}
201
- animate={{ opacity: 1, y: 0 }}
202
- className="relative z-10 mb-6"
203
- >
204
- <div className="flex items-center gap-3">
205
- <div className="h-12 w-12 flex items-center justify-center flex-shrink-0">
206
- <img
207
- src="/logo.png"
208
- alt="EZOFIS AI Logo"
209
- className="h-full w-full object-contain"
210
- onError={(e) => {
211
- // Fallback: hide image if logo not found
212
- e.target.style.display = 'none';
213
- }}
214
- />
215
- </div>
216
- <div>
217
- <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1>
218
- <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p>
219
- </div>
220
- </div>
221
- </motion.div>
222
-
223
- {/* Main Content */}
224
- <motion.div
225
- initial={{ opacity: 0, y: 20 }}
226
- animate={{ opacity: 1, y: 0 }}
227
- transition={{ delay: 0.1 }}
228
- className="relative z-10 space-y-5 flex-1 flex flex-col justify-center ml-24 xl:ml-36"
229
- >
230
- <div className="space-y-3">
231
- <h2 className="text-3xl xl:text-4xl font-bold text-slate-900 leading-tight">
232
- Pure Agentic
233
- <span className="block text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600">
234
- Document Intelligence
235
- </span>
236
- </h2>
237
- <p className="text-base text-slate-600 max-w-lg leading-relaxed">
238
- Deterministic, layout-aware extraction (without LLM) using our proprietary{" "}
239
- <span className="font-semibold text-slate-800">Visual Reasoning Processor (VRP)</span>
240
- </p>
241
- </div>
242
-
243
- {/* Product Preview Card */}
244
- <motion.div
245
- initial={{ opacity: 0, scale: 0.95 }}
246
- animate={{ opacity: 1, scale: 1 }}
247
- transition={{ delay: 0.3 }}
248
- className="bg-white rounded-2xl border border-slate-200/80 shadow-xl shadow-slate-200/50 p-4 max-w-lg"
249
- >
250
- <div className="border-2 border-dashed border-slate-200 rounded-xl p-5 text-center bg-slate-50/50">
251
- <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
252
- <Upload className="w-5 h-5 text-slate-400" />
253
- </div>
254
- <p className="text-slate-700 font-medium mb-1 text-sm">Drop a document to extract data</p>
255
- <p className="text-xs text-slate-400">Invoices, purchase orders, delivery notes, receipts, and operational documents</p>
256
-
257
- <div className="flex items-center justify-center gap-2 mt-3">
258
- {supportedFormats.map((format, i) => (
259
- <span key={i} className={`${format.color} text-white text-xs font-bold px-2 py-1 rounded`}>
260
- {format.ext}
261
- </span>
262
- ))}
263
- </div>
264
- </div>
265
-
266
- <div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100">
267
- <div className="flex items-center gap-2">
268
- <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
269
- <span className="text-xs text-slate-600">Ready to extract</span>
270
- </div>
271
- <div className="flex items-center gap-1 text-emerald-600">
272
- <CheckCircle2 className="w-3.5 h-3.5" />
273
- <span className="text-xs font-semibold">99.8% Accuracy</span>
274
- </div>
275
- </div>
276
- </motion.div>
277
-
278
- {/* Features */}
279
- <div className="grid gap-3">
280
- {features.map((feature, index) => (
281
- <motion.div
282
- key={feature.title}
283
- initial={{ opacity: 0, x: -20 }}
284
- animate={{ opacity: 1, x: 0 }}
285
- transition={{ delay: 0.4 + index * 0.1 }}
286
- className="flex items-start gap-3 group"
287
- >
288
- <div
289
- className={`w-9 h-9 rounded-xl ${feature.bg} flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform`}
290
- >
291
- <feature.icon className={`w-4 h-4 ${feature.color}`} />
292
- </div>
293
- <div>
294
- <h3 className="font-semibold text-slate-900 text-sm">{feature.title}</h3>
295
- <p className="text-xs text-slate-500">{feature.description}</p>
296
- </div>
297
- </motion.div>
298
- ))}
299
- </div>
300
- </motion.div>
301
-
302
- {/* Trust Badge */}
303
- <motion.div
304
- initial={{ opacity: 0 }}
305
- animate={{ opacity: 1 }}
306
- transition={{ delay: 0.6 }}
307
- className="relative z-10 flex items-center gap-3 text-xs text-slate-500 mt-6"
308
- >
309
- <Shield className="w-4 h-4" />
310
- <span>Enterprise-grade security • SOC 2 Compliant • GDPR Ready</span>
311
- </motion.div>
312
- </div>
313
-
314
- {/* Right Side - Sign In Form */}
315
- <div className="w-full lg:w-[44%] flex items-center justify-center p-6 sm:p-10">
316
- <motion.div
317
- initial={{ opacity: 0, y: 20 }}
318
- animate={{ opacity: 1, y: 0 }}
319
- transition={{ delay: 0.2 }}
320
- className="w-full max-w-md"
321
- >
322
- {/* Mobile Logo */}
323
- <div className="lg:hidden flex items-center justify-center gap-3 mb-8">
324
- <div className="h-12 w-12 flex items-center justify-center flex-shrink-0">
325
- <img
326
- src="/logo.png"
327
- alt="EZOFIS AI Logo"
328
- className="h-full w-full object-contain"
329
- onError={(e) => {
330
- // Fallback: hide image if logo not found
331
- e.target.style.display = 'none';
332
- }}
333
- />
334
- </div>
335
- <div>
336
- <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1>
337
- <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p>
338
- </div>
339
- </div>
340
-
341
- <div className="bg-white rounded-3xl border border-slate-200/80 shadow-2xl shadow-slate-200/50 p-8 sm:p-10">
342
- <div className="text-center mb-8">
343
- <h2 className="text-2xl font-bold text-slate-900 mb-2">
344
- {showOtp ? "Enter verification code" : "Secure Access"}
345
- </h2>
346
- <p className="text-slate-500">
347
- {showOtp ? `We sent a code to ${email}` : "Access your document intelligence workspace"}
348
- </p>
349
- </div>
350
-
351
- {/* Error Message */}
352
- {error && (
353
- <motion.div
354
- initial={{ opacity: 0, y: -10 }}
355
- animate={{ opacity: 1, y: 0 }}
356
- className="mb-6 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2 text-sm text-red-700"
357
- >
358
- <AlertCircle className="h-4 w-4 flex-shrink-0 mt-0.5" />
359
- <p>{error}</p>
360
- </motion.div>
361
- )}
362
-
363
- {!showOtp ? (
364
- <>
365
- {/* Google Sign In */}
366
- <Button
367
- onClick={handleGoogleLogin}
368
- disabled={loading}
369
- variant="outline"
370
- className="w-full h-12 text-base font-medium border-slate-200 hover:bg-slate-50 hover:border-slate-300 transition-all group"
371
- >
372
- {loading ? (
373
- <Loader2 className="w-5 h-5 mr-3 animate-spin" />
374
- ) : (
375
- <svg className="w-5 h-5 mr-3" viewBox="0 0 24 24">
376
- <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
377
- <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
378
- <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
379
- <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
380
- </svg>
381
- )}
382
- Continue with Google
383
- <ArrowRight className="w-4 h-4 ml-auto opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
384
- </Button>
385
-
386
- <div className="relative my-8">
387
- <Separator />
388
- <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-4 text-sm text-slate-400">
389
- or continue with email
390
- </span>
391
- </div>
392
-
393
- {/* Email Input */}
394
- <form onSubmit={handleEmailSubmit} className="space-y-4">
395
- <div className="relative">
396
- <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
397
- <Input
398
- type="email"
399
- placeholder="name@company.com"
400
- value={email}
401
- onChange={(e) => {
402
- setEmail(e.target.value);
403
- setError("");
404
- }}
405
- className="h-12 pl-12 text-base border-slate-200 focus:border-blue-500 focus:ring-blue-500"
406
- />
407
- </div>
408
- <Button
409
- type="submit"
410
- disabled={loading}
411
- className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25 transition-all"
412
- >
413
- {loading ? (
414
- <>
415
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
416
- Sending...
417
- </>
418
- ) : (
419
- <>
420
- Continue with Email
421
- <ArrowRight className="w-4 h-4 ml-2" />
422
- </>
423
- )}
424
- </Button>
425
- </form>
426
- </>
427
- ) : (
428
- /* OTP Input */
429
- <form onSubmit={handleOtpVerify} className="space-y-6">
430
- <div className="flex justify-center gap-2">
431
- {otp.map((digit, index) => (
432
- <Input
433
- key={index}
434
- id={`otp-${index}`}
435
- type="text"
436
- inputMode="numeric"
437
- maxLength={1}
438
- value={digit}
439
- onChange={(e) => handleOtpChange(index, e.target.value)}
440
- onKeyDown={(e) => handleOtpKeyDown(index, e)}
441
- onPaste={(e) => handleOtpPaste(e, index)}
442
- className="w-12 h-14 text-center text-xl font-semibold border-slate-200 focus:border-blue-500 focus:ring-blue-500"
443
- />
444
- ))}
445
- </div>
446
-
447
- <Button
448
- type="submit"
449
- disabled={loading || otp.join("").length !== 6}
450
- className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25"
451
- >
452
- {loading ? (
453
- <>
454
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
455
- Verifying...
456
- </>
457
- ) : (
458
- <>
459
- Verify & Sign In
460
- <ArrowRight className="w-4 h-4 ml-2" />
461
- </>
462
- )}
463
- </Button>
464
-
465
- <button
466
- type="button"
467
- onClick={() => {
468
- setShowOtp(false);
469
- setOtp(["", "", "", "", "", ""]);
470
- setError("");
471
- }}
472
- className="w-full text-sm text-slate-500 hover:text-slate-700 transition-colors"
473
- >
474
- ← Back to sign in options
475
- </button>
476
- </form>
477
- )}
478
-
479
- {/* Notice */}
480
- <div className="mt-8 pt-6 border-t border-slate-100">
481
- <div className="flex items-start gap-2 text-xs text-slate-400 mb-4">
482
- <Shield className="w-4 h-4 flex-shrink-0 mt-0.5" />
483
- <span>Only business email addresses are allowed</span>
484
- </div>
485
- <p className="text-xs text-slate-400 text-center leading-relaxed">
486
- By signing in, you agree to our{" "}
487
- <a href="#" className="text-blue-600 hover:underline">
488
- Terms of Service
489
- </a>{" "}
490
- and{" "}
491
- <a href="#" className="text-blue-600 hover:underline">
492
- Privacy Policy
493
- </a>
494
- </p>
495
- </div>
496
- </div>
497
-
498
- {/* Mobile Features */}
499
- <div className="lg:hidden mt-8 space-y-4">
500
- {features.map((feature) => (
501
- <div key={feature.title} className="flex items-center gap-3 text-sm">
502
- <div className={`w-8 h-8 rounded-lg ${feature.bg} flex items-center justify-center`}>
503
- <feature.icon className={`w-4 h-4 ${feature.color}`} />
504
- </div>
505
- <span className="text-slate-600">{feature.title}</span>
506
- </div>
507
- ))}
508
- </div>
509
- </motion.div>
510
- </div>
511
- </div>
512
- );
513
- }
514
- =======
515
  import React, { useState } from "react";
516
  import { motion } from "framer-motion";
517
  import { Button } from "@/components/ui/button";
@@ -1024,4 +510,514 @@ export default function LoginForm() {
1024
  </div>
1025
  );
1026
  }
1027
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState } from "react";
2
  import { motion } from "framer-motion";
3
  import { Button } from "@/components/ui/button";
 
510
  </div>
511
  );
512
  }
513
+ import { motion } from "framer-motion";
514
+ import { Button } from "@/components/ui/button";
515
+ import { Input } from "@/components/ui/input";
516
+ import { Separator } from "@/components/ui/separator";
517
+ import {
518
+ Zap,
519
+ Target,
520
+ Upload,
521
+ CheckCircle2,
522
+ ArrowRight,
523
+ Mail,
524
+ Sparkles,
525
+ Shield,
526
+ Globe,
527
+ AlertCircle,
528
+ Loader2,
529
+ } from "lucide-react";
530
+ import { useAuth } from "@/contexts/AuthContext";
531
+
532
+ export default function LoginForm() {
533
+ const { firebaseLogin, requestOTP, verifyOTP } = useAuth();
534
+ const [email, setEmail] = useState("");
535
+ const [showOtp, setShowOtp] = useState(false);
536
+ const [otp, setOtp] = useState(["", "", "", "", "", ""]);
537
+ const [loading, setLoading] = useState(false);
538
+ const [error, setError] = useState("");
539
+
540
+ // Business email validation
541
+ const PERSONAL_EMAIL_DOMAINS = [
542
+ "gmail.com",
543
+ "yahoo.com",
544
+ "hotmail.com",
545
+ "outlook.com",
546
+ "aol.com",
547
+ "icloud.com",
548
+ "mail.com",
549
+ "protonmail.com",
550
+ "yandex.com",
551
+ "zoho.com",
552
+ "gmx.com",
553
+ "live.com",
554
+ "msn.com",
555
+ ];
556
+
557
+ const isBusinessEmail = (email) => {
558
+ if (!email || !email.includes("@")) return false;
559
+ const domain = email.split("@")[1].toLowerCase();
560
+ return !PERSONAL_EMAIL_DOMAINS.includes(domain);
561
+ };
562
+
563
+ const handleGoogleLogin = async () => {
564
+ setLoading(true);
565
+ setError("");
566
+ try {
567
+ await firebaseLogin();
568
+ } catch (err) {
569
+ setError(err.message || "Failed to sign in with Google");
570
+ } finally {
571
+ setLoading(false);
572
+ }
573
+ };
574
+
575
+ const handleEmailSubmit = async (e) => {
576
+ e.preventDefault();
577
+ setLoading(true);
578
+ setError("");
579
+
580
+ if (!email) {
581
+ setError("Please enter your email address");
582
+ setLoading(false);
583
+ return;
584
+ }
585
+
586
+ if (!isBusinessEmail(email)) {
587
+ setError("Only business email addresses are allowed. Personal email accounts (Gmail, Yahoo, etc.) are not permitted.");
588
+ setLoading(false);
589
+ return;
590
+ }
591
+
592
+ try {
593
+ await requestOTP(email);
594
+ setShowOtp(true);
595
+ } catch (err) {
596
+ setError(err.message || "Failed to send OTP");
597
+ } finally {
598
+ setLoading(false);
599
+ }
600
+ };
601
+
602
+ const handleOtpChange = (index, value) => {
603
+ if (value.length <= 1 && /^\d*$/.test(value)) {
604
+ const newOtp = [...otp];
605
+ newOtp[index] = value;
606
+ setOtp(newOtp);
607
+ setError("");
608
+
609
+ // Auto-focus next input
610
+ if (value && index < 5) {
611
+ const nextInput = document.getElementById(`otp-${index + 1}`);
612
+ nextInput?.focus();
613
+ }
614
+ }
615
+ };
616
+
617
+ const handleOtpPaste = (e, startIndex = 0) => {
618
+ e.preventDefault();
619
+ const pastedData = e.clipboardData.getData("text");
620
+ // Extract only digits from pasted content
621
+ const digits = pastedData.replace(/\D/g, "").slice(0, 6);
622
+
623
+ if (digits.length > 0) {
624
+ const newOtp = [...otp];
625
+ // Fill the OTP array with pasted digits starting from the current field
626
+ for (let i = 0; i < digits.length && (startIndex + i) < 6; i++) {
627
+ newOtp[startIndex + i] = digits[i];
628
+ }
629
+ setOtp(newOtp);
630
+ setError("");
631
+
632
+ // Focus on the next empty input or the last input if all are filled
633
+ const nextEmptyIndex = Math.min(startIndex + digits.length, 5);
634
+ const nextInput = document.getElementById(`otp-${nextEmptyIndex}`);
635
+ nextInput?.focus();
636
+ }
637
+ };
638
+
639
+ const handleOtpKeyDown = (index, e) => {
640
+ if (e.key === "Backspace" && !otp[index] && index > 0) {
641
+ const prevInput = document.getElementById(`otp-${index - 1}`);
642
+ prevInput?.focus();
643
+ }
644
+ };
645
+
646
+ const handleOtpVerify = async (e) => {
647
+ e.preventDefault();
648
+ setLoading(true);
649
+ setError("");
650
+
651
+ const otpString = otp.join("");
652
+ if (otpString.length !== 6) {
653
+ setError("Please enter a valid 6-digit OTP");
654
+ setLoading(false);
655
+ return;
656
+ }
657
+
658
+ try {
659
+ await verifyOTP(email, otpString);
660
+ // Success - user will be redirected by AuthContext
661
+ } catch (err) {
662
+ setError(err.message || "Invalid OTP. Please try again.");
663
+ setOtp(["", "", "", "", "", ""]);
664
+ } finally {
665
+ setLoading(false);
666
+ }
667
+ };
668
+
669
+ const features = [
670
+ {
671
+ icon: Zap,
672
+ title: "Lightning Fast",
673
+ description: "Process documents in seconds and get outputs for ERP ingestion",
674
+ color: "text-amber-500",
675
+ bg: "bg-amber-50",
676
+ },
677
+ {
678
+ icon: Target,
679
+ title: "100% Accuracy",
680
+ description: "Industry-leading extraction with Visual Reasoning Processor",
681
+ color: "text-emerald-500",
682
+ bg: "bg-emerald-50",
683
+ },
684
+ {
685
+ icon: Globe,
686
+ title: "Any Format, Any Language",
687
+ description: "PDF, images, scanned docs — multi-lingual support included",
688
+ color: "text-blue-500",
689
+ bg: "bg-blue-50",
690
+ },
691
+ ];
692
+
693
+ const supportedFormats = [
694
+ { ext: "PDF", color: "bg-red-500" },
695
+ { ext: "PNG", color: "bg-blue-500" },
696
+ { ext: "JPG", color: "bg-green-500" },
697
+ { ext: "TIFF", color: "bg-purple-500" },
698
+ ];
699
+
700
+ return (
701
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex">
702
+ {/* Left Side - Product Showcase */}
703
+ <div className="hidden lg:flex lg:w-[56%] flex-col justify-between p-8 relative overflow-hidden">
704
+ {/* Background Elements */}
705
+ <div className="absolute top-0 right-0 w-96 h-96 bg-blue-100/40 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
706
+ <div className="absolute bottom-0 left-0 w-80 h-80 bg-emerald-100/40 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
707
+
708
+ {/* Logo & Brand */}
709
+ <motion.div
710
+ initial={{ opacity: 0, y: -20 }}
711
+ animate={{ opacity: 1, y: 0 }}
712
+ className="relative z-10 mb-6"
713
+ >
714
+ <div className="flex items-center gap-3">
715
+ <div className="h-12 w-12 flex items-center justify-center flex-shrink-0">
716
+ <img
717
+ src="/logo.png"
718
+ alt="EZOFIS AI Logo"
719
+ className="h-full w-full object-contain"
720
+ onError={(e) => {
721
+ // Fallback: hide image if logo not found
722
+ e.target.style.display = 'none';
723
+ }}
724
+ />
725
+ </div>
726
+ <div>
727
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1>
728
+ <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p>
729
+ </div>
730
+ </div>
731
+ </motion.div>
732
+
733
+ {/* Main Content */}
734
+ <motion.div
735
+ initial={{ opacity: 0, y: 20 }}
736
+ animate={{ opacity: 1, y: 0 }}
737
+ transition={{ delay: 0.1 }}
738
+ className="relative z-10 space-y-5 flex-1 flex flex-col justify-center ml-24 xl:ml-36"
739
+ >
740
+ <div className="space-y-3">
741
+ <h2 className="text-3xl xl:text-4xl font-bold text-slate-900 leading-tight">
742
+ Pure Agentic
743
+ <span className="block text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600">
744
+ Document Intelligence
745
+ </span>
746
+ </h2>
747
+ <p className="text-base text-slate-600 max-w-lg leading-relaxed">
748
+ Deterministic, layout-aware extraction (without LLM) using our proprietary{" "}
749
+ <span className="font-semibold text-slate-800">Visual Reasoning Processor (VRP)</span>
750
+ </p>
751
+ </div>
752
+
753
+ {/* Product Preview Card */}
754
+ <motion.div
755
+ initial={{ opacity: 0, scale: 0.95 }}
756
+ animate={{ opacity: 1, scale: 1 }}
757
+ transition={{ delay: 0.3 }}
758
+ className="bg-white rounded-2xl border border-slate-200/80 shadow-xl shadow-slate-200/50 p-4 max-w-lg"
759
+ >
760
+ <div className="border-2 border-dashed border-slate-200 rounded-xl p-5 text-center bg-slate-50/50">
761
+ <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
762
+ <Upload className="w-5 h-5 text-slate-400" />
763
+ </div>
764
+ <p className="text-slate-700 font-medium mb-1 text-sm">Drop a document to extract data</p>
765
+ <p className="text-xs text-slate-400">Invoices, purchase orders, delivery notes, receipts, and operational documents</p>
766
+
767
+ <div className="flex items-center justify-center gap-2 mt-3">
768
+ {supportedFormats.map((format, i) => (
769
+ <span key={i} className={`${format.color} text-white text-xs font-bold px-2 py-1 rounded`}>
770
+ {format.ext}
771
+ </span>
772
+ ))}
773
+ </div>
774
+ </div>
775
+
776
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100">
777
+ <div className="flex items-center gap-2">
778
+ <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
779
+ <span className="text-xs text-slate-600">Ready to extract</span>
780
+ </div>
781
+ <div className="flex items-center gap-1 text-emerald-600">
782
+ <CheckCircle2 className="w-3.5 h-3.5" />
783
+ <span className="text-xs font-semibold">99.8% Accuracy</span>
784
+ </div>
785
+ </div>
786
+ </motion.div>
787
+
788
+ {/* Features */}
789
+ <div className="grid gap-3">
790
+ {features.map((feature, index) => (
791
+ <motion.div
792
+ key={feature.title}
793
+ initial={{ opacity: 0, x: -20 }}
794
+ animate={{ opacity: 1, x: 0 }}
795
+ transition={{ delay: 0.4 + index * 0.1 }}
796
+ className="flex items-start gap-3 group"
797
+ >
798
+ <div
799
+ className={`w-9 h-9 rounded-xl ${feature.bg} flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform`}
800
+ >
801
+ <feature.icon className={`w-4 h-4 ${feature.color}`} />
802
+ </div>
803
+ <div>
804
+ <h3 className="font-semibold text-slate-900 text-sm">{feature.title}</h3>
805
+ <p className="text-xs text-slate-500">{feature.description}</p>
806
+ </div>
807
+ </motion.div>
808
+ ))}
809
+ </div>
810
+ </motion.div>
811
+
812
+ {/* Trust Badge */}
813
+ <motion.div
814
+ initial={{ opacity: 0 }}
815
+ animate={{ opacity: 1 }}
816
+ transition={{ delay: 0.6 }}
817
+ className="relative z-10 flex items-center gap-3 text-xs text-slate-500 mt-6"
818
+ >
819
+ <Shield className="w-4 h-4" />
820
+ <span>Enterprise-grade security • SOC 2 Compliant • GDPR Ready</span>
821
+ </motion.div>
822
+ </div>
823
+
824
+ {/* Right Side - Sign In Form */}
825
+ <div className="w-full lg:w-[44%] flex items-center justify-center p-6 sm:p-10">
826
+ <motion.div
827
+ initial={{ opacity: 0, y: 20 }}
828
+ animate={{ opacity: 1, y: 0 }}
829
+ transition={{ delay: 0.2 }}
830
+ className="w-full max-w-md"
831
+ >
832
+ {/* Mobile Logo */}
833
+ <div className="lg:hidden flex items-center justify-center gap-3 mb-8">
834
+ <div className="h-12 w-12 flex items-center justify-center flex-shrink-0">
835
+ <img
836
+ src="/logo.png"
837
+ alt="EZOFIS AI Logo"
838
+ className="h-full w-full object-contain"
839
+ onError={(e) => {
840
+ // Fallback: hide image if logo not found
841
+ e.target.style.display = 'none';
842
+ }}
843
+ />
844
+ </div>
845
+ <div>
846
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1>
847
+ <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p>
848
+ </div>
849
+ </div>
850
+
851
+ <div className="bg-white rounded-3xl border border-slate-200/80 shadow-2xl shadow-slate-200/50 p-8 sm:p-10">
852
+ <div className="text-center mb-8">
853
+ <h2 className="text-2xl font-bold text-slate-900 mb-2">
854
+ {showOtp ? "Enter verification code" : "Secure Access"}
855
+ </h2>
856
+ <p className="text-slate-500">
857
+ {showOtp ? `We sent a code to ${email}` : "Access your document intelligence workspace"}
858
+ </p>
859
+ </div>
860
+
861
+ {/* Error Message */}
862
+ {error && (
863
+ <motion.div
864
+ initial={{ opacity: 0, y: -10 }}
865
+ animate={{ opacity: 1, y: 0 }}
866
+ className="mb-6 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2 text-sm text-red-700"
867
+ >
868
+ <AlertCircle className="h-4 w-4 flex-shrink-0 mt-0.5" />
869
+ <p>{error}</p>
870
+ </motion.div>
871
+ )}
872
+
873
+ {!showOtp ? (
874
+ <>
875
+ {/* Google Sign In */}
876
+ <Button
877
+ onClick={handleGoogleLogin}
878
+ disabled={loading}
879
+ variant="outline"
880
+ className="w-full h-12 text-base font-medium border-slate-200 hover:bg-slate-50 hover:border-slate-300 transition-all group"
881
+ >
882
+ {loading ? (
883
+ <Loader2 className="w-5 h-5 mr-3 animate-spin" />
884
+ ) : (
885
+ <svg className="w-5 h-5 mr-3" viewBox="0 0 24 24">
886
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
887
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
888
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
889
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
890
+ </svg>
891
+ )}
892
+ Continue with Google
893
+ <ArrowRight className="w-4 h-4 ml-auto opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
894
+ </Button>
895
+
896
+ <div className="relative my-8">
897
+ <Separator />
898
+ <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-4 text-sm text-slate-400">
899
+ or continue with email
900
+ </span>
901
+ </div>
902
+
903
+ {/* Email Input */}
904
+ <form onSubmit={handleEmailSubmit} className="space-y-4">
905
+ <div className="relative">
906
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
907
+ <Input
908
+ type="email"
909
+ placeholder="name@company.com"
910
+ value={email}
911
+ onChange={(e) => {
912
+ setEmail(e.target.value);
913
+ setError("");
914
+ }}
915
+ className="h-12 pl-12 text-base border-slate-200 focus:border-blue-500 focus:ring-blue-500"
916
+ />
917
+ </div>
918
+ <Button
919
+ type="submit"
920
+ disabled={loading}
921
+ className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25 transition-all"
922
+ >
923
+ {loading ? (
924
+ <>
925
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
926
+ Sending...
927
+ </>
928
+ ) : (
929
+ <>
930
+ Continue with Email
931
+ <ArrowRight className="w-4 h-4 ml-2" />
932
+ </>
933
+ )}
934
+ </Button>
935
+ </form>
936
+ </>
937
+ ) : (
938
+ /* OTP Input */
939
+ <form onSubmit={handleOtpVerify} className="space-y-6">
940
+ <div className="flex justify-center gap-2">
941
+ {otp.map((digit, index) => (
942
+ <Input
943
+ key={index}
944
+ id={`otp-${index}`}
945
+ type="text"
946
+ inputMode="numeric"
947
+ maxLength={1}
948
+ value={digit}
949
+ onChange={(e) => handleOtpChange(index, e.target.value)}
950
+ onKeyDown={(e) => handleOtpKeyDown(index, e)}
951
+ onPaste={(e) => handleOtpPaste(e, index)}
952
+ className="w-12 h-14 text-center text-xl font-semibold border-slate-200 focus:border-blue-500 focus:ring-blue-500"
953
+ />
954
+ ))}
955
+ </div>
956
+
957
+ <Button
958
+ type="submit"
959
+ disabled={loading || otp.join("").length !== 6}
960
+ className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25"
961
+ >
962
+ {loading ? (
963
+ <>
964
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
965
+ Verifying...
966
+ </>
967
+ ) : (
968
+ <>
969
+ Verify & Sign In
970
+ <ArrowRight className="w-4 h-4 ml-2" />
971
+ </>
972
+ )}
973
+ </Button>
974
+
975
+ <button
976
+ type="button"
977
+ onClick={() => {
978
+ setShowOtp(false);
979
+ setOtp(["", "", "", "", "", ""]);
980
+ setError("");
981
+ }}
982
+ className="w-full text-sm text-slate-500 hover:text-slate-700 transition-colors"
983
+ >
984
+ ← Back to sign in options
985
+ </button>
986
+ </form>
987
+ )}
988
+
989
+ {/* Notice */}
990
+ <div className="mt-8 pt-6 border-t border-slate-100">
991
+ <div className="flex items-start gap-2 text-xs text-slate-400 mb-4">
992
+ <Shield className="w-4 h-4 flex-shrink-0 mt-0.5" />
993
+ <span>Only business email addresses are allowed</span>
994
+ </div>
995
+ <p className="text-xs text-slate-400 text-center leading-relaxed">
996
+ By signing in, you agree to our{" "}
997
+ <a href="#" className="text-blue-600 hover:underline">
998
+ Terms of Service
999
+ </a>{" "}
1000
+ and{" "}
1001
+ <a href="#" className="text-blue-600 hover:underline">
1002
+ Privacy Policy
1003
+ </a>
1004
+ </p>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ {/* Mobile Features */}
1009
+ <div className="lg:hidden mt-8 space-y-4">
1010
+ {features.map((feature) => (
1011
+ <div key={feature.title} className="flex items-center gap-3 text-sm">
1012
+ <div className={`w-8 h-8 rounded-lg ${feature.bg} flex items-center justify-center`}>
1013
+ <feature.icon className={`w-4 h-4 ${feature.color}`} />
1014
+ </div>
1015
+ <span className="text-slate-600">{feature.title}</span>
1016
+ </div>
1017
+ ))}
1018
+ </div>
1019
+ </motion.div>
1020
+ </div>
1021
+ </div>
1022
+ );
1023
+ }
frontend/src/components/ocr/ExtractionOutput.jsx CHANGED
@@ -1,1206 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState, useEffect, useRef } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import {
5
- Code2,
6
- Copy,
7
- Check,
8
- Braces,
9
- FileCode2,
10
- FileText,
11
- Sparkles,
12
- ChevronDown,
13
- Upload,
14
- } from "lucide-react";
15
- import { Button } from "@/components/ui/button";
16
- import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
17
- import { cn } from "@/lib/utils";
18
-
19
- // Helper function to convert pipe-separated tables to HTML tables
20
- function convertPipeTablesToHTML(text) {
21
- if (!text) return text;
22
-
23
- const lines = text.split('\n');
24
- const result = [];
25
- let i = 0;
26
-
27
- while (i < lines.length) {
28
- const line = lines[i];
29
-
30
- // Check if this line looks like a table row (has multiple pipes)
31
- if (line.includes('|') && line.split('|').length >= 3) {
32
- // Check if it's a separator line (only |, -, :, spaces)
33
- const isSeparator = /^[\s|\-:]+$/.test(line.trim());
34
-
35
- if (!isSeparator) {
36
- // Start of a table - collect all table rows
37
- const tableRows = [];
38
- let j = i;
39
-
40
- // Collect header row
41
- const headerLine = lines[j];
42
- const headerCells = headerLine.split('|').map(cell => cell.trim()).filter(cell => cell || cell === '');
43
- // Remove empty cells at start/end
44
- if (headerCells.length > 0 && !headerCells[0]) headerCells.shift();
45
- if (headerCells.length > 0 && !headerCells[headerCells.length - 1]) headerCells.pop();
46
-
47
- if (headerCells.length >= 2) {
48
- tableRows.push(headerCells);
49
- j++;
50
-
51
- // Skip separator line if present
52
- if (j < lines.length && /^[\s|\-:]+$/.test(lines[j].trim())) {
53
- j++;
54
- }
55
-
56
- // Collect data rows
57
- while (j < lines.length) {
58
- const rowLine = lines[j];
59
- if (!rowLine.trim()) break; // Empty line ends table
60
-
61
- // Check if it's still a table row
62
- if (rowLine.includes('|') && rowLine.split('|').length >= 2) {
63
- const isRowSeparator = /^[\s|\-:]+$/.test(rowLine.trim());
64
- if (!isRowSeparator) {
65
- const rowCells = rowLine.split('|').map(cell => cell.trim());
66
- // Remove empty cells at start/end
67
- if (rowCells.length > 0 && !rowCells[0]) rowCells.shift();
68
- if (rowCells.length > 0 && !rowCells[rowCells.length - 1]) rowCells.pop();
69
- tableRows.push(rowCells);
70
- j++;
71
- } else {
72
- j++;
73
- }
74
- } else {
75
- break; // Not a table row anymore
76
- }
77
- }
78
-
79
- // Convert to HTML table
80
- if (tableRows.length > 0) {
81
- let htmlTable = '<table class="border-collapse border border-gray-300 w-full my-4">\n<thead>\n<tr>';
82
-
83
- // Header row
84
- tableRows[0].forEach(cell => {
85
- htmlTable += `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${escapeHtml(cell)}</th>`;
86
- });
87
- htmlTable += '</tr>\n</thead>\n<tbody>\n';
88
-
89
- // Data rows
90
- for (let rowIdx = 1; rowIdx < tableRows.length; rowIdx++) {
91
- htmlTable += '<tr>';
92
- tableRows[rowIdx].forEach((cell, colIdx) => {
93
- // Use header cell count to ensure alignment
94
- const cellContent = cell || '';
95
- htmlTable += `<td class="border border-gray-300 px-4 py-2">${escapeHtml(cellContent)}</td>`;
96
- });
97
- htmlTable += '</tr>\n';
98
- }
99
-
100
- htmlTable += '</tbody>\n</table>';
101
- result.push(htmlTable);
102
- i = j;
103
- continue;
104
- }
105
- }
106
- }
107
- }
108
-
109
- // Not a table row, add as-is
110
- result.push(line);
111
- i++;
112
- }
113
-
114
- return result.join('\n');
115
- }
116
-
117
- // Helper function to escape HTML
118
- function escapeHtml(text) {
119
- if (!text) return '';
120
- const div = document.createElement('div');
121
- div.textContent = text;
122
- return div.innerHTML;
123
- }
124
-
125
- // Helper function to convert markdown/HTML text to safe HTML
126
- function renderMarkdownToHTML(text) {
127
- if (!text) return "";
128
-
129
- let html = text;
130
-
131
- // FIRST: Convert pipe-separated tables to HTML tables
132
- html = convertPipeTablesToHTML(html);
133
-
134
- // Convert LaTeX-style superscripts/subscripts FIRST (before protecting tables)
135
- // This ensures they're converted everywhere, including inside tables
136
-
137
- // Convert LaTeX-style superscripts: $^{text}$ or $^text$ to <sup>text</sup>
138
- html = html.replace(/\$\s*\^\s*\{([^}]+)\}\s*\$/g, '<sup>$1</sup>');
139
- html = html.replace(/\$\s*\^\s*([^\s$<>]+)\s*\$/g, '<sup>$1</sup>');
140
-
141
- // Convert LaTeX-style subscripts: $_{text}$ or $_text$ to <sub>text</sub>
142
- html = html.replace(/\$\s*_\s*\{([^}]+)\}\s*\$/g, '<sub>$1</sub>');
143
- html = html.replace(/\$\s*_\s*([^\s$<>]+)\s*\$/g, '<sub>$1</sub>');
144
-
145
- // Split by HTML tags to preserve existing HTML (like tables)
146
- // Process markdown only in non-HTML sections
147
-
148
- // First, protect existing HTML blocks (tables, etc.)
149
- const htmlBlocks = [];
150
- let htmlBlockIndex = 0;
151
-
152
- // Extract and protect HTML table blocks
153
- html = html.replace(/<table[\s\S]*?<\/table>/gi, (match) => {
154
- const placeholder = `__HTML_BLOCK_${htmlBlockIndex}__`;
155
- htmlBlocks[htmlBlockIndex] = match;
156
- htmlBlockIndex++;
157
- return placeholder;
158
- });
159
-
160
- // Convert markdown headers (only if not inside HTML)
161
- html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
162
- html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
163
- html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
164
-
165
- // Convert markdown bold/italic (but not inside HTML tags)
166
- html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
167
- html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
168
-
169
- // Convert markdown links
170
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
171
-
172
- // Convert line breaks to paragraphs (but preserve structure around HTML blocks)
173
- const parts = html.split(/(__HTML_BLOCK_\d+__)/);
174
- const processedParts = parts.map((part, index) => {
175
- if (part.match(/^__HTML_BLOCK_\d+__$/)) {
176
- // Restore HTML block
177
- const blockIndex = parseInt(part.match(/\d+/)[0]);
178
- return htmlBlocks[blockIndex];
179
- } else {
180
- // Process markdown in this part
181
- let processed = part;
182
-
183
- // Convert double line breaks to paragraph breaks
184
- processed = processed.replace(/\n\n+/g, '</p><p>');
185
- // Convert single line breaks to <br> (but not if already in a tag)
186
- processed = processed.replace(/([^\n>])\n([^\n<])/g, '$1<br>$2');
187
-
188
- // Wrap in paragraph if there's content
189
- if (processed.trim() && !processed.trim().startsWith('<')) {
190
- processed = '<p>' + processed + '</p>';
191
- }
192
-
193
- return processed;
194
- }
195
- });
196
-
197
- html = processedParts.join('');
198
-
199
- // Process LaTeX notation in restored HTML blocks (tables) as well
200
- // This handles any LaTeX that might be in table cells
201
- html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*\^\s*\{([^}]+)\}\s*\$([^<]*)(<\/td>|<\/th>)/gi,
202
- (match, openTag, before, supText, after, closeTag) => {
203
- return openTag + before + '<sup>' + supText + '</sup>' + after + closeTag;
204
- });
205
- html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*\^\s*([^\s$<>]+)\s*\$([^<]*)(<\/td>|<\/th>)/gi,
206
- (match, openTag, before, supText, after, closeTag) => {
207
- return openTag + before + '<sup>' + supText + '</sup>' + after + closeTag;
208
- });
209
- html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*_\s*\{([^}]+)\}\s*\$([^<]*)(<\/td>|<\/th>)/gi,
210
- (match, openTag, before, subText, after, closeTag) => {
211
- return openTag + before + '<sub>' + subText + '</sub>' + after + closeTag;
212
- });
213
- html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*_\s*([^\s$<>]+)\s*\$([^<]*)(<\/td>|<\/th>)/gi,
214
- (match, openTag, before, subText, after, closeTag) => {
215
- return openTag + before + '<sub>' + subText + '</sub>' + after + closeTag;
216
- });
217
-
218
- // Clean up empty paragraphs and fix paragraph structure
219
- html = html.replace(/<p><\/p>/g, '');
220
- html = html.replace(/<p>\s*<br>\s*<\/p>/g, '');
221
- html = html.replace(/<p>\s*<\/p>/g, '');
222
-
223
- // Ensure proper spacing around HTML blocks
224
- html = html.replace(/(<\/table>)\s*(<h[1-3])/g, '$1</p><p>$2');
225
- html = html.replace(/(<\/h[1-3]>)\s*(<table)/g, '$1<p>$2');
226
- html = html.replace(/(<\/table>)\s*(<p>)/g, '$1$2');
227
-
228
- return html;
229
- }
230
-
231
- // Mock extracted data
232
- const mockData = {
233
- document: {
234
- type: "Invoice",
235
- confidence: 0.98,
236
- },
237
- vendor: {
238
- name: "Acme Corporation",
239
- address: "123 Business Ave, Suite 400",
240
- city: "San Francisco",
241
- state: "CA",
242
- zip: "94102",
243
- phone: "+1 (555) 123-4567",
244
- },
245
- invoice: {
246
- number: "INV-2024-0847",
247
- date: "2024-01-15",
248
- due_date: "2024-02-14",
249
- po_number: "PO-9823",
250
- },
251
- items: [
252
- { description: "Professional Services", quantity: 40, unit_price: 150.0, total: 6000.0 },
253
- { description: "Software License", quantity: 5, unit_price: 299.99, total: 1499.95 },
254
- { description: "Support Package", quantity: 1, unit_price: 500.0, total: 500.0 },
255
- ],
256
- totals: {
257
- subtotal: 7999.95,
258
- tax_rate: 0.0875,
259
- tax_amount: 699.99,
260
- total: 8699.94,
261
- },
262
- };
263
-
264
- const mockXML = `<?xml version="1.0" encoding="UTF-8"?>
265
- <extraction>
266
- <document type="Invoice" confidence="0.98"/>
267
- <vendor>
268
- <name>Acme Corporation</name>
269
- <address>123 Business Ave, Suite 400</address>
270
- <city>San Francisco</city>
271
- <state>CA</state>
272
- <zip>94102</zip>
273
- </vendor>
274
- <invoice>
275
- <number>INV-2024-0847</number>
276
- <date>2024-01-15</date>
277
- <due_date>2024-02-14</due_date>
278
- </invoice>
279
- <items>
280
- <item>
281
- <description>Professional Services</description>
282
- <quantity>40</quantity>
283
- <total>6000.00</total>
284
- </item>
285
- </items>
286
- <totals>
287
- <subtotal>7999.95</subtotal>
288
- <tax>699.99</tax>
289
- <total>8699.94</total>
290
- </totals>
291
- </extraction>`;
292
-
293
- const mockText = `INVOICE
294
-
295
- ACME CORPORATION
296
- 123 Business Ave, Suite 400
297
- San Francisco, CA 94102
298
- Phone: +1 (555) 123-4567
299
-
300
- Invoice Number: INV-2024-0847
301
- Invoice Date: January 15, 2024
302
- Due Date: February 14, 2024
303
- PO Number: PO-9823
304
-
305
- BILL TO:
306
- Customer Name
307
- 456 Client Street
308
- New York, NY 10001
309
-
310
- ITEMS:
311
- ─────────────────────────────────────────────────────────
312
- Description Qty Unit Price Total
313
- ─────────────────────────────────────────────────────────
314
- Professional Services 40 $150.00 $6,000.00
315
- Software License 5 $299.99 $1,499.95
316
- Support Package 1 $500.00 $500.00
317
- ─────────────────────────────────────────────────────────
318
-
319
- Subtotal: $7,999.95
320
- Tax (8.75%): $699.99
321
- ─────────────────────────
322
- TOTAL: $8,699.94
323
-
324
- Payment Terms: Net 30
325
- Thank you for your business!`;
326
-
327
- // Helper function to convert object to XML
328
- // Prepare fields for JSON/XML output - remove duplicates and restructure
329
- function prepareFieldsForOutput(fields, format = "json") {
330
- if (!fields || typeof fields !== "object") {
331
- return fields;
332
- }
333
-
334
- const output = { ...fields };
335
-
336
- // Extract Fields from root level if it exists
337
- const rootFields = output.Fields;
338
- // Remove Fields from output temporarily (will be added back at top)
339
- delete output.Fields;
340
-
341
- // Remove full_text from top-level if pages array exists (to avoid duplication)
342
- if (output.pages && Array.isArray(output.pages) && output.pages.length > 0) {
343
- delete output.full_text;
344
-
345
- // Clean up each page: remove full_text from page.fields (it duplicates page.text)
346
- output.pages = output.pages.map(page => {
347
- const cleanedPage = { ...page };
348
- if (cleanedPage.fields && typeof cleanedPage.fields === "object") {
349
- const cleanedFields = { ...cleanedPage.fields };
350
- // Remove full_text from page fields (duplicates page.text)
351
- delete cleanedFields.full_text;
352
- cleanedPage.fields = cleanedFields;
353
- }
354
- return cleanedPage;
355
- });
356
- }
357
-
358
- // For JSON and XML: restructure pages into separate top-level fields (page_1, page_2, etc.)
359
- if ((format === "json" || format === "xml") && output.pages && Array.isArray(output.pages)) {
360
- // Get top-level field keys (these are merged from all pages - avoid duplicating in page fields)
361
- const topLevelKeys = new Set(Object.keys(output).filter(k => k !== "pages" && k !== "full_text" && k !== "Fields"));
362
-
363
- output.pages.forEach((page, idx) => {
364
- const pageNum = page.page_number || idx + 1;
365
- const pageFields = page.fields || {};
366
-
367
- // Remove duplicate fields from page.fields:
368
- // 1. Remove full_text (duplicates page.text)
369
- // 2. Remove fields that match top-level fields (already shown at root)
370
- const cleanedPageFields = {};
371
- for (const [key, value] of Object.entries(pageFields)) {
372
- // Skip full_text and fields that match top-level exactly
373
- if (key !== "full_text" && (!topLevelKeys.has(key) || (value !== output[key]))) {
374
- cleanedPageFields[key] = value;
375
- }
376
- }
377
-
378
- const pageObj = {
379
- text: page.text || "",
380
- confidence: page.confidence || 0,
381
- doc_type: page.doc_type || "other"
382
- };
383
-
384
- // Add table and footer_notes if they exist
385
- if (page.table && Array.isArray(page.table) && page.table.length > 0) {
386
- pageObj.table = page.table;
387
- }
388
- if (page.footer_notes && Array.isArray(page.footer_notes) && page.footer_notes.length > 0) {
389
- pageObj.footer_notes = page.footer_notes;
390
- }
391
-
392
- // Only add fields if there are unique page-specific fields
393
- if (Object.keys(cleanedPageFields).length > 0) {
394
- pageObj.fields = cleanedPageFields;
395
- }
396
-
397
- output[`page_${pageNum}`] = pageObj;
398
- });
399
- // Remove pages array - we now have page_1, page_2, etc. as separate fields
400
- delete output.pages;
401
- }
402
-
403
- // Handle page_X structure (from backend) - remove Fields from page objects if they exist
404
- if (output && typeof output === "object") {
405
- const pageKeys = Object.keys(output).filter(k => k.startsWith("page_"));
406
- for (const pageKey of pageKeys) {
407
- const pageData = output[pageKey];
408
- if (pageData && typeof pageData === "object") {
409
- // Remove Fields from page objects (it's now at root level)
410
- delete pageData.Fields;
411
- delete pageData.metadata;
412
- }
413
- }
414
- }
415
-
416
- // Rebuild output with Fields at the top (only if it exists and is not empty)
417
- const finalOutput = {};
418
- if (rootFields && typeof rootFields === "object" && Object.keys(rootFields).length > 0) {
419
- finalOutput.Fields = rootFields;
420
- }
421
-
422
- // Add all other keys
423
- Object.keys(output).forEach(key => {
424
- finalOutput[key] = output[key];
425
- });
426
-
427
- return finalOutput;
428
- }
429
-
430
- function objectToXML(obj, rootName = "extraction") {
431
- // Prepare fields - remove full_text if pages exist
432
- const preparedObj = prepareFieldsForOutput(obj, "xml");
433
-
434
- let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}>\n`;
435
-
436
- const convert = (obj, indent = " ") => {
437
- for (const [key, value] of Object.entries(obj)) {
438
- if (value === null || value === undefined) continue;
439
-
440
- // Skip full_text if pages exist (already handled in prepareFieldsForOutput)
441
- if (key === "full_text" && obj.pages && Array.isArray(obj.pages) && obj.pages.length > 0) {
442
- continue;
443
- }
444
-
445
- if (Array.isArray(value)) {
446
- value.forEach((item) => {
447
- xml += `${indent}<${key}>\n`;
448
- if (typeof item === "object") {
449
- convert(item, indent + " ");
450
- } else {
451
- xml += `${indent} ${escapeXML(String(item))}\n`;
452
- }
453
- xml += `${indent}</${key}>\n`;
454
- });
455
- } else if (typeof value === "object") {
456
- xml += `${indent}<${key}>\n`;
457
- convert(value, indent + " ");
458
- xml += `${indent}</${key}>\n`;
459
- } else {
460
- xml += `${indent}<${key}>${escapeXML(String(value))}</${key}>\n`;
461
- }
462
- }
463
- };
464
-
465
- convert(preparedObj);
466
- xml += `</${rootName}>`;
467
- return xml;
468
- }
469
-
470
- function escapeXML(str) {
471
- return str
472
- .replace(/&/g, "&amp;")
473
- .replace(/</g, "&lt;")
474
- .replace(/>/g, "&gt;")
475
- .replace(/"/g, "&quot;")
476
- .replace(/'/g, "&apos;");
477
- }
478
-
479
- // Helper function to extract text from page structure
480
- function extractTextFromFields(fields) {
481
- if (!fields || typeof fields !== "object") {
482
- return "";
483
- }
484
-
485
- // Check for page_X structure first (preferred format)
486
- const pageKeys = Object.keys(fields).filter(key => key.startsWith("page_"));
487
- if (pageKeys.length > 0) {
488
- // Get text from first page (or combine all pages)
489
- const pageTexts = pageKeys.map(key => {
490
- const page = fields[key];
491
- if (page && page.text) {
492
- return page.text;
493
- }
494
- return "";
495
- }).filter(text => text);
496
-
497
- if (pageTexts.length > 0) {
498
- return pageTexts.join("\n\n");
499
- }
500
- }
501
-
502
- // Fallback to full_text
503
- if (fields.full_text) {
504
- return fields.full_text;
505
- }
506
-
507
- return "";
508
- }
509
-
510
- // Helper function to format fields as readable text
511
- function fieldsToText(fields) {
512
- if (!fields || typeof fields !== "object") {
513
- return "No data extracted.";
514
- }
515
-
516
- // Extract text from page structure or full_text
517
- const extractedText = extractTextFromFields(fields);
518
-
519
- if (extractedText) {
520
- return extractedText;
521
-
522
- // Don't show pages array separately if full_text already contains page markers
523
- // (full_text from backend already includes "=== PAGE 1 ===" etc.)
524
- const hasPageMarkers = fields.full_text.includes("=== PAGE") || fields.full_text.includes("--- Page");
525
-
526
- // Only show pages array if full_text doesn't already have page breakdown
527
- if (!hasPageMarkers && fields.pages && Array.isArray(fields.pages)) {
528
- text += "\n\n=== TEXT BY PAGE ===\n\n";
529
- fields.pages.forEach((page, idx) => {
530
- text += `--- Page ${page.page_number || idx + 1} ---\n`;
531
- text += page.text || "";
532
- text += "\n\n";
533
- });
534
- }
535
-
536
- // Then show other structured fields
537
- const otherFields = { ...fields };
538
- delete otherFields.full_text;
539
- delete otherFields.pages;
540
-
541
- if (Object.keys(otherFields).length > 0) {
542
- text += "\n\n=== STRUCTURED FIELDS ===\n\n";
543
- const formatValue = (key, value, indent = "") => {
544
- if (Array.isArray(value)) {
545
- text += `${indent}${key}:\n`;
546
- value.forEach((item, idx) => {
547
- if (typeof item === "object") {
548
- text += `${indent} Item ${idx + 1}:\n`;
549
- Object.entries(item).forEach(([k, v]) => formatValue(k, v, indent + " "));
550
- } else {
551
- text += `${indent} - ${item}\n`;
552
- }
553
- });
554
- } else if (typeof value === "object" && value !== null) {
555
- text += `${indent}${key}:\n`;
556
- Object.entries(value).forEach(([k, v]) => formatValue(k, v, indent + " "));
557
- } else {
558
- text += `${indent}${key}: ${value}\n`;
559
- }
560
- };
561
-
562
- Object.entries(otherFields).forEach(([key, value]) => {
563
- formatValue(key, value);
564
- text += "\n";
565
- });
566
- }
567
-
568
- return text.trim();
569
- }
570
-
571
- // Fallback: format all fields normally
572
- let text = "";
573
- const formatValue = (key, value, indent = "") => {
574
- if (Array.isArray(value)) {
575
- text += `${indent}${key}:\n`;
576
- value.forEach((item, idx) => {
577
- if (typeof item === "object") {
578
- text += `${indent} Item ${idx + 1}:\n`;
579
- Object.entries(item).forEach(([k, v]) => formatValue(k, v, indent + " "));
580
- } else {
581
- text += `${indent} - ${item}\n`;
582
- }
583
- });
584
- } else if (typeof value === "object" && value !== null) {
585
- text += `${indent}${key}:\n`;
586
- Object.entries(value).forEach(([k, v]) => formatValue(k, v, indent + " "));
587
- } else {
588
- text += `${indent}${key}: ${value}\n`;
589
- }
590
- };
591
-
592
- Object.entries(fields).forEach(([key, value]) => {
593
- formatValue(key, value);
594
- text += "\n";
595
- });
596
-
597
- return text.trim() || "No data extracted.";
598
- }
599
-
600
- export default function ExtractionOutput({ hasFile, isProcessing, isComplete, extractionResult, onNewUpload }) {
601
- const [activeTab, setActiveTab] = useState("json");
602
- const [copied, setCopied] = useState(false);
603
- const [statusMessage, setStatusMessage] = useState("Preparing document...");
604
-
605
- // Get fields from extraction result, default to empty object
606
- const fields = extractionResult?.fields || {};
607
- const confidence = extractionResult?.confidence || 0;
608
- const fieldsExtracted = extractionResult?.fieldsExtracted || 0;
609
- const totalTime = extractionResult?.totalTime || 0;
610
-
611
- // Dynamic status messages that rotate during processing
612
- const statusMessages = [
613
- "Preparing document...",
614
- "Converting pages to images...",
615
- "Visual Reasoning...",
616
- "Reading text from document...",
617
- "Identifying document structure...",
618
- "Extracting tables and data...",
619
- "Analyzing content...",
620
- "Processing pages...",
621
- "Organizing extracted information...",
622
- "Finalizing results...",
623
- ];
624
-
625
- // Rotate status messages during processing
626
- const messageIndexRef = useRef(0);
627
-
628
- useEffect(() => {
629
- if (!isProcessing) {
630
- setStatusMessage("Analyzing document structure");
631
- messageIndexRef.current = 0;
632
- return;
633
- }
634
-
635
- setStatusMessage(statusMessages[0]);
636
- messageIndexRef.current = 0;
637
-
638
- const interval = setInterval(() => {
639
- messageIndexRef.current = (messageIndexRef.current + 1) % statusMessages.length;
640
- setStatusMessage(statusMessages[messageIndexRef.current]);
641
- }, 2500); // Change message every 2.5 seconds
642
-
643
- return () => clearInterval(interval);
644
- }, [isProcessing]);
645
-
646
- // Initialize expanded sections based on available fields
647
- const [expandedSections, setExpandedSections] = useState(() =>
648
- Object.keys(fields).slice(0, 5) // Expand first 5 sections by default
649
- );
650
-
651
- // Helper function to convert HTML to formatted plain text with layout preserved
652
- const htmlToFormattedText = (html) => {
653
- if (!html) return "";
654
-
655
- // Create a temporary div to parse HTML
656
- const tempDiv = document.createElement("div");
657
- tempDiv.innerHTML = html;
658
-
659
- let text = "";
660
-
661
- // Process each element
662
- const processNode = (node) => {
663
- if (node.nodeType === Node.TEXT_NODE) {
664
- return node.textContent;
665
- }
666
-
667
- if (node.nodeType !== Node.ELEMENT_NODE) {
668
- return "";
669
- }
670
-
671
- const tagName = node.tagName?.toLowerCase();
672
- const children = Array.from(node.childNodes);
673
-
674
- switch (tagName) {
675
- case "h1":
676
- return "\n\n" + processChildren(children).trim() + "\n\n";
677
- case "h2":
678
- return "\n\n" + processChildren(children).trim() + "\n\n";
679
- case "h3":
680
- return "\n" + processChildren(children).trim() + "\n";
681
- case "p":
682
- return processChildren(children) + "\n\n";
683
- case "br":
684
- return "\n";
685
- case "strong":
686
- case "b":
687
- return processChildren(children);
688
- case "em":
689
- case "i":
690
- return processChildren(children);
691
- case "sup":
692
- return processChildren(children);
693
- case "sub":
694
- return processChildren(children);
695
- case "table":
696
- return "\n" + processTable(node) + "\n\n";
697
- case "ul":
698
- case "ol":
699
- return "\n" + processList(node) + "\n\n";
700
- case "li":
701
- return " • " + processChildren(children).trim() + "\n";
702
- default:
703
- return processChildren(children);
704
- }
705
- };
706
-
707
- const processChildren = (children) => {
708
- return children.map(processNode).join("");
709
- };
710
-
711
- const processTable = (table) => {
712
- let tableText = "";
713
- const rows = table.querySelectorAll("tr");
714
-
715
- if (rows.length === 0) return "";
716
-
717
- // First pass: calculate column widths
718
- const allRows = Array.from(rows);
719
- const columnCount = Math.max(...allRows.map(row => row.querySelectorAll("td, th").length));
720
- const columnWidths = new Array(columnCount).fill(0);
721
-
722
- allRows.forEach(row => {
723
- const cells = row.querySelectorAll("td, th");
724
- cells.forEach((cell, colIndex) => {
725
- const cellText = processChildren(Array.from(cell.childNodes)).trim().replace(/\s+/g, " ");
726
- columnWidths[colIndex] = Math.max(columnWidths[colIndex] || 0, cellText.length, 10);
727
- });
728
- });
729
-
730
- // Second pass: format rows
731
- allRows.forEach((row, rowIndex) => {
732
- const cells = row.querySelectorAll("td, th");
733
- const cellTexts = Array.from(cells).map(cell => {
734
- let cellContent = processChildren(Array.from(cell.childNodes)).trim();
735
- cellContent = cellContent.replace(/\s+/g, " ");
736
- return cellContent;
737
- });
738
-
739
- // Pad cells to column widths
740
- const paddedCells = cellTexts.map((text, i) => {
741
- const width = columnWidths[i] || 10;
742
- return text.padEnd(width);
743
- });
744
-
745
- tableText += paddedCells.join(" | ") + "\n";
746
-
747
- // Add separator after header row
748
- if (rowIndex === 0 && row.querySelector("th")) {
749
- tableText += columnWidths.map(w => "-".repeat(w)).join("-|-") + "\n";
750
- }
751
- });
752
-
753
- return tableText;
754
- };
755
-
756
- const processList = (list) => {
757
- const items = list.querySelectorAll("li");
758
- return Array.from(items).map(item => {
759
- return " • " + processChildren(Array.from(item.childNodes)).trim();
760
- }).join("\n");
761
- };
762
-
763
- text = processChildren(Array.from(tempDiv.childNodes));
764
-
765
- // Clean up extra newlines
766
- text = text.replace(/\n{3,}/g, "\n\n");
767
- text = text.trim();
768
-
769
- return text;
770
- };
771
-
772
- const handleCopy = () => {
773
- let content = "";
774
- if (activeTab === "json") {
775
- const preparedFields = prepareFieldsForOutput(fields, "json");
776
- content = JSON.stringify(preparedFields, null, 2);
777
- } else if (activeTab === "xml") {
778
- content = objectToXML(fields);
779
- } else {
780
- // For text tab, get the formatted HTML and convert to plain text with layout
781
- const textContent = extractTextFromFields(fields);
782
- const htmlContent = renderMarkdownToHTML(textContent);
783
- content = htmlToFormattedText(htmlContent);
784
- }
785
-
786
- navigator.clipboard.writeText(content);
787
- setCopied(true);
788
- setTimeout(() => setCopied(false), 2000);
789
- };
790
-
791
- // Get prepared fields for display
792
- const preparedFields = React.useMemo(() => {
793
- return prepareFieldsForOutput(fields, "json");
794
- }, [fields]);
795
-
796
- // Update expanded sections when fields change
797
- React.useEffect(() => {
798
- if (extractionResult?.fields) {
799
- setExpandedSections(Object.keys(extractionResult.fields).slice(0, 5));
800
- }
801
- }, [extractionResult]);
802
-
803
- const toggleSection = (section) => {
804
- setExpandedSections((prev) =>
805
- prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
806
- );
807
- };
808
-
809
- const renderValue = (value) => {
810
- if (typeof value === "number") {
811
- return <span className="text-amber-600">{value}</span>;
812
- }
813
- if (typeof value === "string") {
814
- return <span className="text-emerald-600">"{value}"</span>;
815
- }
816
- return String(value);
817
- };
818
-
819
- const renderSection = (key, value, level = 0) => {
820
- const isExpanded = expandedSections.includes(key);
821
- const isObject = typeof value === "object" && value !== null;
822
- const isArray = Array.isArray(value);
823
-
824
- if (!isObject) {
825
- return (
826
- <div
827
- key={key}
828
- className="flex items-start gap-2 py-1"
829
- style={{ paddingLeft: level * 16 }}
830
- >
831
- <span className="text-violet-500">"{key}"</span>
832
- <span className="text-slate-400">:</span>
833
- {renderValue(value)}
834
- </div>
835
- );
836
- }
837
-
838
- return (
839
- <div key={key}>
840
- <button
841
- onClick={() => toggleSection(key)}
842
- className="flex items-center gap-2 py-1 hover:bg-slate-50 w-full text-left rounded"
843
- style={{ paddingLeft: level * 16 }}
844
- >
845
- <ChevronDown
846
- className={cn(
847
- "h-3 w-3 text-slate-400 transition-transform",
848
- !isExpanded && "-rotate-90"
849
- )}
850
- />
851
- <span className="text-violet-500">"{key}"</span>
852
- <span className="text-slate-400">:</span>
853
- <span className="text-slate-400">{isArray ? "[" : "{"}</span>
854
- {!isExpanded && (
855
- <span className="text-slate-300 text-xs">
856
- {isArray ? `${value.length} items` : `${Object.keys(value).length} fields`}
857
- </span>
858
- )}
859
- </button>
860
- <AnimatePresence>
861
- {isExpanded && (
862
- <motion.div
863
- initial={{ height: 0, opacity: 0 }}
864
- animate={{ height: "auto", opacity: 1 }}
865
- exit={{ height: 0, opacity: 0 }}
866
- transition={{ duration: 0.2 }}
867
- className="overflow-hidden"
868
- >
869
- {isArray ? (
870
- value.map((item, idx) => (
871
- <div key={idx} className="border-l border-slate-100 ml-4">
872
- {Object.entries(item).map(([k, v]) => renderSection(k, v, level + 2))}
873
- {idx < value.length - 1 && <div className="h-2" />}
874
- </div>
875
- ))
876
- ) : (
877
- Object.entries(value).map(([k, v]) => renderSection(k, v, level + 1))
878
- )}
879
- <div style={{ paddingLeft: level * 16 }} className="text-slate-400">
880
- {isArray ? "]" : "}"}
881
- </div>
882
- </motion.div>
883
- )}
884
- </AnimatePresence>
885
- </div>
886
- );
887
- };
888
-
889
- return (
890
- <div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
891
- {/* Header */}
892
- <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
893
- <div className="flex items-center gap-3">
894
- <div className="h-8 w-8 rounded-lg bg-emerald-50 flex items-center justify-center">
895
- <Code2 className="h-4 w-4 text-emerald-600" />
896
- </div>
897
- <div>
898
- <h3 className="font-semibold text-slate-800 text-sm">Extracted Data</h3>
899
- <p className="text-xs text-slate-400">
900
- {isComplete
901
- ? `${fieldsExtracted} field${fieldsExtracted !== 1 ? 's' : ''} extracted`
902
- : "Waiting for extraction"}
903
- </p>
904
- </div>
905
- {isComplete && onNewUpload && (
906
- <Button
907
- variant="ghost"
908
- size="sm"
909
- onClick={onNewUpload}
910
- className="h-8 ml-auto text-xs gap-1.5 text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50"
911
- title="Upload new document"
912
- >
913
- <Upload className="h-3.5 w-3.5" />
914
- New
915
- </Button>
916
- )}
917
- </div>
918
-
919
- {isComplete && (
920
- <div className="flex items-center gap-2">
921
- <Tabs value={activeTab} onValueChange={setActiveTab}>
922
- <TabsList className="h-8 bg-slate-100 p-0.5">
923
- <TabsTrigger value="text" className="h-7 text-xs gap-1.5">
924
- <FileText className="h-3 w-3" />
925
- Text
926
- </TabsTrigger>
927
- <TabsTrigger value="json" className="h-7 text-xs gap-1.5">
928
- <Braces className="h-3 w-3" />
929
- JSON
930
- </TabsTrigger>
931
- <TabsTrigger value="xml" className="h-7 text-xs gap-1.5">
932
- <FileCode2 className="h-3 w-3" />
933
- XML
934
- </TabsTrigger>
935
- </TabsList>
936
- </Tabs>
937
- <Button
938
- variant="ghost"
939
- size="sm"
940
- onClick={handleCopy}
941
- className="h-8 text-xs gap-1.5"
942
- >
943
- {copied ? (
944
- <>
945
- <Check className="h-3 w-3 text-emerald-500" />
946
- Copied
947
- </>
948
- ) : (
949
- <>
950
- <Copy className="h-3 w-3" />
951
- Copy
952
- </>
953
- )}
954
- </Button>
955
- </div>
956
- )}
957
- </div>
958
-
959
- {/* Output Area */}
960
- <div className="flex-1 overflow-auto">
961
- {!hasFile ? (
962
- <div className="h-full flex items-center justify-center p-6">
963
- <div className="text-center">
964
- <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
965
- <Code2 className="h-10 w-10 text-slate-300" />
966
- </div>
967
- <p className="text-slate-400 text-sm">Extracted data will appear here</p>
968
- </div>
969
- </div>
970
- ) : isProcessing ? (
971
- <div className="h-full flex items-center justify-center p-6">
972
- <div className="text-center">
973
- <motion.div
974
- animate={{ rotate: 360 }}
975
- transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
976
- 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"
977
- >
978
- <Sparkles className="h-8 w-8 text-indigo-500" />
979
- </motion.div>
980
- <p className="text-slate-700 font-medium mb-1">Extracting data...</p>
981
- <p className="text-slate-400 text-sm">{statusMessage}</p>
982
-
983
- <div className="mt-6 flex items-center justify-center gap-1">
984
- {[0, 1, 2].map((i) => (
985
- <motion.div
986
- key={i}
987
- animate={{ scale: [1, 1.2, 1] }}
988
- transition={{
989
- duration: 0.6,
990
- repeat: Infinity,
991
- delay: i * 0.2,
992
- }}
993
- className="h-2 w-2 rounded-full bg-indigo-400"
994
- />
995
- ))}
996
- </div>
997
- </div>
998
- </div>
999
- ) : isComplete && Object.keys(fields).length === 0 ? (
1000
- <div className="h-full flex items-center justify-center p-6">
1001
- <div className="text-center">
1002
- <div className="h-20 w-20 mx-auto rounded-2xl bg-amber-100 flex items-center justify-center mb-4">
1003
- <Code2 className="h-10 w-10 text-amber-600" />
1004
- </div>
1005
- <p className="text-slate-600 font-medium mb-1">No data extracted</p>
1006
- <p className="text-slate-400 text-sm">The document may not contain extractable fields</p>
1007
- </div>
1008
- </div>
1009
- ) : (
1010
- <div className="p-4 font-mono text-sm">
1011
- {activeTab === "text" ? (
1012
- <div
1013
- className="text-sm text-slate-700 leading-relaxed"
1014
- style={{
1015
- fontFamily: 'system-ui, -apple-system, sans-serif'
1016
- }}
1017
- >
1018
- <div
1019
- className="markdown-content"
1020
- dangerouslySetInnerHTML={{ __html: renderMarkdownToHTML(fieldsToText(fields)) }}
1021
- style={{
1022
- lineHeight: '1.6'
1023
- }}
1024
- />
1025
- <style>{`
1026
- .markdown-content h1 {
1027
- font-size: 1.5rem;
1028
- font-weight: 700;
1029
- color: #0f172a;
1030
- margin-top: 1.5rem;
1031
- margin-bottom: 1rem;
1032
- line-height: 1.3;
1033
- }
1034
- .markdown-content h2 {
1035
- font-size: 1.25rem;
1036
- font-weight: 600;
1037
- color: #0f172a;
1038
- margin-top: 1.25rem;
1039
- margin-bottom: 0.75rem;
1040
- line-height: 1.3;
1041
- }
1042
- .markdown-content h3 {
1043
- font-size: 1.125rem;
1044
- font-weight: 600;
1045
- color: #1e293b;
1046
- margin-top: 1rem;
1047
- margin-bottom: 0.5rem;
1048
- line-height: 1.3;
1049
- }
1050
- .markdown-content p {
1051
- margin-top: 0.75rem;
1052
- margin-bottom: 0.75rem;
1053
- color: #334155;
1054
- }
1055
- .markdown-content table {
1056
- width: 100%;
1057
- border-collapse: collapse;
1058
- margin: 1.5rem 0;
1059
- font-size: 0.875rem;
1060
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
1061
- }
1062
- .markdown-content table caption {
1063
- font-weight: 600;
1064
- margin-bottom: 0.5rem;
1065
- text-align: left;
1066
- }
1067
- .markdown-content table th {
1068
- background-color: #f8fafc;
1069
- border: 1px solid #cbd5e1;
1070
- padding: 0.75rem;
1071
- text-align: left;
1072
- font-weight: 600;
1073
- color: #0f172a;
1074
- }
1075
- .markdown-content table td {
1076
- border: 1px solid #cbd5e1;
1077
- padding: 0.75rem;
1078
- color: #334155;
1079
- }
1080
- .markdown-content table tr:nth-child(even) {
1081
- background-color: #f8fafc;
1082
- }
1083
- .markdown-content table tr:hover {
1084
- background-color: #f1f5f9;
1085
- }
1086
- .markdown-content strong {
1087
- font-weight: 600;
1088
- color: #0f172a;
1089
- }
1090
- .markdown-content em {
1091
- font-style: italic;
1092
- }
1093
- .markdown-content a {
1094
- color: #4f46e5;
1095
- text-decoration: underline;
1096
- }
1097
- .markdown-content a:hover {
1098
- color: #4338ca;
1099
- }
1100
- .markdown-content sup {
1101
- font-size: 0.75em;
1102
- vertical-align: super;
1103
- line-height: 0;
1104
- position: relative;
1105
- top: -0.5em;
1106
- }
1107
- .markdown-content sub {
1108
- font-size: 0.75em;
1109
- vertical-align: sub;
1110
- line-height: 0;
1111
- position: relative;
1112
- bottom: -0.25em;
1113
- }
1114
- .markdown-content ul, .markdown-content ol {
1115
- margin: 0.75rem 0;
1116
- padding-left: 1.5rem;
1117
- }
1118
- .markdown-content li {
1119
- margin: 0.25rem 0;
1120
- }
1121
- `}</style>
1122
- </div>
1123
- ) : activeTab === "json" ? (
1124
- <div className="space-y-1">
1125
- <span className="text-slate-400">{"{"}</span>
1126
- {Object.keys(preparedFields).length > 0 ? (
1127
- Object.entries(preparedFields).map(([key, value]) =>
1128
- renderSection(key, value, 1)
1129
- )
1130
- ) : (
1131
- <div className="pl-4 text-slate-400 italic">No fields extracted</div>
1132
- )}
1133
- <span className="text-slate-400">{"}"}</span>
1134
- </div>
1135
- ) : (
1136
- <pre className="text-sm text-slate-600 whitespace-pre-wrap">
1137
- {objectToXML(fields).split("\n").map((line, i) => (
1138
- <div key={i} className="hover:bg-slate-50 px-2 -mx-2 rounded">
1139
- {line.includes("<") ? (
1140
- <>
1141
- {line.split(/(<\/?[\w\s=".-]+>)/g).map((part, j) => {
1142
- if (part.startsWith("</")) {
1143
- return (
1144
- <span key={j} className="text-rose-500">
1145
- {part}
1146
- </span>
1147
- );
1148
- }
1149
- if (part.startsWith("<")) {
1150
- return (
1151
- <span key={j} className="text-indigo-500">
1152
- {part}
1153
- </span>
1154
- );
1155
- }
1156
- return (
1157
- <span key={j} className="text-slate-700">
1158
- {part}
1159
- </span>
1160
- );
1161
- })}
1162
- </>
1163
- ) : (
1164
- line
1165
- )}
1166
- </div>
1167
- ))}
1168
- </pre>
1169
- )}
1170
- </div>
1171
- )}
1172
- </div>
1173
-
1174
- {/* Confidence Footer */}
1175
- {isComplete && extractionResult && (
1176
- <div className="px-5 py-3 border-t border-slate-100 bg-slate-50/50">
1177
- <div className="flex items-center justify-between text-xs">
1178
- <div className="flex items-center gap-4">
1179
- <div className="flex items-center gap-1.5">
1180
- <div className={cn(
1181
- "h-2 w-2 rounded-full",
1182
- confidence >= 90 ? "bg-emerald-500" : confidence >= 70 ? "bg-amber-500" : "bg-red-500"
1183
- )} />
1184
- <span className="text-slate-500">Confidence:</span>
1185
- <span className="font-semibold text-slate-700">
1186
- {confidence > 0 ? `${confidence.toFixed(1)}%` : "N/A"}
1187
- </span>
1188
- </div>
1189
- <div className="flex items-center gap-1.5">
1190
- <span className="text-slate-500">Fields:</span>
1191
- <span className="font-semibold text-slate-700">{fieldsExtracted}</span>
1192
- </div>
1193
- </div>
1194
- <span className="text-slate-400">
1195
- Processed in {totalTime >= 1000 ? `${(totalTime / 1000).toFixed(1)}s` : `${totalTime}ms`}
1196
- </span>
1197
- </div>
1198
- </div>
1199
- )}
1200
- </div>
1201
- );
1202
- }
1203
- =======
1204
  import React, { useState, useEffect, useRef } from "react";
1205
  import { motion, AnimatePresence } from "framer-motion";
1206
  import {
@@ -2402,4 +1199,1203 @@ export default function ExtractionOutput({ hasFile, isProcessing, isComplete, ex
2402
  </div>
2403
  );
2404
  }
2405
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useEffect, useRef } from "react";
2
  import { motion, AnimatePresence } from "framer-motion";
3
  import {
 
1199
  </div>
1200
  );
1201
  }
1202
+ import { motion, AnimatePresence } from "framer-motion";
1203
+ import {
1204
+ Code2,
1205
+ Copy,
1206
+ Check,
1207
+ Braces,
1208
+ FileCode2,
1209
+ FileText,
1210
+ Sparkles,
1211
+ ChevronDown,
1212
+ Upload,
1213
+ } from "lucide-react";
1214
+ import { Button } from "@/components/ui/button";
1215
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
1216
+ import { cn } from "@/lib/utils";
1217
+
1218
+ // Helper function to convert pipe-separated tables to HTML tables
1219
+ function convertPipeTablesToHTML(text) {
1220
+ if (!text) return text;
1221
+
1222
+ const lines = text.split('\n');
1223
+ const result = [];
1224
+ let i = 0;
1225
+
1226
+ while (i < lines.length) {
1227
+ const line = lines[i];
1228
+
1229
+ // Check if this line looks like a table row (has multiple pipes)
1230
+ if (line.includes('|') && line.split('|').length >= 3) {
1231
+ // Check if it's a separator line (only |, -, :, spaces)
1232
+ const isSeparator = /^[\s|\-:]+$/.test(line.trim());
1233
+
1234
+ if (!isSeparator) {
1235
+ // Start of a table - collect all table rows
1236
+ const tableRows = [];
1237
+ let j = i;
1238
+
1239
+ // Collect header row
1240
+ const headerLine = lines[j];
1241
+ const headerCells = headerLine.split('|').map(cell => cell.trim()).filter(cell => cell || cell === '');
1242
+ // Remove empty cells at start/end
1243
+ if (headerCells.length > 0 && !headerCells[0]) headerCells.shift();
1244
+ if (headerCells.length > 0 && !headerCells[headerCells.length - 1]) headerCells.pop();
1245
+
1246
+ if (headerCells.length >= 2) {
1247
+ tableRows.push(headerCells);
1248
+ j++;
1249
+
1250
+ // Skip separator line if present
1251
+ if (j < lines.length && /^[\s|\-:]+$/.test(lines[j].trim())) {
1252
+ j++;
1253
+ }
1254
+
1255
+ // Collect data rows
1256
+ while (j < lines.length) {
1257
+ const rowLine = lines[j];
1258
+ if (!rowLine.trim()) break; // Empty line ends table
1259
+
1260
+ // Check if it's still a table row
1261
+ if (rowLine.includes('|') && rowLine.split('|').length >= 2) {
1262
+ const isRowSeparator = /^[\s|\-:]+$/.test(rowLine.trim());
1263
+ if (!isRowSeparator) {
1264
+ const rowCells = rowLine.split('|').map(cell => cell.trim());
1265
+ // Remove empty cells at start/end
1266
+ if (rowCells.length > 0 && !rowCells[0]) rowCells.shift();
1267
+ if (rowCells.length > 0 && !rowCells[rowCells.length - 1]) rowCells.pop();
1268
+ tableRows.push(rowCells);
1269
+ j++;
1270
+ } else {
1271
+ j++;
1272
+ }
1273
+ } else {
1274
+ break; // Not a table row anymore
1275
+ }
1276
+ }
1277
+
1278
+ // Convert to HTML table
1279
+ if (tableRows.length > 0) {
1280
+ let htmlTable = '<table class="border-collapse border border-gray-300 w-full my-4">\n<thead>\n<tr>';
1281
+
1282
+ // Header row
1283
+ tableRows[0].forEach(cell => {
1284
+ htmlTable += `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${escapeHtml(cell)}</th>`;
1285
+ });
1286
+ htmlTable += '</tr>\n</thead>\n<tbody>\n';
1287
+
1288
+ // Data rows
1289
+ for (let rowIdx = 1; rowIdx < tableRows.length; rowIdx++) {
1290
+ htmlTable += '<tr>';
1291
+ tableRows[rowIdx].forEach((cell, colIdx) => {
1292
+ // Use header cell count to ensure alignment
1293
+ const cellContent = cell || '';
1294
+ htmlTable += `<td class="border border-gray-300 px-4 py-2">${escapeHtml(cellContent)}</td>`;
1295
+ });
1296
+ htmlTable += '</tr>\n';
1297
+ }
1298
+
1299
+ htmlTable += '</tbody>\n</table>';
1300
+ result.push(htmlTable);
1301
+ i = j;
1302
+ continue;
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+
1308
+ // Not a table row, add as-is
1309
+ result.push(line);
1310
+ i++;
1311
+ }
1312
+
1313
+ return result.join('\n');
1314
+ }
1315
+
1316
+ // Helper function to escape HTML
1317
+ function escapeHtml(text) {
1318
+ if (!text) return '';
1319
+ const div = document.createElement('div');
1320
+ div.textContent = text;
1321
+ return div.innerHTML;
1322
+ }
1323
+
1324
+ // Helper function to convert markdown/HTML text to safe HTML
1325
+ function renderMarkdownToHTML(text) {
1326
+ if (!text) return "";
1327
+
1328
+ let html = text;
1329
+
1330
+ // FIRST: Convert pipe-separated tables to HTML tables
1331
+ html = convertPipeTablesToHTML(html);
1332
+
1333
+ // Convert LaTeX-style superscripts/subscripts FIRST (before protecting tables)
1334
+ // This ensures they're converted everywhere, including inside tables
1335
+
1336
+ // Convert LaTeX-style superscripts: $^{text}$ or $^text$ to <sup>text</sup>
1337
+ html = html.replace(/\$\s*\^\s*\{([^}]+)\}\s*\$/g, '<sup>$1</sup>');
1338
+ html = html.replace(/\$\s*\^\s*([^\s$<>]+)\s*\$/g, '<sup>$1</sup>');
1339
+
1340
+ // Convert LaTeX-style subscripts: $_{text}$ or $_text$ to <sub>text</sub>
1341
+ html = html.replace(/\$\s*_\s*\{([^}]+)\}\s*\$/g, '<sub>$1</sub>');
1342
+ html = html.replace(/\$\s*_\s*([^\s$<>]+)\s*\$/g, '<sub>$1</sub>');
1343
+
1344
+ // Split by HTML tags to preserve existing HTML (like tables)
1345
+ // Process markdown only in non-HTML sections
1346
+
1347
+ // First, protect existing HTML blocks (tables, etc.)
1348
+ const htmlBlocks = [];
1349
+ let htmlBlockIndex = 0;
1350
+
1351
+ // Extract and protect HTML table blocks
1352
+ html = html.replace(/<table[\s\S]*?<\/table>/gi, (match) => {
1353
+ const placeholder = `__HTML_BLOCK_${htmlBlockIndex}__`;
1354
+ htmlBlocks[htmlBlockIndex] = match;
1355
+ htmlBlockIndex++;
1356
+ return placeholder;
1357
+ });
1358
+
1359
+ // Convert markdown headers (only if not inside HTML)
1360
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
1361
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
1362
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
1363
+
1364
+ // Convert markdown bold/italic (but not inside HTML tags)
1365
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1366
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
1367
+
1368
+ // Convert markdown links
1369
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1370
+
1371
+ // Convert line breaks to paragraphs (but preserve structure around HTML blocks)
1372
+ const parts = html.split(/(__HTML_BLOCK_\d+__)/);
1373
+ const processedParts = parts.map((part, index) => {
1374
+ if (part.match(/^__HTML_BLOCK_\d+__$/)) {
1375
+ // Restore HTML block
1376
+ const blockIndex = parseInt(part.match(/\d+/)[0]);
1377
+ return htmlBlocks[blockIndex];
1378
+ } else {
1379
+ // Process markdown in this part
1380
+ let processed = part;
1381
+
1382
+ // Convert double line breaks to paragraph breaks
1383
+ processed = processed.replace(/\n\n+/g, '</p><p>');
1384
+ // Convert single line breaks to <br> (but not if already in a tag)
1385
+ processed = processed.replace(/([^\n>])\n([^\n<])/g, '$1<br>$2');
1386
+
1387
+ // Wrap in paragraph if there's content
1388
+ if (processed.trim() && !processed.trim().startsWith('<')) {
1389
+ processed = '<p>' + processed + '</p>';
1390
+ }
1391
+
1392
+ return processed;
1393
+ }
1394
+ });
1395
+
1396
+ html = processedParts.join('');
1397
+
1398
+ // Process LaTeX notation in restored HTML blocks (tables) as well
1399
+ // This handles any LaTeX that might be in table cells
1400
+ html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*\^\s*\{([^}]+)\}\s*\$([^<]*)(<\/td>|<\/th>)/gi,
1401
+ (match, openTag, before, supText, after, closeTag) => {
1402
+ return openTag + before + '<sup>' + supText + '</sup>' + after + closeTag;
1403
+ });
1404
+ html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*\^\s*([^\s$<>]+)\s*\$([^<]*)(<\/td>|<\/th>)/gi,
1405
+ (match, openTag, before, supText, after, closeTag) => {
1406
+ return openTag + before + '<sup>' + supText + '</sup>' + after + closeTag;
1407
+ });
1408
+ html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*_\s*\{([^}]+)\}\s*\$([^<]*)(<\/td>|<\/th>)/gi,
1409
+ (match, openTag, before, subText, after, closeTag) => {
1410
+ return openTag + before + '<sub>' + subText + '</sub>' + after + closeTag;
1411
+ });
1412
+ html = html.replace(/(<td[^>]*>|<th[^>]*>)([^<]*)\$\s*_\s*([^\s$<>]+)\s*\$([^<]*)(<\/td>|<\/th>)/gi,
1413
+ (match, openTag, before, subText, after, closeTag) => {
1414
+ return openTag + before + '<sub>' + subText + '</sub>' + after + closeTag;
1415
+ });
1416
+
1417
+ // Clean up empty paragraphs and fix paragraph structure
1418
+ html = html.replace(/<p><\/p>/g, '');
1419
+ html = html.replace(/<p>\s*<br>\s*<\/p>/g, '');
1420
+ html = html.replace(/<p>\s*<\/p>/g, '');
1421
+
1422
+ // Ensure proper spacing around HTML blocks
1423
+ html = html.replace(/(<\/table>)\s*(<h[1-3])/g, '$1</p><p>$2');
1424
+ html = html.replace(/(<\/h[1-3]>)\s*(<table)/g, '$1<p>$2');
1425
+ html = html.replace(/(<\/table>)\s*(<p>)/g, '$1$2');
1426
+
1427
+ return html;
1428
+ }
1429
+
1430
+ // Mock extracted data
1431
+ const mockData = {
1432
+ document: {
1433
+ type: "Invoice",
1434
+ confidence: 0.98,
1435
+ },
1436
+ vendor: {
1437
+ name: "Acme Corporation",
1438
+ address: "123 Business Ave, Suite 400",
1439
+ city: "San Francisco",
1440
+ state: "CA",
1441
+ zip: "94102",
1442
+ phone: "+1 (555) 123-4567",
1443
+ },
1444
+ invoice: {
1445
+ number: "INV-2024-0847",
1446
+ date: "2024-01-15",
1447
+ due_date: "2024-02-14",
1448
+ po_number: "PO-9823",
1449
+ },
1450
+ items: [
1451
+ { description: "Professional Services", quantity: 40, unit_price: 150.0, total: 6000.0 },
1452
+ { description: "Software License", quantity: 5, unit_price: 299.99, total: 1499.95 },
1453
+ { description: "Support Package", quantity: 1, unit_price: 500.0, total: 500.0 },
1454
+ ],
1455
+ totals: {
1456
+ subtotal: 7999.95,
1457
+ tax_rate: 0.0875,
1458
+ tax_amount: 699.99,
1459
+ total: 8699.94,
1460
+ },
1461
+ };
1462
+
1463
+ const mockXML = `<?xml version="1.0" encoding="UTF-8"?>
1464
+ <extraction>
1465
+ <document type="Invoice" confidence="0.98"/>
1466
+ <vendor>
1467
+ <name>Acme Corporation</name>
1468
+ <address>123 Business Ave, Suite 400</address>
1469
+ <city>San Francisco</city>
1470
+ <state>CA</state>
1471
+ <zip>94102</zip>
1472
+ </vendor>
1473
+ <invoice>
1474
+ <number>INV-2024-0847</number>
1475
+ <date>2024-01-15</date>
1476
+ <due_date>2024-02-14</due_date>
1477
+ </invoice>
1478
+ <items>
1479
+ <item>
1480
+ <description>Professional Services</description>
1481
+ <quantity>40</quantity>
1482
+ <total>6000.00</total>
1483
+ </item>
1484
+ </items>
1485
+ <totals>
1486
+ <subtotal>7999.95</subtotal>
1487
+ <tax>699.99</tax>
1488
+ <total>8699.94</total>
1489
+ </totals>
1490
+ </extraction>`;
1491
+
1492
+ const mockText = `INVOICE
1493
+
1494
+ ACME CORPORATION
1495
+ 123 Business Ave, Suite 400
1496
+ San Francisco, CA 94102
1497
+ Phone: +1 (555) 123-4567
1498
+
1499
+ Invoice Number: INV-2024-0847
1500
+ Invoice Date: January 15, 2024
1501
+ Due Date: February 14, 2024
1502
+ PO Number: PO-9823
1503
+
1504
+ BILL TO:
1505
+ Customer Name
1506
+ 456 Client Street
1507
+ New York, NY 10001
1508
+
1509
+ ITEMS:
1510
+ ─────────────────────────────────────────────────────────
1511
+ Description Qty Unit Price Total
1512
+ ─────────────────────────────────────────────────────────
1513
+ Professional Services 40 $150.00 $6,000.00
1514
+ Software License 5 $299.99 $1,499.95
1515
+ Support Package 1 $500.00 $500.00
1516
+ ─────────────────────────────────────────────────────────
1517
+
1518
+ Subtotal: $7,999.95
1519
+ Tax (8.75%): $699.99
1520
+ ─────────────────────────
1521
+ TOTAL: $8,699.94
1522
+
1523
+ Payment Terms: Net 30
1524
+ Thank you for your business!`;
1525
+
1526
+ // Helper function to convert object to XML
1527
+ // Prepare fields for JSON/XML output - remove duplicates and restructure
1528
+ function prepareFieldsForOutput(fields, format = "json") {
1529
+ if (!fields || typeof fields !== "object") {
1530
+ return fields;
1531
+ }
1532
+
1533
+ const output = { ...fields };
1534
+
1535
+ // Extract Fields from root level if it exists
1536
+ const rootFields = output.Fields;
1537
+ // Remove Fields from output temporarily (will be added back at top)
1538
+ delete output.Fields;
1539
+
1540
+ // Remove full_text from top-level if pages array exists (to avoid duplication)
1541
+ if (output.pages && Array.isArray(output.pages) && output.pages.length > 0) {
1542
+ delete output.full_text;
1543
+
1544
+ // Clean up each page: remove full_text from page.fields (it duplicates page.text)
1545
+ output.pages = output.pages.map(page => {
1546
+ const cleanedPage = { ...page };
1547
+ if (cleanedPage.fields && typeof cleanedPage.fields === "object") {
1548
+ const cleanedFields = { ...cleanedPage.fields };
1549
+ // Remove full_text from page fields (duplicates page.text)
1550
+ delete cleanedFields.full_text;
1551
+ cleanedPage.fields = cleanedFields;
1552
+ }
1553
+ return cleanedPage;
1554
+ });
1555
+ }
1556
+
1557
+ // For JSON and XML: restructure pages into separate top-level fields (page_1, page_2, etc.)
1558
+ if ((format === "json" || format === "xml") && output.pages && Array.isArray(output.pages)) {
1559
+ // Get top-level field keys (these are merged from all pages - avoid duplicating in page fields)
1560
+ const topLevelKeys = new Set(Object.keys(output).filter(k => k !== "pages" && k !== "full_text" && k !== "Fields"));
1561
+
1562
+ output.pages.forEach((page, idx) => {
1563
+ const pageNum = page.page_number || idx + 1;
1564
+ const pageFields = page.fields || {};
1565
+
1566
+ // Remove duplicate fields from page.fields:
1567
+ // 1. Remove full_text (duplicates page.text)
1568
+ // 2. Remove fields that match top-level fields (already shown at root)
1569
+ const cleanedPageFields = {};
1570
+ for (const [key, value] of Object.entries(pageFields)) {
1571
+ // Skip full_text and fields that match top-level exactly
1572
+ if (key !== "full_text" && (!topLevelKeys.has(key) || (value !== output[key]))) {
1573
+ cleanedPageFields[key] = value;
1574
+ }
1575
+ }
1576
+
1577
+ const pageObj = {
1578
+ text: page.text || "",
1579
+ confidence: page.confidence || 0,
1580
+ doc_type: page.doc_type || "other"
1581
+ };
1582
+
1583
+ // Add table and footer_notes if they exist
1584
+ if (page.table && Array.isArray(page.table) && page.table.length > 0) {
1585
+ pageObj.table = page.table;
1586
+ }
1587
+ if (page.footer_notes && Array.isArray(page.footer_notes) && page.footer_notes.length > 0) {
1588
+ pageObj.footer_notes = page.footer_notes;
1589
+ }
1590
+
1591
+ // Only add fields if there are unique page-specific fields
1592
+ if (Object.keys(cleanedPageFields).length > 0) {
1593
+ pageObj.fields = cleanedPageFields;
1594
+ }
1595
+
1596
+ output[`page_${pageNum}`] = pageObj;
1597
+ });
1598
+ // Remove pages array - we now have page_1, page_2, etc. as separate fields
1599
+ delete output.pages;
1600
+ }
1601
+
1602
+ // Handle page_X structure (from backend) - remove Fields from page objects if they exist
1603
+ if (output && typeof output === "object") {
1604
+ const pageKeys = Object.keys(output).filter(k => k.startsWith("page_"));
1605
+ for (const pageKey of pageKeys) {
1606
+ const pageData = output[pageKey];
1607
+ if (pageData && typeof pageData === "object") {
1608
+ // Remove Fields from page objects (it's now at root level)
1609
+ delete pageData.Fields;
1610
+ delete pageData.metadata;
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ // Rebuild output with Fields at the top (only if it exists and is not empty)
1616
+ const finalOutput = {};
1617
+ if (rootFields && typeof rootFields === "object" && Object.keys(rootFields).length > 0) {
1618
+ finalOutput.Fields = rootFields;
1619
+ }
1620
+
1621
+ // Add all other keys
1622
+ Object.keys(output).forEach(key => {
1623
+ finalOutput[key] = output[key];
1624
+ });
1625
+
1626
+ return finalOutput;
1627
+ }
1628
+
1629
+ function objectToXML(obj, rootName = "extraction") {
1630
+ // Prepare fields - remove full_text if pages exist
1631
+ const preparedObj = prepareFieldsForOutput(obj, "xml");
1632
+
1633
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}>\n`;
1634
+
1635
+ const convert = (obj, indent = " ") => {
1636
+ for (const [key, value] of Object.entries(obj)) {
1637
+ if (value === null || value === undefined) continue;
1638
+
1639
+ // Skip full_text if pages exist (already handled in prepareFieldsForOutput)
1640
+ if (key === "full_text" && obj.pages && Array.isArray(obj.pages) && obj.pages.length > 0) {
1641
+ continue;
1642
+ }
1643
+
1644
+ if (Array.isArray(value)) {
1645
+ value.forEach((item) => {
1646
+ xml += `${indent}<${key}>\n`;
1647
+ if (typeof item === "object") {
1648
+ convert(item, indent + " ");
1649
+ } else {
1650
+ xml += `${indent} ${escapeXML(String(item))}\n`;
1651
+ }
1652
+ xml += `${indent}</${key}>\n`;
1653
+ });
1654
+ } else if (typeof value === "object") {
1655
+ xml += `${indent}<${key}>\n`;
1656
+ convert(value, indent + " ");
1657
+ xml += `${indent}</${key}>\n`;
1658
+ } else {
1659
+ xml += `${indent}<${key}>${escapeXML(String(value))}</${key}>\n`;
1660
+ }
1661
+ }
1662
+ };
1663
+
1664
+ convert(preparedObj);
1665
+ xml += `</${rootName}>`;
1666
+ return xml;
1667
+ }
1668
+
1669
+ function escapeXML(str) {
1670
+ return str
1671
+ .replace(/&/g, "&amp;")
1672
+ .replace(/</g, "&lt;")
1673
+ .replace(/>/g, "&gt;")
1674
+ .replace(/"/g, "&quot;")
1675
+ .replace(/'/g, "&apos;");
1676
+ }
1677
+
1678
+ // Helper function to extract text from page structure
1679
+ function extractTextFromFields(fields) {
1680
+ if (!fields || typeof fields !== "object") {
1681
+ return "";
1682
+ }
1683
+
1684
+ // Check for page_X structure first (preferred format)
1685
+ const pageKeys = Object.keys(fields).filter(key => key.startsWith("page_"));
1686
+ if (pageKeys.length > 0) {
1687
+ // Get text from first page (or combine all pages)
1688
+ const pageTexts = pageKeys.map(key => {
1689
+ const page = fields[key];
1690
+ if (page && page.text) {
1691
+ return page.text;
1692
+ }
1693
+ return "";
1694
+ }).filter(text => text);
1695
+
1696
+ if (pageTexts.length > 0) {
1697
+ return pageTexts.join("\n\n");
1698
+ }
1699
+ }
1700
+
1701
+ // Fallback to full_text
1702
+ if (fields.full_text) {
1703
+ return fields.full_text;
1704
+ }
1705
+
1706
+ return "";
1707
+ }
1708
+
1709
+ // Helper function to format fields as readable text
1710
+ function fieldsToText(fields) {
1711
+ if (!fields || typeof fields !== "object") {
1712
+ return "No data extracted.";
1713
+ }
1714
+
1715
+ // Extract text from page structure or full_text
1716
+ const extractedText = extractTextFromFields(fields);
1717
+
1718
+ if (extractedText) {
1719
+ return extractedText;
1720
+
1721
+ // Don't show pages array separately if full_text already contains page markers
1722
+ // (full_text from backend already includes "=== PAGE 1 ===" etc.)
1723
+ const hasPageMarkers = fields.full_text.includes("=== PAGE") || fields.full_text.includes("--- Page");
1724
+
1725
+ // Only show pages array if full_text doesn't already have page breakdown
1726
+ if (!hasPageMarkers && fields.pages && Array.isArray(fields.pages)) {
1727
+ text += "\n\n=== TEXT BY PAGE ===\n\n";
1728
+ fields.pages.forEach((page, idx) => {
1729
+ text += `--- Page ${page.page_number || idx + 1} ---\n`;
1730
+ text += page.text || "";
1731
+ text += "\n\n";
1732
+ });
1733
+ }
1734
+
1735
+ // Then show other structured fields
1736
+ const otherFields = { ...fields };
1737
+ delete otherFields.full_text;
1738
+ delete otherFields.pages;
1739
+
1740
+ if (Object.keys(otherFields).length > 0) {
1741
+ text += "\n\n=== STRUCTURED FIELDS ===\n\n";
1742
+ const formatValue = (key, value, indent = "") => {
1743
+ if (Array.isArray(value)) {
1744
+ text += `${indent}${key}:\n`;
1745
+ value.forEach((item, idx) => {
1746
+ if (typeof item === "object") {
1747
+ text += `${indent} Item ${idx + 1}:\n`;
1748
+ Object.entries(item).forEach(([k, v]) => formatValue(k, v, indent + " "));
1749
+ } else {
1750
+ text += `${indent} - ${item}\n`;
1751
+ }
1752
+ });
1753
+ } else if (typeof value === "object" && value !== null) {
1754
+ text += `${indent}${key}:\n`;
1755
+ Object.entries(value).forEach(([k, v]) => formatValue(k, v, indent + " "));
1756
+ } else {
1757
+ text += `${indent}${key}: ${value}\n`;
1758
+ }
1759
+ };
1760
+
1761
+ Object.entries(otherFields).forEach(([key, value]) => {
1762
+ formatValue(key, value);
1763
+ text += "\n";
1764
+ });
1765
+ }
1766
+
1767
+ return text.trim();
1768
+ }
1769
+
1770
+ // Fallback: format all fields normally
1771
+ let text = "";
1772
+ const formatValue = (key, value, indent = "") => {
1773
+ if (Array.isArray(value)) {
1774
+ text += `${indent}${key}:\n`;
1775
+ value.forEach((item, idx) => {
1776
+ if (typeof item === "object") {
1777
+ text += `${indent} Item ${idx + 1}:\n`;
1778
+ Object.entries(item).forEach(([k, v]) => formatValue(k, v, indent + " "));
1779
+ } else {
1780
+ text += `${indent} - ${item}\n`;
1781
+ }
1782
+ });
1783
+ } else if (typeof value === "object" && value !== null) {
1784
+ text += `${indent}${key}:\n`;
1785
+ Object.entries(value).forEach(([k, v]) => formatValue(k, v, indent + " "));
1786
+ } else {
1787
+ text += `${indent}${key}: ${value}\n`;
1788
+ }
1789
+ };
1790
+
1791
+ Object.entries(fields).forEach(([key, value]) => {
1792
+ formatValue(key, value);
1793
+ text += "\n";
1794
+ });
1795
+
1796
+ return text.trim() || "No data extracted.";
1797
+ }
1798
+
1799
+ export default function ExtractionOutput({ hasFile, isProcessing, isComplete, extractionResult, onNewUpload }) {
1800
+ const [activeTab, setActiveTab] = useState("json");
1801
+ const [copied, setCopied] = useState(false);
1802
+ const [statusMessage, setStatusMessage] = useState("Preparing document...");
1803
+
1804
+ // Get fields from extraction result, default to empty object
1805
+ const fields = extractionResult?.fields || {};
1806
+ const confidence = extractionResult?.confidence || 0;
1807
+ const fieldsExtracted = extractionResult?.fieldsExtracted || 0;
1808
+ const totalTime = extractionResult?.totalTime || 0;
1809
+
1810
+ // Dynamic status messages that rotate during processing
1811
+ const statusMessages = [
1812
+ "Preparing document...",
1813
+ "Converting pages to images...",
1814
+ "Visual Reasoning...",
1815
+ "Reading text from document...",
1816
+ "Identifying document structure...",
1817
+ "Extracting tables and data...",
1818
+ "Analyzing content...",
1819
+ "Processing pages...",
1820
+ "Organizing extracted information...",
1821
+ "Finalizing results...",
1822
+ ];
1823
+
1824
+ // Rotate status messages during processing
1825
+ const messageIndexRef = useRef(0);
1826
+
1827
+ useEffect(() => {
1828
+ if (!isProcessing) {
1829
+ setStatusMessage("Analyzing document structure");
1830
+ messageIndexRef.current = 0;
1831
+ return;
1832
+ }
1833
+
1834
+ setStatusMessage(statusMessages[0]);
1835
+ messageIndexRef.current = 0;
1836
+
1837
+ const interval = setInterval(() => {
1838
+ messageIndexRef.current = (messageIndexRef.current + 1) % statusMessages.length;
1839
+ setStatusMessage(statusMessages[messageIndexRef.current]);
1840
+ }, 2500); // Change message every 2.5 seconds
1841
+
1842
+ return () => clearInterval(interval);
1843
+ }, [isProcessing]);
1844
+
1845
+ // Initialize expanded sections based on available fields
1846
+ const [expandedSections, setExpandedSections] = useState(() =>
1847
+ Object.keys(fields).slice(0, 5) // Expand first 5 sections by default
1848
+ );
1849
+
1850
+ // Helper function to convert HTML to formatted plain text with layout preserved
1851
+ const htmlToFormattedText = (html) => {
1852
+ if (!html) return "";
1853
+
1854
+ // Create a temporary div to parse HTML
1855
+ const tempDiv = document.createElement("div");
1856
+ tempDiv.innerHTML = html;
1857
+
1858
+ let text = "";
1859
+
1860
+ // Process each element
1861
+ const processNode = (node) => {
1862
+ if (node.nodeType === Node.TEXT_NODE) {
1863
+ return node.textContent;
1864
+ }
1865
+
1866
+ if (node.nodeType !== Node.ELEMENT_NODE) {
1867
+ return "";
1868
+ }
1869
+
1870
+ const tagName = node.tagName?.toLowerCase();
1871
+ const children = Array.from(node.childNodes);
1872
+
1873
+ switch (tagName) {
1874
+ case "h1":
1875
+ return "\n\n" + processChildren(children).trim() + "\n\n";
1876
+ case "h2":
1877
+ return "\n\n" + processChildren(children).trim() + "\n\n";
1878
+ case "h3":
1879
+ return "\n" + processChildren(children).trim() + "\n";
1880
+ case "p":
1881
+ return processChildren(children) + "\n\n";
1882
+ case "br":
1883
+ return "\n";
1884
+ case "strong":
1885
+ case "b":
1886
+ return processChildren(children);
1887
+ case "em":
1888
+ case "i":
1889
+ return processChildren(children);
1890
+ case "sup":
1891
+ return processChildren(children);
1892
+ case "sub":
1893
+ return processChildren(children);
1894
+ case "table":
1895
+ return "\n" + processTable(node) + "\n\n";
1896
+ case "ul":
1897
+ case "ol":
1898
+ return "\n" + processList(node) + "\n\n";
1899
+ case "li":
1900
+ return " • " + processChildren(children).trim() + "\n";
1901
+ default:
1902
+ return processChildren(children);
1903
+ }
1904
+ };
1905
+
1906
+ const processChildren = (children) => {
1907
+ return children.map(processNode).join("");
1908
+ };
1909
+
1910
+ const processTable = (table) => {
1911
+ let tableText = "";
1912
+ const rows = table.querySelectorAll("tr");
1913
+
1914
+ if (rows.length === 0) return "";
1915
+
1916
+ // First pass: calculate column widths
1917
+ const allRows = Array.from(rows);
1918
+ const columnCount = Math.max(...allRows.map(row => row.querySelectorAll("td, th").length));
1919
+ const columnWidths = new Array(columnCount).fill(0);
1920
+
1921
+ allRows.forEach(row => {
1922
+ const cells = row.querySelectorAll("td, th");
1923
+ cells.forEach((cell, colIndex) => {
1924
+ const cellText = processChildren(Array.from(cell.childNodes)).trim().replace(/\s+/g, " ");
1925
+ columnWidths[colIndex] = Math.max(columnWidths[colIndex] || 0, cellText.length, 10);
1926
+ });
1927
+ });
1928
+
1929
+ // Second pass: format rows
1930
+ allRows.forEach((row, rowIndex) => {
1931
+ const cells = row.querySelectorAll("td, th");
1932
+ const cellTexts = Array.from(cells).map(cell => {
1933
+ let cellContent = processChildren(Array.from(cell.childNodes)).trim();
1934
+ cellContent = cellContent.replace(/\s+/g, " ");
1935
+ return cellContent;
1936
+ });
1937
+
1938
+ // Pad cells to column widths
1939
+ const paddedCells = cellTexts.map((text, i) => {
1940
+ const width = columnWidths[i] || 10;
1941
+ return text.padEnd(width);
1942
+ });
1943
+
1944
+ tableText += paddedCells.join(" | ") + "\n";
1945
+
1946
+ // Add separator after header row
1947
+ if (rowIndex === 0 && row.querySelector("th")) {
1948
+ tableText += columnWidths.map(w => "-".repeat(w)).join("-|-") + "\n";
1949
+ }
1950
+ });
1951
+
1952
+ return tableText;
1953
+ };
1954
+
1955
+ const processList = (list) => {
1956
+ const items = list.querySelectorAll("li");
1957
+ return Array.from(items).map(item => {
1958
+ return " • " + processChildren(Array.from(item.childNodes)).trim();
1959
+ }).join("\n");
1960
+ };
1961
+
1962
+ text = processChildren(Array.from(tempDiv.childNodes));
1963
+
1964
+ // Clean up extra newlines
1965
+ text = text.replace(/\n{3,}/g, "\n\n");
1966
+ text = text.trim();
1967
+
1968
+ return text;
1969
+ };
1970
+
1971
+ const handleCopy = () => {
1972
+ let content = "";
1973
+ if (activeTab === "json") {
1974
+ const preparedFields = prepareFieldsForOutput(fields, "json");
1975
+ content = JSON.stringify(preparedFields, null, 2);
1976
+ } else if (activeTab === "xml") {
1977
+ content = objectToXML(fields);
1978
+ } else {
1979
+ // For text tab, get the formatted HTML and convert to plain text with layout
1980
+ const textContent = extractTextFromFields(fields);
1981
+ const htmlContent = renderMarkdownToHTML(textContent);
1982
+ content = htmlToFormattedText(htmlContent);
1983
+ }
1984
+
1985
+ navigator.clipboard.writeText(content);
1986
+ setCopied(true);
1987
+ setTimeout(() => setCopied(false), 2000);
1988
+ };
1989
+
1990
+ // Get prepared fields for display
1991
+ const preparedFields = React.useMemo(() => {
1992
+ return prepareFieldsForOutput(fields, "json");
1993
+ }, [fields]);
1994
+
1995
+ // Update expanded sections when fields change
1996
+ React.useEffect(() => {
1997
+ if (extractionResult?.fields) {
1998
+ setExpandedSections(Object.keys(extractionResult.fields).slice(0, 5));
1999
+ }
2000
+ }, [extractionResult]);
2001
+
2002
+ const toggleSection = (section) => {
2003
+ setExpandedSections((prev) =>
2004
+ prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
2005
+ );
2006
+ };
2007
+
2008
+ const renderValue = (value) => {
2009
+ if (typeof value === "number") {
2010
+ return <span className="text-amber-600">{value}</span>;
2011
+ }
2012
+ if (typeof value === "string") {
2013
+ return <span className="text-emerald-600">"{value}"</span>;
2014
+ }
2015
+ return String(value);
2016
+ };
2017
+
2018
+ const renderSection = (key, value, level = 0) => {
2019
+ const isExpanded = expandedSections.includes(key);
2020
+ const isObject = typeof value === "object" && value !== null;
2021
+ const isArray = Array.isArray(value);
2022
+
2023
+ if (!isObject) {
2024
+ return (
2025
+ <div
2026
+ key={key}
2027
+ className="flex items-start gap-2 py-1"
2028
+ style={{ paddingLeft: level * 16 }}
2029
+ >
2030
+ <span className="text-violet-500">"{key}"</span>
2031
+ <span className="text-slate-400">:</span>
2032
+ {renderValue(value)}
2033
+ </div>
2034
+ );
2035
+ }
2036
+
2037
+ return (
2038
+ <div key={key}>
2039
+ <button
2040
+ onClick={() => toggleSection(key)}
2041
+ className="flex items-center gap-2 py-1 hover:bg-slate-50 w-full text-left rounded"
2042
+ style={{ paddingLeft: level * 16 }}
2043
+ >
2044
+ <ChevronDown
2045
+ className={cn(
2046
+ "h-3 w-3 text-slate-400 transition-transform",
2047
+ !isExpanded && "-rotate-90"
2048
+ )}
2049
+ />
2050
+ <span className="text-violet-500">"{key}"</span>
2051
+ <span className="text-slate-400">:</span>
2052
+ <span className="text-slate-400">{isArray ? "[" : "{"}</span>
2053
+ {!isExpanded && (
2054
+ <span className="text-slate-300 text-xs">
2055
+ {isArray ? `${value.length} items` : `${Object.keys(value).length} fields`}
2056
+ </span>
2057
+ )}
2058
+ </button>
2059
+ <AnimatePresence>
2060
+ {isExpanded && (
2061
+ <motion.div
2062
+ initial={{ height: 0, opacity: 0 }}
2063
+ animate={{ height: "auto", opacity: 1 }}
2064
+ exit={{ height: 0, opacity: 0 }}
2065
+ transition={{ duration: 0.2 }}
2066
+ className="overflow-hidden"
2067
+ >
2068
+ {isArray ? (
2069
+ value.map((item, idx) => (
2070
+ <div key={idx} className="border-l border-slate-100 ml-4">
2071
+ {Object.entries(item).map(([k, v]) => renderSection(k, v, level + 2))}
2072
+ {idx < value.length - 1 && <div className="h-2" />}
2073
+ </div>
2074
+ ))
2075
+ ) : (
2076
+ Object.entries(value).map(([k, v]) => renderSection(k, v, level + 1))
2077
+ )}
2078
+ <div style={{ paddingLeft: level * 16 }} className="text-slate-400">
2079
+ {isArray ? "]" : "}"}
2080
+ </div>
2081
+ </motion.div>
2082
+ )}
2083
+ </AnimatePresence>
2084
+ </div>
2085
+ );
2086
+ };
2087
+
2088
+ return (
2089
+ <div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
2090
+ {/* Header */}
2091
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
2092
+ <div className="flex items-center gap-3">
2093
+ <div className="h-8 w-8 rounded-lg bg-emerald-50 flex items-center justify-center">
2094
+ <Code2 className="h-4 w-4 text-emerald-600" />
2095
+ </div>
2096
+ <div>
2097
+ <h3 className="font-semibold text-slate-800 text-sm">Extracted Data</h3>
2098
+ <p className="text-xs text-slate-400">
2099
+ {isComplete
2100
+ ? `${fieldsExtracted} field${fieldsExtracted !== 1 ? 's' : ''} extracted`
2101
+ : "Waiting for extraction"}
2102
+ </p>
2103
+ </div>
2104
+ {isComplete && onNewUpload && (
2105
+ <Button
2106
+ variant="ghost"
2107
+ size="sm"
2108
+ onClick={onNewUpload}
2109
+ className="h-8 ml-auto text-xs gap-1.5 text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50"
2110
+ title="Upload new document"
2111
+ >
2112
+ <Upload className="h-3.5 w-3.5" />
2113
+ New
2114
+ </Button>
2115
+ )}
2116
+ </div>
2117
+
2118
+ {isComplete && (
2119
+ <div className="flex items-center gap-2">
2120
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
2121
+ <TabsList className="h-8 bg-slate-100 p-0.5">
2122
+ <TabsTrigger value="text" className="h-7 text-xs gap-1.5">
2123
+ <FileText className="h-3 w-3" />
2124
+ Text
2125
+ </TabsTrigger>
2126
+ <TabsTrigger value="json" className="h-7 text-xs gap-1.5">
2127
+ <Braces className="h-3 w-3" />
2128
+ JSON
2129
+ </TabsTrigger>
2130
+ <TabsTrigger value="xml" className="h-7 text-xs gap-1.5">
2131
+ <FileCode2 className="h-3 w-3" />
2132
+ XML
2133
+ </TabsTrigger>
2134
+ </TabsList>
2135
+ </Tabs>
2136
+ <Button
2137
+ variant="ghost"
2138
+ size="sm"
2139
+ onClick={handleCopy}
2140
+ className="h-8 text-xs gap-1.5"
2141
+ >
2142
+ {copied ? (
2143
+ <>
2144
+ <Check className="h-3 w-3 text-emerald-500" />
2145
+ Copied
2146
+ </>
2147
+ ) : (
2148
+ <>
2149
+ <Copy className="h-3 w-3" />
2150
+ Copy
2151
+ </>
2152
+ )}
2153
+ </Button>
2154
+ </div>
2155
+ )}
2156
+ </div>
2157
+
2158
+ {/* Output Area */}
2159
+ <div className="flex-1 overflow-auto">
2160
+ {!hasFile ? (
2161
+ <div className="h-full flex items-center justify-center p-6">
2162
+ <div className="text-center">
2163
+ <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
2164
+ <Code2 className="h-10 w-10 text-slate-300" />
2165
+ </div>
2166
+ <p className="text-slate-400 text-sm">Extracted data will appear here</p>
2167
+ </div>
2168
+ </div>
2169
+ ) : isProcessing ? (
2170
+ <div className="h-full flex items-center justify-center p-6">
2171
+ <div className="text-center">
2172
+ <motion.div
2173
+ animate={{ rotate: 360 }}
2174
+ transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
2175
+ 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"
2176
+ >
2177
+ <Sparkles className="h-8 w-8 text-indigo-500" />
2178
+ </motion.div>
2179
+ <p className="text-slate-700 font-medium mb-1">Extracting data...</p>
2180
+ <p className="text-slate-400 text-sm">{statusMessage}</p>
2181
+
2182
+ <div className="mt-6 flex items-center justify-center gap-1">
2183
+ {[0, 1, 2].map((i) => (
2184
+ <motion.div
2185
+ key={i}
2186
+ animate={{ scale: [1, 1.2, 1] }}
2187
+ transition={{
2188
+ duration: 0.6,
2189
+ repeat: Infinity,
2190
+ delay: i * 0.2,
2191
+ }}
2192
+ className="h-2 w-2 rounded-full bg-indigo-400"
2193
+ />
2194
+ ))}
2195
+ </div>
2196
+ </div>
2197
+ </div>
2198
+ ) : isComplete && Object.keys(fields).length === 0 ? (
2199
+ <div className="h-full flex items-center justify-center p-6">
2200
+ <div className="text-center">
2201
+ <div className="h-20 w-20 mx-auto rounded-2xl bg-amber-100 flex items-center justify-center mb-4">
2202
+ <Code2 className="h-10 w-10 text-amber-600" />
2203
+ </div>
2204
+ <p className="text-slate-600 font-medium mb-1">No data extracted</p>
2205
+ <p className="text-slate-400 text-sm">The document may not contain extractable fields</p>
2206
+ </div>
2207
+ </div>
2208
+ ) : (
2209
+ <div className="p-4 font-mono text-sm">
2210
+ {activeTab === "text" ? (
2211
+ <div
2212
+ className="text-sm text-slate-700 leading-relaxed"
2213
+ style={{
2214
+ fontFamily: 'system-ui, -apple-system, sans-serif'
2215
+ }}
2216
+ >
2217
+ <div
2218
+ className="markdown-content"
2219
+ dangerouslySetInnerHTML={{ __html: renderMarkdownToHTML(fieldsToText(fields)) }}
2220
+ style={{
2221
+ lineHeight: '1.6'
2222
+ }}
2223
+ />
2224
+ <style>{`
2225
+ .markdown-content h1 {
2226
+ font-size: 1.5rem;
2227
+ font-weight: 700;
2228
+ color: #0f172a;
2229
+ margin-top: 1.5rem;
2230
+ margin-bottom: 1rem;
2231
+ line-height: 1.3;
2232
+ }
2233
+ .markdown-content h2 {
2234
+ font-size: 1.25rem;
2235
+ font-weight: 600;
2236
+ color: #0f172a;
2237
+ margin-top: 1.25rem;
2238
+ margin-bottom: 0.75rem;
2239
+ line-height: 1.3;
2240
+ }
2241
+ .markdown-content h3 {
2242
+ font-size: 1.125rem;
2243
+ font-weight: 600;
2244
+ color: #1e293b;
2245
+ margin-top: 1rem;
2246
+ margin-bottom: 0.5rem;
2247
+ line-height: 1.3;
2248
+ }
2249
+ .markdown-content p {
2250
+ margin-top: 0.75rem;
2251
+ margin-bottom: 0.75rem;
2252
+ color: #334155;
2253
+ }
2254
+ .markdown-content table {
2255
+ width: 100%;
2256
+ border-collapse: collapse;
2257
+ margin: 1.5rem 0;
2258
+ font-size: 0.875rem;
2259
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
2260
+ }
2261
+ .markdown-content table caption {
2262
+ font-weight: 600;
2263
+ margin-bottom: 0.5rem;
2264
+ text-align: left;
2265
+ }
2266
+ .markdown-content table th {
2267
+ background-color: #f8fafc;
2268
+ border: 1px solid #cbd5e1;
2269
+ padding: 0.75rem;
2270
+ text-align: left;
2271
+ font-weight: 600;
2272
+ color: #0f172a;
2273
+ }
2274
+ .markdown-content table td {
2275
+ border: 1px solid #cbd5e1;
2276
+ padding: 0.75rem;
2277
+ color: #334155;
2278
+ }
2279
+ .markdown-content table tr:nth-child(even) {
2280
+ background-color: #f8fafc;
2281
+ }
2282
+ .markdown-content table tr:hover {
2283
+ background-color: #f1f5f9;
2284
+ }
2285
+ .markdown-content strong {
2286
+ font-weight: 600;
2287
+ color: #0f172a;
2288
+ }
2289
+ .markdown-content em {
2290
+ font-style: italic;
2291
+ }
2292
+ .markdown-content a {
2293
+ color: #4f46e5;
2294
+ text-decoration: underline;
2295
+ }
2296
+ .markdown-content a:hover {
2297
+ color: #4338ca;
2298
+ }
2299
+ .markdown-content sup {
2300
+ font-size: 0.75em;
2301
+ vertical-align: super;
2302
+ line-height: 0;
2303
+ position: relative;
2304
+ top: -0.5em;
2305
+ }
2306
+ .markdown-content sub {
2307
+ font-size: 0.75em;
2308
+ vertical-align: sub;
2309
+ line-height: 0;
2310
+ position: relative;
2311
+ bottom: -0.25em;
2312
+ }
2313
+ .markdown-content ul, .markdown-content ol {
2314
+ margin: 0.75rem 0;
2315
+ padding-left: 1.5rem;
2316
+ }
2317
+ .markdown-content li {
2318
+ margin: 0.25rem 0;
2319
+ }
2320
+ `}</style>
2321
+ </div>
2322
+ ) : activeTab === "json" ? (
2323
+ <div className="space-y-1">
2324
+ <span className="text-slate-400">{"{"}</span>
2325
+ {Object.keys(preparedFields).length > 0 ? (
2326
+ Object.entries(preparedFields).map(([key, value]) =>
2327
+ renderSection(key, value, 1)
2328
+ )
2329
+ ) : (
2330
+ <div className="pl-4 text-slate-400 italic">No fields extracted</div>
2331
+ )}
2332
+ <span className="text-slate-400">{"}"}</span>
2333
+ </div>
2334
+ ) : (
2335
+ <pre className="text-sm text-slate-600 whitespace-pre-wrap">
2336
+ {objectToXML(fields).split("\n").map((line, i) => (
2337
+ <div key={i} className="hover:bg-slate-50 px-2 -mx-2 rounded">
2338
+ {line.includes("<") ? (
2339
+ <>
2340
+ {line.split(/(<\/?[\w\s=".-]+>)/g).map((part, j) => {
2341
+ if (part.startsWith("</")) {
2342
+ return (
2343
+ <span key={j} className="text-rose-500">
2344
+ {part}
2345
+ </span>
2346
+ );
2347
+ }
2348
+ if (part.startsWith("<")) {
2349
+ return (
2350
+ <span key={j} className="text-indigo-500">
2351
+ {part}
2352
+ </span>
2353
+ );
2354
+ }
2355
+ return (
2356
+ <span key={j} className="text-slate-700">
2357
+ {part}
2358
+ </span>
2359
+ );
2360
+ })}
2361
+ </>
2362
+ ) : (
2363
+ line
2364
+ )}
2365
+ </div>
2366
+ ))}
2367
+ </pre>
2368
+ )}
2369
+ </div>
2370
+ )}
2371
+ </div>
2372
+
2373
+ {/* Confidence Footer */}
2374
+ {isComplete && extractionResult && (
2375
+ <div className="px-5 py-3 border-t border-slate-100 bg-slate-50/50">
2376
+ <div className="flex items-center justify-between text-xs">
2377
+ <div className="flex items-center gap-4">
2378
+ <div className="flex items-center gap-1.5">
2379
+ <div className={cn(
2380
+ "h-2 w-2 rounded-full",
2381
+ confidence >= 90 ? "bg-emerald-500" : confidence >= 70 ? "bg-amber-500" : "bg-red-500"
2382
+ )} />
2383
+ <span className="text-slate-500">Confidence:</span>
2384
+ <span className="font-semibold text-slate-700">
2385
+ {confidence > 0 ? `${confidence.toFixed(1)}%` : "N/A"}
2386
+ </span>
2387
+ </div>
2388
+ <div className="flex items-center gap-1.5">
2389
+ <span className="text-slate-500">Fields:</span>
2390
+ <span className="font-semibold text-slate-700">{fieldsExtracted}</span>
2391
+ </div>
2392
+ </div>
2393
+ <span className="text-slate-400">
2394
+ Processed in {totalTime >= 1000 ? `${(totalTime / 1000).toFixed(1)}s` : `${totalTime}ms`}
2395
+ </span>
2396
+ </div>
2397
+ </div>
2398
+ )}
2399
+ </div>
2400
+ );
2401
+ }
frontend/src/components/ocr/UpgradeModal.jsx CHANGED
@@ -1,218 +1,3 @@
1
- <<<<<<< HEAD
2
- import React from "react";
3
- import { motion } from "framer-motion";
4
- import { cn } from "@/lib/utils";
5
- import {
6
- X,
7
- Sparkles,
8
- Zap,
9
- Shield,
10
- Cloud,
11
- BarChart3,
12
- Bot,
13
- Globe,
14
- Lock,
15
- Rocket,
16
- Users,
17
- CheckCircle2,
18
- ArrowRight
19
- } from "lucide-react";
20
- import { Button } from "@/components/ui/button";
21
-
22
- const features = [
23
- {
24
- icon: Zap,
25
- title: "Production-Scale Processing",
26
- description: "Remove trial limits and run live AP and operations workflows",
27
- color: "amber",
28
- cta: "Explore with a demo",
29
- gradient: "from-amber-500 to-orange-500"
30
- },
31
- {
32
- icon: Bot,
33
- title: "Advanced Agentic Processing",
34
- description: "You can customize your own agentic pipeline with your own data",
35
- color: "indigo",
36
- cta: "Talk to Sales",
37
- gradient: "from-indigo-500 to-violet-500"
38
- },
39
- {
40
- icon: Cloud,
41
- title: "API Access",
42
- description: "Integrate EZOFIS into your workflow with our REST API",
43
- color: "blue",
44
- cta: "Talk to a Techie!",
45
- gradient: "from-blue-500 to-cyan-500"
46
- }
47
- ];
48
-
49
- export default function UpgradeModal({ open, onClose }) {
50
- if (!open) return null;
51
-
52
- return (
53
- <div className="fixed inset-0 z-50 flex items-center justify-center">
54
- {/* Backdrop */}
55
- <motion.div
56
- initial={{ opacity: 0 }}
57
- animate={{ opacity: 1 }}
58
- exit={{ opacity: 0 }}
59
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
60
- onClick={onClose}
61
- />
62
-
63
- {/* Modal */}
64
- <motion.div
65
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
66
- animate={{ opacity: 1, scale: 1, y: 0 }}
67
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
68
- className="relative z-10 w-full max-w-6xl max-h-[90vh] mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col"
69
- onClick={(e) => e.stopPropagation()}
70
- >
71
- {/* Header */}
72
- <div className="sticky top-0 bg-gradient-to-r from-indigo-600 via-violet-600 to-purple-600 text-white px-8 py-6 z-10">
73
- <button
74
- onClick={onClose}
75
- className="absolute right-6 top-6 h-8 w-8 rounded-lg bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
76
- >
77
- <X className="h-4 w-4" />
78
- </button>
79
-
80
- <motion.div
81
- initial={{ opacity: 0, y: 20 }}
82
- animate={{ opacity: 1, y: 0 }}
83
- className="text-center"
84
- >
85
- <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-sm mb-4">
86
- <Sparkles className="h-4 w-4" />
87
- <span className="text-sm font-medium">Trial Limit Reached</span>
88
- </div>
89
- <h2 className="text-3xl font-bold mb-2">You've processed 2 documents</h2>
90
- <p className="text-white/80 text-lg">Continue with production-ready document intelligence</p>
91
- </motion.div>
92
- </div>
93
-
94
- {/* Stats Bar */}
95
- <div className="grid grid-cols-3 gap-6 px-8 py-6 bg-slate-50 border-b border-slate-200">
96
- {[
97
- { label: "Accuracy Rate", value: "99.8%", icon: CheckCircle2 },
98
- { label: "Processing Speed", value: "< 10s", icon: Zap },
99
- { label: "Operational Users", value: "10,000+", icon: Users }
100
- ].map((stat, i) => (
101
- <motion.div
102
- key={stat.label}
103
- initial={{ opacity: 0, y: 20 }}
104
- animate={{ opacity: 1, y: 0 }}
105
- transition={{ delay: i * 0.1 }}
106
- className="text-center"
107
- >
108
- <div className="flex items-center justify-center gap-2 mb-1">
109
- <stat.icon className="h-4 w-4 text-indigo-600" />
110
- <span className="text-2xl font-bold text-slate-900">{stat.value}</span>
111
- </div>
112
- <p className="text-sm text-slate-500">{stat.label}</p>
113
- </motion.div>
114
- ))}
115
- </div>
116
-
117
- {/* Features Grid - Scrollable */}
118
- <div className="flex-1 overflow-auto px-8 py-8">
119
- <div className="text-center mb-8">
120
- <h3 className="text-2xl font-bold text-slate-900 mb-2">
121
- Continue to Production Use
122
- </h3>
123
-
124
- </div>
125
-
126
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
127
- {features.map((feature, index) => (
128
- <motion.div
129
- key={feature.title}
130
- initial={{ opacity: 0, y: 20 }}
131
- animate={{ opacity: 1, y: 0 }}
132
- transition={{ delay: 0.2 + index * 0.1 }}
133
- className="group relative bg-white rounded-2xl border border-slate-200 p-6 hover:shadow-xl hover:shadow-slate-200/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
134
- >
135
- {/* Gradient Background on Hover */}
136
- <div className={`absolute inset-0 bg-gradient-to-br ${feature.gradient} opacity-0 group-hover:opacity-5 transition-opacity duration-300`} />
137
-
138
- <div className="relative">
139
- <div className={cn(
140
- "h-12 w-12 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",
141
- feature.color === "amber" && "bg-amber-50",
142
- feature.color === "indigo" && "bg-indigo-50",
143
- feature.color === "blue" && "bg-blue-50",
144
- feature.color === "emerald" && "bg-emerald-50",
145
- feature.color === "slate" && "bg-slate-50",
146
- feature.color === "purple" && "bg-purple-50"
147
- )}>
148
- <feature.icon className={cn(
149
- "h-6 w-6",
150
- feature.color === "amber" && "text-amber-600",
151
- feature.color === "indigo" && "text-indigo-600",
152
- feature.color === "blue" && "text-blue-600",
153
- feature.color === "emerald" && "text-emerald-600",
154
- feature.color === "slate" && "text-slate-600",
155
- feature.color === "purple" && "text-purple-600"
156
- )} />
157
- </div>
158
- <h4 className="font-semibold text-slate-900 mb-2">{feature.title}</h4>
159
- <p className="text-sm text-slate-600 mb-4 leading-relaxed">{feature.description}</p>
160
-
161
- <Button
162
- variant="ghost"
163
- size="sm"
164
- className={cn(
165
- "w-full h-9 border transition-all group-hover:shadow-md",
166
- feature.color === "amber" && "text-amber-600 hover:bg-amber-50 border-amber-200 hover:border-amber-300",
167
- feature.color === "indigo" && "text-indigo-600 hover:bg-indigo-50 border-indigo-200 hover:border-indigo-300",
168
- feature.color === "blue" && "text-blue-600 hover:bg-blue-50 border-blue-200 hover:border-blue-300",
169
- feature.color === "emerald" && "text-emerald-600 hover:bg-emerald-50 border-emerald-200 hover:border-emerald-300",
170
- feature.color === "slate" && "text-slate-600 hover:bg-slate-50 border-slate-200 hover:border-slate-300",
171
- feature.color === "purple" && "text-purple-600 hover:bg-purple-50 border-purple-200 hover:border-purple-300"
172
- )}
173
- >
174
- {feature.cta}
175
- <ArrowRight className="h-3.5 w-3.5 ml-2 group-hover:translate-x-1 transition-transform" />
176
- </Button>
177
- </div>
178
- </motion.div>
179
- ))}
180
- </div>
181
- </div>
182
-
183
- {/* CTA Footer */}
184
- <div className="sticky bottom-0 bg-white border-t border-slate-200 px-8 py-6">
185
- <div className="flex items-center justify-between gap-6">
186
- <div className="flex-1">
187
- <h4 className="font-semibold text-slate-900 mb-1">Ready to scale?</h4>
188
- <p className="text-sm text-slate-600">No commitment. We’ll tailor the demo to your documents and workflows.</p>
189
- </div>
190
- <div className="flex items-center gap-3">
191
- <Button
192
- variant="outline"
193
- size="lg"
194
- className="h-11 border-slate-300"
195
- >
196
- <Users className="h-4 w-4 mr-2" />
197
- Talk to Sales
198
- </Button>
199
- <Button
200
- size="lg"
201
- className="h-11 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-lg shadow-indigo-500/25 hover:shadow-xl hover:shadow-indigo-500/30"
202
- >
203
- <Rocket className="h-4 w-4 mr-2" />
204
- Start a production evaluation
205
- <Sparkles className="h-4 w-4 ml-2" />
206
- </Button>
207
- </div>
208
- </div>
209
- </div>
210
- </motion.div>
211
- </div>
212
- );
213
- }
214
-
215
- =======
216
  import React from "react";
217
  import { motion } from "framer-motion";
218
  import { cn } from "@/lib/utils";
@@ -425,5 +210,214 @@ export default function UpgradeModal({ open, onClose }) {
425
  </div>
426
  );
427
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React from "react";
2
  import { motion } from "framer-motion";
3
  import { cn } from "@/lib/utils";
 
210
  </div>
211
  );
212
  }
213
+ import { motion } from "framer-motion";
214
+ import { cn } from "@/lib/utils";
215
+ import {
216
+ X,
217
+ Sparkles,
218
+ Zap,
219
+ Shield,
220
+ Cloud,
221
+ BarChart3,
222
+ Bot,
223
+ Globe,
224
+ Lock,
225
+ Rocket,
226
+ Users,
227
+ CheckCircle2,
228
+ ArrowRight
229
+ } from "lucide-react";
230
+ import { Button } from "@/components/ui/button";
231
+
232
+ const features = [
233
+ {
234
+ icon: Zap,
235
+ title: "Production-Scale Processing",
236
+ description: "Remove trial limits and run live AP and operations workflows",
237
+ color: "amber",
238
+ cta: "Explore with a demo",
239
+ gradient: "from-amber-500 to-orange-500"
240
+ },
241
+ {
242
+ icon: Bot,
243
+ title: "Advanced Agentic Processing",
244
+ description: "You can customize your own agentic pipeline with your own data",
245
+ color: "indigo",
246
+ cta: "Talk to Sales",
247
+ gradient: "from-indigo-500 to-violet-500"
248
+ },
249
+ {
250
+ icon: Cloud,
251
+ title: "API Access",
252
+ description: "Integrate EZOFIS into your workflow with our REST API",
253
+ color: "blue",
254
+ cta: "Talk to a Techie!",
255
+ gradient: "from-blue-500 to-cyan-500"
256
+ }
257
+ ];
258
+
259
+ export default function UpgradeModal({ open, onClose }) {
260
+ if (!open) return null;
261
+
262
+ return (
263
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
264
+ {/* Backdrop */}
265
+ <motion.div
266
+ initial={{ opacity: 0 }}
267
+ animate={{ opacity: 1 }}
268
+ exit={{ opacity: 0 }}
269
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
270
+ onClick={onClose}
271
+ />
272
+
273
+ {/* Modal */}
274
+ <motion.div
275
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
276
+ animate={{ opacity: 1, scale: 1, y: 0 }}
277
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
278
+ className="relative z-10 w-full max-w-6xl max-h-[90vh] mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col"
279
+ onClick={(e) => e.stopPropagation()}
280
+ >
281
+ {/* Header */}
282
+ <div className="sticky top-0 bg-gradient-to-r from-indigo-600 via-violet-600 to-purple-600 text-white px-8 py-6 z-10">
283
+ <button
284
+ onClick={onClose}
285
+ className="absolute right-6 top-6 h-8 w-8 rounded-lg bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
286
+ >
287
+ <X className="h-4 w-4" />
288
+ </button>
289
+
290
+ <motion.div
291
+ initial={{ opacity: 0, y: 20 }}
292
+ animate={{ opacity: 1, y: 0 }}
293
+ className="text-center"
294
+ >
295
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-sm mb-4">
296
+ <Sparkles className="h-4 w-4" />
297
+ <span className="text-sm font-medium">Trial Limit Reached</span>
298
+ </div>
299
+ <h2 className="text-3xl font-bold mb-2">You've processed 2 documents</h2>
300
+ <p className="text-white/80 text-lg">Continue with production-ready document intelligence</p>
301
+ </motion.div>
302
+ </div>
303
+
304
+ {/* Stats Bar */}
305
+ <div className="grid grid-cols-3 gap-6 px-8 py-6 bg-slate-50 border-b border-slate-200">
306
+ {[
307
+ { label: "Accuracy Rate", value: "99.8%", icon: CheckCircle2 },
308
+ { label: "Processing Speed", value: "< 10s", icon: Zap },
309
+ { label: "Operational Users", value: "10,000+", icon: Users }
310
+ ].map((stat, i) => (
311
+ <motion.div
312
+ key={stat.label}
313
+ initial={{ opacity: 0, y: 20 }}
314
+ animate={{ opacity: 1, y: 0 }}
315
+ transition={{ delay: i * 0.1 }}
316
+ className="text-center"
317
+ >
318
+ <div className="flex items-center justify-center gap-2 mb-1">
319
+ <stat.icon className="h-4 w-4 text-indigo-600" />
320
+ <span className="text-2xl font-bold text-slate-900">{stat.value}</span>
321
+ </div>
322
+ <p className="text-sm text-slate-500">{stat.label}</p>
323
+ </motion.div>
324
+ ))}
325
+ </div>
326
 
327
+ {/* Features Grid - Scrollable */}
328
+ <div className="flex-1 overflow-auto px-8 py-8">
329
+ <div className="text-center mb-8">
330
+ <h3 className="text-2xl font-bold text-slate-900 mb-2">
331
+ Continue to Production Use
332
+ </h3>
333
+
334
+ </div>
335
+
336
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
337
+ {features.map((feature, index) => (
338
+ <motion.div
339
+ key={feature.title}
340
+ initial={{ opacity: 0, y: 20 }}
341
+ animate={{ opacity: 1, y: 0 }}
342
+ transition={{ delay: 0.2 + index * 0.1 }}
343
+ className="group relative bg-white rounded-2xl border border-slate-200 p-6 hover:shadow-xl hover:shadow-slate-200/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
344
+ >
345
+ {/* Gradient Background on Hover */}
346
+ <div className={`absolute inset-0 bg-gradient-to-br ${feature.gradient} opacity-0 group-hover:opacity-5 transition-opacity duration-300`} />
347
+
348
+ <div className="relative">
349
+ <div className={cn(
350
+ "h-12 w-12 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",
351
+ feature.color === "amber" && "bg-amber-50",
352
+ feature.color === "indigo" && "bg-indigo-50",
353
+ feature.color === "blue" && "bg-blue-50",
354
+ feature.color === "emerald" && "bg-emerald-50",
355
+ feature.color === "slate" && "bg-slate-50",
356
+ feature.color === "purple" && "bg-purple-50"
357
+ )}>
358
+ <feature.icon className={cn(
359
+ "h-6 w-6",
360
+ feature.color === "amber" && "text-amber-600",
361
+ feature.color === "indigo" && "text-indigo-600",
362
+ feature.color === "blue" && "text-blue-600",
363
+ feature.color === "emerald" && "text-emerald-600",
364
+ feature.color === "slate" && "text-slate-600",
365
+ feature.color === "purple" && "text-purple-600"
366
+ )} />
367
+ </div>
368
+ <h4 className="font-semibold text-slate-900 mb-2">{feature.title}</h4>
369
+ <p className="text-sm text-slate-600 mb-4 leading-relaxed">{feature.description}</p>
370
+
371
+ <Button
372
+ variant="ghost"
373
+ size="sm"
374
+ className={cn(
375
+ "w-full h-9 border transition-all group-hover:shadow-md",
376
+ feature.color === "amber" && "text-amber-600 hover:bg-amber-50 border-amber-200 hover:border-amber-300",
377
+ feature.color === "indigo" && "text-indigo-600 hover:bg-indigo-50 border-indigo-200 hover:border-indigo-300",
378
+ feature.color === "blue" && "text-blue-600 hover:bg-blue-50 border-blue-200 hover:border-blue-300",
379
+ feature.color === "emerald" && "text-emerald-600 hover:bg-emerald-50 border-emerald-200 hover:border-emerald-300",
380
+ feature.color === "slate" && "text-slate-600 hover:bg-slate-50 border-slate-200 hover:border-slate-300",
381
+ feature.color === "purple" && "text-purple-600 hover:bg-purple-50 border-purple-200 hover:border-purple-300"
382
+ )}
383
+ >
384
+ {feature.cta}
385
+ <ArrowRight className="h-3.5 w-3.5 ml-2 group-hover:translate-x-1 transition-transform" />
386
+ </Button>
387
+ </div>
388
+ </motion.div>
389
+ ))}
390
+ </div>
391
+ </div>
392
+
393
+ {/* CTA Footer */}
394
+ <div className="sticky bottom-0 bg-white border-t border-slate-200 px-8 py-6">
395
+ <div className="flex items-center justify-between gap-6">
396
+ <div className="flex-1">
397
+ <h4 className="font-semibold text-slate-900 mb-1">Ready to scale?</h4>
398
+ <p className="text-sm text-slate-600">No commitment. We’ll tailor the demo to your documents and workflows.</p>
399
+ </div>
400
+ <div className="flex items-center gap-3">
401
+ <Button
402
+ variant="outline"
403
+ size="lg"
404
+ className="h-11 border-slate-300"
405
+ >
406
+ <Users className="h-4 w-4 mr-2" />
407
+ Talk to Sales
408
+ </Button>
409
+ <Button
410
+ size="lg"
411
+ className="h-11 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-lg shadow-indigo-500/25 hover:shadow-xl hover:shadow-indigo-500/30"
412
+ >
413
+ <Rocket className="h-4 w-4 mr-2" />
414
+ Start a production evaluation
415
+ <Sparkles className="h-4 w-4 ml-2" />
416
+ </Button>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ </motion.div>
421
+ </div>
422
+ );
423
+ }
frontend/src/pages/History.jsx CHANGED
@@ -1,864 +1,3 @@
1
- <<<<<<< HEAD
2
- // frontend/src/pages/History.jsx
3
-
4
- import React, { useState, useEffect } from "react";
5
- import { useNavigate, useSearchParams } from "react-router-dom";
6
- import { motion, AnimatePresence } from "framer-motion";
7
- import {
8
- FileText,
9
- Clock,
10
- CheckCircle2,
11
- ChevronRight,
12
- Download,
13
- Eye,
14
- Trash2,
15
- Search,
16
- Filter,
17
- Calendar,
18
- Upload,
19
- Cpu,
20
- TableProperties,
21
- MonitorPlay,
22
- TrendingUp,
23
- TrendingDown,
24
- Minus,
25
- AlertCircle,
26
- X,
27
- FileSpreadsheet,
28
- Table2,
29
- } from "lucide-react";
30
- import { Button } from "@/components/ui/button";
31
- import { Input } from "@/components/ui/input";
32
- import { Badge } from "@/components/ui/badge";
33
- import {
34
- Select,
35
- SelectContent,
36
- SelectItem,
37
- SelectTrigger,
38
- SelectValue,
39
- } from "@/components/ui/select";
40
- import {
41
- DropdownMenu,
42
- DropdownMenuContent,
43
- DropdownMenuItem,
44
- DropdownMenuSeparator,
45
- DropdownMenuTrigger,
46
- } from "@/components/ui/dropdown-menu";
47
- import { cn } from "@/lib/utils";
48
- import { getHistory } from "@/services/api";
49
-
50
- // minimal "toast"
51
- const toastSuccess = (msg) => {
52
- console.log(msg);
53
- };
54
-
55
- const stageConfig = {
56
- uploading: { label: "Uploading", icon: Upload, color: "blue" },
57
- aiAnalysis: { label: "AI Analysis", icon: Cpu, color: "violet" },
58
- dataExtraction: { label: "Data Extraction", icon: TableProperties, color: "emerald" },
59
- outputRendering: { label: "Output Rendering", icon: MonitorPlay, color: "amber" },
60
- };
61
-
62
- const variationConfig = {
63
- fast: { icon: TrendingDown, color: "text-emerald-500", label: "Faster than avg" },
64
- normal: { icon: Minus, color: "text-slate-400", label: "Normal" },
65
- slow: { icon: TrendingUp, color: "text-amber-500", label: "Slower than avg" },
66
- error: { icon: AlertCircle, color: "text-red-500", label: "Error" },
67
- skipped: { icon: Minus, color: "text-slate-300", label: "Skipped" },
68
- };
69
-
70
- export default function History() {
71
- const navigate = useNavigate();
72
- const [searchParams, setSearchParams] = useSearchParams();
73
- const [searchQuery, setSearchQuery] = useState("");
74
- const [selectedStatus, setSelectedStatus] = useState("all");
75
- const [expandedReport, setExpandedReport] = useState(null);
76
- const [isExporting, setIsExporting] = useState(false);
77
- const [history, setHistory] = useState([]);
78
- const [isLoading, setIsLoading] = useState(true);
79
- const [error, setError] = useState(null);
80
-
81
- // Fetch history on component mount
82
- useEffect(() => {
83
- const fetchHistory = async () => {
84
- setIsLoading(true);
85
- setError(null);
86
- try {
87
- const data = await getHistory();
88
- setHistory(data);
89
-
90
- // Check if there's an extractionId in URL (from share link)
91
- const extractionId = searchParams.get("extractionId");
92
- if (extractionId) {
93
- // Clear the query param and navigate to dashboard
94
- setSearchParams({});
95
- // Small delay to ensure history is loaded
96
- setTimeout(() => {
97
- navigate(`/?extractionId=${extractionId}`);
98
- }, 100);
99
- }
100
- } catch (err) {
101
- console.error("Failed to fetch history:", err);
102
- setError(err.message || "Failed to load history");
103
- setHistory([]); // Fallback to empty array
104
- } finally {
105
- setIsLoading(false);
106
- }
107
- };
108
-
109
- fetchHistory();
110
- }, [searchParams, setSearchParams, navigate]);
111
-
112
- const filteredHistory = history.filter((item) => {
113
- const matchesSearch = item.fileName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false;
114
- const matchesStatus = selectedStatus === "all" || item.status === selectedStatus;
115
- return matchesSearch && matchesStatus;
116
- });
117
-
118
- const formatTime = (ms) => {
119
- if (ms >= 1000) {
120
- return `${(ms / 1000).toFixed(2)}s`;
121
- }
122
- return `${ms}ms`;
123
- };
124
-
125
- const formatTimeForExport = (ms) => {
126
- return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
127
- };
128
-
129
- const formatDate = (dateString) => {
130
- const date = new Date(dateString);
131
- return date.toLocaleDateString("en-US", {
132
- month: "short",
133
- day: "numeric",
134
- hour: "2-digit",
135
- minute: "2-digit",
136
- });
137
- };
138
-
139
- const formatDateForExport = (dateString) => {
140
- const date = new Date(dateString);
141
- return date.toISOString().replace("T", " ").slice(0, 19);
142
- };
143
-
144
- const generateCSV = (data) => {
145
- const headers = [
146
- "File Name",
147
- "File Type",
148
- "File Size",
149
- "Extracted At",
150
- "Status",
151
- "Confidence (%)",
152
- "Fields Extracted",
153
- "Total Time (ms)",
154
- "Upload Time (ms)",
155
- "Upload Status",
156
- "Upload Variation",
157
- "AI Analysis Time (ms)",
158
- "AI Analysis Status",
159
- "AI Analysis Variation",
160
- "Data Extraction Time (ms)",
161
- "Data Extraction Status",
162
- "Data Extraction Variation",
163
- "Output Rendering Time (ms)",
164
- "Output Rendering Status",
165
- "Output Rendering Variation",
166
- "Error Message",
167
- ];
168
-
169
- const rows = data.map((item) => [
170
- item.fileName,
171
- item.fileType,
172
- item.fileSize,
173
- formatDateForExport(item.extractedAt),
174
- item.status,
175
- item.confidence,
176
- item.fieldsExtracted,
177
- item.totalTime,
178
- item.stages.uploading.time,
179
- item.stages.uploading.status,
180
- item.stages.uploading.variation,
181
- item.stages.aiAnalysis.time,
182
- item.stages.aiAnalysis.status,
183
- item.stages.aiAnalysis.variation,
184
- item.stages.dataExtraction.time,
185
- item.stages.dataExtraction.status,
186
- item.stages.dataExtraction.variation,
187
- item.stages.outputRendering.time,
188
- item.stages.outputRendering.status,
189
- item.stages.outputRendering.variation,
190
- item.errorMessage || "",
191
- ]);
192
-
193
- const csvContent = [
194
- headers.join(","),
195
- ...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
196
- ].join("\n");
197
-
198
- return csvContent;
199
- };
200
-
201
- const downloadFile = (content, fileName, mimeType) => {
202
- const blob = new Blob([content], { type: mimeType });
203
- const url = URL.createObjectURL(blob);
204
- const link = document.createElement("a");
205
- link.href = url;
206
- link.download = fileName;
207
- document.body.appendChild(link);
208
- link.click();
209
- document.body.removeChild(link);
210
- URL.revokeObjectURL(url);
211
- };
212
-
213
- const handleExportCSV = () => {
214
- setIsExporting(true);
215
- setTimeout(() => {
216
- const csvContent = generateCSV(filteredHistory);
217
- downloadFile(
218
- csvContent,
219
- `extraction_history_${new Date().toISOString().slice(0, 10)}.csv`,
220
- "text/csv;charset=utf-8;"
221
- );
222
- toastSuccess("CSV exported successfully");
223
- setIsExporting(false);
224
- }, 500);
225
- };
226
-
227
- const generateExcelXML = (data) => {
228
- const headers = [
229
- "File Name",
230
- "File Type",
231
- "File Size",
232
- "Extracted At",
233
- "Status",
234
- "Confidence (%)",
235
- "Fields Extracted",
236
- "Total Time (ms)",
237
- "Upload Time (ms)",
238
- "Upload Status",
239
- "Upload Variation",
240
- "AI Analysis Time (ms)",
241
- "AI Analysis Status",
242
- "AI Analysis Variation",
243
- "Data Extraction Time (ms)",
244
- "Data Extraction Status",
245
- "Data Extraction Variation",
246
- "Output Rendering Time (ms)",
247
- "Output Rendering Status",
248
- "Output Rendering Variation",
249
- "Error Message",
250
- ];
251
-
252
- const rows = data.map((item) => [
253
- item.fileName,
254
- item.fileType,
255
- item.fileSize,
256
- formatDateForExport(item.extractedAt),
257
- item.status,
258
- item.confidence,
259
- item.fieldsExtracted,
260
- item.totalTime,
261
- item.stages.uploading.time,
262
- item.stages.uploading.status,
263
- item.stages.uploading.variation,
264
- item.stages.aiAnalysis.time,
265
- item.stages.aiAnalysis.status,
266
- item.stages.aiAnalysis.variation,
267
- item.stages.dataExtraction.time,
268
- item.stages.dataExtraction.status,
269
- item.stages.dataExtraction.variation,
270
- item.stages.outputRendering.time,
271
- item.stages.outputRendering.status,
272
- item.stages.outputRendering.variation,
273
- item.errorMessage || "",
274
- ]);
275
-
276
- let xml = `<?xml version="1.0" encoding="UTF-8"?>
277
- <?mso-application progid="Excel.Sheet"?>
278
- <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
279
- xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
280
- <Worksheet ss:Name="Extraction History">
281
- <Table>
282
- <Row>`;
283
-
284
- headers.forEach((header) => {
285
- xml += `<Cell><Data ss:Type="String">${header}</Data></Cell>`;
286
- });
287
- xml += `</Row>`;
288
-
289
- rows.forEach((row) => {
290
- xml += `<Row>`;
291
- row.forEach((cell) => {
292
- const type = typeof cell === "number" ? "Number" : "String";
293
- xml += `<Cell><Data ss:Type="${type}">${cell}</Data></Cell>`;
294
- });
295
- xml += `</Row>`;
296
- });
297
-
298
- xml += `</Table></Worksheet></Workbook>`;
299
- return xml;
300
- };
301
-
302
- const handleExportExcel = () => {
303
- setIsExporting(true);
304
- setTimeout(() => {
305
- const excelContent = generateExcelXML(filteredHistory);
306
- downloadFile(
307
- excelContent,
308
- `extraction_history_${new Date().toISOString().slice(0, 10)}.xls`,
309
- "application/vnd.ms-excel"
310
- );
311
- toastSuccess("Excel file exported successfully");
312
- setIsExporting(false);
313
- }, 500);
314
- };
315
-
316
- const handleExportSingleReport = (item, format) => {
317
- if (format === "csv") {
318
- const csvContent = generateCSV([item]);
319
- downloadFile(
320
- csvContent,
321
- `${item.fileName.replace(/\.[^/.]+$/, "")}_report.csv`,
322
- "text/csv;charset=utf-8;"
323
- );
324
- toastSuccess("Report exported as CSV");
325
- } else {
326
- const excelContent = generateExcelXML([item]);
327
- downloadFile(
328
- excelContent,
329
- `${item.fileName.replace(/\.[^/.]+$/, "")}_report.xls`,
330
- "application/vnd.ms-excel"
331
- );
332
- toastSuccess("Report exported as Excel");
333
- }
334
- };
335
-
336
- return (
337
- <div className="min-h-screen bg-[#FAFAFA]">
338
- {/* Header */}
339
- <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16">
340
- <div className="px-8 h-full flex items-center">
341
- <div>
342
- <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight">
343
- Extraction History
344
- </h1>
345
- <p className="text-sm text-slate-500 leading-tight">
346
- View detailed reports and performance metrics for all extractions
347
- </p>
348
- </div>
349
- </div>
350
- </header>
351
-
352
- {/* Content */}
353
- <div className="p-8">
354
- {/* Filters */}
355
- <div className="flex items-center gap-4 mb-6">
356
- <div className="relative flex-1 max-w-md">
357
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
358
- <Input
359
- placeholder="Search by file name..."
360
- value={searchQuery}
361
- onChange={(e) => setSearchQuery(e.target.value)}
362
- className="pl-10 h-11 rounded-xl border-slate-200"
363
- />
364
- </div>
365
- <Select
366
- value={selectedStatus}
367
- onValueChange={(value) => setSelectedStatus(value)}
368
- >
369
- <SelectTrigger className="w-40 h-11 rounded-xl border-slate-200">
370
- <Filter className="h-4 w-4 mr-2 text-slate-400" />
371
- <SelectValue placeholder="Status" />
372
- </SelectTrigger>
373
- <SelectContent>
374
- <SelectItem value="all">All Status</SelectItem>
375
- <SelectItem value="completed">Completed</SelectItem>
376
- <SelectItem value="failed">Failed</SelectItem>
377
- </SelectContent>
378
- </Select>
379
-
380
- {/* Export All Button */}
381
- <DropdownMenu>
382
- <DropdownMenuTrigger asChild>
383
- <Button
384
- 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"
385
- disabled={isExporting || filteredHistory.length === 0}
386
- >
387
- {isExporting ? (
388
- <motion.div
389
- animate={{ rotate: 360 }}
390
- transition={{
391
- duration: 1,
392
- repeat: Infinity,
393
- ease: "linear",
394
- }}
395
- className="mr-2"
396
- >
397
- <Download className="h-4 w-4" />
398
- </motion.div>
399
- ) : (
400
- <Download className="h-4 w-4 mr-2" />
401
- )}
402
- Export All
403
- </Button>
404
- </DropdownMenuTrigger>
405
- <DropdownMenuContent
406
- align="end"
407
- className="w-48 rounded-xl p-2"
408
- >
409
- <DropdownMenuItem
410
- className="rounded-lg cursor-pointer"
411
- onClick={handleExportCSV}
412
- >
413
- <Table2 className="h-4 w-4 mr-2 text-emerald-600" />
414
- Export as CSV
415
- </DropdownMenuItem>
416
- <DropdownMenuItem
417
- className="rounded-lg cursor-pointer"
418
- onClick={handleExportExcel}
419
- >
420
- <FileSpreadsheet className="h-4 w-4 mr-2 text-green-600" />
421
- Export as Excel
422
- </DropdownMenuItem>
423
- <DropdownMenuSeparator />
424
- <div className="px-2 py-1.5 text-xs text-slate-500">
425
- {filteredHistory.length} records will be exported
426
- </div>
427
- </DropdownMenuContent>
428
- </DropdownMenu>
429
- </div>
430
-
431
- {/* Stats Overview */}
432
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
433
- {(() => {
434
- const total = history.length;
435
- const completed = history.filter((h) => h.status === "completed").length;
436
- const successRate = total > 0 ? ((completed / total) * 100).toFixed(1) : 0;
437
- const avgTime = history.length > 0
438
- ? history.reduce((sum, h) => sum + (h.totalTime || 0), 0) / history.length
439
- : 0;
440
- const totalFields = history.reduce((sum, h) => sum + (h.fieldsExtracted || 0), 0);
441
-
442
- return [
443
- {
444
- label: "Total Extractions",
445
- value: total.toString(),
446
- change: "",
447
- color: "indigo",
448
- },
449
- {
450
- label: "Success Rate",
451
- value: `${successRate}%`,
452
- change: total > 0 ? `${completed}/${total} successful` : "No data",
453
- color: "emerald",
454
- },
455
- {
456
- label: "Avg. Processing Time",
457
- value: avgTime >= 1000 ? `${(avgTime / 1000).toFixed(1)}s` : `${Math.round(avgTime)}ms`,
458
- change: "",
459
- color: "violet",
460
- },
461
- {
462
- label: "Fields Extracted",
463
- value: totalFields.toLocaleString(),
464
- change: "",
465
- color: "amber",
466
- },
467
- ].map((stat, index) => (
468
- <motion.div
469
- key={stat.label}
470
- initial={{ opacity: 0, y: 20 }}
471
- animate={{ opacity: 1, y: 0 }}
472
- transition={{ delay: index * 0.1 }}
473
- className="bg-white rounded-2xl border border-slate-200 p-5"
474
- >
475
- <p className="text-sm text-slate-500 mb-1">{stat.label}</p>
476
- <p className="text-2xl font-bold text-slate-900">{stat.value}</p>
477
- <p className={`text-xs text-${stat.color}-600 mt-1`}>
478
- {stat.change}
479
- </p>
480
- </motion.div>
481
- ));
482
- })()}
483
- </div>
484
-
485
- {/* Loading State */}
486
- {isLoading && (
487
- <div className="text-center py-16">
488
- <motion.div
489
- animate={{ rotate: 360 }}
490
- transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
491
- className="h-16 w-16 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4"
492
- >
493
- <Cpu className="h-8 w-8 text-indigo-600" />
494
- </motion.div>
495
- <p className="text-slate-500">Loading extraction history...</p>
496
- </div>
497
- )}
498
-
499
- {/* History List */}
500
- {!isLoading && (
501
- <div className="space-y-4">
502
- {filteredHistory.map((item, index) => (
503
- <motion.div
504
- key={item.id}
505
- initial={{ opacity: 0, y: 20 }}
506
- animate={{ opacity: 1, y: 0 }}
507
- transition={{ delay: index * 0.05 }}
508
- className="bg-white rounded-2xl border border-slate-200 overflow-hidden"
509
- >
510
- {/* Main Row */}
511
- <div
512
- className="p-5 cursor-pointer hover:bg-slate-50/50 transition-colors"
513
- onClick={() =>
514
- setExpandedReport(
515
- expandedReport === item.id ? null : item.id
516
- )
517
- }
518
- >
519
- <div className="flex items-center gap-4">
520
- {/* File Icon */}
521
- <div
522
- className={cn(
523
- "h-12 w-12 rounded-xl flex items-center justify-center",
524
- item.status === "completed" ? "bg-indigo-50" : "bg-red-50"
525
- )}
526
- >
527
- <FileText
528
- className={cn(
529
- "h-6 w-6",
530
- item.status === "completed"
531
- ? "text-indigo-600"
532
- : "text-red-500"
533
- )}
534
- />
535
- </div>
536
-
537
- {/* File Info */}
538
- <div className="flex-1 min-w-0">
539
- <div className="flex items-center gap-2">
540
- <h3 className="font-semibold text-slate-900 truncate">
541
- {item.fileName}
542
- </h3>
543
- <Badge variant="secondary" className="text-xs">
544
- {item.fileType}
545
- </Badge>
546
- </div>
547
- <div className="flex items-center gap-4 mt-1 text-sm text-slate-500">
548
- <span>{item.fileSize}</span>
549
- <span className="flex items-center gap-1">
550
- <Calendar className="h-3 w-3" />
551
- {formatDate(item.extractedAt)}
552
- </span>
553
- </div>
554
- </div>
555
-
556
- {/* Stats */}
557
- <div className="hidden md:flex items-center gap-6">
558
- <div className="text-center">
559
- <p className="text-xs text-slate-400">Time</p>
560
- <p className="font-semibold text-slate-700">
561
- {formatTime(item.totalTime)}
562
- </p>
563
- </div>
564
- <div className="text-center">
565
- <p className="text-xs text-slate-400">Fields</p>
566
- <p className="font-semibold text-slate-700">
567
- {item.fieldsExtracted}
568
- </p>
569
- </div>
570
- <div className="text-center">
571
- <p className="text-xs text-slate-400">Confidence</p>
572
- <p
573
- className={cn(
574
- "font-semibold",
575
- item.confidence >= 95
576
- ? "text-emerald-600"
577
- : item.confidence >= 90
578
- ? "text-amber-600"
579
- : "text-red-600"
580
- )}
581
- >
582
- {item.confidence > 0 ? `${item.confidence}%` : "-"}
583
- </p>
584
- </div>
585
- </div>
586
-
587
- {/* Status & Actions */}
588
- <div className="flex items-center gap-3">
589
- <Badge
590
- className={cn(
591
- "capitalize",
592
- item.status === "completed"
593
- ? "bg-emerald-50 text-emerald-700 border-emerald-200"
594
- : "bg-red-50 text-red-700 border-red-200"
595
- )}
596
- >
597
- {item.status === "completed" ? (
598
- <CheckCircle2 className="h-3 w-3 mr-1" />
599
- ) : (
600
- <AlertCircle className="h-3 w-3 mr-1" />
601
- )}
602
- {item.status}
603
- </Badge>
604
- <ChevronRight
605
- className={cn(
606
- "h-5 w-5 text-slate-400 transition-transform",
607
- expandedReport === item.id && "rotate-90"
608
- )}
609
- />
610
- </div>
611
- </div>
612
- </div>
613
-
614
- {/* Expanded Report */}
615
- <AnimatePresence>
616
- {expandedReport === item.id && (
617
- <motion.div
618
- initial={{ height: 0, opacity: 0 }}
619
- animate={{ height: "auto", opacity: 1 }}
620
- exit={{ height: 0, opacity: 0 }}
621
- transition={{ duration: 0.2 }}
622
- className="overflow-hidden"
623
- >
624
- <div className="px-5 pb-5 pt-2 border-t border-slate-100">
625
- {/* Error Message */}
626
- {item.errorMessage && (
627
- <div className="mb-4 p-4 bg-red-50 border border-red-100 rounded-xl">
628
- <div className="flex items-center gap-2 text-red-700">
629
- <AlertCircle className="h-4 w-4" />
630
- <span className="font-medium">Error Details</span>
631
- </div>
632
- <p className="text-sm text-red-600 mt-1">
633
- {item.errorMessage}
634
- </p>
635
- </div>
636
- )}
637
-
638
- {/* Performance Report Header */}
639
- <div className="flex items-center justify-between mb-4">
640
- <h4 className="font-semibold text-slate-800">
641
- Performance Report
642
- </h4>
643
- <div className="flex items-center gap-2">
644
- <Button
645
- variant="ghost"
646
- size="sm"
647
- className="h-8 text-xs"
648
- onClick={(e) => {
649
- e.stopPropagation();
650
- navigate(`/?extractionId=${item.id}`);
651
- }}
652
- >
653
- <Eye className="h-3 w-3 mr-1" />
654
- View Output
655
- </Button>
656
- <DropdownMenu>
657
- <DropdownMenuTrigger asChild>
658
- <Button
659
- variant="outline"
660
- size="sm"
661
- className="h-8 text-xs"
662
- >
663
- <Download className="h-3 w-3 mr-1" />
664
- Export Report
665
- </Button>
666
- </DropdownMenuTrigger>
667
- <DropdownMenuContent
668
- align="end"
669
- className="w-44 rounded-xl p-2"
670
- >
671
- <DropdownMenuItem
672
- className="rounded-lg cursor-pointer text-xs"
673
- onClick={(e) => {
674
- e.stopPropagation();
675
- handleExportSingleReport(item, "csv");
676
- }}
677
- >
678
- <Table2 className="h-3 w-3 mr-2 text-emerald-600" />
679
- Download CSV
680
- </DropdownMenuItem>
681
- <DropdownMenuItem
682
- className="rounded-lg cursor-pointer text-xs"
683
- onClick={(e) => {
684
- e.stopPropagation();
685
- handleExportSingleReport(item, "excel");
686
- }}
687
- >
688
- <FileSpreadsheet className="h-3 w-3 mr-2 text-green-600" />
689
- Download Excel
690
- </DropdownMenuItem>
691
- </DropdownMenuContent>
692
- </DropdownMenu>
693
- </div>
694
- </div>
695
-
696
- {/* Stage Timing Cards */}
697
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
698
- {Object.entries(item.stages).map(
699
- ([stageKey, stageData]) => {
700
- const config = stageConfig[stageKey];
701
- const variationInfo =
702
- variationConfig[stageData.variation];
703
- const Icon = config.icon;
704
- const VariationIcon = variationInfo.icon;
705
-
706
- return (
707
- <div
708
- key={stageKey}
709
- className={cn(
710
- "relative p-4 rounded-xl border",
711
- stageData.status === "completed"
712
- ? "bg-slate-50 border-slate-200"
713
- : stageData.status === "failed"
714
- ? "bg-red-50 border-red-200"
715
- : "bg-slate-50/50 border-slate-100"
716
- )}
717
- >
718
- <div className="flex items-center gap-2 mb-3">
719
- <div
720
- className={cn(
721
- "h-8 w-8 rounded-lg flex items-center justify-center",
722
- `bg-${config.color}-100`
723
- )}
724
- >
725
- <Icon
726
- className={cn(
727
- "h-4 w-4",
728
- `text-${config.color}-600`
729
- )}
730
- />
731
- </div>
732
- <span className="text-sm font-medium text-slate-700">
733
- {config.label}
734
- </span>
735
- </div>
736
-
737
- <div className="flex items-end justify-between">
738
- <div>
739
- <p
740
- className={cn(
741
- "text-2xl font-bold",
742
- stageData.status === "skipped"
743
- ? "text-slate-300"
744
- : stageData.status === "failed"
745
- ? "text-red-600"
746
- : "text-slate-900"
747
- )}
748
- >
749
- {stageData.status === "skipped"
750
- ? "-"
751
- : formatTime(stageData.time)}
752
- </p>
753
- {stageData.status !== "skipped" && (
754
- <div className="flex items-center gap-1 mt-1">
755
- <VariationIcon
756
- className={cn(
757
- "h-3 w-3",
758
- variationInfo.color
759
- )}
760
- />
761
- <span
762
- className={cn(
763
- "text-xs",
764
- variationInfo.color
765
- )}
766
- >
767
- {variationInfo.label}
768
- </span>
769
- </div>
770
- )}
771
- </div>
772
-
773
- {stageData.status === "completed" && (
774
- <CheckCircle2 className="h-5 w-5 text-emerald-500" />
775
- )}
776
- {stageData.status === "failed" && (
777
- <X className="h-5 w-5 text-red-500" />
778
- )}
779
- </div>
780
-
781
- {/* Progress bar */}
782
- <div className="mt-3 h-1.5 bg-slate-200 rounded-full overflow-hidden">
783
- <motion.div
784
- initial={{ width: 0 }}
785
- animate={{
786
- width:
787
- stageData.status === "completed"
788
- ? "100%"
789
- : stageData.status === "failed"
790
- ? "60%"
791
- : "0%",
792
- }}
793
- transition={{ duration: 0.5, delay: 0.2 }}
794
- className={cn(
795
- "h-full rounded-full",
796
- stageData.status === "failed"
797
- ? "bg-red-500"
798
- : `bg-${config.color}-500`
799
- )}
800
- />
801
- </div>
802
- </div>
803
- );
804
- }
805
- )}
806
- </div>
807
-
808
- {/* Total Time Summary */}
809
- <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">
810
- <div className="flex items-center gap-3">
811
- <Clock className="h-5 w-5 text-indigo-600" />
812
- <div>
813
- <p className="text-sm font-medium text-slate-700">
814
- Total Processing Time
815
- </p>
816
- <p className="text-xs text-slate-500">
817
- From upload to output ready
818
- </p>
819
- </div>
820
- </div>
821
- <div className="text-right">
822
- <p className="text-2xl font-bold text-indigo-600">
823
- {formatTime(item.totalTime)}
824
- </p>
825
- <p className="text-xs text-slate-500">
826
- {item.status === "completed"
827
- ? "Completed successfully"
828
- : "Process failed"}
829
- </p>
830
- </div>
831
- </div>
832
- </div>
833
- </motion.div>
834
- )}
835
- </AnimatePresence>
836
- </motion.div>
837
- ))}
838
- {filteredHistory.length === 0 && !error && (
839
- <div className="text-center py-16">
840
- <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
841
- <FileText className="h-10 w-10 text-slate-300" />
842
- </div>
843
- <p className="text-slate-500 mb-2">
844
- {history.length === 0
845
- ? "No extraction history yet"
846
- : "No extractions match your filters"}
847
- </p>
848
- {history.length === 0 && (
849
- <p className="text-sm text-slate-400">
850
- Upload a document to get started
851
- </p>
852
- )}
853
- </div>
854
- )}
855
- </div>
856
- )}
857
- </div>
858
- </div>
859
- );
860
- }
861
- =======
862
  // frontend/src/pages/History.jsx
863
 
864
  import React, { useState, useEffect } from "react";
@@ -1718,4 +857,859 @@ export default function History() {
1718
  </div>
1719
  );
1720
  }
1721
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // frontend/src/pages/History.jsx
2
 
3
  import React, { useState, useEffect } from "react";
 
857
  </div>
858
  );
859
  }
860
+ import { useNavigate, useSearchParams } from "react-router-dom";
861
+ import { motion, AnimatePresence } from "framer-motion";
862
+ import {
863
+ FileText,
864
+ Clock,
865
+ CheckCircle2,
866
+ ChevronRight,
867
+ Download,
868
+ Eye,
869
+ Trash2,
870
+ Search,
871
+ Filter,
872
+ Calendar,
873
+ Upload,
874
+ Cpu,
875
+ TableProperties,
876
+ MonitorPlay,
877
+ TrendingUp,
878
+ TrendingDown,
879
+ Minus,
880
+ AlertCircle,
881
+ X,
882
+ FileSpreadsheet,
883
+ Table2,
884
+ } from "lucide-react";
885
+ import { Button } from "@/components/ui/button";
886
+ import { Input } from "@/components/ui/input";
887
+ import { Badge } from "@/components/ui/badge";
888
+ import {
889
+ Select,
890
+ SelectContent,
891
+ SelectItem,
892
+ SelectTrigger,
893
+ SelectValue,
894
+ } from "@/components/ui/select";
895
+ import {
896
+ DropdownMenu,
897
+ DropdownMenuContent,
898
+ DropdownMenuItem,
899
+ DropdownMenuSeparator,
900
+ DropdownMenuTrigger,
901
+ } from "@/components/ui/dropdown-menu";
902
+ import { cn } from "@/lib/utils";
903
+ import { getHistory } from "@/services/api";
904
+
905
+ // minimal "toast"
906
+ const toastSuccess = (msg) => {
907
+ console.log(msg);
908
+ };
909
+
910
+ const stageConfig = {
911
+ uploading: { label: "Uploading", icon: Upload, color: "blue" },
912
+ aiAnalysis: { label: "AI Analysis", icon: Cpu, color: "violet" },
913
+ dataExtraction: { label: "Data Extraction", icon: TableProperties, color: "emerald" },
914
+ outputRendering: { label: "Output Rendering", icon: MonitorPlay, color: "amber" },
915
+ };
916
+
917
+ const variationConfig = {
918
+ fast: { icon: TrendingDown, color: "text-emerald-500", label: "Faster than avg" },
919
+ normal: { icon: Minus, color: "text-slate-400", label: "Normal" },
920
+ slow: { icon: TrendingUp, color: "text-amber-500", label: "Slower than avg" },
921
+ error: { icon: AlertCircle, color: "text-red-500", label: "Error" },
922
+ skipped: { icon: Minus, color: "text-slate-300", label: "Skipped" },
923
+ };
924
+
925
+ export default function History() {
926
+ const navigate = useNavigate();
927
+ const [searchParams, setSearchParams] = useSearchParams();
928
+ const [searchQuery, setSearchQuery] = useState("");
929
+ const [selectedStatus, setSelectedStatus] = useState("all");
930
+ const [expandedReport, setExpandedReport] = useState(null);
931
+ const [isExporting, setIsExporting] = useState(false);
932
+ const [history, setHistory] = useState([]);
933
+ const [isLoading, setIsLoading] = useState(true);
934
+ const [error, setError] = useState(null);
935
+
936
+ // Fetch history on component mount
937
+ useEffect(() => {
938
+ const fetchHistory = async () => {
939
+ setIsLoading(true);
940
+ setError(null);
941
+ try {
942
+ const data = await getHistory();
943
+ setHistory(data);
944
+
945
+ // Check if there's an extractionId in URL (from share link)
946
+ const extractionId = searchParams.get("extractionId");
947
+ if (extractionId) {
948
+ // Clear the query param and navigate to dashboard
949
+ setSearchParams({});
950
+ // Small delay to ensure history is loaded
951
+ setTimeout(() => {
952
+ navigate(`/?extractionId=${extractionId}`);
953
+ }, 100);
954
+ }
955
+ } catch (err) {
956
+ console.error("Failed to fetch history:", err);
957
+ setError(err.message || "Failed to load history");
958
+ setHistory([]); // Fallback to empty array
959
+ } finally {
960
+ setIsLoading(false);
961
+ }
962
+ };
963
+
964
+ fetchHistory();
965
+ }, [searchParams, setSearchParams, navigate]);
966
+
967
+ const filteredHistory = history.filter((item) => {
968
+ const matchesSearch = item.fileName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false;
969
+ const matchesStatus = selectedStatus === "all" || item.status === selectedStatus;
970
+ return matchesSearch && matchesStatus;
971
+ });
972
+
973
+ const formatTime = (ms) => {
974
+ if (ms >= 1000) {
975
+ return `${(ms / 1000).toFixed(2)}s`;
976
+ }
977
+ return `${ms}ms`;
978
+ };
979
+
980
+ const formatTimeForExport = (ms) => {
981
+ return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
982
+ };
983
+
984
+ const formatDate = (dateString) => {
985
+ const date = new Date(dateString);
986
+ return date.toLocaleDateString("en-US", {
987
+ month: "short",
988
+ day: "numeric",
989
+ hour: "2-digit",
990
+ minute: "2-digit",
991
+ });
992
+ };
993
+
994
+ const formatDateForExport = (dateString) => {
995
+ const date = new Date(dateString);
996
+ return date.toISOString().replace("T", " ").slice(0, 19);
997
+ };
998
+
999
+ const generateCSV = (data) => {
1000
+ const headers = [
1001
+ "File Name",
1002
+ "File Type",
1003
+ "File Size",
1004
+ "Extracted At",
1005
+ "Status",
1006
+ "Confidence (%)",
1007
+ "Fields Extracted",
1008
+ "Total Time (ms)",
1009
+ "Upload Time (ms)",
1010
+ "Upload Status",
1011
+ "Upload Variation",
1012
+ "AI Analysis Time (ms)",
1013
+ "AI Analysis Status",
1014
+ "AI Analysis Variation",
1015
+ "Data Extraction Time (ms)",
1016
+ "Data Extraction Status",
1017
+ "Data Extraction Variation",
1018
+ "Output Rendering Time (ms)",
1019
+ "Output Rendering Status",
1020
+ "Output Rendering Variation",
1021
+ "Error Message",
1022
+ ];
1023
+
1024
+ const rows = data.map((item) => [
1025
+ item.fileName,
1026
+ item.fileType,
1027
+ item.fileSize,
1028
+ formatDateForExport(item.extractedAt),
1029
+ item.status,
1030
+ item.confidence,
1031
+ item.fieldsExtracted,
1032
+ item.totalTime,
1033
+ item.stages.uploading.time,
1034
+ item.stages.uploading.status,
1035
+ item.stages.uploading.variation,
1036
+ item.stages.aiAnalysis.time,
1037
+ item.stages.aiAnalysis.status,
1038
+ item.stages.aiAnalysis.variation,
1039
+ item.stages.dataExtraction.time,
1040
+ item.stages.dataExtraction.status,
1041
+ item.stages.dataExtraction.variation,
1042
+ item.stages.outputRendering.time,
1043
+ item.stages.outputRendering.status,
1044
+ item.stages.outputRendering.variation,
1045
+ item.errorMessage || "",
1046
+ ]);
1047
+
1048
+ const csvContent = [
1049
+ headers.join(","),
1050
+ ...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
1051
+ ].join("\n");
1052
+
1053
+ return csvContent;
1054
+ };
1055
+
1056
+ const downloadFile = (content, fileName, mimeType) => {
1057
+ const blob = new Blob([content], { type: mimeType });
1058
+ const url = URL.createObjectURL(blob);
1059
+ const link = document.createElement("a");
1060
+ link.href = url;
1061
+ link.download = fileName;
1062
+ document.body.appendChild(link);
1063
+ link.click();
1064
+ document.body.removeChild(link);
1065
+ URL.revokeObjectURL(url);
1066
+ };
1067
+
1068
+ const handleExportCSV = () => {
1069
+ setIsExporting(true);
1070
+ setTimeout(() => {
1071
+ const csvContent = generateCSV(filteredHistory);
1072
+ downloadFile(
1073
+ csvContent,
1074
+ `extraction_history_${new Date().toISOString().slice(0, 10)}.csv`,
1075
+ "text/csv;charset=utf-8;"
1076
+ );
1077
+ toastSuccess("CSV exported successfully");
1078
+ setIsExporting(false);
1079
+ }, 500);
1080
+ };
1081
+
1082
+ const generateExcelXML = (data) => {
1083
+ const headers = [
1084
+ "File Name",
1085
+ "File Type",
1086
+ "File Size",
1087
+ "Extracted At",
1088
+ "Status",
1089
+ "Confidence (%)",
1090
+ "Fields Extracted",
1091
+ "Total Time (ms)",
1092
+ "Upload Time (ms)",
1093
+ "Upload Status",
1094
+ "Upload Variation",
1095
+ "AI Analysis Time (ms)",
1096
+ "AI Analysis Status",
1097
+ "AI Analysis Variation",
1098
+ "Data Extraction Time (ms)",
1099
+ "Data Extraction Status",
1100
+ "Data Extraction Variation",
1101
+ "Output Rendering Time (ms)",
1102
+ "Output Rendering Status",
1103
+ "Output Rendering Variation",
1104
+ "Error Message",
1105
+ ];
1106
+
1107
+ const rows = data.map((item) => [
1108
+ item.fileName,
1109
+ item.fileType,
1110
+ item.fileSize,
1111
+ formatDateForExport(item.extractedAt),
1112
+ item.status,
1113
+ item.confidence,
1114
+ item.fieldsExtracted,
1115
+ item.totalTime,
1116
+ item.stages.uploading.time,
1117
+ item.stages.uploading.status,
1118
+ item.stages.uploading.variation,
1119
+ item.stages.aiAnalysis.time,
1120
+ item.stages.aiAnalysis.status,
1121
+ item.stages.aiAnalysis.variation,
1122
+ item.stages.dataExtraction.time,
1123
+ item.stages.dataExtraction.status,
1124
+ item.stages.dataExtraction.variation,
1125
+ item.stages.outputRendering.time,
1126
+ item.stages.outputRendering.status,
1127
+ item.stages.outputRendering.variation,
1128
+ item.errorMessage || "",
1129
+ ]);
1130
+
1131
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>
1132
+ <?mso-application progid="Excel.Sheet"?>
1133
+ <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
1134
+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
1135
+ <Worksheet ss:Name="Extraction History">
1136
+ <Table>
1137
+ <Row>`;
1138
+
1139
+ headers.forEach((header) => {
1140
+ xml += `<Cell><Data ss:Type="String">${header}</Data></Cell>`;
1141
+ });
1142
+ xml += `</Row>`;
1143
+
1144
+ rows.forEach((row) => {
1145
+ xml += `<Row>`;
1146
+ row.forEach((cell) => {
1147
+ const type = typeof cell === "number" ? "Number" : "String";
1148
+ xml += `<Cell><Data ss:Type="${type}">${cell}</Data></Cell>`;
1149
+ });
1150
+ xml += `</Row>`;
1151
+ });
1152
+
1153
+ xml += `</Table></Worksheet></Workbook>`;
1154
+ return xml;
1155
+ };
1156
+
1157
+ const handleExportExcel = () => {
1158
+ setIsExporting(true);
1159
+ setTimeout(() => {
1160
+ const excelContent = generateExcelXML(filteredHistory);
1161
+ downloadFile(
1162
+ excelContent,
1163
+ `extraction_history_${new Date().toISOString().slice(0, 10)}.xls`,
1164
+ "application/vnd.ms-excel"
1165
+ );
1166
+ toastSuccess("Excel file exported successfully");
1167
+ setIsExporting(false);
1168
+ }, 500);
1169
+ };
1170
+
1171
+ const handleExportSingleReport = (item, format) => {
1172
+ if (format === "csv") {
1173
+ const csvContent = generateCSV([item]);
1174
+ downloadFile(
1175
+ csvContent,
1176
+ `${item.fileName.replace(/\.[^/.]+$/, "")}_report.csv`,
1177
+ "text/csv;charset=utf-8;"
1178
+ );
1179
+ toastSuccess("Report exported as CSV");
1180
+ } else {
1181
+ const excelContent = generateExcelXML([item]);
1182
+ downloadFile(
1183
+ excelContent,
1184
+ `${item.fileName.replace(/\.[^/.]+$/, "")}_report.xls`,
1185
+ "application/vnd.ms-excel"
1186
+ );
1187
+ toastSuccess("Report exported as Excel");
1188
+ }
1189
+ };
1190
+
1191
+ return (
1192
+ <div className="min-h-screen bg-[#FAFAFA]">
1193
+ {/* Header */}
1194
+ <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16">
1195
+ <div className="px-8 h-full flex items-center">
1196
+ <div>
1197
+ <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight">
1198
+ Extraction History
1199
+ </h1>
1200
+ <p className="text-sm text-slate-500 leading-tight">
1201
+ View detailed reports and performance metrics for all extractions
1202
+ </p>
1203
+ </div>
1204
+ </div>
1205
+ </header>
1206
+
1207
+ {/* Content */}
1208
+ <div className="p-8">
1209
+ {/* Filters */}
1210
+ <div className="flex items-center gap-4 mb-6">
1211
+ <div className="relative flex-1 max-w-md">
1212
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
1213
+ <Input
1214
+ placeholder="Search by file name..."
1215
+ value={searchQuery}
1216
+ onChange={(e) => setSearchQuery(e.target.value)}
1217
+ className="pl-10 h-11 rounded-xl border-slate-200"
1218
+ />
1219
+ </div>
1220
+ <Select
1221
+ value={selectedStatus}
1222
+ onValueChange={(value) => setSelectedStatus(value)}
1223
+ >
1224
+ <SelectTrigger className="w-40 h-11 rounded-xl border-slate-200">
1225
+ <Filter className="h-4 w-4 mr-2 text-slate-400" />
1226
+ <SelectValue placeholder="Status" />
1227
+ </SelectTrigger>
1228
+ <SelectContent>
1229
+ <SelectItem value="all">All Status</SelectItem>
1230
+ <SelectItem value="completed">Completed</SelectItem>
1231
+ <SelectItem value="failed">Failed</SelectItem>
1232
+ </SelectContent>
1233
+ </Select>
1234
+
1235
+ {/* Export All Button */}
1236
+ <DropdownMenu>
1237
+ <DropdownMenuTrigger asChild>
1238
+ <Button
1239
+ 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"
1240
+ disabled={isExporting || filteredHistory.length === 0}
1241
+ >
1242
+ {isExporting ? (
1243
+ <motion.div
1244
+ animate={{ rotate: 360 }}
1245
+ transition={{
1246
+ duration: 1,
1247
+ repeat: Infinity,
1248
+ ease: "linear",
1249
+ }}
1250
+ className="mr-2"
1251
+ >
1252
+ <Download className="h-4 w-4" />
1253
+ </motion.div>
1254
+ ) : (
1255
+ <Download className="h-4 w-4 mr-2" />
1256
+ )}
1257
+ Export All
1258
+ </Button>
1259
+ </DropdownMenuTrigger>
1260
+ <DropdownMenuContent
1261
+ align="end"
1262
+ className="w-48 rounded-xl p-2"
1263
+ >
1264
+ <DropdownMenuItem
1265
+ className="rounded-lg cursor-pointer"
1266
+ onClick={handleExportCSV}
1267
+ >
1268
+ <Table2 className="h-4 w-4 mr-2 text-emerald-600" />
1269
+ Export as CSV
1270
+ </DropdownMenuItem>
1271
+ <DropdownMenuItem
1272
+ className="rounded-lg cursor-pointer"
1273
+ onClick={handleExportExcel}
1274
+ >
1275
+ <FileSpreadsheet className="h-4 w-4 mr-2 text-green-600" />
1276
+ Export as Excel
1277
+ </DropdownMenuItem>
1278
+ <DropdownMenuSeparator />
1279
+ <div className="px-2 py-1.5 text-xs text-slate-500">
1280
+ {filteredHistory.length} records will be exported
1281
+ </div>
1282
+ </DropdownMenuContent>
1283
+ </DropdownMenu>
1284
+ </div>
1285
+
1286
+ {/* Stats Overview */}
1287
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
1288
+ {(() => {
1289
+ const total = history.length;
1290
+ const completed = history.filter((h) => h.status === "completed").length;
1291
+ const successRate = total > 0 ? ((completed / total) * 100).toFixed(1) : 0;
1292
+ const avgTime = history.length > 0
1293
+ ? history.reduce((sum, h) => sum + (h.totalTime || 0), 0) / history.length
1294
+ : 0;
1295
+ const totalFields = history.reduce((sum, h) => sum + (h.fieldsExtracted || 0), 0);
1296
+
1297
+ return [
1298
+ {
1299
+ label: "Total Extractions",
1300
+ value: total.toString(),
1301
+ change: "",
1302
+ color: "indigo",
1303
+ },
1304
+ {
1305
+ label: "Success Rate",
1306
+ value: `${successRate}%`,
1307
+ change: total > 0 ? `${completed}/${total} successful` : "No data",
1308
+ color: "emerald",
1309
+ },
1310
+ {
1311
+ label: "Avg. Processing Time",
1312
+ value: avgTime >= 1000 ? `${(avgTime / 1000).toFixed(1)}s` : `${Math.round(avgTime)}ms`,
1313
+ change: "",
1314
+ color: "violet",
1315
+ },
1316
+ {
1317
+ label: "Fields Extracted",
1318
+ value: totalFields.toLocaleString(),
1319
+ change: "",
1320
+ color: "amber",
1321
+ },
1322
+ ].map((stat, index) => (
1323
+ <motion.div
1324
+ key={stat.label}
1325
+ initial={{ opacity: 0, y: 20 }}
1326
+ animate={{ opacity: 1, y: 0 }}
1327
+ transition={{ delay: index * 0.1 }}
1328
+ className="bg-white rounded-2xl border border-slate-200 p-5"
1329
+ >
1330
+ <p className="text-sm text-slate-500 mb-1">{stat.label}</p>
1331
+ <p className="text-2xl font-bold text-slate-900">{stat.value}</p>
1332
+ <p className={`text-xs text-${stat.color}-600 mt-1`}>
1333
+ {stat.change}
1334
+ </p>
1335
+ </motion.div>
1336
+ ));
1337
+ })()}
1338
+ </div>
1339
+
1340
+ {/* Loading State */}
1341
+ {isLoading && (
1342
+ <div className="text-center py-16">
1343
+ <motion.div
1344
+ animate={{ rotate: 360 }}
1345
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
1346
+ className="h-16 w-16 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4"
1347
+ >
1348
+ <Cpu className="h-8 w-8 text-indigo-600" />
1349
+ </motion.div>
1350
+ <p className="text-slate-500">Loading extraction history...</p>
1351
+ </div>
1352
+ )}
1353
+
1354
+ {/* History List */}
1355
+ {!isLoading && (
1356
+ <div className="space-y-4">
1357
+ {filteredHistory.map((item, index) => (
1358
+ <motion.div
1359
+ key={item.id}
1360
+ initial={{ opacity: 0, y: 20 }}
1361
+ animate={{ opacity: 1, y: 0 }}
1362
+ transition={{ delay: index * 0.05 }}
1363
+ className="bg-white rounded-2xl border border-slate-200 overflow-hidden"
1364
+ >
1365
+ {/* Main Row */}
1366
+ <div
1367
+ className="p-5 cursor-pointer hover:bg-slate-50/50 transition-colors"
1368
+ onClick={() =>
1369
+ setExpandedReport(
1370
+ expandedReport === item.id ? null : item.id
1371
+ )
1372
+ }
1373
+ >
1374
+ <div className="flex items-center gap-4">
1375
+ {/* File Icon */}
1376
+ <div
1377
+ className={cn(
1378
+ "h-12 w-12 rounded-xl flex items-center justify-center",
1379
+ item.status === "completed" ? "bg-indigo-50" : "bg-red-50"
1380
+ )}
1381
+ >
1382
+ <FileText
1383
+ className={cn(
1384
+ "h-6 w-6",
1385
+ item.status === "completed"
1386
+ ? "text-indigo-600"
1387
+ : "text-red-500"
1388
+ )}
1389
+ />
1390
+ </div>
1391
+
1392
+ {/* File Info */}
1393
+ <div className="flex-1 min-w-0">
1394
+ <div className="flex items-center gap-2">
1395
+ <h3 className="font-semibold text-slate-900 truncate">
1396
+ {item.fileName}
1397
+ </h3>
1398
+ <Badge variant="secondary" className="text-xs">
1399
+ {item.fileType}
1400
+ </Badge>
1401
+ </div>
1402
+ <div className="flex items-center gap-4 mt-1 text-sm text-slate-500">
1403
+ <span>{item.fileSize}</span>
1404
+ <span className="flex items-center gap-1">
1405
+ <Calendar className="h-3 w-3" />
1406
+ {formatDate(item.extractedAt)}
1407
+ </span>
1408
+ </div>
1409
+ </div>
1410
+
1411
+ {/* Stats */}
1412
+ <div className="hidden md:flex items-center gap-6">
1413
+ <div className="text-center">
1414
+ <p className="text-xs text-slate-400">Time</p>
1415
+ <p className="font-semibold text-slate-700">
1416
+ {formatTime(item.totalTime)}
1417
+ </p>
1418
+ </div>
1419
+ <div className="text-center">
1420
+ <p className="text-xs text-slate-400">Fields</p>
1421
+ <p className="font-semibold text-slate-700">
1422
+ {item.fieldsExtracted}
1423
+ </p>
1424
+ </div>
1425
+ <div className="text-center">
1426
+ <p className="text-xs text-slate-400">Confidence</p>
1427
+ <p
1428
+ className={cn(
1429
+ "font-semibold",
1430
+ item.confidence >= 95
1431
+ ? "text-emerald-600"
1432
+ : item.confidence >= 90
1433
+ ? "text-amber-600"
1434
+ : "text-red-600"
1435
+ )}
1436
+ >
1437
+ {item.confidence > 0 ? `${item.confidence}%` : "-"}
1438
+ </p>
1439
+ </div>
1440
+ </div>
1441
+
1442
+ {/* Status & Actions */}
1443
+ <div className="flex items-center gap-3">
1444
+ <Badge
1445
+ className={cn(
1446
+ "capitalize",
1447
+ item.status === "completed"
1448
+ ? "bg-emerald-50 text-emerald-700 border-emerald-200"
1449
+ : "bg-red-50 text-red-700 border-red-200"
1450
+ )}
1451
+ >
1452
+ {item.status === "completed" ? (
1453
+ <CheckCircle2 className="h-3 w-3 mr-1" />
1454
+ ) : (
1455
+ <AlertCircle className="h-3 w-3 mr-1" />
1456
+ )}
1457
+ {item.status}
1458
+ </Badge>
1459
+ <ChevronRight
1460
+ className={cn(
1461
+ "h-5 w-5 text-slate-400 transition-transform",
1462
+ expandedReport === item.id && "rotate-90"
1463
+ )}
1464
+ />
1465
+ </div>
1466
+ </div>
1467
+ </div>
1468
+
1469
+ {/* Expanded Report */}
1470
+ <AnimatePresence>
1471
+ {expandedReport === item.id && (
1472
+ <motion.div
1473
+ initial={{ height: 0, opacity: 0 }}
1474
+ animate={{ height: "auto", opacity: 1 }}
1475
+ exit={{ height: 0, opacity: 0 }}
1476
+ transition={{ duration: 0.2 }}
1477
+ className="overflow-hidden"
1478
+ >
1479
+ <div className="px-5 pb-5 pt-2 border-t border-slate-100">
1480
+ {/* Error Message */}
1481
+ {item.errorMessage && (
1482
+ <div className="mb-4 p-4 bg-red-50 border border-red-100 rounded-xl">
1483
+ <div className="flex items-center gap-2 text-red-700">
1484
+ <AlertCircle className="h-4 w-4" />
1485
+ <span className="font-medium">Error Details</span>
1486
+ </div>
1487
+ <p className="text-sm text-red-600 mt-1">
1488
+ {item.errorMessage}
1489
+ </p>
1490
+ </div>
1491
+ )}
1492
+
1493
+ {/* Performance Report Header */}
1494
+ <div className="flex items-center justify-between mb-4">
1495
+ <h4 className="font-semibold text-slate-800">
1496
+ Performance Report
1497
+ </h4>
1498
+ <div className="flex items-center gap-2">
1499
+ <Button
1500
+ variant="ghost"
1501
+ size="sm"
1502
+ className="h-8 text-xs"
1503
+ onClick={(e) => {
1504
+ e.stopPropagation();
1505
+ navigate(`/?extractionId=${item.id}`);
1506
+ }}
1507
+ >
1508
+ <Eye className="h-3 w-3 mr-1" />
1509
+ View Output
1510
+ </Button>
1511
+ <DropdownMenu>
1512
+ <DropdownMenuTrigger asChild>
1513
+ <Button
1514
+ variant="outline"
1515
+ size="sm"
1516
+ className="h-8 text-xs"
1517
+ >
1518
+ <Download className="h-3 w-3 mr-1" />
1519
+ Export Report
1520
+ </Button>
1521
+ </DropdownMenuTrigger>
1522
+ <DropdownMenuContent
1523
+ align="end"
1524
+ className="w-44 rounded-xl p-2"
1525
+ >
1526
+ <DropdownMenuItem
1527
+ className="rounded-lg cursor-pointer text-xs"
1528
+ onClick={(e) => {
1529
+ e.stopPropagation();
1530
+ handleExportSingleReport(item, "csv");
1531
+ }}
1532
+ >
1533
+ <Table2 className="h-3 w-3 mr-2 text-emerald-600" />
1534
+ Download CSV
1535
+ </DropdownMenuItem>
1536
+ <DropdownMenuItem
1537
+ className="rounded-lg cursor-pointer text-xs"
1538
+ onClick={(e) => {
1539
+ e.stopPropagation();
1540
+ handleExportSingleReport(item, "excel");
1541
+ }}
1542
+ >
1543
+ <FileSpreadsheet className="h-3 w-3 mr-2 text-green-600" />
1544
+ Download Excel
1545
+ </DropdownMenuItem>
1546
+ </DropdownMenuContent>
1547
+ </DropdownMenu>
1548
+ </div>
1549
+ </div>
1550
+
1551
+ {/* Stage Timing Cards */}
1552
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1553
+ {Object.entries(item.stages).map(
1554
+ ([stageKey, stageData]) => {
1555
+ const config = stageConfig[stageKey];
1556
+ const variationInfo =
1557
+ variationConfig[stageData.variation];
1558
+ const Icon = config.icon;
1559
+ const VariationIcon = variationInfo.icon;
1560
+
1561
+ return (
1562
+ <div
1563
+ key={stageKey}
1564
+ className={cn(
1565
+ "relative p-4 rounded-xl border",
1566
+ stageData.status === "completed"
1567
+ ? "bg-slate-50 border-slate-200"
1568
+ : stageData.status === "failed"
1569
+ ? "bg-red-50 border-red-200"
1570
+ : "bg-slate-50/50 border-slate-100"
1571
+ )}
1572
+ >
1573
+ <div className="flex items-center gap-2 mb-3">
1574
+ <div
1575
+ className={cn(
1576
+ "h-8 w-8 rounded-lg flex items-center justify-center",
1577
+ `bg-${config.color}-100`
1578
+ )}
1579
+ >
1580
+ <Icon
1581
+ className={cn(
1582
+ "h-4 w-4",
1583
+ `text-${config.color}-600`
1584
+ )}
1585
+ />
1586
+ </div>
1587
+ <span className="text-sm font-medium text-slate-700">
1588
+ {config.label}
1589
+ </span>
1590
+ </div>
1591
+
1592
+ <div className="flex items-end justify-between">
1593
+ <div>
1594
+ <p
1595
+ className={cn(
1596
+ "text-2xl font-bold",
1597
+ stageData.status === "skipped"
1598
+ ? "text-slate-300"
1599
+ : stageData.status === "failed"
1600
+ ? "text-red-600"
1601
+ : "text-slate-900"
1602
+ )}
1603
+ >
1604
+ {stageData.status === "skipped"
1605
+ ? "-"
1606
+ : formatTime(stageData.time)}
1607
+ </p>
1608
+ {stageData.status !== "skipped" && (
1609
+ <div className="flex items-center gap-1 mt-1">
1610
+ <VariationIcon
1611
+ className={cn(
1612
+ "h-3 w-3",
1613
+ variationInfo.color
1614
+ )}
1615
+ />
1616
+ <span
1617
+ className={cn(
1618
+ "text-xs",
1619
+ variationInfo.color
1620
+ )}
1621
+ >
1622
+ {variationInfo.label}
1623
+ </span>
1624
+ </div>
1625
+ )}
1626
+ </div>
1627
+
1628
+ {stageData.status === "completed" && (
1629
+ <CheckCircle2 className="h-5 w-5 text-emerald-500" />
1630
+ )}
1631
+ {stageData.status === "failed" && (
1632
+ <X className="h-5 w-5 text-red-500" />
1633
+ )}
1634
+ </div>
1635
+
1636
+ {/* Progress bar */}
1637
+ <div className="mt-3 h-1.5 bg-slate-200 rounded-full overflow-hidden">
1638
+ <motion.div
1639
+ initial={{ width: 0 }}
1640
+ animate={{
1641
+ width:
1642
+ stageData.status === "completed"
1643
+ ? "100%"
1644
+ : stageData.status === "failed"
1645
+ ? "60%"
1646
+ : "0%",
1647
+ }}
1648
+ transition={{ duration: 0.5, delay: 0.2 }}
1649
+ className={cn(
1650
+ "h-full rounded-full",
1651
+ stageData.status === "failed"
1652
+ ? "bg-red-500"
1653
+ : `bg-${config.color}-500`
1654
+ )}
1655
+ />
1656
+ </div>
1657
+ </div>
1658
+ );
1659
+ }
1660
+ )}
1661
+ </div>
1662
+
1663
+ {/* Total Time Summary */}
1664
+ <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">
1665
+ <div className="flex items-center gap-3">
1666
+ <Clock className="h-5 w-5 text-indigo-600" />
1667
+ <div>
1668
+ <p className="text-sm font-medium text-slate-700">
1669
+ Total Processing Time
1670
+ </p>
1671
+ <p className="text-xs text-slate-500">
1672
+ From upload to output ready
1673
+ </p>
1674
+ </div>
1675
+ </div>
1676
+ <div className="text-right">
1677
+ <p className="text-2xl font-bold text-indigo-600">
1678
+ {formatTime(item.totalTime)}
1679
+ </p>
1680
+ <p className="text-xs text-slate-500">
1681
+ {item.status === "completed"
1682
+ ? "Completed successfully"
1683
+ : "Process failed"}
1684
+ </p>
1685
+ </div>
1686
+ </div>
1687
+ </div>
1688
+ </motion.div>
1689
+ )}
1690
+ </AnimatePresence>
1691
+ </motion.div>
1692
+ ))}
1693
+ {filteredHistory.length === 0 && !error && (
1694
+ <div className="text-center py-16">
1695
+ <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
1696
+ <FileText className="h-10 w-10 text-slate-300" />
1697
+ </div>
1698
+ <p className="text-slate-500 mb-2">
1699
+ {history.length === 0
1700
+ ? "No extraction history yet"
1701
+ : "No extractions match your filters"}
1702
+ </p>
1703
+ {history.length === 0 && (
1704
+ <p className="text-sm text-slate-400">
1705
+ Upload a document to get started
1706
+ </p>
1707
+ )}
1708
+ </div>
1709
+ )}
1710
+ </div>
1711
+ )}
1712
+ </div>
1713
+ </div>
1714
+ );
1715
+ }