From 8ee6d798583d5c1134a80115dec920faf920aa1a Mon Sep 17 00:00:00 2001 From: Vukasin Date: Tue, 29 Jul 2025 22:57:58 +0200 Subject: [PATCH] feat: workflow ajustments --- .../workflows/hooks/use-workflow-steps.ts | 115 +++++++++++--- .../workflows/steps/conditional-group.tsx | 141 ++++++++++-------- .../components/workflows/steps/step-card.tsx | 59 ++++++-- .../workflows/steps/workflow-steps.tsx | 14 +- .../components/workflows/workflow-builder.tsx | 10 +- .../workflows/workflow-definitions.ts | 35 ++++- .../components/workflows/workflow-layout.tsx | 2 +- .../workflows/workflow-side-panel.tsx | 5 +- 8 files changed, 278 insertions(+), 103 deletions(-) diff --git a/frontend/src/components/workflows/hooks/use-workflow-steps.ts b/frontend/src/components/workflows/hooks/use-workflow-steps.ts index 5ff8cb80..fd7816f9 100644 --- a/frontend/src/components/workflows/hooks/use-workflow-steps.ts +++ b/frontend/src/components/workflows/hooks/use-workflow-steps.ts @@ -23,6 +23,10 @@ interface UseWorkflowStepsProps { setSelectedStep: (step: ConditionalStep | null) => void; setInsertIndex: (index: number) => void; setSearchQuery: (query: string) => void; + selectedStep: ConditionalStep | null; + insertIndex: number; + parentStepId: string | null; + setParentStepId: (id: string | null) => void; } const STEP_CATEGORIES = CATEGORY_DEFINITIONS; @@ -65,7 +69,11 @@ export function useWorkflowSteps({ setPanelMode, setSelectedStep, setInsertIndex, - setSearchQuery + setSearchQuery, + selectedStep, + insertIndex, + parentStepId, + setParentStepId }: UseWorkflowStepsProps) { const generateId = () => Math.random().toString(36).substr(2, 9); @@ -81,18 +89,20 @@ export function useWorkflowSteps({ })); }, [agentTools]); - const handleAddStep = useCallback((index: number) => { + const handleAddStep = useCallback((index: number, parentId?: string) => { setInsertIndex(index); setPanelMode('add'); setSelectedStep(null); + setParentStepId(parentId || null); setIsPanelOpen(true); - }, [setInsertIndex, setPanelMode, setSelectedStep, setIsPanelOpen]); + }, [setInsertIndex, setPanelMode, setSelectedStep, setParentStepId, setIsPanelOpen]); const handleEditStep = useCallback((step: ConditionalStep) => { setSelectedStep(step); setPanelMode('edit'); + setParentStepId(null); setIsPanelOpen(true); - }, [setSelectedStep, setPanelMode, setIsPanelOpen]); + }, [setSelectedStep, setPanelMode, setParentStepId, setIsPanelOpen]); const handleCreateStep = useCallback((stepType: StepType) => { const newStep: ConditionalStep = { @@ -113,34 +123,103 @@ export function useWorkflowSteps({ }; } - const newSteps = [...steps]; - newSteps.push(newStep); + // Check if we're adding a child step to a condition + if (parentStepId) { + // Find the parent step and add the new step as its child + const findAndUpdateStep = (stepsArray: ConditionalStep[]): ConditionalStep[] => { + return stepsArray.map(step => { + if (step.id === parentStepId) { + return { + ...step, + children: [...(step.children || []), { ...newStep, order: step.children?.length || 0 }] + }; + } + // Also check nested children for conditional steps + if (step.children && step.children.length > 0) { + return { + ...step, + children: findAndUpdateStep(step.children) + }; + } + return step; + }); + }; - // Update order values - newSteps.forEach((step, idx) => { - step.order = idx; - }); + const newSteps = findAndUpdateStep(steps); + onStepsChange(newSteps); + } else { + // Regular step addition to main workflow + const newSteps = [...steps]; + if (insertIndex >= 0 && insertIndex < steps.length) { + newSteps.splice(insertIndex, 0, newStep); + } else { + newSteps.push(newStep); + } - onStepsChange(newSteps); + // Update order values + newSteps.forEach((step, idx) => { + step.order = idx; + }); + + onStepsChange(newSteps); + } - // Instead of closing panel, switch to edit mode with the new step + // Switch to edit mode for the newly created step setSelectedStep(newStep); setPanelMode('edit'); + setParentStepId(null); setSearchQuery(''); - }, [steps, onStepsChange, setSelectedStep, setPanelMode, setSearchQuery]); + }, [steps, onStepsChange, parentStepId, insertIndex, setSelectedStep, setPanelMode, setParentStepId, setSearchQuery]); const handleUpdateStep = useCallback((updates: Partial) => { - const newSteps = steps.map(step => - step.id === updates.id ? { ...step, ...updates } : step - ); + // Recursive function to update steps at any level + const updateStepRecursive = (stepsArray: ConditionalStep[]): ConditionalStep[] => { + return stepsArray.map(step => { + if (step.id === updates.id) { + return { ...step, ...updates }; + } + // Check nested children for conditional steps + if (step.children && step.children.length > 0) { + return { + ...step, + children: updateStepRecursive(step.children) + }; + } + return step; + }); + }; + + const newSteps = updateStepRecursive(steps); onStepsChange(newSteps); - }, [steps, onStepsChange]); + + // Update the selected step if it's the one being updated + if (updates.id === selectedStep?.id) { + setSelectedStep({ ...selectedStep, ...updates }); + } + }, [steps, onStepsChange, selectedStep, setSelectedStep]); const handleDeleteStep = useCallback((stepId: string) => { - const newSteps = steps.filter(step => step.id !== stepId); + // Recursive function to delete steps at any level + const deleteStepRecursive = (stepsArray: ConditionalStep[]): ConditionalStep[] => { + const filtered = stepsArray.filter(step => step.id !== stepId); + return filtered.map(step => { + if (step.children && step.children.length > 0) { + return { + ...step, + children: deleteStepRecursive(step.children) + }; + } + return step; + }); + }; + + const newSteps = deleteStepRecursive(steps); + + // Update order values for top-level steps newSteps.forEach((step, idx) => { step.order = idx; }); + onStepsChange(newSteps); setIsPanelOpen(false); }, [steps, onStepsChange, setIsPanelOpen]); diff --git a/frontend/src/components/workflows/steps/conditional-group.tsx b/frontend/src/components/workflows/steps/conditional-group.tsx index 60888d40..54d15aa8 100644 --- a/frontend/src/components/workflows/steps/conditional-group.tsx +++ b/frontend/src/components/workflows/steps/conditional-group.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import { Plus, AlertTriangle, GripVertical } from 'lucide-react'; +import { Plus, AlertTriangle, GripVertical, Settings } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; @@ -15,28 +15,54 @@ import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' interface ConditionalGroupProps { conditionSteps: ConditionalStep[]; groupKey: string; - onUpdate: (updates: Partial) => void; + onUpdateStep: (updates: Partial) => void; onAddElse: (afterStepId: string) => void; onAddElseIf: (afterStepId: string) => void; onRemove: (stepId: string) => void; onEdit: (step: ConditionalStep) => void; + onAddStep: (index: number, parentStepId?: string) => void; agentTools?: any; isLoadingTools?: boolean; } +const MAX_ELSE_IF_CONDITIONS = 5; // Limit the number of else-if conditions + export function ConditionalGroup({ conditionSteps, groupKey, - onUpdate, + onUpdateStep, onAddElse, onAddElseIf, onRemove, onEdit, + onAddStep, agentTools, isLoadingTools }: ConditionalGroupProps) { const [activeConditionTab, setActiveConditionTab] = useState(conditionSteps[0]?.id || ''); + + // Ensure we always have an "else" step at the end const hasElse = conditionSteps.some(step => step.conditions?.type === 'else'); + const elseIfCount = conditionSteps.filter(step => step.conditions?.type === 'elseif').length; + const canAddElseIf = !hasElse && elseIfCount < MAX_ELSE_IF_CONDITIONS; + + // Create a virtual else step if none exists to show the tab + const displaySteps = React.useMemo(() => { + if (!hasElse) { + const virtualElse: ConditionalStep = { + id: 'virtual-else', + name: 'Else', + description: '', + type: 'condition', + config: {}, + conditions: { type: 'else' }, + children: [], + order: conditionSteps.length + }; + return [...conditionSteps, virtualElse]; + } + return conditionSteps; + }, [conditionSteps, hasElse]); const { attributes, @@ -56,7 +82,25 @@ export function ConditionalGroup({ return String.fromCharCode(65 + index); }; - const activeStep = conditionSteps.find(s => s.id === activeConditionTab) || conditionSteps[0]; + // Ensure we have a valid active tab when steps change + React.useEffect(() => { + if (!displaySteps.find(s => s.id === activeConditionTab)) { + setActiveConditionTab(displaySteps[0]?.id || ''); + } + }, [displaySteps, activeConditionTab]); + + const activeStep = displaySteps.find(s => s.id === activeConditionTab) || displaySteps[0]; + + // Handle clicking add step button + const handleAddStepClick = React.useCallback(() => { + if (activeStep?.id === 'virtual-else') { + // If clicking on virtual else, create a real else step first + onAddElse(conditionSteps[conditionSteps.length - 1].id); + } else { + // Call onAddStep with index and parent step ID + onAddStep(-1, activeStep?.id); + } + }, [activeStep, onAddElse, conditionSteps, onAddStep]); return (
@@ -71,58 +115,56 @@ export function ConditionalGroup({
+ {/* Gear icon for editing - appears on hover */} +
+ +
+ {/* Condition Tabs */} -
- {conditionSteps.map((step, index) => { +
+ {displaySteps.map((step, index) => { const letter = getConditionLetter(index); const isActive = step.id === activeConditionTab; const conditionType = step.conditions?.type === 'if' ? 'If' : step.conditions?.type === 'elseif' ? 'Else If' : step.conditions?.type === 'else' ? 'Else' : 'If'; + return ( - + ); })} - {/* Add Else If button */} - {!hasElse && ( + {/* Plus button to add Else If - always show next to Else if we can add more */} + {canAddElseIf && ( - )} - - {/* Add Else button */} - {!hasElse && ( - )}
@@ -130,30 +172,8 @@ export function ConditionalGroup({ {/* Active condition content */} {activeStep && (
- {(activeStep.conditions?.type === 'if' || activeStep.conditions?.type === 'elseif') ? ( -
- - onUpdate({ - id: activeStep.id, - conditions: { ...activeStep.conditions, expression: e.target.value } - })} - placeholder="e.g., user asks about pricing" - className="w-full" - /> -
- ) : ( -
- Otherwise (fallback condition) -
- )} - {/* Steps within this condition */} -
+
{activeStep.children && activeStep.children.length > 0 ? ( child.id)} strategy={verticalListSortingStrategy}>
@@ -164,7 +184,7 @@ export function ConditionalGroup({ stepNumber={index + 1} isNested={true} onEdit={onEdit} - onUpdate={onUpdate} + onUpdateStep={onUpdateStep} agentTools={agentTools} isLoadingTools={isLoadingTools} /> @@ -177,19 +197,12 @@ export function ConditionalGroup({
)} - {/* Add step button */} + {/* Add step button - triggers special edit mode for adding child steps */}
)} {step.config?.tool_name && ( - + {step.config.tool_name} )} diff --git a/frontend/src/components/workflows/steps/workflow-steps.tsx b/frontend/src/components/workflows/steps/workflow-steps.tsx index 3fd23f71..21e06992 100644 --- a/frontend/src/components/workflows/steps/workflow-steps.tsx +++ b/frontend/src/components/workflows/steps/workflow-steps.tsx @@ -27,12 +27,13 @@ import { interface WorkflowStepsProps { steps: ConditionalStep[]; - onAddStep: (index: number) => void; + onAddStep: (index: number, parentStepId?: string) => void; onEditStep: (step: ConditionalStep) => void; onUpdateStep: (updates: Partial) => void; onDeleteStep: (stepId: string) => void; onAddElseIf: (afterStepId: string) => void; onAddElse: (afterStepId: string) => void; + onStepsChange: (steps: ConditionalStep[]) => void; agentTools?: any; isLoadingTools?: boolean; } @@ -45,6 +46,7 @@ export function WorkflowSteps({ onDeleteStep, onAddElseIf, onAddElse, + onStepsChange, agentTools, isLoadingTools }: WorkflowStepsProps) { @@ -123,8 +125,7 @@ export function WorkflowSteps({ }); // Call the parent's onStepsChange - // This would need to be passed down from the parent - // For now, we'll just update the local state + onStepsChange(newSteps); } } }; @@ -145,11 +146,12 @@ export function WorkflowSteps({ @@ -159,7 +161,7 @@ export function WorkflowSteps({ step={item} stepNumber={index + 1} onEdit={onEditStep} - onUpdate={onUpdateStep} + onUpdateStep={onUpdateStep} agentTools={agentTools} isLoadingTools={isLoadingTools} /> @@ -211,7 +213,7 @@ export function WorkflowSteps({ step={activeStep} stepNumber={1} onEdit={() => { }} - onUpdate={() => { }} + onUpdateStep={() => { }} /> ); })()} diff --git a/frontend/src/components/workflows/workflow-builder.tsx b/frontend/src/components/workflows/workflow-builder.tsx index b74d8faa..8892147d 100644 --- a/frontend/src/components/workflows/workflow-builder.tsx +++ b/frontend/src/components/workflows/workflow-builder.tsx @@ -53,6 +53,7 @@ export function WorkflowBuilder({ const [selectedStep, setSelectedStep] = useState(null); const [insertIndex, setInsertIndex] = useState(-1); const [searchQuery, setSearchQuery] = useState(''); + const [parentStepId, setParentStepId] = useState(null); const { handleAddStep, @@ -72,7 +73,11 @@ export function WorkflowBuilder({ setPanelMode, setSelectedStep, setInsertIndex, - setSearchQuery + setSearchQuery, + selectedStep, + insertIndex, + parentStepId, + setParentStepId }); const handleToggleSidePanel = () => { @@ -98,7 +103,7 @@ export function WorkflowBuilder({ onDeleteStep={handleDeleteStep} isLoadingTools={isLoadingTools} > -
+
{steps.length === 0 ? ( // Empty state @@ -130,6 +135,7 @@ export function WorkflowBuilder({ onDeleteStep={handleDeleteStep} onAddElseIf={handleAddElseIf} onAddElse={handleAddElse} + onStepsChange={onStepsChange} agentTools={agentTools} isLoadingTools={isLoadingTools} /> diff --git a/frontend/src/components/workflows/workflow-definitions.ts b/frontend/src/components/workflows/workflow-definitions.ts index 2c6fb789..cc6d16cc 100644 --- a/frontend/src/components/workflows/workflow-definitions.ts +++ b/frontend/src/components/workflows/workflow-definitions.ts @@ -1,4 +1,4 @@ -import { FileText, Terminal, Rocket, Computer, Eye, Search, Globe, GitBranch, Settings, MonitorPlay } from 'lucide-react'; +import { FileText, Terminal, Rocket, Computer, Eye, Search, Globe, GitBranch, Settings, MonitorPlay, Cog, Key } from 'lucide-react'; export interface StepDefinition { id: string; @@ -44,6 +44,8 @@ export const TOOL_COLORS: Record = { export const ACTION_ICONS: Record = { 'FileText': FileText, 'GitBranch': GitBranch, + 'Cog': Cog, + 'Key': Key, }; // Base step definitions @@ -72,6 +74,24 @@ export const BASE_STEP_DEFINITIONS: StepDefinition[] = [ category: 'conditions', color: 'from-orange-500/20 to-orange-600/10 border-orange-500/20 text-orange-500' }, + { + id: 'mcp_configuration', + name: 'MCP Configuration', + description: 'Configure MCP server connections and settings', + icon: Cog, + category: 'configuration', + color: 'from-indigo-500/20 to-indigo-600/10 border-indigo-500/20 text-indigo-500', + config: { step_type: 'mcp_configuration' } + }, + { + id: 'credentials_profile', + name: 'Credentials Profile', + description: 'Select and configure credential profiles for authentication', + icon: Key, + category: 'configuration', + color: 'from-emerald-500/20 to-emerald-600/10 border-emerald-500/20 text-emerald-500', + config: { step_type: 'credentials_profile' } + }, ]; // Category definitions @@ -86,6 +106,11 @@ export const CATEGORY_DEFINITIONS: CategoryDefinition[] = [ name: 'Conditions', description: 'Add logic and branching' }, + { + id: 'configuration', + name: 'Configuration', + description: 'Setup and configure services' + }, { id: 'tools', name: 'Tools', @@ -131,6 +156,14 @@ export function getStepIconAndColor(stepType: any): { icon: any; color: string } return { icon, color }; } else if (stepType.category === 'conditions') { return { icon: Settings, color: 'from-orange-500/20 to-orange-600/10 border-orange-500/20 text-orange-500' }; + } else if (stepType.category === 'configuration') { + const stepType_id = stepType.config?.step_type || stepType.id; + if (stepType_id === 'mcp_configuration') { + return { icon: Cog, color: 'from-indigo-500/20 to-indigo-600/10 border-indigo-500/20 text-indigo-500' }; + } else if (stepType_id === 'credentials_profile') { + return { icon: Key, color: 'from-emerald-500/20 to-emerald-600/10 border-emerald-500/20 text-emerald-500' }; + } + return { icon: Cog, color: 'from-indigo-500/20 to-indigo-600/10 border-indigo-500/20 text-indigo-500' }; } else { const icon = ACTION_ICONS[stepType.icon] || FileText; return { icon, color: 'from-gray-500/20 to-gray-600/10 border-gray-500/20 text-gray-500' }; diff --git a/frontend/src/components/workflows/workflow-layout.tsx b/frontend/src/components/workflows/workflow-layout.tsx index 2e5c43cd..b5d39d40 100644 --- a/frontend/src/components/workflows/workflow-layout.tsx +++ b/frontend/src/components/workflows/workflow-layout.tsx @@ -60,7 +60,7 @@ export function WorkflowLayout({ isSaving={isSaving} /> -
+
{children}
diff --git a/frontend/src/components/workflows/workflow-side-panel.tsx b/frontend/src/components/workflows/workflow-side-panel.tsx index a73ef47d..aac80647 100644 --- a/frontend/src/components/workflows/workflow-side-panel.tsx +++ b/frontend/src/components/workflows/workflow-side-panel.tsx @@ -159,7 +159,7 @@ export function WorkflowSidePanel({ onUpdateStep({ name: e.target.value })} + onChange={(e) => onUpdateStep({ id: selectedStep.id, name: e.target.value })} placeholder="Step name" className="mt-1" /> @@ -170,7 +170,7 @@ export function WorkflowSidePanel({