mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Tool Picker
This commit is contained in:
parent
58e2a50b74
commit
b9b52f2269
@ -5,8 +5,8 @@ import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
|
|||||||
import ToolButton from "./toolPicker/ToolButton";
|
import ToolButton from "./toolPicker/ToolButton";
|
||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
import { useToolSections } from "../../hooks/useToolSections";
|
import { useToolSections } from "../../hooks/useToolSections";
|
||||||
import SubcategoryHeader from "./shared/SubcategoryHeader";
|
|
||||||
import NoToolsFound from "./shared/NoToolsFound";
|
import NoToolsFound from "./shared/NoToolsFound";
|
||||||
|
import { renderToolButtons } from "./shared/renderToolButtons";
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
@ -15,31 +15,6 @@ interface ToolPickerProps {
|
|||||||
isSearching?: boolean;
|
isSearching?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to render tool buttons for a subcategory
|
|
||||||
const renderToolButtons = (
|
|
||||||
subcategory: any,
|
|
||||||
selectedToolKey: string | null,
|
|
||||||
onSelect: (id: string) => void,
|
|
||||||
showSubcategoryHeader: boolean = true
|
|
||||||
) => (
|
|
||||||
<Box key={subcategory.subcategory} w="100%">
|
|
||||||
{showSubcategoryHeader && (
|
|
||||||
<SubcategoryHeader label={subcategory.subcategory} />
|
|
||||||
)}
|
|
||||||
<Stack gap="xs">
|
|
||||||
{subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => (
|
|
||||||
<ToolButton
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
tool={tool}
|
|
||||||
isSelected={selectedToolKey === id}
|
|
||||||
onSelect={onSelect}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Stack,
|
Stack,
|
||||||
Group,
|
Group,
|
||||||
Select,
|
TextInput,
|
||||||
TextInput,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Divider
|
Divider
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext';
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
import AutomationEntry from './AutomationEntry';
|
import ToolSelector from './ToolSelector';
|
||||||
|
|
||||||
interface AutomationCreationProps {
|
interface AutomationCreationProps {
|
||||||
mode: 'custom' | 'suggested' | 'create';
|
mode: 'custom' | 'suggested' | 'create';
|
||||||
existingAutomation?: any;
|
existingAutomation?: any;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onComplete: () => void;
|
onComplete: (automation: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutomationTool {
|
interface AutomationTool {
|
||||||
@ -38,8 +36,8 @@ interface AutomationTool {
|
|||||||
|
|
||||||
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete }: AutomationCreationProps) {
|
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete }: AutomationCreationProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toolRegistry } = useToolWorkflow();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
|
||||||
const [automationName, setAutomationName] = useState('');
|
const [automationName, setAutomationName] = useState('');
|
||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
@ -49,7 +47,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === 'suggested' && existingAutomation) {
|
if (mode === 'suggested' && existingAutomation) {
|
||||||
setAutomationName(existingAutomation.name);
|
setAutomationName(existingAutomation.name);
|
||||||
|
|
||||||
const tools = existingAutomation.operations.map((op: string) => ({
|
const tools = existingAutomation.operations.map((op: string) => ({
|
||||||
id: `${op}-${Date.now()}`,
|
id: `${op}-${Date.now()}`,
|
||||||
operation: op,
|
operation: op,
|
||||||
@ -57,7 +55,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
configured: false,
|
configured: false,
|
||||||
parameters: {}
|
parameters: {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setSelectedTools(tools);
|
setSelectedTools(tools);
|
||||||
}
|
}
|
||||||
}, [mode, existingAutomation]);
|
}, [mode, existingAutomation]);
|
||||||
@ -67,20 +65,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
return tool?.name || t(`tools.${operation}.name`, operation);
|
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableTools = () => {
|
const addTool = (operation: string) => {
|
||||||
if (!toolRegistry) return [];
|
|
||||||
|
|
||||||
return Object.entries(toolRegistry)
|
|
||||||
.filter(([key]) => key !== 'automate')
|
|
||||||
.map(([key, tool]) => ({
|
|
||||||
value: key,
|
|
||||||
label: (tool as any).name
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTool = (operation: string | null) => {
|
|
||||||
if (!operation) return;
|
|
||||||
|
|
||||||
const newTool: AutomationTool = {
|
const newTool: AutomationTool = {
|
||||||
id: `${operation}-${Date.now()}`,
|
id: `${operation}-${Date.now()}`,
|
||||||
operation,
|
operation,
|
||||||
@ -88,7 +74,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
configured: false,
|
configured: false,
|
||||||
parameters: {}
|
parameters: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
setSelectedTools([...selectedTools, newTool]);
|
setSelectedTools([...selectedTools, newTool]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,7 +129,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
try {
|
try {
|
||||||
const { automationStorage } = await import('../../../services/automationStorage');
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
await automationStorage.saveAutomation(automation);
|
await automationStorage.saveAutomation(automation);
|
||||||
onComplete();
|
onComplete(automation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving automation:', error);
|
console.error('Error saving automation:', error);
|
||||||
}
|
}
|
||||||
@ -153,17 +139,10 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Group justify="space-between" align="center" mb="md">
|
<Text size="sm" mb="md" p="md" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
|
||||||
<Title order={3} size="h4" fw={600} style={{ color: 'var(--mantine-color-text)' }}>
|
{t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")}
|
||||||
{mode === 'create'
|
</Text>
|
||||||
? t('automate.creation.title.create', 'Create Automation')
|
<Divider mb="md" />
|
||||||
: t('automate.creation.title.configure', 'Configure Automation')
|
|
||||||
}
|
|
||||||
</Title>
|
|
||||||
<ActionIcon variant="subtle" onClick={onBack}>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Automation Name */}
|
{/* Automation Name */}
|
||||||
@ -175,15 +154,9 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Add Tool Selector */}
|
{/* Add Tool Selector */}
|
||||||
<Select
|
<ToolSelector
|
||||||
placeholder={t('automate.creation.tools.add', 'Add a tool...')}
|
onSelect={addTool}
|
||||||
data={getAvailableTools()}
|
excludeTools={['automate']}
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
value={null}
|
|
||||||
onChange={addTool}
|
|
||||||
leftSection={<AddIcon />}
|
|
||||||
size="sm"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Selected Tools */}
|
{/* Selected Tools */}
|
||||||
@ -194,7 +167,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
<Text size="xs" c="dimmed" style={{ minWidth: '1rem', textAlign: 'center' }}>
|
<Text size="xs" c="dimmed" style={{ minWidth: '1rem', textAlign: 'center' }}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
@ -207,7 +180,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
|
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@ -227,7 +200,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{index < selectedTools.length - 1 && (
|
{index < selectedTools.length - 1 && (
|
||||||
<Text size="xs" c="dimmed">→</Text>
|
<Text size="xs" c="dimmed">→</Text>
|
||||||
)}
|
)}
|
||||||
@ -260,4 +233,4 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
127
frontend/src/components/tools/automate/ToolSelector.tsx
Normal file
127
frontend/src/components/tools/automate/ToolSelector.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Menu, Stack, Text, ScrollArea } from '@mantine/core';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { useToolSections } from '../../../hooks/useToolSections';
|
||||||
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
|
|
||||||
|
interface ToolSelectorProps {
|
||||||
|
onSelect: (toolKey: string) => void;
|
||||||
|
excludeTools?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolSelector({ onSelect, excludeTools = [] }: ToolSelectorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
|
||||||
|
// Filter out excluded tools (like 'automate' itself)
|
||||||
|
const baseFilteredTools = useMemo(() => {
|
||||||
|
return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key));
|
||||||
|
}, [toolRegistry, excludeTools]);
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
return baseFilteredTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowercaseSearch = searchTerm.toLowerCase();
|
||||||
|
return baseFilteredTools.filter(([key, tool]) => {
|
||||||
|
return (
|
||||||
|
tool.name.toLowerCase().includes(lowercaseSearch) ||
|
||||||
|
tool.description?.toLowerCase().includes(lowercaseSearch) ||
|
||||||
|
key.toLowerCase().includes(lowercaseSearch)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [baseFilteredTools, searchTerm]);
|
||||||
|
|
||||||
|
// Create filtered tool registry for ToolSearch
|
||||||
|
const filteredToolRegistry = useMemo(() => {
|
||||||
|
const registry: Record<string, ToolRegistryEntry> = {};
|
||||||
|
baseFilteredTools.forEach(([key, tool]) => {
|
||||||
|
registry[key] = tool;
|
||||||
|
});
|
||||||
|
return registry;
|
||||||
|
}, [baseFilteredTools]);
|
||||||
|
|
||||||
|
// Use the same tool sections logic as the main ToolPicker
|
||||||
|
const { sections, searchGroups } = useToolSections(filteredTools);
|
||||||
|
|
||||||
|
// Determine what to display: search results or organized sections
|
||||||
|
const isSearching = searchTerm.trim().length > 0;
|
||||||
|
const displayGroups = useMemo(() => {
|
||||||
|
if (isSearching) {
|
||||||
|
return searchGroups || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sections || sections.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the "all" section which contains all tools without duplicates
|
||||||
|
const allSection = sections.find(s => (s as any).key === 'all');
|
||||||
|
return allSection?.subcategories || [];
|
||||||
|
}, [isSearching, searchGroups, sections]);
|
||||||
|
|
||||||
|
const handleToolSelect = (toolKey: string) => {
|
||||||
|
onSelect(toolKey);
|
||||||
|
setOpened(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchFocus = () => {
|
||||||
|
setOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
if (!opened) {
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
opened={opened}
|
||||||
|
onChange={setOpened}
|
||||||
|
width="300px"
|
||||||
|
position="bottom-start"
|
||||||
|
withinPortal
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<div onClick={handleSearchFocus} style={{ cursor: 'text' }}>
|
||||||
|
<ToolSearch
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
toolRegistry={filteredToolRegistry}
|
||||||
|
mode="filter"
|
||||||
|
placeholder={t('automate.creation.tools.add', 'Add a tool...')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown p={0}>
|
||||||
|
<ScrollArea h={350}>
|
||||||
|
<Stack gap="sm" p="sm">
|
||||||
|
{displayGroups.length === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||||
|
{isSearching
|
||||||
|
? t('tools.noSearchResults', 'No tools found')
|
||||||
|
: t('tools.noTools', 'No tools available')
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
displayGroups.map((subcategory) =>
|
||||||
|
renderToolButtons(subcategory, null, handleToolSelect, !isSearching)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
219
frontend/src/components/tools/automate/ToolSequence.tsx
Normal file
219
frontend/src/components/tools/automate/ToolSequence.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Progress,
|
||||||
|
Card,
|
||||||
|
Alert
|
||||||
|
} from '@mantine/core';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
|
import { useFileContext } from '../../../contexts/FileContext';
|
||||||
|
|
||||||
|
interface ToolSequenceProps {
|
||||||
|
automation: any;
|
||||||
|
onBack: () => void;
|
||||||
|
onComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExecutionStep {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
status: 'pending' | 'running' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolSequence({ automation, onBack, onComplete }: ToolSequenceProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { activeFiles } = useFileContext();
|
||||||
|
const [isExecuting, setIsExecuting] = useState(false);
|
||||||
|
const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]);
|
||||||
|
const [currentStepIndex, setCurrentStepIndex] = useState(-1);
|
||||||
|
|
||||||
|
// Initialize execution steps from automation
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (automation?.operations) {
|
||||||
|
const steps = automation.operations.map((op: any, index: number) => ({
|
||||||
|
id: `${op.operation}-${index}`,
|
||||||
|
operation: op.operation,
|
||||||
|
name: op.operation, // You might want to get the display name from tool registry
|
||||||
|
status: 'pending' as const
|
||||||
|
}));
|
||||||
|
setExecutionSteps(steps);
|
||||||
|
}
|
||||||
|
}, [automation]);
|
||||||
|
|
||||||
|
const executeAutomation = async () => {
|
||||||
|
if (!activeFiles || activeFiles.length === 0) {
|
||||||
|
// Show error - need files to execute automation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
|
setCurrentStepIndex(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < executionSteps.length; i++) {
|
||||||
|
setCurrentStepIndex(i);
|
||||||
|
|
||||||
|
// Update step status to running
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === i ? { ...step, status: 'running' } : step
|
||||||
|
));
|
||||||
|
|
||||||
|
// Simulate step execution (replace with actual tool execution)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Update step status to completed
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === i ? { ...step, status: 'completed' } : step
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentStepIndex(-1);
|
||||||
|
setIsExecuting(false);
|
||||||
|
|
||||||
|
// All steps completed - show success
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === currentStepIndex ? { ...step, status: 'error', error: error?.toString() } : step
|
||||||
|
));
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStepIcon = (step: ExecutionStep) => {
|
||||||
|
switch (step.status) {
|
||||||
|
case 'completed':
|
||||||
|
return <CheckIcon style={{ fontSize: 16, color: 'green' }} />;
|
||||||
|
case 'error':
|
||||||
|
return <ErrorIcon style={{ fontSize: 16, color: 'red' }} />;
|
||||||
|
case 'running':
|
||||||
|
return <div style={{ width: 16, height: 16, border: '2px solid #ccc', borderTop: '2px solid #007bff', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />;
|
||||||
|
default:
|
||||||
|
return <div style={{ width: 16, height: 16, border: '2px solid #ccc', borderRadius: '50%' }} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgress = () => {
|
||||||
|
const completedSteps = executionSteps.filter(step => step.status === 'completed').length;
|
||||||
|
return (completedSteps / executionSteps.length) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allStepsCompleted = executionSteps.every(step => step.status === 'completed');
|
||||||
|
const hasErrors = executionSteps.some(step => step.status === 'error');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" align="center" mb="md">
|
||||||
|
<Title order={3} size="h4" fw={600} style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t('automate.sequence.title', 'Tool Sequence')}
|
||||||
|
</Title>
|
||||||
|
<ActionIcon variant="subtle" onClick={onBack}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Automation Info */}
|
||||||
|
<Card padding="md" withBorder>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{automation?.name || t('automate.sequence.unnamed', 'Unnamed Automation')}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t('automate.sequence.steps', '{{count}} steps', { count: executionSteps.length })}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* File Selection Warning */}
|
||||||
|
{(!activeFiles || activeFiles.length === 0) && (
|
||||||
|
<Alert color="orange" title={t('automate.sequence.noFiles', 'No Files Selected')}>
|
||||||
|
{t('automate.sequence.noFilesDesc', 'Please select files to process before running the automation.')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{isExecuting && (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" mb="xs">
|
||||||
|
{t('automate.sequence.progress', 'Progress: {{current}}/{{total}}', {
|
||||||
|
current: currentStepIndex + 1,
|
||||||
|
total: executionSteps.length
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Progress value={getProgress()} size="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Execution Steps */}
|
||||||
|
<Stack gap="xs">
|
||||||
|
{executionSteps.map((step, index) => (
|
||||||
|
<Group key={step.id} gap="sm" align="center">
|
||||||
|
<Text size="xs" c="dimmed" style={{ minWidth: '1rem', textAlign: 'center' }}>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{getStepIcon(step)}
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text size="sm" style={{
|
||||||
|
color: step.status === 'running' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
|
||||||
|
fontWeight: step.status === 'running' ? 500 : 400
|
||||||
|
}}>
|
||||||
|
{step.name}
|
||||||
|
</Text>
|
||||||
|
{step.error && (
|
||||||
|
<Text size="xs" c="red" mt="xs">
|
||||||
|
{step.error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Group justify="space-between" mt="xl">
|
||||||
|
<Button
|
||||||
|
leftSection={<PlayArrowIcon />}
|
||||||
|
onClick={executeAutomation}
|
||||||
|
disabled={isExecuting || !activeFiles || activeFiles.length === 0}
|
||||||
|
loading={isExecuting}
|
||||||
|
>
|
||||||
|
{isExecuting
|
||||||
|
? t('automate.sequence.running', 'Running Automation...')
|
||||||
|
: t('automate.sequence.run', 'Run Automation')
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(allStepsCompleted || hasErrors) && (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={onComplete}
|
||||||
|
>
|
||||||
|
{t('automate.sequence.finish', 'Finish')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
29
frontend/src/components/tools/shared/renderToolButtons.tsx
Normal file
29
frontend/src/components/tools/shared/renderToolButtons.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Stack } from '@mantine/core';
|
||||||
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
|
import SubcategoryHeader from './SubcategoryHeader';
|
||||||
|
|
||||||
|
// Helper function to render tool buttons for a subcategory
|
||||||
|
export const renderToolButtons = (
|
||||||
|
subcategory: any,
|
||||||
|
selectedToolKey: string | null,
|
||||||
|
onSelect: (id: string) => void,
|
||||||
|
showSubcategoryHeader: boolean = true
|
||||||
|
) => (
|
||||||
|
<Box key={subcategory.subcategory} w="100%">
|
||||||
|
{showSubcategoryHeader && (
|
||||||
|
<SubcategoryHeader label={subcategory.subcategory} />
|
||||||
|
)}
|
||||||
|
<Stack gap="xs">
|
||||||
|
{subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => (
|
||||||
|
<ToolButton
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
tool={tool}
|
||||||
|
isSelected={selectedToolKey === id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
@ -12,6 +12,7 @@ interface ToolSearchProps {
|
|||||||
onToolSelect?: (toolId: string) => void;
|
onToolSelect?: (toolId: string) => void;
|
||||||
mode: 'filter' | 'dropdown';
|
mode: 'filter' | 'dropdown';
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolSearch = ({
|
const ToolSearch = ({
|
||||||
@ -20,7 +21,8 @@ const ToolSearch = ({
|
|||||||
toolRegistry,
|
toolRegistry,
|
||||||
onToolSelect,
|
onToolSelect,
|
||||||
mode = 'filter',
|
mode = 'filter',
|
||||||
selectedToolKey
|
selectedToolKey,
|
||||||
|
placeholder
|
||||||
}: ToolSearchProps) => {
|
}: ToolSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@ -61,7 +63,7 @@ const ToolSearch = ({
|
|||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||||
icon={<span className="material-symbols-rounded">search</span>}
|
icon={<span className="material-symbols-rounded">search</span>}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
@ -6,6 +6,7 @@ import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
|||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
||||||
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
||||||
|
import ToolSequence from "../components/tools/automate/ToolSequence";
|
||||||
|
|
||||||
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps } from "../types/tool";
|
||||||
@ -15,7 +16,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const { setCurrentMode } = useFileContext();
|
const { setCurrentMode } = useFileContext();
|
||||||
const { selectedFiles } = useToolFileSelection();
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<'selection' | 'creation'>('selection');
|
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'sequence'>('selection');
|
||||||
const [stepData, setStepData] = useState<any>({});
|
const [stepData, setStepData] = useState<any>({});
|
||||||
|
|
||||||
const automateOperation = useAutomateOperation();
|
const automateOperation = useAutomateOperation();
|
||||||
@ -49,6 +50,15 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
mode={stepData.mode}
|
mode={stepData.mode}
|
||||||
existingAutomation={stepData.automation}
|
existingAutomation={stepData.automation}
|
||||||
onBack={() => handleStepChange({ step: 'selection' })}
|
onBack={() => handleStepChange({ step: 'selection' })}
|
||||||
|
onComplete={(automation: any) => handleStepChange({ step: 'sequence', automation })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'sequence':
|
||||||
|
return (
|
||||||
|
<ToolSequence
|
||||||
|
automation={stepData.automation}
|
||||||
|
onBack={() => handleStepChange({ step: 'creation', mode: stepData.mode, automation: stepData.automation })}
|
||||||
onComplete={handleComplete}
|
onComplete={handleComplete}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -68,7 +78,12 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
{
|
{
|
||||||
title: t('automate.stepTitle', 'Automations'),
|
title: t('automate.stepTitle', 'Automations'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
content: renderCurrentStep()
|
content: currentStep === 'selection' ? renderCurrentStep() : null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('automate.sequenceTitle', 'Tool Sequence'),
|
||||||
|
isVisible: currentStep === 'creation' || currentStep === 'sequence',
|
||||||
|
content: currentStep === 'creation' || currentStep === 'sequence' ? renderCurrentStep() : null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
review: {
|
review: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user