Seth commited on
Commit
f21322e
·
1 Parent(s): d7fe8fb
frontend/index.html CHANGED
@@ -1,18 +1,3 @@
1
- <<<<<<< HEAD
2
- <!doctype html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="UTF-8" />
6
- <link rel="icon" type="image/png" href="/logo.png" />
7
- <title>EZOFIS AI - VRP Document Intelligence</title>
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- </head>
10
- <body class="bg-[#FAFAFA]">
11
- <div id="root"></div>
12
- <script type="module" src="/src/main.jsx"></script>
13
- </body>
14
- </html>
15
- =======
16
  <!doctype html>
17
  <html lang="en">
18
  <head>
@@ -26,4 +11,3 @@
26
  <script type="module" src="/src/main.jsx"></script>
27
  </body>
28
  </html>
29
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
  <html lang="en">
3
  <head>
 
11
  <script type="module" src="/src/main.jsx"></script>
12
  </body>
13
  </html>
 
frontend/src/components/ShareModal.jsx CHANGED
@@ -1,202 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import { X, Mail, Send, Loader2 } from "lucide-react";
5
- import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
7
-
8
- export default function ShareModal({ isOpen, onClose, onShare, extractionId }) {
9
- const [email, setEmail] = useState("");
10
- const [isLoading, setIsLoading] = useState(false);
11
- const [error, setError] = useState("");
12
- const [success, setSuccess] = useState(false);
13
- const [successMessage, setSuccessMessage] = useState("");
14
-
15
- const handleSubmit = async (e) => {
16
- e.preventDefault();
17
- setError("");
18
- setSuccess(false);
19
-
20
- // Parse and validate multiple emails (comma or semicolon separated)
21
- if (!email.trim()) {
22
- setError("Please enter at least one recipient email address");
23
- return;
24
- }
25
-
26
- // Split by comma or semicolon, trim each email, and filter out empty strings
27
- const emailList = email
28
- .split(/[,;]/)
29
- .map((e) => e.trim())
30
- .filter((e) => e.length > 0);
31
-
32
- if (emailList.length === 0) {
33
- setError("Please enter at least one recipient email address");
34
- return;
35
- }
36
-
37
- // Validate each email
38
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
39
- const invalidEmails = emailList.filter((e) => !emailRegex.test(e));
40
-
41
- if (invalidEmails.length > 0) {
42
- setError(`Invalid email address(es): ${invalidEmails.join(", ")}`);
43
- return;
44
- }
45
-
46
- setIsLoading(true);
47
- try {
48
- const result = await onShare(extractionId, emailList);
49
- setSuccessMessage(result?.message || `Successfully shared with ${emailList.length} recipient(s)`);
50
- setSuccess(true);
51
- setEmail("");
52
- // Close modal after 2 seconds
53
- setTimeout(() => {
54
- setSuccess(false);
55
- setSuccessMessage("");
56
- onClose();
57
- }, 2000);
58
- } catch (err) {
59
- setError(err.message || "Failed to share extraction. Please try again.");
60
- } finally {
61
- setIsLoading(false);
62
- }
63
- };
64
-
65
- const handleClose = () => {
66
- if (!isLoading) {
67
- setEmail("");
68
- setError("");
69
- setSuccess(false);
70
- onClose();
71
- }
72
- };
73
-
74
- if (!isOpen) return null;
75
-
76
- return (
77
- <AnimatePresence>
78
- <div className="fixed inset-0 z-50 flex items-center justify-center">
79
- {/* Backdrop */}
80
- <motion.div
81
- initial={{ opacity: 0 }}
82
- animate={{ opacity: 1 }}
83
- exit={{ opacity: 0 }}
84
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
85
- onClick={handleClose}
86
- />
87
-
88
- {/* Modal */}
89
- <motion.div
90
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
91
- animate={{ opacity: 1, scale: 1, y: 0 }}
92
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
93
- className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden"
94
- onClick={(e) => e.stopPropagation()}
95
- >
96
- {/* Header */}
97
- <div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
98
- <h2 className="text-xl font-semibold text-slate-900">Share Output</h2>
99
- <button
100
- onClick={handleClose}
101
- disabled={isLoading}
102
- className="p-2 rounded-lg hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
103
- >
104
- <X className="h-5 w-5 text-slate-500" />
105
- </button>
106
- </div>
107
-
108
- {/* Content */}
109
- <div className="px-6 py-6">
110
- {success ? (
111
- <motion.div
112
- initial={{ opacity: 0, scale: 0.9 }}
113
- animate={{ opacity: 1, scale: 1 }}
114
- className="text-center py-8"
115
- >
116
- <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-emerald-100 flex items-center justify-center">
117
- <Send className="h-8 w-8 text-emerald-600" />
118
- </div>
119
- <h3 className="text-lg font-semibold text-slate-900 mb-2">
120
- Share Sent Successfully!
121
- </h3>
122
- <p className="text-sm text-slate-600">
123
- {successMessage || "The recipient(s) will receive an email with a link to view the extraction."}
124
- </p>
125
- </motion.div>
126
- ) : (
127
- <form onSubmit={handleSubmit} className="space-y-4">
128
- <div>
129
- <label
130
- htmlFor="recipient-email"
131
- className="block text-sm font-medium text-slate-700 mb-2"
132
- >
133
- Recipient Email(s)
134
- </label>
135
- <p className="text-xs text-slate-500 mb-2">
136
- Separate multiple emails with commas or semicolons
137
- </p>
138
- <div className="relative">
139
- <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
140
- <Input
141
- id="recipient-email"
142
- type="text"
143
- value={email}
144
- onChange={(e) => setEmail(e.target.value)}
145
- placeholder="Enter email addresses (comma or semicolon separated)"
146
- className="pl-10 h-12 rounded-xl border-slate-200 focus:border-indigo-500 focus:ring-indigo-500"
147
- disabled={isLoading}
148
- autoFocus
149
- />
150
- </div>
151
- {error && (
152
- <motion.p
153
- initial={{ opacity: 0, y: -10 }}
154
- animate={{ opacity: 1, y: 0 }}
155
- className="mt-2 text-sm text-red-600"
156
- >
157
- {error}
158
- </motion.p>
159
- )}
160
- </div>
161
-
162
- <div className="pt-4 flex gap-3">
163
- <Button
164
- type="button"
165
- variant="outline"
166
- onClick={handleClose}
167
- disabled={isLoading}
168
- className="flex-1 h-11 rounded-xl"
169
- >
170
- Cancel
171
- </Button>
172
- <Button
173
- type="submit"
174
- disabled={isLoading || !email.trim()}
175
- className="flex-1 h-11 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
176
- >
177
- {isLoading ? (
178
- <>
179
- <Loader2 className="h-4 w-4 mr-2 animate-spin" />
180
- Sending...
181
- </>
182
- ) : (
183
- <>
184
- <Send className="h-4 w-4 mr-2" />
185
- Send
186
- </>
187
- )}
188
- </Button>
189
- </div>
190
- </form>
191
- )}
192
- </div>
193
- </motion.div>
194
- </div>
195
- </AnimatePresence>
196
- );
197
- }
198
-
199
- =======
200
  import React, { useState } from "react";
201
  import { motion, AnimatePresence } from "framer-motion";
202
  import { X, Mail, Send, Loader2 } from "lucide-react";
@@ -393,5 +194,198 @@ export default function ShareModal({ isOpen, onClose, onShare, extractionId }) {
393
  </AnimatePresence>
394
  );
395
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState } from "react";
2
  import { motion, AnimatePresence } from "framer-motion";
3
  import { X, Mail, Send, Loader2 } from "lucide-react";
 
194
  </AnimatePresence>
195
  );
196
  }
