mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Code review
This commit is contained in:
parent
d04979ab46
commit
63c7cd9a47
@ -1,145 +1,62 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
|
||||||
Stack,
|
Stack,
|
||||||
Group,
|
Group,
|
||||||
TextInput,
|
TextInput,
|
||||||
ActionIcon,
|
|
||||||
Divider,
|
Divider,
|
||||||
Modal
|
Modal
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
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 AddCircleOutline from '@mui/icons-material/AddCircleOutline';
|
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
import ToolSelector from './ToolSelector';
|
import ToolList from './ToolList';
|
||||||
import AutomationEntry from './AutomationEntry';
|
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
||||||
|
import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm';
|
||||||
|
|
||||||
export enum AutomationMode {
|
|
||||||
CREATE = 'create',
|
|
||||||
EDIT = 'edit',
|
|
||||||
SUGGESTED = 'suggested'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AutomationCreationProps {
|
interface AutomationCreationProps {
|
||||||
mode: AutomationMode;
|
mode: AutomationMode;
|
||||||
existingAutomation?: any;
|
existingAutomation?: AutomationConfig;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onComplete: (automation: any) => void;
|
onComplete: (automation: AutomationConfig) => void;
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
}
|
|
||||||
|
|
||||||
interface AutomationTool {
|
|
||||||
id: string;
|
|
||||||
operation: string;
|
|
||||||
name: string;
|
|
||||||
configured: boolean;
|
|
||||||
parameters?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {
|
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [automationName, setAutomationName] = useState('');
|
const {
|
||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
automationName,
|
||||||
|
setAutomationName,
|
||||||
|
selectedTools,
|
||||||
|
addTool,
|
||||||
|
removeTool,
|
||||||
|
updateTool,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
canSaveAutomation,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
} = useAutomationForm({ mode, existingAutomation, toolRegistry });
|
||||||
|
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
|
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
|
||||||
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
|
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
|
||||||
|
|
||||||
// Initialize based on mode and existing automation
|
|
||||||
useEffect(() => {
|
|
||||||
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
|
||||||
setAutomationName(existingAutomation.name || '');
|
|
||||||
|
|
||||||
// Handle both string array (suggested) and object array (custom) operations
|
|
||||||
const operations = existingAutomation.operations || [];
|
|
||||||
const tools = operations.map((op: any, index: number) => {
|
|
||||||
const operation = typeof op === 'string' ? op : op.operation;
|
|
||||||
return {
|
|
||||||
id: `${operation}-${Date.now()}-${index}`,
|
|
||||||
operation: operation,
|
|
||||||
name: getToolName(operation),
|
|
||||||
configured: mode === AutomationMode.EDIT ? true : (typeof op === 'object' ? op.configured || false : false),
|
|
||||||
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedTools(tools);
|
|
||||||
} else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
|
|
||||||
// Initialize with 2 empty tools for new automation
|
|
||||||
const defaultTools = [
|
|
||||||
{
|
|
||||||
id: `tool-1-${Date.now()}`,
|
|
||||||
operation: '',
|
|
||||||
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
|
||||||
configured: false,
|
|
||||||
parameters: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `tool-2-${Date.now() + 1}`,
|
|
||||||
operation: '',
|
|
||||||
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
|
||||||
configured: false,
|
|
||||||
parameters: {}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
setSelectedTools(defaultTools);
|
|
||||||
}
|
|
||||||
}, [mode, existingAutomation, selectedTools.length, t]);
|
|
||||||
|
|
||||||
const getToolName = (operation: string) => {
|
|
||||||
const tool = toolRegistry?.[operation] as any;
|
|
||||||
return tool?.name || t(`tools.${operation}.name`, operation);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getToolDefaultParameters = (operation: string): any => {
|
|
||||||
const config = toolRegistry[operation]?.operationConfig;
|
|
||||||
if (config?.defaultParameters) {
|
|
||||||
return { ...config.defaultParameters }; // Return a copy to avoid mutations
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTool = (operation: string) => {
|
|
||||||
|
|
||||||
const newTool: AutomationTool = {
|
|
||||||
id: `${operation}-${Date.now()}`,
|
|
||||||
operation,
|
|
||||||
name: getToolName(operation),
|
|
||||||
configured: false,
|
|
||||||
parameters: getToolDefaultParameters(operation)
|
|
||||||
};
|
|
||||||
|
|
||||||
setSelectedTools([...selectedTools, newTool]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTool = (index: number) => {
|
|
||||||
// Don't allow removing tools if only 2 remain
|
|
||||||
if (selectedTools.length <= 2) return;
|
|
||||||
setSelectedTools(selectedTools.filter((_, i) => i !== index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const configureTool = (index: number) => {
|
const configureTool = (index: number) => {
|
||||||
setConfiguringToolIndex(index);
|
setConfiguringToolIndex(index);
|
||||||
setConfigModalOpen(true);
|
setConfigModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolConfigSave = (parameters: any) => {
|
const handleToolConfigSave = (parameters: Record<string, any>) => {
|
||||||
if (configuraingToolIndex >= 0) {
|
if (configuraingToolIndex >= 0) {
|
||||||
const updatedTools = [...selectedTools];
|
updateTool(configuraingToolIndex, {
|
||||||
updatedTools[configuraingToolIndex] = {
|
|
||||||
...updatedTools[configuraingToolIndex],
|
|
||||||
configured: true,
|
configured: true,
|
||||||
parameters
|
parameters
|
||||||
};
|
});
|
||||||
setSelectedTools(updatedTools);
|
|
||||||
}
|
}
|
||||||
setConfigModalOpen(false);
|
setConfigModalOpen(false);
|
||||||
setConfiguringToolIndex(-1);
|
setConfiguringToolIndex(-1);
|
||||||
@ -150,19 +67,15 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
setConfiguringToolIndex(-1);
|
setConfiguringToolIndex(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasUnsavedChanges = () => {
|
const handleToolAdd = () => {
|
||||||
return (
|
const newTool: AutomationTool = {
|
||||||
automationName.trim() !== '' ||
|
id: `tool-${Date.now()}`,
|
||||||
selectedTools.some(tool => tool.operation !== '' || tool.configured)
|
operation: '',
|
||||||
);
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
};
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
const canSaveAutomation = () => {
|
};
|
||||||
return (
|
updateTool(selectedTools.length, newTool);
|
||||||
automationName.trim() !== '' &&
|
|
||||||
selectedTools.length > 0 &&
|
|
||||||
selectedTools.every(tool => tool.configured && tool.operation !== '')
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
@ -190,14 +103,14 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
description: '',
|
description: '',
|
||||||
operations: selectedTools.map(tool => ({
|
operations: selectedTools.map(tool => ({
|
||||||
operation: tool.operation,
|
operation: tool.operation,
|
||||||
parameters: tool.parameters
|
parameters: tool.parameters || {}
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { automationStorage } = await import('../../../services/automationStorage');
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
await automationStorage.saveAutomation(automation);
|
const savedAutomation = await automationStorage.saveAutomation(automation);
|
||||||
onComplete(automation);
|
onComplete(savedAutomation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving automation:', error);
|
console.error('Error saving automation:', error);
|
||||||
}
|
}
|
||||||
@ -224,130 +137,16 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
|
|
||||||
{/* Selected Tools List */}
|
{/* Selected Tools List */}
|
||||||
{selectedTools.length > 0 && (
|
{selectedTools.length > 0 && (
|
||||||
<div>
|
<ToolList
|
||||||
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
tools={selectedTools}
|
||||||
{t('automate.creation.tools.selected', 'Selected Tools')} ({selectedTools.length})
|
toolRegistry={toolRegistry}
|
||||||
</Text>
|
onToolUpdate={updateTool}
|
||||||
<Stack gap="0" style={{
|
onToolRemove={removeTool}
|
||||||
}}>
|
onToolConfigure={configureTool}
|
||||||
{selectedTools.map((tool, index) => (
|
onToolAdd={handleToolAdd}
|
||||||
<React.Fragment key={tool.id}>
|
getToolName={getToolName}
|
||||||
<div
|
getToolDefaultParameters={getToolDefaultParameters}
|
||||||
style={{
|
/>
|
||||||
border: '1px solid var(--mantine-color-gray-2)',
|
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
|
||||||
position: 'relative',
|
|
||||||
padding: 'var(--mantine-spacing-xs)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Delete X in top right */}
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => removeTool(index)}
|
|
||||||
title={t('automate.creation.tools.remove', 'Remove tool')}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '4px',
|
|
||||||
right: '4px',
|
|
||||||
zIndex: 1,
|
|
||||||
color: 'var(--mantine-color-gray-6)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon style={{ fontSize: 12 }} />
|
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<div style={{ paddingRight: '1.25rem' }}>
|
|
||||||
{/* Tool Selection Dropdown with inline settings cog */}
|
|
||||||
<Group gap="xs" align="center" wrap="nowrap">
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<ToolSelector
|
|
||||||
key={`tool-selector-${tool.id}`}
|
|
||||||
onSelect={(newOperation) => {
|
|
||||||
const updatedTools = [...selectedTools];
|
|
||||||
|
|
||||||
// Get default parameters from the tool
|
|
||||||
const defaultParams = getToolDefaultParameters(newOperation);
|
|
||||||
|
|
||||||
updatedTools[index] = {
|
|
||||||
...updatedTools[index],
|
|
||||||
operation: newOperation,
|
|
||||||
name: getToolName(newOperation),
|
|
||||||
configured: false,
|
|
||||||
parameters: defaultParams
|
|
||||||
};
|
|
||||||
setSelectedTools(updatedTools);
|
|
||||||
}}
|
|
||||||
excludeTools={['automate']}
|
|
||||||
toolRegistry={toolRegistry}
|
|
||||||
selectedValue={tool.operation}
|
|
||||||
placeholder={tool.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings cog - only show if tool is selected, aligned right */}
|
|
||||||
{tool.operation && (
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => configureTool(index)}
|
|
||||||
title={t('automate.creation.tools.configure', 'Configure tool')}
|
|
||||||
style={{ color: 'var(--mantine-color-gray-6)' }}
|
|
||||||
>
|
|
||||||
<SettingsIcon style={{ fontSize: 16 }} />
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Configuration status underneath */}
|
|
||||||
{tool.operation && !tool.configured && (
|
|
||||||
<Text pl="md" size="xs" c="dimmed" mt="xs">
|
|
||||||
{t('automate.creation.tools.notConfigured', "! Not Configured")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{index < selectedTools.length - 1 && (
|
|
||||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
|
||||||
<Text size="xs" c="dimmed">↓</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Arrow before Add Tool Button */}
|
|
||||||
{selectedTools.length > 0 && (
|
|
||||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
|
||||||
<Text size="xs" c="dimmed">↓</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add Tool Button */}
|
|
||||||
<div style={{
|
|
||||||
border: '1px solid var(--mantine-color-gray-2)',
|
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}>
|
|
||||||
<AutomationEntry
|
|
||||||
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
|
||||||
badgeIcon={AddCircleOutline}
|
|
||||||
operations={[]}
|
|
||||||
onClick={() => {
|
|
||||||
const newTool: AutomationTool = {
|
|
||||||
id: `tool-${Date.now()}`,
|
|
||||||
operation: '',
|
|
||||||
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
|
||||||
configured: false,
|
|
||||||
parameters: {}
|
|
||||||
};
|
|
||||||
setSelectedTools([...selectedTools, newTool]);
|
|
||||||
}}
|
|
||||||
keepIconColor={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -5,25 +5,21 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
|||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { useFileSelection } from "../../../contexts/FileContext";
|
import { useFileSelection } from "../../../contexts/FileContext";
|
||||||
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
|
||||||
|
import { AutomationConfig, ExecutionStep } from "../../../types/automation";
|
||||||
|
import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation";
|
||||||
|
import { useResourceCleanup } from "../../../utils/resourceManager";
|
||||||
|
|
||||||
interface AutomationRunProps {
|
interface AutomationRunProps {
|
||||||
automation: any;
|
automation: AutomationConfig;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
automateOperation?: any;
|
automateOperation?: any; // TODO: Type this properly when available
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecutionStep {
|
|
||||||
id: string;
|
|
||||||
operation: string;
|
|
||||||
name: string;
|
|
||||||
status: 'pending' | 'running' | 'completed' | 'error';
|
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
|
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
const cleanup = useResourceCleanup();
|
||||||
|
|
||||||
// Progress tracking state
|
// Progress tracking state
|
||||||
const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]);
|
const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]);
|
||||||
@ -42,7 +38,7 @@ 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: EXECUTION_STATUS.PENDING as const
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setExecutionSteps(steps);
|
setExecutionSteps(steps);
|
||||||
@ -56,8 +52,10 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
// Reset progress state when component unmounts
|
// Reset progress state when component unmounts
|
||||||
setExecutionSteps([]);
|
setExecutionSteps([]);
|
||||||
setCurrentStepIndex(-1);
|
setCurrentStepIndex(-1);
|
||||||
|
// Clean up any blob URLs
|
||||||
|
cleanup();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [cleanup]);
|
||||||
|
|
||||||
const executeAutomation = async () => {
|
const executeAutomation = async () => {
|
||||||
if (!selectedFiles || selectedFiles.length === 0) {
|
if (!selectedFiles || selectedFiles.length === 0) {
|
||||||
@ -71,7 +69,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
|
|
||||||
// Reset progress tracking
|
// Reset progress tracking
|
||||||
setCurrentStepIndex(0);
|
setCurrentStepIndex(0);
|
||||||
setExecutionSteps(prev => prev.map(step => ({ ...step, status: 'pending' as const, error: undefined })));
|
setExecutionSteps(prev => prev.map(step => ({ ...step, status: EXECUTION_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
|
||||||
@ -81,17 +79,17 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
onStepStart: (stepIndex: number, operationName: string) => {
|
onStepStart: (stepIndex: number, operationName: string) => {
|
||||||
setCurrentStepIndex(stepIndex);
|
setCurrentStepIndex(stepIndex);
|
||||||
setExecutionSteps(prev => prev.map((step, idx) =>
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
idx === stepIndex ? { ...step, status: 'running' as const } : step
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING as const } : step
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
onStepComplete: (stepIndex: number, resultFiles: File[]) => {
|
onStepComplete: (stepIndex: number, resultFiles: File[]) => {
|
||||||
setExecutionSteps(prev => prev.map((step, idx) =>
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
idx === stepIndex ? { ...step, status: 'completed' as const } : step
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED as const } : step
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
onStepError: (stepIndex: number, error: string) => {
|
onStepError: (stepIndex: number, error: string) => {
|
||||||
setExecutionSteps(prev => prev.map((step, idx) =>
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
idx === stepIndex ? { ...step, status: 'error' as const, error } : step
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.ERROR as const, error } : step
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -109,24 +107,24 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
|
|
||||||
const getProgress = () => {
|
const getProgress = () => {
|
||||||
if (executionSteps.length === 0) return 0;
|
if (executionSteps.length === 0) return 0;
|
||||||
const completedSteps = executionSteps.filter(step => step.status === 'completed').length;
|
const completedSteps = executionSteps.filter(step => step.status === EXECUTION_STATUS.COMPLETED).length;
|
||||||
return (completedSteps / executionSteps.length) * 100;
|
return (completedSteps / executionSteps.length) * 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStepIcon = (step: ExecutionStep) => {
|
const getStepIcon = (step: ExecutionStep) => {
|
||||||
switch (step.status) {
|
switch (step.status) {
|
||||||
case 'completed':
|
case EXECUTION_STATUS.COMPLETED:
|
||||||
return <CheckIcon style={{ fontSize: 16, color: 'green' }} />;
|
return <CheckIcon style={{ fontSize: 16, color: 'green' }} />;
|
||||||
case 'error':
|
case EXECUTION_STATUS.ERROR:
|
||||||
return <span style={{ fontSize: 16, color: 'red' }}>✕</span>;
|
return <span style={{ fontSize: 16, color: 'red' }}>✕</span>;
|
||||||
case 'running':
|
case EXECUTION_STATUS.RUNNING:
|
||||||
return <div style={{
|
return <div style={{
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
border: '2px solid #ccc',
|
border: '2px solid #ccc',
|
||||||
borderTop: '2px solid #007bff',
|
borderTop: '2px solid #007bff',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
animation: 'spin 1s linear infinite'
|
animation: `spin ${AUTOMATION_CONSTANTS.SPINNER_ANIMATION_DURATION} linear infinite`
|
||||||
}} />;
|
}} />;
|
||||||
default:
|
default:
|
||||||
return <div style={{
|
return <div style={{
|
||||||
@ -175,8 +173,8 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
<Text
|
<Text
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{
|
style={{
|
||||||
color: step.status === 'running' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
|
color: step.status === EXECUTION_STATUS.RUNNING ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
|
||||||
fontWeight: step.status === 'running' ? 500 : 400
|
fontWeight: step.status === EXECUTION_STATUS.RUNNING ? 500 : 400
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step.name}
|
{step.name}
|
||||||
|
@ -5,13 +5,14 @@ import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
|||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import AutomationEntry from "./AutomationEntry";
|
import AutomationEntry from "./AutomationEntry";
|
||||||
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
||||||
|
import { AutomationConfig } from "../../../types/automation";
|
||||||
|
|
||||||
interface AutomationSelectionProps {
|
interface AutomationSelectionProps {
|
||||||
savedAutomations: any[];
|
savedAutomations: AutomationConfig[];
|
||||||
onCreateNew: () => void;
|
onCreateNew: () => void;
|
||||||
onRun: (automation: any) => void;
|
onRun: (automation: AutomationConfig) => void;
|
||||||
onEdit: (automation: any) => void;
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
onDelete: (automation: any) => void;
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSelection({
|
export default function AutomationSelection({
|
||||||
@ -44,7 +45,7 @@ export default function AutomationSelection({
|
|||||||
key={automation.id}
|
key={automation.id}
|
||||||
title={automation.name}
|
title={automation.name}
|
||||||
badgeIcon={SettingsIcon}
|
badgeIcon={SettingsIcon}
|
||||||
operations={automation.operations.map((op: any) => typeof op === 'string' ? op : op.operation)}
|
operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)}
|
||||||
onClick={() => onRun(automation)}
|
onClick={() => onRun(automation)}
|
||||||
showMenu={true}
|
showMenu={true}
|
||||||
onEdit={() => onEdit(automation)}
|
onEdit={() => onEdit(automation)}
|
||||||
|
149
frontend/src/components/tools/automate/ToolList.tsx
Normal file
149
frontend/src/components/tools/automate/ToolList.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Text, Stack, Group, ActionIcon } from '@mantine/core';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import AddCircleOutline from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import { AutomationTool } from '../../../types/automation';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import ToolSelector from './ToolSelector';
|
||||||
|
import AutomationEntry from './AutomationEntry';
|
||||||
|
|
||||||
|
interface ToolListProps {
|
||||||
|
tools: AutomationTool[];
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
|
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
||||||
|
onToolRemove: (index: number) => void;
|
||||||
|
onToolConfigure: (index: number) => void;
|
||||||
|
onToolAdd: () => void;
|
||||||
|
getToolName: (operation: string) => string;
|
||||||
|
getToolDefaultParameters: (operation: string) => Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolList({
|
||||||
|
tools,
|
||||||
|
toolRegistry,
|
||||||
|
onToolUpdate,
|
||||||
|
onToolRemove,
|
||||||
|
onToolConfigure,
|
||||||
|
onToolAdd,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
}: ToolListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleToolSelect = (index: number, newOperation: string) => {
|
||||||
|
const defaultParams = getToolDefaultParameters(newOperation);
|
||||||
|
|
||||||
|
onToolUpdate(index, {
|
||||||
|
operation: newOperation,
|
||||||
|
name: getToolName(newOperation),
|
||||||
|
configured: false,
|
||||||
|
parameters: defaultParams
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length})
|
||||||
|
</Text>
|
||||||
|
<Stack gap="0">
|
||||||
|
{tools.map((tool, index) => (
|
||||||
|
<React.Fragment key={tool.id}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
position: 'relative',
|
||||||
|
padding: 'var(--mantine-spacing-xs)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Delete X in top right */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => onToolRemove(index)}
|
||||||
|
title={t('automate.creation.tools.remove', 'Remove tool')}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '4px',
|
||||||
|
right: '4px',
|
||||||
|
zIndex: 1,
|
||||||
|
color: 'var(--mantine-color-gray-6)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon style={{ fontSize: 12 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<div style={{ paddingRight: '1.25rem' }}>
|
||||||
|
{/* Tool Selection Dropdown with inline settings cog */}
|
||||||
|
<Group gap="xs" align="center" wrap="nowrap">
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<ToolSelector
|
||||||
|
key={`tool-selector-${tool.id}`}
|
||||||
|
onSelect={(newOperation) => handleToolSelect(index, newOperation)}
|
||||||
|
excludeTools={['automate']}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
selectedValue={tool.operation}
|
||||||
|
placeholder={tool.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings cog - only show if tool is selected, aligned right */}
|
||||||
|
{tool.operation && (
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onToolConfigure(index)}
|
||||||
|
title={t('automate.creation.tools.configure', 'Configure tool')}
|
||||||
|
style={{ color: 'var(--mantine-color-gray-6)' }}
|
||||||
|
>
|
||||||
|
<SettingsIcon style={{ fontSize: 16 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Configuration status underneath */}
|
||||||
|
{tool.operation && !tool.configured && (
|
||||||
|
<Text pl="md" size="xs" c="dimmed" mt="xs">
|
||||||
|
{t('automate.creation.tools.notConfigured', "! Not Configured")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < tools.length - 1 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Arrow before Add Tool Button */}
|
||||||
|
{tools.length > 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Tool Button */}
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<AutomationEntry
|
||||||
|
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
||||||
|
badgeIcon={AddCircleOutline}
|
||||||
|
operations={[]}
|
||||||
|
onClick={onToolAdd}
|
||||||
|
keepIconColor={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
42
frontend/src/constants/automation.ts
Normal file
42
frontend/src/constants/automation.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Constants for automation functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AUTOMATION_CONSTANTS = {
|
||||||
|
// Timeouts
|
||||||
|
OPERATION_TIMEOUT: 300000, // 5 minutes in milliseconds
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
DEFAULT_TOOL_COUNT: 2,
|
||||||
|
MIN_TOOL_COUNT: 2,
|
||||||
|
|
||||||
|
// File prefixes
|
||||||
|
FILE_PREFIX: 'automated_',
|
||||||
|
RESPONSE_ZIP_PREFIX: 'response_',
|
||||||
|
RESULT_FILE_PREFIX: 'result_',
|
||||||
|
PROCESSED_FILE_PREFIX: 'processed_',
|
||||||
|
|
||||||
|
// Operation types
|
||||||
|
CONVERT_OPERATION_TYPE: 'convert',
|
||||||
|
|
||||||
|
// Storage keys
|
||||||
|
DB_NAME: 'StirlingPDF_Automations',
|
||||||
|
DB_VERSION: 1,
|
||||||
|
STORE_NAME: 'automations',
|
||||||
|
|
||||||
|
// UI delays
|
||||||
|
SPINNER_ANIMATION_DURATION: '1s'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const AUTOMATION_STEPS = {
|
||||||
|
SELECTION: 'selection',
|
||||||
|
CREATION: 'creation',
|
||||||
|
RUN: 'run'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXECUTION_STATUS = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
RUNNING: 'running',
|
||||||
|
COMPLETED: 'completed',
|
||||||
|
ERROR: 'error'
|
||||||
|
} as const;
|
@ -2,13 +2,8 @@ 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';
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
|
import { AutomateParameters } from '../../../types/automation';
|
||||||
interface AutomateParameters {
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
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 toolRegistry = useFlatToolRegistry();
|
||||||
@ -22,7 +17,7 @@ export function useAutomateOperation() {
|
|||||||
|
|
||||||
// Execute the automation sequence and return the final results
|
// Execute the automation sequence and return the final results
|
||||||
const finalResults = await executeAutomationSequence(
|
const finalResults = await executeAutomationSequence(
|
||||||
params.automationConfig,
|
params.automationConfig!,
|
||||||
files,
|
files,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
(stepIndex: number, operationName: string) => {
|
(stepIndex: number, operationName: string) => {
|
||||||
@ -49,6 +44,6 @@ export function useAutomateOperation() {
|
|||||||
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
|
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
|
||||||
buildFormData: () => new FormData(), // Not used with customProcessor
|
buildFormData: () => new FormData(), // Not used with customProcessor
|
||||||
customProcessor,
|
customProcessor,
|
||||||
filePrefix: 'automated_'
|
filePrefix: AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
});
|
});
|
||||||
}
|
}
|
114
frontend/src/hooks/tools/automate/useAutomationForm.ts
Normal file
114
frontend/src/hooks/tools/automate/useAutomationForm.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
|
||||||
|
interface UseAutomationFormProps {
|
||||||
|
mode: AutomationMode;
|
||||||
|
existingAutomation?: AutomationConfig;
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [automationName, setAutomationName] = useState('');
|
||||||
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
|
|
||||||
|
const getToolName = (operation: string) => {
|
||||||
|
const tool = toolRegistry?.[operation] as any;
|
||||||
|
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToolDefaultParameters = (operation: string): Record<string, any> => {
|
||||||
|
const config = toolRegistry[operation]?.operationConfig;
|
||||||
|
if (config?.defaultParameters) {
|
||||||
|
return { ...config.defaultParameters };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize based on mode and existing automation
|
||||||
|
useEffect(() => {
|
||||||
|
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
||||||
|
setAutomationName(existingAutomation.name || '');
|
||||||
|
|
||||||
|
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 : (typeof op === 'object' ? op.configured || false : false),
|
||||||
|
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedTools(tools);
|
||||||
|
} else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
|
||||||
|
// Initialize with default empty tools for new automation
|
||||||
|
const defaultTools = Array.from({ length: AUTOMATION_CONSTANTS.DEFAULT_TOOL_COUNT }, (_, index) => ({
|
||||||
|
id: `tool-${index + 1}-${Date.now()}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
}));
|
||||||
|
setSelectedTools(defaultTools);
|
||||||
|
}
|
||||||
|
}, [mode, existingAutomation, selectedTools.length, t, getToolName]);
|
||||||
|
|
||||||
|
const addTool = (operation: string) => {
|
||||||
|
const newTool: AutomationTool = {
|
||||||
|
id: `${operation}-${Date.now()}`,
|
||||||
|
operation,
|
||||||
|
name: getToolName(operation),
|
||||||
|
configured: false,
|
||||||
|
parameters: getToolDefaultParameters(operation)
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedTools([...selectedTools, newTool]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTool = (index: number) => {
|
||||||
|
if (selectedTools.length <= AUTOMATION_CONSTANTS.MIN_TOOL_COUNT) return;
|
||||||
|
setSelectedTools(selectedTools.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTool = (index: number, updates: Partial<AutomationTool>) => {
|
||||||
|
const updatedTools = [...selectedTools];
|
||||||
|
updatedTools[index] = { ...updatedTools[index], ...updates };
|
||||||
|
setSelectedTools(updatedTools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUnsavedChanges = () => {
|
||||||
|
return (
|
||||||
|
automationName.trim() !== '' ||
|
||||||
|
selectedTools.some(tool => tool.operation !== '' || tool.configured)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSaveAutomation = () => {
|
||||||
|
return (
|
||||||
|
automationName.trim() !== '' &&
|
||||||
|
selectedTools.length > 0 &&
|
||||||
|
selectedTools.every(tool => tool.configured && tool.operation !== '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
automationName,
|
||||||
|
setAutomationName,
|
||||||
|
selectedTools,
|
||||||
|
setSelectedTools,
|
||||||
|
addTool,
|
||||||
|
removeTool,
|
||||||
|
updateTool,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
canSaveAutomation,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
};
|
||||||
|
}
|
@ -6,34 +6,36 @@ import { useFileSelection } from "../contexts/FileContext";
|
|||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
||||||
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
||||||
import AutomationCreation, { AutomationMode } from "../components/tools/automate/AutomationCreation";
|
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
||||||
import AutomationRun from "../components/tools/automate/AutomationRun";
|
import AutomationRun from "../components/tools/automate/AutomationRun";
|
||||||
|
|
||||||
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps } from "../types/tool";
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||||
|
import { AutomationConfig, AutomationStepData, AutomationMode } from "../types/automation";
|
||||||
|
import { AUTOMATION_STEPS } from "../constants/automation";
|
||||||
|
|
||||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>('selection');
|
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION);
|
||||||
const [stepData, setStepData] = useState<any>({});
|
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
|
|
||||||
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, refreshAutomations } = useSavedAutomations();
|
const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations();
|
||||||
|
|
||||||
const handleStepChange = (data: any) => {
|
const handleStepChange = (data: AutomationStepData) => {
|
||||||
// If navigating away from run step, reset automation results
|
// If navigating away from run step, reset automation results
|
||||||
if (currentStep === 'run' && data.step !== 'run') {
|
if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) {
|
||||||
automateOperation.resetResults();
|
automateOperation.resetResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If navigating to run step with a different automation, reset results
|
// If navigating to run step with a different automation, reset results
|
||||||
if (data.step === 'run' && data.automation &&
|
if (data.step === AUTOMATION_STEPS.RUN && data.automation &&
|
||||||
stepData.automation && data.automation.id !== stepData.automation.id) {
|
stepData.automation && data.automation.id !== stepData.automation.id) {
|
||||||
automateOperation.resetResults();
|
automateOperation.resetResults();
|
||||||
}
|
}
|
||||||
@ -47,8 +49,8 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
automateOperation.resetResults();
|
automateOperation.resetResults();
|
||||||
|
|
||||||
// Reset to selection step
|
// Reset to selection step
|
||||||
setCurrentStep('selection');
|
setCurrentStep(AUTOMATION_STEPS.SELECTION);
|
||||||
setStepData({});
|
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
onComplete?.([]); // Pass empty array since automation creation doesn't produce files
|
onComplete?.([]); // Pass empty array since automation creation doesn't produce files
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,10 +60,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
return (
|
return (
|
||||||
<AutomationSelection
|
<AutomationSelection
|
||||||
savedAutomations={savedAutomations}
|
savedAutomations={savedAutomations}
|
||||||
onCreateNew={() => handleStepChange({ step: 'creation', mode: AutomationMode.CREATE })}
|
onCreateNew={() => handleStepChange({ step: AUTOMATION_STEPS.CREATION, mode: AutomationMode.CREATE })}
|
||||||
onRun={(automation: any) => handleStepChange({ step: 'run', automation })}
|
onRun={(automation: AutomationConfig) => handleStepChange({ step: AUTOMATION_STEPS.RUN, automation })}
|
||||||
onEdit={(automation: any) => handleStepChange({ step: 'creation', mode: AutomationMode.EDIT, automation })}
|
onEdit={(automation: AutomationConfig) => handleStepChange({ step: AUTOMATION_STEPS.CREATION, mode: AutomationMode.EDIT, automation })}
|
||||||
onDelete={async (automation: any) => {
|
onDelete={async (automation: AutomationConfig) => {
|
||||||
try {
|
try {
|
||||||
await deleteAutomation(automation.id);
|
await deleteAutomation(automation.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -77,10 +79,10 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
<AutomationCreation
|
<AutomationCreation
|
||||||
mode={stepData.mode}
|
mode={stepData.mode}
|
||||||
existingAutomation={stepData.automation}
|
existingAutomation={stepData.automation}
|
||||||
onBack={() => handleStepChange({ step: 'selection' })}
|
onBack={() => handleStepChange({ step: AUTOMATION_STEPS.SELECTION })}
|
||||||
onComplete={() => {
|
onComplete={() => {
|
||||||
refreshAutomations();
|
refreshAutomations();
|
||||||
handleStepChange({ step: 'selection' });
|
handleStepChange({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
}}
|
}}
|
||||||
toolRegistry={toolRegistry}
|
toolRegistry={toolRegistry}
|
||||||
/>
|
/>
|
||||||
@ -116,34 +118,34 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const automationSteps = [
|
const automationSteps = [
|
||||||
createStep(t('automate.selection.title', 'Automation Selection'), {
|
createStep(t('automate.selection.title', 'Automation Selection'), {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isCollapsed: currentStep !== 'selection',
|
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
|
||||||
onCollapsedClick: () => setCurrentStep('selection')
|
onCollapsedClick: () => setCurrentStep(AUTOMATION_STEPS.SELECTION)
|
||||||
}, currentStep === 'selection' ? renderCurrentStep() : null),
|
}, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null),
|
||||||
|
|
||||||
createStep(stepData.mode === AutomationMode.EDIT
|
createStep(stepData.mode === AutomationMode.EDIT
|
||||||
? t('automate.creation.editTitle', 'Edit Automation')
|
? t('automate.creation.editTitle', 'Edit Automation')
|
||||||
: t('automate.creation.createTitle', 'Create Automation'), {
|
: t('automate.creation.createTitle', 'Create Automation'), {
|
||||||
isVisible: currentStep === 'creation',
|
isVisible: currentStep === AUTOMATION_STEPS.CREATION,
|
||||||
isCollapsed: false
|
isCollapsed: false
|
||||||
}, currentStep === 'creation' ? renderCurrentStep() : null),
|
}, currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null),
|
||||||
|
|
||||||
// Files step - only visible during run mode
|
// Files step - only visible during run mode
|
||||||
{
|
{
|
||||||
...filesStep,
|
...filesStep,
|
||||||
isVisible: currentStep === 'run'
|
isVisible: currentStep === AUTOMATION_STEPS.RUN
|
||||||
},
|
},
|
||||||
|
|
||||||
// Run step
|
// Run step
|
||||||
createStep(t('automate.run.title', 'Run Automation'), {
|
createStep(t('automate.run.title', 'Run Automation'), {
|
||||||
isVisible: currentStep === 'run',
|
isVisible: currentStep === AUTOMATION_STEPS.RUN,
|
||||||
isCollapsed: hasResults,
|
isCollapsed: hasResults,
|
||||||
}, currentStep === 'run' ? renderCurrentStep() : null)
|
}, currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null)
|
||||||
];
|
];
|
||||||
|
|
||||||
return createToolFlow({
|
return createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles: currentStep === 'run' ? selectedFiles : [],
|
selectedFiles: currentStep === AUTOMATION_STEPS.RUN ? selectedFiles : [],
|
||||||
isCollapsed: currentStep !== 'run' || hasResults,
|
isCollapsed: currentStep !== AUTOMATION_STEPS.RUN || hasResults,
|
||||||
isVisible: false, // Hide the default files step since we add our own
|
isVisible: false, // Hide the default files step since we add our own
|
||||||
},
|
},
|
||||||
steps: automationSteps,
|
steps: automationSteps,
|
||||||
|
67
frontend/src/types/automation.ts
Normal file
67
frontend/src/types/automation.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Types for automation functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AutomationOperation {
|
||||||
|
operation: string;
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
operations: AutomationOperation[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationTool {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
configured: boolean;
|
||||||
|
parameters?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationStepData {
|
||||||
|
step: 'selection' | 'creation' | 'run';
|
||||||
|
mode?: AutomationMode;
|
||||||
|
automation?: AutomationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionStep {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
status: 'pending' | 'running' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationExecutionCallbacks {
|
||||||
|
onStepStart?: (stepIndex: number, operationName: string) => void;
|
||||||
|
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void;
|
||||||
|
onStepError?: (stepIndex: number, error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||||
|
automationConfig?: AutomationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutomationMode {
|
||||||
|
CREATE = 'create',
|
||||||
|
EDIT = 'edit',
|
||||||
|
SUGGESTED = 'suggested'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuggestedAutomation {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
operations: string[];
|
||||||
|
icon: any; // MUI Icon component
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the AutomateParameters interface that was previously defined inline
|
||||||
|
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||||
|
automationConfig?: AutomationConfig;
|
||||||
|
}
|
@ -1,32 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ToolRegistry } from '../data/toolsTaxonomy';
|
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||||
import { zipFileService } from '../services/zipFileService';
|
import { AutomationConfig, AutomationExecutionCallbacks } from '../types/automation';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
import { AutomationFileProcessor } from './automationFileProcessor';
|
||||||
|
import { ResourceManager } from './resourceManager';
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract zip files from response blob
|
|
||||||
*/
|
|
||||||
const extractZipFiles = async (blob: Blob): Promise<File[]> => {
|
|
||||||
try {
|
|
||||||
// Convert blob to File for the zip service
|
|
||||||
const zipFile = new File([blob], `response_${Date.now()}.zip`, { type: 'application/zip' });
|
|
||||||
|
|
||||||
// Extract PDF files from the ZIP
|
|
||||||
const result = await zipFileService.extractPdfFiles(zipFile);
|
|
||||||
|
|
||||||
if (!result.success || result.extractedFiles.length === 0) {
|
|
||||||
console.error('ZIP extraction failed:', result.errors);
|
|
||||||
throw new Error(`ZIP extraction failed: ${result.errors.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📦 Extracted ${result.extractedFiles.length} files from ZIP`);
|
|
||||||
return result.extractedFiles;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to extract ZIP files:', error);
|
|
||||||
// Fallback: treat as single PDF file
|
|
||||||
const file = new File([blob], `result_${Date.now()}.pdf`, { type: 'application/pdf' });
|
|
||||||
return [file];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a tool operation directly without using React hooks
|
* Execute a tool operation directly without using React hooks
|
||||||
@ -68,15 +46,20 @@ export const executeToolOperation = async (
|
|||||||
|
|
||||||
const response = await axios.post(endpoint, formData, {
|
const response = await axios.post(endpoint, formData, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
timeout: 300000 // 5 minute timeout for large files
|
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
// Multi-file responses are typically ZIP files
|
// Multi-file responses are typically ZIP files, but may be single files
|
||||||
const resultFiles = await extractZipFiles(response.data);
|
const result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
||||||
console.log(`📁 Extracted ${resultFiles.length} files from response`);
|
|
||||||
return resultFiles;
|
if (result.errors.length > 0) {
|
||||||
|
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📁 Processed ${result.files.length} files from response`);
|
||||||
|
return result.files;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Single-file processing - separate API call per file
|
// Single-file processing - separate API call per file
|
||||||
@ -95,16 +78,16 @@ export const executeToolOperation = async (
|
|||||||
|
|
||||||
const response = await axios.post(endpoint, formData, {
|
const response = await axios.post(endpoint, formData, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
timeout: 300000 // 5 minute timeout for large files
|
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
// Create result file
|
// Create result file
|
||||||
const resultFile = new File(
|
const resultFile = ResourceManager.createResultFile(
|
||||||
[response.data],
|
response.data,
|
||||||
`processed_${file.name}`,
|
file.name,
|
||||||
{ type: 'application/pdf' }
|
AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
);
|
);
|
||||||
resultFiles.push(resultFile);
|
resultFiles.push(resultFile);
|
||||||
console.log(`✅ Created result file: ${resultFile.name}`);
|
console.log(`✅ Created result file: ${resultFile.name}`);
|
||||||
|
187
frontend/src/utils/automationFileProcessor.ts
Normal file
187
frontend/src/utils/automationFileProcessor.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* File processing utilities specifically for automation workflows
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { zipFileService } from '../services/zipFileService';
|
||||||
|
import { ResourceManager } from './resourceManager';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
|
||||||
|
export interface AutomationProcessingOptions {
|
||||||
|
timeout?: number;
|
||||||
|
responseType?: 'blob' | 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationProcessingResult {
|
||||||
|
success: boolean;
|
||||||
|
files: File[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutomationFileProcessor {
|
||||||
|
/**
|
||||||
|
* Check if a blob is a ZIP file by examining its header
|
||||||
|
*/
|
||||||
|
static isZipFile(blob: Blob): boolean {
|
||||||
|
// This is a simple check - in a real implementation you might want to read the first few bytes
|
||||||
|
// For now, we'll rely on the extraction attempt and fallback
|
||||||
|
return blob.type === 'application/zip' || blob.type === 'application/x-zip-compressed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract files from a ZIP blob during automation execution, with fallback for non-ZIP files
|
||||||
|
*/
|
||||||
|
static async extractAutomationZipFiles(blob: Blob): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const zipFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESPONSE_ZIP_PREFIX,
|
||||||
|
'.zip',
|
||||||
|
'application/zip'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await zipFileService.extractPdfFiles(zipFile);
|
||||||
|
|
||||||
|
if (!result.success || result.extractedFiles.length === 0) {
|
||||||
|
console.warn('ZIP extraction failed, treating as single file');
|
||||||
|
// Fallback: treat as single PDF file
|
||||||
|
const fallbackFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESULT_FILE_PREFIX,
|
||||||
|
'.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [fallbackFile],
|
||||||
|
errors: [`ZIP extraction failed, treated as single file: ${result.errors?.join(', ') || 'Unknown error'}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: result.extractedFiles,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to extract automation ZIP files, falling back to single file:', error);
|
||||||
|
// Fallback: treat as single PDF file
|
||||||
|
const fallbackFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESULT_FILE_PREFIX,
|
||||||
|
'.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [fallbackFile],
|
||||||
|
errors: [`ZIP extraction failed, treated as single file: ${error}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single file through an automation step
|
||||||
|
*/
|
||||||
|
static async processAutomationSingleFile(
|
||||||
|
endpoint: string,
|
||||||
|
formData: FormData,
|
||||||
|
originalFileName: string,
|
||||||
|
options: AutomationProcessingOptions = {}
|
||||||
|
): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: options.responseType || 'blob',
|
||||||
|
timeout: options.timeout || AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed - HTTP ${response.status}: ${response.statusText}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultFile = ResourceManager.createResultFile(
|
||||||
|
response.data,
|
||||||
|
originalFileName,
|
||||||
|
AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [resultFile],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process multiple files through an automation step
|
||||||
|
*/
|
||||||
|
static async processAutomationMultipleFiles(
|
||||||
|
endpoint: string,
|
||||||
|
formData: FormData,
|
||||||
|
options: AutomationProcessingOptions = {}
|
||||||
|
): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: options.responseType || 'blob',
|
||||||
|
timeout: options.timeout || AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed - HTTP ${response.status}: ${response.statusText}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-file responses are typically ZIP files
|
||||||
|
return await this.extractAutomationZipFiles(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build form data for automation tool operations
|
||||||
|
*/
|
||||||
|
static buildAutomationFormData(
|
||||||
|
parameters: Record<string, any>,
|
||||||
|
files: File | File[],
|
||||||
|
fileFieldName: string = 'fileInput'
|
||||||
|
): FormData {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Add files
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
files.forEach(file => formData.append(fileFieldName, file));
|
||||||
|
} else {
|
||||||
|
formData.append(fileFieldName, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parameters
|
||||||
|
Object.entries(parameters).forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(item => formData.append(key, item));
|
||||||
|
} else if (value !== undefined && value !== null) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
}
|
71
frontend/src/utils/resourceManager.ts
Normal file
71
frontend/src/utils/resourceManager.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Utilities for managing file resources and blob URLs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
|
||||||
|
export class ResourceManager {
|
||||||
|
private static blobUrls = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blob URL and track it for cleanup
|
||||||
|
*/
|
||||||
|
static createBlobUrl(blob: Blob): string {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
this.blobUrls.add(url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a specific blob URL
|
||||||
|
*/
|
||||||
|
static revokeBlobUrl(url: string): void {
|
||||||
|
if (this.blobUrls.has(url)) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
this.blobUrls.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke all tracked blob URLs
|
||||||
|
*/
|
||||||
|
static revokeAllBlobUrls(): void {
|
||||||
|
this.blobUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
this.blobUrls.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a File with proper naming convention
|
||||||
|
*/
|
||||||
|
static createResultFile(
|
||||||
|
data: BlobPart,
|
||||||
|
originalName: string,
|
||||||
|
prefix: string = AUTOMATION_CONSTANTS.PROCESSED_FILE_PREFIX,
|
||||||
|
type: string = 'application/pdf'
|
||||||
|
): File {
|
||||||
|
return new File([data], `${prefix}${originalName}`, { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a timestamped file for responses
|
||||||
|
*/
|
||||||
|
static createTimestampedFile(
|
||||||
|
data: BlobPart,
|
||||||
|
prefix: string,
|
||||||
|
extension: string = '.pdf',
|
||||||
|
type: string = 'application/pdf'
|
||||||
|
): File {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return new File([data], `${prefix}${timestamp}${extension}`, { type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for automatic cleanup on component unmount
|
||||||
|
*/
|
||||||
|
export function useResourceCleanup(): () => void {
|
||||||
|
return useCallback(() => {
|
||||||
|
ResourceManager.revokeAllBlobUrls();
|
||||||
|
}, []);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user