diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index 9dec8f45a..033cca399 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -5,8 +5,8 @@ import { ToolRegistryEntry } from "../../data/toolsTaxonomy"; import ToolButton from "./toolPicker/ToolButton"; import "./toolPicker/ToolPicker.css"; import { useToolSections } from "../../hooks/useToolSections"; -import SubcategoryHeader from "./shared/SubcategoryHeader"; import NoToolsFound from "./shared/NoToolsFound"; +import { renderToolButtons } from "./shared/renderToolButtons"; interface ToolPickerProps { selectedToolKey: string | null; @@ -15,31 +15,6 @@ interface ToolPickerProps { 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 -) => ( - - {showSubcategoryHeader && ( - - )} - - {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( - - ))} - - -); - const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => { const { t } = useTranslation(); const [quickHeaderHeight, setQuickHeaderHeight] = useState(0); diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 4beb99ce4..8a51bc224 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -1,31 +1,29 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Button, - Text, - Title, - Stack, - Group, - Select, - TextInput, +import { + Button, + Text, + Title, + Stack, + Group, + TextInput, ActionIcon, Divider } 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 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 { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; import ToolConfigurationModal from './ToolConfigurationModal'; -import AutomationEntry from './AutomationEntry'; +import ToolSelector from './ToolSelector'; interface AutomationCreationProps { mode: 'custom' | 'suggested' | 'create'; existingAutomation?: any; onBack: () => void; - onComplete: () => void; + onComplete: (automation: any) => void; } interface AutomationTool { @@ -38,8 +36,8 @@ interface AutomationTool { export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete }: AutomationCreationProps) { const { t } = useTranslation(); - const { toolRegistry } = useToolWorkflow(); - + const toolRegistry = useFlatToolRegistry(); + const [automationName, setAutomationName] = useState(''); const [selectedTools, setSelectedTools] = useState([]); const [configModalOpen, setConfigModalOpen] = useState(false); @@ -49,7 +47,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o useEffect(() => { if (mode === 'suggested' && existingAutomation) { setAutomationName(existingAutomation.name); - + const tools = existingAutomation.operations.map((op: string) => ({ id: `${op}-${Date.now()}`, operation: op, @@ -57,7 +55,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o configured: false, parameters: {} })); - + setSelectedTools(tools); } }, [mode, existingAutomation]); @@ -67,20 +65,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o return tool?.name || t(`tools.${operation}.name`, operation); }; - const getAvailableTools = () => { - 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) => { - const addTool = (operation: string | null) => { - if (!operation) return; - const newTool: AutomationTool = { id: `${operation}-${Date.now()}`, operation, @@ -88,7 +74,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o configured: false, parameters: {} }; - + setSelectedTools([...selectedTools, newTool]); }; @@ -143,7 +129,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o try { const { automationStorage } = await import('../../../services/automationStorage'); await automationStorage.saveAutomation(automation); - onComplete(); + onComplete(automation); } catch (error) { console.error('Error saving automation:', error); } @@ -153,17 +139,10 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o return ( - - - {mode === 'create' - ? t('automate.creation.title.create', 'Create Automation') - : t('automate.creation.title.configure', 'Configure Automation') - } - - - - - + + {t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")} + + {/* Automation Name */} @@ -175,15 +154,9 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o /> {/* Add Tool Selector */} - } - size="sm" + {/* Selected Tools */} @@ -194,7 +167,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o {index + 1} - + @@ -207,7 +180,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o )} - + - + {index < selectedTools.length - 1 && ( → )} @@ -260,4 +233,4 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o )} ); -} \ No newline at end of file +} diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx new file mode 100644 index 000000000..7ba4b2e15 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -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 = {}; + 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 ( + + + + + + + + + + + {displayGroups.length === 0 ? ( + + {isSearching + ? t('tools.noSearchResults', 'No tools found') + : t('tools.noTools', 'No tools available') + } + + ) : ( + displayGroups.map((subcategory) => + renderToolButtons(subcategory, null, handleToolSelect, !isSearching) + ) + )} + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/automate/ToolSequence.tsx b/frontend/src/components/tools/automate/ToolSequence.tsx new file mode 100644 index 000000000..e16985163 --- /dev/null +++ b/frontend/src/components/tools/automate/ToolSequence.tsx @@ -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([]); + 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 ; + case 'error': + return ; + case 'running': + return ; + default: + return ; + } + }; + + 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 ( + + + + {t('automate.sequence.title', 'Tool Sequence')} + + + + + + + + {/* Automation Info */} + + + {automation?.name || t('automate.sequence.unnamed', 'Unnamed Automation')} + + + {t('automate.sequence.steps', '{{count}} steps', { count: executionSteps.length })} + + + + {/* File Selection Warning */} + {(!activeFiles || activeFiles.length === 0) && ( + + {t('automate.sequence.noFilesDesc', 'Please select files to process before running the automation.')} + + )} + + {/* Progress Bar */} + {isExecuting && ( + + + {t('automate.sequence.progress', 'Progress: {{current}}/{{total}}', { + current: currentStepIndex + 1, + total: executionSteps.length + })} + + + + )} + + {/* Execution Steps */} + + {executionSteps.map((step, index) => ( + + + {index + 1} + + + {getStepIcon(step)} + + + + {step.name} + + {step.error && ( + + {step.error} + + )} + + + ))} + + + {/* Action Buttons */} + + } + onClick={executeAutomation} + disabled={isExecuting || !activeFiles || activeFiles.length === 0} + loading={isExecuting} + > + {isExecuting + ? t('automate.sequence.running', 'Running Automation...') + : t('automate.sequence.run', 'Run Automation') + } + + + {(allStepsCompleted || hasErrors) && ( + + {t('automate.sequence.finish', 'Finish')} + + )} + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx new file mode 100644 index 000000000..28218f26f --- /dev/null +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -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 +) => ( + + {showSubcategoryHeader && ( + + )} + + {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( + + ))} + + +); \ 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 f01a9f87d..516a40ad1 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -12,6 +12,7 @@ interface ToolSearchProps { onToolSelect?: (toolId: string) => void; mode: 'filter' | 'dropdown'; selectedToolKey?: string | null; + placeholder?: string; } const ToolSearch = ({ @@ -20,7 +21,8 @@ const ToolSearch = ({ toolRegistry, onToolSelect, mode = 'filter', - selectedToolKey + selectedToolKey, + placeholder }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -61,7 +63,7 @@ const ToolSearch = ({ ref={searchRef} value={value} onChange={handleSearchChange} - placeholder={t("toolPicker.searchPlaceholder", "Search tools...")} + placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} icon={search} autoComplete="off" /> diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 740f1f791..7d5232b50 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -6,6 +6,7 @@ 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 ToolSequence from "../components/tools/automate/ToolSequence"; import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation"; import { BaseToolProps } from "../types/tool"; @@ -15,7 +16,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { setCurrentMode } = useFileContext(); const { selectedFiles } = useToolFileSelection(); - const [currentStep, setCurrentStep] = useState<'selection' | 'creation'>('selection'); + const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'sequence'>('selection'); const [stepData, setStepData] = useState({}); const automateOperation = useAutomateOperation(); @@ -49,6 +50,15 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { mode={stepData.mode} existingAutomation={stepData.automation} onBack={() => handleStepChange({ step: 'selection' })} + onComplete={(automation: any) => handleStepChange({ step: 'sequence', automation })} + /> + ); + + case 'sequence': + return ( + handleStepChange({ step: 'creation', mode: stepData.mode, automation: stepData.automation })} onComplete={handleComplete} /> ); @@ -68,7 +78,12 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { { title: t('automate.stepTitle', 'Automations'), 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: {