| | import React, { useState, useEffect, useRef } from "react"; |
| | import { motion } from "framer-motion"; |
| | import { FileText, ZoomIn, ZoomOut, RotateCw } from "lucide-react"; |
| | import { Button } from "@/components/ui/button"; |
| |
|
| | export default function DocumentPreview({ file, isProcessing, isFromHistory = false }) { |
| | const [previewUrls, setPreviewUrls] = useState([]); |
| | const [zoom, setZoom] = useState(100); |
| | const [rotation, setRotation] = useState(0); |
| | const objectUrlsRef = useRef([]); |
| |
|
| | useEffect(() => { |
| | if (!file) { |
| | |
| | objectUrlsRef.current.forEach((url) => { |
| | if (url && url.startsWith("blob:")) { |
| | URL.revokeObjectURL(url); |
| | } |
| | }); |
| | objectUrlsRef.current = []; |
| | setPreviewUrls([]); |
| | return; |
| | } |
| |
|
| | const loadPreview = async () => { |
| | const urls = []; |
| | const newObjectUrls = []; |
| |
|
| | |
| | if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) { |
| | try { |
| | |
| | const pdfjsLib = await import("pdfjs-dist"); |
| | |
| | |
| | |
| | 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); |
| | |
| | urls.push(null); |
| | } |
| | } else { |
| | |
| | const url = URL.createObjectURL(file); |
| | urls.push(url); |
| | newObjectUrls.push(url); |
| | } |
| |
|
| | |
| | objectUrlsRef.current.forEach((url) => { |
| | if (url && url.startsWith("blob:")) { |
| | URL.revokeObjectURL(url); |
| | } |
| | }); |
| | objectUrlsRef.current = newObjectUrls; |
| | setPreviewUrls(urls); |
| | }; |
| |
|
| | loadPreview(); |
| |
|
| | |
| | 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> |
| | </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"> |
| | {isFromHistory |
| | ? "Original document not available for historical extractions" |
| | : "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> |
| | ); |
| | } |
| |
|