197
+ import { motion, AnimatePresence } from "framer-motion";
198
+ import { X, Mail, Send, Loader2 } from "lucide-react";
199
+ import { Button } from "@/components/ui/button";
200
+ import { Input } from "@/components/ui/input";
201
+
202
+ export default function ShareModal({ isOpen, onClose, onShare, extractionId }) {
203
+ const [email, setEmail] = useState("");
204
+ const [isLoading, setIsLoading] = useState(false);
205
+ const [error, setError] = useState("");
206
+ const [success, setSuccess] = useState(false);
207
+ const [successMessage, setSuccessMessage] = useState("");
208
+
209
+ const handleSubmit = async (e) => {
210
+ e.preventDefault();
211
+ setError("");
212
+ setSuccess(false);
213
+
214
+ // Parse and validate multiple emails (comma or semicolon separated)
215
+ if (!email.trim()) {
216
+ setError("Please enter at least one recipient email address");
217
+ return;
218
+ }
219
+
220
+ // Split by comma or semicolon, trim each email, and filter out empty strings
221
+ const emailList = email
222
+ .split(/[,;]/)
223
+ .map((e) => e.trim())
224
+ .filter((e) => e.length > 0);
225
+
226
+ if (emailList.length === 0) {
227
+ setError("Please enter at least one recipient email address");
228
+ return;
229
+ }
230
+
231
+ // Validate each email
232
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
233
+ const invalidEmails = emailList.filter((e) => !emailRegex.test(e));
234
+
235
+ if (invalidEmails.length > 0) {
236
+ setError(`Invalid email address(es): ${invalidEmails.join(", ")}`);
237
+ return;
238
+ }
239
+
240
+ setIsLoading(true);
241
+ try {
242
+ const result = await onShare(extractionId, emailList);
243
+ setSuccessMessage(result?.message || `Successfully shared with ${emailList.length} recipient(s)`);
244
+ setSuccess(true);
245
+ setEmail("");
246
+ // Close modal after 2 seconds
247
+ setTimeout(() => {
248
+ setSuccess(false);
249
+ setSuccessMessage("");
250
+ onClose();
251
+ }, 2000);
252
+ } catch (err) {
253
+ setError(err.message || "Failed to share extraction. Please try again.");
254
+ } finally {
255
+ setIsLoading(false);
256
+ }
257
+ };
258
+
259
+ const handleClose = () => {
260
+ if (!isLoading) {
261
+ setEmail("");
262
+ setError("");
263
+ setSuccess(false);
264
+ onClose();
265
+ }
266
+ };
267
+
268
+ if (!isOpen) return null;
269
+
270
+ return (
271
+ <AnimatePresence>
272
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
273
+ {/* Backdrop */}
274
+ <motion.div
275
+ initial={{ opacity: 0 }}
276
+ animate={{ opacity: 1 }}
277
+ exit={{ opacity: 0 }}
278
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
279
+ onClick={handleClose}
280
+ />
281
+
282
+ {/* Modal */}
283
+ <motion.div
284
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
285
+ animate={{ opacity: 1, scale: 1, y: 0 }}
286
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
287
+ className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden"
288
+ onClick={(e) => e.stopPropagation()}
289
+ >
290
+ {/* Header */}
291
+ <div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
292
+ <h2 className="text-xl font-semibold text-slate-900">Share Output</h2>
293
+ <button
294
+ onClick={handleClose}
295
+ disabled={isLoading}
296
+ className="p-2 rounded-lg hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
297
+ >
298
+ <X className="h-5 w-5 text-slate-500" />
299
+ </button>
300
+ </div>
301
 
302
+ {/* Content */}
303
+ <div className="px-6 py-6">
304
+ {success ? (
305
+ <motion.div
306
+ initial={{ opacity: 0, scale: 0.9 }}
307
+ animate={{ opacity: 1, scale: 1 }}
308
+ className="text-center py-8"
309
+ >
310
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-emerald-100 flex items-center justify-center">
311
+ <Send className="h-8 w-8 text-emerald-600" />
312
+ </div>
313
+ <h3 className="text-lg font-semibold text-slate-900 mb-2">
314
+ Share Sent Successfully!
315
+ </h3>
316
+ <p className="text-sm text-slate-600">
317
+ {successMessage || "The recipient(s) will receive an email with a link to view the extraction."}
318
+ </p>
319
+ </motion.div>
320
+ ) : (
321
+ <form onSubmit={handleSubmit} className="space-y-4">
322
+ <div>
323
+ <label
324
+ htmlFor="recipient-email"
325
+ className="block text-sm font-medium text-slate-700 mb-2"
326
+ >
327
+ Recipient Email(s)
328
+ </label>
329
+ <p className="text-xs text-slate-500 mb-2">
330
+ Separate multiple emails with commas or semicolons
331
+ </p>
332
+ <div className="relative">
333
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
334
+ <Input
335
+ id="recipient-email"
336
+ type="text"
337
+ value={email}
338
+ onChange={(e) => setEmail(e.target.value)}
339
+ placeholder="Enter email addresses (comma or semicolon separated)"
340
+ className="pl-10 h-12 rounded-xl border-slate-200 focus:border-indigo-500 focus:ring-indigo-500"
341
+ disabled={isLoading}
342
+ autoFocus
343
+ />
344
+ </div>
345
+ {error && (
346
+ <motion.p
347
+ initial={{ opacity: 0, y: -10 }}
348
+ animate={{ opacity: 1, y: 0 }}
349
+ className="mt-2 text-sm text-red-600"
350
+ >
351
+ {error}
352
+ </motion.p>
353
+ )}
354
+ </div>
355
+
356
+ <div className="pt-4 flex gap-3">
357
+ <Button
358
+ type="button"
359
+ variant="outline"
360
+ onClick={handleClose}
361
+ disabled={isLoading}
362
+ className="flex-1 h-11 rounded-xl"
363
+ >
364
+ Cancel
365
+ </Button>
366
+ <Button
367
+ type="submit"
368
+ disabled={isLoading || !email.trim()}
369
+ className="flex-1 h-11 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
370
+ >
371
+ {isLoading ? (
372
+ <>
373
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
374
+ Sending...
375
+ </>
376
+ ) : (
377
+ <>
378
+ <Send className="h-4 w-4 mr-2" />
379
+ Send
380
+ </>
381
+ )}
382
+ </Button>
383
+ </div>
384
+ </form>
385
+ )}
386
+ </div>
387
+ </motion.div>
388
+ </div>
389
+ </AnimatePresence>
390
+ );
391
+ }
frontend/src/components/ocr/DocumentPreview.jsx CHANGED
@@ -1,234 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState, useEffect, useRef } from "react";
3
- import { motion } from "framer-motion";
4
- import { FileText, ZoomIn, ZoomOut, RotateCw } from "lucide-react";
5
- import { Button } from "@/components/ui/button";
6
-
7
- export default function DocumentPreview({ file, isProcessing, isFromHistory = false }) {
8
- const [previewUrls, setPreviewUrls] = useState([]);
9
- const [zoom, setZoom] = useState(100);
10
- const [rotation, setRotation] = useState(0);
11
- const objectUrlsRef = useRef([]);
12
-
13
- useEffect(() => {
14
- if (!file) {
15
- // Cleanup previous URLs
16
- objectUrlsRef.current.forEach((url) => {
17
- if (url && url.startsWith("blob:")) {
18
- URL.revokeObjectURL(url);
19
- }
20
- });
21
- objectUrlsRef.current = [];
22
- setPreviewUrls([]);
23
- return;
24
- }
25
-
26
- const loadPreview = async () => {
27
- const urls = [];
28
- const newObjectUrls = [];
29
-
30
- // Check if it's a PDF
31
- if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) {
32
- try {
33
- // Use pdf.js to render PDF pages
34
- const pdfjsLib = await import("pdfjs-dist");
35
-
36
- // Configure worker - use jsdelivr CDN which is more reliable
37
- // This will use the same version as the installed package
38
- const version = pdfjsLib.version || "4.0.379";
39
- pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/build/pdf.worker.min.mjs`;
40
-
41
- const arrayBuffer = await file.arrayBuffer();
42
- const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
43
- const numPages = pdf.numPages;
44
-
45
- for (let pageNum = 1; pageNum <= numPages; pageNum++) {
46
- const page = await pdf.getPage(pageNum);
47
- const viewport = page.getViewport({ scale: 2.0 });
48
-
49
- const canvas = document.createElement("canvas");
50
- const context = canvas.getContext("2d");
51
- canvas.height = viewport.height;
52
- canvas.width = viewport.width;
53
-
54
- await page.render({
55
- canvasContext: context,
56
- viewport: viewport,
57
- }).promise;
58
-
59
- urls.push(canvas.toDataURL("image/jpeg", 0.95));
60
- }
61
- } catch (error) {
62
- console.error("Error loading PDF:", error);
63
- // Fallback: show error message
64
- urls.push(null);
65
- }
66
- } else {
67
- // For images, create object URL
68
- const url = URL.createObjectURL(file);
69
- urls.push(url);
70
- newObjectUrls.push(url);
71
- }
72
-
73
- // Cleanup old object URLs
74
- objectUrlsRef.current.forEach((url) => {
75
- if (url && url.startsWith("blob:")) {
76
- URL.revokeObjectURL(url);
77
- }
78
- });
79
- objectUrlsRef.current = newObjectUrls;
80
- setPreviewUrls(urls);
81
- };
82
-
83
- loadPreview();
84
-
85
- // Cleanup function - revoke object URLs when component unmounts or file changes
86
- return () => {
87
- objectUrlsRef.current.forEach((url) => {
88
- if (url && url.startsWith("blob:")) {
89
- URL.revokeObjectURL(url);
90
- }
91
- });
92
- objectUrlsRef.current = [];
93
- };
94
- }, [file]);
95
-
96
- return (
97
- <div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
98
- {/* Header */}
99
- <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
100
- <div className="flex items-center gap-3">
101
- <div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center">
102
- <FileText className="h-4 w-4 text-indigo-600" />
103
- </div>
104
- <div>
105
- <h3 className="font-semibold text-slate-800 text-sm">Document Preview</h3>
106
- <p className="text-xs text-slate-400">{file?.name || "No file selected"}</p>
107
- </div>
108
- </div>
109
-
110
- {file && (
111
- <div className="flex items-center gap-1">
112
- <Button
113
- variant="ghost"
114
- size="icon"
115
- className="h-8 w-8 text-slate-400 hover:text-slate-600"
116
- onClick={() => setZoom(Math.max(50, zoom - 25))}
117
- >
118
- <ZoomOut className="h-4 w-4" />
119
- </Button>
120
- <span className="text-xs text-slate-500 w-12 text-center">{zoom}%</span>
121
- <Button
122
- variant="ghost"
123
- size="icon"
124
- className="h-8 w-8 text-slate-400 hover:text-slate-600"
125
- onClick={() => setZoom(Math.min(200, zoom + 25))}
126
- >
127
- <ZoomIn className="h-4 w-4" />
128
- </Button>
129
- <div className="w-px h-4 bg-slate-200 mx-2" />
130
- <Button
131
- variant="ghost"
132
- size="icon"
133
- className="h-8 w-8 text-slate-400 hover:text-slate-600"
134
- onClick={() => setRotation((rotation + 90) % 360)}
135
- >
136
- <RotateCw className="h-4 w-4" />
137
- </Button>
138
- </div>
139
- )}
140
- </div>
141
-
142
- {/* Preview Area */}
143
- <div className="flex-1 p-6 bg-slate-50/50 overflow-auto">
144
- {!file ? (
145
- <div className="h-full flex items-center justify-center">
146
- <div className="text-center">
147
- <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
148
- <FileText className="h-10 w-10 text-slate-300" />
149
- </div>
150
- <p className="text-slate-400 text-sm">Upload a document to preview</p>
151
- </div>
152
- </div>
153
- ) : previewUrls.length === 0 ? (
154
- <div className="h-full flex items-center justify-center">
155
- <div className="text-center">
156
- <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
157
- <FileText className="h-10 w-10 text-slate-300" />
158
- </div>
159
- <p className="text-slate-400 text-sm">Loading preview...</p>
160
- </div>
161
- </div>
162
- ) : (
163
- <div className="space-y-4">
164
- {previewUrls.map((url, index) => (
165
- <motion.div
166
- key={index}
167
- initial={{ opacity: 0, y: 20 }}
168
- animate={{ opacity: 1, y: 0 }}
169
- transition={{ delay: index * 0.1 }}
170
- className="relative bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex items-center justify-center"
171
- style={{
172
- minHeight: "400px",
173
- }}
174
- >
175
- {url ? (
176
- <img
177
- src={url}
178
- alt={`Page ${index + 1}`}
179
- className="w-full h-auto"
180
- style={{
181
- transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
182
- maxWidth: "100%",
183
- objectFit: "contain",
184
- transition: "transform 0.2s ease",
185
- }}
186
- />
187
- ) : (
188
- <div className="p-8 text-center">
189
- <p className="text-slate-400 text-sm">
190
- {isFromHistory
191
- ? "Original document not available for historical extractions"
192
- : "Unable to load preview"}
193
- </p>
194
- </div>
195
- )}
196
-
197
- {/* Processing overlay */}
198
- {isProcessing && (
199
- <motion.div
200
- initial={{ opacity: 0 }}
201
- animate={{ opacity: 1 }}
202
- className="absolute inset-0 bg-indigo-600/5 backdrop-blur-[1px] pointer-events-none"
203
- >
204
- <motion.div
205
- initial={{ top: 0 }}
206
- animate={{ top: "100%" }}
207
- transition={{
208
- duration: 2,
209
- repeat: Infinity,
210
- ease: "linear",
211
- }}
212
- className="absolute left-0 right-0 h-1 bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
213
- />
214
- </motion.div>
215
- )}
216
-
217
- {/* Page number */}
218
- {previewUrls.length > 1 && (
219
- <div className="absolute bottom-3 right-3 text-xs text-slate-400 bg-white/90 px-2 py-1 rounded">
220
- Page {index + 1}
221
- </div>
222
- )}
223
- </motion.div>
224
- ))}
225
- </div>
226
- )}
227
- </div>
228
- </div>
229
- );
230
- }
231
- =======
232
  import React, { useState, useEffect, useRef } from "react";
233
  import { motion } from "framer-motion";
234
  import { FileText, ZoomIn, ZoomOut, RotateCw } from "lucide-react";
@@ -458,4 +227,231 @@ export default function DocumentPreview({ file, isProcessing, isFromHistory = fa
458
  </div>
459
  );
460
  }
461
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useEffect, useRef } from "react";
2
  import { motion } from "framer-motion";
3
  import { FileText, ZoomIn, ZoomOut, RotateCw } from "lucide-react";
 
227
  </div>
228
  );
229
  }
230
+ import { motion } from "framer-motion";
231
+ import { FileText, ZoomIn, ZoomOut, RotateCw } from "lucide-react";
232
+ import { Button } from "@/components/ui/button";
233
+
234
+ export default function DocumentPreview({ file, isProcessing, isFromHistory = false }) {
235
+ const [previewUrls, setPreviewUrls] = useState([]);
236
+ const [zoom, setZoom] = useState(100);
237
+ const [rotation, setRotation] = useState(0);
238
+ const objectUrlsRef = useRef([]);
239
+
240
+ useEffect(() => {
241
+ if (!file) {
242
+ // Cleanup previous URLs
243
+ objectUrlsRef.current.forEach((url) => {
244
+ if (url && url.startsWith("blob:")) {
245
+ URL.revokeObjectURL(url);
246
+ }
247
+ });
248
+ objectUrlsRef.current = [];
249
+ setPreviewUrls([]);
250
+ return;
251
+ }
252
+
253
+ const loadPreview = async () => {
254
+ const urls = [];
255
+ const newObjectUrls = [];
256
+
257
+ // Check if it's a PDF
258
+ if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) {
259
+ try {
260
+ // Use pdf.js to render PDF pages
261
+ const pdfjsLib = await import("pdfjs-dist");
262
+
263
+ // Configure worker - use jsdelivr CDN which is more reliable
264
+ // This will use the same version as the installed package
265
+ const version = pdfjsLib.version || "4.0.379";
266
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/build/pdf.worker.min.mjs`;
267
+
268
+ const arrayBuffer = await file.arrayBuffer();
269
+ const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
270
+ const numPages = pdf.numPages;
271
+
272
+ for (let pageNum = 1; pageNum <= numPages; pageNum++) {
273
+ const page = await pdf.getPage(pageNum);
274
+ const viewport = page.getViewport({ scale: 2.0 });
275
+
276
+ const canvas = document.createElement("canvas");
277
+ const context = canvas.getContext("2d");
278
+ canvas.height = viewport.height;
279
+ canvas.width = viewport.width;
280
+
281
+ await page.render({
282
+ canvasContext: context,
283
+ viewport: viewport,
284
+ }).promise;
285
+
286
+ urls.push(canvas.toDataURL("image/jpeg", 0.95));
287
+ }
288
+ } catch (error) {
289
+ console.error("Error loading PDF:", error);
290
+ // Fallback: show error message
291
+ urls.push(null);
292
+ }
293
+ } else {
294
+ // For images, create object URL
295
+ const url = URL.createObjectURL(file);
296
+ urls.push(url);
297
+ newObjectUrls.push(url);
298
+ }
299
+
300
+ // Cleanup old object URLs
301
+ objectUrlsRef.current.forEach((url) => {
302
+ if (url && url.startsWith("blob:")) {
303
+ URL.revokeObjectURL(url);
304
+ }
305
+ });
306
+ objectUrlsRef.current = newObjectUrls;
307
+ setPreviewUrls(urls);
308
+ };
309
+
310
+ loadPreview();
311
+
312
+ // Cleanup function - revoke object URLs when component unmounts or file changes
313
+ return () => {
314
+ objectUrlsRef.current.forEach((url) => {
315
+ if (url && url.startsWith("blob:")) {
316
+ URL.revokeObjectURL(url);
317
+ }
318
+ });
319
+ objectUrlsRef.current = [];
320
+ };
321
+ }, [file]);
322
+
323
+ return (
324
+ <div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
325
+ {/* Header */}
326
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
327
+ <div className="flex items-center gap-3">
328
+ <div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center">
329
+ <FileText className="h-4 w-4 text-indigo-600" />
330
+ </div>
331
+ <div>
332
+ <h3 className="font-semibold text-slate-800 text-sm">Document Preview</h3>
333
+ <p className="text-xs text-slate-400">{file?.name || "No file selected"}</p>
334
+ </div>
335
+ </div>
336
+
337
+ {file && (
338
+ <div className="flex items-center gap-1">
339
+ <Button
340
+ variant="ghost"
341
+ size="icon"
342
+ className="h-8 w-8 text-slate-400 hover:text-slate-600"
343
+ onClick={() => setZoom(Math.max(50, zoom - 25))}
344
+ >
345
+ <ZoomOut className="h-4 w-4" />
346
+ </Button>
347
+ <span className="text-xs text-slate-500 w-12 text-center">{zoom}%</span>
348
+ <Button
349
+ variant="ghost"
350
+ size="icon"
351
+ className="h-8 w-8 text-slate-400 hover:text-slate-600"
352
+ onClick={() => setZoom(Math.min(200, zoom + 25))}
353
+ >
354
+ <ZoomIn className="h-4 w-4" />
355
+ </Button>
356
+ <div className="w-px h-4 bg-slate-200 mx-2" />
357
+ <Button
358
+ variant="ghost"
359
+ size="icon"
360
+ className="h-8 w-8 text-slate-400 hover:text-slate-600"
361
+ onClick={() => setRotation((rotation + 90) % 360)}
362
+ >
363
+ <RotateCw className="h-4 w-4" />
364
+ </Button>
365
+ </div>
366
+ )}
367
+ </div>
368
+
369
+ {/* Preview Area */}
370
+ <div className="flex-1 p-6 bg-slate-50/50 overflow-auto">
371
+ {!file ? (
372
+ <div className="h-full flex items-center justify-center">
373
+ <div className="text-center">
374
+ <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
375
+ <FileText className="h-10 w-10 text-slate-300" />
376
+ </div>
377
+ <p className="text-slate-400 text-sm">Upload a document to preview</p>
378
+ </div>
379
+ </div>
380
+ ) : previewUrls.length === 0 ? (
381
+ <div className="h-full flex items-center justify-center">
382
+ <div className="text-center">
383
+ <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
384
+ <FileText className="h-10 w-10 text-slate-300" />
385
+ </div>
386
+ <p className="text-slate-400 text-sm">Loading preview...</p>
387
+ </div>
388
+ </div>
389
+ ) : (
390
+ <div className="space-y-4">
391
+ {previewUrls.map((url, index) => (
392
+ <motion.div
393
+ key={index}
394
+ initial={{ opacity: 0, y: 20 }}
395
+ animate={{ opacity: 1, y: 0 }}
396
+ transition={{ delay: index * 0.1 }}
397
+ className="relative bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex items-center justify-center"
398
+ style={{
399
+ minHeight: "400px",
400
+ }}
401
+ >
402
+ {url ? (
403
+ <img
404
+ src={url}
405
+ alt={`Page ${index + 1}`}
406
+ className="w-full h-auto"
407
+ style={{
408
+ transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
409
+ maxWidth: "100%",
410
+ objectFit: "contain",
411
+ transition: "transform 0.2s ease",
412
+ }}
413
+ />
414
+ ) : (
415
+ <div className="p-8 text-center">
416
+ <p className="text-slate-400 text-sm">
417
+ {isFromHistory
418
+ ? "Original document not available for historical extractions"
419
+ : "Unable to load preview"}
420
+ </p>
421
+ </div>
422
+ )}
423
+
424
+ {/* Processing overlay */}
425
+ {isProcessing && (
426
+ <motion.div
427
+ initial={{ opacity: 0 }}
428
+ animate={{ opacity: 1 }}
429
+ className="absolute inset-0 bg-indigo-600/5 backdrop-blur-[1px] pointer-events-none"
430
+ >
431
+ <motion.div
432
+ initial={{ top: 0 }}
433
+ animate={{ top: "100%" }}
434
+ transition={{
435
+ duration: 2,
436
+ repeat: Infinity,
437
+ ease: "linear",
438
+ }}
439
+ className="absolute left-0 right-0 h-1 bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
440
+ />
441
+ </motion.div>
442
+ )}
443
+
444
+ {/* Page number */}
445
+ {previewUrls.length > 1 && (
446
+ <div className="absolute bottom-3 right-3 text-xs text-slate-400 bg-white/90 px-2 py-1 rounded">
447
+ Page {index + 1}
448
+ </div>
449
+ )}
450
+ </motion.div>
451
+ ))}
452
+ </div>
453
+ )}
454
+ </div>
455
+ </div>
456
+ );
457
+ }
frontend/src/components/ocr/ProcessingStatus.jsx CHANGED
@@ -1,123 +1,3 @@
1
- <<<<<<< HEAD
2
- import React from "react";
3
- import { motion } from "framer-motion";
4
- import {
5
- FileSearch,
6
- Cpu,
7
- TableProperties,
8
- CheckCircle2,
9
- Loader2,
10
- } from "lucide-react";
11
- import { cn } from "@/lib/utils";
12
-
13
- const steps = [
14
- { id: "upload", label: "Received", icon: FileSearch },
15
- { id: "analyze", label: "Analysis", icon: Cpu },
16
- { id: "extract", label: "Extraction", icon: TableProperties },
17
- { id: "complete", label: "Done", icon: CheckCircle2 },
18
- ];
19
-
20
- export default function ProcessingStatus({ isProcessing, isComplete, currentStage }) {
21
- const getCurrentStep = () => {
22
- if (isComplete) return 4; // Done
23
- if (!isProcessing) return 0; // Not started
24
-
25
- // Use provided currentStage or default based on isProcessing
26
- if (currentStage === "extraction") return 3; // Extraction
27
- if (currentStage === "analysis") return 2; // Analysis
28
- if (currentStage === "received") return 1; // Received
29
-
30
- // Default: if processing, start at Analysis
31
- return 2; // Analysis
32
- };
33
-
34
- const currentStep = getCurrentStep();
35
-
36
- if (!isProcessing && !isComplete) return null;
37
-
38
- return (
39
- <motion.div
40
- initial={{ opacity: 0, y: -10 }}
41
- animate={{ opacity: 1, y: 0 }}
42
- className="bg-white rounded-xl border border-slate-200 px-4 py-3"
43
- >
44
- <div className="flex items-center justify-between gap-2">
45
- {steps.map((step, index) => {
46
- const isActive = index + 1 === currentStep;
47
- const isCompleted = index + 1 < currentStep || isComplete;
48
- const Icon = step.icon;
49
-
50
- return (
51
- <React.Fragment key={step.id}>
52
- <div className="flex items-center gap-2">
53
- <motion.div
54
- initial={false}
55
- animate={{
56
- scale: (isActive && !isComplete) ? 1.05 : 1,
57
- backgroundColor: isCompleted
58
- ? "rgb(16 185 129)"
59
- : (isActive && !isComplete)
60
- ? "rgb(99 102 241)"
61
- : "rgb(241 245 249)",
62
- }}
63
- className={cn(
64
- "h-8 w-8 rounded-lg flex items-center justify-center transition-colors",
65
- (isCompleted || isActive) && "shadow-md"
66
- )}
67
- style={{
68
- boxShadow: (isActive && !isComplete)
69
- ? "0 4px 8px -2px rgba(99, 102, 241, 0.3)"
70
- : isCompleted
71
- ? "0 4px 8px -2px rgba(16, 185, 129, 0.3)"
72
- : "none",
73
- }}
74
- >
75
- {(isActive && !isComplete) ? (
76
- <motion.div
77
- animate={{ rotate: 360 }}
78
- transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
79
- >
80
- <Loader2 className="h-4 w-4 text-white" />
81
- </motion.div>
82
- ) : isCompleted ? (
83
- <CheckCircle2 className="h-4 w-4 text-white" />
84
- ) : (
85
- <Icon className={cn("h-4 w-4 text-slate-400")} />
86
- )}
87
- </motion.div>
88
- <span
89
- className={cn(
90
- "text-xs font-medium hidden sm:inline",
91
- isActive ? "text-indigo-600" : isCompleted ? "text-emerald-600" : "text-slate-400"
92
- )}
93
- >
94
- {step.label}
95
- </span>
96
- </div>
97
-
98
- {index < steps.length - 1 && (
99
- <div className="flex-1 h-0.5 mx-1 relative overflow-hidden rounded-full bg-slate-100">
100
- <motion.div
101
- initial={{ width: 0 }}
102
- animate={{
103
- width: isCompleted ? "100%" : isActive ? "50%" : "0%",
104
- }}
105
- transition={{ duration: 0.5 }}
106
- className={cn(
107
- "absolute inset-y-0 left-0",
108
- isCompleted ? "bg-emerald-500" : "bg-indigo-500"
109
- )}
110
- />
111
- </div>
112
- )}
113
- </React.Fragment>
114
- );
115
- })}
116
- </div>
117
- </motion.div>
118
- );
119
- }
120
- =======
121
  import React from "react";
