This commit is contained in:
Connor Yoh 2025-08-12 16:48:34 +01:00
parent 9d2ca3c8c8
commit 3e4afd166e
5 changed files with 220 additions and 157 deletions

View File

@ -0,0 +1,106 @@
import React from 'react';
import { Stack } from '@mantine/core';
import { createToolSteps, ToolStepProvider } from './ToolStep';
import OperationButton from './OperationButton';
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
export interface FilesStepConfig {
selectedFiles: File[];
isCollapsed?: boolean;
placeholder?: string;
onCollapsedClick?: () => void;
}
export interface MiddleStepConfig {
title: string;
isVisible?: boolean;
isCollapsed?: boolean;
isCompleted?: boolean;
onCollapsedClick?: () => void;
completedMessage?: string;
content: React.ReactNode;
tooltip?: {
content?: React.ReactNode;
tips?: any[];
header?: {
title: string;
logo?: React.ReactNode;
};
};
}
export interface ExecuteButtonConfig {
text: string;
loadingText: string;
onClick: () => Promise<void>;
isVisible?: boolean;
disabled?: boolean;
testId?: string;
}
export interface ResultsStepConfig {
isVisible: boolean;
operation: ToolOperationHook<any>;
title: string;
onFileClick?: (file: File) => void;
testId?: string;
}
export interface ToolFlowConfig {
files: FilesStepConfig;
steps: MiddleStepConfig[];
executeButton?: ExecuteButtonConfig;
results: ResultsStepConfig;
}
/**
* Creates a flexible tool flow with configurable steps and state management left to the tool.
* Reduces boilerplate while allowing tools to manage their own collapse/expansion logic.
*/
export function createToolFlow(config: ToolFlowConfig) {
const steps = createToolSteps();
return (
<ToolStepProvider>
{/* Files Step */}
{steps.createFilesStep({
selectedFiles: config.files.selectedFiles,
isCollapsed: config.files.isCollapsed,
placeholder: config.files.placeholder,
onCollapsedClick: config.files.onCollapsedClick
})}
{/* Middle Steps */}
{config.steps.map((stepConfig, index) =>
steps.create(stepConfig.title, {
isVisible: stepConfig.isVisible,
isCollapsed: stepConfig.isCollapsed,
isCompleted: stepConfig.isCompleted,
onCollapsedClick: stepConfig.onCollapsedClick,
completedMessage: stepConfig.completedMessage,
tooltip: stepConfig.tooltip
}, stepConfig.content)
)}
{/* Execute Button */}
{config.executeButton && config.executeButton.isVisible !== false && (
<OperationButton
onClick={config.executeButton.onClick}
isLoading={config.results.operation.isLoading}
disabled={config.executeButton.disabled}
loadingText={config.executeButton.loadingText}
submitText={config.executeButton.text}
data-testid={config.executeButton.testId}
/>
)}
{/* Results Step */}
{steps.createResultsStep({
isVisible: config.results.isVisible,
operation: config.results.operation,
title: config.results.title,
onFileClick: config.results.onFileClick
})}
</ToolStepProvider>
);
}

View File

