From 85caad5f5cf053e29cd88dd1640beb75782468d1 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Thu, 21 Aug 2025 17:25:55 +0100 Subject: [PATCH] Thursday demo --- .../tools/automate/AutomationCreation.tsx | 21 +-- .../tools/automate/AutomationExecutor.tsx | 112 -------------- .../tools/automate/AutomationRun.tsx | 134 ++++++++++------- .../tools/automate/ToolConfigurationModal.tsx | 139 +++-------------- frontend/src/data/toolsTaxonomy.ts | 9 +- .../src/data/useTranslatedToolRegistry.tsx | 18 ++- .../addPassword/useAddPasswordOperation.ts | 49 +++--- .../tools/automate/useAutomateOperation.ts | 12 +- .../tools/automate/useAutomationExecution.ts | 141 ------------------ .../tools/compress/useCompressOperation.ts | 23 ++- .../tools/compress/useCompressParameters.ts | 2 +- .../hooks/tools/shared/useToolOperation.ts | 3 + .../hooks/tools/split/useSplitOperation.ts | 24 +-- .../hooks/tools/split/useSplitParameters.ts | 2 +- frontend/src/tools/AddPassword.tsx | 8 - frontend/src/tools/Automate.tsx | 21 ++- frontend/src/tools/Compress.tsx | 5 - frontend/src/tools/Split.tsx | 3 - frontend/src/utils/automationExecutor.ts | 102 ++++--------- 19 files changed, 258 insertions(+), 570 deletions(-) delete mode 100644 frontend/src/components/tools/automate/AutomationExecutor.tsx delete mode 100644 frontend/src/hooks/tools/automate/useAutomationExecution.ts diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 3bd89be05..df897f094 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -99,6 +99,14 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o return tool?.name || t(`tools.${operation}.name`, operation); }; + const getToolDefaultParameters = (operation: string): any => { + const config = toolRegistry[operation]?.operationConfig; + if (config?.defaultParameters) { + return { ...config.defaultParameters }; // Return a copy to avoid mutations + } + return {}; + }; + const addTool = (operation: string) => { const newTool: AutomationTool = { @@ -106,7 +114,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o operation, name: getToolName(operation), configured: false, - parameters: {} + parameters: getToolDefaultParameters(operation) }; setSelectedTools([...selectedTools, newTool]); @@ -259,15 +267,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o const updatedTools = [...selectedTools]; // Get default parameters from the tool - let defaultParams = {}; - const tool = toolRegistry?.[newOperation]; - if (tool?.component && (tool.component as any).getDefaultParameters) { - try { - defaultParams = (tool.component as any).getDefaultParameters(); - } catch (error) { - console.warn(`Failed to get default parameters for ${newOperation}:`, error); - } - } + const defaultParams = getToolDefaultParameters(newOperation); updatedTools[index] = { ...updatedTools[index], @@ -370,6 +370,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o tool={currentConfigTool} onSave={handleToolConfigSave} onCancel={handleToolConfigCancel} + toolRegistry={toolRegistry} /> )} diff --git a/frontend/src/components/tools/automate/AutomationExecutor.tsx b/frontend/src/components/tools/automate/AutomationExecutor.tsx deleted file mode 100644 index c259515e5..000000000 --- a/frontend/src/components/tools/automate/AutomationExecutor.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; -import { ToolComponent } from '../../../types/tool'; - -interface AutomationExecutorProps { - automation: any; - files: File[]; - onStepStart: (stepIndex: number) => void; - onStepComplete: (stepIndex: number, results: File[]) => void; - onStepError: (stepIndex: number, error: string) => void; - onComplete: (finalResults: File[]) => void; - shouldExecute: boolean; -} - -/** - * Component that manages the execution of automation steps using real tool hooks. - * This component creates operation hook instances for each tool in the automation. - */ -export const AutomationExecutor: React.FC = ({ - automation, - files, - onStepStart, - onStepComplete, - onStepError, - onComplete, - shouldExecute -}) => { - const toolRegistry = useFlatToolRegistry(); - const [currentStepIndex, setCurrentStepIndex] = useState(-1); - const [currentFiles, setCurrentFiles] = useState(files); - const [isExecuting, setIsExecuting] = useState(false); - - // Create operation hooks for all tools in the automation - const operationHooks = React.useMemo(() => { - if (!automation?.operations) return {}; - - const hooks: Record = {}; - automation.operations.forEach((op: any, index: number) => { - const tool = toolRegistry[op.operation]; - if (tool?.component) { - const toolComponent = tool.component as ToolComponent; - if ('tool' in toolComponent) { - // We still can't call the hook here dynamically - // This approach also won't work - } - } - }); - - return hooks; - }, [automation, toolRegistry]); - - // Execute automation when shouldExecute becomes true - useEffect(() => { - if (shouldExecute && !isExecuting && automation?.operations?.length > 0) { - executeAutomation(); - } - }, [shouldExecute, isExecuting, automation]); - - const executeAutomation = async () => { - if (!automation?.operations || automation.operations.length === 0) { - return; - } - - setIsExecuting(true); - setCurrentFiles(files); - let filesToProcess = [...files]; - - try { - for (let i = 0; i < automation.operations.length; i++) { - setCurrentStepIndex(i); - const operation = automation.operations[i]; - - onStepStart(i); - - // Get the tool - const tool = toolRegistry[operation.operation]; - if (!tool?.component) { - throw new Error(`Tool not found: ${operation.operation}`); - } - - const toolComponent = tool.component as ToolComponent; - if (!('tool' in toolComponent)) { - throw new Error(`Tool ${operation.operation} does not support automation`); - } - - // For now, simulate the execution - // TODO: We need to find a way to actually execute the tool operation - await new Promise(resolve => setTimeout(resolve, 2000)); - - // For now, assume the operation succeeded with the same files - const resultFiles = filesToProcess; // TODO: Get actual results - - onStepComplete(i, resultFiles); - filesToProcess = resultFiles; - setCurrentFiles(resultFiles); - } - - onComplete(filesToProcess); - setIsExecuting(false); - setCurrentStepIndex(-1); - - } catch (error: any) { - console.error('Automation execution failed:', error); - onStepError(currentStepIndex, error.message); - setIsExecuting(false); - setCurrentStepIndex(-1); - } - }; - - // This component doesn't render anything visible - return null; -}; \ No newline at end of file diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx index a107b573f..555107cb8 100644 --- a/frontend/src/components/tools/automate/AutomationRun.tsx +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -1,24 +1,22 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Text, Stack, Group, Progress, Card } from "@mantine/core"; +import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import CheckIcon from "@mui/icons-material/Check"; -import ErrorIcon from "@mui/icons-material/Error"; import { useFileSelection } from "../../../contexts/FileSelectionContext"; import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry"; -import { executeAutomationSequence } from "../../../utils/automationExecutor"; interface AutomationRunProps { automation: any; onComplete: () => void; - automateOperation?: any; // Add the operation hook to store results + automateOperation?: any; } interface ExecutionStep { id: string; operation: string; name: string; - status: "pending" | "running" | "completed" | "error"; + status: 'pending' | 'running' | 'completed' | 'error'; error?: string; } @@ -26,9 +24,14 @@ export default function AutomationRun({ automation, onComplete, automateOperatio const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); const toolRegistry = useFlatToolRegistry(); - const [isExecuting, setIsExecuting] = useState(false); + + // Progress tracking state const [executionSteps, setExecutionSteps] = useState([]); const [currentStepIndex, setCurrentStepIndex] = useState(-1); + + // Use the operation hook's loading state + const isExecuting = automateOperation?.isLoading || false; + const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null; // Initialize execution steps from automation React.useEffect(() => { @@ -39,16 +42,25 @@ export default function AutomationRun({ automation, onComplete, automateOperatio id: `${op.operation}-${index}`, operation: op.operation, name: tool?.name || op.operation, - status: "pending" as const, + status: 'pending' as const }; }); setExecutionSteps(steps); + setCurrentStepIndex(-1); } - }, [automation]); // Remove toolRegistry from dependencies to prevent infinite loops + }, [automation, toolRegistry]); + + // Cleanup when component unmounts + React.useEffect(() => { + return () => { + // Reset progress state when component unmounts + setExecutionSteps([]); + setCurrentStepIndex(-1); + }; + }, []); const executeAutomation = async () => { if (!selectedFiles || selectedFiles.length === 0) { - // Show error - need files to execute automation return; } @@ -57,59 +69,74 @@ export default function AutomationRun({ automation, onComplete, automateOperatio return; } - setIsExecuting(true); + // Reset progress tracking setCurrentStepIndex(0); + setExecutionSteps(prev => prev.map(step => ({ ...step, status: 'pending' as const, error: undefined }))); try { // Use the automateOperation.executeOperation to handle file consumption properly await automateOperation.executeOperation( - { automationConfig: automation }, + { + automationConfig: automation, + onStepStart: (stepIndex: number, operationName: string) => { + setCurrentStepIndex(stepIndex); + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'running' as const } : step + )); + }, + onStepComplete: (stepIndex: number, resultFiles: File[]) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'completed' as const } : step + )); + }, + onStepError: (stepIndex: number, error: string) => { + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'error' as const, error } : step + )); + } + }, selectedFiles ); - - // All steps completed successfully + + // Mark all as completed and reset current step setCurrentStepIndex(-1); - setIsExecuting(false); - console.log(`✅ Automation completed successfully`); } catch (error: any) { console.error("Automation execution failed:", error); - setIsExecuting(false); setCurrentStepIndex(-1); } }; - const getStepIcon = (step: ExecutionStep) => { - switch (step.status) { - case "completed": - return ; - case "error": - return ; - case "running": - return ( -
- ); - default: - return
; - } - }; - const getProgress = () => { - const completedSteps = executionSteps.filter((step) => step.status === "completed").length; + if (executionSteps.length === 0) return 0; + const completedSteps = executionSteps.filter(step => step.status === 'completed').length; return (completedSteps / executionSteps.length) * 100; }; - const allStepsCompleted = executionSteps.every((step) => step.status === "completed"); - const hasErrors = executionSteps.some((step) => step.status === "error"); + const getStepIcon = (step: ExecutionStep) => { + switch (step.status) { + case 'completed': + return ; + case 'error': + return ; + case 'running': + return
; + default: + return
; + } + }; return (
@@ -128,10 +155,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio {isExecuting && (
- {t("automate.sequence.progress", "Progress: {{current}}/{{total}}", { - current: currentStepIndex + 1, - total: executionSteps.length, - })} + Progress: {currentStepIndex + 1}/{executionSteps.length}
@@ -148,11 +172,11 @@ export default function AutomationRun({ automation, onComplete, automateOperatio {getStepIcon(step)}
- {step.name} @@ -179,6 +203,12 @@ export default function AutomationRun({ automation, onComplete, automateOperatio ? t("automate.sequence.running", "Running Automation...") : t("automate.sequence.run", "Run Automation")} + + {hasResults && ( + + )} @@ -192,4 +222,4 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
); -} +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx index 32537026e..f9456ad09 100644 --- a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -1,19 +1,19 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Modal, - Title, - Button, - Group, +import { + Modal, + Title, + Button, + Group, Stack, Text, - Alert, - Loader + Alert } from '@mantine/core'; import SettingsIcon from '@mui/icons-material/Settings'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import WarningIcon from '@mui/icons-material/Warning'; +import { ToolRegistry } from '../../../data/toolsTaxonomy'; interface ToolConfigurationModalProps { opened: boolean; tool: { @@ -24,136 +24,31 @@ interface ToolConfigurationModalProps { }; onSave: (parameters: any) => void; onCancel: () => void; + toolRegistry: ToolRegistry; } -export default function ToolConfigurationModal({ opened, tool, onSave, onCancel }: ToolConfigurationModalProps) { +export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) { const { t } = useTranslation(); - + const [parameters, setParameters] = useState({}); const [isValid, setIsValid] = useState(true); - const [SettingsComponent, setSettingsComponent] = useState | null>(null); - const [parameterHook, setParameterHook] = useState(null); - const [loading, setLoading] = useState(true); - // Dynamically load the settings component and parameter hook based on tool - useEffect(() => { - const loadToolComponents = async () => { - setLoading(true); - - try { - let settingsModule, parameterModule; - - switch (tool.operation) { - case 'compress': - [settingsModule, parameterModule] = await Promise.all([ - import('../compress/CompressSettings'), - import('../../../hooks/tools/compress/useCompressParameters') - ]); - break; - - case 'split': - [settingsModule, parameterModule] = await Promise.all([ - import('../split/SplitSettings'), - import('../../../hooks/tools/split/useSplitParameters') - ]); - break; - - case 'addPassword': - [settingsModule, parameterModule] = await Promise.all([ - import('../addPassword/AddPasswordSettings'), - import('../../../hooks/tools/addPassword/useAddPasswordParameters') - ]); - break; - - case 'removePassword': - [settingsModule, parameterModule] = await Promise.all([ - import('../removePassword/RemovePasswordSettings'), - import('../../../hooks/tools/removePassword/useRemovePasswordParameters') - ]); - break; - - case 'changePermissions': - [settingsModule, parameterModule] = await Promise.all([ - import('../changePermissions/ChangePermissionsSettings'), - import('../../../hooks/tools/changePermissions/useChangePermissionsParameters') - ]); - break; - - case 'sanitize': - [settingsModule, parameterModule] = await Promise.all([ - import('../sanitize/SanitizeSettings'), - import('../../../hooks/tools/sanitize/useSanitizeParameters') - ]); - break; - - case 'ocr': - [settingsModule, parameterModule] = await Promise.all([ - import('../ocr/OCRSettings'), - import('../../../hooks/tools/ocr/useOCRParameters') - ]); - break; - - case 'convert': - [settingsModule, parameterModule] = await Promise.all([ - import('../convert/ConvertSettings'), - import('../../../hooks/tools/convert/useConvertParameters') - ]); - break; - - default: - setSettingsComponent(null); - setParameterHook(null); - setLoading(false); - return; - } - - setSettingsComponent(() => settingsModule.default); - setParameterHook(() => parameterModule); - } catch (error) { - console.error(`Error loading components for ${tool.operation}:`, error); - setSettingsComponent(null); - setParameterHook(null); - } - - setLoading(false); - }; - - if (opened && tool.operation) { - loadToolComponents(); - } - }, [opened, tool.operation]); + // Get tool info from registry + const toolInfo = toolRegistry[tool.operation]; + const SettingsComponent = toolInfo?.settingsComponent; - // Initialize parameters from tool or use defaults from hook + // Initialize parameters from tool (which should contain defaults from registry) useEffect(() => { if (tool.parameters) { setParameters(tool.parameters); - } else if (parameterHook) { - // If we have a parameter module, use its default parameters - try { - const defaultParams = parameterHook.defaultParameters || {}; - setParameters(defaultParams); - } catch (error) { - console.warn(`Error getting default parameters for ${tool.operation}:`, error); - setParameters({}); - } } else { + // Fallback to empty parameters if none provided setParameters({}); } - }, [tool.parameters, parameterHook, tool.operation]); + }, [tool.parameters, tool.operation]); // Render the settings component const renderToolSettings = () => { - if (loading) { - return ( - - - - {t('automate.config.loading', 'Loading tool configuration...')} - - - ); - } - if (!SettingsComponent) { return ( } color="orange"> @@ -224,4 +119,4 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel ); -} \ No newline at end of file +} diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index 87ec6d2cd..ed7cee091 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -1,7 +1,8 @@ import { type TFunction } from 'i18next'; import React from 'react'; -import { ToolOperationHook } from '../hooks/tools/shared/useToolOperation'; +import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; import { BaseToolProps } from '../types/tool'; +import { BaseParameters } from '../types/parameters'; export enum SubcategoryId { SIGNING = 'signing', @@ -36,8 +37,10 @@ export type ToolRegistryEntry = { endpoints?: string[]; link?: string; type?: string; - // Hook for automation execution - operationHook?: () => ToolOperationHook; + // Operation configuration for automation + operationConfig?: ToolOperationConfig; + // Settings component for automation configuration + settingsComponent?: React.ComponentType; } export type ToolRegistry = Record; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index f02914ee9..769e2cbd1 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -14,6 +14,12 @@ import Repair from '../tools/Repair'; import SingleLargePage from '../tools/SingleLargePage'; import UnlockPdfForms from '../tools/UnlockPdfForms'; import RemoveCertificateSign from '../tools/RemoveCertificateSign'; +import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation'; +import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation'; +import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation'; +import CompressSettings from '../components/tools/compress/CompressSettings'; +import SplitSettings from '../components/tools/split/SplitSettings'; +import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings'; const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented @@ -56,7 +62,9 @@ export function useFlatToolRegistry(): ToolRegistry { category: ToolCategory.STANDARD_TOOLS, subcategory: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, - endpoints: ["add-password"] + endpoints: ["add-password"], + operationConfig: addPasswordOperationConfig, + settingsComponent: AddPasswordSettings }, "watermark": { icon: branding_watermark, @@ -198,7 +206,9 @@ export function useFlatToolRegistry(): ToolRegistry { view: "split", description: t("home.split.desc", "Split PDFs into multiple documents"), category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + subcategory: SubcategoryId.PAGE_FORMATTING, + operationConfig: splitOperationConfig, + settingsComponent: SplitSettings }, "reorganize-pages": { icon: move_down, @@ -534,7 +544,9 @@ export function useFlatToolRegistry(): ToolRegistry { description: t("home.compress.desc", "Compress PDFs to reduce their file size."), category: ToolCategory.RECOMMENDED_TOOLS, subcategory: SubcategoryId.GENERAL, - maxFiles: -1 + maxFiles: -1, + operationConfig: compressOperationConfig, + settingsComponent: CompressSettings }, "convert": { icon: sync_alt, diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts index d94b9650e..bd2f2176c 100644 --- a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts +++ b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts @@ -1,30 +1,45 @@ import { useTranslation } from 'react-i18next'; import { useToolOperation } from '../shared/useToolOperation'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; -import { AddPasswordFullParameters } from './useAddPasswordParameters'; +import { AddPasswordFullParameters, defaultParameters } from './useAddPasswordParameters'; +import { defaultParameters as permissionsDefaults } from '../changePermissions/useChangePermissionsParameters'; import { getFormData } from '../changePermissions/useChangePermissionsOperation'; +// Static function that can be used by both the hook and automation executor +export const buildAddPasswordFormData = (parameters: AddPasswordFullParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + formData.append("password", parameters.password); + formData.append("ownerPassword", parameters.ownerPassword); + formData.append("keyLength", parameters.keyLength.toString()); + getFormData(parameters.permissions).forEach(([key, value]) => { + formData.append(key, value); + }); + return formData; +}; + +// Full default parameters including permissions for automation +const fullDefaultParameters: AddPasswordFullParameters = { + ...defaultParameters, + permissions: permissionsDefaults, +}; + +// Static configuration object +export const addPasswordOperationConfig = { + operationType: 'addPassword', + endpoint: '/api/v1/security/add-password', + buildFormData: buildAddPasswordFormData, + filePrefix: 'encrypted_', // Will be overridden in hook with translation + multiFileEndpoint: false, + defaultParameters: fullDefaultParameters, +} as const; + export const useAddPasswordOperation = () => { const { t } = useTranslation(); - const buildFormData = (parameters: AddPasswordFullParameters, file: File): FormData => { - const formData = new FormData(); - formData.append("fileInput", file); - formData.append("password", parameters.password); - formData.append("ownerPassword", parameters.ownerPassword); - formData.append("keyLength", parameters.keyLength.toString()); - getFormData(parameters.permissions).forEach(([key, value]) => { - formData.append(key, value); - }); - return formData; - }; - return useToolOperation({ - operationType: 'addPassword', - endpoint: '/api/v1/security/add-password', - buildFormData, + ...addPasswordOperationConfig, filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_', - multiFileEndpoint: false, getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.')) }); }; diff --git a/frontend/src/hooks/tools/automate/useAutomateOperation.ts b/frontend/src/hooks/tools/automate/useAutomateOperation.ts index 4e3a16fe1..acf28cb86 100644 --- a/frontend/src/hooks/tools/automate/useAutomateOperation.ts +++ b/frontend/src/hooks/tools/automate/useAutomateOperation.ts @@ -1,12 +1,18 @@ import { useToolOperation } from '../shared/useToolOperation'; import { useCallback } from 'react'; import { executeAutomationSequence } from '../../../utils/automationExecutor'; +import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; interface AutomateParameters { automationConfig?: any; + onStepStart?: (stepIndex: number, operationName: string) => void; + onStepComplete?: (stepIndex: number, resultFiles: File[]) => void; + onStepError?: (stepIndex: number, error: string) => void; } export function useAutomateOperation() { + const toolRegistry = useFlatToolRegistry(); + const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => { console.log('🚀 Starting automation execution via customProcessor', { params, files }); @@ -18,21 +24,25 @@ export function useAutomateOperation() { const finalResults = await executeAutomationSequence( params.automationConfig, files, + toolRegistry, (stepIndex: number, operationName: string) => { console.log(`Step ${stepIndex + 1} started: ${operationName}`); + params.onStepStart?.(stepIndex, operationName); }, (stepIndex: number, resultFiles: File[]) => { console.log(`Step ${stepIndex + 1} completed with ${resultFiles.length} files`); + params.onStepComplete?.(stepIndex, resultFiles); }, (stepIndex: number, error: string) => { console.error(`Step ${stepIndex + 1} failed:`, error); + params.onStepError?.(stepIndex, error); throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`); } ); console.log(`✅ Automation completed, returning ${finalResults.length} files`); return finalResults; - }, []); + }, [toolRegistry]); return useToolOperation({ operationType: 'automate', diff --git a/frontend/src/hooks/tools/automate/useAutomationExecution.ts b/frontend/src/hooks/tools/automate/useAutomationExecution.ts deleted file mode 100644 index 5b7e0f903..000000000 --- a/frontend/src/hooks/tools/automate/useAutomationExecution.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; -import { ToolComponent } from '../../../types/tool'; - -interface ExecutionStep { - id: string; - operation: string; - name: string; - status: 'pending' | 'running' | 'completed' | 'error'; - error?: string; - parameters?: any; -} - -interface AutomationExecutionState { - isExecuting: boolean; - currentStepIndex: number; - executionSteps: ExecutionStep[]; - currentFiles: File[]; -} - -/** - * Hook for managing automation execution with real tool operations - */ -export const useAutomationExecution = () => { - const toolRegistry = useFlatToolRegistry(); - const [state, setState] = useState({ - isExecuting: false, - currentStepIndex: -1, - executionSteps: [], - currentFiles: [] - }); - - // Store operation hook instances for the current automation - const [operationHooks, setOperationHooks] = useState>({}); - - const initializeAutomation = useCallback((automation: any, initialFiles: File[]) => { - if (!automation?.operations) return; - - const steps = automation.operations.map((op: any, index: number) => { - const tool = toolRegistry[op.operation]; - return { - id: `${op.operation}-${index}`, - operation: op.operation, - name: tool?.name || op.operation, - status: 'pending' as const, - parameters: op.parameters || {} - }; - }); - - // Initialize operation hooks for all tools in the automation - const hooks: Record = {}; - steps.forEach((step: ExecutionStep) => { - const tool = toolRegistry[step.operation]; - if (tool?.component) { - const toolComponent = tool.component as ToolComponent; - if (toolComponent.tool) { - const hookFactory = toolComponent.tool(); - // We still can't call hooks here - this approach won't work - } - } - }); - - setState({ - isExecuting: false, - currentStepIndex: -1, - executionSteps: steps, - currentFiles: [...initialFiles] - }); - }, [toolRegistry]); - - const executeAutomation = useCallback(async () => { - if (state.executionSteps.length === 0 || state.currentFiles.length === 0) { - throw new Error('No steps or files to execute'); - } - - setState(prev => ({ ...prev, isExecuting: true, currentStepIndex: 0 })); - let filesToProcess = [...state.currentFiles]; - - try { - for (let i = 0; i < state.executionSteps.length; i++) { - setState(prev => ({ ...prev, currentStepIndex: i })); - const step = state.executionSteps[i]; - - // Update step status to running - setState(prev => ({ - ...prev, - executionSteps: prev.executionSteps.map((s, idx) => - idx === i ? { ...s, status: 'running' } : s - ) - })); - - // Get the tool and validate it supports automation - const tool = toolRegistry[step.operation]; - if (!tool?.component) { - throw new Error(`Tool not found: ${step.operation}`); - } - - const toolComponent = tool.component as ToolComponent; - if (!toolComponent.tool) { - throw new Error(`Tool ${step.operation} does not support automation`); - } - - // For now, simulate execution until we solve the hook problem - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Update step status to completed - setState(prev => ({ - ...prev, - executionSteps: prev.executionSteps.map((s, idx) => - idx === i ? { ...s, status: 'completed' } : s - ) - })); - - // TODO: Update filesToProcess with actual results - } - - setState(prev => ({ - ...prev, - isExecuting: false, - currentStepIndex: -1, - currentFiles: filesToProcess - })); - - } catch (error: any) { - console.error('Automation execution failed:', error); - setState(prev => ({ - ...prev, - isExecuting: false, - executionSteps: prev.executionSteps.map((s, idx) => - idx === prev.currentStepIndex ? { ...s, status: 'error', error: error.message } : s - ) - })); - } - }, [state.executionSteps, state.currentFiles, toolRegistry]); - - return { - ...state, - initializeAutomation, - executeAutomation - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/compress/useCompressOperation.ts b/frontend/src/hooks/tools/compress/useCompressOperation.ts index c93d4b9e7..08d73859e 100644 --- a/frontend/src/hooks/tools/compress/useCompressOperation.ts +++ b/frontend/src/hooks/tools/compress/useCompressOperation.ts @@ -1,9 +1,10 @@ import { useTranslation } from 'react-i18next'; -import { useToolOperation } from '../shared/useToolOperation'; +import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; -import { CompressParameters } from './useCompressParameters'; +import { CompressParameters, defaultParameters } from './useCompressParameters'; -const buildFormData = (parameters: CompressParameters, file: File): FormData => { +// Static configuration that can be used by both the hook and automation executor +export const buildCompressFormData = (parameters: CompressParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); @@ -21,15 +22,21 @@ const buildFormData = (parameters: CompressParameters, file: File): FormData => return formData; }; +// Static configuration object +export const compressOperationConfig = { + operationType: 'compress', + endpoint: '/api/v1/misc/compress-pdf', + buildFormData: buildCompressFormData, + filePrefix: 'compressed_', + multiFileEndpoint: false, // Individual API calls per file + defaultParameters, +} as const; + export const useCompressOperation = () => { const { t } = useTranslation(); return useToolOperation({ - operationType: 'compress', - endpoint: '/api/v1/misc/compress-pdf', - buildFormData, - filePrefix: 'compressed_', - multiFileEndpoint: false, // Individual API calls per file + ...compressOperationConfig, getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.')) }); }; diff --git a/frontend/src/hooks/tools/compress/useCompressParameters.ts b/frontend/src/hooks/tools/compress/useCompressParameters.ts index b423bdbec..d551dc292 100644 --- a/frontend/src/hooks/tools/compress/useCompressParameters.ts +++ b/frontend/src/hooks/tools/compress/useCompressParameters.ts @@ -10,7 +10,7 @@ export interface CompressParameters extends BaseParameters { fileSizeUnit: 'KB' | 'MB'; } -const defaultParameters: CompressParameters = { +export const defaultParameters: CompressParameters = { compressionLevel: 5, grayscale: false, expectedSize: '', diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index 623b93d84..46645dd1a 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -61,6 +61,9 @@ export interface ToolOperationConfig { /** Extract user-friendly error messages from API errors */ getErrorMessage?: (error: any) => string; + + /** Default parameter values for automation */ + defaultParameters?: TParams; } /** diff --git a/frontend/src/hooks/tools/split/useSplitOperation.ts b/frontend/src/hooks/tools/split/useSplitOperation.ts index 7702bffad..cc1c6a5d9 100644 --- a/frontend/src/hooks/tools/split/useSplitOperation.ts +++ b/frontend/src/hooks/tools/split/useSplitOperation.ts @@ -1,11 +1,11 @@ import { useTranslation } from 'react-i18next'; import { useToolOperation } from '../shared/useToolOperation'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; -import { SplitParameters } from './useSplitParameters'; +import { SplitParameters, defaultParameters } from './useSplitParameters'; import { SPLIT_MODES } from '../../../constants/splitConstants'; - -const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => { +// Static functions that can be used by both the hook and automation executor +export const buildSplitFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => { const formData = new FormData(); selectedFiles.forEach(file => { @@ -40,7 +40,7 @@ const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): Form return formData; }; -const getEndpoint = (parameters: SplitParameters): string => { +export const getSplitEndpoint = (parameters: SplitParameters): string => { switch (parameters.mode) { case SPLIT_MODES.BY_PAGES: return "/api/v1/general/split-pages"; @@ -55,15 +55,21 @@ const getEndpoint = (parameters: SplitParameters): string => { } }; +// Static configuration object +export const splitOperationConfig = { + operationType: 'splitPdf', + endpoint: getSplitEndpoint, + buildFormData: buildSplitFormData, + filePrefix: 'split_', + multiFileEndpoint: true, // Single API call with all files + defaultParameters, +} as const; + export const useSplitOperation = () => { const { t } = useTranslation(); return useToolOperation({ - operationType: 'split', - endpoint: (params) => getEndpoint(params), - buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData - filePrefix: 'split_', - multiFileEndpoint: true, // Single API call with all files + ...splitOperationConfig, getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.')) }); }; diff --git a/frontend/src/hooks/tools/split/useSplitParameters.ts b/frontend/src/hooks/tools/split/useSplitParameters.ts index b31ddbae6..e48504304 100644 --- a/frontend/src/hooks/tools/split/useSplitParameters.ts +++ b/frontend/src/hooks/tools/split/useSplitParameters.ts @@ -17,7 +17,7 @@ export interface SplitParameters extends BaseParameters { export type SplitParametersHook = BaseParametersHook; -const defaultParameters: SplitParameters = { +export const defaultParameters: SplitParameters = { mode: '', pages: '', hDiv: '2', diff --git a/frontend/src/tools/AddPassword.tsx b/frontend/src/tools/AddPassword.tsx index 56a69ce60..846b487c0 100644 --- a/frontend/src/tools/AddPassword.tsx +++ b/frontend/src/tools/AddPassword.tsx @@ -114,12 +114,4 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -// Static method to get the operation hook for automation -AddPassword.tool = () => useAddPasswordOperation; - -// Static method to get default parameters for automation -AddPassword.getDefaultParameters = () => { - return defaultParameters; -}; - export default AddPassword as ToolComponent; diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index fd80aebb1..8983e0652 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -25,14 +25,28 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const automateOperation = useAutomateOperation(); const toolRegistry = useFlatToolRegistry(); const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null; - const { savedAutomations, deleteAutomation } = useSavedAutomations(); + const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations(); const handleStepChange = (data: any) => { + // If navigating away from run step, reset automation results + if (currentStep === 'run' && data.step !== 'run') { + automateOperation.resetResults(); + } + + // If navigating to run step with a different automation, reset results + if (data.step === 'run' && data.automation && + stepData.automation && data.automation.id !== stepData.automation.id) { + automateOperation.resetResults(); + } + setStepData(data); setCurrentStep(data.step); }; const handleComplete = () => { + // Reset automation results when completing + automateOperation.resetResults(); + // Reset to selection step setCurrentStep('selection'); setStepData({}); @@ -65,7 +79,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { mode={stepData.mode} existingAutomation={stepData.automation} onBack={() => handleStepChange({ step: 'selection' })} - onComplete={() => handleStepChange({ step: 'selection' })} + onComplete={() => { + refreshAutomations(); + handleStepChange({ step: 'selection' }); + }} toolRegistry={toolRegistry} /> ); diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index d4106a48b..780102976 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -95,10 +95,5 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -// Static method to get the operation hook for automation -Compress.tool = () => useCompressOperation; - -// Static method to get default parameters for automation -Compress.getDefaultParameters = () => useCompressParameters(); export default Compress as ToolComponent; diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 5b6f8733a..5e16c778e 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -92,7 +92,4 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -// Static method to get the operation hook for automation -Split.tool = () => useSplitOperation; - export default Split as ToolComponent; diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts index 523ffac00..473f775a0 100644 --- a/frontend/src/utils/automationExecutor.ts +++ b/frontend/src/utils/automationExecutor.ts @@ -1,76 +1,31 @@ import axios from 'axios'; - -// Tool operation configurations extracted from the hook implementations -const TOOL_CONFIGS: Record = { - 'compressPdfs': { - endpoint: '/api/v1/misc/compress-pdf', - multiFileEndpoint: false, - buildFormData: (parameters: any, file: File): FormData => { - const formData = new FormData(); - formData.append("fileInput", file); - - if (parameters.compressionMethod === 'quality') { - formData.append("optimizeLevel", parameters.compressionLevel?.toString() || '1'); - } else { - const fileSize = parameters.fileSizeValue ? `${parameters.fileSizeValue}${parameters.fileSizeUnit}` : ''; - if (fileSize) { - formData.append("expectedOutputSize", fileSize); - } - } - - formData.append("grayscale", parameters.grayscale?.toString() || 'false'); - return formData; - } - }, - - 'split': { - endpoint: (parameters: any): string => { - // Simplified endpoint selection - you'd need the full logic from useSplitOperation - return "/api/v1/general/split-pages"; - }, - multiFileEndpoint: true, - buildFormData: (parameters: any, files: File[]): FormData => { - const formData = new FormData(); - files.forEach(file => { - formData.append("fileInput", file); - }); - - // Add split parameters - simplified version - if (parameters.pages) { - formData.append("pageNumbers", parameters.pages); - } - - return formData; - } - }, - - 'addPassword': { - endpoint: '/api/v1/security/add-password', - multiFileEndpoint: false, - buildFormData: (parameters: any, file: File): FormData => { - const formData = new FormData(); - formData.append("fileInput", file); - - if (parameters.password) { - formData.append("password", parameters.password); - } - - // Add other password parameters as needed - return formData; - } - } - - // TODO: Add configurations for other tools -}; +import { ToolRegistry } from '../data/toolsTaxonomy'; +import { zipFileService } from '../services/zipFileService'; /** * Extract zip files from response blob */ const extractZipFiles = async (blob: Blob): Promise => { - // This would need the actual zip extraction logic from the codebase - // For now, create a single file from the blob - const file = new File([blob], `result_${Date.now()}.pdf`, { type: 'application/pdf' }); - return [file]; + try { + // Convert blob to File for the zip service + const zipFile = new File([blob], `response_${Date.now()}.zip`, { type: 'application/zip' }); + + // Extract PDF files from the ZIP + const result = await zipFileService.extractPdfFiles(zipFile); + + if (!result.success || result.extractedFiles.length === 0) { + console.error('ZIP extraction failed:', result.errors); + throw new Error(`ZIP extraction failed: ${result.errors.join(', ')}`); + } + + console.log(`📦 Extracted ${result.extractedFiles.length} files from ZIP`); + return result.extractedFiles; + } catch (error) { + console.error('Failed to extract ZIP files:', error); + // Fallback: treat as single PDF file + const file = new File([blob], `result_${Date.now()}.pdf`, { type: 'application/pdf' }); + return [file]; + } }; /** @@ -79,11 +34,12 @@ const extractZipFiles = async (blob: Blob): Promise => { export const executeToolOperation = async ( operationName: string, parameters: any, - files: File[] + files: File[], + toolRegistry: ToolRegistry ): Promise => { console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length }); - const config = TOOL_CONFIGS[operationName]; + const config = toolRegistry[operationName]?.operationConfig; if (!config) { console.error(`❌ Tool operation not supported: ${operationName}`); throw new Error(`Tool operation not supported: ${operationName}`); @@ -99,7 +55,7 @@ export const executeToolOperation = async ( : config.endpoint; console.log(`🌐 Making multi-file request to: ${endpoint}`); - const formData = config.buildFormData(parameters, files); + const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files); console.log(`📤 FormData entries:`, Array.from(formData.entries())); const response = await axios.post(endpoint, formData, { @@ -126,7 +82,7 @@ export const executeToolOperation = async ( : config.endpoint; console.log(`🌐 Making single-file request ${i+1}/${files.length} to: ${endpoint} for file: ${file.name}`); - const formData = config.buildFormData(parameters, file); + const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file); console.log(`📤 FormData entries:`, Array.from(formData.entries())); const response = await axios.post(endpoint, formData, { @@ -162,6 +118,7 @@ export const executeToolOperation = async ( export const executeAutomationSequence = async ( automation: any, initialFiles: File[], + toolRegistry: ToolRegistry, onStepStart?: (stepIndex: number, operationName: string) => void, onStepComplete?: (stepIndex: number, resultFiles: File[]) => void, onStepError?: (stepIndex: number, error: string) => void @@ -189,7 +146,8 @@ export const executeAutomationSequence = async ( const resultFiles = await executeToolOperation( operation.operation, operation.parameters || {}, - currentFiles + currentFiles, + toolRegistry ); console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`);