Thursday demo

This commit is contained in:
Connor Yoh 2025-08-21 17:25:55 +01:00
parent 4956d6b4da
commit 85caad5f5c
19 changed files with 258 additions and 570 deletions

View File

@ -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}
/>
)}

View File

@ -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<AutomationExecutorProps> = ({
automation,
files,
onStepStart,
onStepComplete,
onStepError,
onComplete,
shouldExecute
}) => {
const toolRegistry = useFlatToolRegistry();
const [currentStepIndex, setCurrentStepIndex] = useState(-1);
const [currentFiles, setCurrentFiles] = useState<File[]>(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<string, any> = {};
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;
};

View File

@ -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,10 +24,15 @@ 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<ExecutionStep[]>([]);
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(() => {
if (automation?.operations) {
@ -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 <CheckIcon style={{ fontSize: 16, color: "green" }} />;
case "error":
return <ErrorIcon style={{ fontSize: 16, color: "red" }} />;
case "running":
return (
<div
style={{
width: 16,
height: 16,
border: "2px solid #ccc",
borderTop: "2px solid #007bff",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
);
default:
return <div style={{ width: 16, height: 16, border: "2px solid #ccc", borderRadius: "50%" }} />;
}
};
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 <CheckIcon style={{ fontSize: 16, color: 'green' }} />;
case 'error':
return <span style={{ fontSize: 16, color: 'red' }}></span>;
case 'running':
return <div style={{
width: 16,
height: 16,
border: '2px solid #ccc',
borderTop: '2px solid #007bff',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />;
default:
return <div style={{
width: 16,
height: 16,
border: '2px solid #ccc',
borderRadius: '50%'
}} />;
}
};
return (
<div>
@ -128,10 +155,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
{isExecuting && (
<div>
<Text size="sm" mb="xs">
{t("automate.sequence.progress", "Progress: {{current}}/{{total}}", {
current: currentStepIndex + 1,
total: executionSteps.length,
})}
Progress: {currentStepIndex + 1}/{executionSteps.length}
</Text>
<Progress value={getProgress()} size="lg" />
</div>
@ -151,8 +175,8 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
<Text
size="sm"
style={{
color: step.status === "running" ? "var(--mantine-color-blue-6)" : "var(--mantine-color-text)",
fontWeight: step.status === "running" ? 500 : 400,
color: step.status === 'running' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
fontWeight: step.status === 'running' ? 500 : 400
}}
>
{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")}
</Button>
{hasResults && (
<Button variant="light" onClick={onComplete}>
{t("automate.sequence.finish", "Finish")}
</Button>
)}
</Group>
</Stack>

View File

@ -7,13 +7,13 @@ import {
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<any>({});
const [isValid, setIsValid] = useState(true);
const [SettingsComponent, setSettingsComponent] = useState<React.ComponentType<any> | null>(null);
const [parameterHook, setParameterHook] = useState<any>(null);
const [loading, setLoading] = useState(true);
// Dynamically load the settings component and parameter hook based on tool
useEffect(() => {
const loadToolComponents = async () => {
setLoading(true);
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation];
const SettingsComponent = toolInfo?.settingsComponent;
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]);
// 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 (
<Stack align="center" gap="md" py="xl">
<Loader size="md" />
<Text size="sm" c="dimmed">
{t('automate.config.loading', 'Loading tool configuration...')}
</Text>
</Stack>
);
}
if (!SettingsComponent) {
return (
<Alert icon={<WarningIcon />} color="orange">

View File

@ -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<any>;
// Operation configuration for automation
operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration
settingsComponent?: React.ComponentType<any>;
}
export type ToolRegistry = Record<string, ToolRegistryEntry>;

View File

@ -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: <span className="material-symbols-rounded">branding_watermark</span>,
@ -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: <span className="material-symbols-rounded">move_down</span>,
@ -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: <span className="material-symbols-rounded">sync_alt</span>,

View File

@ -1,13 +1,12 @@
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';
export const useAddPasswordOperation = () => {
const { t } = useTranslation();
const buildFormData = (parameters: AddPasswordFullParameters, file: File): FormData => {
// 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);
@ -17,14 +16,30 @@ export const useAddPasswordOperation = () => {
formData.append(key, value);
});
return formData;
};
};
return useToolOperation<AddPasswordFullParameters>({
// 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,
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
buildFormData: buildAddPasswordFormData,
filePrefix: 'encrypted_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters: fullDefaultParameters,
} as const;
export const useAddPasswordOperation = () => {
const { t } = useTranslation();
return useToolOperation<AddPasswordFullParameters>({
...addPasswordOperationConfig,
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
});
};

View File

@ -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<AutomateParameters>({
operationType: 'automate',

View File

@ -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<AutomationExecutionState>({
isExecuting: false,
currentStepIndex: -1,
executionSteps: [],
currentFiles: []
});
// Store operation hook instances for the current automation
const [operationHooks, setOperationHooks] = useState<Record<string, any>>({});
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<string, any> = {};
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
};
};

View File

@ -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<CompressParameters>({
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.'))
});
};

View File

@ -10,7 +10,7 @@ export interface CompressParameters extends BaseParameters {
fileSizeUnit: 'KB' | 'MB';
}
const defaultParameters: CompressParameters = {
export const defaultParameters: CompressParameters = {
compressionLevel: 5,
grayscale: false,
expectedSize: '',

View File

@ -61,6 +61,9 @@ export interface ToolOperationConfig<TParams = void> {
/** Extract user-friendly error messages from API errors */
getErrorMessage?: (error: any) => string;
/** Default parameter values for automation */
defaultParameters?: TParams;
}
/**

View File

@ -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<SplitParameters>({
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.'))
});
};

View File

@ -17,7 +17,7 @@ export interface SplitParameters extends BaseParameters {
export type SplitParametersHook = BaseParametersHook<SplitParameters>;
const defaultParameters: SplitParameters = {
export const defaultParameters: SplitParameters = {
mode: '',
pages: '',
hDiv: '2',

View File

@ -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;

View File

@ -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}
/>
);

View File

@ -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;

View File

@ -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;

View File

@ -1,76 +1,31 @@
import axios from 'axios';
// Tool operation configurations extracted from the hook implementations
const TOOL_CONFIGS: Record<string, any> = {
'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<File[]> => {
// This would need the actual zip extraction logic from the codebase
// For now, create a single file from the blob
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<File[]> => {
export const executeToolOperation = async (
operationName: string,
parameters: any,
files: File[]
files: File[],
toolRegistry: ToolRegistry
): Promise<File[]> => {
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`);