first pass

This commit is contained in:
Connor Yoh 2025-08-19 12:46:09 +01:00
parent c1b7911518
commit 9a2fd952b1
8 changed files with 985 additions and 2 deletions

View File

@ -86,12 +86,22 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
}, ref) => { }, ref) => {
const { isRainbowMode } = useRainbowThemeContext(); const { isRainbowMode } = useRainbowThemeContext();
const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
const { handleReaderToggle } = useToolWorkflow(); const { handleReaderToggle, handleToolSelect, selectedToolKey } = useToolWorkflow();
const [configModalOpen, setConfigModalOpen] = useState(false); const [configModalOpen, setConfigModalOpen] = useState(false);
const [activeButton, setActiveButton] = useState<string>('tools'); const [activeButton, setActiveButton] = useState<string>('tools');
const scrollableRef = useRef<HTMLDivElement>(null); const scrollableRef = useRef<HTMLDivElement>(null);
const isOverflow = useIsOverflowing(scrollableRef); const isOverflow = useIsOverflowing(scrollableRef);
// Sync activeButton with selectedToolKey
React.useEffect(() => {
if (selectedToolKey === 'automate') {
setActiveButton('automate');
} else if (selectedToolKey) {
// If any other tool is selected, default to 'tools' view
setActiveButton('tools');
}
}, [selectedToolKey]);
const handleFilesButtonClick = () => { const handleFilesButtonClick = () => {
openFilesModal(); openFilesModal();
}; };
@ -131,7 +141,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
size: 'lg', size: 'lg',
isRound: false, isRound: false,
type: 'navigation', type: 'navigation',
onClick: () => setActiveButton('automate') onClick: () => {
setActiveButton('automate');
handleToolSelect('automate');
}
}, },
{ {
id: 'files', id: 'files',

View File

@ -0,0 +1,308 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
Card,
Text,
Title,
Stack,
Group,
Select,
TextInput,
Textarea,
Badge,
ActionIcon,
Modal,
Box
} from '@mantine/core';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import SettingsIcon from '@mui/icons-material/Settings';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext';
import ToolConfigurationModal from './ToolConfigurationModal';
interface AutomationCreationProps {
mode: 'custom' | 'suggested' | 'create';
existingAutomation?: any;
onBack: () => void;
onComplete: () => void;
}
interface AutomationTool {
id: string;
operation: string;
name: string;
configured: boolean;
parameters?: any;
}
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete }: AutomationCreationProps) {
const { t } = useTranslation();
const { toolRegistry } = useToolWorkflow();
const [automationName, setAutomationName] = useState('');
const [automationDescription, setAutomationDescription] = useState('');
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
// Initialize based on mode and existing automation
useEffect(() => {
if (mode === 'suggested' && existingAutomation) {
setAutomationName(existingAutomation.name);
setAutomationDescription(existingAutomation.description || '');
const tools = existingAutomation.operations.map((op: string) => ({
id: `${op}-${Date.now()}`,
operation: op,
name: getToolName(op),
configured: false,
parameters: {}
}));
setSelectedTools(tools);
}
}, [mode, existingAutomation]);
const getToolName = (operation: string) => {
const tool = toolRegistry?.[operation] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
};
const getAvailableTools = () => {
if (!toolRegistry) return [];
return Object.entries(toolRegistry)
.filter(([key]) => key !== 'automate') // Don't allow recursive automations
.map(([key, tool]) => ({
value: key,
label: (tool as any).name
}));
};
const addTool = (operation: string | null) => {
if (!operation) return;
const newTool: AutomationTool = {
id: `${operation}-${Date.now()}`,
operation,
name: getToolName(operation),
configured: false,
parameters: {}
};
setSelectedTools([...selectedTools, newTool]);
};
const removeTool = (index: number) => {
setSelectedTools(selectedTools.filter((_, i) => i !== index));
};
const configureTool = (index: number) => {
setConfiguringToolIndex(index);
setConfigModalOpen(true);
};
const handleToolConfigSave = (parameters: any) => {
if (configuraingToolIndex >= 0) {
const updatedTools = [...selectedTools];
updatedTools[configuraingToolIndex] = {
...updatedTools[configuraingToolIndex],
configured: true,
parameters
};
setSelectedTools(updatedTools);
}
setConfigModalOpen(false);
setConfiguringToolIndex(-1);
};
const handleToolConfigCancel = () => {
setConfigModalOpen(false);
setConfiguringToolIndex(-1);
};
const canSaveAutomation = () => {
return (
automationName.trim() !== '' &&
selectedTools.length > 0 &&
selectedTools.every(tool => tool.configured)
);
};
const saveAutomation = async () => {
if (!canSaveAutomation()) return;
const automation = {
name: automationName.trim(),
description: automationDescription.trim(),
operations: selectedTools.map(tool => ({
operation: tool.operation,
parameters: tool.parameters
}))
};
try {
const { automationStorage } = await import('../../../services/automationStorage');
await automationStorage.saveAutomation(automation);
onComplete();
} catch (error) {
console.error('Error saving automation:', error);
// TODO: Show error notification to user
}
};
const currentConfigTool = configuraingToolIndex >= 0 ? selectedTools[configuraingToolIndex] : null;
return (
<Stack gap="xl">
<Group justify="space-between" align="center">
<div>
<Title order={2} mb="xs">
{mode === 'create'
? t('automate.creation.title.create', 'Create New Automation')
: mode === 'suggested'
? t('automate.creation.title.configure', 'Configure Automation')
: t('automate.creation.title.edit', 'Edit Automation')
}
</Title>
<Text size="sm" c="dimmed">
{t('automate.creation.description', 'Add and configure tools to create your workflow')}
</Text>
</div>
<Button
leftSection={<ArrowBackIcon />}
variant="light"
onClick={onBack}
>
{t('automate.creation.back', 'Back')}
</Button>
</Group>
{/* Automation Details */}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Stack gap="md">
<TextInput
label={t('automate.creation.name.label', 'Automation Name')}
placeholder={t('automate.creation.name.placeholder', 'Enter a name for this automation')}
value={automationName}
onChange={(e) => setAutomationName(e.currentTarget.value)}
required
/>
<Textarea
label={t('automate.creation.description.label', 'Description')}
placeholder={t('automate.creation.description.placeholder', 'Optional description of what this automation does')}
value={automationDescription}
onChange={(e) => setAutomationDescription(e.currentTarget.value)}
minRows={2}
/>
</Stack>
</Card>
{/* Tool Selection */}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Group justify="space-between" align="center" mb="md">
<Text fw={600}>
{t('automate.creation.tools.title', 'Tools in Workflow')}
</Text>
<Select
placeholder={t('automate.creation.tools.add', 'Add a tool...')}
data={getAvailableTools()}
searchable
clearable
value={null}
onChange={addTool}
leftSection={<AddIcon />}
/>
</Group>
{selectedTools.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" py="md">
{t('automate.creation.tools.empty', 'No tools added yet. Select a tool from the dropdown above.')}
</Text>
) : (
<Stack gap="sm">
{selectedTools.map((tool, index) => (
<Box key={tool.id}>
<Group gap="sm" align="center">
<Badge size="sm" variant="light">
{index + 1}
</Badge>
<Card
shadow="xs"
padding="sm"
radius="sm"
withBorder
style={{ flex: 1 }}
>
<Group justify="space-between" align="center">
<div>
<Group gap="xs" align="center">
<Text fw={500}>{tool.name}</Text>
{tool.configured ? (
<Badge size="xs" color="green" leftSection={<CheckIcon style={{ fontSize: 10 }} />}>
{t('automate.creation.tools.configured', 'Configured')}
</Badge>
) : (
<Badge size="xs" color="orange" leftSection={<CloseIcon style={{ fontSize: 10 }} />}>
{t('automate.creation.tools.needsConfig', 'Needs Configuration')}
</Badge>
)}
</Group>
</div>
<Group gap="xs">
<ActionIcon
variant="light"
onClick={() => configureTool(index)}
title={t('automate.creation.tools.configure', 'Configure')}
>
<SettingsIcon />
</ActionIcon>
<ActionIcon
variant="light"
color="red"
onClick={() => removeTool(index)}
title={t('automate.creation.tools.remove', 'Remove')}
>
<DeleteIcon />
</ActionIcon>
</Group>
</Group>
</Card>
{index < selectedTools.length - 1 && (
<ArrowForwardIcon style={{ color: 'var(--mantine-color-dimmed)' }} />
)}
</Group>
</Box>
))}
</Stack>
)}
</Card>
{/* Save Button */}
<Group justify="flex-end">
<Button
leftSection={<CheckIcon />}
onClick={saveAutomation}
disabled={!canSaveAutomation()}
>
{t('automate.creation.save', 'Save Automation')}
</Button>
</Group>
{/* Tool Configuration Modal */}
{currentConfigTool && (
<ToolConfigurationModal
opened={configModalOpen}
tool={currentConfigTool}
onSave={handleToolConfigSave}
onCancel={handleToolConfigCancel}
/>
)}
</Stack>
);
}

