Spaces:
Sleeping
Sleeping
| // App.js - Main React Component | |
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | |
| import './App.css'; | |
| import { w3cwebsocket as W3CWebSocket } from "websocket"; | |
| // Document Processing Component (Left Zone) | |
| const DocumentPanel = ({ onDocumentSelect, onExtract, currentDocument, isProcessing }) => { | |
| const [dragActive, setDragActive] = useState(false); | |
| const fileInputRef = useRef(null); | |
| const handleDrag = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.type === "dragenter" || e.type === "dragover") { | |
| setDragActive(true); | |
| } else if (e.type === "dragleave") { | |
| setDragActive(false); | |
| } | |
| }; | |
| const handleDrop = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { | |
| handleFileSelect(e.dataTransfer.files[0]); | |
| } | |
| }; | |
| const handleFileSelect = async (file) => { | |
| // Upload file | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const response = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| onDocumentSelect(result); | |
| } else { | |
| console.error('Upload failed'); | |
| } | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| } | |
| }; | |
| const handleFileInput = (e) => { | |
| if (e.target.files && e.target.files[0]) { | |
| handleFileSelect(e.target.files[0]); | |
| } | |
| }; | |
| const handleDelete = async () => { | |
| if (currentDocument) { | |
| try { | |
| await fetch(`/api/documents/${currentDocument.document_id}`, { | |
| method: 'DELETE', | |
| }); | |
| onDocumentSelect(null); | |
| } catch (error) { | |
| console.error('Delete error:', error); | |
| } | |
| } | |
| }; | |
| const renderThumbnail = () => { | |
| if (!currentDocument) { | |
| return <div className="thumbnail-placeholder">Document</div>; | |
| } | |
| // Use server-generated thumbnail for all document types | |
| const thumbnailUrl = `/api/documents/${currentDocument.document_id}/thumbnail`; | |
| return ( | |
| <img | |
| src={thumbnailUrl} | |
| alt={`Miniature de ${currentDocument.filename}`} | |
| className="thumbnail-image" | |
| onError={(e) => { | |
| // Fallback if thumbnail generation fails | |
| e.target.style.display = 'none'; | |
| e.target.nextSibling.style.display = 'flex'; | |
| }} | |
| /> | |
| ); | |
| }; | |
| return ( | |
| <div className="panel document-panel"> | |
| <div className="panel-header"> | |
| <h2>Document Processing</h2> | |
| <p>Upload and intelligent analysis of your documents</p> | |
| </div> | |
| <div className="document-content"> | |
| {!currentDocument ? ( | |
| <div | |
| className={`upload-zone ${dragActive ? 'drag-active' : ''}`} | |
| onDragEnter={handleDrag} | |
| onDragLeave={handleDrag} | |
| onDragOver={handleDrag} | |
| onDrop={handleDrop} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <div className="upload-icon">+</div> | |
| <h3>Drop your document here</h3> | |
| <p>or click to browse your files</p> | |
| <div className="supported-formats"> | |
| PDF β’ JPG β’ PNG β’ GIF β’ WebP β’ BMP β’ TIFF | |
| </div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tiff" | |
| onChange={handleFileInput} | |
| style={{ display: 'none' }} | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="document-preview fade-in"> | |
| <div className="document-info"> | |
| <div className="document-header"> | |
| <h3>{currentDocument.filename}</h3> | |
| <div className="document-meta"> | |
| <div className="file-size"> | |
| {(currentDocument.file_size / 1024).toFixed(1)} KB | |
| </div> | |
| <span className={`status status-${currentDocument.status}`}> | |
| {currentDocument.status} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Document thumbnail with PDF support */} | |
| <div className="document-thumbnail"> | |
| {renderThumbnail()} | |
| <div className="thumbnail-placeholder" style={{ display: 'none' }}> | |
| π {currentDocument.filename} | |
| </div> | |
| </div> | |
| {/* Horizontal action buttons */} | |
| <div className="action-buttons"> | |
| <button | |
| className="btn btn-primary" | |
| onClick={() => onExtract(currentDocument.document_id)} | |
| disabled={isProcessing} | |
| > | |
| {isProcessing ? ( | |
| <> | |
| <span className="spinner"></span> | |
| Analyzing... | |
| </> | |
| ) : ( | |
| 'Extract' | |
| )} | |
| </button> | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => fileInputRef.current?.click()} | |
| disabled={isProcessing} | |
| > | |
| Change | |
| </button> | |
| <button | |
| className="btn btn-danger" | |
| onClick={handleDelete} | |
| disabled={isProcessing} | |
| > | |
| Delete | |
| </button> | |
| </div> | |
| </div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tiff" | |
| onChange={handleFileInput} | |
| style={{ display: 'none' }} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // Enhanced Results Component (Right Zone) | |
| const ResultsPanel = ({ extractionResult }) => { | |
| const [copiedText, setCopiedText] = useState(''); | |
| const getMetricData = (result, key) => { | |
| if (!result || result[key] === undefined || result[key] === null) { | |
| return { value: 0, display: 'N/A', quality: 'poor' }; | |
| } | |
| const value = typeof result[key] === 'number' ? result[key] : parseFloat(result[key]) || 0; | |
| // Keep decimal precision for more realistic display | |
| const percentage = Math.round(value * 1000) / 10; // One decimal place | |
| let quality = 'poor'; | |
| if (percentage >= 90) quality = 'excellent'; | |
| else if (percentage >= 75) quality = 'good'; | |
| else if (percentage >= 50) quality = 'average'; | |
| return { | |
| value: percentage, | |
| display: `${percentage.toFixed(1)}%`, | |
| quality, | |
| color: quality === 'excellent' ? '#34C759' : | |
| quality === 'good' ? '#007AFF' : | |
| quality === 'average' ? '#FF9500' : '#FF3B30' | |
| }; | |
| }; | |
| const copyToClipboard = async () => { | |
| const text = formatExtractedText(extractionResult); | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setCopiedText('Copied!'); | |
| setTimeout(() => setCopiedText(''), 2000); | |
| } catch (err) { | |
| setCopiedText('Error'); | |
| setTimeout(() => setCopiedText(''), 2000); | |
| } | |
| }; | |
| const formatExtractedText = (result) => { | |
| if (!result) return ''; | |
| let text = ''; | |
| // Header with metadata | |
| text += `=====================================\n`; | |
| text += ` DOCUMENT ANALYSIS REPORT\n`; | |
| text += `=====================================\n\n`; | |
| // Document information | |
| if (result.document_type) { | |
| text += `DOCUMENT TYPE: ${result.document_type}\n`; | |
| } | |
| if (result.confidence_level) { | |
| text += `CONFIDENCE LEVEL: ${(result.confidence_level * 100).toFixed(1)}%\n`; | |
| } | |
| if (result.processing_time) { | |
| text += `PROCESSING TIME: ${result.processing_time}\n`; | |
| } | |
| text += `\n`; | |
| // Analysis scores | |
| text += `ANALYSIS SCORES:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| const imageQuality = getMetricData(result, 'image_quality_score'); | |
| const businessLogic = getMetricData(result, 'business_logic_score'); | |
| const infoRelevance = getMetricData(result, 'information_relevance_score'); | |
| text += ` β’ Image quality..........: ${imageQuality.display}\n`; | |
| text += ` β’ Business logic.........: ${businessLogic.display}\n`; | |
| text += ` β’ Information relevance..: ${infoRelevance.display}\n\n`; | |
| // OCR content | |
| if (result.ocr_text) { | |
| text += `EXTRACTED TEXT:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| text += `${result.ocr_text}\n\n`; | |
| } | |
| // Structured data | |
| if (result.key_fields && result.key_fields.length > 0) { | |
| text += `IDENTIFIED KEY FIELDS:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| result.key_fields.forEach((field, index) => { | |
| text += ` ${index + 1}. ${field.field}: ${field.value}\n`; | |
| if (field.confidence) { | |
| text += ` Confidence: ${(field.confidence * 100).toFixed(1)}%\n`; | |
| } | |
| }); | |
| text += '\n'; | |
| } | |
| // Dates | |
| if (result.dates && result.dates.length > 0) { | |
| text += `DETECTED DATES:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| result.dates.forEach((date, index) => { | |
| text += ` ${index + 1}. ${date.date_type}: ${date.date_value}\n`; | |
| if (date.confidence) { | |
| text += ` Confidence: ${(date.confidence * 100).toFixed(1)}%\n`; | |
| } | |
| }); | |
| text += '\n'; | |
| } | |
| // Amounts | |
| if (result.amounts && result.amounts.length > 0) { | |
| text += `IDENTIFIED AMOUNTS:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| result.amounts.forEach((amount, index) => { | |
| text += ` ${index + 1}. ${amount.amount_type}: ${amount.amount_value}`; | |
| if (amount.currency) text += ` ${amount.currency}`; | |
| text += '\n'; | |
| if (amount.confidence) { | |
| text += ` Confidence: ${(amount.confidence * 100).toFixed(1)}%\n`; | |
| } | |
| }); | |
| text += '\n'; | |
| } | |
| // Named entities | |
| if (result.entities && result.entities.length > 0) { | |
| text += `RECOGNIZED ENTITIES:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| const groupedEntities = {}; | |
| result.entities.forEach(entity => { | |
| if (!groupedEntities[entity.entity_type]) { | |
| groupedEntities[entity.entity_type] = []; | |
| } | |
| groupedEntities[entity.entity_type].push(entity); | |
| }); | |
| Object.entries(groupedEntities).forEach(([type, entities]) => { | |
| text += ` ${type}:\n`; | |
| entities.forEach(entity => { | |
| text += ` β’ ${entity.entity_value}`; | |
| if (entity.confidence) { | |
| text += ` (${(entity.confidence * 100).toFixed(1)}%)`; | |
| } | |
| text += '\n'; | |
| }); | |
| }); | |
| text += '\n'; | |
| } | |
| // Analysis summary | |
| if (result.summary) { | |
| text += `ANALYSIS SUMMARY:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| if (result.summary.total_fields) { | |
| text += ` β’ Extracted fields: ${result.summary.total_fields}\n`; | |
| } | |
| if (result.summary.pages_processed) { | |
| text += ` β’ Pages processed: ${result.summary.pages_processed}\n`; | |
| } | |
| if (result.summary.accuracy_score) { | |
| text += ` β’ Accuracy score: ${(result.summary.accuracy_score * 100).toFixed(1)}%\n`; | |
| } | |
| } | |
| // AI feedback | |
| if (result.supervisor_feedback) { | |
| text += `\nAI ANALYSIS:\n`; | |
| text += `ββββββββββββββββββββββββββββββββββββββββ\n`; | |
| text += `${result.supervisor_feedback}\n`; | |
| } | |
| text += `\n=====================================\n`; | |
| text += `Generated on ${new Date().toLocaleString('en-US')}\n`; | |
| text += `=====================================`; | |
| return text; | |
| }; | |
| const imageQuality = getMetricData(extractionResult, 'image_quality_score'); | |
| const businessLogic = getMetricData(extractionResult, 'business_logic_score'); | |
| const infoRelevance = getMetricData(extractionResult, 'information_relevance_score'); | |
| return ( | |
| <div className="panel results-panel"> | |
| <div className="panel-header"> | |
| <h2>Analysis Results</h2> | |
| <p>Performance metrics and extracted data</p> | |
| </div> | |
| <div className="results-content"> | |
| {/* Enhanced metrics */} | |
| <div className="analysis-metrics"> | |
| <div className="metric-card"> | |
| <div className="metric-label">Image Quality</div> | |
| <div className={`metric-value metric-value ${imageQuality.quality}`}> | |
| {imageQuality.display} | |
| </div> | |
| <div className="metric-bar"> | |
| <div | |
| className="metric-bar-fill" | |
| style={{ | |
| width: `${imageQuality.value}%`, | |
| background: imageQuality.color | |
| }} | |
| ></div> | |
| </div> | |
| </div> | |
| <div className="metric-card"> | |
| <div className="metric-label">Business Logic</div> | |
| <div className={`metric-value ${businessLogic.quality}`}> | |
| {businessLogic.display} | |
| </div> | |
| <div className="metric-bar"> | |
| <div | |
| className="metric-bar-fill" | |
| style={{ | |
| width: `${businessLogic.value}%`, | |
| background: businessLogic.color | |
| }} | |
| ></div> | |
| </div> | |
| </div> | |
| <div className="metric-card"> | |
| <div className="metric-label">Info Relevance</div> | |
| <div className={`metric-value ${infoRelevance.quality}`}> | |
| {infoRelevance.display} | |
| </div> | |
| <div className="metric-bar"> | |
| <div | |
| className="metric-bar-fill" | |
| style={{ | |
| width: `${infoRelevance.value}%`, | |
| background: infoRelevance.color | |
| }} | |
| ></div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Enhanced extracted content zone */} | |
| <div className="extracted-content"> | |
| <div className="content-header"> | |
| <h3>Extracted Data</h3> | |
| <button | |
| className="copy-button" | |
| onClick={copyToClipboard} | |
| title="Copy to clipboard" | |
| > | |
| {copiedText || 'Copy'} | |
| </button> | |
| </div> | |
| <div className="extracted-text"> | |
| {extractionResult ? formatExtractedText(extractionResult) : ''} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // Real Terminal with actual system logs | |
| const Terminal = ({ logs }) => { | |
| const terminalRef = useRef(null); | |
| useEffect(() => { | |
| if (terminalRef.current) { | |
| terminalRef.current.scrollTop = terminalRef.current.scrollHeight; | |
| } | |
| }, [logs]); | |
| return ( | |
| <div className="terminal-section"> | |
| <div className="terminal-header"> | |
| <div className="terminal-title"> | |
| System Terminal | |
| </div> | |
| <div className="terminal-controls"> | |
| <button className="terminal-btn close"></button> | |
| <button className="terminal-btn minimize"></button> | |
| <button className="terminal-btn maximize"></button> | |
| </div> | |
| </div> | |
| <div className="terminal-content" ref={terminalRef}> | |
| <div className="terminal-body"> | |
| {logs.map((log, index) => { | |
| let logClass = 'log-info'; | |
| let content = ''; | |
| if (log.type === 'log') { | |
| logClass = 'log-message'; | |
| content = log.content; | |
| } else { | |
| content = log.content; | |
| if (log.type === 'error') logClass = 'log-error'; | |
| else if (log.type === 'success') logClass = 'log-success'; | |
| else if (log.type === 'warning') logClass = 'log-warning'; | |
| else if (log.type === 'system') logClass = 'log-system'; | |
| } | |
| return <div key={index} className={logClass}>{content}</div>; | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // Main App Component | |
| const App = () => { | |
| const [currentDocument, setCurrentDocument] = useState(null); | |
| const [extractionResult, setExtractionResult] = useState(null); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [logs, setLogs] = useState([]); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const wsClient = useRef(null); | |
| const addLog = useCallback((log) => { | |
| setLogs(prevLogs => [...prevLogs, log].slice(-100)); // Keep last 100 logs | |
| }, []); | |
| useEffect(() => { | |
| const connectWebSocket = () => { | |
| // Determine WebSocket protocol based on browser's protocol | |
| const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; | |
| // Construct the WebSocket URL based on the browser's host | |
| const wsUrl = `${wsProtocol}//${window.location.host}/ws`; | |
| console.log(`Attempting to connect to WebSocket at: ${wsUrl}`); | |
| const client = new W3CWebSocket(wsUrl); | |
| wsClient.current = client; | |
| client.onopen = () => { | |
| console.log("WebSocket Client Connected"); | |
| setIsConnected(true); | |
| addLog({ type: 'system', content: `[${new Date().toLocaleTimeString()}] System Connected to backend.` }); | |
| }; | |
| client.onmessage = (message) => { | |
| const data = JSON.parse(message.data); | |
| if (data.type === 'log') { | |
| // This is a real log from the backend | |
| addLog({ type: 'log', content: data.content }); | |
| } else { | |
| // This is a direct message (e.g., upload success) | |
| addLog({ type: data.type, content: `[${data.timestamp}] ${data.message}` }); | |
| } | |
| }; | |
| client.onerror = (error) => { | |
| console.error("WebSocket Error: ", error); | |
| setIsConnected(false); | |
| addLog({ type: 'error', content: `[${new Date().toLocaleTimeString()}] WebSocket connection error. See browser console for details.` }); | |
| }; | |
| client.onclose = () => { | |
| console.log("WebSocket Client Disconnected"); | |
| setIsConnected(false); | |
| addLog({ type: 'system', content: `[${new Date().toLocaleTimeString()}] System Disconnected. Attempting to reconnect in 5s...` }); | |
| // Implement reconnection logic | |
| setTimeout(connectWebSocket, 5000); | |
| }; | |
| }; | |
| connectWebSocket(); | |
| // Cleanup on component unmount | |
| return () => { | |
| if (wsClient.current) { | |
| wsClient.current.close(); | |
| } | |
| }; | |
| }, [addLog]); // Re-run effect if addLog changes | |
| const handleDocumentSelect = (document) => { | |
| setCurrentDocument(document); | |
| setExtractionResult(null); | |
| addLog({ type: 'system', content: `[${new Date().toLocaleTimeString()}] Document "${document.filename}" uploaded successfully (${(document.file_size / 1024).toFixed(1)} KB)` }); | |
| addLog({ type: 'info', content: 'Preparing multi-agent analysis pipeline...' }); | |
| }; | |
| const handleExtract = async (documentId) => { | |
| if (!documentId) return; | |
| setIsProcessing(true); | |
| setExtractionResult(null); // Clear previous results | |
| try { | |
| // Start the extraction process | |
| const response = await fetch(`/api/extract/${documentId}`, { | |
| method: 'POST', | |
| }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| console.log('Extraction result:', result); // Debug log | |
| setExtractionResult(result); | |
| // Final success message | |
| addLog({ type: 'system', content: 'Analysis results loaded successfully' }); | |
| } else { | |
| const errorText = await response.text(); | |
| console.error('Extraction error response:', errorText); | |
| addLog({ type: 'error', content: `Error during analysis: ${response.status}` }); | |
| } | |
| } catch (error) { | |
| console.error('Extraction error:', error); | |
| addLog({ type: 'error', content: `Network error: ${error.message}` }); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| }; | |
| return ( | |
| <div className="app-container"> | |
| {/* Header */} | |
| <header className="app-header"> | |
| <div className="app-title"> | |
| OCR/LAD/RAD Intelligence Platform | |
| </div> | |
| <div className={`connection-status ${isConnected ? 'connected' : ''}`}> | |
| <div className="status-light"></div> | |
| {isConnected ? 'System Connected' : 'System Disconnected'} | |
| </div> | |
| </header> | |
| {/* Main content - 2 columns */} | |
| <main className="main-content"> | |
| <DocumentPanel | |
| onDocumentSelect={handleDocumentSelect} | |
| onExtract={handleExtract} | |
| currentDocument={currentDocument} | |
| isProcessing={isProcessing} | |
| /> | |
| <ResultsPanel | |
| extractionResult={extractionResult} | |
| /> | |
| </main> | |
| {/* Terminal at bottom with real system logs */} | |
| <Terminal logs={logs} /> | |
| </div> | |
| ); | |
| }; | |
| export default App; |