luna_ocr / src /App.js
veela4's picture
Add files using upload-large-folder tool
373c769 verified
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 (
<div className="app" onPaste={handlePaste}>
{/* Loading Screen */}
{showLoading && (
<LoadingScreen onComplete={() => setShowLoading(false)} />
)}
{/* Giant Central Orb - Fades in after loading */}
<div className={`giant-orb-background ${!showLoading ? 'loaded' : ''}`}>
<div className="giant-orb-container">
<Orb hue={0} hoverIntensity={0.5} rotateOnHover={true} forceHoverState={false} />
</div>
</div>
<div className="glass-container">
<header className="app-header">
<motion.div
className="logo"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<TrueFocus
sentence="Luna OCR"
manualMode={true}
blurAmount={8.5}
borderColor="#ffffff"
glowColor="rgba(255, 255, 255, 0.8)"
animationDuration={0.5}
pauseBetweenAnimations={2}
/>
</motion.div>
<motion.div
className="api-key-input"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
{!serverHasApiKey && (
<div className="api-key-container">
<input
type="password"
placeholder={apiKey ? "API Key loaded from storage" : "Enter Google API Key..."}
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
className="glass-input"
/>
{apiKey && (
<button
type="button"
onClick={() => handleApiKeyChange('')}
className="clear-api-key-btn"
title="Clear saved API key"
>
×
</button>
)}
</div>
)}
{serverHasApiKey && (
<div className="server-api-notice">
<p>🔒 API key configured on server - no user key required</p>
</div>
)}
</motion.div>
</header>
<FlowingMenu
items={menuItems}
activeItem={activeTab}
onItemClick={setActiveTab}
/>
<main className="app-main">
<AnimatePresence mode="wait">
{activeTab === 'upload' && (
<motion.div
key="upload"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="tab-content"
>
<FileUploader
getRootProps={getRootProps}
getInputProps={getInputProps}
isDragActive={isDragActive}
onFileSelect={handleFileSelect}
fileInputRef={fileInputRef}
onFileChange={handleFileChange}
/>
</motion.div>
)}
{activeTab === 'preview' && uploadedFile && (
<motion.div
key="preview"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="tab-content"
>
<PreviewPanel
file={uploadedFile}
processingMode={processingMode}
onModeChange={setProcessingMode}
onProcess={processFile}
isProcessing={isProcessing}
apiKey={apiKey}
/>
</motion.div>
)}
{activeTab === 'results' && extractedText && (
<motion.div
key="results"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="tab-content"
>
<ResultsPanel
text={extractedText}
previewMode={previewMode}
onPreviewModeChange={setPreviewMode}
fileName={uploadedFile?.name}
/>
</motion.div>
)}
</AnimatePresence>
</main>
</div>
{/* Processing Progress Overlay */}
<ProcessingProgress
progress={processingProgress}
isVisible={isProcessing}
/>
</div>
);
}
export default App;