mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 10:35:22 +00:00
remove OCR language picker from settings code to make it a little easier to read. Added the link to docs on using different languages with OCR
This commit is contained in:
parent
710b4837a0
commit
a0e57655db
94
frontend/src/components/tools/ocr/LanguagePicker.module.css
Normal file
94
frontend/src/components/tools/ocr/LanguagePicker.module.css
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/* Language Picker Component */
|
||||||
|
.languagePicker {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center; /* Center align items vertically */
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
background-color: white; /* Default white background for light mode */
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode background */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePicker {
|
||||||
|
background-color: #2A2F36; /* Specific dark background color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePicker:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode hover */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePicker:hover {
|
||||||
|
background-color: #3A3F46; /* Slightly lighter hover color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePicker:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerIcon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* Center the icon vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerDropdown {
|
||||||
|
background-color: white; /* Default white background for light mode */
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode dropdown background */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePickerDropdown {
|
||||||
|
background-color: #2A2F36;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerOption {
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerOption:hover {
|
||||||
|
background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode option hover */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePickerOption:hover {
|
||||||
|
background-color: #3A3F46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerOption.selected {
|
||||||
|
background-color: var(--mantine-color-blue-1); /* Light blue for light mode */
|
||||||
|
color: var(--mantine-color-blue-9); /* Dark blue text for light mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode selected option */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePickerOption.selected {
|
||||||
|
background-color: var(--mantine-color-blue-8); /* Darker blue for better contrast */
|
||||||
|
color: white; /* White text for dark mode selected items */
|
||||||
|
}
|
||||||
|
|
||||||
|
.languagePickerOption.selected:hover {
|
||||||
|
background-color: var(--mantine-color-blue-2); /* Slightly darker on hover for light mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode selected option hover */
|
||||||
|
[data-mantine-color-scheme="dark"] .languagePickerOption.selected:hover {
|
||||||
|
background-color: var(--mantine-color-blue-7); /* Slightly lighter on hover */
|
||||||
|
}
|
221
frontend/src/components/tools/ocr/LanguagePicker.tsx
Normal file
221
frontend/src/components/tools/ocr/LanguagePicker.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Stack, Text, Loader, Popover, useMantineTheme, useMantineColorScheme, Box } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
|
||||||
|
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||||
|
|
||||||
|
export interface LanguageOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguagePickerProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
languagesEndpoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguagePicker: React.FC<LanguagePickerProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Select language',
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
languagesEndpoint = '/api/v1/ui-data/ocr-pdf'
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const [availableLanguages, setAvailableLanguages] = useState<LanguageOption[]>([]);
|
||||||
|
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch available languages from backend
|
||||||
|
const fetchLanguages = async () => {
|
||||||
|
console.log('[LanguagePicker] Starting language fetch...');
|
||||||
|
console.log('[LanguagePicker] Fetching from URL:', languagesEndpoint);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(languagesEndpoint);
|
||||||
|
console.log('[LanguagePicker] Response received:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
ok: response.ok,
|
||||||
|
headers: Object.fromEntries(response.headers.entries())
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data: { languages: string[] } = await response.json();
|
||||||
|
const languages = data.languages;
|
||||||
|
console.log('[LanguagePicker] Raw response data:', languages);
|
||||||
|
console.log('[LanguagePicker] Response type:', typeof languages, 'Array?', Array.isArray(languages));
|
||||||
|
|
||||||
|
const languageOptions = languages.map(lang => {
|
||||||
|
// TODO: Use actual language translations when they become available
|
||||||
|
// For now, use temporary English translations
|
||||||
|
const translatedName = tempOcrLanguages.lang[lang as keyof typeof tempOcrLanguages.lang] || lang;
|
||||||
|
const displayName = translatedName;
|
||||||
|
|
||||||
|
console.log(`[LanguagePicker] Language mapping: ${lang} -> ${displayName} (translated: ${!!translatedName})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: lang,
|
||||||
|
label: displayName
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log('[LanguagePicker] Transformed language options:', languageOptions);
|
||||||
|
|
||||||
|
setAvailableLanguages(languageOptions);
|
||||||
|
console.log('[LanguagePicker] Successfully set', languageOptions.length, 'languages');
|
||||||
|
} else {
|
||||||
|
console.error('[LanguagePicker] Response not OK:', response.status, response.statusText);
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('[LanguagePicker] Error response body:', errorText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LanguagePicker] Fetch failed with error:', error);
|
||||||
|
console.error('[LanguagePicker] Error details:', {
|
||||||
|
name: error instanceof Error ? error.name : 'Unknown',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingLanguages(false);
|
||||||
|
console.log('[LanguagePicker] Language loading completed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLanguages();
|
||||||
|
}, [languagesEndpoint]);
|
||||||
|
|
||||||
|
if (isLoadingLanguages) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<Loader size="xs" />
|
||||||
|
<Text size="sm">Loading available languages...</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedLanguage = availableLanguages.find(lang => lang.value === value);
|
||||||
|
|
||||||
|
// Get appropriate background colors based on color scheme
|
||||||
|
const getBackgroundColor = () => {
|
||||||
|
if (colorScheme === 'dark') {
|
||||||
|
return '#2A2F36'; // Specific dark background color
|
||||||
|
}
|
||||||
|
return 'white'; // White background for light mode
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedItemBackgroundColor = () => {
|
||||||
|
if (colorScheme === 'dark') {
|
||||||
|
return 'var(--mantine-color-blue-8)'; // Darker blue for better contrast
|
||||||
|
}
|
||||||
|
return 'var(--mantine-color-blue-1)'; // Light blue for light mode
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedItemTextColor = () => {
|
||||||
|
if (colorScheme === 'dark') {
|
||||||
|
return 'white'; // White text for dark mode selected items
|
||||||
|
}
|
||||||
|
return 'var(--mantine-color-blue-9)'; // Dark blue text for light mode
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{label && (
|
||||||
|
<Text size="sm" fw={500} mb={4}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Popover width="target" position="bottom" withArrow={false} shadow="md">
|
||||||
|
<Popover.Target>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center', // Center align items vertically
|
||||||
|
height: '32px',
|
||||||
|
border: `1px solid var(--border-default)`,
|
||||||
|
backgroundColor: getBackgroundColor(),
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '13px',
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" style={{ flex: 1 }}>
|
||||||
|
{selectedLanguage?.label || placeholder}
|
||||||
|
</Text>
|
||||||
|
<UnfoldMoreIcon style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}} />
|
||||||
|
</Box>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown style={{
|
||||||
|
backgroundColor: getBackgroundColor(),
|
||||||
|
border: '1px solid var(--border-default)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
padding: '4px'
|
||||||
|
}}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Box style={{
|
||||||
|
maxHeight: '180px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
borderBottom: '1px solid var(--border-default)',
|
||||||
|
paddingBottom: '8px'
|
||||||
|
}}>
|
||||||
|
{availableLanguages.map((lang) => (
|
||||||
|
<Box
|
||||||
|
key={lang.value}
|
||||||
|
style={{
|
||||||
|
padding: '6px 10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 'var(--radius-xs)',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: value === lang.value ? getSelectedItemTextColor() : 'var(--text-primary)',
|
||||||
|
backgroundColor: value === lang.value ? getSelectedItemBackgroundColor() : 'transparent',
|
||||||
|
transition: 'background-color 0.2s ease'
|
||||||
|
}}
|
||||||
|
onClick={() => onChange(lang.value)}
|
||||||
|
>
|
||||||
|
<Text size="sm">{lang.label}</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box style={{
|
||||||
|
padding: '8px',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
<Text size="xs" c="dimmed" mb={4}>
|
||||||
|
Looking for additional languages?
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="blue"
|
||||||
|
style={{ textDecoration: 'underline', cursor: 'pointer' }}
|
||||||
|
onClick={() => window.open('https://docs.stirlingpdf.com/Advanced%20Configuration/OCR', '_blank')}
|
||||||
|
>
|
||||||
|
View setup guide →
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguagePicker;
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Stack, Select, MultiSelect, Text, Loader } from '@mantine/core';
|
import { Stack, Select, MultiSelect, Text, Divider } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
|
import LanguagePicker from './LanguagePicker';
|
||||||
|
|
||||||
export interface OCRParameters {
|
export interface OCRParameters {
|
||||||
languages: string[];
|
languages: string[];
|
||||||
@ -22,8 +22,6 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
|||||||
disabled = false
|
disabled = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [availableLanguages, setAvailableLanguages] = useState<{value: string, label: string}[]>([]);
|
|
||||||
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
|
||||||
|
|
||||||
// Define the additional options available
|
// Define the additional options available
|
||||||
const additionalOptionsData = [
|
const additionalOptionsData = [
|
||||||
@ -31,89 +29,11 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
|||||||
{ value: 'deskew', label: 'Deskew pages' },
|
{ value: 'deskew', label: 'Deskew pages' },
|
||||||
{ value: 'clean', label: 'Clean input file' },
|
{ value: 'clean', label: 'Clean input file' },
|
||||||
{ value: 'cleanFinal', label: 'Clean final output' },
|
{ value: 'cleanFinal', label: 'Clean final output' },
|
||||||
{ value: 'removeImagesAfter', label: 'Remove images after OCR' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch available languages from backend
|
|
||||||
const fetchLanguages = async () => {
|
|
||||||
console.log('[OCR Languages] Starting language fetch...');
|
|
||||||
const url = '/api/v1/ui-data/ocr-pdf';
|
|
||||||
console.log('[OCR Languages] Fetching from URL:', url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
console.log('[OCR Languages] Response received:', {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
ok: response.ok,
|
|
||||||
headers: Object.fromEntries(response.headers.entries())
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: { languages: string[] } = await response.json();
|
|
||||||
const languages = data.languages;
|
|
||||||
console.log('[OCR Languages] Raw response data:', languages);
|
|
||||||
console.log('[OCR Languages] Response type:', typeof languages, 'Array?', Array.isArray(languages));
|
|
||||||
|
|
||||||
const languageOptions = languages.map(lang => {
|
|
||||||
// TODO: Use actual language translations when they become available
|
|
||||||
// For now, use temporary English translations
|
|
||||||
const translatedName = tempOcrLanguages.lang[lang as keyof typeof tempOcrLanguages.lang] || lang;
|
|
||||||
const displayName = translatedName;
|
|
||||||
|
|
||||||
console.log(`[OCR Languages] Language mapping: ${lang} -> ${displayName} (translated: ${!!translatedName})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: lang,
|
|
||||||
label: displayName
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log('[OCR Languages] Transformed language options:', languageOptions);
|
|
||||||
|
|
||||||
setAvailableLanguages(languageOptions);
|
|
||||||
console.log('[OCR Languages] Successfully set', languageOptions.length, 'languages');
|
|
||||||
} else {
|
|
||||||
console.error('[OCR Languages] Response not OK:', response.status, response.statusText);
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('[OCR Languages] Error response body:', errorText);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[OCR Languages] Fetch failed with error:', error);
|
|
||||||
console.error('[OCR Languages] Error details:', {
|
|
||||||
name: error instanceof Error ? error.name : 'Unknown',
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoadingLanguages(false);
|
|
||||||
console.log('[OCR Languages] Language loading completed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchLanguages();
|
|
||||||
}, [t]); // Add t to dependencies since we're using it in the effect
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" fw={500}>OCR Configuration</Text>
|
<Text size="sm" fw={500}>OCR Configuration</Text>
|
||||||
|
|
||||||
{isLoadingLanguages ? (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<Loader size="xs" />
|
|
||||||
<Text size="sm">Loading available languages...</Text>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Select
|
|
||||||
label="Languages"
|
|
||||||
placeholder="Select primary language for OCR"
|
|
||||||
value={parameters.languages[0] || ''}
|
|
||||||
onChange={(value) => onParameterChange('languages', value ? [value] : [])}
|
|
||||||
data={availableLanguages}
|
|
||||||
disabled={disabled}
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="OCR Mode"
|
label="OCR Mode"
|
||||||
@ -121,12 +41,24 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
|||||||
onChange={(value) => onParameterChange('ocrType', value || 'skip-text')}
|
onChange={(value) => onParameterChange('ocrType', value || 'skip-text')}
|
||||||
data={[
|
data={[
|
||||||
{ value: 'skip-text', label: 'Auto (skip text layers)' },
|
{ value: 'skip-text', label: 'Auto (skip text layers)' },
|
||||||
{ value: 'force-ocr', label: 'Force OCR - Process all pages' },
|
{ value: 'force-ocr', label: 'Force (re-OCR all, replace text)' },
|
||||||
{ value: 'Normal', label: 'Normal - Error if text exists' },
|
{ value: 'Normal', label: 'Strict (abort if text found)' },
|
||||||
]}
|
]}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<LanguagePicker
|
||||||
|
value={parameters.languages[0] || ''}
|
||||||
|
onChange={(value) => onParameterChange('languages', [value])}
|
||||||
|
placeholder="Select primary language for OCR"
|
||||||
|
disabled={disabled}
|
||||||
|
label="Languages"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Output"
|
label="Output"
|
||||||
value={parameters.ocrRenderType}
|
value={parameters.ocrRenderType}
|
||||||
@ -138,23 +70,18 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="Additional Options"
|
label="Additional Options"
|
||||||
placeholder="Select additional options"
|
placeholder="Select Options"
|
||||||
value={parameters.additionalOptions}
|
value={parameters.additionalOptions}
|
||||||
onChange={(value) => onParameterChange('additionalOptions', value)}
|
onChange={(value) => onParameterChange('additionalOptions', value)}
|
||||||
data={additionalOptionsData}
|
data={additionalOptionsData}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
clearable
|
clearable
|
||||||
styles={{
|
comboboxProps={{ position: 'top', middlewares: { flip: false, shift: false } }}
|
||||||
input: {
|
|
||||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
|
||||||
borderColor: 'var(--mantine-color-gray-3)',
|
|
||||||
},
|
|
||||||
dropdown: {
|
|
||||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
@ -33,12 +33,13 @@ const ToolStep = ({
|
|||||||
}: ToolStepProps) => {
|
}: ToolStepProps) => {
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
const parent = useContext(ToolStepContext);
|
||||||
|
|
||||||
// Auto-detect if we should show numbers based on sibling count
|
// Auto-detect if we should show numbers based on sibling count
|
||||||
const shouldShowNumber = useMemo(() => {
|
const shouldShowNumber = useMemo(() => {
|
||||||
if (showNumber !== undefined) return showNumber;
|
if (showNumber !== undefined) return showNumber;
|
||||||
const parent = useContext(ToolStepContext);
|
|
||||||
return parent ? parent.visibleStepCount >= 3 : false;
|
return parent ? parent.visibleStepCount >= 3 : false;
|
||||||
}, [showNumber]);
|
}, [showNumber, parent]);
|
||||||
|
|
||||||
const stepNumber = useContext(ToolStepContext)?.getStepNumber?.() || 1;
|
const stepNumber = useContext(ToolStepContext)?.getStepNumber?.() || 1;
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ export const ToolStepContainer = ({ children }: ToolStepContainerProps) => {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
React.Children.forEach(children, (child) => {
|
React.Children.forEach(children, (child) => {
|
||||||
if (React.isValidElement(child) && child.type === ToolStep) {
|
if (React.isValidElement(child) && child.type === ToolStep) {
|
||||||
const isVisible = child.props.isVisible !== false;
|
const isVisible = (child.props as ToolStepProps).isVisible !== false;
|
||||||
if (isVisible) count++;
|
if (isVisible) count++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,42 @@ import { useFileContext } from '../../../contexts/FileContext';
|
|||||||
import { FileOperation } from '../../../types/fileContext';
|
import { FileOperation } from '../../../types/fileContext';
|
||||||
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
|
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
|
||||||
|
|
||||||
|
//Extract files from a ZIP blob
|
||||||
|
async function extractZipFile(zipBlob: Blob): Promise<File[]> {
|
||||||
|
const JSZip = await import('jszip');
|
||||||
|
const zip = new JSZip.default();
|
||||||
|
|
||||||
|
const arrayBuffer = await zipBlob.arrayBuffer();
|
||||||
|
const zipContent = await zip.loadAsync(arrayBuffer);
|
||||||
|
|
||||||
|
const extractedFiles: File[] = [];
|
||||||
|
|
||||||
|
for (const [filename, file] of Object.entries(zipContent.files)) {
|
||||||
|
if (!file.dir) {
|
||||||
|
const content = await file.async('blob');
|
||||||
|
const extractedFile = new File([content], filename, { type: getMimeType(filename) });
|
||||||
|
extractedFiles.push(extractedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get MIME type based on file extension
|
||||||
|
function getMimeType(filename: string): string {
|
||||||
|
const ext = filename.toLowerCase().split('.').pop();
|
||||||
|
switch (ext) {
|
||||||
|
case 'pdf':
|
||||||
|
return 'application/pdf';
|
||||||
|
case 'txt':
|
||||||
|
return 'text/plain';
|
||||||
|
case 'zip':
|
||||||
|
return 'application/zip';
|
||||||
|
default:
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface OCROperationHook {
|
export interface OCROperationHook {
|
||||||
files: File[];
|
files: File[];
|
||||||
thumbnails: string[];
|
thumbnails: string[];
|
||||||
@ -155,16 +191,79 @@ export const useOCROperation = (): OCROperationHook => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { formData, endpoint } = buildFormData(parameters, file);
|
const { formData, endpoint } = buildFormData(parameters, file);
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: "blob",
|
||||||
|
timeout: 300000 // 5 minute timeout for OCR
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for HTTP errors
|
||||||
|
if (response.status >= 400) {
|
||||||
|
// Try to read error response as text
|
||||||
|
const errorText = await response.data.text();
|
||||||
|
throw new Error(`OCR service HTTP error ${response.status}: ${errorText.substring(0, 300)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response
|
||||||
|
if (!response.data || response.data.size === 0) {
|
||||||
|
throw new Error('Empty response from OCR service');
|
||||||
|
}
|
||||||
|
|
||||||
const contentType = response.headers['content-type'] || 'application/pdf';
|
const contentType = response.headers['content-type'] || 'application/pdf';
|
||||||
|
|
||||||
|
// Check if response is actually a PDF by examining the first few bytes
|
||||||
|
const arrayBuffer = await response.data.arrayBuffer();
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
const header = new TextDecoder().decode(uint8Array.slice(0, 4));
|
||||||
|
|
||||||
|
// Check if it's a ZIP file (OCR service returns ZIP when sidecar is enabled or for multi-file results)
|
||||||
|
if (header.startsWith('PK')) {
|
||||||
|
try {
|
||||||
|
// Extract ZIP file contents
|
||||||
|
const zipFiles = await extractZipFile(response.data);
|
||||||
|
|
||||||
|
// Add extracted files to processed files
|
||||||
|
processedFiles.push(...zipFiles);
|
||||||
|
} catch (extractError) {
|
||||||
|
// Fallback to treating as single ZIP file
|
||||||
|
const blob = new Blob([response.data], { type: 'application/zip' });
|
||||||
|
const processedFile = new File([blob], `ocr_${file.name}.zip`, { type: 'application/zip' });
|
||||||
|
processedFiles.push(processedFile);
|
||||||
|
}
|
||||||
|
continue; // Skip the PDF validation for ZIP files
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!header.startsWith('%PDF')) {
|
||||||
|
// Check if it's an error response
|
||||||
|
const text = new TextDecoder().decode(uint8Array.slice(0, 500));
|
||||||
|
|
||||||
|
if (text.includes('error') || text.includes('Error') || text.includes('exception') || text.includes('html')) {
|
||||||
|
// Check for specific OCR tool unavailable error
|
||||||
|
if (text.includes('OCR tools') && text.includes('not installed')) {
|
||||||
|
throw new Error('OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.');
|
||||||
|
}
|
||||||
|
throw new Error(`OCR service error: ${text.substring(0, 300)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an HTML error page
|
||||||
|
if (text.includes('<html') || text.includes('<!DOCTYPE')) {
|
||||||
|
// Try to extract error message from HTML
|
||||||
|
const errorMatch = text.match(/<title[^>]*>([^<]+)<\/title>/i) ||
|
||||||
|
text.match(/<h1[^>]*>([^<]+)<\/h1>/i) ||
|
||||||
|
text.match(/<body[^>]*>([^<]+)<\/body>/i);
|
||||||
|
const errorMessage = errorMatch ? errorMatch[1].trim() : 'Unknown error';
|
||||||
|
throw new Error(`OCR service error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Response is not a valid PDF file. Header: "${header}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob([response.data], { type: contentType });
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
const processedFile = new File([blob], `ocr_${file.name}`, { type: contentType });
|
const processedFile = new File([blob], `ocr_${file.name}`, { type: contentType });
|
||||||
|
|
||||||
processedFiles.push(processedFile);
|
processedFiles.push(processedFile);
|
||||||
} catch (fileError) {
|
} catch (fileError) {
|
||||||
console.error(`Failed to process OCR for ${file.name}:`, fileError);
|
const errorMessage = fileError instanceof Error ? fileError.message : 'Unknown error';
|
||||||
failedFiles.push(file.name);
|
failedFiles.push(`${file.name} (${errorMessage})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +274,19 @@ export const useOCROperation = (): OCROperationHook => {
|
|||||||
if (failedFiles.length > 0) {
|
if (failedFiles.length > 0) {
|
||||||
setStatus(`Processed ${processedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`);
|
setStatus(`Processed ${processedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
setStatus(`OCR completed successfully for ${processedFiles.length} file(s)`);
|
const hasPdfFiles = processedFiles.some(file => file.name.endsWith('.pdf'));
|
||||||
|
const hasTxtFiles = processedFiles.some(file => file.name.endsWith('.txt'));
|
||||||
|
let statusMessage = `OCR completed successfully for ${processedFiles.length} file(s)`;
|
||||||
|
|
||||||
|
if (hasPdfFiles && hasTxtFiles) {
|
||||||
|
statusMessage += ' (Extracted PDF and text files)';
|
||||||
|
} else if (hasPdfFiles) {
|
||||||
|
statusMessage += ' (Extracted PDF files)';
|
||||||
|
} else if (hasTxtFiles) {
|
||||||
|
statusMessage += ' (Extracted text files)';
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(statusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles(processedFiles);
|
setFiles(processedFiles);
|
||||||
@ -186,18 +297,34 @@ export const useOCROperation = (): OCROperationHook => {
|
|||||||
// Cleanup old blob URLs
|
// Cleanup old blob URLs
|
||||||
cleanupBlobUrls();
|
cleanupBlobUrls();
|
||||||
|
|
||||||
// Create download URL
|
// Create download URL - for multiple files, we'll create a new ZIP
|
||||||
if (processedFiles.length === 1) {
|
if (processedFiles.length === 1) {
|
||||||
const url = window.URL.createObjectURL(processedFiles[0]);
|
const url = window.URL.createObjectURL(processedFiles[0]);
|
||||||
setDownloadUrl(url);
|
setDownloadUrl(url);
|
||||||
setBlobUrls([url]);
|
setBlobUrls([url]);
|
||||||
setDownloadFilename(`ocr_${selectedFiles[0].name}`);
|
setDownloadFilename(processedFiles[0].name);
|
||||||
} else {
|
} else {
|
||||||
// For multiple files, we could create a zip, but for now just handle the first file
|
// For multiple files, create a new ZIP containing all extracted files
|
||||||
const url = window.URL.createObjectURL(processedFiles[0]);
|
try {
|
||||||
setDownloadUrl(url);
|
const JSZip = await import('jszip');
|
||||||
setBlobUrls([url]);
|
const zip = new JSZip.default();
|
||||||
setDownloadFilename(`ocr_${validFiles.length}_files.pdf`);
|
|
||||||
|
for (const file of processedFiles) {
|
||||||
|
zip.file(file.name, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
const url = window.URL.createObjectURL(zipBlob);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setBlobUrls([url]);
|
||||||
|
setDownloadFilename(`ocr_extracted_files.zip`);
|
||||||
|
} catch (zipError) {
|
||||||
|
// Fallback to first file
|
||||||
|
const url = window.URL.createObjectURL(processedFiles[0]);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setBlobUrls([url]);
|
||||||
|
setDownloadFilename(processedFiles[0].name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markOperationApplied(fileId, operationId);
|
markOperationApplied(fileId, operationId);
|
||||||
|
@ -167,6 +167,39 @@ export const mantineTheme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MultiSelect: {
|
||||||
|
styles: {
|
||||||
|
input: {
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
borderColor: 'var(--border-default)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
'&:focus': {
|
||||||
|
borderColor: 'var(--color-primary-500)',
|
||||||
|
boxShadow: '0 0 0 1px var(--color-primary-500)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
fontWeight: 'var(--font-weight-medium)',
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
borderColor: 'var(--border-subtle)',
|
||||||
|
boxShadow: 'var(--shadow-lg)',
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'var(--hover-bg)',
|
||||||
|
},
|
||||||
|
'&[data-selected]': {
|
||||||
|
backgroundColor: 'var(--color-primary-100)',
|
||||||
|
color: 'var(--color-primary-900)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
Checkbox: {
|
Checkbox: {
|
||||||
styles: {
|
styles: {
|
||||||
input: {
|
input: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user