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,
TextInput,
ActionIcon,
Divider
Divider,
Modal
} from '@mantine/core';
import DeleteIcon from '@mui/icons-material/Delete';
import SettingsIcon from '@mui/icons-material/Settings';
import CheckIcon from '@mui/icons-material/Check';
import 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 ToolConfigurationModal from './ToolConfigurationModal';
import ToolSelector from './ToolSelector';
import AutomationEntry from './AutomationEntry';
interface AutomationCreationProps {
mode: 'custom' | 'suggested' | 'create';
@ -41,6 +45,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
// Initialize based on mode and existing automation
useEffect(() => {
@ -56,8 +61,27 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
}));
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 tool = toolRegistry?.[operation] as any;
@ -78,6 +102,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
};
const removeTool = (index: number) => {
// Don't allow removing tools if only 2 remain
if (selectedTools.length <= 2) return;
setSelectedTools(selectedTools.filter((_, i) => i !== index));
};
@ -105,14 +131,38 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
setConfiguringToolIndex(-1);
};
const hasUnsavedChanges = () => {
return (
automationName.trim() !== '' ||
selectedTools.some(tool => tool.operation !== '' || tool.configured)
);
};
const canSaveAutomation = () => {
return (
automationName.trim() !== '' &&
selectedTools.length > 0 &&
selectedTools.every(tool => tool.configured)
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 () => {
if (!canSaveAutomation()) return;
@ -152,48 +202,70 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
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
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']}
toolRegistry={toolRegistry}
selectedValue={tool.operation}
placeholder={tool.name}
/>
</div>
{/* Selected Tools */}
{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>
<Group gap="xs" style={{ flexShrink: 0 }}>
{tool.configured ? (
<CheckIcon style={{ fontSize: 14, color: 'green' }} />
) : (
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
)}
</Group>
<Group gap="xs">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => configureTool(index)}
title={t('automate.creation.tools.configure', 'Configure tool')}
>
<SettingsIcon style={{ fontSize: 16 }} />
</ActionIcon>
<ActionIcon
variant="subtle"
size="sm"
color="red"
onClick={() => removeTool(index)}
title={t('automate.creation.tools.remove', 'Remove tool')}
>
<DeleteIcon style={{ fontSize: 16 }} />
</ActionIcon>
@ -202,11 +274,45 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
</div>
{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>
</div>
)}
<Divider />
@ -231,6 +337,28 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
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>
);
}

View File

@ -10,9 +10,17 @@ interface ToolSelectorProps {
onSelect: (toolKey: string) => void;
excludeTools?: string[];
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 [opened, setOpened] = useState(false);
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 (
<Menu
opened={opened}
@ -98,7 +114,8 @@ export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry
onChange={handleSearchChange}
toolRegistry={filteredToolRegistry}
mode="filter"
placeholder={t('automate.creation.tools.add', 'Add a tool...')}
placeholder={getDisplayValue()}
hideIcon={true}
/>
</div>
</Menu.Target>

View File

@ -13,6 +13,7 @@ interface ToolSearchProps {
mode: 'filter' | 'dropdown';
selectedToolKey?: string | null;
placeholder?: string;
hideIcon?: boolean;
}
const ToolSearch = ({
@ -22,7 +23,8 @@ const ToolSearch = ({
onToolSelect,
mode = 'filter',
selectedToolKey,
placeholder
placeholder,
hideIcon = false
}: ToolSearchProps) => {
const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false);
@ -64,7 +66,7 @@ const ToolSearch = ({
value={value}
onChange={handleSearchChange}
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"
/>
</div>

View File

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