import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Box, Typography, Card, CardContent, Button, Chip, Stack, Divider, IconButton, Paper, Alert, CircularProgress, Tooltip, Dialog, DialogContent, DialogTitle, DialogActions, Container, Fade, Skeleton, TextField, InputAdornment, } 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, OpenInFull as ExpandIcon, Close as CloseIcon, Delete as DeleteIcon, } 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 { t } = useTranslation(); 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 [ocrSearchTerm, setOcrSearchTerm] = useState(''); const [expandedOcrText, setExpandedOcrText] = useState(false); 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); // Delete functionality state const [deleting, setDeleting] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = 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); }; // Delete handlers const handleDeleteDocument = async () => { if (!document) return; setDeleting(true); try { await documentService.delete(document.id); // Navigate back to documents page after successful deletion navigate('/documents'); } catch (error) { console.error('Failed to delete document:', error); // Show error message to user alert(t('common.status.error')); } finally { setDeleting(false); setDeleteConfirmOpen(false); } }; const handleDeleteClick = () => { setDeleteConfirmOpen(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(t('documentDetails.errors.notFound')); setLoading(false); return; } try { setLoading(true); setError(null); const response = await documentService.getById(id); setDocument(response.data); } catch (err: any) { const errorMessage = err.message || t('common.status.error'); 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 || t('documentDetails.ocr.noText')); } catch (err) { console.error('Failed to fetch OCR text:', err); setOcrText(t('documentDetails.ocr.loadFailed')); } 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(t('documentDetails.dialogs.processedImage.noImage')); } 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 || t('documentDetails.errors.notFound')} ); } return ( {/* Modern Header */} {document?.original_filename || t('navigation.documents')} {/* Floating Action Menu */} {document?.has_ocr_text && ( )} {deleting ? : } {t('documentDetails.subtitle')} {/* 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 */} {t('documentDetails.metadata.fileSize')} {formatFileSize(document.file_size)} {t('documentDetails.metadata.uploadDate')} {formatDate(document.created_at)} {document.source_type && ( {t('documentDetails.metadata.sourceType')} )} {document.source_path && ( {t('documentDetails.metadata.originalPath')} {document.source_path} )} {document.original_created_at && ( {t('documentDetails.metadata.originalCreated')} {formatDate(document.original_created_at)} )} {document.original_modified_at && ( {t('documentDetails.metadata.originalModified')} {formatDate(document.original_modified_at)} )} {document.has_ocr_text && ( {t('documentDetails.metadata.ocrStatus')} } /> )} {/* 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 && ( {t('documentDetails.ocr.title')} {ocrData?.ocr_text && ( setExpandedOcrText(true)} sx={{ backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, '&:hover': { backgroundColor: theme.palette.primary.dark, }, borderRadius: 2, px: 2, }} > {t('documentDetails.ocr.expand')} )} {ocrLoading ? ( {t('documentDetails.ocr.loading')} ) : ocrData ? ( <> {/* Enhanced OCR Stats */} {ocrData.ocr_confidence && ( {Math.round(ocrData.ocr_confidence)}% {t('documentDetails.ocr.confidence')} )} {ocrData.ocr_word_count != null && ( {ocrData.ocr_word_count.toLocaleString()} {t('documentDetails.ocr.words')} )} {ocrData.ocr_processing_time_ms && ( {ocrData.ocr_processing_time_ms}ms {t('documentDetails.ocr.processingTime')} )} {/* OCR Error Display */} {ocrData.ocr_error && ( {t('documentDetails.ocr.error')} {ocrData.ocr_error} )} {/* Full OCR Text Display */} {ocrData.ocr_text ? ( {ocrData.ocr_text} ) : ( {t('documentDetails.ocr.noText')} )} {/* Processing Info */} {ocrData.ocr_completed_at && ( {t('documentDetails.ocr.completed', { date: new Date(ocrData.ocr_completed_at).toLocaleString() })} )} ) : ( {t('documentDetails.ocr.loadFailed')} )} )} {/* Processing Timeline */} {/* Tags and Labels */} {t('documentDetails.tagsLabels.title')} {/* Tags */} {document.tags && document.tags.length > 0 && ( {t('documentDetails.tagsLabels.tags')} {document.tags.map((tag, index) => ( ))} )} {/* Labels */} {t('documentDetails.tagsLabels.labels')} {documentLabels.length > 0 ? ( {documentLabels.map((label) => ( ))} ) : ( {t('documentDetails.tagsLabels.noLabels')} )} {/* OCR Text Dialog */} setShowOcrDialog(false)} maxWidth="md" fullWidth > {t('documentDetails.dialogs.ocrText.title')} {ocrData && ( {ocrData.ocr_confidence && ( )} {ocrData.ocr_word_count != null && ( )} )} {ocrLoading ? ( {t('documentDetails.dialogs.ocrText.loading')} ) : ( <> {ocrData && ocrData.ocr_error && ( {t('documentDetails.dialogs.ocrText.error', { message: ocrData.ocr_error })} )} {ocrText || t('documentDetails.dialogs.ocrText.noText')} {ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && ( {ocrData.ocr_processing_time_ms && t('documentDetails.dialogs.ocrText.processingTime', { time: ocrData.ocr_processing_time_ms })} {ocrData.ocr_processing_time_ms && ocrData.ocr_completed_at && ' • '} {ocrData.ocr_completed_at && t('documentDetails.dialogs.ocrText.completed', { date: new Date(ocrData.ocr_completed_at).toLocaleString() })} )} )} {/* Expanded OCR Text Dialog with Search */} { setExpandedOcrText(false); setOcrSearchTerm(''); }} maxWidth="lg" fullWidth PaperProps={{ sx: { height: '90vh', backgroundColor: theme.palette.background.paper, } }} > {t('documentDetails.dialogs.ocrExpanded.title')} {ocrData && ( {ocrData.ocr_confidence && ( )} {ocrData.ocr_word_count != null && ( )} )} { setExpandedOcrText(false); setOcrSearchTerm(''); }} sx={{ backgroundColor: theme.palette.action.hover, '&:hover': { backgroundColor: theme.palette.action.selected, }, }} > {/* Search Bar */} setOcrSearchTerm(e.target.value)} InputProps={{ startAdornment: ( ), endAdornment: ocrSearchTerm && ( setOcrSearchTerm('')} > ), }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2, }, }} /> {ocrSearchTerm && ( {(() => { const text = ocrData?.ocr_text || ''; const matches = text.toLowerCase().split(ocrSearchTerm.toLowerCase()).length - 1; return matches > 0 ? t('documentDetails.dialogs.ocrExpanded.matches', { count: matches, plural: matches === 1 ? '' : 'es' }) : t('documentDetails.dialogs.ocrExpanded.noMatches'); })()} )} {/* OCR Text Content */} {ocrLoading ? ( {t('documentDetails.dialogs.ocrExpanded.loading')} ) : ( <> {ocrData && ocrData.ocr_error && ( {t('documentDetails.dialogs.ocrExpanded.error', { message: ocrData.ocr_error })} )} $1' ) : ocrData.ocr_text ) : t('documentDetails.dialogs.ocrExpanded.noText') }} /> {ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && ( {ocrData.ocr_processing_time_ms && t('documentDetails.dialogs.ocrText.processingTime', { time: ocrData.ocr_processing_time_ms })} {ocrData.ocr_processing_time_ms && ocrData.ocr_completed_at && ' • '} {ocrData.ocr_completed_at && t('documentDetails.dialogs.ocrText.completed', { date: 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 > {t('documentDetails.dialogs.processedImage.title')} {processedImageUrl ? ( Processed image that was fed to OCR {t('documentDetails.dialogs.processedImage.description')} ) : ( {t('documentDetails.dialogs.processedImage.noImage')} )} {/* Label Edit Dialog */} setShowLabelDialog(false)} maxWidth="md" fullWidth > {t('documentDetails.dialogs.editLabels.title')} {t('documentDetails.dialogs.editLabels.description')} {/* Retry History Modal */} {document && ( setRetryHistoryModalOpen(false)} documentId={document.id} documentName={document.original_filename} /> )} {/* Delete Confirmation Dialog */} setDeleteConfirmOpen(false)} maxWidth="sm" fullWidth > {t('documentDetails.dialogs.delete.title')} {t('documentDetails.dialogs.delete.warning')} {t('documentDetails.dialogs.delete.details')} ); }; export default DocumentDetailsPage;