feat(lang): create frontend components to support multiple ocr languages
This commit is contained in:
parent
7317fd5ebb
commit
bd3f35cf38
|
|
@ -3,6 +3,7 @@ import { useDropzone } from 'react-dropzone'
|
||||||
import { DocumentArrowUpIcon } from '@heroicons/react/24/outline'
|
import { DocumentArrowUpIcon } from '@heroicons/react/24/outline'
|
||||||
import { Document, documentService } from '../services/api'
|
import { Document, documentService } from '../services/api'
|
||||||
import { useNotifications } from '../contexts/NotificationContext'
|
import { useNotifications } from '../contexts/NotificationContext'
|
||||||
|
import LanguageSelector from './LanguageSelector'
|
||||||
|
|
||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
onUploadSuccess: (document: Document) => void
|
onUploadSuccess: (document: Document) => void
|
||||||
|
|
@ -11,6 +12,8 @@ interface FileUploadProps {
|
||||||
function FileUpload({ onUploadSuccess }: FileUploadProps) {
|
function FileUpload({ onUploadSuccess }: FileUploadProps) {
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [selectedLanguages, setSelectedLanguages] = useState<string[]>(['eng'])
|
||||||
|
const [primaryLanguage, setPrimaryLanguage] = useState<string>('eng')
|
||||||
const { addBatchNotification } = useNotifications()
|
const { addBatchNotification } = useNotifications()
|
||||||
|
|
||||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||||
|
|
@ -21,7 +24,7 @@ function FileUpload({ onUploadSuccess }: FileUploadProps) {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await documentService.upload(file)
|
const response = await documentService.upload(file, selectedLanguages.length > 0 ? selectedLanguages : undefined)
|
||||||
onUploadSuccess(response.data)
|
onUploadSuccess(response.data)
|
||||||
|
|
||||||
// Trigger success notification
|
// Trigger success notification
|
||||||
|
|
@ -34,7 +37,16 @@ function FileUpload({ onUploadSuccess }: FileUploadProps) {
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}, [onUploadSuccess, addBatchNotification])
|
}, [onUploadSuccess, addBatchNotification, selectedLanguages])
|
||||||
|
|
||||||
|
const handleLanguagesChange = (languages: string[], primary?: string) => {
|
||||||
|
setSelectedLanguages(languages)
|
||||||
|
if (primary) {
|
||||||
|
setPrimaryLanguage(primary)
|
||||||
|
} else if (languages.length > 0) {
|
||||||
|
setPrimaryLanguage(languages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
|
|
@ -49,7 +61,16 @@ function FileUpload({ onUploadSuccess }: FileUploadProps) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full space-y-4">
|
||||||
|
{/* Language Selector */}
|
||||||
|
<LanguageSelector
|
||||||
|
selectedLanguages={selectedLanguages}
|
||||||
|
primaryLanguage={primaryLanguage}
|
||||||
|
onLanguagesChange={handleLanguagesChange}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* File Upload Area */}
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { LanguageInfo } from '../services/api'
|
||||||
|
|
||||||
|
interface LanguageSelectorProps {
|
||||||
|
selectedLanguages: string[]
|
||||||
|
primaryLanguage?: string
|
||||||
|
onLanguagesChange: (languages: string[], primary?: string) => void
|
||||||
|
maxLanguages?: number
|
||||||
|
disabled?: boolean
|
||||||
|
showPrimarySelector?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common languages with display names
|
||||||
|
const COMMON_LANGUAGES: LanguageInfo[] = [
|
||||||
|
{ code: 'eng', name: 'English', installed: true },
|
||||||
|
{ code: 'spa', name: 'Spanish', installed: true },
|
||||||
|
{ code: 'fra', name: 'French', installed: true },
|
||||||
|
{ code: 'deu', name: 'German', installed: true },
|
||||||
|
{ code: 'ita', name: 'Italian', installed: true },
|
||||||
|
{ code: 'por', name: 'Portuguese', installed: true },
|
||||||
|
{ code: 'rus', name: 'Russian', installed: true },
|
||||||
|
{ code: 'chi_sim', name: 'Chinese (Simplified)', installed: true },
|
||||||
|
{ code: 'chi_tra', name: 'Chinese (Traditional)', installed: true },
|
||||||
|
{ code: 'jpn', name: 'Japanese', installed: true },
|
||||||
|
{ code: 'kor', name: 'Korean', installed: true },
|
||||||
|
{ code: 'ara', name: 'Arabic', installed: true },
|
||||||
|
{ code: 'hin', name: 'Hindi', installed: true },
|
||||||
|
{ code: 'nld', name: 'Dutch', installed: true },
|
||||||
|
{ code: 'swe', name: 'Swedish', installed: true },
|
||||||
|
{ code: 'nor', name: 'Norwegian', installed: true },
|
||||||
|
{ code: 'dan', name: 'Danish', installed: true },
|
||||||
|
{ code: 'fin', name: 'Finnish', installed: true },
|
||||||
|
{ code: 'pol', name: 'Polish', installed: true },
|
||||||
|
{ code: 'ces', name: 'Czech', installed: true },
|
||||||
|
{ code: 'hun', name: 'Hungarian', installed: true },
|
||||||
|
{ code: 'tur', name: 'Turkish', installed: true },
|
||||||
|
{ code: 'tha', name: 'Thai', installed: true },
|
||||||
|
{ code: 'vie', name: 'Vietnamese', installed: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
function LanguageSelector({
|
||||||
|
selectedLanguages,
|
||||||
|
primaryLanguage,
|
||||||
|
onLanguagesChange,
|
||||||
|
maxLanguages = 4,
|
||||||
|
disabled = false,
|
||||||
|
showPrimarySelector = true,
|
||||||
|
className = '',
|
||||||
|
}: LanguageSelectorProps) {
|
||||||
|
const [availableLanguages, setAvailableLanguages] = useState<LanguageInfo[]>(COMMON_LANGUAGES)
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
// Auto-set primary language to first selected if not specified
|
||||||
|
const effectivePrimary = primaryLanguage || selectedLanguages[0] || ''
|
||||||
|
|
||||||
|
const handleLanguageToggle = (languageCode: string) => {
|
||||||
|
if (disabled) return
|
||||||
|
|
||||||
|
let newLanguages: string[]
|
||||||
|
let newPrimary = effectivePrimary
|
||||||
|
|
||||||
|
if (selectedLanguages.includes(languageCode)) {
|
||||||
|
// Remove language
|
||||||
|
newLanguages = selectedLanguages.filter(lang => lang !== languageCode)
|
||||||
|
// If removing the primary language, set new primary to first remaining language
|
||||||
|
if (languageCode === effectivePrimary && newLanguages.length > 0) {
|
||||||
|
newPrimary = newLanguages[0]
|
||||||
|
} else if (newLanguages.length === 0) {
|
||||||
|
newPrimary = ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add language (check max limit)
|
||||||
|
if (selectedLanguages.length >= maxLanguages) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newLanguages = [...selectedLanguages, languageCode]
|
||||||
|
// If this is the first language, make it primary
|
||||||
|
if (newLanguages.length === 1) {
|
||||||
|
newPrimary = languageCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLanguagesChange(newLanguages, newPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrimaryChange = (languageCode: string) => {
|
||||||
|
if (disabled || !selectedLanguages.includes(languageCode)) return
|
||||||
|
onLanguagesChange(selectedLanguages, languageCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLanguage = (languageCode: string) => {
|
||||||
|
handleLanguageToggle(languageCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguageName = (code: string) => {
|
||||||
|
const language = availableLanguages.find(lang => lang.code === code)
|
||||||
|
return language?.name || code
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
{/* Selected Languages Display */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
OCR Languages {selectedLanguages.length > 0 && `(${selectedLanguages.length}/${maxLanguages})`}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{selectedLanguages.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedLanguages.map((langCode) => (
|
||||||
|
<span
|
||||||
|
key={langCode}
|
||||||
|
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
||||||
|
langCode === effectivePrimary
|
||||||
|
? 'bg-blue-100 text-blue-800 border-2 border-blue-300'
|
||||||
|
: 'bg-gray-100 text-gray-800 border border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getLanguageName(langCode)}
|
||||||
|
{langCode === effectivePrimary && (
|
||||||
|
<span className="ml-1 text-xs font-bold text-blue-600">(Primary)</span>
|
||||||
|
)}
|
||||||
|
{!disabled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeLanguage(langCode)}
|
||||||
|
className="ml-2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500 italic">
|
||||||
|
No languages selected. Documents will use default OCR language.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language Selector Button */}
|
||||||
|
{!disabled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full px-4 py-2 text-left border border-gray-300 rounded-lg bg-white hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{selectedLanguages.length === 0
|
||||||
|
? 'Select OCR languages...'
|
||||||
|
: `Add more languages (${maxLanguages - selectedLanguages.length} remaining)`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dropdown Panel */}
|
||||||
|
{isOpen && !disabled && (
|
||||||
|
<div className="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-64 overflow-y-auto">
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="text-xs text-gray-500 mb-2 uppercase tracking-wide font-semibold">
|
||||||
|
Available Languages
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{availableLanguages
|
||||||
|
.filter(lang => lang.installed)
|
||||||
|
.map((language) => {
|
||||||
|
const isSelected = selectedLanguages.includes(language.code)
|
||||||
|
const isPrimary = language.code === effectivePrimary
|
||||||
|
const canSelect = !isSelected && selectedLanguages.length < maxLanguages
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={language.code}
|
||||||
|
className={`flex items-center justify-between p-2 rounded ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-blue-50 border border-blue-200'
|
||||||
|
: canSelect
|
||||||
|
? 'hover:bg-gray-50 cursor-pointer'
|
||||||
|
: 'opacity-50 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleLanguageToggle(language.code)}
|
||||||
|
disabled={!canSelect && !isSelected}
|
||||||
|
className={`flex items-center space-x-2 ${
|
||||||
|
canSelect || isSelected ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-5 h-5 border-2 rounded flex items-center justify-center ${
|
||||||
|
isSelected
|
||||||
|
? 'border-blue-500 bg-blue-500'
|
||||||
|
: 'border-gray-300'
|
||||||
|
}`}>
|
||||||
|
{isSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm ${isSelected ? 'font-medium text-blue-900' : 'text-gray-700'}`}>
|
||||||
|
{language.name}
|
||||||
|
</span>
|
||||||
|
{isPrimary && (
|
||||||
|
<span className="text-xs bg-blue-600 text-white px-2 py-0.5 rounded font-bold">
|
||||||
|
PRIMARY
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Primary selector */}
|
||||||
|
{isSelected && showPrimarySelector && selectedLanguages.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePrimaryChange(language.code)}
|
||||||
|
className={`text-xs px-2 py-1 rounded font-medium ${
|
||||||
|
isPrimary
|
||||||
|
? 'bg-blue-600 text-white cursor-default'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPrimary ? 'Primary' : 'Set Primary'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLanguages.length >= maxLanguages && (
|
||||||
|
<div className="mt-3 p-2 bg-amber-50 border border-amber-200 rounded text-xs text-amber-800">
|
||||||
|
Maximum {maxLanguages} languages allowed for optimal performance.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 p-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="w-full text-center text-sm text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
{selectedLanguages.length > 1 && (
|
||||||
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
|
<p>
|
||||||
|
<strong>Primary language</strong> is processed first for better accuracy.
|
||||||
|
Multiple languages help with mixed-language documents.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageSelector
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Refresh as RefreshIcon, Language as LanguageIcon } from '@mui/icons-material';
|
import { Refresh as RefreshIcon, Language as LanguageIcon } from '@mui/icons-material';
|
||||||
import OcrLanguageSelector from '../OcrLanguageSelector';
|
import OcrLanguageSelector from '../OcrLanguageSelector';
|
||||||
|
import LanguageSelector from '../LanguageSelector';
|
||||||
import { ocrService } from '../../services/api';
|
import { ocrService } from '../../services/api';
|
||||||
|
|
||||||
interface OcrRetryDialogProps {
|
interface OcrRetryDialogProps {
|
||||||
|
|
@ -38,8 +39,16 @@ const OcrRetryDialog: React.FC<OcrRetryDialogProps> = ({
|
||||||
onRetryError,
|
onRetryError,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<string>('');
|
const [selectedLanguage, setSelectedLanguage] = useState<string>('');
|
||||||
|
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([]);
|
||||||
|
const [primaryLanguage, setPrimaryLanguage] = useState<string>('');
|
||||||
|
const [useMultiLanguage, setUseMultiLanguage] = useState<boolean>(false);
|
||||||
const [retrying, setRetrying] = useState<boolean>(false);
|
const [retrying, setRetrying] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleLanguagesChange = (languages: string[], primary?: string) => {
|
||||||
|
setSelectedLanguages(languages);
|
||||||
|
setPrimaryLanguage(primary || languages[0] || '');
|
||||||
|
};
|
||||||
|
|
||||||
// Simple language code to name mapping for display
|
// Simple language code to name mapping for display
|
||||||
const getLanguageDisplayName = (langCode: string): string => {
|
const getLanguageDisplayName = (langCode: string): string => {
|
||||||
const languageNames: Record<string, string> = {
|
const languageNames: Record<string, string> = {
|
||||||
|
|
@ -95,15 +104,32 @@ const OcrRetryDialog: React.FC<OcrRetryDialogProps> = ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setRetrying(true);
|
setRetrying(true);
|
||||||
|
|
||||||
|
// Use multi-language if enabled and languages are selected, otherwise use single language
|
||||||
|
const languagesToUse = useMultiLanguage && selectedLanguages.length > 0
|
||||||
|
? selectedLanguages
|
||||||
|
: undefined;
|
||||||
|
const singleLanguageToUse = !useMultiLanguage && selectedLanguage
|
||||||
|
? selectedLanguage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const response = await ocrService.retryWithLanguage(
|
const response = await ocrService.retryWithLanguage(
|
||||||
document.id,
|
document.id,
|
||||||
selectedLanguage || undefined
|
singleLanguageToUse,
|
||||||
|
languagesToUse
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const waitTime = response.data.estimated_wait_minutes || 'Unknown';
|
const waitTime = response.data.estimated_wait_minutes || 'Unknown';
|
||||||
const languageInfo = selectedLanguage ?
|
let languageInfo = '';
|
||||||
` with language "${getLanguageDisplayName(selectedLanguage)}"` : '';
|
|
||||||
|
if (languagesToUse && languagesToUse.length > 0) {
|
||||||
|
const langNames = languagesToUse.map(lang => getLanguageDisplayName(lang));
|
||||||
|
languageInfo = ` with languages: ${langNames.join(', ')} (Primary: ${getLanguageDisplayName(primaryLanguage)})`;
|
||||||
|
} else if (singleLanguageToUse) {
|
||||||
|
languageInfo = ` with language "${getLanguageDisplayName(singleLanguageToUse)}"`;
|
||||||
|
}
|
||||||
|
|
||||||
onRetrySuccess(
|
onRetrySuccess(
|
||||||
`OCR retry queued for "${document.filename}"${languageInfo}. Estimated wait time: ${waitTime} minutes.`
|
`OCR retry queued for "${document.filename}"${languageInfo}. Estimated wait time: ${waitTime} minutes.`
|
||||||
);
|
);
|
||||||
|
|
@ -124,6 +150,9 @@ const OcrRetryDialog: React.FC<OcrRetryDialogProps> = ({
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!retrying) {
|
if (!retrying) {
|
||||||
setSelectedLanguage('');
|
setSelectedLanguage('');
|
||||||
|
setSelectedLanguages([]);
|
||||||
|
setPrimaryLanguage('');
|
||||||
|
setUseMultiLanguage(false);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -170,8 +199,30 @@ const OcrRetryDialog: React.FC<OcrRetryDialogProps> = ({
|
||||||
OCR Language Selection
|
OCR Language Selection
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
Choose a different language if the previous OCR attempt used the wrong language for this document.
|
Choose a different language or language combination if the previous OCR attempt failed due to incorrect language settings.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Toggle between single and multi-language */}
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant={!useMultiLanguage ? "contained" : "outlined"}
|
||||||
|
size="small"
|
||||||
|
onClick={() => setUseMultiLanguage(false)}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Single Language
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={useMultiLanguage ? "contained" : "outlined"}
|
||||||
|
size="small"
|
||||||
|
onClick={() => setUseMultiLanguage(true)}
|
||||||
|
>
|
||||||
|
Multiple Languages
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Single Language Selector */}
|
||||||
|
{!useMultiLanguage && (
|
||||||
<OcrLanguageSelector
|
<OcrLanguageSelector
|
||||||
value={selectedLanguage}
|
value={selectedLanguage}
|
||||||
onChange={setSelectedLanguage}
|
onChange={setSelectedLanguage}
|
||||||
|
|
@ -180,6 +231,19 @@ const OcrRetryDialog: React.FC<OcrRetryDialogProps> = ({
|
||||||
helperText="Leave empty to use your default language setting"
|
helperText="Leave empty to use your default language setting"
|
||||||
showCurrentIndicator={true}
|
showCurrentIndicator={true}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Multi-Language Selector */}
|
||||||
|
{useMultiLanguage && (
|
||||||
|
<LanguageSelector
|
||||||
|
selectedLanguages={selectedLanguages}
|
||||||
|
primaryLanguage={primaryLanguage}
|
||||||
|
onLanguagesChange={handleLanguagesChange}
|
||||||
|
disabled={retrying}
|
||||||
|
showPrimarySelector={true}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Alert severity="info" sx={{ mt: 2 }}>
|
<Alert severity="info" sx={{ mt: 2 }}>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import api from '../../services/api';
|
||||||
import { useNotifications } from '../../contexts/NotificationContext';
|
import { useNotifications } from '../../contexts/NotificationContext';
|
||||||
import LabelSelector from '../Labels/LabelSelector';
|
import LabelSelector from '../Labels/LabelSelector';
|
||||||
import { type LabelData } from '../Labels/Label';
|
import { type LabelData } from '../Labels/Label';
|
||||||
|
import LanguageSelector from '../LanguageSelector';
|
||||||
|
|
||||||
interface UploadedDocument {
|
interface UploadedDocument {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -65,6 +66,8 @@ const UploadZone: React.FC<UploadZoneProps> = ({ onUploadComplete }) => {
|
||||||
const [selectedLabels, setSelectedLabels] = useState<LabelData[]>([]);
|
const [selectedLabels, setSelectedLabels] = useState<LabelData[]>([]);
|
||||||
const [availableLabels, setAvailableLabels] = useState<LabelData[]>([]);
|
const [availableLabels, setAvailableLabels] = useState<LabelData[]>([]);
|
||||||
const [labelsLoading, setLabelsLoading] = useState<boolean>(false);
|
const [labelsLoading, setLabelsLoading] = useState<boolean>(false);
|
||||||
|
const [selectedLanguages, setSelectedLanguages] = useState<string[]>(['eng']);
|
||||||
|
const [primaryLanguage, setPrimaryLanguage] = useState<string>('eng');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLabels();
|
fetchLabels();
|
||||||
|
|
@ -99,6 +102,15 @@ const UploadZone: React.FC<UploadZoneProps> = ({ onUploadComplete }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLanguagesChange = (languages: string[], primary?: string) => {
|
||||||
|
setSelectedLanguages(languages);
|
||||||
|
if (primary) {
|
||||||
|
setPrimaryLanguage(primary);
|
||||||
|
} else if (languages.length > 0) {
|
||||||
|
setPrimaryLanguage(languages[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
|
@ -151,6 +163,13 @@ const UploadZone: React.FC<UploadZoneProps> = ({ onUploadComplete }) => {
|
||||||
formData.append('label_ids', JSON.stringify(labelIds));
|
formData.append('label_ids', JSON.stringify(labelIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add selected languages to the form data
|
||||||
|
if (selectedLanguages.length > 0) {
|
||||||
|
selectedLanguages.forEach((lang, index) => {
|
||||||
|
formData.append(`ocr_languages[${index}]`, lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setFiles(prev => prev.map(f =>
|
setFiles(prev => prev.map(f =>
|
||||||
f.id === fileItem.id
|
f.id === fileItem.id
|
||||||
|
|
@ -338,6 +357,26 @@ const UploadZone: React.FC<UploadZoneProps> = ({ onUploadComplete }) => {
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Language Selection */}
|
||||||
|
<Card elevation={0} sx={{ mb: 3 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
|
||||||
|
🌐 OCR Language Settings
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Select languages for optimal OCR text recognition
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ '& > div': { width: '100%' } }}>
|
||||||
|
<LanguageSelector
|
||||||
|
selectedLanguages={selectedLanguages}
|
||||||
|
primaryLanguage={primaryLanguage}
|
||||||
|
onLanguagesChange={handleLanguagesChange}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Label Selection */}
|
{/* Label Selection */}
|
||||||
<Card elevation={0} sx={{ mb: 3 }}>
|
<Card elevation={0} sx={{ mb: 3 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon,
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import api, { queueService } from '../services/api';
|
import api, { queueService } from '../services/api';
|
||||||
import OcrLanguageSelector from '../components/OcrLanguageSelector';
|
import OcrLanguageSelector from '../components/OcrLanguageSelector';
|
||||||
|
import LanguageSelector from '../components/LanguageSelector';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -53,6 +54,9 @@ interface User {
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
ocrLanguage: string;
|
ocrLanguage: string;
|
||||||
|
preferredLanguages: string[];
|
||||||
|
primaryLanguage: string;
|
||||||
|
autoDetectLanguageCombination: boolean;
|
||||||
concurrentOcrJobs: number;
|
concurrentOcrJobs: number;
|
||||||
ocrTimeoutSeconds: number;
|
ocrTimeoutSeconds: number;
|
||||||
maxFileSizeMb: number;
|
maxFileSizeMb: number;
|
||||||
|
|
@ -188,6 +192,9 @@ const SettingsPage: React.FC = () => {
|
||||||
const [tabValue, setTabValue] = useState<number>(0);
|
const [tabValue, setTabValue] = useState<number>(0);
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
ocrLanguage: 'eng',
|
ocrLanguage: 'eng',
|
||||||
|
preferredLanguages: ['eng'],
|
||||||
|
primaryLanguage: 'eng',
|
||||||
|
autoDetectLanguageCombination: false,
|
||||||
concurrentOcrJobs: 4,
|
concurrentOcrJobs: 4,
|
||||||
ocrTimeoutSeconds: 300,
|
ocrTimeoutSeconds: 300,
|
||||||
maxFileSizeMb: 50,
|
maxFileSizeMb: 50,
|
||||||
|
|
@ -268,6 +275,9 @@ const SettingsPage: React.FC = () => {
|
||||||
const response = await api.get('/settings');
|
const response = await api.get('/settings');
|
||||||
setSettings({
|
setSettings({
|
||||||
ocrLanguage: response.data.ocr_language || 'eng',
|
ocrLanguage: response.data.ocr_language || 'eng',
|
||||||
|
preferredLanguages: response.data.preferred_languages || ['eng'],
|
||||||
|
primaryLanguage: response.data.primary_language || 'eng',
|
||||||
|
autoDetectLanguageCombination: response.data.auto_detect_language_combination || false,
|
||||||
concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4,
|
concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4,
|
||||||
ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300,
|
ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300,
|
||||||
maxFileSizeMb: response.data.max_file_size_mb || 50,
|
maxFileSizeMb: response.data.max_file_size_mb || 50,
|
||||||
|
|
@ -430,6 +440,20 @@ const SettingsPage: React.FC = () => {
|
||||||
handleSettingsChange('searchResultsPerPage', event.target.value);
|
handleSettingsChange('searchResultsPerPage', event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLanguagesChange = (languages: string[], primary?: string) => {
|
||||||
|
// Update multiple fields at once
|
||||||
|
const updates = {
|
||||||
|
preferredLanguages: languages,
|
||||||
|
primaryLanguage: primary || languages[0] || 'eng',
|
||||||
|
ocrLanguage: primary || languages[0] || 'eng', // Backward compatibility
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update all language-related settings
|
||||||
|
Object.entries(updates).forEach(([key, value]) => {
|
||||||
|
handleSettingsChange(key as keyof Settings, value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const fetchOcrStatus = async (): Promise<void> => {
|
const fetchOcrStatus = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const response = await queueService.getOcrStatus();
|
const response = await queueService.getOcrStatus();
|
||||||
|
|
@ -521,14 +545,34 @@ const SettingsPage: React.FC = () => {
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12}>
|
||||||
<OcrLanguageSelector
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
value={settings.ocrLanguage}
|
Configure languages for OCR text extraction. Multiple languages help with mixed-language documents.
|
||||||
onChange={(language) => handleSettingsChange('ocrLanguage', language)}
|
</Typography>
|
||||||
|
<Box sx={{ '& > div': { width: '100%' } }}>
|
||||||
|
<LanguageSelector
|
||||||
|
selectedLanguages={settings.preferredLanguages}
|
||||||
|
primaryLanguage={settings.primaryLanguage}
|
||||||
|
onLanguagesChange={handleLanguagesChange}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
showCurrentIndicator={false}
|
showPrimarySelector={true}
|
||||||
helperText="Default language for OCR text extraction from your documents"
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={settings.autoDetectLanguageCombination}
|
||||||
|
onChange={(e) => handleSettingsChange('autoDetectLanguageCombination', e.target.checked)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Auto-detect language combinations"
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
|
||||||
|
Automatically suggest optimal language combinations based on document content analysis
|
||||||
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
||||||
|
|
@ -230,9 +230,17 @@ export interface OcrResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const documentService = {
|
export const documentService = {
|
||||||
upload: (file: File) => {
|
upload: (file: File, languages?: string[]) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
|
// Add multiple languages if provided
|
||||||
|
if (languages && languages.length > 0) {
|
||||||
|
languages.forEach((lang, index) => {
|
||||||
|
formData.append(`ocr_languages[${index}]`, lang)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return api.post('/documents', formData, {
|
return api.post('/documents', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
|
|
@ -417,6 +425,7 @@ export interface AvailableLanguagesResponse {
|
||||||
|
|
||||||
export interface RetryOcrRequest {
|
export interface RetryOcrRequest {
|
||||||
language?: string
|
language?: string
|
||||||
|
languages?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queueService = {
|
export const queueService = {
|
||||||
|
|
@ -450,9 +459,11 @@ export const ocrService = {
|
||||||
return api.get('/ocr/health')
|
return api.get('/ocr/health')
|
||||||
},
|
},
|
||||||
|
|
||||||
retryWithLanguage: (documentId: string, language?: string) => {
|
retryWithLanguage: (documentId: string, language?: string, languages?: string[]) => {
|
||||||
const data: RetryOcrRequest = {}
|
const data: RetryOcrRequest = {}
|
||||||
if (language) {
|
if (languages && languages.length > 0) {
|
||||||
|
data.languages = languages
|
||||||
|
} else if (language) {
|
||||||
data.language = language
|
data.language = language
|
||||||
}
|
}
|
||||||
return api.post(`/documents/${documentId}/retry-ocr`, data)
|
return api.post(`/documents/${documentId}/retry-ocr`, data)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue