mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Initial working version of automation support
This commit is contained in:
parent
b9b8e6e4e1
commit
e929d7e349
@ -12,7 +12,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||
import EnhancedToolConfigurationModal from './EnhancedToolConfigurationModal';
|
||||
import ToolList from './ToolList';
|
||||
import IconSelector from './IconSelector';
|
||||
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
||||
@ -216,7 +216,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
||||
|
||||
{/* Tool Configuration Modal */}
|
||||
{currentConfigTool && (
|
||||
<ToolConfigurationModal
|
||||
<EnhancedToolConfigurationModal
|
||||
opened={configModalOpen}
|
||||
tool={currentConfigTool}
|
||||
onSave={handleToolConfigSave}
|
||||
|
@ -0,0 +1,244 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Text,
|
||||
Stack,
|
||||
Group,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Divider,
|
||||
Badge,
|
||||
Alert
|
||||
} from '@mantine/core';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||
import EnhancedToolConfigurationModal from './EnhancedToolConfigurationModal';
|
||||
import ToolList from './ToolList';
|
||||
import IconSelector from './IconSelector';
|
||||
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
||||
import { useEnhancedAutomationForm } from '../../../hooks/tools/automate/useEnhancedAutomationForm';
|
||||
|
||||
interface AutomationCreationEnhancedProps {
|
||||
mode: AutomationMode;
|
||||
existingAutomation?: AutomationConfig;
|
||||
onBack: () => void;
|
||||
onComplete: (automation: AutomationConfig) => void;
|
||||
toolRegistry: ToolRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced automation creation component that works with both definition-based and legacy tools
|
||||
*/
|
||||
export default function AutomationCreationEnhanced({
|
||||
mode,
|
||||
existingAutomation,
|
||||
onBack,
|
||||
onComplete,
|
||||
toolRegistry
|
||||
}: AutomationCreationEnhancedProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
automationName,
|
||||
setAutomationName,
|
||||
automationDescription,
|
||||
setAutomationDescription,
|
||||
automationIcon,
|
||||
setAutomationIcon,
|
||||
selectedTools,
|
||||
addTool,
|
||||
removeTool,
|
||||
updateTool,
|
||||
hasUnsavedChanges,
|
||||
canSaveAutomation,
|
||||
getToolName,
|
||||
getAutomatableTools,
|
||||
isToolAutomatable
|
||||
} = useEnhancedAutomationForm({ mode, existingAutomation, toolRegistry });
|
||||
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [configTool, setConfigTool] = useState<AutomationTool | null>(null);
|
||||
|
||||
const automatableTools = getAutomatableTools();
|
||||
const definitionBasedCount = automatableTools.filter(tool => tool.hasDefinition).length;
|
||||
const legacyCount = automatableTools.filter(tool => tool.hasLegacySettings).length;
|
||||
|
||||
const handleToolConfig = (tool: AutomationTool) => {
|
||||
setConfigTool(tool);
|
||||
setConfigModalOpen(true);
|
||||
};
|
||||
|
||||
const handleConfigSave = (parameters: unknown) => {
|
||||
if (configTool) {
|
||||
updateTool(configTool.id, {
|
||||
parameters: parameters as Record<string, unknown>,
|
||||
configured: true
|
||||
});
|
||||
}
|
||||
setConfigModalOpen(false);
|
||||
setConfigTool(null);
|
||||
};
|
||||
|
||||
const handleConfigCancel = () => {
|
||||
setConfigModalOpen(false);
|
||||
setConfigTool(null);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const automation: AutomationConfig = {
|
||||
id: existingAutomation?.id || `automation-${Date.now()}`,
|
||||
name: automationName,
|
||||
description: automationDescription,
|
||||
icon: automationIcon,
|
||||
operations: selectedTools.map(tool => ({
|
||||
operation: tool.operation,
|
||||
parameters: tool.parameters
|
||||
})),
|
||||
createdAt: existingAutomation?.createdAt || new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
onComplete(automation);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap="lg">
|
||||
{/* Info about tool types */}
|
||||
<Alert icon={<InfoIcon />} color="blue">
|
||||
<Text size="sm">
|
||||
{t('automate.enhanced.info',
|
||||
'Enhanced automation now supports {{definitionCount}} definition-based tools and {{legacyCount}} legacy tools.',
|
||||
{ definitionCount: definitionBasedCount, legacyCount: legacyCount }
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* Automation Details */}
|
||||
<Stack gap="md">
|
||||
<Text size="lg" fw={600}>
|
||||
{mode === AutomationMode.EDIT
|
||||
? t('automate.edit.title', 'Edit Automation')
|
||||
: t('automate.create.title', 'Create New Automation')
|
||||
}
|
||||
</Text>
|
||||
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label={t('automate.name.label', 'Automation Name')}
|
||||
placeholder={t('automate.name.placeholder', 'Enter automation name')}
|
||||
value={automationName}
|
||||
onChange={(e) => setAutomationName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<IconSelector
|
||||
value={automationIcon}
|
||||
onChange={setAutomationIcon}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Textarea
|
||||
label={t('automate.description.label', 'Description')}
|
||||
placeholder={t('automate.description.placeholder', 'Describe what this automation does')}
|
||||
value={automationDescription}
|
||||
onChange={(e) => setAutomationDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Tool Selection and Configuration */}
|
||||
<Stack gap="md">
|
||||
<Text size="md" fw={600}>
|
||||
{t('automate.tools.title', 'Selected Tools')}
|
||||
</Text>
|
||||
|
||||
{selectedTools.length > 0 && (
|
||||
<Stack gap="sm">
|
||||
{selectedTools.map((tool, index) => {
|
||||
const toolEntry = toolRegistry[tool.operation as keyof ToolRegistry];
|
||||
const hasDefinition = !!toolEntry?.definition;
|
||||
|
||||
return (
|
||||
<Group key={tool.id} justify="space-between" p="sm" style={{ border: '1px solid #e0e0e0', borderRadius: '4px' }}>
|
||||
<Group>
|
||||
<Text fw={500}>{index + 1}.</Text>
|
||||
<Text>{tool.name}</Text>
|
||||
{hasDefinition && (
|
||||
<Badge size="xs" color="blue">Definition-based</Badge>
|
||||
)}
|
||||
{!hasDefinition && toolEntry?.settingsComponent && (
|
||||
<Badge size="xs" color="orange">Legacy</Badge>
|
||||
)}
|
||||
{tool.configured && (
|
||||
<Badge size="xs" color="green">Configured</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => handleToolConfig(tool)}
|
||||
>
|
||||
{tool.configured
|
||||
? t('automate.tool.reconfigure', 'Reconfigure')
|
||||
: t('automate.tool.configure', 'Configure')
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color="red"
|
||||
onClick={() => removeTool(tool.id)}
|
||||
>
|
||||
{t('common.remove', 'Remove')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<ToolList
|
||||
toolRegistry={toolRegistry}
|
||||
onToolSelect={addTool}
|
||||
selectedToolIds={selectedTools.map(t => t.operation)}
|
||||
showOnlyAutomatable={true}
|
||||
automatableToolsFilter={isToolAutomatable}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
{t('common.back', 'Back')}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<CheckIcon />}
|
||||
onClick={handleSave}
|
||||
disabled={!canSaveAutomation}
|
||||
>
|
||||
{mode === AutomationMode.EDIT
|
||||
? t('automate.update', 'Update Automation')
|
||||
: t('automate.create', 'Create Automation')
|
||||
}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Configuration Modal */}
|
||||
{configModalOpen && configTool && (
|
||||
<EnhancedToolConfigurationModal
|
||||
opened={configModalOpen}
|
||||
tool={configTool}
|
||||
onSave={handleConfigSave}
|
||||
onCancel={handleConfigCancel}
|
||||
toolRegistry={toolRegistry}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Text, Stack } from '@mantine/core';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { ToolDefinition } from '../shared/toolDefinition';
|
||||
|
||||
interface DefinitionBasedToolConfigProps {
|
||||
definition: ToolDefinition<unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
onParameterChange: (key: string, value: unknown) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders tool settings from a ToolDefinition
|
||||
* This allows automation to work with definition-based tools automatically
|
||||
*/
|
||||
export default function DefinitionBasedToolConfig({
|
||||
definition,
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: DefinitionBasedToolConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Get steps from definition (handle both static and dynamic)
|
||||
const stepDefinitions = typeof definition.steps === 'function'
|
||||
? definition.steps(parameters, true, false) // hasFiles = true, hasResults = false (automation context)
|
||||
: definition.steps;
|
||||
|
||||
// Show all steps that aren't explicitly hidden
|
||||
const visibleSteps = stepDefinitions.filter((stepDef) => {
|
||||
return stepDef.isVisible !== false; // Show unless explicitly set to false (not a function)
|
||||
});
|
||||
|
||||
if (visibleSteps.length === 0) {
|
||||
return (
|
||||
<Alert icon={<WarningIcon />} color="orange">
|
||||
<Text size="sm">
|
||||
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
|
||||
</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{visibleSteps.map((stepDef) => (
|
||||
<div key={stepDef.key}>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
{stepDef.title(t)}
|
||||
</Text>
|
||||
<stepDef.component
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Modal,
|
||||
Title,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
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';
|
||||
import { ToolDefinition } from '../shared/toolDefinition';
|
||||
import { getAvailableToExtensions } from '../../../utils/convertUtils';
|
||||
import DefinitionBasedToolConfig from './DefinitionBasedToolConfig';
|
||||
|
||||
interface EnhancedToolConfigurationModalProps {
|
||||
opened: boolean;
|
||||
tool: {
|
||||
id: string;
|
||||
operation: string;
|
||||
name: string;
|
||||
parameters?: unknown;
|
||||
};
|
||||
onSave: (parameters: unknown) => void;
|
||||
onCancel: () => void;
|
||||
toolRegistry: ToolRegistry;
|
||||
}
|
||||
|
||||
export default function EnhancedToolConfigurationModal({
|
||||
opened,
|
||||
tool,
|
||||
onSave,
|
||||
onCancel,
|
||||
toolRegistry
|
||||
}: EnhancedToolConfigurationModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [legacyParameters, setLegacyParameters] = useState<Record<string, unknown>>({});
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
// Get tool info from registry
|
||||
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
|
||||
const hasDefinition = !!toolInfo?.definition;
|
||||
const SettingsComponent = toolInfo?.settingsComponent;
|
||||
|
||||
// For definition-based tools, use the actual hook
|
||||
const definitionParams = hasDefinition ? (toolInfo.definition as ToolDefinition<unknown>).useParameters() : null;
|
||||
|
||||
// Initialize legacy parameters
|
||||
useEffect(() => {
|
||||
if (!hasDefinition) {
|
||||
if (tool.parameters) {
|
||||
setLegacyParameters(tool.parameters as Record<string, unknown>);
|
||||
} else {
|
||||
setLegacyParameters({});
|
||||
}
|
||||
}
|
||||
}, [tool.parameters, tool.operation, hasDefinition]);
|
||||
|
||||
// Handle parameter changes
|
||||
const handleParameterChange = (key: string, value: unknown) => {
|
||||
if (hasDefinition && definitionParams) {
|
||||
definitionParams.updateParameter(key, value);
|
||||
} else {
|
||||
setLegacyParameters((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const parameters = hasDefinition && definitionParams ? definitionParams.parameters : legacyParameters;
|
||||
|
||||
// Render the settings component
|
||||
const renderToolSettings = () => {
|
||||
if (hasDefinition) {
|
||||
// Use definition-based rendering
|
||||
const definition = toolInfo.definition as ToolDefinition<unknown>;
|
||||
return (
|
||||
<DefinitionBasedToolConfig
|
||||
definition={definition}
|
||||
parameters={parameters}
|
||||
onParameterChange={handleParameterChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!SettingsComponent) {
|
||||
return (
|
||||
<Alert icon={<WarningIcon />} color="orange">
|
||||
<Text size="sm">
|
||||
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
|
||||
</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy settings component rendering
|
||||
if (tool.operation === 'convert') {
|
||||
return (
|
||||
<SettingsComponent
|
||||
parameters={parameters}
|
||||
onParameterChange={handleParameterChange}
|
||||
getAvailableToExtensions={getAvailableToExtensions}
|
||||
selectedFiles={[]}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsComponent
|
||||
parameters={parameters}
|
||||
onParameterChange={handleParameterChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isValid) {
|
||||
const finalParameters = hasDefinition && definitionParams ? definitionParams.parameters : legacyParameters;
|
||||
onSave(finalParameters);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onCancel}
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<SettingsIcon />
|
||||
<Title order={4}>
|
||||
{t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })}
|
||||
</Title>
|
||||
</Group>
|
||||
}
|
||||
size="lg"
|
||||
scrollAreaComponent="div"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{hasDefinition
|
||||
? t('automate.config.descriptionDefinition', 'Configure settings for this tool. These settings will be applied when the automation runs.')
|
||||
: t('automate.config.descriptionLegacy', 'Configure settings for this tool using the legacy interface.')
|
||||
}
|
||||
</Text>
|
||||
|
||||
<div style={{ minHeight: '200px' }}>
|
||||
{renderToolSettings()}
|
||||
</div>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
leftSection={<CloseIcon />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<CheckIcon />}
|
||||
onClick={handleSave}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{t('common.save', 'Save')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
||||
import { BaseToolProps } from '../types/tool';
|
||||
import { WorkbenchType } from '../types/workbench';
|
||||
import { ToolId } from '../types/toolId';
|
||||
import { ToolDefinition } from '../components/tools/shared/toolDefinition';
|
||||
|
||||
export enum SubcategoryId {
|
||||
SIGNING = 'signing',
|
||||
@ -41,6 +42,8 @@ export type ToolRegistryEntry = {
|
||||
urlPath?: string;
|
||||
// Workbench type for navigation
|
||||
workbench?: WorkbenchType;
|
||||
// Tool definition for definition-based tools (cast to specific type at point of use)
|
||||
definition?: ToolDefinition<unknown>;
|
||||
// Operation configuration for automation
|
||||
operationConfig?: ToolOperationConfig<any>;
|
||||
// Settings component for automation configuration
|
||||
|
@ -9,37 +9,31 @@ import Sanitize from "../tools/Sanitize";
|
||||
import AddPassword from "../tools/AddPassword";
|
||||
import ChangePermissions from "../tools/ChangePermissions";
|
||||
import RemovePassword from "../tools/RemovePassword";
|
||||
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
|
||||
import { SubcategoryId, ToolCategoryId, ToolRegistry, ToolRegistryEntry } from "./toolsTaxonomy";
|
||||
import AddWatermark from "../tools/AddWatermark";
|
||||
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";
|
||||
// Tool definitions
|
||||
import { compressDefinition } from "../tools/definitions/compressDefinition";
|
||||
import { splitDefinition } from "../tools/definitions/splitDefinition";
|
||||
import { addWatermarkDefinition } from "../tools/definitions/addWatermarkDefinition";
|
||||
import { repairDefinition } from "../tools/definitions/repairDefinition";
|
||||
import { sanitizeDefinition } from "../tools/definitions/sanitizeDefinition";
|
||||
import { removePasswordDefinition } from "../tools/definitions/removePasswordDefinition";
|
||||
import { unlockPdfFormsDefinition } from "../tools/definitions/unlockPdfFormsDefinition";
|
||||
import { singleLargePageDefinition } from "../tools/definitions/singleLargePageDefinition";
|
||||
import { removeCertificateSignDefinition } from "../tools/definitions/removeCertificateSignDefinition";
|
||||
import { changePermissionsDefinition } from "../tools/definitions/changePermissionsDefinition";
|
||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||
import { removePasswordOperationConfig } from "../hooks/tools/removePassword/useRemovePasswordOperation";
|
||||
import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOperation";
|
||||
import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation";
|
||||
import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
|
||||
import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
||||
import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
||||
import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
|
||||
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
|
||||
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
||||
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||
import RepairSettings from "../components/tools/repair/RepairSettings";
|
||||
import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings";
|
||||
import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings";
|
||||
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
import { ToolId } from "../types/toolId";
|
||||
import { ToolDefinition } from "../components/tools/shared/toolDefinition";
|
||||
|
||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||
|
||||
@ -167,13 +161,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.watermark.title", "Add Watermark"),
|
||||
component: AddWatermark,
|
||||
definition: addWatermarkDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
maxFiles: -1,
|
||||
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||
endpoints: ["add-watermark"],
|
||||
operationConfig: addWatermarkOperationConfig,
|
||||
settingsComponent: AddWatermarkSingleStepSettings,
|
||||
},
|
||||
"add-stamp": {
|
||||
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -187,13 +180,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.sanitize.title", "Sanitize"),
|
||||
component: Sanitize,
|
||||
definition: sanitizeDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
maxFiles: -1,
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
|
||||
endpoints: ["sanitize-pdf"],
|
||||
operationConfig: sanitizeOperationConfig,
|
||||
settingsComponent: SanitizeSettings,
|
||||
},
|
||||
flatten: {
|
||||
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -207,13 +199,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
|
||||
component: UnlockPdfForms,
|
||||
definition: unlockPdfFormsDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||
maxFiles: -1,
|
||||
endpoints: ["unlock-pdf-forms"],
|
||||
operationConfig: unlockPdfFormsOperationConfig,
|
||||
settingsComponent: UnlockPdfFormsSettings,
|
||||
},
|
||||
"manage-certificates": {
|
||||
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -230,13 +221,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.changePermissions.title", "Change Permissions"),
|
||||
component: ChangePermissions,
|
||||
definition: changePermissionsDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||
maxFiles: -1,
|
||||
endpoints: ["add-password"],
|
||||
operationConfig: changePermissionsOperationConfig,
|
||||
settingsComponent: ChangePermissionsSettings,
|
||||
},
|
||||
// Verification
|
||||
|
||||
@ -301,11 +291,10 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.split.title", "Split"),
|
||||
component: SplitPdfPanel,
|
||||
definition: splitDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.split.desc", "Split PDFs into multiple documents"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
operationConfig: splitOperationConfig,
|
||||
settingsComponent: SplitSettings,
|
||||
},
|
||||
"reorganize-pages": {
|
||||
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -350,13 +339,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
|
||||
component: SingleLargePage,
|
||||
|
||||
definition: singleLargePageDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["pdf-to-single-page"],
|
||||
operationConfig: singleLargePageOperationConfig,
|
||||
},
|
||||
"add-attachments": {
|
||||
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -425,24 +413,23 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.removePassword.title", "Remove Password"),
|
||||
component: RemovePassword,
|
||||
definition: removePasswordDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.REMOVAL,
|
||||
endpoints: ["remove-password"],
|
||||
maxFiles: -1,
|
||||
operationConfig: removePasswordOperationConfig,
|
||||
settingsComponent: RemovePasswordSettings,
|
||||
},
|
||||
"remove-certificate-sign": {
|
||||
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
|
||||
component: RemoveCertificateSign,
|
||||
definition: removeCertificateSignDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.REMOVAL,
|
||||
maxFiles: -1,
|
||||
endpoints: ["remove-certificate-sign"],
|
||||
operationConfig: removeCertificateSignOperationConfig,
|
||||
},
|
||||
|
||||
// Automation
|
||||
@ -500,13 +487,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.repair.title", "Repair"),
|
||||
component: Repair,
|
||||
definition: repairDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
|
||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["repair"],
|
||||
operationConfig: repairOperationConfig,
|
||||
settingsComponent: RepairSettings,
|
||||
},
|
||||
"detect-split-scanned-photos": {
|
||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@ -617,12 +603,11 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.compress.title", "Compress"),
|
||||
component: CompressPdfPanel,
|
||||
definition: compressDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
|
||||
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
|
||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
maxFiles: -1,
|
||||
operationConfig: compressOperationConfig,
|
||||
settingsComponent: CompressSettings,
|
||||
},
|
||||
convert: {
|
||||
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
|
||||
|
@ -15,9 +15,11 @@ export function useAutomateOperation() {
|
||||
throw new Error('No automation configuration provided');
|
||||
}
|
||||
|
||||
// Execute the automation sequence and return the final results
|
||||
console.log('🔍 Full automation config:', params.automationConfig);
|
||||
|
||||
// Execute the automation sequence using the regular executor
|
||||
const finalResults = await executeAutomationSequence(
|
||||
params.automationConfig!,
|
||||
params.automationConfig,
|
||||
files,
|
||||
toolRegistry,
|
||||
(stepIndex: number, operationName: string) => {
|
||||
@ -31,7 +33,6 @@ export function useAutomateOperation() {
|
||||
(stepIndex: number, error: string) => {
|
||||
console.error(`Step ${stepIndex + 1} failed:`, error);
|
||||
params.onStepError?.(stepIndex, error);
|
||||
throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`);
|
||||
}
|
||||
);
|
||||
|
||||
|
79
frontend/src/hooks/tools/automate/useAutomationExecutor.ts
Normal file
79
frontend/src/hooks/tools/automate/useAutomationExecutor.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||
import { AutomationTool } from '../../../types/automation';
|
||||
import { ToolDefinition } from '../../../components/tools/shared/toolDefinition';
|
||||
|
||||
export function useAutomationExecutor(toolRegistry: ToolRegistry) {
|
||||
const executeStep = useCallback(async (tool: AutomationTool, files: File[]): Promise<File[]> => {
|
||||
const toolEntry = toolRegistry[tool.operation as keyof ToolRegistry];
|
||||
if (!toolEntry) {
|
||||
throw new Error(`Tool ${tool.operation} not found in registry`);
|
||||
}
|
||||
|
||||
// Handle definition-based tools
|
||||
if (toolEntry.definition) {
|
||||
const definition = toolEntry.definition as ToolDefinition<unknown>;
|
||||
console.log(`🎯 Using definition-based tool: ${definition.id}`);
|
||||
|
||||
const operation = definition.useOperation();
|
||||
const result = await operation.executeOperation(tool.parameters, files);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Operation failed');
|
||||
}
|
||||
|
||||
console.log(`✅ Definition-based tool returned ${result.files.length} files`);
|
||||
return result.files;
|
||||
}
|
||||
|
||||
// Handle legacy tools with operationConfig
|
||||
if (toolEntry.operationConfig) {
|
||||
// Import the legacy executor function and use it
|
||||
const { executeToolOperationWithPrefix } = await import('../../../utils/automationExecutor');
|
||||
return executeToolOperationWithPrefix(
|
||||
tool.operation,
|
||||
tool.parameters || {},
|
||||
files,
|
||||
toolRegistry
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Tool ${tool.operation} has no execution method available`);
|
||||
}, [toolRegistry]);
|
||||
|
||||
const executeSequence = useCallback(async (
|
||||
tools: AutomationTool[],
|
||||
initialFiles: File[],
|
||||
onStepStart?: (stepIndex: number, operationName: string) => void,
|
||||
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
|
||||
onStepError?: (stepIndex: number, error: string) => void
|
||||
): Promise<File[]> => {
|
||||
let currentFiles = initialFiles;
|
||||
|
||||
for (let i = 0; i < tools.length; i++) {
|
||||
const tool = tools[i];
|
||||
try {
|
||||
onStepStart?.(i, tool.operation);
|
||||
console.log(`🔄 Executing step ${i + 1}/${tools.length}: ${tool.operation}`);
|
||||
|
||||
const resultFiles = await executeStep(tool, currentFiles);
|
||||
currentFiles = resultFiles;
|
||||
|
||||
onStepComplete?.(i, resultFiles);
|
||||
console.log(`✅ Step ${i + 1} completed with ${resultFiles.length} files`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`❌ Step ${i + 1} failed:`, error);
|
||||
onStepError?.(i, errorMessage);
|
||||
throw new Error(`Step ${i + 1} failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
return currentFiles;
|
||||
}, [executeStep]);
|
||||
|
||||
return {
|
||||
executeStep,
|
||||
executeSequence
|
||||
};
|
||||
}
|
159
frontend/src/hooks/tools/automate/useEnhancedAutomationForm.ts
Normal file
159
frontend/src/hooks/tools/automate/useEnhancedAutomationForm.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
||||
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||
import { ToolDefinition } from '../../../components/tools/shared/toolDefinition';
|
||||
|
||||
interface UseEnhancedAutomationFormProps {
|
||||
mode: AutomationMode;
|
||||
existingAutomation?: AutomationConfig;
|
||||
toolRegistry: ToolRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced automation form hook that works with both definition-based and legacy tools
|
||||
*/
|
||||
export function useEnhancedAutomationForm({ mode, existingAutomation, toolRegistry }: UseEnhancedAutomationFormProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [automationName, setAutomationName] = useState('');
|
||||
const [automationDescription, setAutomationDescription] = useState('');
|
||||
const [automationIcon, setAutomationIcon] = useState<string>('');
|
||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||
|
||||
const getToolName = useCallback((operation: string) => {
|
||||
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
|
||||
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||
}, [toolRegistry, t]);
|
||||
|
||||
const getToolDefaultParameters = useCallback((operation: string): Record<string, unknown> => {
|
||||
const toolEntry = toolRegistry[operation as keyof ToolRegistry];
|
||||
if (!toolEntry) return {};
|
||||
|
||||
// Check if it's a definition-based tool
|
||||
if (toolEntry.definition) {
|
||||
const definition = toolEntry.definition as ToolDefinition<unknown>;
|
||||
|
||||
// For definition-based tools, we need to get defaults from the parameters hook
|
||||
// This is tricky because we can't call hooks here, but we can provide sensible defaults
|
||||
// TODO: Consider creating a static defaultParameters method on definitions
|
||||
|
||||
// For now, return empty object - the definition components should handle their own defaults
|
||||
return {};
|
||||
}
|
||||
|
||||
// Legacy operationConfig approach
|
||||
const config = toolEntry.operationConfig;
|
||||
if (config?.defaultParameters) {
|
||||
return { ...config.defaultParameters };
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [toolRegistry]);
|
||||
|
||||
/**
|
||||
* Get list of automatable tools from the registry
|
||||
* Includes both definition-based and legacy tools with settingsComponent
|
||||
*/
|
||||
const getAutomatableTools = useCallback(() => {
|
||||
return Object.entries(toolRegistry)
|
||||
.filter(([_, toolEntry]) => {
|
||||
// Include definition-based tools OR legacy tools with settings
|
||||
return toolEntry.definition || toolEntry.settingsComponent;
|
||||
})
|
||||
.map(([toolId, toolEntry]) => ({
|
||||
id: toolId,
|
||||
name: toolEntry.name,
|
||||
hasDefinition: !!toolEntry.definition,
|
||||
hasLegacySettings: !!toolEntry.settingsComponent
|
||||
}));
|
||||
}, [toolRegistry]);
|
||||
|
||||
/**
|
||||
* Check if a tool supports automation
|
||||
*/
|
||||
const isToolAutomatable = useCallback((operation: string) => {
|
||||
const toolEntry = toolRegistry[operation as keyof ToolRegistry];
|
||||
return !!(toolEntry?.definition || toolEntry?.settingsComponent);
|
||||
}, [toolRegistry]);
|
||||
|
||||
// Initialize based on mode and existing automation
|
||||
useEffect(() => {
|
||||
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
||||
setAutomationName(existingAutomation.name || '');
|
||||
setAutomationDescription(existingAutomation.description || '');
|
||||
setAutomationIcon(existingAutomation.icon || '');
|
||||
|
||||
const operations = existingAutomation.operations || [];
|
||||
const tools = operations.map((op, index) => {
|
||||
const operation = typeof op === 'string' ? op : op.operation;
|
||||
return {
|
||||
id: `${operation}-${Date.now()}-${index}`,
|
||||
operation: operation,
|
||||
name: getToolName(operation),
|
||||
configured: mode === AutomationMode.EDIT ? true : false,
|
||||
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
||||
};
|
||||
});
|
||||
setSelectedTools(tools);
|
||||
} else {
|
||||
// Creating new automation
|
||||
setAutomationName('');
|
||||
setAutomationDescription('');
|
||||
setAutomationIcon('');
|
||||
setSelectedTools([]);
|
||||
}
|
||||
}, [mode, existingAutomation, getToolName]);
|
||||
|
||||
const addTool = useCallback((operation: string) => {
|
||||
if (!isToolAutomatable(operation)) {
|
||||
console.warn(`Tool ${operation} is not automatable`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newTool: AutomationTool = {
|
||||
id: `${operation}-${Date.now()}`,
|
||||
operation,
|
||||
name: getToolName(operation),
|
||||
configured: false,
|
||||
parameters: getToolDefaultParameters(operation)
|
||||
};
|
||||
setSelectedTools(prev => [...prev, newTool]);
|
||||
}, [getToolName, getToolDefaultParameters, isToolAutomatable]);
|
||||
|
||||
const removeTool = useCallback((toolId: string) => {
|
||||
setSelectedTools(prev => prev.filter(tool => tool.id !== toolId));
|
||||
}, []);
|
||||
|
||||
const updateTool = useCallback((toolId: string, updates: Partial<AutomationTool>) => {
|
||||
setSelectedTools(prev => prev.map(tool =>
|
||||
tool.id === toolId ? { ...tool, ...updates } : tool
|
||||
));
|
||||
}, []);
|
||||
|
||||
const hasUnsavedChanges = selectedTools.length > 0 || automationName.trim() !== '';
|
||||
|
||||
const canSaveAutomation = automationName.trim() !== '' &&
|
||||
selectedTools.length > 0 &&
|
||||
selectedTools.every(tool => tool.configured);
|
||||
|
||||
return {
|
||||
automationName,
|
||||
setAutomationName,
|
||||
automationDescription,
|
||||
setAutomationDescription,
|
||||
automationIcon,
|
||||
setAutomationIcon,
|
||||
selectedTools,
|
||||
addTool,
|
||||
removeTool,
|
||||
updateTool,
|
||||
hasUnsavedChanges,
|
||||
canSaveAutomation,
|
||||
getToolName,
|
||||
getToolDefaultParameters,
|
||||
getAutomatableTools,
|
||||
isToolAutomatable
|
||||
};
|
||||
}
|
@ -1,11 +1,65 @@
|
||||
import axios from 'axios';
|
||||
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||
import { AutomationConfig, AutomationExecutionCallbacks } from '../types/automation';
|
||||
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||
import { AutomationFileProcessor } from './automationFileProcessor';
|
||||
import { ResourceManager } from './resourceManager';
|
||||
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
||||
|
||||
/**
|
||||
* Execute operation using a static config (for definition-based tools)
|
||||
*/
|
||||
const executeWithStaticConfig = async (
|
||||
config: any,
|
||||
parameters: any,
|
||||
files: File[]
|
||||
): Promise<File[]> => {
|
||||
if (config.toolType === ToolType.singleFile) {
|
||||
// Single file processing - process each file individually
|
||||
const processedFiles: File[] = [];
|
||||
for (const file of files) {
|
||||
const endpoint = typeof config.endpoint === 'function'
|
||||
? config.endpoint(parameters)
|
||||
: config.endpoint;
|
||||
|
||||
const formData = config.buildFormData(parameters, file);
|
||||
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
responseType: 'blob',
|
||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||
});
|
||||
|
||||
const prefix = config.filePrefix || '';
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
|
||||
const processedFile = new File([response.data], `${prefix}${fileName}`, { type: 'application/pdf' });
|
||||
processedFiles.push(processedFile);
|
||||
}
|
||||
return processedFiles;
|
||||
} else if (config.toolType === ToolType.multiFile) {
|
||||
// Multi-file processing - single API call with all files
|
||||
const endpoint = typeof config.endpoint === 'function'
|
||||
? config.endpoint(parameters)
|
||||
: config.endpoint;
|
||||
|
||||
const formData = config.buildFormData(parameters, files);
|
||||
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
responseType: 'blob',
|
||||
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||
});
|
||||
|
||||
// Handle multi-file response (usually ZIP)
|
||||
if (response.data.type === 'application/pdf') {
|
||||
const fileName = files[0]?.name || 'document.pdf';
|
||||
return [new File([response.data], fileName, { type: 'application/pdf' })];
|
||||
} else {
|
||||
// Extract ZIP files
|
||||
const { extractZipFiles } = await import('./automationFileProcessor');
|
||||
return await extractZipFiles(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported tool type: ${config.toolType}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute a tool operation directly without using React hooks
|
||||
@ -31,17 +85,44 @@ export const executeToolOperationWithPrefix = async (
|
||||
): Promise<File[]> => {
|
||||
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
|
||||
|
||||
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
|
||||
if (!config) {
|
||||
console.error(`❌ Tool operation not supported: ${operationName}`);
|
||||
const toolInfo = toolRegistry[operationName as keyof ToolRegistry];
|
||||
const config = toolInfo?.operationConfig;
|
||||
const definition = toolInfo?.definition;
|
||||
|
||||
if (!config && !definition) {
|
||||
console.error(`❌ Tool operation not supported: ${operationName}. Missing both operationConfig and definition.`);
|
||||
throw new Error(`Tool operation not supported: ${operationName}`);
|
||||
}
|
||||
|
||||
console.log(`📋 Using config:`, config);
|
||||
console.log(`📋 Using config:`, config, `definition:`, definition);
|
||||
|
||||
try {
|
||||
// Handle definition-based tools by extracting their static execution logic
|
||||
if (definition && !config) {
|
||||
console.log(`🎯 Using definition-based tool: ${definition.id}`);
|
||||
|
||||
// Get the static operation config from the tool's operation hook
|
||||
// This is exported as a static config object from each tool
|
||||
const operationModule = await import(`../hooks/tools/${definition.id}/use${definition.id.charAt(0).toUpperCase() + definition.id.slice(1)}Operation`);
|
||||
const staticConfig = operationModule[`${definition.id}OperationConfig`];
|
||||
|
||||
if (staticConfig) {
|
||||
console.log(`📋 Using static config for ${definition.id}:`, staticConfig);
|
||||
// Use the static config to execute the operation
|
||||
if (staticConfig.customProcessor) {
|
||||
const resultFiles = await staticConfig.customProcessor(parameters, files);
|
||||
return resultFiles;
|
||||
} else {
|
||||
// Handle single/multi-file operations using the static config
|
||||
return await executeWithStaticConfig(staticConfig, parameters, files);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Definition-based tool ${definition.id} does not have a static config`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if tool uses custom processor (like Convert tool)
|
||||
if (config.customProcessor) {
|
||||
if (config && config.customProcessor) {
|
||||
console.log(`🎯 Using custom processor for ${config.operationType}`);
|
||||
const resultFiles = await config.customProcessor(parameters, files);
|
||||
console.log(`✅ Custom processor returned ${resultFiles.length} files`);
|
||||
|
151
frontend/src/utils/automationExecutorEnhanced.ts
Normal file
151
frontend/src/utils/automationExecutorEnhanced.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||
import { ToolDefinition } from '../components/tools/shared/toolDefinition';
|
||||
import { AutomationTool } from '../types/automation';
|
||||
|
||||
/**
|
||||
* Enhanced automation executor that works with both definition-based and legacy tools
|
||||
*/
|
||||
export class EnhancedAutomationExecutor {
|
||||
constructor(private toolRegistry: ToolRegistry) {}
|
||||
|
||||
/**
|
||||
* Execute a single automation step
|
||||
*/
|
||||
async executeStep(tool: AutomationTool, files: File[]): Promise<File[]> {
|
||||
const toolEntry = this.toolRegistry[tool.operation as keyof ToolRegistry];
|
||||
if (!toolEntry) {
|
||||
throw new Error(`Tool ${tool.operation} not found in registry`);
|
||||
}
|
||||
|
||||
// Check if it's a definition-based tool
|
||||
if (toolEntry.definition) {
|
||||
return this.executeDefinitionBasedStep(toolEntry.definition as ToolDefinition<unknown>, tool, files);
|
||||
}
|
||||
|
||||
// Check if it has legacy operationConfig
|
||||
if (toolEntry.operationConfig) {
|
||||
return this.executeLegacyStep(toolEntry.operationConfig, tool, files);
|
||||
}
|
||||
|
||||
throw new Error(`Tool ${tool.operation} has no execution method available`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a step using a tool definition
|
||||
*/
|
||||
private async executeDefinitionBasedStep(
|
||||
definition: ToolDefinition<unknown>,
|
||||
tool: AutomationTool,
|
||||
files: File[]
|
||||
): Promise<File[]> {
|
||||
// Create the operation hook instance
|
||||
const operationHook = definition.useOperation();
|
||||
|
||||
// Execute the operation with the tool's parameters and files
|
||||
await operationHook.executeOperation(tool.parameters, files);
|
||||
|
||||
// Return the resulting files
|
||||
return operationHook.files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a step using legacy operation config
|
||||
*/
|
||||
private async executeLegacyStep(
|
||||
operationConfig: any,
|
||||
tool: AutomationTool,
|
||||
files: File[]
|
||||
): Promise<File[]> {
|
||||
// This would use the existing legacy execution logic
|
||||
// Implementation depends on the current operationConfig structure
|
||||
console.log('Executing legacy step:', tool.operation, tool.parameters);
|
||||
|
||||
// Placeholder - return files unchanged for now
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a complete automation workflow
|
||||
*/
|
||||
async executeWorkflow(tools: AutomationTool[], initialFiles: File[]): Promise<File[]> {
|
||||
let currentFiles = initialFiles;
|
||||
|
||||
for (const tool of tools) {
|
||||
try {
|
||||
currentFiles = await this.executeStep(tool, currentFiles);
|
||||
console.log(`Step ${tool.operation} completed, ${currentFiles.length} files`);
|
||||
} catch (error) {
|
||||
console.error(`Step ${tool.operation} failed:`, error);
|
||||
throw new Error(`Automation failed at step: ${tool.operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
return currentFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool information for automation UI
|
||||
*/
|
||||
getToolInfo(operation: string) {
|
||||
const toolEntry = this.toolRegistry[operation as keyof ToolRegistry];
|
||||
if (!toolEntry) return null;
|
||||
|
||||
return {
|
||||
name: toolEntry.name,
|
||||
hasDefinition: !!toolEntry.definition,
|
||||
hasLegacyConfig: !!toolEntry.operationConfig,
|
||||
isAutomatable: !!(toolEntry.definition || toolEntry.operationConfig)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default parameters for a tool
|
||||
*/
|
||||
getDefaultParameters(operation: string): Record<string, unknown> {
|
||||
const toolEntry = this.toolRegistry[operation as keyof ToolRegistry];
|
||||
if (!toolEntry) return {};
|
||||
|
||||
if (toolEntry.definition) {
|
||||
// For definition-based tools, we'd need to instantiate the parameters hook
|
||||
// This is complex in a static context, so for now return empty object
|
||||
return {};
|
||||
}
|
||||
|
||||
if (toolEntry.operationConfig?.defaultParameters) {
|
||||
return { ...toolEntry.operationConfig.defaultParameters };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an automation workflow is executable
|
||||
*/
|
||||
validateWorkflow(tools: AutomationTool[]): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
const toolInfo = this.getToolInfo(tool.operation);
|
||||
|
||||
if (!toolInfo) {
|
||||
errors.push(`Tool '${tool.operation}' not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!toolInfo.isAutomatable) {
|
||||
errors.push(`Tool '${tool.operation}' is not automatable`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tool.configured) {
|
||||
errors.push(`Tool '${tool.operation}' is not configured`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user