mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
first pass
This commit is contained in:
parent
c1b7911518
commit
9a2fd952b1
@ -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',
|
||||||
|
308
frontend/src/components/tools/automate/AutomationCreation.tsx
Normal file
308
frontend/src/components/tools/automate/AutomationCreation.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
134
frontend/src/components/tools/automate/AutomationSelection.tsx
Normal file
134
frontend/src/components/tools/automate/AutomationSelection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
23
frontend/src/hooks/tools/automate/useAutomateOperation.ts
Normal file
23
frontend/src/hooks/tools/automate/useAutomateOperation.ts
Normal 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_'
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
183
frontend/src/services/automationStorage.ts
Normal file
183
frontend/src/services/automationStorage.ts
Normal 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();
|
82
frontend/src/tools/Automate.tsx
Normal file
82
frontend/src/tools/Automate.tsx
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user