mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
change requests
This commit is contained in:
parent
1f6b0fea9f
commit
59515f4183
89
frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx
Normal file
89
frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx
Normal 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;
|
@ -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 {
|
||||
|
@ -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<LanguagePickerProps> = ({
|
||||
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<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 (
|
||||
<Box>
|
||||
@ -105,7 +121,7 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
|
||||
>
|
||||
<div className={styles.languagePickerContent}>
|
||||
<Text size="sm" className={styles.languagePickerText}>
|
||||
{selectedLanguage?.label || placeholder}
|
||||
{getDisplayText()}
|
||||
</Text>
|
||||
<UnfoldMoreIcon className={styles.languagePickerIcon} />
|
||||
</div>
|
||||
@ -117,10 +133,16 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
|
||||
{availableLanguages.map((lang) => (
|
||||
<Box
|
||||
key={lang.value}
|
||||
className={`${styles.languagePickerOption} ${value === lang.value ? styles.selected : ''}`}
|
||||
onClick={() => onChange(lang.value)}
|
||||
className={`${styles.languagePickerOption} ${styles.languagePickerOptionWithCheckbox}`}
|
||||
onClick={() => handleLanguageToggle(lang.value)}
|
||||
>
|
||||
<Text size="sm">{lang.label}</Text>
|
||||
<Checkbox
|
||||
checked={value.includes(lang.value)}
|
||||
onChange={() => {}} // Handled by parent onClick
|
||||
className={styles.languagePickerCheckbox}
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
@ -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<OCRSettingsProps> = ({
|
||||
}) => {
|
||||
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 (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={500}>{t('ocr.settings.title', 'OCR Configuration')}</Text>
|
||||
|
||||
|
||||
<Select
|
||||
label={t('ocr.settings.ocrMode.label', 'OCR Mode')}
|
||||
value={parameters.ocrType}
|
||||
@ -50,38 +41,12 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
||||
<Divider />
|
||||
|
||||
<LanguagePicker
|
||||
value={parameters.languages[0] || ''}
|
||||
onChange={(value) => onParameterChange('languages', [value])}
|
||||
placeholder={t('ocr.settings.languages.placeholder', 'Select primary language for OCR')}
|
||||
value={parameters.languages || []}
|
||||
onChange={(value) => onParameterChange('languages', value)}
|
||||
placeholder={t('ocr.settings.languages.placeholder', 'Select languages')}
|
||||
disabled={disabled}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
>
|
||||
<Text fw={500} size="lg" mb="sm">
|
||||
{shouldShowNumber ? `${stepNumber}. ` : ''}{title}
|
||||
</Text>
|
||||
{/* Chevron icon to collapse/expand the step */}
|
||||
<Flex
|
||||
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 ? (
|
||||
<Box>
|
||||
|
@ -11,7 +11,7 @@ export interface OCRParametersHook {
|
||||
const defaultParameters: OCRParameters = {
|
||||
languages: ['eng'],
|
||||
ocrType: 'skip-text',
|
||||
ocrRenderType: 'sandwich',
|
||||
ocrRenderType: 'hocr',
|
||||
additionalOptions: [],
|
||||
};
|
||||
|
||||
|
@ -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 */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
isVisible={filesVisible}
|
||||
isCollapsed={hasFiles ? filesCollapsed : false}
|
||||
isCompleted={hasFiles}
|
||||
onCollapsedClick={undefined}
|
||||
completedMessage={hasFiles && filesCollapsed ?
|
||||
selectedFiles.length === 1
|
||||
? `Selected: ${selectedFiles[0].name}`
|
||||
: `Selected: ${selectedFiles.length} files`
|
||||
@ -99,11 +146,14 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title="Settings"
|
||||
isVisible={hasFiles}
|
||||
isVisible={settingsVisible}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? "OCR processing completed" : undefined}
|
||||
isCompleted={hasFiles && hasValidSettings}
|
||||
onCollapsedClick={() => {
|
||||
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">
|
||||
<OCRSettings
|
||||
@ -112,6 +162,33 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
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
|
||||
onClick={handleOCR}
|
||||
isLoading={ocrOperation.isLoading}
|
||||
@ -119,13 +196,13 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
loadingText={t("loading")}
|
||||
submitText="Process OCR and Review"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={hasResults}
|
||||
isVisible={resultsVisible}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{ocrOperation.status && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user