122
  import { motion } from "framer-motion";
123
  import {
@@ -236,4 +116,3 @@ export default function ProcessingStatus({ isProcessing, isComplete, currentStag
236
  </motion.div>
237
  );
238
  }
239
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React from "react";
2
  import { motion } from "framer-motion";
3
  import {
 
116
  </motion.div>
117
  );
118
  }
 
frontend/src/components/ocr/UploadZone.jsx CHANGED
@@ -1,256 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useState, useEffect } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles, AlertCircle } from "lucide-react";
5
- import { cn } from "@/lib/utils";
6
- import { Input } from "@/components/ui/input";
7
-
8
- // Allowed file types
9
- const ALLOWED_TYPES = [
10
- "application/pdf",
11
- "image/png",
12
- "image/jpeg",
13
- "image/jpg",
14
- "image/tiff",
15
- "image/tif"
16
- ];
17
-
18
- // Allowed file extensions (for fallback validation)
19
- const ALLOWED_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"];
20
-
21
- // Maximum file size: 4 MB
22
- const MAX_FILE_SIZE = 4 * 1024 * 1024; // 4 MB in bytes
23
-
24
- export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFields = "", onKeyFieldsChange = () => {} }) {
25
- const [isDragging, setIsDragging] = useState(false);
26
- const [error, setError] = useState(null);
27
-
28
- const validateFile = (file) => {
29
- // Reset error
30
- setError(null);
31
-
32
- // Check file type
33
- const fileExtension = "." + file.name.split(".").pop().toLowerCase();
34
- const isValidType = ALLOWED_TYPES.includes(file.type) || ALLOWED_EXTENSIONS.includes(fileExtension);
35
-
36
- if (!isValidType) {
37
- setError("Only PDF, PNG, JPG, and TIFF files are allowed.");
38
- return false;
39
- }
40
-
41
- // Check file size
42
- if (file.size > MAX_FILE_SIZE) {
43
- const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
44
- setError(`File size exceeds 4 MB limit. Your file is ${fileSizeMB} MB.`);
45
- return false;
46
- }
47
-
48
- return true;
49
- };
50
-
51
- const handleFileSelect = (file) => {
52
- if (validateFile(file)) {
53
- setError(null);
54
- onFileSelect(file);
55
- }
56
- };
57
-
58
- const handleDragOver = (e) => {
59
- e.preventDefault();
60
- setIsDragging(true);
61
- };
62
-
63
- const handleDragLeave = () => {
64
- setIsDragging(false);
65
- };
66
-
67
- const handleDrop = (e) => {
68
- e.preventDefault();
69
- setIsDragging(false);
70
- const file = e.dataTransfer.files[0];
71
- if (file) {
72
- handleFileSelect(file);
73
- }
74
- };
75
-
76
- const getFileIcon = (type) => {
77
- if (type?.includes("image")) return Image;
78
- if (type?.includes("spreadsheet") || type?.includes("excel")) return FileSpreadsheet;
79
- return FileText;
80
- };
81
-
82
- const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText;
83
-
84
- // Clear error when file is cleared
85
- useEffect(() => {
86
- if (!selectedFile) {
87
- setError(null);
88
- }
89
- }, [selectedFile]);
90
-
91
- return (
92
- <div className="w-full">
93
- <AnimatePresence mode="wait">
94
- {!selectedFile ? (
95
- <motion.div
96
- key="upload"
97
- initial={{ opacity: 0, y: 10 }}
98
- animate={{ opacity: 1, y: 0 }}
99
- exit={{ opacity: 0, y: -10 }}
100
- transition={{ duration: 0.2 }}
101
- onDragOver={handleDragOver}
102
- onDragLeave={handleDragLeave}
103
- onDrop={handleDrop}
104
- className={cn(
105
- "relative group cursor-pointer",
106
- "border-2 border-dashed rounded-2xl",
107
- "transition-all duration-300 ease-out",
108
- isDragging
109
- ? "border-indigo-400 bg-indigo-50/50"
110
- : "border-slate-200 hover:border-indigo-300 hover:bg-slate-50/50"
111
- )}
112
- >
113
- <label className="flex flex-col items-center justify-center py-16 px-8 cursor-pointer">
114
- <motion.div
115
- animate={isDragging ? { scale: 1.1, y: -5 } : { scale: 1, y: 0 }}
116
- className={cn(
117
- "h-16 w-16 rounded-2xl flex items-center justify-center mb-6 transition-colors duration-300",
118
- isDragging
119
- ? "bg-indigo-100"
120
- : "bg-gradient-to-br from-slate-100 to-slate-50 group-hover:from-indigo-100 group-hover:to-violet-50"
121
- )}
122
- >
123
- <Upload
124
- className={cn(
125
- "h-7 w-7 transition-colors duration-300",
126
- isDragging ? "text-indigo-600" : "text-slate-400 group-hover:text-indigo-500"
127
- )}
128
- />
129
- </motion.div>
130
-
131
- <div className="text-center">
132
- <p className="text-lg font-semibold text-slate-700 mb-1">
133
- {isDragging ? "Drop your file here" : "Drop your file here, or browse"}
134
- </p>
135
- <p className="text-sm text-slate-400">
136
- Supports PDF, PNG, JPG, TIFF up to 4MB
137
- </p>
138
- </div>
139
-
140
- <div className="flex items-center gap-2 mt-6">
141
- <div className="flex -space-x-1">
142
- {[
143
- "bg-red-100 text-red-600",
144
- "bg-blue-100 text-blue-600",
145
- "bg-green-100 text-green-600",
146
- "bg-amber-100 text-amber-600",
147
- ].map((color, i) => (
148
- <div
149
- key={i}
150
- className={`h-8 w-8 rounded-lg ${color.split(" ")[0]} flex items-center justify-center border-2 border-white`}
151
- >
152
- <FileText className={`h-4 w-4 ${color.split(" ")[1]}`} />
153
- </div>
154
- ))}
155
- </div>
156
- <span className="text-xs text-slate-400 ml-2">Multiple formats supported</span>
157
- </div>
158
-
159
- <input
160
- type="file"
161
- className="hidden"
162
- accept=".pdf,.png,.jpg,.jpeg,.tiff,.tif"
163
- onChange={(e) => {
164
- const file = e.target.files[0];
165
- if (file) {
166
- handleFileSelect(file);
167
- }
168
- // Reset input so same file can be selected again after error
169
- e.target.value = "";
170
- }}
171
- />
172
- </label>
173
-
174
- {/* Decorative gradient border on hover */}
175
- <div className="absolute inset-0 -z-10 rounded-2xl bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-500 opacity-0 group-hover:opacity-10 blur-xl transition-opacity duration-500" />
176
- </motion.div>
177
- ) : (
178
- <motion.div
179
- key="selected"
180
- initial={{ opacity: 0, scale: 0.95 }}
181
- animate={{ opacity: 1, scale: 1 }}
182
- exit={{ opacity: 0, scale: 0.95 }}
183
- className="grid grid-cols-1 lg:grid-cols-2 gap-3"
184
- >
185
- {/* File Info Box */}
186
- <div className="relative bg-gradient-to-br from-indigo-50 to-violet-50 rounded-xl p-3 border border-indigo-100">
187
- <div className="flex items-center gap-3">
188
- <div className="h-10 w-10 rounded-lg bg-white shadow-sm flex items-center justify-center flex-shrink-0">
189
- <FileIcon className="h-5 w-5 text-indigo-600" />
190
- </div>
191
- <div className="flex-1 min-w-0">
192
- <p className="font-medium text-slate-800 truncate text-sm">{selectedFile.name}</p>
193
- <div className="flex items-center gap-2 text-xs text-slate-500">
194
- <span>{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</span>
195
- <span className="text-indigo-500">•</span>
196
- <span className="text-indigo-600 flex items-center gap-1">
197
- <Sparkles className="h-3 w-3" />
198
- Ready for extraction
199
- </span>
200
- </div>
201
- </div>
202
- <button
203
- onClick={onClear}
204
- className="h-8 w-8 rounded-lg bg-white hover:bg-red-50 border border-slate-200 hover:border-red-200 flex items-center justify-center text-slate-400 hover:text-red-500 transition-colors"
205
- >
206
- <X className="h-4 w-4" />
207
- </button>
208
- </div>
209
- </div>
210
-
211
- {/* Key Fields Box */}
212
- <div className="relative bg-white rounded-xl p-3 border border-slate-200">
213
- <label className="block text-xs font-medium text-slate-600 mb-1.5">
214
- <span className="font-bold">Key Fields</span> <span className="font-normal">(if required)</span>
215
- </label>
216
- <Input
217
- type="text"
218
- value={keyFields || ""}
219
- onChange={(e) => {
220
- if (onKeyFieldsChange) {
221
- onKeyFieldsChange(e.target.value);
222
- }
223
- }}
224
- placeholder="Invoice Number, Invoice Date, PO Number, Supplier Name, Total Amount, Payment terms, Additional Notes"
225
- className="h-8 text-xs border-slate-200 focus:border-indigo-300 focus:ring-indigo-200"
226
- />
227
- </div>
228
- </motion.div>
229
- )}
230
- </AnimatePresence>
231
-
232
- {/* Error Message */}
233
- {error && (
234
- <motion.div
235
- initial={{ opacity: 0, y: -10 }}
236
- animate={{ opacity: 1, y: 0 }}
237
- exit={{ opacity: 0, y: -10 }}
238
- className="mt-3 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2"
239
- >
240
- <AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" />
241
- <p className="text-sm text-red-700 flex-1">{error}</p>
242
- <button
243
- onClick={() => setError(null)}
244
- className="text-red-600 hover:text-red-800 transition-colors"
245
- >
246
- <X className="h-4 w-4" />
247
- </button>
248
- </motion.div>
249
- )}
250
- </div>
251
- );
252
- }
253
- =======
254
  import React, { useState, useEffect } from "react";
255
  import { motion, AnimatePresence } from "framer-motion";
256
  import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles, AlertCircle } from "lucide-react";
