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