View File

@ -0,0 +1,134 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button, Card, Text, Title, Stack, Group, Badge, Divider } from "@mantine/core";
import AddIcon from "@mui/icons-material/Add";
import SettingsIcon from "@mui/icons-material/Settings";
import StarIcon from "@mui/icons-material/Star";
interface AutomationSelectionProps {
onSelectCustom: () => void;
onSelectSuggested: (automation: any) => void;
onCreateNew: () => void;
}
interface SavedAutomation {
id: string;
name: string;
description?: string;
operations: any[];
createdAt: string;
}
export default function AutomationSelection({ onSelectCustom, onSelectSuggested, onCreateNew }: AutomationSelectionProps) {
const { t } = useTranslation();
const [savedAutomations, setSavedAutomations] = useState<SavedAutomation[]>([]);
// Load saved automations from IndexedDB
useEffect(() => {
loadSavedAutomations();
}, []);
const loadSavedAutomations = async () => {
try {
const { automationStorage } = await import("../../../services/automationStorage");
const automations = await automationStorage.getAllAutomations();
setSavedAutomations(automations);
} catch (error) {
console.error("Error loading saved automations:", error);
setSavedAutomations([]);
}
};
// Suggested automations - these are pre-defined common workflows
const suggestedAutomations = [
{
id: "compress-and-merge",
name: t("automate.suggested.compressAndMerge.name", "Compress & Merge"),
description: t("automate.suggested.compressAndMerge.description", "Compress multiple PDFs then merge them into one"),
operations: ["compress", "merge"],
icon: <StarIcon />,
},
{
id: "ocr-and-convert",
name: t("automate.suggested.ocrAndConvert.name", "OCR & Convert"),
description: t("automate.suggested.ocrAndConvert.description", "Apply OCR to PDFs then convert to different format"),
operations: ["ocr", "convert"],
icon: <StarIcon />,
},
{
id: "secure-workflow",
name: t("automate.suggested.secureWorkflow.name", "Secure Workflow"),
description: t("automate.suggested.secureWorkflow.description", "Sanitize, add password, and set permissions"),
operations: ["sanitize", "addPassword", "changePermissions"],
icon: <StarIcon />,
},
];
return (
<Stack gap="xl">
{/* Create New Automation */}
<Title order={3} size="h4" mb="md">
{t("automate.selection.saved.title", "Saved")}
</Title>
<Button variant="subtle" onClick={onCreateNew}>
<Group gap="md" align="center">
<AddIcon color="primary" />
<Text fw={600}>{t("automate.selection.createNew.title", "Create New Automation")}</Text>
</Group>
</Button>
{savedAutomations.map((automation) => (
<Button variant="subtle" fullWidth={true} onClick={() => onSelectCustom()}>
<div style={{ flex: 1 }}>
<Group gap="xs">
{automation.operations.map((op: any, index: number) => (
<React.Fragment key={`${op.operation || op}-${index}`}>
<Badge size="xs" variant="outline">
{String(t(`tools.${op.operation || op}.name`, op.operation || op))}
</Badge>
{index < automation.operations.length - 1 && (
<Text size="xs" c="dimmed">
</Text>
)}
</React.Fragment>
))}
</Group>
</div>
</Button>
))}
<Divider />
{/* Suggested Automations */}
<div>
<Title order={3} size="h4" mb="md">
{t("automate.selection.suggested.title", "Suggested")}
</Title>
<Stack gap="md">
{suggestedAutomations.map((automation) => (
<Button
size="md"
variant="subtle"
fullWidth={true}
onClick={() => onSelectSuggested(automation)}
style={{ paddingLeft: "0" }}
>
<Group gap="xs">
{automation.operations.map((op, index) => (
<React.Fragment key={op}>
{t(`${op}.title`, op)}
{index < automation.operations.length - 1 && (
<Text size="xs" c="dimmed">
</Text>
)}
</React.Fragment>
))}
</Group>
</Button>
))}
</Stack>
</div>
</Stack>
);
}