@@ -502,4 +249,253 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
502
  </div>
503
  );
504
  }
505
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useEffect } from "react";
2
  import { motion, AnimatePresence } from "framer-motion";
3
  import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles, AlertCircle } from "lucide-react";
 
249
  </div>
250
  );
251
  }
252
+ import { motion, AnimatePresence } from "framer-motion";
253
+ import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles, AlertCircle } from "lucide-react";
254
+ import { cn } from "@/lib/utils";
255
+ import { Input } from "@/components/ui/input";
256
+
257
+ // Allowed file types
258
+ const ALLOWED_TYPES = [
259
+ "application/pdf",
260
+ "image/png",
261
+ "image/jpeg",
262
+ "image/jpg",
263
+ "image/tiff",
264
+ "image/tif"
265
+ ];
266
+
267
+ // Allowed file extensions (for fallback validation)
268
+ const ALLOWED_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"];
269
+
270
+ // Maximum file size: 4 MB
271
+ const MAX_FILE_SIZE = 4 * 1024 * 1024; // 4 MB in bytes
272
+
273
+ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFields = "", onKeyFieldsChange = () => {} }) {
274
+ const [isDragging, setIsDragging] = useState(false);
275
+ const [error, setError] = useState(null);
276
+
277
+ const validateFile = (file) => {
278
+ // Reset error
279
+ setError(null);
280
+
281
+ // Check file type
282
+ const fileExtension = "." + file.name.split(".").pop().toLowerCase();
283
+ const isValidType = ALLOWED_TYPES.includes(file.type) || ALLOWED_EXTENSIONS.includes(fileExtension);
284
+
285
+ if (!isValidType) {
286
+ setError("Only PDF, PNG, JPG, and TIFF files are allowed.");
287
+ return false;
288
+ }
289
+
290
+ // Check file size
291
+ if (file.size > MAX_FILE_SIZE) {
292
+ const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
293
+ setError(`File size exceeds 4 MB limit. Your file is ${fileSizeMB} MB.`);
294
+ return false;
295
+ }
296
+
297
+ return true;
298
+ };
299
+
300
+ const handleFileSelect = (file) => {
301
+ if (validateFile(file)) {
302
+ setError(null);
303
+ onFileSelect(file);
304
+ }
305
+ };
306
+
307
+ const handleDragOver = (e) => {
308
+ e.preventDefault();
309
+ setIsDragging(true);
310
+ };
311
+
312
+ const handleDragLeave = () => {
313
+ setIsDragging(false);
314
+ };
315
+
316
+ const handleDrop = (e) => {
317
+ e.preventDefault();
318
+ setIsDragging(false);
319
+ const file = e.dataTransfer.files[0];
320
+ if (file) {
321
+ handleFileSelect(file);
322
+ }
323
+ };
324
+
325
+ const getFileIcon = (type) => {
326
+ if (type?.includes("image")) return Image;
327
+ if (type?.includes("spreadsheet") || type?.includes("excel")) return FileSpreadsheet;
328
+ return FileText;
329
+ };
330
+
331
+ const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText;
332
+
333
+ // Clear error when file is cleared
334
+ useEffect(() => {
335
+ if (!selectedFile) {
336
+ setError(null);
337
+ }
338
+ }, [selectedFile]);
339
+
340
+ return (
341
+ <div className="w-full">
342
+ <AnimatePresence mode="wait">
343
+ {!selectedFile ? (
344
+ <motion.div
345
+ key="upload"
346
+ initial={{ opacity: 0, y: 10 }}
347
+ animate={{ opacity: 1, y: 0 }}
348
+ exit={{ opacity: 0, y: -10 }}
349
+ transition={{ duration: 0.2 }}
350
+ onDragOver={handleDragOver}
351
+ onDragLeave={handleDragLeave}
352
+ onDrop={handleDrop}
353
+ className={cn(
354
+ "relative group cursor-pointer",
355
+ "border-2 border-dashed rounded-2xl",
356
+ "transition-all duration-300 ease-out",
357
+ isDragging
358
+ ? "border-indigo-400 bg-indigo-50/50"
359
+ : "border-slate-200 hover:border-indigo-300 hover:bg-slate-50/50"
360
+ )}
361
+ >
362
+ <label className="flex flex-col items-center justify-center py-16 px-8 cursor-pointer">
363
+ <motion.div
364
+ animate={isDragging ? { scale: 1.1, y: -5 } : { scale: 1, y: 0 }}
365
+ className={cn(
366
+ "h-16 w-16 rounded-2xl flex items-center justify-center mb-6 transition-colors duration-300",
367
+ isDragging
368
+ ? "bg-indigo-100"
369
+ : "bg-gradient-to-br from-slate-100 to-slate-50 group-hover:from-indigo-100 group-hover:to-violet-50"
370
+ )}
371
+ >
372
+ <Upload
373
+ className={cn(
374
+ "h-7 w-7 transition-colors duration-300",
375
+ isDragging ? "text-indigo-600" : "text-slate-400 group-hover:text-indigo-500"
376
+ )}
377
+ />
378
+ </motion.div>
379
+
380
+ <div className="text-center">
381
+ <p className="text-lg font-semibold text-slate-700 mb-1">
382
+ {isDragging ? "Drop your file here" : "Drop your file here, or browse"}
383
+ </p>
384
+ <p className="text-sm text-slate-400">
385
+ Supports PDF, PNG, JPG, TIFF up to 4MB
386
+ </p>
387
+ </div>
388
+
389
+ <div className="flex items-center gap-2 mt-6">
390
+ <div className="flex -space-x-1">
391
+ {[
392
+ "bg-red-100 text-red-600",
393
+ "bg-blue-100 text-blue-600",
394
+ "bg-green-100 text-green-600",
395
+ "bg-amber-100 text-amber-600",
396
+ ].map((color, i) => (
397
+ <div
398
+ key={i}
399
+ className={`h-8 w-8 rounded-lg ${color.split(" ")[0]} flex items-center justify-center border-2 border-white`}
400
+ >
401
+ <FileText className={`h-4 w-4 ${color.split(" ")[1]}`} />
402
+ </div>
403
+ ))}
404
+ </div>
405
+ <span className="text-xs text-slate-400 ml-2">Multiple formats supported</span>
406
+ </div>
407
+
408
+ <input
409
+ type="file"
410
+ className="hidden"
411
+ accept=".pdf,.png,.jpg,.jpeg,.tiff,.tif"
412
+ onChange={(e) => {
413
+ const file = e.target.files[0];
414
+ if (file) {
415
+ handleFileSelect(file);
416
+ }
417
+ // Reset input so same file can be selected again after error
418
+ e.target.value = "";
419
+ }}
420
+ />
421
+ </label>
422
+
423
+ {/* Decorative gradient border on hover */}
424
+ <div className="absolute inset-0 -z-10 rounded-2xl bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-500 opacity-0 group-hover:opacity-10 blur-xl transition-opacity duration-500" />
425
+ </motion.div>
426
+ ) : (
427
+ <motion.div
428
+ key="selected"
429
+ initial={{ opacity: 0, scale: 0.95 }}
430
+ animate={{ opacity: 1, scale: 1 }}
431
+ exit={{ opacity: 0, scale: 0.95 }}
432
+ className="grid grid-cols-1 lg:grid-cols-2 gap-3"
433
+ >
434
+ {/* File Info Box */}
435
+ <div className="relative bg-gradient-to-br from-indigo-50 to-violet-50 rounded-xl p-3 border border-indigo-100">
436
+ <div className="flex items-center gap-3">
437
+ <div className="h-10 w-10 rounded-lg bg-white shadow-sm flex items-center justify-center flex-shrink-0">
438
+ <FileIcon className="h-5 w-5 text-indigo-600" />
439
+ </div>
440
+ <div className="flex-1 min-w-0">
441
+ <p className="font-medium text-slate-800 truncate text-sm">{selectedFile.name}</p>
442
+ <div className="flex items-center gap-2 text-xs text-slate-500">
443
+ <span>{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</span>
444
+ <span className="text-indigo-500">•</span>
445
+ <span className="text-indigo-600 flex items-center gap-1">
446
+ <Sparkles className="h-3 w-3" />
447
+ Ready for extraction
448
+ </span>
449
+ </div>
450
+ </div>
451
+ <button
452
+ onClick={onClear}
453
+ className="h-8 w-8 rounded-lg bg-white hover:bg-red-50 border border-slate-200 hover:border-red-200 flex items-center justify-center text-slate-400 hover:text-red-500 transition-colors"
454
+ >
455
+ <X className="h-4 w-4" />
456
+ </button>
457
+ </div>
458
+ </div>
459
+
460
+ {/* Key Fields Box */}
461
+ <div className="relative bg-white rounded-xl p-3 border border-slate-200">
462
+ <label className="block text-xs font-medium text-slate-600 mb-1.5">
463
+ <span className="font-bold">Key Fields</span> <span className="font-normal">(if required)</span>
464
+ </label>
465
+ <Input
466
+ type="text"
467
+ value={keyFields || ""}
468
+ onChange={(e) => {
469
+ if (onKeyFieldsChange) {
470
+ onKeyFieldsChange(e.target.value);
471
+ }
472
+ }}
473
+ placeholder="Invoice Number, Invoice Date, PO Number, Supplier Name, Total Amount, Payment terms, Additional Notes"
474
+ className="h-8 text-xs border-slate-200 focus:border-indigo-300 focus:ring-indigo-200"
475
+ />
476
+ </div>
477
+ </motion.div>
478
+ )}
479
+ </AnimatePresence>
480
+
481
+ {/* Error Message */}
482
+ {error && (
483
+ <motion.div
484
+ initial={{ opacity: 0, y: -10 }}
485
+ animate={{ opacity: 1, y: 0 }}
486
+ exit={{ opacity: 0, y: -10 }}
487
+ className="mt-3 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2"
488
+ >
489
+ <AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" />
490
+ <p className="text-sm text-red-700 flex-1">{error}</p>
491
+ <button
492
+ onClick={() => setError(null)}
493
+ className="text-red-600 hover:text-red-800 transition-colors"
494
+ >
495
+ <X className="h-4 w-4" />
496
+ </button>
497
+ </motion.div>
498
+ )}
499
+ </div>
500
+ );
501
+ }
frontend/src/components/ui/separator.jsx CHANGED
@@ -1,21 +1,3 @@
1
- <<<<<<< HEAD
2
- import React from "react";
3
- import { cn } from "@/lib/utils";
4
-
5
- export function Separator({ className, orientation = "horizontal", ...props }) {
6
- return (
7
- <div
8
- className={cn(
9
- "shrink-0 bg-slate-200",
10
- orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
11
- className
12
- )}
13
- {...props}
14
- />
15
- );
16
- }
17
-
18
- =======
19
  import React from "react";
20
  import { cn } from "@/lib/utils";
21
 
@@ -31,5 +13,3 @@ export function Separator({ className, orientation = "horizontal", ...props }) {
31
  />
32
  );
33
  }
