From 3e4afd166eee33dce624a6ebf50d7908a8dc619e Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 12 Aug 2025 16:48:34 +0100 Subject: [PATCH] Workflow --- .../tools/shared/createToolFlow.tsx | 106 ++++++++++++++++ frontend/src/tools/Compress.tsx | 49 +++----- frontend/src/tools/Convert.tsx | 53 ++++---- frontend/src/tools/OCR.tsx | 117 ++++++++---------- frontend/src/tools/Split.tsx | 52 +++----- 5 files changed, 220 insertions(+), 157 deletions(-) create mode 100644 frontend/src/components/tools/shared/createToolFlow.tsx diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx new file mode 100644 index 000000000..4622e4334 --- /dev/null +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -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; + isVisible?: boolean; + disabled?: boolean; + testId?: string; +} + +export interface ResultsStepConfig { + isVisible: boolean; + operation: ToolOperationHook; + 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 ( + + {/* 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 && ( + + )} + + {/* Results Step */} + {steps.createResultsStep({ + isVisible: config.results.isVisible, + operation: config.results.operation, + title: config.results.title, + onFileClick: config.results.onFileClick + })} + + ); +} \ No newline at end of file diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index d86136c75..5c323e3c5 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -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 ( - - {/* 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 - }, ( - + tooltip: compressTips, + content: ( - - ))} - - {/* 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 - })} - + } + })} ); } diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx index 788c5aac8..75e8932db 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -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 (
- - {/* 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, - }, ( - + content: ( { selectedFiles={selectedFiles} disabled={endpointLoading} /> - - ))} - {!hasResults && ( - - )} - - {/* 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 - })} - + onFileClick: handleThumbnailClick, + testId: "conversion-results" + } + })}
); diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx index 8e033bb79..9f5758d99 100644 --- a/frontend/src/tools/OCR.tsx +++ b/frontend/src/tools/OCR.tsx @@ -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 ( - - {/* 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: ( + + ) }, - tooltip: ocrTips - }, ( - - - - ))} - - {/* 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'); - }, - }, ( - - ))} - - {/* Process Button - Available after all configuration */} - {hasValidSettings && !hasResults && ( - - )} - - {/* 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: ( + + ) + } + ], + 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 - })} - + } + })} ); } diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index abcc77dd9..851b65cc3 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -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 ( - - {/* 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, - }, ( - + content: ( - - - ))} - - {!hasResults && ( - - )} - - {/* 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 - })} - + } + })} ); }