change requests

This commit is contained in:
EthanHealy01 2025-07-30 17:25:13 +01:00
parent 1f6b0fea9f
commit 59515f4183
7 changed files with 267 additions and 90 deletions

View File

@ -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<AdvancedOCRSettingsProps> = ({
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 (
<Stack gap="md">
<div>
<Text size="sm" fw={500} mb="sm" mt="md">
{t('ocr.settings.output.label', 'Output Render Type ')}
</Text>
<Group justify="space-between" align="center" gap="xs" wrap="nowrap">
<Text size="xs" style={{ flex: '0 1 auto', lineHeight: 1.3, textAlign: 'left' }}>
{t('ocr.settings.output.hocr', 'HOCR (Auto)')}
</Text>
<Switch
checked={ocrRenderType === 'sandwich'}
onChange={(event) => onParameterChange('ocrRenderType', event.currentTarget.checked ? 'sandwich' : 'hocr')}
disabled={disabled}
size="sm"
style={{ flexShrink: 0 }}
/>
<Text size="xs" style={{ flex: '0 1 auto', lineHeight: 1.3, textAlign: 'right' }}>
{t('ocr.settings.output.sandwich', 'Searchable PDF')}
</Text>
</Group>
</div>
<Divider />
<div>
<Text size="sm" fw={500} mb="md">
{t('ocr.settings.advancedOptions.label', 'Processing Options')}
</Text>
<Stack gap="sm">
{advancedOptionsData.map((option) => (
<Checkbox
key={option.value}
checked={advancedOptions.includes(option.value)}
onChange={(event) => handleCheckboxChange(option.value, event.currentTarget.checked)}
label={option.label}
disabled={disabled}
size="sm"
/>
))}
</Stack>
</div>
</Stack>
);
};
export default AdvancedOCRSettings;

View File

@ -64,6 +64,16 @@
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.languagePickerOptionWithCheckbox {
display: flex;
align-items: center;
justify-content: space-between;
}
.languagePickerCheckbox {
margin-left: auto;
}
.languagePickerOption:hover { .languagePickerOption:hover {
background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */ 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); 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 */ /* Additional helper classes for the component */
.languagePickerTarget { .languagePickerTarget {

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useTranslation } from 'react-i18next';
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages'; import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
@ -11,8 +11,8 @@ export interface LanguageOption {
} }
export interface LanguagePickerProps { export interface LanguagePickerProps {
value: string; value: string[];
onChange: (value: string) => void; onChange: (value: string[]) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
@ -22,7 +22,7 @@ export interface LanguagePickerProps {
const LanguagePicker: React.FC<LanguagePickerProps> = ({ const LanguagePicker: React.FC<LanguagePickerProps> = ({
value, value,
onChange, onChange,
placeholder = 'Select language', placeholder = 'Select languages',
disabled = false, disabled = false,
label, label,
languagesEndpoint = '/api/v1/ui-data/ocr-pdf' languagesEndpoint = '/api/v1/ui-data/ocr-pdf'
@ -85,7 +85,23 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
); );
} }
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 ( return (
<Box> <Box>
@ -105,7 +121,7 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
> >
<div className={styles.languagePickerContent}> <div className={styles.languagePickerContent}>
<Text size="sm" className={styles.languagePickerText}> <Text size="sm" className={styles.languagePickerText}>
{selectedLanguage?.label || placeholder} {getDisplayText()}
</Text> </Text>
<UnfoldMoreIcon className={styles.languagePickerIcon} /> <UnfoldMoreIcon className={styles.languagePickerIcon} />
</div> </div>
@ -117,10 +133,16 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
{availableLanguages.map((lang) => ( {availableLanguages.map((lang) => (
<Box <Box
key={lang.value} key={lang.value}
className={`${styles.languagePickerOption} ${value === lang.value ? styles.selected : ''}`} className={`${styles.languagePickerOption} ${styles.languagePickerOptionWithCheckbox}`}
onClick={() => onChange(lang.value)} onClick={() => handleLanguageToggle(lang.value)}
> >
<Text size="sm">{lang.label}</Text> <Text size="sm">{lang.label}</Text>
<Checkbox
checked={value.includes(lang.value)}
onChange={() => {}} // Handled by parent onClick
className={styles.languagePickerCheckbox}
size="sm"
/>
</Box> </Box>
))} ))}
</Box> </Box>

View File

@ -1,5 +1,5 @@
import React from 'react'; 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 { useTranslation } from 'react-i18next';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
@ -23,18 +23,9 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
}) => { }) => {
const { t } = useTranslation(); 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 ( return (
<Stack gap="md"> <Stack gap="md">
<Text size="sm" fw={500}>{t('ocr.settings.title', 'OCR Configuration')}</Text>
<Select <Select
label={t('ocr.settings.ocrMode.label', 'OCR Mode')} label={t('ocr.settings.ocrMode.label', 'OCR Mode')}
value={parameters.ocrType} value={parameters.ocrType}
@ -50,38 +41,12 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
<Divider /> <Divider />
<LanguagePicker <LanguagePicker
value={parameters.languages[0] || ''} value={parameters.languages || []}
onChange={(value) => onParameterChange('languages', [value])} onChange={(value) => onParameterChange('languages', value)}
placeholder={t('ocr.settings.languages.placeholder', 'Select primary language for OCR')} placeholder={t('ocr.settings.languages.placeholder', 'Select languages')}
disabled={disabled} disabled={disabled}
label={t('ocr.settings.languages.label', 'Languages')} label={t('ocr.settings.languages.label', 'Languages')}
/> />
<Divider />
<Select
label={t('ocr.settings.output.label', 'Output')}
value={parameters.ocrRenderType}
onChange={(value) => 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}
/>
<Divider />
<MultiSelect
label={t('ocr.settings.additionalOptions.label', 'Additional Options')}
placeholder={t('ocr.settings.additionalOptions.placeholder', 'Select Options')}
value={parameters.additionalOptions}
onChange={(value) => onParameterChange('additionalOptions', value)}
data={additionalOptionsData}
disabled={disabled}
clearable
comboboxProps={{ position: 'top', middlewares: { flip: false, shift: false } }}
/>
</Stack> </Stack>
); );
}; };

View File

@ -1,5 +1,7 @@
import React, { createContext, useContext, useMemo, useRef } from 'react'; 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 { interface ToolStepContextType {
visibleStepCount: number; visibleStepCount: number;
@ -48,15 +50,45 @@ const ToolStep = ({
p="md" p="md"
withBorder withBorder
style={{ style={{
cursor: isCollapsed && onCollapsedClick ? 'pointer' : 'default',
opacity: isCollapsed ? 0.8 : 1, opacity: isCollapsed ? 0.8 : 1,
transition: 'opacity 0.2s ease' transition: 'opacity 0.2s ease'
}} }}
onClick={isCollapsed && onCollapsedClick ? onCollapsedClick : undefined}
> >
<Text fw={500} size="lg" mb="sm"> {/* Chevron icon to collapse/expand the step */}
{shouldShowNumber ? `${stepNumber}. ` : ''}{title} <Flex
</Text> align="center"
justify="space-between"
mb="sm"
style={{
cursor: onCollapsedClick ? 'pointer' : 'default'
}}
onClick={onCollapsedClick}
>
<Flex align="center" gap="sm">
{shouldShowNumber && (
<Text fw={500} size="lg" c="dimmed">
{stepNumber}
</Text>
)}
<Text fw={500} size="lg">
{title}
</Text>
</Flex>
{isCollapsed ? (
<ChevronRightIcon style={{
fontSize: '1.2rem',
color: 'var(--mantine-color-dimmed)',
opacity: onCollapsedClick ? 1 : 0.5
}} />
) : (
<ExpandMoreIcon style={{
fontSize: '1.2rem',
color: 'var(--mantine-color-dimmed)',
opacity: onCollapsedClick ? 1 : 0.5
}} />
)}
</Flex>
{isCollapsed ? ( {isCollapsed ? (
<Box> <Box>

View File

@ -11,7 +11,7 @@ export interface OCRParametersHook {
const defaultParameters: OCRParameters = { const defaultParameters: OCRParameters = {
languages: ['eng'], languages: ['eng'],
ocrType: 'skip-text', ocrType: 'skip-text',
ocrRenderType: 'sandwich', ocrRenderType: 'hocr',
additionalOptions: [], additionalOptions: [],
}; };

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Button, Stack, Text } from "@mantine/core"; import { Button, Stack, Text, Box } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useEndpointEnabled } from "../hooks/useEndpointConfig";
@ -13,6 +13,7 @@ import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"
import ResultsPreview from "../components/tools/shared/ResultsPreview"; import ResultsPreview from "../components/tools/shared/ResultsPreview";
import OCRSettings from "../components/tools/ocr/OCRSettings"; import OCRSettings from "../components/tools/ocr/OCRSettings";
import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings";
import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters"; import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters";
import { useOCROperation } from "../hooks/tools/ocr/useOCROperation"; import { useOCROperation } from "../hooks/tools/ocr/useOCROperation";
@ -26,14 +27,37 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const ocrParams = useOCRParameters(); const ocrParams = useOCRParameters();
const ocrOperation = useOCROperation(); const ocrOperation = useOCROperation();
// Step expansion state management
const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files');
const [hasAccessedAdvanced, setHasAccessedAdvanced] = useState(false);
// Endpoint validation // Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("ocr-pdf"); 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(() => { useEffect(() => {
ocrOperation.resetResults(); ocrOperation.resetResults();
onPreviewFile?.(null); onPreviewFile?.(null);
}, [ocrParams.parameters, selectedFiles]); }, [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 () => { const handleOCR = async () => {
try { try {
await ocrOperation.executeOperation( await ocrOperation.executeOperation(
@ -60,12 +84,34 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
ocrOperation.resetResults(); ocrOperation.resetResults();
onPreviewFile?.(null); onPreviewFile?.(null);
setCurrentMode('ocr'); setCurrentMode('ocr');
setExpandedStep('settings');
}; };
const hasFiles = selectedFiles.length > 0; // Step navigation handlers
const hasResults = ocrOperation.files.length > 0 || ocrOperation.downloadUrl !== null; const handleStepClick = (step: 'files' | 'settings' | 'advanced') => {
const filesCollapsed = hasFiles; // Prevent expanding steps that aren't ready
const settingsCollapsed = hasResults; 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(() => const previewResults = useMemo(() =>
ocrOperation.files?.map((file: File, index: number) => ({ ocrOperation.files?.map((file: File, index: number) => ({
@ -81,10 +127,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
{/* Files Step */} {/* Files Step */}
<ToolStep <ToolStep
title="Files" title="Files"
isVisible={true} isVisible={filesVisible}
isCollapsed={filesCollapsed} isCollapsed={hasFiles ? filesCollapsed : false}
isCompleted={filesCollapsed} isCompleted={hasFiles}
completedMessage={hasFiles ? onCollapsedClick={undefined}
completedMessage={hasFiles && filesCollapsed ?
selectedFiles.length === 1 selectedFiles.length === 1
? `Selected: ${selectedFiles[0].name}` ? `Selected: ${selectedFiles[0].name}`
: `Selected: ${selectedFiles.length} files` : `Selected: ${selectedFiles.length} files`
@ -99,11 +146,14 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
{/* Settings Step */} {/* Settings Step */}
<ToolStep <ToolStep
title="Settings" title="Settings"
isVisible={hasFiles} isVisible={settingsVisible}
isCollapsed={settingsCollapsed} isCollapsed={settingsCollapsed}
isCompleted={settingsCollapsed} isCompleted={hasFiles && hasValidSettings}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined} onCollapsedClick={() => {
completedMessage={settingsCollapsed ? "OCR processing completed" : undefined} if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
}}
completedMessage={hasFiles && hasValidSettings && settingsCollapsed ? "Basic settings configured" : undefined}
> >
<Stack gap="sm"> <Stack gap="sm">
<OCRSettings <OCRSettings
@ -112,6 +162,33 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
disabled={endpointLoading} disabled={endpointLoading}
/> />
</Stack>
</ToolStep>
{/* Advanced Step */}
<ToolStep
title="Advanced"
isVisible={true}
isCollapsed={expandedStep !== 'advanced'}
isCompleted={hasFiles && hasResults}
onCollapsedClick={() => {
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}
>
<AdvancedOCRSettings
ocrRenderType={ocrParams.parameters.ocrRenderType}
advancedOptions={ocrParams.parameters.additionalOptions}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
</ToolStep>
{/* Process Button - Available after all configuration */}
{hasValidSettings && !hasResults && (
<Box mt="md">
<OperationButton <OperationButton
onClick={handleOCR} onClick={handleOCR}
isLoading={ocrOperation.isLoading} isLoading={ocrOperation.isLoading}
@ -119,13 +196,13 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
loadingText={t("loading")} loadingText={t("loading")}
submitText="Process OCR and Review" submitText="Process OCR and Review"
/> />
</Stack> </Box>
</ToolStep> )}
{/* Results Step */} {/* Results Step */}
<ToolStep <ToolStep
title="Results" title="Results"
isVisible={hasResults} isVisible={resultsVisible}
> >
<Stack gap="sm"> <Stack gap="sm">
{ocrOperation.status && ( {ocrOperation.status && (