diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index d169d160a..3bd89be05 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -257,12 +257,24 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o key={`tool-selector-${tool.id}`} onSelect={(newOperation) => { 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); + } + } + updatedTools[index] = { ...updatedTools[index], operation: newOperation, name: getToolName(newOperation), configured: false, - parameters: {} + parameters: defaultParams }; setSelectedTools(updatedTools); }} diff --git a/frontend/src/components/tools/automate/AutomationExecutor.tsx b/frontend/src/components/tools/automate/AutomationExecutor.tsx new file mode 100644 index 000000000..1f3c7d601 --- /dev/null +++ b/frontend/src/components/tools/automate/AutomationExecutor.tsx @@ -0,0 +1,112 @@ +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 (toolComponent.tool) { + // 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 (!toolComponent.tool) { + 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 27ee28e7b..29e875700 100644 --- a/frontend/src/components/tools/automate/AutomationRun.tsx +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -16,11 +16,14 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import CheckIcon from '@mui/icons-material/Check'; import ErrorIcon from '@mui/icons-material/Error'; import { useFileContext } from '../../../contexts/FileContext'; +import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; +import { executeAutomationSequence } from '../../../utils/automationExecutor'; interface AutomationRunProps { automation: any; onBack: () => void; onComplete: () => void; + automateOperation?: any; // Add the operation hook to store results } interface ExecutionStep { @@ -31,25 +34,34 @@ interface ExecutionStep { error?: string; } -export default function AutomationRun({ automation, onBack, onComplete }: AutomationRunProps) { +export default function AutomationRun({ automation, onBack, onComplete, automateOperation }: AutomationRunProps) { const { t } = useTranslation(); - const { activeFiles } = useFileContext(); + const { activeFiles, consumeFiles } = useFileContext(); + const toolRegistry = useFlatToolRegistry(); const [isExecuting, setIsExecuting] = useState(false); const [executionSteps, setExecutionSteps] = useState([]); const [currentStepIndex, setCurrentStepIndex] = useState(-1); + const [currentFiles, setCurrentFiles] = useState([]); // Initialize execution steps from automation React.useEffect(() => { if (automation?.operations) { - const steps = automation.operations.map((op: any, index: number) => ({ - id: `${op.operation}-${index}`, - operation: op.operation, - name: op.operation, // You might want to get the display name from tool registry - status: 'pending' as const - })); + 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 + }; + }); setExecutionSteps(steps); } - }, [automation]); + // Initialize current files with active files + if (activeFiles) { + setCurrentFiles([...activeFiles]); + } + }, [automation, toolRegistry, activeFiles]); const executeAutomation = async () => { if (!activeFiles || activeFiles.length === 0) { @@ -61,33 +73,52 @@ export default function AutomationRun({ automation, onBack, onComplete }: Automa setCurrentStepIndex(0); try { - for (let i = 0; i < executionSteps.length; i++) { - setCurrentStepIndex(i); - - // Update step status to running - setExecutionSteps(prev => prev.map((step, idx) => - idx === i ? { ...step, status: 'running' } : step - )); - - // Simulate step execution (replace with actual tool execution) - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Update step status to completed - setExecutionSteps(prev => prev.map((step, idx) => - idx === i ? { ...step, status: 'completed' } : step - )); - } + // Execute the automation sequence using the new executor + const finalResults = await executeAutomationSequence( + automation, + activeFiles, + (stepIndex: number, operationName: string) => { + // Step started + setCurrentStepIndex(stepIndex); + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'running' } : step + )); + }, + (stepIndex: number, resultFiles: File[]) => { + // Step completed + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'completed' } : step + )); + setCurrentFiles(resultFiles); + }, + (stepIndex: number, error: string) => { + // Step failed + setExecutionSteps(prev => prev.map((step, idx) => + idx === stepIndex ? { ...step, status: 'error', error } : step + )); + } + ); + // All steps completed successfully setCurrentStepIndex(-1); setIsExecuting(false); + setCurrentFiles(finalResults); + + // Properly integrate results with FileContext + if (finalResults.length > 0) { + console.log(`🎨 Integrating ${finalResults.length} result files with FileContext`); + + // Use FileContext's consumeFiles to properly add results + // This replaces input files with output files (like other tools do) + await consumeFiles(activeFiles, finalResults); + + console.log(`✅ Successfully integrated automation results with FileContext`); + } - // All steps completed - show success - } catch (error) { - // Handle error - setExecutionSteps(prev => prev.map((step, idx) => - idx === currentStepIndex ? { ...step, status: 'error', error: error?.toString() } : step - )); + } catch (error: any) { + console.error('Automation execution failed:', error); setIsExecuting(false); + setCurrentStepIndex(-1); } }; diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index cb6b18c0d..ee46cf1a6 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -1,5 +1,6 @@ import { type TFunction } from 'i18next'; import React from 'react'; +import { ToolOperationHook } from '../hooks/tools/shared/useToolOperation'; export enum SubcategoryId { SIGNING = 'signing', @@ -34,6 +35,8 @@ export type ToolRegistryEntry = { endpoints?: string[]; link?: string; type?: string; + // Hook for automation execution + operationHook?: () => ToolOperationHook; } export type ToolRegistry = Record; diff --git a/frontend/src/hooks/tools/automate/useAutomationExecution.ts b/frontend/src/hooks/tools/automate/useAutomationExecution.ts new file mode 100644 index 000000000..9f1a85ae5 --- /dev/null +++ b/frontend/src/hooks/tools/automate/useAutomationExecution.ts @@ -0,0 +1,141 @@ +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) => { + 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/useCompressParameters.ts b/frontend/src/hooks/tools/compress/useCompressParameters.ts index bbd1a0994..8ffea0b64 100644 --- a/frontend/src/hooks/tools/compress/useCompressParameters.ts +++ b/frontend/src/hooks/tools/compress/useCompressParameters.ts @@ -9,7 +9,7 @@ export interface CompressParametersHook { getEndpointName: () => string; } -const initialParameters: CompressParameters = { +export const initialParameters: CompressParameters = { compressionLevel: 5, grayscale: false, expectedSize: '', diff --git a/frontend/src/tools/AddPassword.tsx b/frontend/src/tools/AddPassword.tsx index 20b603044..56a69ce60 100644 --- a/frontend/src/tools/AddPassword.tsx +++ b/frontend/src/tools/AddPassword.tsx @@ -9,11 +9,11 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; -import { useAddPasswordParameters } from "../hooks/tools/addPassword/useAddPasswordParameters"; +import { useAddPasswordParameters, defaultParameters } from "../hooks/tools/addPassword/useAddPasswordParameters"; import { useAddPasswordOperation } from "../hooks/tools/addPassword/useAddPasswordOperation"; import { useAddPasswordTips } from "../components/tooltips/useAddPasswordTips"; import { useAddPasswordPermissionsTips } from "../components/tooltips/useAddPasswordPermissionsTips"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -114,4 +114,12 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default AddPassword; +// 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/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx index 01306e6f0..32960586d 100644 --- a/frontend/src/tools/AddWatermark.tsx +++ b/frontend/src/tools/AddWatermark.tsx @@ -21,7 +21,7 @@ import { useWatermarkFileTips, useWatermarkFormattingTips, } from "../components/tooltips/useWatermarkTips"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -209,4 +209,7 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => }); }; -export default AddWatermark; +// Static method to get the operation hook for automation +AddWatermark.tool = () => useAddWatermarkOperation; + +export default AddWatermark as ToolComponent; diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 737069d96..e1f621815 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -76,6 +76,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { automation={stepData.automation} onBack={() => handleStepChange({ step: 'selection'})} onComplete={handleComplete} + automateOperation={automateOperation} /> ); @@ -103,7 +104,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { isCollapsed: currentStep !== 'selection', onCollapsedClick: () => setCurrentStep('selection') }, currentStep === 'selection' ? renderCurrentStep() : null), - + createStep(stepData.mode === AutomationMode.EDIT ? t('automate.creation.editTitle', 'Edit Automation') : t('automate.creation.createTitle', 'Create Automation'), { @@ -132,7 +133,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }, steps: automationSteps, review: { - isVisible: hasResults, + isVisible: true, operation: automateOperation, title: t('automate.reviewTitle', 'Automation Results') } diff --git a/frontend/src/tools/ChangePermissions.tsx b/frontend/src/tools/ChangePermissions.tsx index 928422059..b64a2afc6 100644 --- a/frontend/src/tools/ChangePermissions.tsx +++ b/frontend/src/tools/ChangePermissions.tsx @@ -11,7 +11,7 @@ import ChangePermissionsSettings from "../components/tools/changePermissions/Cha import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters"; import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -97,4 +97,7 @@ const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps }); }; -export default ChangePermissions; +// Static method to get the operation hook for automation +ChangePermissions.tool = () => useChangePermissionsOperation; + +export default ChangePermissions as ToolComponent; diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index d7e0ebc2c..47c76c4b6 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -8,9 +8,9 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import CompressSettings from "../components/tools/compress/CompressSettings"; -import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters"; +import { useCompressParameters, initialParameters } from "../hooks/tools/compress/useCompressParameters"; import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; import { useCompressTips } from "../components/tooltips/useCompressTips"; const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -95,4 +95,12 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Compress; +// Static method to get the operation hook for automation +Compress.tool = () => useCompressOperation; + +// Static method to get default parameters for automation +Compress.getDefaultParameters = () => { + return initialParameters; +}; + +export default Compress as ToolComponent; diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx index e2616f4a4..a34a0c311 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -10,7 +10,7 @@ import ConvertSettings from "../components/tools/convert/ConvertSettings"; import { useConvertParameters } from "../hooks/tools/convert/useConvertParameters"; import { useConvertOperation } from "../hooks/tools/convert/useConvertOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -133,4 +133,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Convert; +// Static method to get the operation hook for automation +Convert.tool = () => useConvertOperation; + +export default Convert as ToolComponent; diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx index 72fac0b37..34eac5b2a 100644 --- a/frontend/src/tools/OCR.tsx +++ b/frontend/src/tools/OCR.tsx @@ -11,7 +11,7 @@ import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings"; import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters"; import { useOCROperation } from "../hooks/tools/ocr/useOCROperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; import { useOCRTips } from "../components/tooltips/useOCRTips"; const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -136,4 +136,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default OCR; +// Static method to get the operation hook for automation +OCR.tool = () => useOCROperation; + +export default OCR as ToolComponent; diff --git a/frontend/src/tools/RemoveCertificateSign.tsx b/frontend/src/tools/RemoveCertificateSign.tsx index e33675625..290d5da6a 100644 --- a/frontend/src/tools/RemoveCertificateSign.tsx +++ b/frontend/src/tools/RemoveCertificateSign.tsx @@ -8,7 +8,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters"; import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -77,4 +77,7 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP }); }; -export default RemoveCertificateSign; \ No newline at end of file +// Static method to get the operation hook for automation +RemoveCertificateSign.tool = () => useRemoveCertificateSignOperation; + +export default RemoveCertificateSign as ToolComponent; \ No newline at end of file diff --git a/frontend/src/tools/RemovePassword.tsx b/frontend/src/tools/RemovePassword.tsx index 31744186b..2c171d592 100644 --- a/frontend/src/tools/RemovePassword.tsx +++ b/frontend/src/tools/RemovePassword.tsx @@ -11,7 +11,7 @@ import RemovePasswordSettings from "../components/tools/removePassword/RemovePas import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters"; import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation"; import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -95,4 +95,7 @@ const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = }); }; -export default RemovePassword; +// Static method to get the operation hook for automation +RemovePassword.tool = () => useRemovePasswordOperation; + +export default RemovePassword as ToolComponent; diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx index fc30b9b95..00c57bf8a 100644 --- a/frontend/src/tools/Repair.tsx +++ b/frontend/src/tools/Repair.tsx @@ -8,7 +8,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters"; import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -77,4 +77,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Repair; +// Static method to get the operation hook for automation +Repair.tool = () => useRepairOperation; + +export default Repair as ToolComponent; diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx index 258f0f930..806cacd7a 100644 --- a/frontend/src/tools/Sanitize.tsx +++ b/frontend/src/tools/Sanitize.tsx @@ -8,7 +8,7 @@ import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters"; import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; import { useFileContext } from "../contexts/FileContext"; const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -93,4 +93,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Sanitize; +// Static method to get the operation hook for automation +Sanitize.tool = () => useSanitizeOperation; + +export default Sanitize as ToolComponent; diff --git a/frontend/src/tools/SingleLargePage.tsx b/frontend/src/tools/SingleLargePage.tsx index 0c4fb96db..d60ceb4a7 100644 --- a/frontend/src/tools/SingleLargePage.tsx +++ b/frontend/src/tools/SingleLargePage.tsx @@ -8,7 +8,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters"; import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -77,4 +77,7 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) }); }; -export default SingleLargePage; \ No newline at end of file +// Static method to get the operation hook for automation +SingleLargePage.tool = () => useSingleLargePageOperation; + +export default SingleLargePage as ToolComponent; \ No newline at end of file diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index ea68404f0..5b6f8733a 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -9,7 +9,7 @@ import SplitSettings from "../components/tools/split/SplitSettings"; import { useSplitParameters } from "../hooks/tools/split/useSplitParameters"; import { useSplitOperation } from "../hooks/tools/split/useSplitOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -92,4 +92,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Split; +// Static method to get the operation hook for automation +Split.tool = () => useSplitOperation; + +export default Split as ToolComponent; diff --git a/frontend/src/tools/UnlockPdfForms.tsx b/frontend/src/tools/UnlockPdfForms.tsx index b8aee7894..2cf258208 100644 --- a/frontend/src/tools/UnlockPdfForms.tsx +++ b/frontend/src/tools/UnlockPdfForms.tsx @@ -8,7 +8,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters"; import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; -import { BaseToolProps } from "../types/tool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -77,4 +77,7 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = }); }; -export default UnlockPdfForms; \ No newline at end of file +// Static method to get the operation hook for automation +UnlockPdfForms.tool = () => useUnlockPdfFormsOperation; + +export default UnlockPdfForms as ToolComponent; \ No newline at end of file diff --git a/frontend/src/types/tool.ts b/frontend/src/types/tool.ts index e5e8c24e2..366a35a1a 100644 --- a/frontend/src/types/tool.ts +++ b/frontend/src/types/tool.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { ToolOperationHook } from '../hooks/tools/shared/useToolOperation'; export type MaxFiles = number; // 1=single, >1=limited, -1=unlimited export type ToolCategory = 'manipulation' | 'conversion' | 'analysis' | 'utility' | 'optimization' | 'security'; @@ -11,6 +12,29 @@ export interface BaseToolProps { onPreviewFile?: (file: File | null) => void; } +/** + * Interface for tool components that support automation. + * Tools implementing this interface can be used in automation workflows. + */ +export interface AutomationCapableTool { + /** + * Static method that returns the operation hook for this tool. + * This enables automation to execute the tool programmatically. + */ + tool: () => () => ToolOperationHook; + + /** + * Static method that returns the default parameters for this tool. + * This enables automation creation to initialize tools with proper defaults. + */ + getDefaultParameters: () => any; +} + +/** + * Type for tool components that can be used in automation + */ +export type ToolComponent = React.ComponentType & AutomationCapableTool; + export interface ToolStepConfig { type: ToolStepType; title: string; diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts new file mode 100644 index 000000000..523ffac00 --- /dev/null +++ b/frontend/src/utils/automationExecutor.ts @@ -0,0 +1,208 @@ +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 +}; + +/** + * 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]; +}; + +/** + * Execute a tool operation directly without using React hooks + */ +export const executeToolOperation = async ( + operationName: string, + parameters: any, + files: File[] +): Promise => { + console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length }); + + const config = TOOL_CONFIGS[operationName]; + if (!config) { + console.error(`❌ Tool operation not supported: ${operationName}`); + throw new Error(`Tool operation not supported: ${operationName}`); + } + + console.log(`📋 Using config:`, config); + + try { + if (config.multiFileEndpoint) { + // Multi-file processing - single API call with all files + const endpoint = typeof config.endpoint === 'function' + ? config.endpoint(parameters) + : config.endpoint; + + console.log(`🌐 Making multi-file request to: ${endpoint}`); + const formData = config.buildFormData(parameters, files); + console.log(`📤 FormData entries:`, Array.from(formData.entries())); + + const response = await axios.post(endpoint, formData, { + responseType: 'blob', + timeout: 300000 // 5 minute timeout for large files + }); + + console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`); + + // Multi-file responses are typically ZIP files + const resultFiles = await extractZipFiles(response.data); + console.log(`📁 Extracted ${resultFiles.length} files from response`); + return resultFiles; + + } else { + // Single-file processing - separate API call per file + console.log(`🔄 Processing ${files.length} files individually`); + const resultFiles: File[] = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const endpoint = typeof config.endpoint === 'function' + ? config.endpoint(parameters) + : config.endpoint; + + console.log(`🌐 Making single-file request ${i+1}/${files.length} to: ${endpoint} for file: ${file.name}`); + const formData = config.buildFormData(parameters, file); + console.log(`📤 FormData entries:`, Array.from(formData.entries())); + + const response = await axios.post(endpoint, formData, { + responseType: 'blob', + timeout: 300000 // 5 minute timeout for large files + }); + + console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`); + + // Create result file + const resultFile = new File( + [response.data], + `processed_${file.name}`, + { type: 'application/pdf' } + ); + resultFiles.push(resultFile); + console.log(`✅ Created result file: ${resultFile.name}`); + } + + console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`); + return resultFiles; + } + + } catch (error: any) { + console.error(`Tool operation ${operationName} failed:`, error); + throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`); + } +}; + +/** + * Execute an entire automation sequence + */ +export const executeAutomationSequence = async ( + automation: any, + initialFiles: File[], + onStepStart?: (stepIndex: number, operationName: string) => void, + onStepComplete?: (stepIndex: number, resultFiles: File[]) => void, + onStepError?: (stepIndex: number, error: string) => void +): Promise => { + console.log(`🚀 Starting automation sequence: ${automation.name || 'Unnamed'}`); + console.log(`📁 Initial files: ${initialFiles.length}`); + console.log(`🔧 Operations: ${automation.operations?.length || 0}`); + + if (!automation?.operations || automation.operations.length === 0) { + throw new Error('No operations in automation'); + } + + let currentFiles = [...initialFiles]; + + for (let i = 0; i < automation.operations.length; i++) { + const operation = automation.operations[i]; + + console.log(`📋 Step ${i + 1}/${automation.operations.length}: ${operation.operation}`); + console.log(`📄 Input files: ${currentFiles.length}`); + console.log(`⚙️ Parameters:`, operation.parameters || {}); + + try { + onStepStart?.(i, operation.operation); + + const resultFiles = await executeToolOperation( + operation.operation, + operation.parameters || {}, + currentFiles + ); + + console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`); + currentFiles = resultFiles; + onStepComplete?.(i, resultFiles); + + } catch (error: any) { + console.error(`❌ Step ${i + 1} failed:`, error); + onStepError?.(i, error.message); + throw error; + } + } + + console.log(`🎉 Automation sequence completed: ${currentFiles.length} final files`); + return currentFiles; +}; \ No newline at end of file