34
-
35
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React from "react";
2
  import { cn } from "@/lib/utils";
3
 
 
13
  />
14
  );
15
  }
 
 
frontend/src/config/firebase.js CHANGED
@@ -1,35 +1,3 @@
1
- <<<<<<< HEAD
2
- /**
3
- * Firebase configuration and initialization
4
- */
5
- import { initializeApp } from 'firebase/app';
6
- import { getAuth, GoogleAuthProvider } from 'firebase/auth';
7
-
8
- // Firebase configuration from environment variables
9
- const firebaseConfig = {
10
- apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
11
- authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
12
- projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
13
- storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
14
- messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
15
- appId: import.meta.env.VITE_FIREBASE_APP_ID,
16
- };
17
-
18
- // Initialize Firebase
19
- const app = initializeApp(firebaseConfig);
20
-
21
- // Initialize Firebase Authentication and get a reference to the service
22
- export const auth = getAuth(app);
23
-
24
- // Configure Google Auth Provider
25
- export const googleProvider = new GoogleAuthProvider();
26
- googleProvider.setCustomParameters({
27
- prompt: 'select_account'
28
- });
29
-
30
- export default app;
31
-
32
- =======
33
  /**
34
  * Firebase configuration and initialization
35
  */
@@ -59,5 +27,3 @@ googleProvider.setCustomParameters({
59
  });
60
 
61
  export default app;
62
-
63
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * Firebase configuration and initialization
3
  */
 
27
  });
28
 
29
  export default app;
 
 
frontend/src/contexts/AuthContext.jsx CHANGED
@@ -1,120 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { createContext, useContext, useState, useEffect } from "react";
3
- import { signInWithPopup, signOut as firebaseSignOut } from "firebase/auth";
4
- import { auth, googleProvider } from "@/config/firebase";
5
- import { getCurrentUser, firebaseLogin, requestOTP, verifyOTP, logout as apiLogout } from "@/services/auth";
6
-
7
- const AuthContext = createContext(null);
8
-
9
- export function AuthProvider({ children }) {
10
- const [user, setUser] = useState(null);
11
- const [loading, setLoading] = useState(true);
12
- const [token, setToken] = useState(localStorage.getItem("auth_token"));
13
-
14
- useEffect(() => {
15
- // Check if user is already authenticated
16
- if (token) {
17
- checkAuth();
18
- } else {
19
- setLoading(false);
20
- }
21
- }, [token]);
22
-
23
- const checkAuth = async () => {
24
- try {
25
- const userData = await getCurrentUser();
26
- setUser(userData);
27
- } catch (error) {
28
- // Token is invalid, clear it
29
- localStorage.removeItem("auth_token");
30
- setToken(null);
31
- setUser(null);
32
- } finally {
33
- setLoading(false);
34
- }
35
- };
36
-
37
- const handleFirebaseLogin = async () => {
38
- try {
39
- const result = await signInWithPopup(auth, googleProvider);
40
- const idToken = await result.user.getIdToken();
41
- const response = await firebaseLogin(idToken);
42
- handleAuthCallback(response.token);
43
- } catch (error) {
44
- if (error.code === 'auth/popup-closed' || error.code === 'auth/cancelled-popup-request') {
45
- // User closed popup or cancelled - don't show error
46
- return;
47
- }
48
- console.error("Firebase login error:", error);
49
- throw new Error(error.message || "Firebase authentication failed");
50
- }
51
- };
52
-
53
- const handleOTPRequest = async (email) => {
54
- try {
55
- await requestOTP(email);
56
- } catch (error) {
57
- console.error("OTP request error:", error);
58
- throw error;
59
- }
60
- };
61
-
62
- const handleOTPVerify = async (email, otp) => {
63
- try {
64
- const response = await verifyOTP(email, otp);
65
- handleAuthCallback(response.token);
66
- } catch (error) {
67
- console.error("OTP verify error:", error);
68
- throw error;
69
- }
70
- };
71
-
72
- const handleLogout = async () => {
73
- try {
74
- // Sign out from Firebase if user was using Firebase auth
75
- if (auth.currentUser) {
76
- await firebaseSignOut(auth);
77
- }
78
- await apiLogout();
79
- } catch (error) {
80
- console.error("Logout error:", error);
81
- } finally {
82
- localStorage.removeItem("auth_token");
83
- setToken(null);
84
- setUser(null);
85
- }
86
- };
87
-
88
- const handleAuthCallback = (newToken) => {
89
- localStorage.setItem("auth_token", newToken);
90
- setToken(newToken);
91
- checkAuth();
92
- };
93
-
94
- const value = {
95
- user,
96
- token,
97
- loading,
98
- firebaseLogin: handleFirebaseLogin,
99
- requestOTP: handleOTPRequest,
100
- verifyOTP: handleOTPVerify,
101
- logout: handleLogout,
102
- handleAuthCallback,
103
- isAuthenticated: !!user,
104
- };
105
-
106
- return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
107
- }
108
-
109
- export function useAuth() {
110
- const context = useContext(AuthContext);
111
- if (!context) {
112
- throw new Error("useAuth must be used within an AuthProvider");
113
- }
114
- return context;
115
- }
116
-
117
- =======
118
  import React, { createContext, useContext, useState, useEffect } from "react";
119
  import { signInWithPopup, signOut as firebaseSignOut } from "firebase/auth";
120
  import { auth, googleProvider } from "@/config/firebase";
@@ -229,5 +112,3 @@ export function useAuth() {
229
  }
230
  return context;
231
  }
232
-
233
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { createContext, useContext, useState, useEffect } from "react";
2
  import { signInWithPopup, signOut as firebaseSignOut } from "firebase/auth";
3
  import { auth, googleProvider } from "@/config/firebase";
 
112
  }
113
  return context;
114
  }
 
 
