diff --git a/frontend/src/components/tools/ocr/LanguagePicker.module.css b/frontend/src/components/tools/ocr/LanguagePicker.module.css new file mode 100644 index 000000000..c40a821c1 --- /dev/null +++ b/frontend/src/components/tools/ocr/LanguagePicker.module.css @@ -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 */ +} \ No newline at end of file diff --git a/frontend/src/components/tools/ocr/LanguagePicker.tsx b/frontend/src/components/tools/ocr/LanguagePicker.tsx new file mode 100644 index 000000000..91b8114ad --- /dev/null +++ b/frontend/src/components/tools/ocr/LanguagePicker.tsx @@ -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 = ({ + 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([]); + 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 ( +
+ + Loading available languages... +
+ ); + } + + 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 ( + + {label && ( + + {label} + + )} + + + + + {selectedLanguage?.label || placeholder} + + + + + + + + {availableLanguages.map((lang) => ( + onChange(lang.value)} + > + {lang.label} + + ))} + + + + Looking for additional languages? + + window.open('https://docs.stirlingpdf.com/Advanced%20Configuration/OCR', '_blank')} + > + View setup guide → + + + + + + + ); +}; + +export default LanguagePicker; \ No newline at end of file diff --git a/frontend/src/components/tools/ocr/OCRSettings.tsx b/frontend/src/components/tools/ocr/OCRSettings.tsx index 2344d9ebd..be981bdb0 100644 --- a/frontend/src/components/tools/ocr/OCRSettings.tsx +++ b/frontend/src/components/tools/ocr/OCRSettings.tsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { Stack, Select, MultiSelect, Text, Loader } from '@mantine/core'; +import React from 'react'; +import { Stack, Select, MultiSelect, Text, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import { tempOcrLanguages } from '../../../utils/tempOcrLanguages'; +import LanguagePicker from './LanguagePicker'; export interface OCRParameters { languages: string[]; @@ -22,8 +22,6 @@ const OCRSettings: React.FC = ({ disabled = false }) => { const { t } = useTranslation(); - const [availableLanguages, setAvailableLanguages] = useState<{value: string, label: string}[]>([]); - const [isLoadingLanguages, setIsLoadingLanguages] = useState(true); // Define the additional options available const additionalOptionsData = [ @@ -31,89 +29,11 @@ const OCRSettings: React.FC = ({ { value: 'deskew', label: 'Deskew pages' }, { value: 'clean', label: 'Clean input file' }, { 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 ( OCR Configuration - - {isLoadingLanguages ? ( -
- - Loading available languages... -
- ) : ( - = ({ onChange={(value) => onParameterChange('ocrType', value || 'skip-text')} data={[ { value: 'skip-text', label: 'Auto (skip text layers)' }, - { value: 'force-ocr', label: 'Force OCR - Process all pages' }, - { value: 'Normal', label: 'Normal - Error if text exists' }, + { value: 'force-ocr', label: 'Force (re-OCR all, replace text)' }, + { value: 'Normal', label: 'Strict (abort if text found)' }, ]} disabled={disabled} /> + + + onParameterChange('languages', [value])} + placeholder="Select primary language for OCR" + disabled={disabled} + label="Languages" + /> + + +