add tools

This commit is contained in:
Connor Yoh 2025-08-19 17:38:29 +01:00
parent a85ab4a832
commit c6bd7a1bfc
4 changed files with 203 additions and 55 deletions

View File

@ -8,15 +8,19 @@ import {
Group, Group,
TextInput, TextInput,
ActionIcon, ActionIcon,
Divider Divider,
Modal
} from '@mantine/core'; } from '@mantine/core';
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 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 AddCircleOutline from '@mui/icons-material/AddCircleOutline';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import ToolConfigurationModal from './ToolConfigurationModal'; import ToolConfigurationModal from './ToolConfigurationModal';
import ToolSelector from './ToolSelector'; import ToolSelector from './ToolSelector';
import AutomationEntry from './AutomationEntry';
interface AutomationCreationProps { interface AutomationCreationProps {
mode: 'custom' | 'suggested' | 'create'; mode: 'custom' | 'suggested' | 'create';
@ -41,6 +45,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]); const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const [configModalOpen, setConfigModalOpen] = useState(false); const [configModalOpen, setConfigModalOpen] = useState(false);
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1); const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
// Initialize based on mode and existing automation // Initialize based on mode and existing automation
useEffect(() => { useEffect(() => {
@ -56,8 +61,27 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
})); }));
setSelectedTools(tools); setSelectedTools(tools);
} else if (mode === 'create' && selectedTools.length === 0) {
// Initialize with 2 empty tools for new automation
const defaultTools = [
{
id: `tool-1-${Date.now()}`,
operation: '',
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
configured: false,
parameters: {}
},
{
id: `tool-2-${Date.now() + 1}`,
operation: '',
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
configured: false,
parameters: {}
} }
}, [mode, existingAutomation]); ];
setSelectedTools(defaultTools);
}
}, [mode, existingAutomation, selectedTools.length, t]);
const getToolName = (operation: string) => { const getToolName = (operation: string) => {
const tool = toolRegistry?.[operation] as any; const tool = toolRegistry?.[operation] as any;
@ -78,6 +102,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
}; };
const removeTool = (index: number) => { const removeTool = (index: number) => {
// Don't allow removing tools if only 2 remain
if (selectedTools.length <= 2) return;
setSelectedTools(selectedTools.filter((_, i) => i !== index)); setSelectedTools(selectedTools.filter((_, i) => i !== index));
}; };
@ -105,14 +131,38 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
setConfiguringToolIndex(-1); setConfiguringToolIndex(-1);
}; };
const hasUnsavedChanges = () => {
return (
automationName.trim() !== '' ||
selectedTools.some(tool => tool.operation !== '' || tool.configured)
);
};
const canSaveAutomation = () => { const canSaveAutomation = () => {
return ( return (
automationName.trim() !== '' && automationName.trim() !== '' &&
selectedTools.length > 0 && selectedTools.length > 0 &&
selectedTools.every(tool => tool.configured) selectedTools.every(tool => tool.configured && tool.operation !== '')
); );
}; };
const handleBackClick = () => {
if (hasUnsavedChanges()) {
setUnsavedWarningOpen(true);
} else {
onBack();
}
};
const handleConfirmBack = () => {
setUnsavedWarningOpen(false);
onBack();
};
const handleCancelBack = () => {
setUnsavedWarningOpen(false);
};
const saveAutomation = async () => { const saveAutomation = async () => {
if (!canSaveAutomation()) return; if (!canSaveAutomation()) return;
@ -152,48 +202,70 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
size="sm" size="sm"
/> />
{/* Add Tool Selector */}
{/* Selected Tools List */}
{selectedTools.length > 0 && (
<div>
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
{t('automate.creation.tools.selected', 'Selected Tools')} ({selectedTools.length})
</Text>
<Stack gap="0" style={{
}}>
{selectedTools.map((tool, index) => (
<React.Fragment key={tool.id}>
<div
style={{
border: '1px solid var(--mantine-color-gray-2)',
borderRadius: 'var(--mantine-radius-sm)',
backgroundColor: 'white'
}}
>
<Group gap="xs" align="center" wrap="nowrap" style={{ width: '100%' }}>
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
{/* Tool Selection Dropdown */}
<ToolSelector <ToolSelector
onSelect={addTool} key={`tool-selector-${tool.id}`}
onSelect={(newOperation) => {
const updatedTools = [...selectedTools];
updatedTools[index] = {
...updatedTools[index],
operation: newOperation,
name: getToolName(newOperation),
configured: false,
parameters: {}
};
setSelectedTools(updatedTools);
}}
excludeTools={['automate']} excludeTools={['automate']}
toolRegistry={toolRegistry} toolRegistry={toolRegistry}
selectedValue={tool.operation}
placeholder={tool.name}
/> />
</div>
{/* Selected Tools */} <Group gap="xs" style={{ flexShrink: 0 }}>
{selectedTools.length > 0 && (
<Stack gap="xs">
{selectedTools.map((tool, index) => (
<Group key={tool.id} gap="xs" align="center">
<Text size="xs" c="dimmed" style={{ minWidth: '1rem', textAlign: 'center' }}>
{index + 1}
</Text>
<div style={{ flex: 1 }}>
<Group justify="space-between" align="center">
<Group gap="xs" align="center">
<Text size="sm" style={{ color: 'var(--mantine-color-text)' }}>
{tool.name}
</Text>
{tool.configured ? ( {tool.configured ? (
<CheckIcon style={{ fontSize: 14, color: 'green' }} /> <CheckIcon style={{ fontSize: 14, color: 'green' }} />
) : ( ) : (
<CloseIcon style={{ fontSize: 14, color: 'orange' }} /> <CloseIcon style={{ fontSize: 14, color: 'orange' }} />
)} )}
</Group>
<Group gap="xs">
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
size="sm" size="sm"
onClick={() => configureTool(index)} onClick={() => configureTool(index)}
title={t('automate.creation.tools.configure', 'Configure tool')}
> >
<SettingsIcon style={{ fontSize: 16 }} /> <SettingsIcon style={{ fontSize: 16 }} />
</ActionIcon> </ActionIcon>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
size="sm" size="sm"
color="red" color="red"
onClick={() => removeTool(index)} onClick={() => removeTool(index)}
title={t('automate.creation.tools.remove', 'Remove tool')}
> >
<DeleteIcon style={{ fontSize: 16 }} /> <DeleteIcon style={{ fontSize: 16 }} />
</ActionIcon> </ActionIcon>
@ -202,11 +274,45 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
</div> </div>
{index < selectedTools.length - 1 && ( {index < selectedTools.length - 1 && (
<Text size="xs" c="dimmed"></Text> <div style={{ textAlign: 'center', padding: '8px 0' }}>
<Text size="xs" c="dimmed"></Text>
</div>
)} )}
</Group> </React.Fragment>
))} ))}
{/* Arrow before Add Tool Button */}
{selectedTools.length > 0 && (
<div style={{ textAlign: 'center', padding: '8px 0' }}>
<Text size="xs" c="dimmed"></Text>
</div>
)}
{/* Add Tool Button */}
<div style={{
border: '1px solid var(--mantine-color-gray-2)',
borderRadius: 'var(--mantine-radius-sm)',
overflow: 'hidden'
}}>
<AutomationEntry
title={t('automate.creation.tools.addTool', 'Add Tool')}
badgeIcon={AddCircleOutline}
operations={[]}
onClick={() => {
const newTool: AutomationTool = {
id: `tool-${Date.now()}`,
operation: '',
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
configured: false,
parameters: {}
};
setSelectedTools([...selectedTools, newTool]);
}}
keepIconColor={true}
/>
</div>
</Stack> </Stack>
</div>
)} )}
<Divider /> <Divider />
@ -231,6 +337,28 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
onCancel={handleToolConfigCancel} onCancel={handleToolConfigCancel}
/> />
)} )}
{/* Unsaved Changes Warning Modal */}
<Modal
opened={unsavedWarningOpen}
onClose={handleCancelBack}
title={t('automate.creation.unsavedChanges.title', 'Unsaved Changes')}
centered
>
<Stack gap="md">
<Text>
{t('automate.creation.unsavedChanges.message', 'You have unsaved changes. Are you sure you want to go back? All changes will be lost.')}
</Text>
<Group gap="md" justify="flex-end">
<Button variant="outline" onClick={handleCancelBack}>
{t('automate.creation.unsavedChanges.cancel', 'Cancel')}
</Button>
<Button color="red" onClick={handleConfirmBack}>
{t('automate.creation.unsavedChanges.confirm', 'Go Back')}
</Button>
</Group>
</Stack>
</Modal>
</div> </div>
); );
} }