frontend/src/pages/Dashboard.jsx CHANGED
@@ -1,481 +1,3 @@
1
- <<<<<<< HEAD
2
- // frontend/src/pages/Dashboard.jsx
3
-
4
- import React, { useState, useEffect } from "react";
5
- import { useSearchParams } from "react-router-dom";
6
- import { motion } from "framer-motion";
7
- import { Sparkles, Zap, FileText, TrendingUp, Clock, AlertCircle } from "lucide-react";
8
- import { Button } from "@/components/ui/button";
9
- import UploadZone from "@/components/ocr/UploadZone";
10
- import DocumentPreview from "@/components/ocr/DocumentPreview";
11
- import ExtractionOutput from "@/components/ocr/ExtractionOutput";
12
- import ExportButtons from "@/components/ExportButtons";
13
- import ProcessingStatus from "@/components/ocr/ProcessingStatus";
14
- import UpgradeModal from "@/components/ocr/UpgradeModal";
15
- import { extractDocument, getHistory, getExtractionById } from "@/services/api";
16
-
17
- export default function Dashboard() {
18
- const [searchParams, setSearchParams] = useSearchParams();
19
- const [selectedFile, setSelectedFile] = useState(null);
20
- const [keyFields, setKeyFields] = useState("");
21
- const [isProcessing, setIsProcessing] = useState(false);
22
- const [isComplete, setIsComplete] = useState(false);
23
- const [extractionResult, setExtractionResult] = useState(null);
24
- const [error, setError] = useState(null);
25
- const [processingStage, setProcessingStage] = useState("received"); // received, analysis, extraction, done
26
- const [stats, setStats] = useState({ totalExtracted: 0, averageAccuracy: 0 });
27
- const [isLoadingFromHistory, setIsLoadingFromHistory] = useState(false);
28
- const [showUpgradeModal, setShowUpgradeModal] = useState(false);
29
-
30
- const TRIAL_LIMIT = 2; // Maximum number of extractions allowed in trial
31
-
32
- const handleFileSelect = (file) => {
33
- // Check if user has reached trial limit
34
- if (stats.totalExtracted >= TRIAL_LIMIT) {
35
- setShowUpgradeModal(true);
36
- return;
37
- }
38
- setSelectedFile(file);
39
- setIsComplete(false);
40
- setExtractionResult(null);
41
- setError(null);
42
- };
43
-
44
- const handleClear = () => {
45
- setSelectedFile(null);
46
- setKeyFields("");
47
- setIsProcessing(false);
48
- setIsComplete(false);
49
- setExtractionResult(null);
50
- setError(null);
51
- setProcessingStage("received");
52
- };
53
-
54
- // Load extraction from history if extractionId is in URL
55
- useEffect(() => {
56
- const extractionId = searchParams.get("extractionId");
57
- console.log("Dashboard useEffect - extractionId:", extractionId, "isLoadingFromHistory:", isLoadingFromHistory, "extractionResult:", extractionResult);
58
-
59
- if (extractionId && !isLoadingFromHistory) {
60
- // Only load if we don't already have this extraction loaded
61
- const currentExtractionId = extractionResult?.id;
62
- if (currentExtractionId && currentExtractionId === parseInt(extractionId)) {
63
- console.log("Extraction already loaded, skipping");
64
- return;
65
- }
66
-
67
- const loadExtractionFromHistory = async () => {
68
- setIsLoadingFromHistory(true);
69
- setError(null);
70
- try {
71
- console.log("Loading extraction from history, ID:", extractionId);
72
- const extraction = await getExtractionById(parseInt(extractionId));
73
- console.log("Extraction loaded:", extraction);
74
- console.log("Extraction fields:", extraction.fields);
75
- console.log("Fields type:", typeof extraction.fields);
76
- console.log("Fields keys:", extraction.fields ? Object.keys(extraction.fields) : "none");
77
-
78
- if (!extraction) {
79
- throw new Error("No extraction data received");
80
- }
81
-
82
- // Ensure fields is an object, not a string
83
- let fieldsData = extraction.fields || {};
84
- if (typeof fieldsData === 'string') {
85
- try {
86
- fieldsData = JSON.parse(fieldsData);
87
- } catch (e) {
88
- console.error("Failed to parse fields as JSON:", e);
89
- fieldsData = {};
90
- }
91
- }
92
-
93
- console.log("Processed fields:", fieldsData);
94
-
95
- // Create file object from base64 if available, otherwise create empty file
96
- let fileForPreview;
97
- if (extraction.fileBase64) {
98
- // Convert base64 to binary
99
- const binaryString = atob(extraction.fileBase64);
100
- const bytes = new Uint8Array(binaryString.length);
101
- for (let i = 0; i < binaryString.length; i++) {
102
- bytes[i] = binaryString.charCodeAt(i);
103
- }
104
- const fileBlob = new Blob([bytes], { type: extraction.fileType || "application/pdf" });
105
- fileForPreview = new File(
106
- [fileBlob],
107
- extraction.fileName || "document.pdf",
108
- { type: extraction.fileType || "application/pdf" }
109
- );
110
- console.log("Created file from base64:", fileForPreview.name, fileForPreview.size, "bytes");
111
- } else {
112
- // Fallback: create empty file if base64 not available
113
- const fileBlob = new Blob([], { type: extraction.fileType || "application/pdf" });
114
- fileForPreview = new File(
115
- [fileBlob],
116
- extraction.fileName || "document.pdf",
117
- { type: extraction.fileType || "application/pdf" }
118
- );
119
- console.log("No base64 available, created empty file");
120
- }
121
-
122
- // Set the extraction result - match the structure from extractDocument
123
- const result = {
124
- id: extraction.id,
125
- fields: fieldsData,
126
- confidence: extraction.confidence || 0,
127
- fieldsExtracted: extraction.fieldsExtracted || 0,
128
- totalTime: extraction.totalTime || 0,
129
- fileName: extraction.fileName,
130
- fileType: extraction.fileType,
131
- fileSize: extraction.fileSize,
132
- };
133
-
134
- console.log("Setting extraction result:", result);
135
- setExtractionResult(result);
136
- setSelectedFile(fileForPreview);
137
- setIsComplete(true);
138
- setIsProcessing(false);
139
- setProcessingStage("done");
140
-
141
- // Remove the extractionId from URL
142
- setSearchParams({});
143
- } catch (err) {
144
- console.error("Failed to load extraction from history:", err);
145
- const errorMessage = err.message || "Failed to load extraction from history";
146
- setError(errorMessage);
147
- // Don't clear the URL params on error so user can see what went wrong
148
- } finally {
149
- setIsLoadingFromHistory(false);
150
- }
151
- };
152
-
153
- loadExtractionFromHistory();
154
- }
155
- }, [searchParams, isLoadingFromHistory, setSearchParams]);
156
-
157
- // Fetch and calculate stats from history
158
- useEffect(() => {
159
- const fetchStats = async () => {
160
- try {
161
- const history = await getHistory();
162
-
163
- // Calculate total extracted (only completed extractions)
164
- const completedExtractions = history.filter(item => item.status === "completed");
165
- const totalExtracted = completedExtractions.length;
166
-
167
- // Calculate average accuracy from completed extractions
168
- const accuracies = completedExtractions
169
- .map(item => item.confidence || 0)
170
- .filter(acc => acc > 0);
171
-
172
- const averageAccuracy = accuracies.length > 0
173
- ? accuracies.reduce((sum, acc) => sum + acc, 0) / accuracies.length
174
- : 0;
175
-
176
- setStats({
177
- totalExtracted,
178
- averageAccuracy: Math.round(averageAccuracy * 10) / 10 // Round to 1 decimal place
179
- });
180
- } catch (err) {
181
- console.error("Failed to fetch stats:", err);
182
- // Keep default values on error
183
- }
184
- };
185
-
186
- // Fetch stats on mount and when extraction completes
187
- fetchStats();
188
- }, [isComplete]);
189
-
190
- const handleExtract = async () => {
191
- if (!selectedFile) return;
192
-
193
- // Check if user has reached trial limit before processing
194
- if (stats.totalExtracted >= TRIAL_LIMIT) {
195
- setShowUpgradeModal(true);
196
- return;
197
- }
198
-
199
- setIsProcessing(true);
200
- setIsComplete(false);
201
- setError(null);
202
- setExtractionResult(null);
203
- setProcessingStage("received");
204
-
205
- // Move to Analysis stage immediately after starting
206
- setTimeout(() => {
207
- setProcessingStage("analysis");
208
- }, 100);
209
-
210
- // Move to Extraction stage after analysis phase (2.5 seconds)
211
- let extractionTimer = setTimeout(() => {
212
- setProcessingStage("extraction");
213
- }, 2500);
214
-
215
- try {
216
- const result = await extractDocument(selectedFile, keyFields);
217
-
218
- // Clear the extraction timer
219
- clearTimeout(extractionTimer);
220
-
221
- // Move to extraction stage if not already there, then to done
222
- setProcessingStage("extraction");
223
-
224
- // Small delay to show extraction stage, then move to done when results are rendered
225
- setTimeout(() => {
226
- setProcessingStage("done");
227
- setExtractionResult(result);
228
- setIsComplete(true);
229
- setIsProcessing(false);
230
- }, 500); // Give time to see extraction stage
231
- } catch (err) {
232
- clearTimeout(extractionTimer);
233
- console.error("Extraction error:", err);
234
- setError(err.message || "Failed to extract document. Please try again.");
235
- setIsComplete(false);
236
- setProcessingStage("received");
237
- setIsProcessing(false);
238
- }
239
- };
240
-
241
- return (
242
- <div className="min-h-screen bg-[#FAFAFA]">
243
- {/* Header */}
244
- <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16">
245
- <div className="px-8 h-full flex items-center justify-between">
246
- <div>
247
- <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight">
248
- Multi-Lingual Document Extraction
249
- </h1>
250
- <p className="text-sm text-slate-500 leading-tight">
251
- Upload any document and extract structured data with VRP (No LLM)
252
- </p>
253
- </div>
254
- <div className="flex items-center gap-3">
255
- {/* Stats Pills */}
256
- <div className="hidden lg:flex items-center gap-2">
257
- <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-lg">
258
- <FileText className="h-4 w-4 text-slate-500" />
259
- <span className="text-sm font-medium text-slate-700">
260
- {stats.totalExtracted}/{TRIAL_LIMIT} Used
261
- </span>
262
- </div>
263
- <div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 rounded-lg">
264
- <TrendingUp className="h-4 w-4 text-emerald-600" />
265
- <span className="text-sm font-medium text-emerald-700">
266
- {stats.averageAccuracy > 0 ? `${stats.averageAccuracy}%` : "0%"} Accuracy
267
- </span>
268
- </div>
269
- </div>
270
-
271
- <ExportButtons isComplete={isComplete} extractionResult={extractionResult} />
272
- </div>
273
- </div>
274
- </header>
275
-
276
- {/* Main Content */}
277
- <div className="p-8">
278
- {/* Upload Section */}
279
- <motion.div
280
- initial={{ opacity: 0, y: 20 }}
281
- animate={{ opacity: 1, y: 0 }}
282
- className="max-w-3xl mx-auto mb-4"
283
- >
284
- <UploadZone
285
- onFileSelect={handleFileSelect}
286
- selectedFile={selectedFile}
287
- onClear={handleClear}
288
- keyFields={keyFields}
289
- onKeyFieldsChange={setKeyFields}
290
- />
291
-
292
- {/* Extract Button */}
293
- {selectedFile && !isProcessing && !isComplete && (
294
- <motion.div
295
- initial={{ opacity: 0, y: 10 }}
296
- animate={{ opacity: 1, y: 0 }}
297
- className="mt-4 flex justify-center"
298
- >
299
- <Button
300
- onClick={handleExtract}
301
- size="lg"
302
- className="h-14 px-8 rounded-2xl font-semibold text-base bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-xl shadow-indigo-500/25 hover:shadow-2xl hover:shadow-indigo-500/30 transition-all duration-300 hover:-translate-y-0.5"
303
- >
304
- <Sparkles className="h-5 w-5 mr-2" />
305
- Start Extraction
306
- <Zap className="h-4 w-4 ml-2 opacity-70" />
307
- </Button>
308
- </motion.div>
309
- )}
310
- </motion.div>
311
-
312
- {/* Error Message */}
313
- {error && (
314
- <motion.div
315
- initial={{ opacity: 0, y: -10 }}
316
- animate={{ opacity: 1, y: 0 }}
317
- className="max-w-3xl mx-auto mb-6"
318
- >
319
- <div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex items-start gap-3">
320
- <AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
321
- <div className="flex-1">
322
- <h3 className="font-semibold text-red-900 mb-1">Extraction Failed</h3>
323
- <p className="text-sm text-red-700">{error}</p>
324
- </div>
325
- <button
326
- onClick={() => setError(null)}
327
- className="text-red-400 hover:text-red-600 transition-colors"
328
- >
329
- ×
330
- </button>
331
- </div>
332
- </motion.div>
333
- )}
334
-
335
- {/* Loading from History */}
336
- {isLoadingFromHistory && (
337
- <motion.div
338
- initial={{ opacity: 0, y: -10 }}
339
- animate={{ opacity: 1, y: 0 }}
340
- className="max-w-3xl mx-auto mb-6"
341
- >
342
- <div className="bg-blue-50 border border-blue-200 rounded-2xl p-4 flex items-center gap-3">
343
- <Clock className="h-5 w-5 text-blue-600 animate-spin" />
344
- <div className="flex-1">
345
- <h3 className="font-semibold text-blue-900 mb-1">Loading extraction...</h3>
346
- <p className="text-sm text-blue-700">Retrieving extraction data from history</p>
347
- </div>
348
- </div>
349
- </motion.div>
350
- )}
351
-
352
- {/* Processing Status */}
353
- {(isProcessing || isComplete) && !isLoadingFromHistory && (
354
- <div className="max-w-3xl mx-auto mb-4">
355
- <ProcessingStatus
356
- isProcessing={isProcessing}
357
- isComplete={isComplete}
358
- currentStage={processingStage}
359
- />
360
- </div>
361
- )}
362
-
363
- {/* Split View */}
364
- {selectedFile && (
365
- <motion.div
366
- initial={{ opacity: 0, y: 20 }}
367
- animate={{ opacity: 1, y: 0 }}
368
- transition={{ delay: 0.2 }}
369
- className="grid grid-cols-1 lg:grid-cols-2 gap-4"
370
- style={{ height: "calc(100vh - 320px)", minHeight: "450px" }}
371
- >
372
- <DocumentPreview
373
- file={selectedFile}
374
- isProcessing={isProcessing}
375
- isFromHistory={!!extractionResult?.id}
376
- />
377
- <ExtractionOutput
378
- hasFile={!!selectedFile}
379
- isProcessing={isProcessing}
380
- isComplete={isComplete}
381
- extractionResult={extractionResult}
382
- onNewUpload={handleClear}
383
- />
384
- </motion.div>
385
- )}
386
-
387
- {/* Empty State Features */}
388
- {!selectedFile && (
389
- <motion.div
390
- initial={{ opacity: 0 }}
391
- animate={{ opacity: 1 }}
392
- transition={{ delay: 0.3 }}
393
- className="max-w-5xl mx-auto mt-12"
394
- >
395
- <div className="text-center mb-10">
396
- <h2 className="text-2xl font-bold text-slate-900 mb-2">
397
- Pure Agentic Document Intelligence
398
- </h2>
399
- <p className="text-slate-500">
400
- Extract structured data from any document without LLM using VRP (Visual Resoning Processor)
401
- </p>
402
- </div>
403
-
404
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
405
- {[
406
- {
407
- icon: Zap,
408
- title: "Lightning Fast",
409
- description:
410
- "Process documents faster with our agentic pipeline",
411
- color: "amber",
412
- },
413
- {
414
- icon: Sparkles,
415
- title: `${stats.averageAccuracy > 0 ? stats.averageAccuracy : "99.8"}% Accuracy`,
416
- description:
417
- "Industry-leading extraction accuracy",
418
- color: "indigo",
419
- },
420
- {
421
- icon: Clock,
422
- title: "Any Format",
423
- description:
424
- "Support for PDF, images, and scanned documents",
425
- color: "emerald",
426
- },
427
- ].map((feature, index) => (
428
- <motion.div
429
- key={feature.title}
430
- initial={{ opacity: 0, y: 20 }}
431
- animate={{ opacity: 1, y: 0 }}
432
- transition={{ delay: 0.4 + index * 0.1 }}
433
- className="group 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"
434
- >
435
- <div
436
- className={`h-12 w-12 rounded-xl bg-${feature.color}-50 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}
437
- >
438
- <feature.icon
439
- className={`h-6 w-6 text-${feature.color}-600`}
440
- />
441
- </div>
442
- <h3 className="font-semibold text-slate-900 mb-2">
443
- {feature.title}
444
- </h3>
445
- <p className="text-sm text-slate-500 leading-relaxed">
446
- {feature.description}
447
- </p>
448
- </motion.div>
449
- ))}
450
- </div>
451
-
452
- {/* Supported Formats */}
453
- <div className="mt-12 text-center">
454
- <p className="text-xs text-slate-400 uppercase tracking-wider mb-4 font-medium">
455
- Supported Formats
456
- </p>
457
- <div className="flex items-center justify-center gap-6 flex-wrap">
458
- {["PDF", "PNG", "JPG", "TIFF", "JPEG"].map((format) => (
459
- <div
460
- key={format}
461
- className="flex items-center gap-2 text-slate-400"
462
- >
463
- <FileText className="h-4 w-4" />
464
- <span className="text-sm font-medium">{format}</span>
465
- </div>
466
- ))}
467
- </div>
468
- </div>
469
- </motion.div>
470
- )}
471
- </div>
472
-
473
- {/* Upgrade Modal */}
474
- <UpgradeModal open={showUpgradeModal} onClose={() => setShowUpgradeModal(false)} />
475
- </div>
476
- );
477
- }
478
- =======
479
  // frontend/src/pages/Dashboard.jsx
480
 
481
  import React, { useState, useEffect } from "react";
@@ -952,4 +474,478 @@ export default function Dashboard() {
952
  </div>
953
  );
954
  }
955
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // frontend/src/pages/Dashboard.jsx
2
 
3
  import React, { useState, useEffect } from "react";
 
474
  </div>
475
  );
476
  }
477
+
478
+ import React, { useState, useEffect } from "react";
479
+ import { useSearchParams } from "react-router-dom";
480
+ import { motion } from "framer-motion";
481
+ import { Sparkles, Zap, FileText, TrendingUp, Clock, AlertCircle } from "lucide-react";
482
+ import { Button } from "@/components/ui/button";
483
+ import UploadZone from "@/components/ocr/UploadZone";
484
+ import DocumentPreview from "@/components/ocr/DocumentPreview";
485
+ import ExtractionOutput from "@/components/ocr/ExtractionOutput";
486
+ import ExportButtons from "@/components/ExportButtons";
487
+ import ProcessingStatus from "@/components/ocr/ProcessingStatus";
488
+ import UpgradeModal from "@/components/ocr/UpgradeModal";
489
+ import { extractDocument, getHistory, getExtractionById } from "@/services/api";
490
+
491
+ export default function Dashboard() {
492
+ const [searchParams, setSearchParams] = useSearchParams();
493
+ const [selectedFile, setSelectedFile] = useState(null);
494
+ const [keyFields, setKeyFields] = useState("");
495
+ const [isProcessing, setIsProcessing] = useState(false);
496
+ const [isComplete, setIsComplete] = useState(false);
497
+ const [extractionResult, setExtractionResult] = useState(null);
498
+ const [error, setError] = useState(null);
499
+ const [processingStage, setProcessingStage] = useState("received"); // received, analysis, extraction, done
500
+ const [stats, setStats] = useState({ totalExtracted: 0, averageAccuracy: 0 });
501
+ const [isLoadingFromHistory, setIsLoadingFromHistory] = useState(false);
502
+ const [showUpgradeModal, setShowUpgradeModal] = useState(false);
503
+
504
+ const TRIAL_LIMIT = 2; // Maximum number of extractions allowed in trial
505
+
506
+ const handleFileSelect = (file) => {
507
+ // Check if user has reached trial limit
508
+ if (stats.totalExtracted >= TRIAL_LIMIT) {
509
+ setShowUpgradeModal(true);
510
+ return;
511
+ }
512
+ setSelectedFile(file);
513
+ setIsComplete(false);
514
+ setExtractionResult(null);
515
+ setError(null);
516
+ };
517
+
518
+ const handleClear = () => {
519
+ setSelectedFile(null);
520
+ setKeyFields("");
521
+ setIsProcessing(false);
522
+ setIsComplete(false);
523
+ setExtractionResult(null);
524
+ setError(null);
525
+ setProcessingStage("received");
526
+ };
527
+
528
+ // Load extraction from history if extractionId is in URL
529
+ useEffect(() => {
530
+ const extractionId = searchParams.get("extractionId");
531
+ console.log("Dashboard useEffect - extractionId:", extractionId, "isLoadingFromHistory:", isLoadingFromHistory, "extractionResult:", extractionResult);
532
+
533
+ if (extractionId && !isLoadingFromHistory) {
534
+ // Only load if we don't already have this extraction loaded
535
+ const currentExtractionId = extractionResult?.id;
536
+ if (currentExtractionId && currentExtractionId === parseInt(extractionId)) {
537
+ console.log("Extraction already loaded, skipping");
538
+ return;
539
+ }
540
+
541
+ const loadExtractionFromHistory = async () => {
542
+ setIsLoadingFromHistory(true);
543
+ setError(null);
544
+ try {
545
+ console.log("Loading extraction from history, ID:", extractionId);
546
+ const extraction = await getExtractionById(parseInt(extractionId));
547
+ console.log("Extraction loaded:", extraction);
548
+ console.log("Extraction fields:", extraction.fields);
549
+ console.log("Fields type:", typeof extraction.fields);
550
+ console.log("Fields keys:", extraction.fields ? Object.keys(extraction.fields) : "none");
551
+
552
+ if (!extraction) {
553
+ throw new Error("No extraction data received");
554
+ }
555
+
556
+ // Ensure fields is an object, not a string
557
+ let fieldsData = extraction.fields || {};
558
+ if (typeof fieldsData === 'string') {
559
+ try {
560
+ fieldsData = JSON.parse(fieldsData);
561
+ } catch (e) {
562
+ console.error("Failed to parse fields as JSON:", e);
563
+ fieldsData = {};
564
+ }
565
+ }
566
+
567
+ console.log("Processed fields:", fieldsData);
568
+
569
+ // Create file object from base64 if available, otherwise create empty file
570
+ let fileForPreview;
571
+ if (extraction.fileBase64) {
572
+ // Convert base64 to binary
573
+ const binaryString = atob(extraction.fileBase64);
574
+ const bytes = new Uint8Array(binaryString.length);
575
+ for (let i = 0; i < binaryString.length; i++) {
576
+ bytes[i] = binaryString.charCodeAt(i);
577
+ }
578
+ const fileBlob = new Blob([bytes], { type: extraction.fileType || "application/pdf" });
579
+ fileForPreview = new File(
580
+ [fileBlob],
581
+ extraction.fileName || "document.pdf",
582
+ { type: extraction.fileType || "application/pdf" }
583
+ );
584
+ console.log("Created file from base64:", fileForPreview.name, fileForPreview.size, "bytes");
585
+ } else {
586
+ // Fallback: create empty file if base64 not available
587
+ const fileBlob = new Blob([], { type: extraction.fileType || "application/pdf" });
588
+ fileForPreview = new File(
589
+ [fileBlob],
590
+ extraction.fileName || "document.pdf",
591
+ { type: extraction.fileType || "application/pdf" }
592
+ );
593
+ console.log("No base64 available, created empty file");
594
+ }
595
+
596
+ // Set the extraction result - match the structure from extractDocument
597
+ const result = {
598
+ id: extraction.id,
599
+ fields: fieldsData,
600
+ confidence: extraction.confidence || 0,
601
+ fieldsExtracted: extraction.fieldsExtracted || 0,
602
+ totalTime: extraction.totalTime || 0,
603
+ fileName: extraction.fileName,
604
+ fileType: extraction.fileType,
605
+ fileSize: extraction.fileSize,
606
+ };
607
+
608
+ console.log("Setting extraction result:", result);
609
+ setExtractionResult(result);
610
+ setSelectedFile(fileForPreview);
611
+ setIsComplete(true);
612
+ setIsProcessing(false);
613
+ setProcessingStage("done");
614
+
615
+ // Remove the extractionId from URL
616
+ setSearchParams({});
617
+ } catch (err) {
618
+ console.error("Failed to load extraction from history:", err);
619
+ const errorMessage = err.message || "Failed to load extraction from history";
620
+ setError(errorMessage);
621
+ // Don't clear the URL params on error so user can see what went wrong
622
+ } finally {
623
+ setIsLoadingFromHistory(false);
624
+ }
625
+ };
626
+
627
+ loadExtractionFromHistory();
628
+ }
629
+ }, [searchParams, isLoadingFromHistory, setSearchParams]);
630
+
631
+ // Fetch and calculate stats from history
632
+ useEffect(() => {
633
+ const fetchStats = async () => {
634
+ try {
635
+ const history = await getHistory();
636
+
637
+ // Calculate total extracted (only completed extractions)
638
+ const completedExtractions = history.filter(item => item.status === "completed");
639
+ const totalExtracted = completedExtractions.length;
640
+
641
+ // Calculate average accuracy from completed extractions
642
+ const accuracies = completedExtractions
643
+ .map(item => item.confidence || 0)
644
+ .filter(acc => acc > 0);
645
+
646
+ const averageAccuracy = accuracies.length > 0
647
+ ? accuracies.reduce((sum, acc) => sum + acc, 0) / accuracies.length
648
+ : 0;
649
+
650
+ setStats({
651
+ totalExtracted,
652
+ averageAccuracy: Math.round(averageAccuracy * 10) / 10 // Round to 1 decimal place
653
+ });
654
+ } catch (err) {
655
+ console.error("Failed to fetch stats:", err);
656
+ // Keep default values on error
657
+ }
658
+ };
659
+
660
+ // Fetch stats on mount and when extraction completes
661
+ fetchStats();
662
+ }, [isComplete]);
663
+
664
+ const handleExtract = async () => {
665
+ if (!selectedFile) return;
666
+
667
+ // Check if user has reached trial limit before processing
668
+ if (stats.totalExtracted >= TRIAL_LIMIT) {
669
+ setShowUpgradeModal(true);
670
+ return;
671
+ }
672
+
673
+ setIsProcessing(true);
674
+ setIsComplete(false);
675
+ setError(null);
676
+ setExtractionResult(null);
677
+ setProcessingStage("received");
678
+
679
+ // Move to Analysis stage immediately after starting
680
+ setTimeout(() => {
681
+ setProcessingStage("analysis");
682
+ }, 100);
683
+
684
+ // Move to Extraction stage after analysis phase (2.5 seconds)
685
+ let extractionTimer = setTimeout(() => {
686
+ setProcessingStage("extraction");
687
+ }, 2500);
688
+
689
+ try {
690
+ const result = await extractDocument(selectedFile, keyFields);
691
+
692
+ // Clear the extraction timer
693
+ clearTimeout(extractionTimer);
694
+
695
+ // Move to extraction stage if not already there, then to done
696
+ setProcessingStage("extraction");
697
+
698
+ // Small delay to show extraction stage, then move to done when results are rendered
699
+ setTimeout(() => {
700
+ setProcessingStage("done");
701
+ setExtractionResult(result);
702
+ setIsComplete(true);
703
+ setIsProcessing(false);
704
+ }, 500); // Give time to see extraction stage
705
+ } catch (err) {
706
+ clearTimeout(extractionTimer);
707
+ console.error("Extraction error:", err);
708
+ setError(err.message || "Failed to extract document. Please try again.");
709
+ setIsComplete(false);
710
+ setProcessingStage("received");
711
+ setIsProcessing(false);
712
+ }
713
+ };
714
+
715
+ return (
716
+ <div className="min-h-screen bg-[#FAFAFA]">
717
+ {/* Header */}
718
+ <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16">
719
+ <div className="px-8 h-full flex items-center justify-between">
720
+ <div>
721
+ <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight">
722
+ Multi-Lingual Document Extraction
723
+ </h1>
724
+ <p className="text-sm text-slate-500 leading-tight">
725
+ Upload any document and extract structured data with VRP (No LLM)
726
+ </p>
727
+ </div>
728
+ <div className="flex items-center gap-3">
729
+ {/* Stats Pills */}
730
+ <div className="hidden lg:flex items-center gap-2">
731
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-lg">
732
+ <FileText className="h-4 w-4 text-slate-500" />
733
+ <span className="text-sm font-medium text-slate-700">
734
+ {stats.totalExtracted}/{TRIAL_LIMIT} Used
735
+ </span>
736
+ </div>
737
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 rounded-lg">
738
+ <TrendingUp className="h-4 w-4 text-emerald-600" />
739
+ <span className="text-sm font-medium text-emerald-700">
740
+ {stats.averageAccuracy > 0 ? `${stats.averageAccuracy}%` : "0%"} Accuracy
741
+ </span>
742
+ </div>
743
+ </div>
744
+
745
+ <ExportButtons isComplete={isComplete} extractionResult={extractionResult} />
746
+ </div>
747
+ </div>
748
+ </header>
749
+
750
+ {/* Main Content */}
751
+ <div className="p-8">
752
+ {/* Upload Section */}
753
+ <motion.div
754
+ initial={{ opacity: 0, y: 20 }}
755
+ animate={{ opacity: 1, y: 0 }}
756
+ className="max-w-3xl mx-auto mb-4"
757
+ >
758
+ <UploadZone
759
+ onFileSelect={handleFileSelect}
760
+ selectedFile={selectedFile}
761
+ onClear={handleClear}
762
+ keyFields={keyFields}
763
+ onKeyFieldsChange={setKeyFields}
764
+ />
765
+
766
+ {/* Extract Button */}
767
+ {selectedFile && !isProcessing && !isComplete && (
768
+ <motion.div
769
+ initial={{ opacity: 0, y: 10 }}
770
+ animate={{ opacity: 1, y: 0 }}
771
+ className="mt-4 flex justify-center"
772
+ >
773
+ <Button
774
+ onClick={handleExtract}
775
+ size="lg"
776
+ className="h-14 px-8 rounded-2xl font-semibold text-base bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-xl shadow-indigo-500/25 hover:shadow-2xl hover:shadow-indigo-500/30 transition-all duration-300 hover:-translate-y-0.5"
777
+ >
778
+ <Sparkles className="h-5 w-5 mr-2" />
779
+ Start Extraction
780
+ <Zap className="h-4 w-4 ml-2 opacity-70" />
781
+ </Button>
782
+ </motion.div>
783
+ )}
784
+ </motion.div>
785
+
786
+ {/* Error Message */}
787
+ {error && (
788
+ <motion.div
789
+ initial={{ opacity: 0, y: -10 }}
790
+ animate={{ opacity: 1, y: 0 }}
791
+ className="max-w-3xl mx-auto mb-6"
792
+ >
793
+ <div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex items-start gap-3">
794
+ <AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
795
+ <div className="flex-1">
796
+ <h3 className="font-semibold text-red-900 mb-1">Extraction Failed</h3>
797
+ <p className="text-sm text-red-700">{error}</p>
798
+ </div>
799
+ <button
800
+ onClick={() => setError(null)}
801
+ className="text-red-400 hover:text-red-600 transition-colors"
802
+ >
803
+ ×
804
+ </button>
805
+ </div>
806
+ </motion.div>
807
+ )}
808
+
809
+ {/* Loading from History */}
810
+ {isLoadingFromHistory && (
811
+ <motion.div
812
+ initial={{ opacity: 0, y: -10 }}
813
+ animate={{ opacity: 1, y: 0 }}
814
+ className="max-w-3xl mx-auto mb-6"
815
+ >
816
+ <div className="bg-blue-50 border border-blue-200 rounded-2xl p-4 flex items-center gap-3">
817
+ <Clock className="h-5 w-5 text-blue-600 animate-spin" />
818
+ <div className="flex-1">
819
+ <h3 className="font-semibold text-blue-900 mb-1">Loading extraction...</h3>
820
+ <p className="text-sm text-blue-700">Retrieving extraction data from history</p>
821
+ </div>
822
+ </div>
823
+ </motion.div>
824
+ )}
825
+
826
+ {/* Processing Status */}
827
+ {(isProcessing || isComplete) && !isLoadingFromHistory && (
828
+ <div className="max-w-3xl mx-auto mb-4">
829
+ <ProcessingStatus
830
+ isProcessing={isProcessing}
831
+ isComplete={isComplete}
832
+ currentStage={processingStage}
833
+ />
834
+ </div>
835
+ )}
836
+
837
+ {/* Split View */}
838
+ {selectedFile && (
839
+ <motion.div
840
+ initial={{ opacity: 0, y: 20 }}
841
+ animate={{ opacity: 1, y: 0 }}
842
+ transition={{ delay: 0.2 }}
843
+ className="grid grid-cols-1 lg:grid-cols-2 gap-4"
844
+ style={{ height: "calc(100vh - 320px)", minHeight: "450px" }}
845
+ >
846
+ <DocumentPreview
847
+ file={selectedFile}
848
+ isProcessing={isProcessing}
849
+ isFromHistory={!!extractionResult?.id}
850
+ />
851
+ <ExtractionOutput
852
+ hasFile={!!selectedFile}
853
+ isProcessing={isProcessing}
854
+ isComplete={isComplete}
855
+ extractionResult={extractionResult}
856
+ onNewUpload={handleClear}
857
+ />
858
+ </motion.div>
859
+ )}
860
+
861
+ {/* Empty State Features */}
862
+ {!selectedFile && (
863
+ <motion.div
864
+ initial={{ opacity: 0 }}
865
+ animate={{ opacity: 1 }}
866
+ transition={{ delay: 0.3 }}
867
+ className="max-w-5xl mx-auto mt-12"
868
+ >
869
+ <div className="text-center mb-10">
870
+ <h2 className="text-2xl font-bold text-slate-900 mb-2">
871
+ Pure Agentic Document Intelligence
872
+ </h2>
873
+ <p className="text-slate-500">
874
+ Extract structured data from any document without LLM using VRP (Visual Resoning Processor)
875
+ </p>
876
+ </div>
877
+
878
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
879
+ {[
880
+ {
881
+ icon: Zap,
882
+ title: "Lightning Fast",
883
+ description:
884
+ "Process documents faster with our agentic pipeline",
885
+ color: "amber",
886
+ },
887
+ {
888
+ icon: Sparkles,
889
+ title: `${stats.averageAccuracy > 0 ? stats.averageAccuracy : "99.8"}% Accuracy`,
890
+ description:
891
+ "Industry-leading extraction accuracy",
892
+ color: "indigo",
893
+ },
894
+ {
895
+ icon: Clock,
896
+ title: "Any Format",
897
+ description:
898
+ "Support for PDF, images, and scanned documents",
899
+ color: "emerald",
900
+ },
901
+ ].map((feature, index) => (
902
+ <motion.div
903
+ key={feature.title}
904
+ initial={{ opacity: 0, y: 20 }}
905
+ animate={{ opacity: 1, y: 0 }}
906
+ transition={{ delay: 0.4 + index * 0.1 }}
907
+ className="group 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"
908
+ >
909
+ <div
910
+ className={`h-12 w-12 rounded-xl bg-${feature.color}-50 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}
911
+ >
912
+ <feature.icon
913
+ className={`h-6 w-6 text-${feature.color}-600`}
914
+ />
915
+ </div>
916
+ <h3 className="font-semibold text-slate-900 mb-2">
917
+ {feature.title}
918
+ </h3>
919
+ <p className="text-sm text-slate-500 leading-relaxed">
920
+ {feature.description}
921
+ </p>
922
+ </motion.div>
923
+ ))}
924
+ </div>
925
+
926
+ {/* Supported Formats */}
927
+ <div className="mt-12 text-center">
928
+ <p className="text-xs text-slate-400 uppercase tracking-wider mb-4 font-medium">
929
+ Supported Formats
930
+ </p>
931
+ <div className="flex items-center justify-center gap-6 flex-wrap">
932
+ {["PDF", "PNG", "JPG", "TIFF", "JPEG"].map((format) => (
933
+ <div
934
+ key={format}
935
+ className="flex items-center gap-2 text-slate-400"
936
+ >
937
+ <FileText className="h-4 w-4" />
938
+ <span className="text-sm font-medium">{format}</span>
939
+ </div>
940
+ ))}
941
+ </div>
942
+ </div>
943
+ </motion.div>
944
+ )}
945
+ </div>
946
+
947
+ {/* Upgrade Modal */}
948
+ <UpgradeModal open={showUpgradeModal} onClose={() => setShowUpgradeModal(false)} />
949
+ </div>
950
+ );
951
+ }
frontend/src/pages/ShareHandler.jsx CHANGED
@@ -1,100 +1,3 @@
1
- <<<<<<< HEAD
2
- import React, { useEffect, useState } from "react";
3
- import { useParams, useNavigate } from "react-router-dom";
4
- import { useAuth } from "@/contexts/AuthContext";
5
- import { accessSharedExtraction } from "@/services/api";
6
- import LoginForm from "@/components/auth/LoginForm";
7
-
8
- export default function ShareHandler() {
9
- const { token } = useParams();
10
- const navigate = useNavigate();
11
- const { isAuthenticated, loading } = useAuth();
12
- const [isProcessing, setIsProcessing] = useState(false);
13
- const [error, setError] = useState(null);
14
-
15
- useEffect(() => {
16
- const processShare = async () => {
17
- if (loading) return; // Wait for auth to load
18
-
19
- if (!isAuthenticated) {
20
- // User not logged in - they'll be shown login form
21
- // After login, AuthContext will trigger a re-render and this will run again
22
- return;
23
- }
24
-
25
- // User is authenticated, process the share
26
- if (isProcessing) return; // Prevent duplicate calls
27
- setIsProcessing(true);
28
- setError(null);
29
-
30
- try {
31
- const result = await accessSharedExtraction(token);
32
- if (result.success && result.extraction_id) {
33
- // Redirect to history page with the extraction ID
34
- navigate(`/history?extractionId=${result.extraction_id}`);
35
- } else {
36
- setError("Failed to access shared extraction");
37
- }
38
- } catch (err) {
39
- console.error("Share access error:", err);
40
- setError(err.message || "Failed to access shared extraction");
41
- // Still redirect to history after 3 seconds
42
- setTimeout(() => {
43
- navigate("/history");
44
- }, 3000);
45
- } finally {
46
- setIsProcessing(false);
47
- }
48
- };
49
-
50
- processShare();
51
- // eslint-disable-next-line react-hooks/exhaustive-deps
52
- }, [token, isAuthenticated, loading]);
53
-
54
- // Show login form if not authenticated
55
- if (!isAuthenticated && !loading) {
56
- return <LoginForm />;
57
- }
58
-
59
- // Show loading state while processing
60
- if (isProcessing || loading) {
61
- return (
62
- <div className="min-h-screen flex items-center justify-center bg-[#FAFAFA]">
63
- <div className="text-center">
64
- <div className="h-16 w-16 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4 animate-pulse">
65
- <div className="h-8 w-8 rounded-lg bg-indigo-600"></div>
66
- </div>
67
- <p className="text-slate-600">Loading shared extraction...</p>
68
- </div>
69
- </div>
70
- );
71
- }
72
-
73
- // Show error state
74
- if (error) {
75
- return (
76
- <div className="min-h-screen flex items-center justify-center bg-[#FAFAFA]">
77
- <div className="text-center max-w-md mx-4">
78
- <div className="h-16 w-16 mx-auto rounded-2xl bg-red-100 flex items-center justify-center mb-4">
79
- <div className="h-8 w-8 rounded-lg bg-red-600"></div>
80
- </div>
81
- <h2 className="text-xl font-semibold text-slate-900 mb-2">Error</h2>
82
- <p className="text-slate-600 mb-4">{error}</p>
83
- <button
84
- onClick={() => navigate("/history")}
85
- className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
86
- >
87
- Go to History
88
- </button>
89
- </div>
90
- </div>
91
- );
92
- }
93
-
94
- return null;
95
- }
96
-
97
- =======
98
  import React, { useEffect, useState } from "react";