View File

@ -0,0 +1,230 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Modal,
Title,
Button,
Group,
Stack,
Text,
Alert,
Loader
} 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 { useToolWorkflow } from '../../../contexts/ToolWorkflowContext';
interface ToolConfigurationModalProps {
opened: boolean;
tool: {
id: string;
operation: string;
name: string;
parameters?: any;
};
onSave: (parameters: any) => void;
onCancel: () => void;
}
export default function ToolConfigurationModal({ opened, tool, onSave, onCancel }: ToolConfigurationModalProps) {
const { t } = useTranslation();
const { toolRegistry } = useToolWorkflow();
const [parameters, setParameters] = useState<any>({});
const [isValid, setIsValid] = useState(true);
const [SettingsComponent, setSettingsComponent] = useState<React.ComponentType<any> | null>(null);
const [parameterHook, setParameterHook] = useState<any>(null);
const [loading, setLoading] = useState(true);
// Dynamically load the settings component and parameter hook based on tool
useEffect(() => {
const loadToolComponents = async () => {
setLoading(true);
try {
let settingsModule, parameterModule;
switch (tool.operation) {
case 'compress':
[settingsModule, parameterModule] = await Promise.all([
import('../compress/CompressSettings'),
import('../../../hooks/tools/compress/useCompressParameters')
]);
break;
case 'split':
[settingsModule, parameterModule] = await Promise.all([
import('../split/SplitSettings'),
import('../../../hooks/tools/split/useSplitParameters')
]);
break;
case 'addPassword':
[settingsModule, parameterModule] = await Promise.all([
import('../addPassword/AddPasswordSettings'),
import('../../../hooks/tools/addPassword/useAddPasswordParameters')
]);
break;
case 'removePassword':
[settingsModule, parameterModule] = await Promise.all([
import('../removePassword/RemovePasswordSettings'),
import('../../../hooks/tools/removePassword/useRemovePasswordParameters')
]);
break;
case 'changePermissions':
[settingsModule, parameterModule] = await Promise.all([
import('../changePermissions/ChangePermissionsSettings'),
import('../../../hooks/tools/changePermissions/useChangePermissionsParameters')
]);
break;
case 'sanitize':
[settingsModule, parameterModule] = await Promise.all([
import('../sanitize/SanitizeSettings'),
import('../../../hooks/tools/sanitize/useSanitizeParameters')
]);
break;
case 'ocr':
[settingsModule, parameterModule] = await Promise.all([
import('../ocr/OCRSettings'),
import('../../../hooks/tools/ocr/useOCRParameters')
]);
break;
case 'convert':
[settingsModule, parameterModule] = await Promise.all([
import('../convert/ConvertSettings'),
import('../../../hooks/tools/convert/useConvertParameters')
]);
break;
default:
setSettingsComponent(null);
setParameterHook(null);
setLoading(false);
return;
}
setSettingsComponent(() => settingsModule.default);
setParameterHook(() => parameterModule);
} catch (error) {
console.error(`Error loading components for ${tool.operation}:`, error);
setSettingsComponent(null);
setParameterHook(null);
}
setLoading(false);
};
if (opened && tool.operation) {
loadToolComponents();
}
}, [opened, tool.operation]);
// Initialize parameters from tool or use defaults from hook
useEffect(() => {
if (tool.parameters) {
setParameters(tool.parameters);
} else if (parameterHook) {
// If we have a parameter hook, use it to get default values
try {
const defaultParams = parameterHook();
setParameters(defaultParams.parameters || {});
} catch (error) {
console.warn(`Error getting default parameters for ${tool.operation}:`, error);
setParameters({});
}
} else {
setParameters({});
}
}, [tool.parameters, parameterHook, tool.operation]);
// Render the settings component
const renderToolSettings = () => {
if (loading) {
return (
<Stack align="center" gap="md" py="xl">
<Loader size="md" />
<Text size="sm" c="dimmed">
{t('automate.config.loading', 'Loading tool configuration...')}
</Text>
</Stack>
);
}
if (!SettingsComponent) {
return (
<Alert icon={<WarningIcon />} color="orange">
<Text size="sm">
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
</Text>
</Alert>
);
}
return (
<SettingsComponent
parameters={parameters}
onParameterChange={(key: string, value: any) => {
setParameters((prev: any) => ({ ...prev, [key]: value }));
}}
disabled={false}
/>
);
};
const handleSave = () => {
if (isValid) {
onSave(parameters);
}
};
return (
<Modal
opened={opened}
onClose={onCancel}
title={
<Group gap="xs">
<SettingsIcon />
<Title order={3}>
{t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })}
</Title>
</Group>
}
size="lg"
centered
>
<Stack gap="md">
<Text size="sm" c="dimmed">
{t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')}
</Text>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{renderToolSettings()}
</div>
<Group justify="flex-end" gap="sm">
<Button
variant="light"
leftSection={<CloseIcon />}
onClick={onCancel}
>
{t('automate.config.cancel', 'Cancel')}
</Button>
<Button
leftSection={<CheckIcon />}
onClick={handleSave}
disabled={!isValid}
>
{t('automate.config.save', 'Save Configuration')}
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@ -0,0 +1,23 @@
import { useToolOperation } from '../shared/useToolOperation';
import { useCallback } from 'react';
interface AutomateParameters {
automationConfig?: any;
}
export function useAutomateOperation() {
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
// For now, this is a placeholder - the automation execution will be implemented later
// This function would send the automation config to the backend pipeline endpoint
console.log('Automation execution not yet implemented', { params, files });
throw new Error('Automation execution not yet implemented');
}, []);
return useToolOperation<AutomateParameters>({
operationType: 'automate',
endpoint: '/api/v1/pipeline/handleData',
buildFormData: () => new FormData(), // Placeholder, not used with customProcessor
customProcessor,
filePrefix: 'automated_'
});
}