View File

@ -10,9 +10,17 @@ interface ToolSelectorProps {
onSelect: (toolKey: string) => void; onSelect: (toolKey: string) => void;
excludeTools?: string[]; excludeTools?: string[];
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
selectedValue?: string; // For showing current selection when editing existing tool
placeholder?: string; // Custom placeholder text
} }
export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry }: ToolSelectorProps) { export default function ToolSelector({
onSelect,
excludeTools = [],
toolRegistry,
selectedValue,
placeholder
}: ToolSelectorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@ -83,6 +91,14 @@ export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry
} }
}; };
// Get display value for selected tool
const getDisplayValue = () => {
if (selectedValue && toolRegistry[selectedValue]) {
return toolRegistry[selectedValue].name;
}
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
};
return ( return (
<Menu <Menu
opened={opened} opened={opened}
@ -98,7 +114,8 @@ export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry
onChange={handleSearchChange} onChange={handleSearchChange}
toolRegistry={filteredToolRegistry} toolRegistry={filteredToolRegistry}
mode="filter" mode="filter"
placeholder={t('automate.creation.tools.add', 'Add a tool...')} placeholder={getDisplayValue()}
hideIcon={true}
/> />
</div> </div>
</Menu.Target> </Menu.Target>

View File

@ -13,6 +13,7 @@ interface ToolSearchProps {
mode: 'filter' | 'dropdown'; mode: 'filter' | 'dropdown';
selectedToolKey?: string | null; selectedToolKey?: string | null;
placeholder?: string; placeholder?: string;
hideIcon?: boolean;
} }
const ToolSearch = ({ const ToolSearch = ({
@ -22,7 +23,8 @@ const ToolSearch = ({
onToolSelect, onToolSelect,
mode = 'filter', mode = 'filter',
selectedToolKey, selectedToolKey,
placeholder placeholder,
hideIcon = false
}: ToolSearchProps) => { }: ToolSearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
@ -64,7 +66,7 @@ const ToolSearch = ({
value={value} value={value}
onChange={handleSearchChange} onChange={handleSearchChange}
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
icon={<span className="material-symbols-rounded">search</span>} icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
autoComplete="off" autoComplete="off"
/> />
</div> </div>

View File

@ -81,6 +81,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
{ {
title: t('automate.stepTitle', 'Automations'), title: t('automate.stepTitle', 'Automations'),
isVisible: true, isVisible: true,
onCollapsedClick: ()=> setCurrentStep('selection'),
content: currentStep === 'selection' ? renderCurrentStep() : null content: currentStep === 'selection' ? renderCurrentStep() : null
}, },
{ {