feat(document_details): add "expand" to OCR text box
This commit is contained in:
parent
a393bd030f
commit
418eb48ee4
|
|
@ -21,6 +21,8 @@ import {
|
||||||
Container,
|
Container,
|
||||||
Fade,
|
Fade,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import Grid from '@mui/material/GridLegacy';
|
import Grid from '@mui/material/GridLegacy';
|
||||||
import {
|
import {
|
||||||
|
|
@ -46,6 +48,8 @@ import {
|
||||||
History as HistoryIcon,
|
History as HistoryIcon,
|
||||||
Speed as SpeedIcon,
|
Speed as SpeedIcon,
|
||||||
MoreVert as MoreIcon,
|
MoreVert as MoreIcon,
|
||||||
|
OpenInFull as ExpandIcon,
|
||||||
|
Close as CloseIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { documentService, OcrResponse, type Document } from '../services/api';
|
import { documentService, OcrResponse, type Document } from '../services/api';
|
||||||
import DocumentViewer from '../components/DocumentViewer';
|
import DocumentViewer from '../components/DocumentViewer';
|
||||||
|
|
@ -77,6 +81,7 @@ const DocumentDetailsPage: React.FC = () => {
|
||||||
const [processedImageLoading, setProcessedImageLoading] = useState<boolean>(false);
|
const [processedImageLoading, setProcessedImageLoading] = useState<boolean>(false);
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||||
const [documentLabels, setDocumentLabels] = useState<LabelData[]>([]);
|
const [documentLabels, setDocumentLabels] = useState<LabelData[]>([]);
|
||||||
|
const [ocrSearchTerm, setOcrSearchTerm] = useState<string>(''); const [expandedOcrText, setExpandedOcrText] = useState<boolean>(false);
|
||||||
const [availableLabels, setAvailableLabels] = useState<LabelData[]>([]);
|
const [availableLabels, setAvailableLabels] = useState<LabelData[]>([]);
|
||||||
const [showLabelDialog, setShowLabelDialog] = useState<boolean>(false);
|
const [showLabelDialog, setShowLabelDialog] = useState<boolean>(false);
|
||||||
const [labelsLoading, setLabelsLoading] = useState<boolean>(false);
|
const [labelsLoading, setLabelsLoading] = useState<boolean>(false);
|
||||||
|
|
@ -723,9 +728,32 @@ const DocumentDetailsPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: 4 }}>
|
<CardContent sx={{ p: 4 }}>
|
||||||
<Typography variant="h5" sx={{ mb: 3, fontWeight: 700 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
🔍 Extracted Text (OCR)
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
</Typography>
|
🔍 Extracted Text (OCR)
|
||||||
|
</Typography>
|
||||||
|
{ocrData?.ocr_text && (
|
||||||
|
<Tooltip title="Expand to view full text with search">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setExpandedOcrText(true)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.dark,
|
||||||
|
},
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography variant="button" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
Expand
|
||||||
|
</Typography>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{ocrLoading ? (
|
{ocrLoading ? (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||||
|
|
@ -1071,6 +1099,167 @@ const DocumentDetailsPage: React.FC = () => {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Expanded OCR Text Dialog with Search */}
|
||||||
|
<Dialog
|
||||||
|
open={expandedOcrText}
|
||||||
|
onClose={() => {
|
||||||
|
setExpandedOcrText(false);
|
||||||
|
setOcrSearchTerm('');
|
||||||
|
}}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
height: '90vh',
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||||
|
🔍 Extracted Text (OCR) - Full View
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{ocrData && (
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{ocrData.ocr_confidence && (
|
||||||
|
<Chip
|
||||||
|
label={`${Math.round(ocrData.ocr_confidence)}% confidence`}
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ocrData.ocr_word_count && (
|
||||||
|
<Chip
|
||||||
|
label={`${ocrData.ocr_word_count} words`}
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setExpandedOcrText(false);
|
||||||
|
setOcrSearchTerm('');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.action.selected,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ p: 0 }}>
|
||||||
|
{/* Search Bar */}
|
||||||
|
<Box sx={{ p: 3, borderBottom: `1px solid ${theme.palette.divider}` }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
placeholder="Search within extracted text..."
|
||||||
|
value={ocrSearchTerm}
|
||||||
|
onChange={(e) => setOcrSearchTerm(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon color="action" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: ocrSearchTerm && (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setOcrSearchTerm('')}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{ocrSearchTerm && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
{(() => {
|
||||||
|
const text = ocrData?.ocr_text || '';
|
||||||
|
const matches = text.toLowerCase().split(ocrSearchTerm.toLowerCase()).length - 1;
|
||||||
|
return matches > 0 ? `${matches} match${matches === 1 ? '' : 'es'} found` : 'No matches found';
|
||||||
|
})()}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* OCR Text Content */}
|
||||||
|
<Box sx={{ p: 3, height: 'calc(100% - 120px)', overflow: 'auto' }}>
|
||||||
|
{ocrLoading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="body2" sx={{ ml: 2 }}>
|
||||||
|
Loading OCR text...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{ocrData && ocrData.ocr_error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
OCR Error: {ocrData.ocr_error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: 2,
|
||||||
|
minHeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontFamily: '"Inter", monospace',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
lineHeight: 1.8,
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: ocrData?.ocr_text ? 'text.primary' : 'text.secondary',
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: ocrData?.ocr_text ? (
|
||||||
|
ocrSearchTerm
|
||||||
|
? ocrData.ocr_text.replace(
|
||||||
|
new RegExp(`(${ocrSearchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'),
|
||||||
|
'<mark style="background-color: #ffeb3b; color: #000; padding: 2px 4px; border-radius: 2px;">$1</mark>'
|
||||||
|
)
|
||||||
|
: ocrData.ocr_text
|
||||||
|
) : 'No OCR text available for this document.'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
{ocrData && (ocrData.ocr_processing_time_ms || ocrData.ocr_completed_at) && (
|
||||||
|
<Box sx={{ mt: 3, pt: 2, borderTop: `1px solid ${theme.palette.divider}` }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{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()}`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Document View Dialog */}
|
{/* Document View Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showViewDialog}
|
open={showViewDialog}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue