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); 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 addTool = (operation: string) => {
const newTool: AutomationTool = { const newTool: AutomationTool = {
@ -106,7 +114,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
operation, operation,
name: getToolName(operation), name: getToolName(operation),
configured: false, configured: false,
parameters: {} parameters: getToolDefaultParameters(operation)
}; };
setSelectedTools([...selectedTools, newTool]); setSelectedTools([...selectedTools, newTool]);
@ -259,15 +267,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
const updatedTools = [...selectedTools]; const updatedTools = [...selectedTools];
// Get default parameters from the tool // Get default parameters from the tool
let defaultParams = {}; const defaultParams = getToolDefaultParameters(newOperation);
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] = {
...updatedTools[index], ...updatedTools[index],
@ -370,6 +370,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
tool={currentConfigTool} tool={currentConfigTool}
onSave={handleToolConfigSave} onSave={handleToolConfigSave}
onCancel={handleToolConfigCancel} 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 React, { useState } from "react";
import { useTranslation } from "react-i18next"; 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 PlayArrowIcon from "@mui/icons-material/PlayArrow";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import ErrorIcon from "@mui/icons-material/Error";
import { useFileSelection } from "../../../contexts/FileSelectionContext"; import { useFileSelection } from "../../../contexts/FileSelectionContext";
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry"; import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
import { executeAutomationSequence } from "../../../utils/automationExecutor";
interface AutomationRunProps { interface AutomationRunProps {
automation: any; automation: any;
onComplete: () => void; onComplete: () => void;
automateOperation?: any; // Add the operation hook to store results automateOperation?: any;
} }
interface ExecutionStep { interface ExecutionStep {
id: string; id: string;
operation: string; operation: string;
name: string; name: string;
status: "pending" | "running" | "completed" | "error"; status: 'pending' | 'running' | 'completed' | 'error';
error?: string; error?: string;
} }
@ -26,9 +24,14 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
const { t } = useTranslation(); const { t } = useTranslation();
const { selectedFiles } = useFileSelection(); const { selectedFiles } = useFileSelection();
const toolRegistry = useFlatToolRegistry(); const toolRegistry = useFlatToolRegistry();
const [isExecuting, setIsExecuting] = useState(false);
// Progress tracking state
const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]); const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]);
const [currentStepIndex, setCurrentStepIndex] = useState(-1); 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 // Initialize execution steps from automation
React.useEffect(() => { React.useEffect(() => {
@ -39,16 +42,25 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
id: `${op.operation}-${index}`, id: `${op.operation}-${index}`,
operation: op.operation, operation: op.operation,
name: tool?.name || op.operation, name: tool?.name || op.operation,
status: "pending" as const, status: 'pending' as const
}; };
}); });
setExecutionSteps(steps); 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 () => { const executeAutomation = async () => {
if (!selectedFiles || selectedFiles.length === 0) { if (!selectedFiles || selectedFiles.length === 0) {
// Show error - need files to execute automation
return; return;
} }
@ -57,59 +69,74 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
return; return;
} }
setIsExecuting(true); // Reset progress tracking
setCurrentStepIndex(0); setCurrentStepIndex(0);
setExecutionSteps(prev => prev.map(step => ({ ...step, status: 'pending' as const, error: undefined })));
try { try {
// Use the automateOperation.executeOperation to handle file consumption properly // Use the automateOperation.executeOperation to handle file consumption properly
await automateOperation.executeOperation( 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 selectedFiles
); );
// All steps completed successfully // Mark all as completed and reset current step
setCurrentStepIndex(-1); setCurrentStepIndex(-1);
setIsExecuting(false);
console.log(`✅ Automation completed successfully`); console.log(`✅ Automation completed successfully`);
} catch (error: any) { } catch (error: any) {
console.error("Automation execution failed:", error); console.error("Automation execution failed:", error);
setIsExecuting(false);
setCurrentStepIndex(-1); 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 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; return (completedSteps / executionSteps.length) * 100;
}; };
const allStepsCompleted = executionSteps.every((step) => step.status === "completed"); const getStepIcon = (step: ExecutionStep) => {
const hasErrors = executionSteps.some((step) => step.status === "error"); 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 ( return (
<div> <div>
@ -128,10 +155,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
{isExecuting && ( {isExecuting && (
<div> <div>
<Text size="sm" mb="xs"> <Text size="sm" mb="xs">
{t("automate.sequence.progress", "Progress: {{current}}/{{total}}", { Progress: {currentStepIndex + 1}/{executionSteps.length}
current: currentStepIndex + 1,
total: executionSteps.length,
})}
</Text> </Text>
<Progress value={getProgress()} size="lg" /> <Progress value={getProgress()} size="lg" />
</div> </div>
@ -148,11 +172,11 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
{getStepIcon(step)} {getStepIcon(step)}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Text <Text
size="sm" size="sm"
style={{ style={{
color: step.status === "running" ? "var(--mantine-color-blue-6)" : "var(--mantine-color-text)", color: step.status === 'running' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
fontWeight: step.status === "running" ? 500 : 400, fontWeight: step.status === 'running' ? 500 : 400
}} }}
> >
{step.name} {step.name}
@ -179,6 +203,12 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
? t("automate.sequence.running", "Running Automation...") ? t("automate.sequence.running", "Running Automation...")
: t("automate.sequence.run", "Run Automation")} : t("automate.sequence.run", "Run Automation")}
</Button> </Button>
{hasResults && (
<Button variant="light" onClick={onComplete}>
{t("automate.sequence.finish", "Finish")}
</Button>
)}
</Group> </Group>
</Stack> </Stack>
@ -192,4 +222,4 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
</style> </style>
</div> </div>
); );
} }

View File

@ -1,19 +1,19 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Modal, Modal,
Title, Title,
Button, Button,
Group, Group,
Stack, Stack,
Text, Text,
Alert, Alert
Loader
} from '@mantine/core'; } from '@mantine/core';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface ToolConfigurationModalProps { interface ToolConfigurationModalProps {
opened: boolean; opened: boolean;
tool: { tool: {
@ -24,136 +24,31 @@ interface ToolConfigurationModalProps {
}; };
onSave: (parameters: any) => void; onSave: (parameters: any) => void;
onCancel: () => 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 { t } = useTranslation();
const [parameters, setParameters] = useState<any>({}); const [parameters, setParameters] = useState<any>({});
const [isValid, setIsValid] = useState(true); 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 // Get tool info from registry
useEffect(() => { const toolInfo = toolRegistry[tool.operation];
const loadToolComponents = async () => { const SettingsComponent = toolInfo?.settingsComponent;
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]);
// Initialize parameters from tool or use defaults from hook // Initialize parameters from tool (which should contain defaults from registry)
useEffect(() => { useEffect(() => {
if (tool.parameters) { if (tool.parameters) {
setParameters(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 { } else {
// Fallback to empty parameters if none provided
setParameters({}); setParameters({});
} }
}, [tool.parameters, parameterHook, tool.operation]); }, [tool.parameters, tool.operation]);
// Render the settings component // Render the settings component
const renderToolSettings = () => { 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) { if (!SettingsComponent) {
return ( return (
<Alert icon={<WarningIcon />} color="orange"> <Alert icon={<WarningIcon />} color="orange">
@ -224,4 +119,4 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel
</Stack> </Stack>
</Modal> </Modal>
); );
} }

View File

@ -1,7 +1,8 @@
import { type TFunction } from 'i18next'; import { type TFunction } from 'i18next';
import React from 'react'; 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 { BaseToolProps } from '../types/tool';
import { BaseParameters } from '../types/parameters';
export enum SubcategoryId { export enum SubcategoryId {
SIGNING = 'signing', SIGNING = 'signing',
@ -36,8 +37,10 @@ export type ToolRegistryEntry = {
endpoints?: string[]; endpoints?: string[];
link?: string; link?: string;
type?: string; type?: string;
// Hook for automation execution // Operation configuration for automation
operationHook?: () => ToolOperationHook<any>; operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration
settingsComponent?: React.ComponentType<any>;
} }
export type ToolRegistry = Record<string, ToolRegistryEntry>; export type ToolRegistry = Record<string, ToolRegistryEntry>;

View File

@ -14,6 +14,12 @@ import Repair from '../tools/Repair';
import SingleLargePage from '../tools/SingleLargePage'; import SingleLargePage from '../tools/SingleLargePage';
import UnlockPdfForms from '../tools/UnlockPdfForms'; import UnlockPdfForms from '../tools/UnlockPdfForms';
import RemoveCertificateSign from '../tools/RemoveCertificateSign'; 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 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, category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.DOCUMENT_SECURITY, subcategory: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1, maxFiles: -1,
endpoints: ["add-password"] endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig,
settingsComponent: AddPasswordSettings
}, },
"watermark": { "watermark": {
icon: <span className="material-symbols-rounded">branding_watermark</span>, icon: <span className="material-symbols-rounded">branding_watermark</span>,
@ -198,7 +206,9 @@ export function useFlatToolRegistry(): ToolRegistry {
view: "split", view: "split",
description: t("home.split.desc", "Split PDFs into multiple documents"), description: t("home.split.desc", "Split PDFs into multiple documents"),
category: ToolCategory.STANDARD_TOOLS, category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING subcategory: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig,
settingsComponent: SplitSettings
}, },
"reorganize-pages": { "reorganize-pages": {
icon: <span className="material-symbols-rounded">move_down</span>, 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."), description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
category: ToolCategory.RECOMMENDED_TOOLS, category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL, subcategory: SubcategoryId.GENERAL,
maxFiles: -1 maxFiles: -1,
operationConfig: compressOperationConfig,
settingsComponent: CompressSettings
}, },
"convert": { "convert": {
icon: <span className="material-symbols-rounded">sync_alt</span>, icon: <span className="material-symbols-rounded">sync_alt</span>,

View File

@ -1,30 +1,45 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; 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'; 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 = () => { export const useAddPasswordOperation = () => {
const { t } = useTranslation(); 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<AddPasswordFullParameters>({ return useToolOperation<AddPasswordFullParameters>({
operationType: 'addPassword', ...addPasswordOperationConfig,
endpoint: '/api/v1/security/add-password',
buildFormData,
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_', filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
multiFileEndpoint: false,
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.')) 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 { useToolOperation } from '../shared/useToolOperation';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { executeAutomationSequence } from '../../../utils/automationExecutor'; import { executeAutomationSequence } from '../../../utils/automationExecutor';
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
interface AutomateParameters { interface AutomateParameters {
automationConfig?: any; automationConfig?: any;
onStepStart?: (stepIndex: number, operationName: string) => void;
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void;
onStepError?: (stepIndex: number, error: string) => void;
} }
export function useAutomateOperation() { export function useAutomateOperation() {
const toolRegistry = useFlatToolRegistry();
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => { const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
console.log('🚀 Starting automation execution via customProcessor', { params, files }); console.log('🚀 Starting automation execution via customProcessor', { params, files });
@ -18,21 +24,25 @@ export function useAutomateOperation() {
const finalResults = await executeAutomationSequence( const finalResults = await executeAutomationSequence(
params.automationConfig, params.automationConfig,
files, files,
toolRegistry,
(stepIndex: number, operationName: string) => { (stepIndex: number, operationName: string) => {
console.log(`Step ${stepIndex + 1} started: ${operationName}`); console.log(`Step ${stepIndex + 1} started: ${operationName}`);
params.onStepStart?.(stepIndex, operationName);
}, },
(stepIndex: number, resultFiles: File[]) => { (stepIndex: number, resultFiles: File[]) => {
console.log(`Step ${stepIndex + 1} completed with ${resultFiles.length} files`); console.log(`Step ${stepIndex + 1} completed with ${resultFiles.length} files`);
params.onStepComplete?.(stepIndex, resultFiles);
}, },
(stepIndex: number, error: string) => { (stepIndex: number, error: string) => {
console.error(`Step ${stepIndex + 1} failed:`, error); console.error(`Step ${stepIndex + 1} failed:`, error);
params.onStepError?.(stepIndex, error);
throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`); throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`);
} }
); );
console.log(`✅ Automation completed, returning ${finalResults.length} files`); console.log(`✅ Automation completed, returning ${finalResults.length} files`);
return finalResults; return finalResults;
}, []); }, [toolRegistry]);
return useToolOperation<AutomateParameters>({ return useToolOperation<AutomateParameters>({
operationType: 'automate', 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 { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; 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(); const formData = new FormData();
formData.append("fileInput", file); formData.append("fileInput", file);
@ -21,15 +22,21 @@ const buildFormData = (parameters: CompressParameters, file: File): FormData =>
return 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 = () => { export const useCompressOperation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return useToolOperation<CompressParameters>({ return useToolOperation<CompressParameters>({
operationType: 'compress', ...compressOperationConfig,
endpoint: '/api/v1/misc/compress-pdf',
buildFormData,
filePrefix: 'compressed_',
multiFileEndpoint: false, // Individual API calls per file
getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.')) 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'; fileSizeUnit: 'KB' | 'MB';
} }
const defaultParameters: CompressParameters = { export const defaultParameters: CompressParameters = {
compressionLevel: 5, compressionLevel: 5,
grayscale: false, grayscale: false,
expectedSize: '', expectedSize: '',

View File

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

View File

@ -1,11 +1,11 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { SplitParameters } from './useSplitParameters'; import { SplitParameters, defaultParameters } from './useSplitParameters';
import { SPLIT_MODES } from '../../../constants/splitConstants'; import { SPLIT_MODES } from '../../../constants/splitConstants';
// Static functions that can be used by both the hook and automation executor
const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => { export const buildSplitFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => {
const formData = new FormData(); const formData = new FormData();
selectedFiles.forEach(file => { selectedFiles.forEach(file => {
@ -40,7 +40,7 @@ const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): Form
return formData; return formData;
}; };
const getEndpoint = (parameters: SplitParameters): string => { export const getSplitEndpoint = (parameters: SplitParameters): string => {
switch (parameters.mode) { switch (parameters.mode) {
case SPLIT_MODES.BY_PAGES: case SPLIT_MODES.BY_PAGES:
return "/api/v1/general/split-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 = () => { export const useSplitOperation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return useToolOperation<SplitParameters>({ return useToolOperation<SplitParameters>({
operationType: 'split', ...splitOperationConfig,
endpoint: (params) => getEndpoint(params),
buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData
filePrefix: 'split_',
multiFileEndpoint: true, // Single API call with all files
getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.')) 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>; export type SplitParametersHook = BaseParametersHook<SplitParameters>;
const defaultParameters: SplitParameters = { export const defaultParameters: SplitParameters = {
mode: '', mode: '',
pages: '', pages: '',
hDiv: '2', 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; export default AddPassword as ToolComponent;

View File

@ -25,14 +25,28 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const automateOperation = useAutomateOperation(); const automateOperation = useAutomateOperation();
const toolRegistry = useFlatToolRegistry(); const toolRegistry = useFlatToolRegistry();
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null; const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
const { savedAutomations, deleteAutomation } = useSavedAutomations(); const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations();
const handleStepChange = (data: any) => { 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); setStepData(data);
setCurrentStep(data.step); setCurrentStep(data.step);
}; };
const handleComplete = () => { const handleComplete = () => {
// Reset automation results when completing
automateOperation.resetResults();
// Reset to selection step // Reset to selection step
setCurrentStep('selection'); setCurrentStep('selection');
setStepData({}); setStepData({});
@ -65,7 +79,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
mode={stepData.mode} mode={stepData.mode}
existingAutomation={stepData.automation} existingAutomation={stepData.automation}
onBack={() => handleStepChange({ step: 'selection' })} onBack={() => handleStepChange({ step: 'selection' })}
onComplete={() => handleStepChange({ step: 'selection' })} onComplete={() => {
refreshAutomations();
handleStepChange({ step: 'selection' });
}}
toolRegistry={toolRegistry} 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; 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; export default Split as ToolComponent;

View File

@ -1,76 +1,31 @@
import axios from 'axios'; import axios from 'axios';
import { ToolRegistry } from '../data/toolsTaxonomy';
// Tool operation configurations extracted from the hook implementations import { zipFileService } from '../services/zipFileService';
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
};
/** /**
* Extract zip files from response blob * Extract zip files from response blob
*/ */
const extractZipFiles = async (blob: Blob): Promise<File[]> => { const extractZipFiles = async (blob: Blob): Promise<File[]> => {
// This would need the actual zip extraction logic from the codebase try {
// For now, create a single file from the blob // Convert blob to File for the zip service
const file = new File([blob], `result_${Date.now()}.pdf`, { type: 'application/pdf' }); const zipFile = new File([blob], `response_${Date.now()}.zip`, { type: 'application/zip' });
return [file];
// 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 ( export const executeToolOperation = async (
operationName: string, operationName: string,
parameters: any, parameters: any,
files: File[] files: File[],
toolRegistry: ToolRegistry
): Promise<File[]> => { ): Promise<File[]> => {
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length }); console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
const config = TOOL_CONFIGS[operationName]; const config = toolRegistry[operationName]?.operationConfig;
if (!config) { if (!config) {
console.error(`❌ Tool operation not supported: ${operationName}`); console.error(`❌ Tool operation not supported: ${operationName}`);
throw new Error(`Tool operation not supported: ${operationName}`); throw new Error(`Tool operation not supported: ${operationName}`);
@ -99,7 +55,7 @@ export const executeToolOperation = async (
: config.endpoint; : config.endpoint;
console.log(`🌐 Making multi-file request to: ${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())); console.log(`📤 FormData entries:`, Array.from(formData.entries()));
const response = await axios.post(endpoint, formData, { const response = await axios.post(endpoint, formData, {
@ -126,7 +82,7 @@ export const executeToolOperation = async (
: config.endpoint; : config.endpoint;
console.log(`🌐 Making single-file request ${i+1}/${files.length} to: ${endpoint} for file: ${file.name}`); 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())); console.log(`📤 FormData entries:`, Array.from(formData.entries()));
const response = await axios.post(endpoint, formData, { const response = await axios.post(endpoint, formData, {
@ -162,6 +118,7 @@ export const executeToolOperation = async (
export const executeAutomationSequence = async ( export const executeAutomationSequence = async (
automation: any, automation: any,
initialFiles: File[], initialFiles: File[],
toolRegistry: ToolRegistry,
onStepStart?: (stepIndex: number, operationName: string) => void, onStepStart?: (stepIndex: number, operationName: string) => void,
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void, onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
onStepError?: (stepIndex: number, error: string) => void onStepError?: (stepIndex: number, error: string) => void
@ -189,7 +146,8 @@ export const executeAutomationSequence = async (
const resultFiles = await executeToolOperation( const resultFiles = await executeToolOperation(
operation.operation, operation.operation,
operation.parameters || {}, operation.parameters || {},
currentFiles currentFiles,
toolRegistry
); );
console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`); console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`);