From 59515f4183224410bb63d1c0b165ba43e4e28f54 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 30 Jul 2025 17:25:13 +0100 Subject: [PATCH] change requests --- .../tools/ocr/AdvancedOCRSettings.tsx | 89 ++++++++++++++ .../tools/ocr/LanguagePicker.module.css | 28 ++--- .../components/tools/ocr/LanguagePicker.tsx | 38 ++++-- .../src/components/tools/ocr/OCRSettings.tsx | 45 +------ .../src/components/tools/shared/ToolStep.tsx | 44 ++++++- .../src/hooks/tools/ocr/useOCRParameters.ts | 2 +- frontend/src/tools/OCR.tsx | 111 +++++++++++++++--- 7 files changed, 267 insertions(+), 90 deletions(-) create mode 100644 frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx diff --git a/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx b/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx new file mode 100644 index 000000000..db8f931b0 --- /dev/null +++ b/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Stack, Text, Divider, Switch, Group, Checkbox } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { OCRParameters } from './OCRSettings'; + +export interface AdvancedOCRParameters { + ocrRenderType: string; + advancedOptions: string[]; +} + +interface AdvancedOCRSettingsProps { + ocrRenderType: string; + advancedOptions: string[]; + onParameterChange: (key: keyof OCRParameters, value: any) => void; + disabled?: boolean; +} + +const AdvancedOCRSettings: React.FC = ({ + ocrRenderType, + advancedOptions, + onParameterChange, + disabled = false +}) => { + const { t } = useTranslation(); + + // Define the advanced options available + const advancedOptionsData = [ + { value: 'sidecar', label: t('ocr.settings.advancedOptions.sidecar', 'Create a text file') }, + { value: 'deskew', label: t('ocr.settings.advancedOptions.deskew', 'Deskew pages') }, + { value: 'clean', label: t('ocr.settings.advancedOptions.clean', 'Clean input file') }, + { value: 'cleanFinal', label: t('ocr.settings.advancedOptions.cleanFinal', 'Clean final output') }, + ]; + + // Handle individual checkbox changes + const handleCheckboxChange = (optionValue: string, checked: boolean) => { + const newOptions = checked + ? [...advancedOptions, optionValue] + : advancedOptions.filter(option => option !== optionValue); + onParameterChange('additionalOptions', newOptions); + }; + + return ( + + +
+ + {t('ocr.settings.output.label', 'Output Render Type ')} + + + + {t('ocr.settings.output.hocr', 'HOCR (Auto)')} + + onParameterChange('ocrRenderType', event.currentTarget.checked ? 'sandwich' : 'hocr')} + disabled={disabled} + size="sm" + style={{ flexShrink: 0 }} + /> + + {t('ocr.settings.output.sandwich', 'Searchable PDF')} + + +
+ + + +
+ + {t('ocr.settings.advancedOptions.label', 'Processing Options')} + + + {advancedOptionsData.map((option) => ( + handleCheckboxChange(option.value, event.currentTarget.checked)} + label={option.label} + disabled={disabled} + size="sm" + /> + ))} + +
+
+ ); +}; + +export default AdvancedOCRSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/ocr/LanguagePicker.module.css b/frontend/src/components/tools/ocr/LanguagePicker.module.css index 7e37f5493..c44e75291 100644 --- a/frontend/src/components/tools/ocr/LanguagePicker.module.css +++ b/frontend/src/components/tools/ocr/LanguagePicker.module.css @@ -64,6 +64,16 @@ transition: background-color 0.2s ease; } +.languagePickerOptionWithCheckbox { + display: flex; + align-items: center; + justify-content: space-between; +} + +.languagePickerCheckbox { + margin-left: auto; +} + .languagePickerOption:hover { background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */ } @@ -73,25 +83,7 @@ background-color: var(--mantine-color-dark-5); } -.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: var(--mantine-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 */ -} /* Additional helper classes for the component */ .languagePickerTarget { diff --git a/frontend/src/components/tools/ocr/LanguagePicker.tsx b/frontend/src/components/tools/ocr/LanguagePicker.tsx index a13e65f60..34ba25454 100644 --- a/frontend/src/components/tools/ocr/LanguagePicker.tsx +++ b/frontend/src/components/tools/ocr/LanguagePicker.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Stack, Text, Loader, Popover, Box } from '@mantine/core'; +import { Stack, Text, Loader, Popover, Box, Checkbox } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { tempOcrLanguages } from '../../../utils/tempOcrLanguages'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; @@ -11,8 +11,8 @@ export interface LanguageOption { } export interface LanguagePickerProps { - value: string; - onChange: (value: string) => void; + value: string[]; + onChange: (value: string[]) => void; placeholder?: string; disabled?: boolean; label?: string; @@ -22,7 +22,7 @@ export interface LanguagePickerProps { const LanguagePicker: React.FC = ({ value, onChange, - placeholder = 'Select language', + placeholder = 'Select languages', disabled = false, label, languagesEndpoint = '/api/v1/ui-data/ocr-pdf' @@ -85,7 +85,23 @@ const LanguagePicker: React.FC = ({ ); } - const selectedLanguage = availableLanguages.find(lang => lang.value === value); + const handleLanguageToggle = (languageValue: string) => { + const newSelection = value.includes(languageValue) + ? value.filter(v => v !== languageValue) + : [...value, languageValue]; + onChange(newSelection); + }; + + const getDisplayText = () => { + if (value.length === 0) { + return placeholder; + } else if (value.length === 1) { + const selectedLanguage = availableLanguages.find(lang => lang.value === value[0]); + return selectedLanguage?.label || value[0]; + } else { + return `${value.length} languages selected`; + } + }; return ( @@ -105,7 +121,7 @@ const LanguagePicker: React.FC = ({ >
- {selectedLanguage?.label || placeholder} + {getDisplayText()}
@@ -117,10 +133,16 @@ const LanguagePicker: React.FC = ({ {availableLanguages.map((lang) => ( onChange(lang.value)} + className={`${styles.languagePickerOption} ${styles.languagePickerOptionWithCheckbox}`} + onClick={() => handleLanguageToggle(lang.value)} > {lang.label} + {}} // Handled by parent onClick + className={styles.languagePickerCheckbox} + size="sm" + /> ))}
diff --git a/frontend/src/components/tools/ocr/OCRSettings.tsx b/frontend/src/components/tools/ocr/OCRSettings.tsx index 38d3ee6a2..588884889 100644 --- a/frontend/src/components/tools/ocr/OCRSettings.tsx +++ b/frontend/src/components/tools/ocr/OCRSettings.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Stack, Select, MultiSelect, Text, Divider } from '@mantine/core'; +import { Stack, Select, Text, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import LanguagePicker from './LanguagePicker'; @@ -23,18 +23,9 @@ const OCRSettings: React.FC = ({ }) => { const { t } = useTranslation(); - // Define the additional options available - const additionalOptionsData = [ - { value: 'sidecar', label: t('ocr.settings.additionalOptions.sidecar', 'Create sidecar text file') }, - { value: 'deskew', label: t('ocr.settings.additionalOptions.deskew', 'Deskew pages') }, - { value: 'clean', label: t('ocr.settings.additionalOptions.clean', 'Clean input file') }, - { value: 'cleanFinal', label: t('ocr.settings.additionalOptions.cleanFinal', 'Clean final output') }, - ]; - return ( - {t('ocr.settings.title', 'OCR Configuration')} - + onParameterChange('ocrRenderType', value || 'sandwich')} - data={[ - { value: 'sandwich', label: t('ocr.settings.output.sandwich', 'Searchable PDF (Sandwich)') }, - { value: 'hocr', label: t('ocr.settings.output.hocr', 'HOCR XML') } - ]} - disabled={disabled} - /> - - - - onParameterChange('additionalOptions', value)} - data={additionalOptionsData} - disabled={disabled} - clearable - comboboxProps={{ position: 'top', middlewares: { flip: false, shift: false } }} - /> ); }; diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx index 2fd203e2e..1d64a25a3 100644 --- a/frontend/src/components/tools/shared/ToolStep.tsx +++ b/frontend/src/components/tools/shared/ToolStep.tsx @@ -1,5 +1,7 @@ import React, { createContext, useContext, useMemo, useRef } from 'react'; -import { Paper, Text, Stack, Box } from '@mantine/core'; +import { Paper, Text, Stack, Box, Flex } from '@mantine/core'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; interface ToolStepContextType { visibleStepCount: number; @@ -48,15 +50,45 @@ const ToolStep = ({ p="md" withBorder style={{ - cursor: isCollapsed && onCollapsedClick ? 'pointer' : 'default', opacity: isCollapsed ? 0.8 : 1, transition: 'opacity 0.2s ease' }} - onClick={isCollapsed && onCollapsedClick ? onCollapsedClick : undefined} > - - {shouldShowNumber ? `${stepNumber}. ` : ''}{title} - + {/* Chevron icon to collapse/expand the step */} + + + {shouldShowNumber && ( + + {stepNumber} + + )} + + {title} + + + + {isCollapsed ? ( + + ) : ( + + )} + {isCollapsed ? ( diff --git a/frontend/src/hooks/tools/ocr/useOCRParameters.ts b/frontend/src/hooks/tools/ocr/useOCRParameters.ts index 413514d1a..5ac0f61c3 100644 --- a/frontend/src/hooks/tools/ocr/useOCRParameters.ts +++ b/frontend/src/hooks/tools/ocr/useOCRParameters.ts @@ -11,7 +11,7 @@ export interface OCRParametersHook { const defaultParameters: OCRParameters = { languages: ['eng'], ocrType: 'skip-text', - ocrRenderType: 'sandwich', + ocrRenderType: 'hocr', additionalOptions: [], }; diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx index 6f2efb9bc..d338e1cc1 100644 --- a/frontend/src/tools/OCR.tsx +++ b/frontend/src/tools/OCR.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useMemo } from "react"; -import { Button, Stack, Text } from "@mantine/core"; +import React, { useEffect, useMemo, useState } from "react"; +import { Button, Stack, Text, Box } from "@mantine/core"; import { useTranslation } from "react-i18next"; import DownloadIcon from "@mui/icons-material/Download"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; @@ -13,6 +13,7 @@ import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator" import ResultsPreview from "../components/tools/shared/ResultsPreview"; import OCRSettings from "../components/tools/ocr/OCRSettings"; +import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings"; import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters"; import { useOCROperation } from "../hooks/tools/ocr/useOCROperation"; @@ -26,14 +27,37 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const ocrParams = useOCRParameters(); const ocrOperation = useOCROperation(); + // Step expansion state management + const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files'); + const [hasAccessedAdvanced, setHasAccessedAdvanced] = useState(false); + // Endpoint validation const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("ocr-pdf"); + // Calculate state variables + const hasFiles = selectedFiles.length > 0; + const hasResults = ocrOperation.files.length > 0 || ocrOperation.downloadUrl !== null; + const hasValidSettings = ocrParams.validateParameters(); + useEffect(() => { ocrOperation.resetResults(); onPreviewFile?.(null); }, [ocrParams.parameters, selectedFiles]); + // Auto-advance logic - only auto-advance from files to settings when files are first selected + useEffect(() => { + if (selectedFiles.length > 0 && expandedStep === 'files') { + setExpandedStep('settings'); + } + }, [selectedFiles.length, expandedStep]); + + // Collapse all steps when results appear + useEffect(() => { + if (hasResults) { + setExpandedStep(null); + } + }, [hasResults]); + const handleOCR = async () => { try { await ocrOperation.executeOperation( @@ -60,12 +84,34 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { ocrOperation.resetResults(); onPreviewFile?.(null); setCurrentMode('ocr'); + setExpandedStep('settings'); }; - const hasFiles = selectedFiles.length > 0; - const hasResults = ocrOperation.files.length > 0 || ocrOperation.downloadUrl !== null; - const filesCollapsed = hasFiles; - const settingsCollapsed = hasResults; + // Step navigation handlers + const handleStepClick = (step: 'files' | 'settings' | 'advanced') => { + // Prevent expanding steps that aren't ready + if (step === 'settings' && !hasFiles) { + return; + } + + if (step === 'advanced' && !hasAccessedAdvanced) { + setHasAccessedAdvanced(true); + } + setExpandedStep(step); + }; + + const handleAdvanceToAdvanced = () => { + setHasAccessedAdvanced(true); + setExpandedStep('advanced'); + }; + + // Step visibility and collapse logic + const filesVisible = true; + const settingsVisible = true; + const resultsVisible = hasResults; + + const filesCollapsed = expandedStep !== 'files'; + const settingsCollapsed = expandedStep !== 'settings'; const previewResults = useMemo(() => ocrOperation.files?.map((file: File, index: number) => ({ @@ -81,10 +127,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { {/* Files Step */} { {/* Settings Step */} { + if (!hasFiles) return; // Only allow if files are selected + setExpandedStep(expandedStep === 'settings' ? null : 'settings'); + }} + completedMessage={hasFiles && hasValidSettings && settingsCollapsed ? "Basic settings configured" : undefined} > { disabled={endpointLoading} /> + + + + {/* Advanced Step */} + { + if (!hasFiles) return; // Only allow if files are selected + setHasAccessedAdvanced(true); + setExpandedStep(expandedStep === 'advanced' ? null : 'advanced'); + }} + completedMessage={hasFiles && hasResults && expandedStep !== 'advanced' ? "OCR processing completed" : undefined} + > + + + + {/* Process Button - Available after all configuration */} + {hasValidSettings && !hasResults && ( + { loadingText={t("loading")} submitText="Process OCR and Review" /> - - + + )} {/* Results Step */} {ocrOperation.status && (