@ -1,12 +1,11 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect } from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolSteps, ToolStepProvider } from "../components/tools/shared/ToolStep";
import OperationButton from "../components/tools/shared/OperationButton";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import CompressSettings from "../components/tools/compress/CompressSettings";
@ -65,49 +64,41 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesCollapsed = hasFiles;
const settingsCollapsed = !hasFiles || hasResults;
const steps = createToolSteps();
return (
<Stack gap="md" h="100%" p="sm" style={{ overflow: 'auto' }}>
<ToolStepProvider>
{/* Files Step */}
{steps.createFilesStep({
{createToolFlow({
files: {
selectedFiles,
isCollapsed: filesCollapsed
})}
{/* Settings Step */}
{steps.create("Settings", {
},
steps: [{
title: "Settings",
isCollapsed: settingsCollapsed,
isCompleted: hasResults,
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
completedMessage: t("compress.header", "Compression completed"),
tooltip: compressTips
}, (
<Stack gap="md">
tooltip: compressTips,
content: (
<CompressSettings
parameters={compressParams.parameters}
onParameterChange={compressParams.updateParameter}
disabled={endpointLoading}
/>
</Stack>
))}
<OperationButton
onClick={handleCompress}
isLoading={compressOperation.isLoading}
disabled={!compressParams.validateParameters() || !hasFiles || !endpointEnabled}
loadingText={t("loading")}
submitText={t("compress.submit", "Compress")}
/>
{/* Results Step */}
{steps.createResultsStep({
)
}],
executeButton: {
text: t("compress.submit", "Compress"),
loadingText: t("loading"),
onClick: handleCompress,
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
},
results: {
isVisible: hasResults,
operation: compressOperation,
title: t("compress.title", "Compression Results"),
onFileClick: handleThumbnailClick
})}
</ToolStepProvider>
}
})}
</Stack>
);
}

View File

@ -5,8 +5,7 @@ import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolSteps, ToolStepProvider } from "../components/tools/shared/ToolStep";
import OperationButton from "../components/tools/shared/OperationButton";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
@ -101,26 +100,21 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
setCurrentMode('convert');
};
const steps = createToolSteps();
return (
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
<Stack gap="sm" p="sm">
<ToolStepProvider>
{/* Files Step */}
{steps.createFilesStep({
{createToolFlow({
files: {
selectedFiles,
isCollapsed: filesCollapsed,
placeholder: t("convert.selectFilesPlaceholder", "Select files in the main view to get started")
})}
{/* Settings Step */}
{steps.create(t("convert.settings", "Settings"), {
},
steps: [{
title: t("convert.settings", "Settings"),
isCollapsed: settingsCollapsed,
isCompleted: settingsCollapsed,
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
}, (
<Stack gap="sm">
content: (
<ConvertSettings
parameters={convertParams.parameters}
onParameterChange={convertParams.updateParameter}
@ -128,27 +122,24 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
selectedFiles={selectedFiles}
disabled={endpointLoading}
/>
</Stack>
))}
{!hasResults && (
<OperationButton
onClick={handleConvert}
isLoading={convertOperation.isLoading}
disabled={!convertParams.validateParameters() || !hasFiles || !endpointEnabled}
loadingText={t("convert.converting", "Converting...")}
submitText={t("convert.convertFiles", "Convert Files")}
data-testid="convert-button"
/>
)}
{/* Results Step */}
{steps.createResultsStep({
)
}],
executeButton: {
text: t("convert.convertFiles", "Convert Files"),
loadingText: t("convert.converting", "Converting..."),
onClick: handleConvert,
isVisible: !hasResults,
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
testId: "convert-button"
},
results: {
isVisible: hasResults,
operation: convertOperation,
title: t("convert.conversionResults", "Conversion Results"),
onFileClick: handleThumbnailClick
})}
</ToolStepProvider>
onFileClick: handleThumbnailClick,
testId: "conversion-results"
}
})}
</Stack>
</div>
);

View File

