luna_ocr / src /components /PreviewPanel.js
veela4's picture
Add files using upload-large-folder tool
373c769 verified
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Eye, Zap, Brain, Settings, ExternalLink, FileText } from 'lucide-react';
import DocumentViewer from './DocumentViewer';
const PreviewPanel = ({
file,
processingMode,
onModeChange,
onProcess,
isProcessing,
apiKey
}) => {
const [previewUrl, setPreviewUrl] = useState(null);
const [fileInfo, setFileInfo] = useState(null);
const [showDocumentViewer, setShowDocumentViewer] = useState(false);
// OCR borders for future use
// const [showOcrBorders, setShowOcrBorders] = useState(false);
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
setFileInfo({
name: file.name,
size: (file.size / 1024 / 1024).toFixed(2),
type: file.type,
lastModified: new Date(file.lastModified).toLocaleString()
});
return () => URL.revokeObjectURL(url);
}
}, [file]);
const processingModes = [
{
id: 'standard',
name: 'Standard Mode',
model: 'Gemini 2.5 Flash',
icon: <Zap size={20} />,
description: 'Fast processing',
features: ['Same features as Pro', 'Faster speed', 'Slightly less accuracy'],
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
id: 'structured',
name: 'Structured Mode',
model: 'Gemini 2.5 Pro',
icon: <Brain size={20} />,
description: 'Maximum accuracy',
features: ['Best accuracy', 'Advanced formatting', 'Slower processing'],
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
}
];
return (
<>
{showDocumentViewer && (
<DocumentViewer
file={file}
onBack={() => setShowDocumentViewer(false)}
/>
)}
<div className="preview-panel">
<div className="preview-grid">
{/* File Preview */}
<motion.div
className="preview-section glass-panel"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<div className="section-header">
<Eye size={20} />
<h3>File Preview</h3>
</div>
<div className="preview-container">
{previewUrl && (
<motion.div
className="preview-image-container"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.3 }}
>
{file.type === 'application/pdf' ? (
<div className="pdf-preview glass-preview">
<div className="preview-content">
<div className="document-icon">
<FileText size={48} />
</div>
<p className="preview-title">PDF Document</p>
<span className="pdf-note">Ready for processing</span>
<motion.button
className="preview-button"
onClick={() => setShowDocumentViewer(true)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={16} />
Open Viewer
</motion.button>
</div>
</div>
) : file.type === 'text/html' || file.name.toLowerCase().endsWith('.html') ? (
<div className="html-preview glass-preview">
<div className="preview-content">
<div className="document-icon">
<FileText size={48} />
</div>
<p className="preview-title">HTML Document</p>
<span className="html-note">Ready for processing</span>
<motion.button
className="preview-button"
onClick={() => setShowDocumentViewer(true)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={16} />
Open Viewer
</motion.button>
</div>
</div>
) : (
<img
src={previewUrl}
alt="Preview"
className="preview-image"
/>
)}
</motion.div>
)}
{fileInfo && (
<div className="file-info nested-panel">
<div className="info-grid">
<div className="info-item">
<span className="info-label">Name:</span>
<span className="info-value">{fileInfo.name}</span>
</div>
<div className="info-item">
<span className="info-label">Size:</span>
<span className="info-value">{fileInfo.size} MB</span>
</div>
<div className="info-item">
<span className="info-label">Type:</span>
<span className="info-value">{fileInfo.type}</span>
</div>
<div className="info-item">
<span className="info-label">Modified:</span>
<span className="info-value">{fileInfo.lastModified}</span>
</div>
</div>
</div>
)}
</div>
</motion.div>
{/* Processing Options */}
<motion.div
className="options-section glass-panel"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<div className="section-header">
<Settings size={20} />
<h3>Processing Options</h3>
</div>
<div className="processing-modes">
{processingModes.map((mode, index) => (
<motion.div
key={mode.id}
className={`mode-card nested-panel ${processingMode === mode.id ? 'active' : ''}`}
onClick={() => onModeChange(mode.id)}
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<div className="mode-header">
<div className="mode-icon" style={{ background: mode.gradient }}>
{mode.icon}
</div>
<div className="mode-info">
<h4>{mode.name}</h4>
<span className="mode-model">{mode.model}</span>
</div>
</div>
<p className="mode-description">{mode.description}</p>
<div className="mode-features">
{mode.features.map((feature, idx) => (
<span key={idx} className="feature-tag">
{feature}
</span>
))}
</div>
{processingMode === mode.id && (
<motion.div
className="mode-indicator"
layoutId="modeIndicator"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(99, 102, 241, 0.1)',
borderRadius: '12px',
zIndex: -1,
border: '1px solid rgba(99, 102, 241, 0.3)'
}}
transition={{
type: "spring",
stiffness: 500,
damping: 30
}}
/>
)}
</motion.div>
))}
</div>
<motion.button
className="process-button flowing-button"
onClick={onProcess}
disabled={!apiKey || isProcessing}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{
width: '100%',
marginTop: '24px',
background: isProcessing
? 'linear-gradient(135deg, #6b7280 0%, #4b5563 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}
>
<AnimatePresence mode="wait">
{isProcessing ? (
<motion.div
key="processing"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="button-content"
>
<motion.div
className="processing-spinner"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
</motion.div>
Processing...
</motion.div>
) : (
<motion.div
key="ready"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="button-content"
>
🚀 Extract Text
</motion.div>
)}
</AnimatePresence>
</motion.button>
{!apiKey && (
<motion.p
className="api-warning"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
style={{
color: 'rgba(251, 86, 7, 0.8)',
fontSize: '0.75rem',
textAlign: 'center',
marginTop: '8px'
}}
>
Please enter your Google API Key to continue
</motion.p>
)}
</motion.div>
</div>
<style jsx>{`
.preview-panel {
width: 100%;
min-height: auto;
padding-bottom: 40px;
}
.preview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
height: 100%;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
color: var(--text-primary);
}
.section-header h3 {
font-size: 1.25rem;
font-weight: 600;
}
.preview-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.preview-image-container {
border-radius: 12px;
overflow: hidden;
background: rgba(100, 100, 100, 0.1);
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
}
.preview-image {
width: 100%;
height: 300px;
object-fit: contain;
background: rgba(100, 100, 100, 0.1);
}
.pdf-preview {
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: rgba(100, 100, 100, 0.1);
}
.pdf-icon {
font-size: 4rem;
filter: drop-shadow(0 0 20px rgba(99, 102, 241, 0.3));
}
.pdf-note {
font-size: 0.75rem;
color: var(--text-muted);
}
.file-info {
padding: 16px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
}
.info-value {
font-size: 0.875rem;
color: var(--text-secondary);
word-break: break-all;
}
.processing-modes {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
.mode-card {
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.mode-card.active {
border-color: rgba(99, 102, 241, 0.3);
}
.mode-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.mode-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.mode-info h4 {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.mode-model {
font-size: 0.75rem;
color: var(--text-muted);
}
.mode-description {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 12px;
}
.mode-features {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.feature-tag {
background: rgba(100, 100, 100, 0.1);
color: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
backdrop-filter: blur(10px);
}
.button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.processing-spinner {
font-size: 1.2rem;
}
@media (max-width: 768px) {
.preview-grid {
grid-template-columns: 1fr;
}
.info-grid {
grid-template-columns: 1fr;
}
}
.glass-preview {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
overflow: hidden;
}
.preview-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 32px;
}
.document-icon {
color: rgba(79, 172, 254, 0.8);
filter: drop-shadow(0 0 20px rgba(79, 172, 254, 0.4));
}
.preview-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.pdf-note, .html-note {
font-size: 0.875rem;
color: var(--text-muted);
margin: 0;
}
.preview-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: linear-gradient(135deg, #4facfe, #8b5cf6);
border: none;
border-radius: 8px;
color: white;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.preview-button:hover {
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
transform: translateY(-1px);
}
`}</style>
</div>
</>
);
};
export default PreviewPanel;