99
  import { useParams, useNavigate } from "react-router-dom";
100
  import { useAuth } from "@/contexts/AuthContext";
@@ -189,5 +92,3 @@ export default function ShareHandler() {
189
 
190
  return null;
191
  }
192
-
193
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useEffect, useState } from "react";
2
  import { useParams, useNavigate } from "react-router-dom";
3
  import { useAuth } from "@/contexts/AuthContext";
 
92
 
93
  return null;
94
  }
 
 
frontend/src/services/auth.js CHANGED
@@ -1,116 +1,3 @@
1
- <<<<<<< HEAD
2
- /**
3
- * Authentication service for Firebase and OTP authentication
4
- */
5
-
6
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "";
7
-
8
- /**
9
- * Get the current authenticated user
10
- * @returns {Promise<Object>} User object
11
- */
12
- export async function getCurrentUser() {
13
- const token = localStorage.getItem("auth_token");
14
- if (!token) {
15
- throw new Error("No token found");
16
- }
17
-
18
- const response = await fetch(`${API_BASE_URL}/api/auth/me`, {
19
- method: "GET",
20
- headers: {
21
- Authorization: `Bearer ${token}`,
22
- },
23
- });
24
-
25
- if (!response.ok) {
26
- if (response.status === 401) {
27
- localStorage.removeItem("auth_token");
28
- }
29
- const errorData = await response.json().catch(() => ({}));
30
- throw new Error(errorData.detail || "Failed to get user");
31
- }
32
-
33
- return await response.json();
34
- }
35
-
36
- /**
37
- * Login with Firebase ID token
38
- * @param {string} idToken - Firebase ID token
39
- * @returns {Promise<Object>} Response with token and user
40
- */
41
- export async function firebaseLogin(idToken) {
42
- const response = await fetch(`${API_BASE_URL}/api/auth/firebase/login`, {
43
- method: "POST",
44
- headers: {
45
- "Content-Type": "application/json",
46
- },
47
- body: JSON.stringify({ id_token: idToken }),
48
- });
49
-
50
- if (!response.ok) {
51
- const errorData = await response.json().catch(() => ({}));
52
- throw new Error(errorData.detail || "Firebase login failed");
53
- }
54
-
55
- return await response.json();
56
- }
57
-
58
- /**
59
- * Request OTP for email login
60
- * @param {string} email - Email address
61
- * @returns {Promise<Object>} Response with success message
62
- */
63
- export async function requestOTP(email) {
64
- const response = await fetch(`${API_BASE_URL}/api/auth/otp/request`, {
65
- method: "POST",
66
- headers: {
67
- "Content-Type": "application/json",
68
- },
69
- body: JSON.stringify({ email }),
70
- });
71
-
72
- if (!response.ok) {
73
- const errorData = await response.json().catch(() => ({}));
74
- throw new Error(errorData.detail || "Failed to send OTP");
75
- }
76
-
77
- return await response.json();
78
- }
79
-
80
- /**
81
- * Verify OTP and login
82
- * @param {string} email - Email address
83
- * @param {string} otp - OTP code
84
- * @returns {Promise<Object>} Response with token and user
85
- */
86
- export async function verifyOTP(email, otp) {
87
- const response = await fetch(`${API_BASE_URL}/api/auth/otp/verify`, {
88
- method: "POST",
89
- headers: {
90
- "Content-Type": "application/json",
91
- },
92
- body: JSON.stringify({ email, otp }),
93
- });
94
-
95
- if (!response.ok) {
96
- const errorData = await response.json().catch(() => ({}));
97
- throw new Error(errorData.detail || "OTP verification failed");
98
- }
99
-
100
- return await response.json();
101
- }
102
-
103
- /**
104
- * Logout the current user
105
- * @returns {Promise<void>}
106
- */
107
- export async function logout() {
108
- // For JWT tokens, logout is handled client-side by removing the token
109
- // No server-side logout needed
110
- return Promise.resolve();
111
- }
112
-
113
- =======
114
  /**
115
  * Authentication service for Firebase and OTP authentication
116
  */
@@ -221,5 +108,3 @@ export async function logout() {
221
  // No server-side logout needed
222
  return Promise.resolve();
223
  }
224
-
225
- >>>>>>> daae7a900bd14d0802e4f04b99edb85493053f1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * Authentication service for Firebase and OTP authentication
3
  */
 
108
  // No server-side logout needed
109
  return Promise.resolve();
110
  }