@ -1,12 +1,11 @@
import React, { useEffect, useMemo, useState } from "react";
import { Stack, Box } from "@mantine/core";
import React, { useEffect, useState } from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolSteps, ToolStepProvider } from "../components/tools/shared/ToolStep";
import OperationButton from "../components/tools/shared/OperationButton";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import OCRSettings from "../components/tools/ocr/OCRSettings";
import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings";
@ -75,78 +74,66 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
};
const resultsVisible = hasResults;
const filesCollapsed = expandedStep !== 'files';
const settingsCollapsed = expandedStep !== 'settings';
const steps = createToolSteps();
return (
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
<ToolStepProvider>
{/* Files Step */}
{steps.createFilesStep({
{createToolFlow({
files: {
selectedFiles,
isCollapsed: hasFiles && filesCollapsed,
})}
{/* Settings Step */}
{steps.create("Settings", {
isCollapsed: !hasFiles || settingsCollapsed,
isCompleted: hasFiles && hasValidSettings,
onCollapsedClick: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
},
steps: [
{
title: "Settings",
isCollapsed: !hasFiles || settingsCollapsed,
isCompleted: hasFiles && hasValidSettings,
onCollapsedClick: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
},
tooltip: ocrTips,
content: (
<OCRSettings
parameters={ocrParams.parameters}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
)
},
tooltip: ocrTips
}, (
<Stack gap="sm">
<OCRSettings
parameters={ocrParams.parameters}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
</Stack>
))}
{/* Advanced Step */}
{steps.create("Advanced", {
isCollapsed: expandedStep !== 'advanced',
isCompleted: hasFiles && hasResults,
onCollapsedClick: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === 'advanced' ? null : 'advanced');
},
}, (
<AdvancedOCRSettings
advancedOptions={ocrParams.parameters.additionalOptions}
ocrRenderType={ocrParams.parameters.ocrRenderType}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
))}
{/* Process Button - Available after all configuration */}
{hasValidSettings && !hasResults && (
<OperationButton
onClick={handleOCR}
isLoading={ocrOperation.isLoading}
disabled={!ocrParams.validateParameters() || !hasFiles || !endpointEnabled}
loadingText={t("loading")}
submitText={t("ocr.operation.submit", "Process OCR and Review")}
/>
)}
{/* Results Step */}
{steps.createResultsStep({
isVisible: resultsVisible,
{
title: "Advanced",
isCollapsed: expandedStep !== 'advanced',
isCompleted: hasFiles && hasResults,
onCollapsedClick: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === 'advanced' ? null : 'advanced');
},
content: (
<AdvancedOCRSettings
advancedOptions={ocrParams.parameters.additionalOptions}
ocrRenderType={ocrParams.parameters.ocrRenderType}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
)
}
],
executeButton: {
text: t("ocr.operation.submit", "Process OCR and Review"),
loadingText: t("loading"),
onClick: handleOCR,
isVisible: hasValidSettings && !hasResults,
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled
},
results: {
isVisible: hasResults,
operation: ocrOperation,
title: t("ocr.results.title", "OCR Results"),
onFileClick: handleThumbnailClick
})}
</ToolStepProvider>
}
})}
</Stack>
);
}

View File

@ -1,12 +1,11 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect } from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolSteps, ToolStepProvider } from "../components/tools/shared/ToolStep";
import OperationButton from "../components/tools/shared/OperationButton";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import SplitSettings from "../components/tools/split/SplitSettings";
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
@ -64,52 +63,41 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesCollapsed = hasFiles;
const settingsCollapsed = !hasFiles || hasResults;
const steps = createToolSteps();
return (
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
<ToolStepProvider>
{/* Files Step */}
{steps.createFilesStep({
{createToolFlow({
files: {
selectedFiles,
isCollapsed: filesCollapsed,
placeholder: "Select a PDF file in the main view to get started"
})}
{/* Settings Step */}
{steps.create("Settings", {
},
steps: [{
title: "Settings",
isCollapsed: settingsCollapsed,
isCompleted: hasResults,
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
}, (
<Stack gap="sm">
content: (
<SplitSettings
parameters={splitParams.parameters}
onParameterChange={splitParams.updateParameter}
disabled={endpointLoading}
/>
</Stack>
))}
{!hasResults && (
<OperationButton
onClick={handleSplit}
isLoading={splitOperation.isLoading}
disabled={!splitParams.validateParameters() || !hasFiles || !endpointEnabled}
loadingText={t("loading")}
submitText={t("split.submit", "Split PDF")}
/>
)}
{/* Results Step */}
{steps.createResultsStep({
)
}],
executeButton: {
text: t("split.submit", "Split PDF"),
loadingText: t("loading"),
onClick: handleSplit,
isVisible: !hasResults,
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled
},
results: {
isVisible: hasResults,
operation: splitOperation,
title: "Split Results",
onFileClick: handleThumbnailClick
})}
</ToolStepProvider>
}
})}
</Stack>
);
}