View File

@ -8,6 +8,7 @@ import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import BrandingWatermarkIcon from "@mui/icons-material/BrandingWatermark"; import BrandingWatermarkIcon from "@mui/icons-material/BrandingWatermark";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { Tool, ToolDefinition, ToolRegistry } from "../types/tool"; import { Tool, ToolDefinition, ToolRegistry } from "../types/tool";
@ -124,6 +125,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
description: "Remove password protection from PDF files", description: "Remove password protection from PDF files",
endpoints: ["remove-password"] endpoints: ["remove-password"]
}, },
automate: {
id: "automate",
icon: <AutoFixHighIcon />,
component: React.lazy(() => import("../tools/Automate")),
maxFiles: -1,
category: "utility",
description: "Create and run automated workflows with multiple tools",
endpoints: ["handleData"] // Uses the existing pipeline endpoint
},
}; };

View File

@ -0,0 +1,183 @@
/**
* Service for managing automation configurations in IndexedDB
*/
export interface AutomationConfig {
id: string;
name: string;
description?: string;
operations: Array<{
operation: string;
parameters: any;
}>;
createdAt: string;
updatedAt: string;
}
class AutomationStorage {
private dbName = 'StirlingPDF_Automations';
private dbVersion = 1;
private storeName = 'automations';
private db: IDBDatabase | null = null;
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => {
reject(new Error('Failed to open automation storage database'));
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
async ensureDB(): Promise<IDBDatabase> {
if (!this.db) {
await this.init();
}
if (!this.db) {
throw new Error('Database not initialized');
}
return this.db;
}
async saveAutomation(automation: Omit<AutomationConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<AutomationConfig> {
const db = await this.ensureDB();
const timestamp = new Date().toISOString();
const automationWithMeta: AutomationConfig = {
id: `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
...automation,
createdAt: timestamp,
updatedAt: timestamp
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.add(automationWithMeta);
request.onsuccess = () => {
resolve(automationWithMeta);
};
request.onerror = () => {
reject(new Error('Failed to save automation'));
};
});
}
async updateAutomation(automation: AutomationConfig): Promise<AutomationConfig> {
const db = await this.ensureDB();
const updatedAutomation: AutomationConfig = {
...automation,
updatedAt: new Date().toISOString()
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(updatedAutomation);
request.onsuccess = () => {
resolve(updatedAutomation);
};
request.onerror = () => {
reject(new Error('Failed to update automation'));
};
});
}
async getAutomation(id: string): Promise<AutomationConfig | null> {
const db = await this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result || null);
};
request.onerror = () => {
reject(new Error('Failed to get automation'));
};
});
}
async getAllAutomations(): Promise<AutomationConfig[]> {
const db = await this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => {
const automations = request.result || [];
// Sort by creation date, newest first
automations.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
resolve(automations);
};
request.onerror = () => {
reject(new Error('Failed to get automations'));
};
});
}
async deleteAutomation(id: string): Promise<void> {
const db = await this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error('Failed to delete automation'));
};
});
}
async searchAutomations(query: string): Promise<AutomationConfig[]> {
const automations = await this.getAllAutomations();
if (!query.trim()) {
return automations;
}
const lowerQuery = query.toLowerCase();
return automations.filter(automation =>
automation.name.toLowerCase().includes(lowerQuery) ||
(automation.description && automation.description.toLowerCase().includes(lowerQuery)) ||
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
);
}
}
// Export singleton instance
export const automationStorage = new AutomationStorage();

View File

@ -0,0 +1,82 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import AutomationSelection from "../components/tools/automate/AutomationSelection";
import AutomationCreation from "../components/tools/automate/AutomationCreation";
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
import { BaseToolProps } from "../types/tool";
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { setCurrentMode } = useFileContext();
const { selectedFiles } = useToolFileSelection();
const [currentStep, setCurrentStep] = useState<'selection' | 'creation'>('selection');
const [stepData, setStepData] = useState<any>({});
const automateOperation = useAutomateOperation();
const handleStepChange = (data: any) => {
setStepData(data);
setCurrentStep(data.step);
};
const handleComplete = () => {
// Reset to selection step
setCurrentStep('selection');
setStepData({});
onComplete?.([]); // Pass empty array since automation creation doesn't produce files
};
const renderCurrentStep = () => {
switch (currentStep) {
case 'selection':
return (
<AutomationSelection
onSelectCustom={() => handleStepChange({ step: 'creation', mode: 'custom' })}
onSelectSuggested={(automation: any) => handleStepChange({ step: 'creation', mode: 'suggested', automation })}
onCreateNew={() => handleStepChange({ step: 'creation', mode: 'create' })}
/>
);
case 'creation':
return (
<AutomationCreation
mode={stepData.mode}
existingAutomation={stepData.automation}
onBack={() => handleStepChange({ step: 'selection' })}
onComplete={handleComplete}
/>
);
default:
return <div>{t('automate.invalidStep', 'Invalid step')}</div>;
}
};
return createToolFlow({
files: {
selectedFiles: [],
isCollapsed: true, // Hide files step for automate tool
placeholder: t('automate.filesHidden', 'Files will be selected during automation execution')
},
steps: [
{
title: t('automate.stepTitle', 'Automations'),
isVisible: true,
content: renderCurrentStep()
}
],
review: {
isVisible: false, // Hide review step for automate tool
operation: automateOperation,
title: t('automate.reviewTitle', 'Automation Results')
}
});
};
export default Automate;