467 lines
17 KiB
TypeScript
467 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
FormControl,
|
|
FormLabel,
|
|
RadioGroup,
|
|
FormControlLabel,
|
|
Radio,
|
|
TextField,
|
|
Chip,
|
|
Box,
|
|
Typography,
|
|
Alert,
|
|
LinearProgress,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
Checkbox,
|
|
Slider,
|
|
Stack,
|
|
Card,
|
|
CardContent,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import {
|
|
ExpandMore as ExpandMoreIcon,
|
|
Schedule as ScheduleIcon,
|
|
Assessment as AssessmentIcon,
|
|
Refresh as RefreshIcon,
|
|
} from '@mui/icons-material';
|
|
import { documentService, BulkOcrRetryRequest, OcrRetryFilter, BulkOcrRetryResponse, ErrorHelper, ErrorCodes } from '../services/api';
|
|
|
|
interface BulkRetryModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSuccess: (result: BulkOcrRetryResponse) => void;
|
|
selectedDocumentIds?: string[];
|
|
}
|
|
|
|
const COMMON_MIME_TYPES = [
|
|
{ value: 'application/pdf', label: 'PDF' },
|
|
{ value: 'image/png', label: 'PNG' },
|
|
{ value: 'image/jpeg', label: 'JPEG' },
|
|
{ value: 'image/tiff', label: 'TIFF' },
|
|
{ value: 'text/plain', label: 'Text' },
|
|
];
|
|
|
|
const COMMON_FAILURE_REASONS = [
|
|
{ value: 'pdf_font_encoding', label: 'Font Encoding Issues' },
|
|
{ value: 'ocr_timeout', label: 'Processing Timeout' },
|
|
{ value: 'pdf_corruption', label: 'File Corruption' },
|
|
{ value: 'low_ocr_confidence', label: 'Low Confidence' },
|
|
{ value: 'no_extractable_text', label: 'No Text Found' },
|
|
{ value: 'ocr_memory_limit', label: 'Memory Limit' },
|
|
];
|
|
|
|
const FILE_SIZE_PRESETS = [
|
|
{ label: '< 1MB', value: 1024 * 1024 },
|
|
{ label: '< 5MB', value: 5 * 1024 * 1024 },
|
|
{ label: '< 10MB', value: 10 * 1024 * 1024 },
|
|
{ label: '< 50MB', value: 50 * 1024 * 1024 },
|
|
];
|
|
|
|
export const BulkRetryModal: React.FC<BulkRetryModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onSuccess,
|
|
selectedDocumentIds = [],
|
|
}) => {
|
|
const [mode, setMode] = useState<'all' | 'specific' | 'filter'>('all');
|
|
const [filter, setFilter] = useState<OcrRetryFilter>({});
|
|
const [priorityOverride, setPriorityOverride] = useState<number>(10);
|
|
const [usePriorityOverride, setUsePriorityOverride] = useState(false);
|
|
const [previewOnly, setPreviewOnly] = useState(true);
|
|
const [loading, setLoading] = useState(false);
|
|
const [previewResult, setPreviewResult] = useState<BulkOcrRetryResponse | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Initialize mode based on selected documents
|
|
useEffect(() => {
|
|
if (selectedDocumentIds.length > 0) {
|
|
setMode('specific');
|
|
}
|
|
}, [selectedDocumentIds]);
|
|
|
|
const handleModeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setMode(event.target.value as 'all' | 'specific' | 'filter');
|
|
setPreviewResult(null);
|
|
setError(null);
|
|
};
|
|
|
|
const handleFilterChange = (key: keyof OcrRetryFilter, value: any) => {
|
|
setFilter(prev => ({
|
|
...prev,
|
|
[key]: value,
|
|
}));
|
|
setPreviewResult(null);
|
|
};
|
|
|
|
const handleMimeTypeToggle = (mimeType: string) => {
|
|
const current = filter.mime_types || [];
|
|
if (current.includes(mimeType)) {
|
|
handleFilterChange('mime_types', current.filter(t => t !== mimeType));
|
|
} else {
|
|
handleFilterChange('mime_types', [...current, mimeType]);
|
|
}
|
|
};
|
|
|
|
const handleFailureReasonToggle = (reason: string) => {
|
|
const current = filter.failure_reasons || [];
|
|
if (current.includes(reason)) {
|
|
handleFilterChange('failure_reasons', current.filter(r => r !== reason));
|
|
} else {
|
|
handleFilterChange('failure_reasons', [...current, reason]);
|
|
}
|
|
};
|
|
|
|
const buildRequest = (preview: boolean): BulkOcrRetryRequest => {
|
|
const request: BulkOcrRetryRequest = {
|
|
mode,
|
|
preview_only: preview,
|
|
};
|
|
|
|
if (mode === 'specific') {
|
|
request.document_ids = selectedDocumentIds;
|
|
} else if (mode === 'filter') {
|
|
request.filter = filter;
|
|
}
|
|
|
|
if (usePriorityOverride) {
|
|
request.priority_override = priorityOverride;
|
|
}
|
|
|
|
return request;
|
|
};
|
|
|
|
const handlePreview = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const request = buildRequest(true);
|
|
const response = await documentService.bulkRetryOcr(request);
|
|
setPreviewResult(response.data);
|
|
} catch (err: any) {
|
|
const errorInfo = ErrorHelper.formatErrorForDisplay(err, true);
|
|
let errorMessage = 'Failed to preview retry operation';
|
|
|
|
// Handle specific bulk retry preview errors
|
|
if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) ||
|
|
ErrorHelper.isErrorCode(err, ErrorCodes.USER_TOKEN_EXPIRED)) {
|
|
errorMessage = 'Your session has expired. Please refresh the page and log in again.';
|
|
} else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_PERMISSION_DENIED)) {
|
|
errorMessage = 'You do not have permission to preview retry operations.';
|
|
} else if (ErrorHelper.isErrorCode(err, ErrorCodes.DOCUMENT_NOT_FOUND)) {
|
|
errorMessage = 'No documents found matching the specified criteria.';
|
|
} else if (errorInfo.category === 'server') {
|
|
errorMessage = 'Server error. Please try again later.';
|
|
} else if (errorInfo.category === 'network') {
|
|
errorMessage = 'Network error. Please check your connection and try again.';
|
|
} else {
|
|
errorMessage = errorInfo.message || 'Failed to preview retry operation';
|
|
}
|
|
|
|
setError(errorMessage);
|
|
setPreviewResult(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleExecute = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const request = buildRequest(false);
|
|
const response = await documentService.bulkRetryOcr(request);
|
|
onSuccess(response.data);
|
|
onClose();
|
|
} catch (err: any) {
|
|
const errorInfo = ErrorHelper.formatErrorForDisplay(err, true);
|
|
let errorMessage = 'Failed to execute retry operation';
|
|
|
|
// Handle specific bulk retry execution errors
|
|
if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) ||
|
|
ErrorHelper.isErrorCode(err, ErrorCodes.USER_TOKEN_EXPIRED)) {
|
|
errorMessage = 'Your session has expired. Please refresh the page and log in again.';
|
|
} else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_PERMISSION_DENIED)) {
|
|
errorMessage = 'You do not have permission to execute retry operations.';
|
|
} else if (ErrorHelper.isErrorCode(err, ErrorCodes.DOCUMENT_NOT_FOUND)) {
|
|
errorMessage = 'No documents found matching the specified criteria.';
|
|
} else if (ErrorHelper.isErrorCode(err, ErrorCodes.DOCUMENT_OCR_FAILED)) {
|
|
errorMessage = 'Some documents cannot be retried due to processing issues.';
|
|
} else if (errorInfo.category === 'server') {
|
|
errorMessage = 'Server error. Please try again later or contact support.';
|
|
} else if (errorInfo.category === 'network') {
|
|
errorMessage = 'Network error. Please check your connection and try again.';
|
|
} else {
|
|
errorMessage = errorInfo.message || 'Failed to execute retry operation';
|
|
}
|
|
|
|
setError(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatFileSize = (bytes: number) => {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
};
|
|
|
|
const formatDuration = (minutes: number) => {
|
|
if (minutes < 1) return `${Math.round(minutes * 60)} seconds`;
|
|
if (minutes < 60) return `${Math.round(minutes)} minutes`;
|
|
return `${Math.round(minutes / 60)} hours`;
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
|
<DialogTitle>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
<RefreshIcon />
|
|
Bulk OCR Retry
|
|
</Box>
|
|
</DialogTitle>
|
|
|
|
<DialogContent>
|
|
<Stack spacing={3}>
|
|
{error && (
|
|
<Alert severity="error">{error}</Alert>
|
|
)}
|
|
|
|
{/* Selection Mode */}
|
|
<FormControl component="fieldset">
|
|
<FormLabel component="legend">Retry Mode</FormLabel>
|
|
<RadioGroup value={mode} onChange={handleModeChange}>
|
|
<FormControlLabel
|
|
value="all"
|
|
control={<Radio />}
|
|
label="Retry all failed OCR documents"
|
|
/>
|
|
<FormControlLabel
|
|
value="specific"
|
|
control={<Radio />}
|
|
label={`Retry selected documents (${selectedDocumentIds.length} selected)`}
|
|
disabled={selectedDocumentIds.length === 0}
|
|
/>
|
|
<FormControlLabel
|
|
value="filter"
|
|
control={<Radio />}
|
|
label="Retry documents matching criteria"
|
|
/>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{/* Filter Options */}
|
|
{mode === 'filter' && (
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="h6">Filter Criteria</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<Stack spacing={3}>
|
|
{/* MIME Types */}
|
|
<Box>
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
File Types
|
|
</Typography>
|
|
<Box display="flex" flexWrap="wrap" gap={1}>
|
|
{COMMON_MIME_TYPES.map(({ value, label }) => (
|
|
<Chip
|
|
key={value}
|
|
label={label}
|
|
variant={filter.mime_types?.includes(value) ? 'filled' : 'outlined'}
|
|
onClick={() => handleMimeTypeToggle(value)}
|
|
clickable
|
|
/>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Failure Reasons */}
|
|
<Box>
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Failure Reasons
|
|
</Typography>
|
|
<Box display="flex" flexWrap="wrap" gap={1}>
|
|
{COMMON_FAILURE_REASONS.map(({ value, label }) => (
|
|
<Chip
|
|
key={value}
|
|
label={label}
|
|
variant={filter.failure_reasons?.includes(value) ? 'filled' : 'outlined'}
|
|
onClick={() => handleFailureReasonToggle(value)}
|
|
clickable
|
|
color="secondary"
|
|
/>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* File Size */}
|
|
<Box>
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Maximum File Size
|
|
</Typography>
|
|
<Box display="flex" flexWrap="wrap" gap={1} mb={2}>
|
|
{FILE_SIZE_PRESETS.map(({ label, value }) => (
|
|
<Chip
|
|
key={value}
|
|
label={label}
|
|
variant={filter.max_file_size === value ? 'filled' : 'outlined'}
|
|
onClick={() => handleFilterChange('max_file_size',
|
|
filter.max_file_size === value ? undefined : value)}
|
|
clickable
|
|
color="primary"
|
|
/>
|
|
))}
|
|
</Box>
|
|
{filter.max_file_size && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Max file size: {formatFileSize(filter.max_file_size)}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Limit */}
|
|
<TextField
|
|
label="Maximum Documents to Retry"
|
|
type="number"
|
|
value={filter.limit || ''}
|
|
onChange={(e) => handleFilterChange('limit',
|
|
e.target.value ? parseInt(e.target.value) : undefined)}
|
|
InputProps={{
|
|
inputProps: { min: 1, max: 1000 }
|
|
}}
|
|
helperText="Leave empty for no limit"
|
|
/>
|
|
</Stack>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
)}
|
|
|
|
{/* Priority Override */}
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="h6">Advanced Options</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<Stack spacing={2}>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={usePriorityOverride}
|
|
onChange={(e) => setUsePriorityOverride(e.target.checked)}
|
|
/>
|
|
}
|
|
label="Override processing priority"
|
|
/>
|
|
{usePriorityOverride && (
|
|
<Box>
|
|
<Typography gutterBottom>
|
|
Priority: {priorityOverride} (Higher = More Urgent)
|
|
</Typography>
|
|
<Slider
|
|
value={priorityOverride}
|
|
onChange={(_, value) => setPriorityOverride(value as number)}
|
|
min={1}
|
|
max={20}
|
|
marks={[
|
|
{ value: 1, label: 'Low' },
|
|
{ value: 10, label: 'Normal' },
|
|
{ value: 20, label: 'High' },
|
|
]}
|
|
valueLabelDisplay="auto"
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
|
|
{/* Preview Results */}
|
|
{previewResult && (
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Preview Results
|
|
</Typography>
|
|
<Stack spacing={2}>
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Typography>Documents matched:</Typography>
|
|
<Typography fontWeight="bold">{previewResult.matched_count}</Typography>
|
|
</Box>
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Typography>Estimated processing time:</Typography>
|
|
<Typography fontWeight="bold">
|
|
<ScheduleIcon sx={{ mr: 0.5, verticalAlign: 'middle', fontSize: 'small' }} />
|
|
{formatDuration(previewResult.estimated_total_time_minutes)}
|
|
</Typography>
|
|
</Box>
|
|
{previewResult.documents && previewResult.documents.length > 0 && (
|
|
<Box>
|
|
<Typography variant="subtitle2" gutterBottom>
|
|
Sample Documents:
|
|
</Typography>
|
|
<Box maxHeight={200} overflow="auto">
|
|
{(previewResult.documents || []).slice(0, 10).map((doc) => (
|
|
<Box key={doc.id} py={0.5}>
|
|
<Typography variant="body2">
|
|
{doc.filename} ({formatFileSize(doc.file_size)})
|
|
{doc.ocr_failure_reason && (
|
|
<Chip
|
|
size="small"
|
|
label={doc.ocr_failure_reason}
|
|
sx={{ ml: 1, fontSize: '0.7rem' }}
|
|
/>
|
|
)}
|
|
</Typography>
|
|
</Box>
|
|
))}
|
|
{previewResult.documents && previewResult.documents.length > 10 && (
|
|
<Typography variant="body2" color="text.secondary" mt={1}>
|
|
... and {previewResult.documents.length - 10} more documents
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{loading && <LinearProgress />}
|
|
</Stack>
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button onClick={onClose} disabled={loading}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handlePreview}
|
|
disabled={loading}
|
|
variant="outlined"
|
|
>
|
|
Preview
|
|
</Button>
|
|
<Button
|
|
onClick={handleExecute}
|
|
disabled={loading || !previewResult || previewResult.matched_count === 0}
|
|
variant="contained"
|
|
color="primary"
|
|
>
|
|
{loading ? 'Processing...' : `Retry ${previewResult?.matched_count || 0} Documents`}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}; |