From 775723f5603e5bd7187fd1f087c8db4e3ac531b5 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Wed, 20 Aug 2025 18:30:33 +0100 Subject: [PATCH] Selection and creation --- .../tools/automate/AutomationCreation.tsx | 141 ++++++++++-------- .../tools/automate/AutomationEntry.tsx | 107 ++++++++++--- .../tools/automate/AutomationSelection.tsx | 23 ++- .../tools/automate/ToolConfigurationModal.tsx | 6 +- .../tools/automate/ToolSelector.tsx | 113 +++++++++----- .../tools/toolPicker/ToolSearch.tsx | 42 ++++-- .../src/data/useTranslatedToolRegistry.tsx | 6 +- frontend/src/tools/Automate.tsx | 19 ++- 8 files changed, 311 insertions(+), 146 deletions(-) diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index e9f8de43f..d169d160a 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -22,8 +22,14 @@ import ToolConfigurationModal from './ToolConfigurationModal'; import ToolSelector from './ToolSelector'; import AutomationEntry from './AutomationEntry'; +export enum AutomationMode { + CREATE = 'create', + EDIT = 'edit', + SUGGESTED = 'suggested' +} + interface AutomationCreationProps { - mode: 'custom' | 'suggested' | 'create'; + mode: AutomationMode; existingAutomation?: any; onBack: () => void; onComplete: (automation: any) => void; @@ -49,19 +55,24 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o // Initialize based on mode and existing automation useEffect(() => { - if (mode === 'suggested' && existingAutomation) { - setAutomationName(existingAutomation.name); + if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) { + setAutomationName(existingAutomation.name || ''); - const tools = existingAutomation.operations.map((op: string) => ({ - id: `${op}-${Date.now()}`, - operation: op, - name: getToolName(op), - configured: false, - parameters: {} - })); + // Handle both string array (suggested) and object array (custom) operations + const operations = existingAutomation.operations || []; + const tools = operations.map((op: any, index: number) => { + const operation = typeof op === 'string' ? op : op.operation; + return { + id: `${operation}-${Date.now()}-${index}`, + operation: operation, + name: getToolName(operation), + configured: mode === AutomationMode.EDIT ? true : (typeof op === 'object' ? op.configured || false : false), + parameters: typeof op === 'object' ? op.parameters || {} : {} + }; + }); setSelectedTools(tools); - } else if (mode === 'create' && selectedTools.length === 0) { + } else if (mode === AutomationMode.CREATE && selectedTools.length === 0) { // Initialize with 2 empty tools for new automation const defaultTools = [ { @@ -217,60 +228,72 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o style={{ border: '1px solid var(--mantine-color-gray-2)', borderRadius: 'var(--mantine-radius-sm)', - backgroundColor: 'white' + position: 'relative', + padding: 'var(--mantine-spacing-xs)' }} > - + {/* Delete X in top right */} + removeTool(index)} + title={t('automate.creation.tools.remove', 'Remove tool')} + style={{ + position: 'absolute', + top: '4px', + right: '4px', + zIndex: 1, + color: 'var(--mantine-color-gray-6)' + }} + > + + -
- {/* Tool Selection Dropdown */} - { - 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} - /> -
+
+ {/* Tool Selection Dropdown with inline settings cog */} + +
+ { + 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} + /> +
- - {tool.configured ? ( - - ) : ( - + {/* Settings cog - only show if tool is selected, aligned right */} + {tool.operation && ( + configureTool(index)} + title={t('automate.creation.tools.configure', 'Configure tool')} + style={{ color: 'var(--mantine-color-gray-6)' }} + > + + )} - - configureTool(index)} - title={t('automate.creation.tools.configure', 'Configure tool')} - > - - - - removeTool(index)} - title={t('automate.creation.tools.remove', 'Remove tool')} - > - - -
+ + {/* Configuration status underneath */} + {tool.operation && !tool.configured && ( + + {t('automate.creation.tools.notConfigured', "! Not Configured")} + + )} +
{index < selectedTools.length - 1 && ( diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx index f2e28ee59..906b834f5 100644 --- a/frontend/src/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -1,6 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Group, Text, Badge } from '@mantine/core'; +import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; interface AutomationEntryProps { /** Optional title for the automation (usually for custom ones) */ @@ -13,6 +16,12 @@ interface AutomationEntryProps { onClick: () => void; /** Whether to keep the icon at normal color (for special cases like "Add New") */ keepIconColor?: boolean; + /** Show menu for saved/suggested automations */ + showMenu?: boolean; + /** Edit handler */ + onEdit?: () => void; + /** Delete handler */ + onDelete?: () => void; } export default function AutomationEntry({ @@ -20,9 +29,17 @@ export default function AutomationEntry({ badgeIcon: BadgeIcon, operations, onClick, - keepIconColor = false + keepIconColor = false, + showMenu = false, + onEdit, + onDelete }: AutomationEntryProps) { const { t } = useTranslation(); + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + // Keep item in hovered state if menu is open + const shouldShowHovered = isHovered || isMenuOpen; const renderContent = () => { if (title) { @@ -32,11 +49,11 @@ export default function AutomationEntry({ {BadgeIcon && ( )} - + {title}
@@ -48,14 +65,14 @@ export default function AutomationEntry({ {BadgeIcon && ( )} {operations.map((op, index) => ( - + {t(`${op}.title`, op)} @@ -73,20 +90,74 @@ export default function AutomationEntry({ }; return ( - + +
+ {renderContent()} +
+ + {showMenu && ( + setIsMenuOpen(true)} + onClose={() => setIsMenuOpen(false)} + > + + e.stopPropagation()} + style={{ + opacity: shouldShowHovered ? 1 : 0, + transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', + transition: 'opacity 0.3s ease, transform 0.3s ease', + pointerEvents: shouldShowHovered ? 'auto' : 'none' + }} + > + + + + + + {onEdit && ( + } + onClick={(e) => { + e.stopPropagation(); + onEdit(); + }} + > + {t('common.edit', 'Edit')} + + )} + {onDelete && ( + } + onClick={(e) => { + e.stopPropagation(); + onDelete(); + }} + > + {t('common.delete', 'Delete')} + + )} + + + )} +
+ ); } diff --git a/frontend/src/components/tools/automate/AutomationSelection.tsx b/frontend/src/components/tools/automate/AutomationSelection.tsx index 3f32e5f6b..96fe4d75e 100644 --- a/frontend/src/components/tools/automate/AutomationSelection.tsx +++ b/frontend/src/components/tools/automate/AutomationSelection.tsx @@ -5,17 +5,23 @@ import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; import SettingsIcon from "@mui/icons-material/Settings"; import AutomationEntry from "./AutomationEntry"; import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations"; -import { useSavedAutomations } from "../../../hooks/tools/automate/useSavedAutomations"; interface AutomationSelectionProps { - onSelectCustom: () => void; - onSelectSuggested: (automation: any) => void; + savedAutomations: any[]; onCreateNew: () => void; + onRun: (automation: any) => void; + onEdit: (automation: any) => void; + onDelete: (automation: any) => void; } -export default function AutomationSelection({ onSelectCustom, onSelectSuggested, onCreateNew }: AutomationSelectionProps) { +export default function AutomationSelection({ + savedAutomations, + onCreateNew, + onRun, + onEdit, + onDelete +}: AutomationSelectionProps) { const { t } = useTranslation(); - const { savedAutomations } = useSavedAutomations(); const suggestedAutomations = useSuggestedAutomations(); return ( @@ -39,7 +45,10 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested, title={automation.name} badgeIcon={SettingsIcon} operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)} - onClick={() => onSelectCustom()} + onClick={() => onRun(automation)} + showMenu={true} + onEdit={() => onEdit(automation)} + onDelete={() => onDelete(automation)} /> ))} @@ -55,7 +64,7 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested, key={automation.id} badgeIcon={automation.icon} operations={automation.operations} - onClick={() => onSelectSuggested(automation)} + onClick={() => onRun(automation)} /> ))} diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx index 00ddb0cd6..32537026e 100644 --- a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -128,10 +128,10 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel if (tool.parameters) { setParameters(tool.parameters); } else if (parameterHook) { - // If we have a parameter hook, use it to get default values + // If we have a parameter module, use its default parameters try { - const defaultParams = parameterHook(); - setParameters(defaultParams.parameters || {}); + const defaultParams = parameterHook.defaultParameters || {}; + setParameters(defaultParams); } catch (error) { console.warn(`Error getting default parameters for ${tool.operation}:`, error); setParameters({}); diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 30515179b..9e7635016 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; @@ -14,10 +14,10 @@ interface ToolSelectorProps { placeholder?: string; // Custom placeholder text } -export default function ToolSelector({ - onSelect, - excludeTools = [], - toolRegistry, +export default function ToolSelector({ + onSelect, + excludeTools = [], + toolRegistry, selectedValue, placeholder }: ToolSelectorProps) { @@ -35,7 +35,7 @@ export default function ToolSelector({ if (!searchTerm.trim()) { return baseFilteredTools; } - + const lowercaseSearch = searchTerm.toLowerCase(); return baseFilteredTools.filter(([key, tool]) => { return ( @@ -57,28 +57,34 @@ export default function ToolSelector({ // 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) => { + const handleToolSelect = useCallback((toolKey: string) => { onSelect(toolKey); setOpened(false); - setSearchTerm(''); - }; + setSearchTerm(''); // Clear search to show the selected tool display + }, [onSelect]); + + const renderedTools = useMemo(() => + displayGroups.map((subcategory) => + renderToolButtons(subcategory, null, handleToolSelect, !isSearching) + ), [displayGroups, handleToolSelect, isSearching] + ); const handleSearchFocus = () => { setOpened(true); @@ -100,44 +106,77 @@ export default function ToolSelector({ }; return ( - - -
- -
-
+
+ { + setOpened(isOpen); + // Clear search term when menu closes to show proper display + if (!isOpen) { + setSearchTerm(''); + } + }} + closeOnClickOutside={true} + closeOnEscape={true} + position="bottom-start" + offset={4} + withinPortal={false} + trapFocus={false} + shadow="sm" + transitionProps={{ duration: 0 }} + > + +
+ {selectedValue && toolRegistry[selectedValue] && !opened ? ( + // Show selected tool in AutomationEntry style when tool is selected and not searching +
+
+
+ {toolRegistry[selectedValue].icon} +
+ + {toolRegistry[selectedValue].name} + +
+
+ ) : ( + // Show search input when no tool selected or actively searching + + )} +
+
- + {displayGroups.length === 0 ? ( - {isSearching + {isSearching ? t('tools.noSearchResults', 'No tools found') : t('tools.noTools', 'No tools available') } ) : ( - displayGroups.map((subcategory) => - renderToolButtons(subcategory, null, handleToolSelect, !isSearching) - ) + renderedTools )}
+
); -} \ No newline at end of file +} diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index 0cb86aa1e..c17784a52 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -14,21 +14,24 @@ interface ToolSearchProps { selectedToolKey?: string | null; placeholder?: string; hideIcon?: boolean; + onFocus?: () => void; } -const ToolSearch = ({ - value, - onChange, - toolRegistry, - onToolSelect, +const ToolSearch = ({ + value, + onChange, + toolRegistry, + onToolSelect, mode = 'filter', selectedToolKey, placeholder, - hideIcon = false + hideIcon = false, + onFocus }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); const searchRef = useRef(null); + const dropdownRef = useRef(null); const filteredTools = useMemo(() => { if (!value.trim()) return []; @@ -51,7 +54,12 @@ const ToolSearch = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (searchRef.current && !searchRef.current.contains(event.target as Node)) { + if ( + searchRef.current && + dropdownRef.current && + !searchRef.current.contains(event.target as Node) && + !dropdownRef.current.contains(event.target as Node) + ) { setDropdownOpen(false); } }; @@ -68,6 +76,7 @@ const ToolSearch = ({ placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} icon={hideIcon ? undefined : search} autoComplete="off" + /> ); @@ -81,19 +90,19 @@ const ToolSearch = ({ {searchInput} {dropdownOpen && filteredTools.length > 0 && (
@@ -101,7 +110,10 @@ const ToolSearch = ({