AIEXTRACT1 / frontend /src /components /ocr /DocumentPreview.jsx
Seth0330's picture
Update frontend/src/components/ocr/DocumentPreview.jsx
333e020 verified
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { FileText, ZoomIn, ZoomOut, RotateCw, Maximize2 } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function DocumentPreview({ file, isProcessing }) {
const [previewUrls, setPreviewUrls] = useState([]);
const [zoom, setZoom] = useState(100);
const [rotation, setRotation] = useState(0);
const objectUrlsRef = useRef([]);
useEffect(() => {
if (!file) {
// Cleanup previous URLs
objectUrlsRef.current.forEach((url) => {
if (url && url.startsWith("blob:")) {
URL.revokeObjectURL(url);
}
});
objectUrlsRef.current = [];
setPreviewUrls([]);
return;
}
const loadPreview = async () => {
const urls = [];
const newObjectUrls = [];
// Check if it's a PDF
if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) {
try {
// Use pdf.js to render PDF pages
const pdfjsLib = await import("pdfjs-dist");
// Configure worker - use jsdelivr CDN which is more reliable
// This will use the same version as the installed package
const version = pdfjsLib.version || "4.0.379";
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/build/pdf.worker.min.mjs`;
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const numPages = pdf.numPages;
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport,
}).promise;
urls.push(canvas.toDataURL("image/jpeg", 0.95));
}
} catch (error) {
console.error("Error loading PDF:", error);
// Fallback: show error message
urls.push(null);
}
} else {
// For images, create object URL
const url = URL.createObjectURL(file);
urls.push(url);
newObjectUrls.push(url);
}
// Cleanup old object URLs
objectUrlsRef.current.forEach((url) => {
if (url && url.startsWith("blob:")) {
URL.revokeObjectURL(url);
}
});
objectUrlsRef.current = newObjectUrls;
setPreviewUrls(urls);
};
loadPreview();
// Cleanup function - revoke object URLs when component unmounts or file changes
return () => {
objectUrlsRef.current.forEach((url) => {
if (url && url.startsWith("blob:")) {
URL.revokeObjectURL(url);
}
});
objectUrlsRef.current = [];
};
}, [file]);
return (
<div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center">
<FileText className="h-4 w-4 text-indigo-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 text-sm">Document Preview</h3>
<p className="text-xs text-slate-400">{file?.name || "No file selected"}</p>
</div>
</div>
{file && (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-600"
onClick={() => setZoom(Math.max(50, zoom - 25))}
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-xs text-slate-500 w-12 text-center">{zoom}%</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-600"
onClick={() => setZoom(Math.min(200, zoom + 25))}
>
<ZoomIn className="h-4 w-4" />
</Button>
<div className="w-px h-4 bg-slate-200 mx-2" />
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-600"
onClick={() => setRotation((rotation + 90) % 360)}
>
<RotateCw className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-600"
onClick={() => {
setZoom(100);
setRotation(0);
}}
>
<Maximize2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
{/* Preview Area */}
<div className="flex-1 p-6 bg-slate-50/50 overflow-auto">
{!file ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
<FileText className="h-10 w-10 text-slate-300" />
</div>
<p className="text-slate-400 text-sm">Upload a document to preview</p>
</div>
</div>
) : previewUrls.length === 0 ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
<FileText className="h-10 w-10 text-slate-300" />
</div>
<p className="text-slate-400 text-sm">Loading preview...</p>
</div>
</div>
) : (
<div className="space-y-4">
{previewUrls.map((url, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="relative bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex items-center justify-center"
style={{
minHeight: "400px",
}}
>
{url ? (
<img
src={url}
alt={`Page ${index + 1}`}
className="w-full h-auto"
style={{
transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
maxWidth: "100%",
objectFit: "contain",
transition: "transform 0.2s ease",
}}
/>
) : (
<div className="p-8 text-center">
<p className="text-slate-400 text-sm">Unable to load preview</p>
</div>
)}
{/* Processing overlay */}
{isProcessing && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 bg-indigo-600/5 backdrop-blur-[1px] pointer-events-none"
>
<motion.div
initial={{ top: 0 }}
animate={{ top: "100%" }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
}}
className="absolute left-0 right-0 h-1 bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
/>
</motion.div>
)}
{/* Page number */}
{previewUrls.length > 1 && (
<div className="absolute bottom-3 right-3 text-xs text-slate-400 bg-white/90 px-2 py-1 rounded">
Page {index + 1}
</div>
)}
</motion.div>
))}
</div>
)}
</div>
</div>
);
}