import React, { useState, useCallback, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useDropzone } from 'react-dropzone'; import FlowingMenu from './components/FlowingMenu'; import FileUploader from './components/FileUploader'; import PreviewPanel from './components/PreviewPanel'; import ResultsPanel from './components/ResultsPanel'; import Orb from './components/Orb'; import TrueFocus from './components/TrueFocus'; import LoadingScreen from './components/LoadingScreen'; import ProcessingProgress from './components/ProcessingProgress'; import ApiKeyEncryption from './utils/encryption'; import './index.css'; function App() { const [apiKey, setApiKey] = useState(() => { // Load encrypted API key from localStorage on initialization return ApiKeyEncryption.retrieveApiKey() || ''; }); // Handle API key changes and save encrypted to localStorage const handleApiKeyChange = (newApiKey) => { setApiKey(newApiKey); // Store encrypted API key if (newApiKey.trim()) { ApiKeyEncryption.storeApiKey(newApiKey); console.log('🔐 API key encrypted and stored securely'); } else { ApiKeyEncryption.clearApiKey(); console.log('🗑️ Encrypted API key cleared'); } }; const [uploadedFile, setUploadedFile] = useState(null); const [extractedText, setExtractedText] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0, status: '', fileName: '' }); const [processingMode, setProcessingMode] = useState('standard'); const [activeTab, setActiveTab] = useState('upload'); const [previewMode, setPreviewMode] = useState('text'); const [showLoading, setShowLoading] = useState(true); const fileInputRef = useRef(null); // Check if server has API key configured const [serverHasApiKey, setServerHasApiKey] = useState(false); // Migrate old unencrypted API key to encrypted storage useEffect(() => { const migrateOldApiKey = () => { const oldApiKey = localStorage.getItem('gemini-api-key'); if (oldApiKey && !ApiKeyEncryption.retrieveApiKey()) { console.log('🔄 Migrating old API key to encrypted storage...'); ApiKeyEncryption.storeApiKey(oldApiKey); localStorage.removeItem('gemini-api-key'); // Remove old unencrypted key setApiKey(oldApiKey); console.log('✅ API key migration completed'); } }; migrateOldApiKey(); }, []); // Cleanup temp files on app load/reload and check server API key useEffect(() => { const initializeApp = async () => { try { // Check if server has API key configured const healthResponse = await fetch('http://localhost:3002/api/health'); if (healthResponse.ok) { const healthData = await healthResponse.json(); setServerHasApiKey(healthData.hasApiKey || false); } // Cleanup temp files await fetch('http://localhost:3002/api/cleanup', { method: 'POST' }); console.log('✅ Cleanup completed on app load'); } catch (error) { console.log('⚠️ App initialization failed (server might be starting):', error.message); } }; // Run initialization after a short delay to ensure server is ready setTimeout(initializeApp, 2000); }, []); const menuItems = [ { id: 'upload', label: 'Upload', icon: '↑' }, { id: 'preview', label: 'Preview', icon: '○' }, { id: 'results', label: 'RESULTS', icon: '↓' } ]; const onDrop = useCallback(async (acceptedFiles) => { const file = acceptedFiles[0]; if (file) { // Trigger cleanup before processing new file try { await fetch('http://localhost:3002/api/cleanup', { method: 'POST' }); console.log('✅ Pre-upload cleanup completed'); } catch (error) { console.log('⚠️ Pre-upload cleanup failed:', error.message); } setUploadedFile(file); setActiveTab('preview'); } }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { 'image/*': ['.png', '.jpg', '.jpeg'], 'application/pdf': ['.pdf'], 'text/html': ['.html', '.htm'] }, multiple: false }); const handlePaste = useCallback((e) => { const items = e.clipboardData?.items; if (items) { for (let item of items) { if (item.type.indexOf('image') !== -1) { const file = item.getAsFile(); if (file) { setUploadedFile(file); setActiveTab('preview'); } } } } }, []); const handleFileSelect = () => { fileInputRef.current?.click(); }; const handleFileChange = (e) => { const file = e.target.files?.[0]; if (file) { setUploadedFile(file); setActiveTab('preview'); } }; const processFile = async () => { if (!uploadedFile || !apiKey) return; setIsProcessing(true); // Initialize progress setProcessingProgress({ current: 0, total: 1, status: '🔄 Initializing...', fileName: uploadedFile.name }); let progressInterval = null; try { // Create FormData for file upload const formData = new FormData(); formData.append('file', uploadedFile); formData.append('apiKey', apiKey); formData.append('mode', processingMode); console.log('Processing file:', uploadedFile.name, 'Mode:', processingMode); // Start the OCR request const ocrPromise = fetch('http://localhost:3002/api/ocr', { method: 'POST', body: formData, }); // Start progress polling immediately progressInterval = setInterval(async () => { try { // We'll get the sessionId from the response, but for now poll a generic endpoint // In a real implementation, you'd start polling after getting the sessionId const progressResponse = await fetch(`http://localhost:3002/api/progress-latest`); if (progressResponse.ok) { const progressData = await progressResponse.json(); // Progress data received and processed if (progressData.current > 0) { setProcessingProgress({ current: progressData.current, total: progressData.total, status: progressData.status, fileName: progressData.fileName || uploadedFile.name, totalPages: progressData.totalPages || 1, currentPage: progressData.currentPage || 1, totalCharacters: progressData.totalCharacters || 0, pageCharacters: progressData.pageCharacters || 0, phase: progressData.phase || 'processing', consoleLogs: progressData.consoleLogs || [] }); } } } catch (error) { // Ignore progress polling errors console.log('Progress polling error (ignored):', error.message); } }, 1000); // Poll every 1 second to reduce spam // Wait for OCR to complete const response = await ocrPromise; const result = await response.json(); // Clear progress polling if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } if (result.success) { // Final progress update setProcessingProgress(prev => ({ ...prev, current: prev.total, status: '✅ Processing complete!' })); await new Promise(resolve => setTimeout(resolve, 500)); // Use the extracted text from Gemini setExtractedText(result.data.extractedText); setActiveTab('results'); console.log('✅ OCR Success:', { fileName: result.data.fileName, characters: result.data.metadata.characterCount, words: result.data.metadata.wordCount, mode: result.data.processingMode }); } else { console.error('❌ OCR Error:', result.error); alert(`OCR Error: ${result.error}`); } } catch (error) { console.error('❌ Network Error:', error); // Clear progress polling on error if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } // Check if backend is running if (error.message.includes('fetch')) { alert(`Network Error: Cannot connect to OCR backend. Please make sure: 1. Backend server is running on port 3002 2. Run: cd server && npm install && npm start 3. Check console for any backend errors 4. Visit http://localhost:3002 to verify backend is running Error: ${error.message}`); } else { alert(`Processing Error: ${error.message}`); } } finally { // Clean up if (progressInterval) { clearInterval(progressInterval); } setIsProcessing(false); setProcessingProgress({ current: 0, total: 0, status: '', fileName: '' }); } }; return (
🔒 API key configured on server - no user key required