|
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);
|
|
|
|
|
|
|
|
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; |