|
|
import React, { useState } from 'react'; |
|
|
import { FileText, AlertCircle } from 'lucide-react'; |
|
|
import FileUpload from './components/FileUpload'; |
|
|
import ProgressIndicator from './components/ProgressIndicator'; |
|
|
import ResultCard from './components/ResultCard'; |
|
|
import ImagePreview from './components/ImagePreview'; |
|
|
import { convertFileToImages, dataUrlToBlob } from './utils/fileConverter'; |
|
|
import { processSingleInvoice } from './utils/api'; |
|
|
|
|
|
function App() { |
|
|
const [processing, setProcessing] = useState(false); |
|
|
const [results, setResults] = useState([]); |
|
|
const [progress, setProgress] = useState({ total: 0, completed: 0, current: '' }); |
|
|
const [error, setError] = useState(null); |
|
|
const [imageDataMap, setImageDataMap] = useState({}); |
|
|
const [previewImages, setPreviewImages] = useState([]); |
|
|
const [processingIndex, setProcessingIndex] = useState(null); |
|
|
const [resolutionMap, setResolutionMap] = useState({}); |
|
|
const [resultResolutionMap, setResultResolutionMap] = useState({}); |
|
|
const [enhancedMap, setEnhancedMap] = useState({}); |
|
|
const [reasoningMap, setReasoningMap] = useState({}); |
|
|
|
|
|
const handleFilesSelected = async (files) => { |
|
|
setProcessing(false); |
|
|
setResults([]); |
|
|
setError(null); |
|
|
setImageDataMap({}); |
|
|
setPreviewImages([]); |
|
|
setResolutionMap({}); |
|
|
setEnhancedMap({}); |
|
|
setReasoningMap({}); |
|
|
|
|
|
try { |
|
|
|
|
|
const allImages = []; |
|
|
const imageData = {}; |
|
|
const previews = []; |
|
|
|
|
|
for (const file of files) { |
|
|
try { |
|
|
const images = await convertFileToImages(file); |
|
|
images.forEach((img, idx) => { |
|
|
const key = `${file.name}_${idx}`; |
|
|
allImages.push({ |
|
|
key: key, |
|
|
dataUrl: img.dataUrl, |
|
|
filename: img.filename, |
|
|
originalFile: img.originalFile, |
|
|
pageNumber: img.pageNumber, |
|
|
}); |
|
|
imageData[key] = img.dataUrl; |
|
|
previews.push({ |
|
|
key: key, |
|
|
dataUrl: img.dataUrl, |
|
|
filename: img.filename, |
|
|
}); |
|
|
}); |
|
|
} catch (err) { |
|
|
console.error(`Error converting ${file.name}:`, err); |
|
|
setError(`Failed to convert ${file.name}: ${err.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
setImageDataMap(imageData); |
|
|
setPreviewImages(previews); |
|
|
|
|
|
|
|
|
const initialResolutions = {}; |
|
|
previews.forEach(p => { |
|
|
initialResolutions[p.key] = { dataUrl: p.dataUrl, resolution: 100 }; |
|
|
}); |
|
|
setResolutionMap(initialResolutions); |
|
|
|
|
|
} catch (err) { |
|
|
console.error('Processing error:', err); |
|
|
setError(`Processing failed: ${err.message}`); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleProcessImages = async () => { |
|
|
setProcessing(true); |
|
|
setResults([]); |
|
|
setError(null); |
|
|
setProgress({ total: previewImages.length, completed: 0, current: '' }); |
|
|
|
|
|
try { |
|
|
for (let i = 0; i < previewImages.length; i++) { |
|
|
const preview = previewImages[i]; |
|
|
setProcessingIndex(i); |
|
|
setProgress(prev => ({ |
|
|
...prev, |
|
|
current: preview.filename |
|
|
})); |
|
|
|
|
|
try { |
|
|
|
|
|
const processData = resolutionMap[preview.key] || { dataUrl: preview.dataUrl, resolution: 100 }; |
|
|
const blob = dataUrlToBlob(processData.dataUrl); |
|
|
const isEnhanced = enhancedMap[preview.key] || false; |
|
|
const reasoningMode = reasoningMap[preview.key] ? "reason" : "simple"; |
|
|
|
|
|
const result = await processSingleInvoice(blob, preview.filename, isEnhanced, reasoningMode); |
|
|
|
|
|
const resultWithMetadata = { |
|
|
...result, |
|
|
filename: preview.filename, |
|
|
originalFile: preview.filename, |
|
|
index: i, |
|
|
success: true, |
|
|
key: preview.key, |
|
|
processedImageData: processData.dataUrl, |
|
|
processedResolution: processData.resolution |
|
|
}; |
|
|
|
|
|
setResults(prev => [...prev, resultWithMetadata]); |
|
|
} catch (error) { |
|
|
const errorResult = { |
|
|
filename: preview.filename, |
|
|
index: i, |
|
|
success: false, |
|
|
error: error.response?.data?.detail || error.message, |
|
|
key: preview.key |
|
|
}; |
|
|
setResults(prev => [...prev, errorResult]); |
|
|
} |
|
|
|
|
|
setProgress(prev => ({ ...prev, completed: i + 1 })); |
|
|
} |
|
|
} catch (err) { |
|
|
console.error('Processing error:', err); |
|
|
setError(`Processing failed: ${err.message}`); |
|
|
} finally { |
|
|
setProcessing(false); |
|
|
setProcessingIndex(null); |
|
|
setProgress(prev => ({ ...prev, current: '' })); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleReprocess = async (result, resolution, adjustedDataUrl) => { |
|
|
const index = results.findIndex(r => r.key === result.key); |
|
|
if (index === -1) return; |
|
|
|
|
|
setProcessingIndex(index); |
|
|
|
|
|
try { |
|
|
|
|
|
const blob = dataUrlToBlob(adjustedDataUrl || imageDataMap[result.key]); |
|
|
const isEnhanced = enhancedMap[result.key] || false; |
|
|
const reasoningMode = reasoningMap[result.key] ? "reason" : "simple"; |
|
|
|
|
|
const newResult = await processSingleInvoice(blob, result.filename, isEnhanced, reasoningMode); |
|
|
|
|
|
const resultWithMetadata = { |
|
|
...newResult, |
|
|
filename: result.filename, |
|
|
originalFile: result.originalFile, |
|
|
index: index, |
|
|
success: true, |
|
|
key: result.key, |
|
|
processedImageData: adjustedDataUrl || imageDataMap[result.key], |
|
|
processedResolution: resolution |
|
|
}; |
|
|
|
|
|
setResults(prev => { |
|
|
const newResults = [...prev]; |
|
|
newResults[index] = resultWithMetadata; |
|
|
return newResults; |
|
|
}); |
|
|
} catch (error) { |
|
|
setError(`Reprocessing failed: ${error.message}`); |
|
|
} finally { |
|
|
setProcessingIndex(null); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleResolutionChange = (key, dataUrl, resolution) => { |
|
|
setResolutionMap(prev => ({ |
|
|
...prev, |
|
|
[key]: { dataUrl, resolution } |
|
|
})); |
|
|
}; |
|
|
|
|
|
const handleEnhanceToggle = (key) => { |
|
|
setEnhancedMap(prev => ({ |
|
|
...prev, |
|
|
[key]: !prev[key] |
|
|
})); |
|
|
}; |
|
|
|
|
|
const handleReasoningModeToggle = (key) => { |
|
|
setReasoningMap(prev => ({ |
|
|
...prev, |
|
|
[key]: !prev[key] |
|
|
})); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen py-8 px-4 sm:px-6 lg:px-8"> |
|
|
<div className="max-w-7xl mx-auto"> |
|
|
{/* Header */} |
|
|
<div className="text-center mb-8"> |
|
|
<div className="flex items-center justify-center mb-4"> |
|
|
<div className="p-3 bg-white rounded-2xl shadow-lg"> |
|
|
<FileText className="w-12 h-12 text-primary-600" /> |
|
|
</div> |
|
|
</div> |
|
|
<h1 className="text-4xl font-bold text-white mb-2 drop-shadow-lg"> |
|
|
Invoice Information Extractor |
|
|
</h1> |
|
|
<p className="text-lg text-white/90 drop-shadow"> |
|
|
Extract text, detect signatures and stamps from invoices using AI |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
{/* Main Content */} |
|
|
<div className="space-y-6"> |
|
|
{/* Upload Section */} |
|
|
<div className="glass-morphism p-6"> |
|
|
<FileUpload onFilesSelected={handleFilesSelected} disabled={processing} /> |
|
|
</div> |
|
|
|
|
|
{/* Image Previews with Resolution Sliders */} |
|
|
{previewImages.length > 0 && !processing && results.length === 0 && ( |
|
|
<div className="space-y-4"> |
|
|
<div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-md"> |
|
|
<h2 className="text-xl font-bold text-gray-800"> |
|
|
Preview & Adjust Resolution |
|
|
</h2> |
|
|
<span className="text-sm text-gray-600"> |
|
|
{previewImages.length} {previewImages.length === 1 ? 'Image' : 'Images'} |
|
|
</span> |
|
|
</div> |
|
|
|
|
|
<div className={`grid gap-4 ${ |
|
|
previewImages.length === 1 |
|
|
? 'grid-cols-1 max-w-2xl mx-auto' |
|
|
: previewImages.length === 2 |
|
|
? 'grid-cols-1 md:grid-cols-2' |
|
|
: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' |
|
|
}`}> |
|
|
{previewImages.map((preview, idx) => ( |
|
|
<ImagePreview |
|
|
key={preview.key} |
|
|
onReasoningModeToggle={() => handleReasoningModeToggle(preview.key)} |
|
|
useReasoning={reasoningMap[preview.key] || false} |
|
|
imageData={preview.dataUrl} |
|
|
fileName={preview.filename} |
|
|
onResolutionChange={(dataUrl, resolution) => |
|
|
handleResolutionChange(preview.key, dataUrl, resolution) |
|
|
} |
|
|
onEnhanceToggle={() => handleEnhanceToggle(preview.key)} |
|
|
isEnhanced={enhancedMap[preview.key] || false} |
|
|
/> |
|
|
))} |
|
|
</div> |
|
|
|
|
|
<button |
|
|
onClick={handleProcessImages} |
|
|
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white font-semibold py-4 px-6 rounded-lg hover:from-green-600 hover:to-green-700 transition-all shadow-lg hover:shadow-xl flex items-center justify-center space-x-2 text-lg" |
|
|
> |
|
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> |
|
|
</svg> |
|
|
<span>Process {previewImages.length} Image{previewImages.length > 1 ? 's' : ''}</span> |
|
|
</button> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Error Display */} |
|
|
{error && ( |
|
|
<div className="bg-red-50 border-2 border-red-200 rounded-xl p-4 flex items-start"> |
|
|
<AlertCircle className="w-6 h-6 text-red-600 mr-3 flex-shrink-0 mt-0.5" /> |
|
|
<div> |
|
|
<h3 className="text-red-800 font-semibold mb-1">Error</h3> |
|
|
<p className="text-red-700 text-sm">{error}</p> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Progress Indicator */} |
|
|
{processing && ( |
|
|
<div className="space-y-4"> |
|
|
<ProgressIndicator |
|
|
total={progress.total} |
|
|
completed={progress.completed} |
|
|
current={progress.current} |
|
|
results={results} |
|
|
/> |
|
|
|
|
|
{/* Show image being processed with scanning animation */} |
|
|
{processingIndex !== null && previewImages[processingIndex] && ( |
|
|
<div className="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200 p-6"> |
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center"> |
|
|
<svg className="w-5 h-5 mr-2 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> |
|
|
</svg> |
|
|
Processing: {previewImages[processingIndex].filename} |
|
|
</h3> |
|
|
<div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center"> |
|
|
<img |
|
|
src={previewImages[processingIndex].dataUrl} |
|
|
alt="Processing" |
|
|
className="max-w-full max-h-96 rounded shadow-md" |
|
|
/> |
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/10 rounded-lg"> |
|
|
<div className="scanning-line"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Results Section */} |
|
|
{results.length > 0 && ( |
|
|
<div className="space-y-4"> |
|
|
<div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-md"> |
|
|
<h2 className="text-2xl font-bold text-gray-800 flex items-center"> |
|
|
<FileText className="w-6 h-6 mr-2 text-primary-600" /> |
|
|
Processing Results |
|
|
</h2> |
|
|
<span className="text-sm font-medium text-gray-600 bg-primary-50 px-3 py-1 rounded-full"> |
|
|
{results.length} {results.length === 1 ? 'Document' : 'Documents'} |
|
|
</span> |
|
|
</div> |
|
|
|
|
|
{results.map((result, index) => { |
|
|
const imageKey = result.key || `${result.originalFile}_${index}`; |
|
|
const originalImage = imageDataMap[imageKey] || imageDataMap[Object.keys(imageDataMap)[index]]; |
|
|
const processedImage = result.processedImageData || originalImage; |
|
|
return ( |
|
|
<ResultCard |
|
|
key={index} |
|
|
result={result} |
|
|
imageData={originalImage} |
|
|
processedImageData={processedImage} |
|
|
onReprocess={handleReprocess} |
|
|
isProcessing={processingIndex === index} |
|
|
/> |
|
|
); |
|
|
})} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Info Cards */} |
|
|
{!processing && results.length === 0 && ( |
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
|
<div className="glass-morphism p-6 text-center"> |
|
|
<div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3"> |
|
|
<FileText className="w-6 h-6 text-primary-600" /> |
|
|
</div> |
|
|
<h3 className="font-semibold text-gray-800 mb-2">Multiple Formats</h3> |
|
|
<p className="text-sm text-gray-600"> |
|
|
Upload images or PDFs. PDFs are automatically converted to images. |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<div className="glass-morphism p-6 text-center"> |
|
|
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3"> |
|
|
<svg className="w-6 h-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /> |
|
|
</svg> |
|
|
</div> |
|
|
<h3 className="font-semibold text-gray-800 mb-2">Batch Processing</h3> |
|
|
<p className="text-sm text-gray-600"> |
|
|
Process multiple documents at once and see results one by one. |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<div className="glass-morphism p-6 text-center"> |
|
|
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3"> |
|
|
<svg className="w-6 h-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> |
|
|
</svg> |
|
|
</div> |
|
|
<h3 className="font-semibold text-gray-800 mb-2">Visual Detection</h3> |
|
|
<p className="text-sm text-gray-600"> |
|
|
Automatically detect and highlight signatures and stamps on documents. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Footer */} |
|
|
<div className="mt-12 text-center text-white/80 text-sm"> |
|
|
<p>By Team DevBytes ⚡</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export default App; |
|
|
|