Update frontend/src/components/ocr/UploadZone.jsx
Browse files
frontend/src/components/ocr/UploadZone.jsx
CHANGED
|
@@ -1,11 +1,58 @@
|
|
| 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();
|
|
@@ -20,7 +67,9 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
|
|
| 20 |
e.preventDefault();
|
| 21 |
setIsDragging(false);
|
| 22 |
const file = e.dataTransfer.files[0];
|
| 23 |
-
if (file)
|
|
|
|
|
|
|
| 24 |
};
|
| 25 |
|
| 26 |
const getFileIcon = (type) => {
|
|
@@ -31,6 +80,13 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
|
|
| 31 |
|
| 32 |
const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText;
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return (
|
| 35 |
<div className="w-full">
|
| 36 |
<AnimatePresence mode="wait">
|
|
@@ -76,7 +132,7 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
|
|
| 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
|
| 80 |
</p>
|
| 81 |
</div>
|
| 82 |
|
|
@@ -102,8 +158,15 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
|
|
| 102 |
<input
|
| 103 |
type="file"
|
| 104 |
className="hidden"
|
| 105 |
-
accept=".pdf,.png,.jpg,.jpeg,.tiff,.
|
| 106 |
-
onChange={(e) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
/>
|
| 108 |
</label>
|
| 109 |
|
|
@@ -164,6 +227,25 @@ export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFie
|
|
| 164 |
</motion.div>
|
| 165 |
)}
|
| 166 |
</AnimatePresence>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
</div>
|
| 168 |
);
|
| 169 |
}
|
|
|
|
| 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";
|
| 4 |
import { cn } from "@/lib/utils";
|
| 5 |
import { Input } from "@/components/ui/input";
|
| 6 |
|
| 7 |
+
// Allowed file types
|
| 8 |
+
const ALLOWED_TYPES = [
|
| 9 |
+
"application/pdf",
|
| 10 |
+
"image/png",
|
| 11 |
+
"image/jpeg",
|
| 12 |
+
"image/jpg",
|
| 13 |
+
"image/tiff",
|
| 14 |
+
"image/tif"
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
// Allowed file extensions (for fallback validation)
|
| 18 |
+
const ALLOWED_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"];
|
| 19 |
+
|
| 20 |
+
// Maximum file size: 4 MB
|
| 21 |
+
const MAX_FILE_SIZE = 4 * 1024 * 1024; // 4 MB in bytes
|
| 22 |
+
|
| 23 |
export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFields = "", onKeyFieldsChange = () => {} }) {
|
| 24 |
const [isDragging, setIsDragging] = useState(false);
|
| 25 |
+
const [error, setError] = useState(null);
|
| 26 |
+
|
| 27 |
+
const validateFile = (file) => {
|
| 28 |
+
// Reset error
|
| 29 |
+
setError(null);
|
| 30 |
+
|
| 31 |
+
// Check file type
|
| 32 |
+
const fileExtension = "." + file.name.split(".").pop().toLowerCase();
|
| 33 |
+
const isValidType = ALLOWED_TYPES.includes(file.type) || ALLOWED_EXTENSIONS.includes(fileExtension);
|
| 34 |
+
|
| 35 |
+
if (!isValidType) {
|
| 36 |
+
setError("Only PDF, PNG, JPG, and TIFF files are allowed.");
|
| 37 |
+
return false;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Check file size
|
| 41 |
+
if (file.size > MAX_FILE_SIZE) {
|
| 42 |
+
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
|
| 43 |
+
setError(`File size exceeds 4 MB limit. Your file is ${fileSizeMB} MB.`);
|
| 44 |
+
return false;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return true;
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const handleFileSelect = (file) => {
|
| 51 |
+
if (validateFile(file)) {
|
| 52 |
+
setError(null);
|
| 53 |
+
onFileSelect(file);
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
|
| 57 |
const handleDragOver = (e) => {
|
| 58 |
e.preventDefault();
|
|
|
|
| 67 |
e.preventDefault();
|
| 68 |
setIsDragging(false);
|
| 69 |
const file = e.dataTransfer.files[0];
|
| 70 |
+
if (file) {
|
| 71 |
+
handleFileSelect(file);
|
| 72 |
+
}
|
| 73 |
};
|
| 74 |
|
| 75 |
const getFileIcon = (type) => {
|
|
|
|
| 80 |
|
| 81 |
const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText;
|
| 82 |
|
| 83 |
+
// Clear error when file is cleared
|
| 84 |
+
useEffect(() => {
|
| 85 |
+
if (!selectedFile) {
|
| 86 |
+
setError(null);
|
| 87 |
+
}
|
| 88 |
+
}, [selectedFile]);
|
| 89 |
+
|
| 90 |
return (
|
| 91 |
<div className="w-full">
|
| 92 |
<AnimatePresence mode="wait">
|
|
|
|
| 132 |
{isDragging ? "Drop your file here" : "Drop your file here, or browse"}
|
| 133 |
</p>
|
| 134 |
<p className="text-sm text-slate-400">
|
| 135 |
+
Supports PDF, PNG, JPG, TIFF up to 4MB
|
| 136 |
</p>
|
| 137 |
</div>
|
| 138 |
|
|
|
|
| 158 |
<input
|
| 159 |
type="file"
|
| 160 |
className="hidden"
|
| 161 |
+
accept=".pdf,.png,.jpg,.jpeg,.tiff,.tif"
|
| 162 |
+
onChange={(e) => {
|
| 163 |
+
const file = e.target.files[0];
|
| 164 |
+
if (file) {
|
| 165 |
+
handleFileSelect(file);
|
| 166 |
+
}
|
| 167 |
+
// Reset input so same file can be selected again after error
|
| 168 |
+
e.target.value = "";
|
| 169 |
+
}}
|
| 170 |
/>
|
| 171 |
</label>
|
| 172 |
|
|
|
|
| 227 |
</motion.div>
|
| 228 |
)}
|
| 229 |
</AnimatePresence>
|
| 230 |
+
|
| 231 |
+
{/* Error Message */}
|
| 232 |
+
{error && (
|
| 233 |
+
<motion.div
|
| 234 |
+
initial={{ opacity: 0, y: -10 }}
|
| 235 |
+
animate={{ opacity: 1, y: 0 }}
|
| 236 |
+
exit={{ opacity: 0, y: -10 }}
|
| 237 |
+
className="mt-3 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2"
|
| 238 |
+
>
|
| 239 |
+
<AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" />
|
| 240 |
+
<p className="text-sm text-red-700 flex-1">{error}</p>
|
| 241 |
+
<button
|
| 242 |
+
onClick={() => setError(null)}
|
| 243 |
+
className="text-red-600 hover:text-red-800 transition-colors"
|
| 244 |
+
>
|
| 245 |
+
<X className="h-4 w-4" />
|
| 246 |
+
</button>
|
| 247 |
+
</motion.div>
|
| 248 |
+
)}
|
| 249 |
</div>
|
| 250 |
);
|
| 251 |
}
|