import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Box, Typography, Card, CardContent, Button, Chip, Stack, Divider, IconButton, Paper, Alert, CircularProgress, Tooltip, Dialog, DialogContent, DialogTitle, DialogActions, Container, Fade, Skeleton, } from '@mui/material'; import Grid from '@mui/material/GridLegacy'; import { ArrowBack as BackIcon, Download as DownloadIcon, PictureAsPdf as PdfIcon, Image as ImageIcon, Description as DocIcon, TextSnippet as TextIcon, CalendarToday as DateIcon, Storage as SizeIcon, Tag as TagIcon, Label as LabelIcon, Visibility as ViewIcon, Search as SearchIcon, Edit as EditIcon, PhotoFilter as ProcessedImageIcon, Source as SourceIcon, AccessTime as AccessTimeIcon, Create as CreateIcon, Info as InfoIcon, Refresh as RefreshIcon, History as HistoryIcon, Speed as SpeedIcon, MoreVert as MoreIcon, } from '@mui/icons-material'; import { documentService, OcrResponse, type Document } from '../services/api'; import DocumentViewer from '../components/DocumentViewer'; import LabelSelector from '../components/Labels/LabelSelector'; import { type LabelData } from '../components/Labels/Label'; import MetadataDisplay from '../components/MetadataDisplay'; import FileIntegrityDisplay from '../components/FileIntegrityDisplay'; import ProcessingTimeline from '../components/ProcessingTimeline'; import { RetryHistoryModal } from '../components/RetryHistoryModal'; import { useTheme } from '../contexts/ThemeContext'; import { useTheme as useMuiTheme } from '@mui/material/styles'; import api from '../services/api'; const DocumentDetailsPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { mode, modernTokens, glassEffect } = useTheme(); const theme = useMuiTheme(); const [document, setDocument] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [ocrText, setOcrText] = useState(''); const [ocrData, setOcrData] = useState(null); const [showOcrDialog, setShowOcrDialog] = useState(false); const [ocrLoading, setOcrLoading] = useState(false); const [showViewDialog, setShowViewDialog] = useState(false); const [showProcessedImageDialog, setShowProcessedImageDialog] = useState(false); const [processedImageUrl, setProcessedImageUrl] = useState(null); const [processedImageLoading, setProcessedImageLoading] = useState(false); const [thumbnailUrl, setThumbnailUrl] = useState(null); const [documentLabels, setDocumentLabels] = useState([]); const [availableLabels, setAvailableLabels] = useState([]); const [showLabelDialog, setShowLabelDialog] = useState(false); const [labelsLoading, setLabelsLoading] = useState(false); // Retry functionality state const [retryingOcr, setRetryingOcr] = useState(false); const [retryHistoryModalOpen, setRetryHistoryModalOpen] = useState(false); // Retry handlers const handleRetryOcr = async () => { if (!document) return; setRetryingOcr(true); try { await documentService.bulkRetryOcr({ mode: 'specific', document_ids: [document.id], priority_override: 15, }); // Show success message and refresh document setTimeout(() => { fetchDocumentDetails(); }, 1000); } catch (error) { console.error('Failed to retry OCR:', error); } finally { setRetryingOcr(false); } }; const handleShowRetryHistory = () => { setRetryHistoryModalOpen(true); }; useEffect(() => { if (id) { fetchDocumentDetails(); } }, [id]); useEffect(() => { if (document && document.has_ocr_text && !ocrData) { fetchOcrText(); } }, [document]); useEffect(() => { if (document) { loadThumbnail(); fetchDocumentLabels(); } }, [document]); useEffect(() => { fetchAvailableLabels(); }, []); const fetchDocumentDetails = async (): Promise => { if (!id) { setError('No document ID provided'); setLoading(false); return; } try { setLoading(true); setError(null); const response = await documentService.getById(id); setDocument(response.data); } catch (err: any) { const errorMessage = err.message || 'Failed to load document details'; setError(errorMessage); console.error('Failed to fetch document details:', err); } finally { setLoading(false); } }; const handleDownload = async (): Promise => { if (!document) return; try { const response = await documentService.download(document.id); const url = window.URL.createObjectURL(new Blob([response.data])); const link = window.document.createElement('a'); link.href = url; link.setAttribute('download', document.original_filename); window.document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (err) { console.error('Download failed:', err); } }; const fetchOcrText = async (): Promise => { if (!document || !document.has_ocr_text) return; try { setOcrLoading(true); const response = await documentService.getOcrText(document.id); setOcrData(response.data); setOcrText(response.data.ocr_text || 'No OCR text available'); } catch (err) { console.error('Failed to fetch OCR text:', err); setOcrText('Failed to load OCR text. Please try again.'); } finally { setOcrLoading(false); } }; const handleViewOcr = (): void => { setShowOcrDialog(true); if (!ocrData) { fetchOcrText(); } }; const handleViewProcessedImage = async (): Promise => { if (!document) return; setProcessedImageLoading(true); try { const response = await documentService.getProcessedImage(document.id); const url = window.URL.createObjectURL(new Blob([response.data], { type: 'image/png' })); setProcessedImageUrl(url); setShowProcessedImageDialog(true); } catch (err: any) { console.log('Processed image not available:', err); alert('No processed image available for this document. This feature requires "Save Processed Images" to be enabled in OCR settings.'); } finally { setProcessedImageLoading(false); } }; const loadThumbnail = async (): Promise => { if (!document) return; try { const response = await documentService.getThumbnail(document.id); const url = window.URL.createObjectURL(new Blob([response.data])); setThumbnailUrl(url); } catch (err) { console.log('Thumbnail not available:', err); // Thumbnail not available, use fallback icon } }; const handleViewDocument = (): void => { setShowViewDialog(true); }; const getFileIcon = (mimeType?: string): React.ReactElement => { if (mimeType?.includes('pdf')) return ; if (mimeType?.includes('image')) return ; if (mimeType?.includes('text')) return ; return ; }; const formatFileSize = (bytes: number): string => { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }; const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const fetchDocumentLabels = async (): Promise => { if (!id) return; try { const response = await api.get(`/labels/documents/${id}`); if (response.status === 200 && Array.isArray(response.data)) { setDocumentLabels(response.data); } } catch (error) { console.error('Failed to fetch document labels:', error); } }; const fetchAvailableLabels = async (): Promise => { try { setLabelsLoading(true); const response = await api.get('/labels?include_counts=false'); if (response.status === 200 && Array.isArray(response.data)) { setAvailableLabels(response.data); } } catch (error) { console.error('Failed to fetch available labels:', error); } finally { setLabelsLoading(false); } }; const handleCreateLabel = async (labelData: Omit) => { try { const response = await api.post('/labels', labelData); const newLabel = response.data; setAvailableLabels(prev => [...prev, newLabel]); return newLabel; } catch (error) { console.error('Failed to create label:', error); throw error; } }; const handleSaveLabels = async (selectedLabels: LabelData[]): Promise => { if (!id) return; try { const labelIds = selectedLabels.map(label => label.id); await api.put(`/labels/documents/${id}`, { label_ids: labelIds }); setDocumentLabels(selectedLabels); setShowLabelDialog(false); } catch (error) { console.error('Failed to save labels:', error); } }; if (loading) { return ( ); } if (error || !document) { return ( {error || 'Document not found'} ); } return ( {/* Modern Header */} {document?.original_filename || 'Document Details'} {/* Floating Action Menu */} {document?.has_ocr_text && ( )} Comprehensive document analysis and metadata viewer {/* Modern Content Layout */} {/* Hero Document Preview */} {/* Document Preview */} {thumbnailUrl ? ( {document.original_filename} { e.currentTarget.style.transform = 'scale(1.05) rotateY(5deg)'; e.currentTarget.style.boxShadow = theme.shadows[12]; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1) rotateY(0deg)'; e.currentTarget.style.boxShadow = theme.shadows[8]; }} /> ) : ( {getFileIcon(document.mime_type)} )} {/* File Type Badge */} {/* Quick Stats */} File Size {formatFileSize(document.file_size)} Upload Date {formatDate(document.created_at)} {document.source_type && ( Source Type )} {document.source_path && ( Original Path {document.source_path} )} {document.original_created_at && ( Original Created {formatDate(document.original_created_at)} )} {document.original_modified_at && ( Original Modified {formatDate(document.original_modified_at)} )} {document.has_ocr_text && ( OCR Status } /> )} {/* Action Buttons */} {document.mime_type?.includes('image') && ( {processedImageLoading ? ( ) : ( )} )} {retryingOcr ? ( ) : ( )} {/* File Integrity Display - Moved here */} {/* Main Content Area */} {/* OCR Text Section - Moved higher */} {document.has_ocr_text && ( 🔍 Extracted Text (OCR) {ocrLoading ? ( Loading OCR analysis... ) : ocrData ? ( <> {/* Enhanced OCR Stats */} {ocrData.ocr_confidence && ( {Math.round(ocrData.ocr_confidence)}% Confidence )} {ocrData.ocr_word_count && ( {ocrData.ocr_word_count.toLocaleString()} Words )} {ocrData.ocr_processing_time_ms && ( {ocrData.ocr_processing_time_ms}ms Processing Time )} {/* OCR Error Display */} {ocrData.ocr_error && ( OCR Processing Error {ocrData.ocr_error} )} {/* Full OCR Text Display */} {ocrData.ocr_text ? ( {ocrData.ocr_text} ) : ( No OCR text available for this document. )} {/* Processing Info */} {ocrData.ocr_completed_at && ( ✅ Processing completed: {new Date(ocrData.ocr_completed_at).toLocaleString()} )} ) : ( OCR text is available but failed to load. Please try refreshing the page. )} )} {/* Processing Timeline */} {/* Tags and Labels */} 🏷️ Tags & Labels {/* Tags */} {document.tags && document.tags.length > 0 && ( Tags {document.tags.map((tag, index) => ( ))} )} {/* Labels */} Labels {documentLabels.length > 0 ? ( {documentLabels.map((label) => ( ))} ) : ( No labels assigned to this document )} {/* OCR Text Dialog */} setShowOcrDialog(false)} maxWidth="md" fullWidth > Extracted Text (OCR) {ocrData && ( {ocrData.ocr_confidence && ( )} {ocrData.ocr_word_count && ( )} )} {ocrLoading ? ( Loading OCR text... ) : ( <> {ocrData && ocrData.ocr_error && ( OCR Error: {ocrData.ocr_error} )} {ocrText || 'No OCR text available for this document.'} {ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && ( {ocrData.ocr_processing_time_ms && `Processing time: ${ocrData.ocr_processing_time_ms}ms`} {ocrData.ocr_processing_time_ms && ocrData.ocr_completed_at && ' • '} {ocrData.ocr_completed_at && `Completed: ${new Date(ocrData.ocr_completed_at).toLocaleString()}`} )} )} {/* Document View Dialog */} setShowViewDialog(false)} maxWidth="lg" fullWidth PaperProps={{ sx: { height: '90vh' } }} > {document?.original_filename} {document && ( )} {/* Processed Image Dialog */} setShowProcessedImageDialog(false)} maxWidth="lg" fullWidth > Processed Image - OCR Enhancement Applied {processedImageUrl ? ( Processed image that was fed to OCR This is the enhanced image that was actually processed by the OCR engine. You can adjust OCR enhancement settings in the Settings page. ) : ( No processed image available )} {/* Label Edit Dialog */} setShowLabelDialog(false)} maxWidth="md" fullWidth > Edit Document Labels Select labels to assign to this document {/* Retry History Modal */} {document && ( setRetryHistoryModalOpen(false)} documentId={document.id} documentName={document.original_filename} /> )} ); }; export default DocumentDetailsPage;