Seth0330 commited on
Commit
935fdc2
·
verified ·
1 Parent(s): 8f8ce18

Update frontend/src/components/ocr/UploadZone.jsx

Browse files
frontend/src/components/ocr/UploadZone.jsx CHANGED
@@ -1,334 +1,169 @@
1
- // frontend/src/pages/Dashboard.jsx
2
-
3
- import React, { useState, useEffect } from "react";
4
- import { motion } from "framer-motion";
5
- import { Sparkles, Zap, FileText, TrendingUp, Clock, AlertCircle } from "lucide-react";
6
- import { Button } from "@/components/ui/button";
7
- import UploadZone from "@/components/ocr/UploadZone";
8
- import DocumentPreview from "@/components/ocr/DocumentPreview";
9
- import ExtractionOutput from "@/components/ocr/ExtractionOutput";
10
- import ExportButtons from "@/components/ExportButtons";
11
- import ProcessingStatus from "@/components/ocr/ProcessingStatus";
12
- import { extractDocument, getHistory } from "@/services/api";
13
-
14
- export default function Dashboard() {
15
- const [selectedFile, setSelectedFile] = useState(null);
16
- const [keyFields, setKeyFields] = useState("");
17
- const [isProcessing, setIsProcessing] = useState(false);
18
- const [isComplete, setIsComplete] = useState(false);
19
- const [extractionResult, setExtractionResult] = useState(null);
20
- const [error, setError] = useState(null);
21
- const [processingStage, setProcessingStage] = useState("received"); // received, analysis, extraction, done
22
- const [stats, setStats] = useState({ totalExtracted: 0, averageAccuracy: 0 });
23
-
24
- const handleFileSelect = (file) => {
25
- setSelectedFile(file);
26
- setIsComplete(false);
27
- setExtractionResult(null);
28
- setError(null);
29
  };
30
 
31
- const handleClear = () => {
32
- setSelectedFile(null);
33
- setKeyFields("");
34
- setIsProcessing(false);
35
- setIsComplete(false);
36
- setExtractionResult(null);
37
- setError(null);
38
- setProcessingStage("received");
39
  };
40
 
41
- // Fetch and calculate stats from history
42
- useEffect(() => {
43
- const fetchStats = async () => {
44
- try {
45
- const history = await getHistory();
46
-
47
- // Calculate total extracted (only completed extractions)
48
- const completedExtractions = history.filter(item => item.status === "completed");
49
- const totalExtracted = completedExtractions.length;
50
-
51
- // Calculate average accuracy from completed extractions
52
- const accuracies = completedExtractions
53
- .map(item => item.confidence || 0)
54
- .filter(acc => acc > 0);
55
-
56
- const averageAccuracy = accuracies.length > 0
57
- ? accuracies.reduce((sum, acc) => sum + acc, 0) / accuracies.length
58
- : 0;
59
-
60
- setStats({
61
- totalExtracted,
62
- averageAccuracy: Math.round(averageAccuracy * 10) / 10 // Round to 1 decimal place
63
- });
64
- } catch (err) {
65
- console.error("Failed to fetch stats:", err);
66
- // Keep default values on error
67
- }
68
- };
69
-
70
- fetchStats();
71
-
72
- // Refresh stats after extraction completes
73
- if (isComplete) {
74
- fetchStats();
75
- }
76
- }, [isComplete]);
77
-
78
- const handleExtract = async () => {
79
- if (!selectedFile) return;
80
-
81
- setIsProcessing(true);
82
- setIsComplete(false);
83
- setError(null);
84
- setExtractionResult(null);
85
- setProcessingStage("received");
86
-
87
- // Move to Analysis stage immediately after starting
88
- setTimeout(() => {
89
- setProcessingStage("analysis");
90
- }, 100);
91
-
92
- // Move to Extraction stage after analysis phase (2.5 seconds)
93
- let extractionTimer = setTimeout(() => {
94
- setProcessingStage("extraction");
95
- }, 2500);
96
-
97
- try {
98
- const result = await extractDocument(selectedFile, keyFields);
99
-
100
- // Clear the extraction timer
101
- clearTimeout(extractionTimer);
102
-
103
- // Move to extraction stage if not already there, then to done
104
- setProcessingStage("extraction");
105
-
106
- // Small delay to show extraction stage, then move to done when results are rendered
107
- setTimeout(() => {
108
- setProcessingStage("done");
109
- setExtractionResult(result);
110
- setIsComplete(true);
111
- setIsProcessing(false);
112
- }, 500); // Give time to see extraction stage
113
- } catch (err) {
114
- clearTimeout(extractionTimer);
115
- console.error("Extraction error:", err);
116
- setError(err.message || "Failed to extract document. Please try again.");
117
- setIsComplete(false);
118
- setProcessingStage("received");
119
- setIsProcessing(false);
120
- }
121
  };
122
 
123
- return (
124
- <div className="min-h-screen bg-[#FAFAFA]">
125
- {/* Header */}
126
- <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40">
127
- <div className="px-8 py-4 flex items-center justify-between">
128
- <div>
129
- <h1 className="text-xl font-bold text-slate-900 tracking-tight">
130
- Document Extraction
131
- </h1>
132
- <p className="text-sm text-slate-500 mt-0.5">
133
- Upload any document and extract structured data with AI
134
- </p>
135
- </div>
136
- <div className="flex items-center gap-3">
137
- {/* Stats Pills */}
138
- <div className="hidden lg:flex items-center gap-2">
139
- <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-lg">
140
- <FileText className="h-4 w-4 text-slate-500" />
141
- <span className="text-sm font-medium text-slate-700">
142
- {stats.totalExtracted} Extracted
143
- </span>
144
- </div>
145
- <div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 rounded-lg">
146
- <TrendingUp className="h-4 w-4 text-emerald-600" />
147
- <span className="text-sm font-medium text-emerald-700">
148
- {stats.averageAccuracy > 0 ? `${stats.averageAccuracy}%` : "0%"} Accuracy
149
- </span>
150
- </div>
151
- </div>
152
-
153
- <ExportButtons isComplete={isComplete} extractionResult={extractionResult} />
154
- </div>
155
- </div>
156
- </header>
157
-
158
- {/* Main Content */}
159
- <div className="p-8">
160
- {/* Upload Section */}
161
- <motion.div
162
- initial={{ opacity: 0, y: 20 }}
163
- animate={{ opacity: 1, y: 0 }}
164
- className="max-w-3xl mx-auto mb-4"
165
- >
166
- <UploadZone
167
- onFileSelect={handleFileSelect}
168
- selectedFile={selectedFile}
169
- onClear={handleClear}
170
- keyFields={keyFields}
171
- onKeyFieldsChange={setKeyFields}
172
- />
173
 
174
- {/* Extract Button */}
175
- {selectedFile && !isProcessing && !isComplete && (
176
- <motion.div
177
- initial={{ opacity: 0, y: 10 }}
178
- animate={{ opacity: 1, y: 0 }}
179
- className="mt-4 flex justify-center"
180
- >
181
- <Button
182
- onClick={handleExtract}
183
- size="lg"
184
- 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"
185
- >
186
- <Sparkles className="h-5 w-5 mr-2" />
187
- Start Extraction
188
- <Zap className="h-4 w-4 ml-2 opacity-70" />
189
- </Button>
190
- </motion.div>
191
- )}
192
- </motion.div>
193
 
194
- {/* Error Message */}
195
- {error && (
 
 
196
  <motion.div
197
- initial={{ opacity: 0, y: -10 }}
 
198
  animate={{ opacity: 1, y: 0 }}
199
- className="max-w-3xl mx-auto mb-6"
 
 
 
 
 
 
 
 
 
 
 
 
200
  >
201
- <div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex items-start gap-3">
202
- <AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
203
- <div className="flex-1">
204
- <h3 className="font-semibold text-red-900 mb-1">Extraction Failed</h3>
205
- <p className="text-sm text-red-700">{error}</p>
206
- </div>
207
- <button
208
- onClick={() => setError(null)}
209
- className="text-red-400 hover:text-red-600 transition-colors"
210
  >
211
- ×
212
- </button>
213
- </div>
214
- </motion.div>
215
- )}
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- {/* Processing Status */}
218
- {(isProcessing || isComplete) && (
219
- <div className="max-w-3xl mx-auto mb-4">
220
- <ProcessingStatus
221
- isProcessing={isProcessing}
222
- isComplete={isComplete}
223
- currentStage={processingStage}
224
- />
225
- </div>
226
- )}
 
 
 
 
 
 
 
 
227
 
228
- {/* Split View */}
229
- {selectedFile && (
230
- <motion.div
231
- initial={{ opacity: 0, y: 20 }}
232
- animate={{ opacity: 1, y: 0 }}
233
- transition={{ delay: 0.2 }}
234
- className="grid grid-cols-1 lg:grid-cols-2 gap-4"
235
- style={{ height: "calc(100vh - 320px)", minHeight: "450px" }}
236
- >
237
- <DocumentPreview file={selectedFile} isProcessing={isProcessing} />
238
- <ExtractionOutput
239
- hasFile={!!selectedFile}
240
- isProcessing={isProcessing}
241
- isComplete={isComplete}
242
- extractionResult={extractionResult}
243
- />
244
- </motion.div>
245
- )}
246
 
247
- {/* Empty State Features */}
248
- {!selectedFile && (
 
 
249
  <motion.div
250
- initial={{ opacity: 0 }}
251
- animate={{ opacity: 1 }}
252
- transition={{ delay: 0.3 }}
253
- className="max-w-5xl mx-auto mt-12"
 
254
  >
255
- <div className="text-center mb-10">
256
- <h2 className="text-2xl font-bold text-slate-900 mb-2">
257
- Powered by Advanced AI
258
- </h2>
259
- <p className="text-slate-500">
260
- Extract structured data from any document
261
- </p>
262
- </div>
263
-
264
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
265
- {[
266
- {
267
- icon: Zap,
268
- title: "Lightning Fast",
269
- description:
270
- "Process documents faster with our optimized AI pipeline",
271
- color: "amber",
272
- },
273
- {
274
- icon: Sparkles,
275
- title: `${stats.averageAccuracy > 0 ? stats.averageAccuracy : "98.5"}% Accuracy`,
276
- description:
277
- "Industry-leading extraction accuracy",
278
- color: "indigo",
279
- },
280
- {
281
- icon: Clock,
282
- title: "Any Format",
283
- description:
284
- "Support for PDF, images, spreadsheets, and scanned documents",
285
- color: "emerald",
286
- },
287
- ].map((feature, index) => (
288
- <motion.div
289
- key={feature.title}
290
- initial={{ opacity: 0, y: 20 }}
291
- animate={{ opacity: 1, y: 0 }}
292
- transition={{ delay: 0.4 + index * 0.1 }}
293
- 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"
294
- >
295
- <div
296
- 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`}
297
- >
298
- <feature.icon
299
- className={`h-6 w-6 text-${feature.color}-600`}
300
- />
301
  </div>
302
- <h3 className="font-semibold text-slate-900 mb-2">
303
- {feature.title}
304
- </h3>
305
- <p className="text-sm text-slate-500 leading-relaxed">
306
- {feature.description}
307
- </p>
308
- </motion.div>
309
- ))}
310
  </div>
311
 
312
- {/* Supported Formats */}
313
- <div className="mt-12 text-center">
314
- <p className="text-xs text-slate-400 uppercase tracking-wider mb-4 font-medium">
315
- Supported Formats
316
- </p>
317
- <div className="flex items-center justify-center gap-6 flex-wrap">
318
- {["PDF", "PNG", "JPG", "TIFF", "DOCX", "XLSX"].map((format) => (
319
- <div
320
- key={format}
321
- className="flex items-center gap-2 text-slate-400"
322
- >
323
- <FileText className="h-4 w-4" />
324
- <span className="text-sm font-medium">{format}</span>
325
- </div>
326
- ))}
327
- </div>
328
  </div>
329
  </motion.div>
330
  )}
331
- </div>
332
  </div>
333
  );
334
  }
 
1
+ import React, { useState } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+ import { Input } from "@/components/ui/input";
6
+
7
+ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFields = "", onKeyFieldsChange = () => {} }) {
8
+ const [isDragging, setIsDragging] = useState(false);
9
+
10
+ const handleDragOver = (e) => {
11
+ e.preventDefault();
12
+ setIsDragging(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  };
14
 
15
+ const handleDragLeave = () => {
16
+ setIsDragging(false);
 
 
 
 
 
 
17
  };
18
 
19
+ const handleDrop = (e) => {
20
+ e.preventDefault();
21
+ setIsDragging(false);
22
+ const file = e.dataTransfer.files[0];
23
+ if (file) onFileSelect(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  };
25
 
26
+ const getFileIcon = (type) => {
27
+ if (type?.includes("image")) return Image;
28
+ if (type?.includes("spreadsheet") || type?.includes("excel")) return FileSpreadsheet;
29
+ return FileText;
30
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ return (
35
+ <div className="w-full">
36
+ <AnimatePresence mode="wait">
37
+ {!selectedFile ? (
38
  <motion.div
39
+ key="upload"
40
+ initial={{ opacity: 0, y: 10 }}
41
  animate={{ opacity: 1, y: 0 }}
42
+ exit={{ opacity: 0, y: -10 }}
43
+ transition={{ duration: 0.2 }}
44
+ onDragOver={handleDragOver}
45
+ onDragLeave={handleDragLeave}
46
+ onDrop={handleDrop}
47
+ className={cn(
48
+ "relative group cursor-pointer",
49
+ "border-2 border-dashed rounded-2xl",
50
+ "transition-all duration-300 ease-out",
51
+ isDragging
52
+ ? "border-indigo-400 bg-indigo-50/50"
53
+ : "border-slate-200 hover:border-indigo-300 hover:bg-slate-50/50"
54
+ )}
55
  >
56
+ <label className="flex flex-col items-center justify-center py-16 px-8 cursor-pointer">
57
+ <motion.div
58
+ animate={isDragging ? { scale: 1.1, y: -5 } : { scale: 1, y: 0 }}
59
+ className={cn(
60
+ "h-16 w-16 rounded-2xl flex items-center justify-center mb-6 transition-colors duration-300",
61
+ isDragging
62
+ ? "bg-indigo-100"
63
+ : "bg-gradient-to-br from-slate-100 to-slate-50 group-hover:from-indigo-100 group-hover:to-violet-50"
64
+ )}
65
  >
66
+ <Upload
67
+ className={cn(
68
+ "h-7 w-7 transition-colors duration-300",
69
+ isDragging ? "text-indigo-600" : "text-slate-400 group-hover:text-indigo-500"
70
+ )}
71
+ />
72
+ </motion.div>
73
+
74
+ <div className="text-center">
75
+ <p className="text-lg font-semibold text-slate-700 mb-1">
76
+ {isDragging ? "Drop your file here" : "Drop your file here, or browse"}
77
+ </p>
78
+ <p className="text-sm text-slate-400">
79
+ Supports PDF, PNG, JPG, TIFF, DOCX up to 50MB
80
+ </p>
81
+ </div>
82
 
83
+ <div className="flex items-center gap-2 mt-6">
84
+ <div className="flex -space-x-1">
85
+ {[
86
+ "bg-red-100 text-red-600",
87
+ "bg-blue-100 text-blue-600",
88
+ "bg-green-100 text-green-600",
89
+ "bg-amber-100 text-amber-600",
90
+ ].map((color, i) => (
91
+ <div
92
+ key={i}
93
+ className={`h-8 w-8 rounded-lg ${color.split(" ")[0]} flex items-center justify-center border-2 border-white`}
94
+ >
95
+ <FileText className={`h-4 w-4 ${color.split(" ")[1]}`} />
96
+ </div>
97
+ ))}
98
+ </div>
99
+ <span className="text-xs text-slate-400 ml-2">Multiple formats supported</span>
100
+ </div>
101
 
102
+ <input
103
+ type="file"
104
+ className="hidden"
105
+ accept=".pdf,.png,.jpg,.jpeg,.tiff,.docx,.xlsx"
106
+ onChange={(e) => e.target.files[0] && onFileSelect(e.target.files[0])}
107
+ />
108
+ </label>
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ {/* Decorative gradient border on hover */}
111
+ <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" />
112
+ </motion.div>
113
+ ) : (
114
  <motion.div
115
+ key="selected"
116
+ initial={{ opacity: 0, scale: 0.95 }}
117
+ animate={{ opacity: 1, scale: 1 }}
118
+ exit={{ opacity: 0, scale: 0.95 }}
119
+ className="grid grid-cols-1 lg:grid-cols-2 gap-3"
120
  >
121
+ {/* File Info Box */}
122
+ <div className="relative bg-gradient-to-br from-indigo-50 to-violet-50 rounded-xl p-3 border border-indigo-100">
123
+ <div className="flex items-center gap-3">
124
+ <div className="h-10 w-10 rounded-lg bg-white shadow-sm flex items-center justify-center flex-shrink-0">
125
+ <FileIcon className="h-5 w-5 text-indigo-600" />
126
+ </div>
127
+ <div className="flex-1 min-w-0">
128
+ <p className="font-medium text-slate-800 truncate text-sm">{selectedFile.name}</p>
129
+ <div className="flex items-center gap-2 text-xs text-slate-500">
130
+ <span>{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</span>
131
+ <span className="text-indigo-500">•</span>
132
+ <span className="text-indigo-600 flex items-center gap-1">
133
+ <Sparkles className="h-3 w-3" />
134
+ Ready for extraction
135
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
+ </div>
138
+ <button
139
+ onClick={onClear}
140
+ 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"
141
+ >
142
+ <X className="h-4 w-4" />
143
+ </button>
144
+ </div>
145
  </div>
146
 
147
+ {/* Key Fields Box */}
148
+ <div className="relative bg-white rounded-xl p-3 border border-slate-200">
149
+ <label className="block text-xs font-medium text-slate-600 mb-1.5">
150
+ Key fields if required
151
+ </label>
152
+ <Input
153
+ type="text"
154
+ value={keyFields || ""}
155
+ onChange={(e) => {
156
+ if (onKeyFieldsChange) {
157
+ onKeyFieldsChange(e.target.value);
158
+ }
159
+ }}
160
+ placeholder="Invoice Number, Invoice Date, PO Number, Supplier Name, Total Amount, Payment terms, Additional Notes"
161
+ className="h-8 text-xs border-slate-200 focus:border-indigo-300 focus:ring-indigo-200"
162
+ />
163
  </div>
164
  </motion.div>
165
  )}
166
+ </AnimatePresence>
167
  </div>
168
  );
169
  }