feat: workflow ajustments

This commit is contained in:
Vukasin 2025-07-29 22:57:58 +02:00
parent 6148b199ae
commit 8ee6d79858
8 changed files with 278 additions and 103 deletions

View File

@ -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<ConditionalStep>) => {
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]);

View File

@ -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<ConditionalStep>) => void;
onUpdateStep: (updates: Partial<ConditionalStep>) => 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<string>(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 (
<div ref={setNodeRef} style={style} className={cn(isDragging && "opacity-50")}>
@ -71,58 +115,56 @@ export function ConditionalGroup({
</div>
</div>
{/* Gear icon for editing - appears on hover */}
<div className="absolute top-4 right-4 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(activeStep)}
className="h-8 w-8 p-0"
>
<Settings className="h-4 w-4" />
</Button>
</div>
{/* Condition Tabs */}
<div className="flex items-center gap-2 flex-wrap pl-5">
{conditionSteps.map((step, index) => {
<div className="flex items-center gap-2 flex-wrap pl-5 pr-12">
{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 (
<button
<Button
key={step.id}
onClick={() => setActiveConditionTab(step.id)}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all",
"h-9 px-3 border border-dashed text-xs",
isActive
? "bg-blue-500 text-white border-blue-500 shadow-sm"
: "bg-white dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700"
? "bg-blue-500 hover:bg-blue-600 "
: "bg-white dark:bg-zinc-800 border-dashed border-zinc-300 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-700"
)}
>
<span className="font-mono text-xs font-bold">{letter}</span>
<span></span>
<span>{conditionType}</span>
{step.hasIssues && (
<AlertTriangle className="h-3 w-3 text-red-500" />
)}
</button>
</Button>
);
})}
{/* Add Else If button */}
{!hasElse && (
{/* Plus button to add Else If - always show next to Else if we can add more */}
{canAddElseIf && (
<Button
variant="outline"
size="sm"
onClick={() => onAddElseIf(conditionSteps[conditionSteps.length - 1].id)}
className="h-9 px-3 border-dashed text-xs"
className="h-6 w-6 p-0 border border-dashed rounded-md border-zinc-300 dark:border-zinc-600 hover:border-zinc-400 dark:hover:border-zinc-500 bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
>
<Plus className="h-3 w-3 mr-1" />
Else If
</Button>
)}
{/* Add Else button */}
{!hasElse && (
<Button
variant="outline"
size="sm"
onClick={() => onAddElse(conditionSteps[conditionSteps.length - 1].id)}
className="h-9 px-3 border-dashed text-xs"
>
<Plus className="h-3 w-3 mr-1" />
Else
<Plus className="h-3 w-3 text-zinc-600 dark:text-zinc-400" />
</Button>
)}
</div>
@ -130,30 +172,8 @@ export function ConditionalGroup({
{/* Active condition content */}
{activeStep && (
<div className="bg-white dark:bg-zinc-800 rounded-2xl p-4 border border-zinc-200 dark:border-zinc-700">
{(activeStep.conditions?.type === 'if' || activeStep.conditions?.type === 'elseif') ? (
<div className="space-y-3">
<Label className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{activeStep.conditions?.type === 'if' ? 'If condition' : 'Else if condition'}
</Label>
<Input
type="text"
value={activeStep.conditions.expression || ''}
onChange={(e) => onUpdate({
id: activeStep.id,
conditions: { ...activeStep.conditions, expression: e.target.value }
})}
placeholder="e.g., user asks about pricing"
className="w-full"
/>
</div>
) : (
<div className="text-sm text-zinc-600 dark:text-zinc-400 font-medium">
Otherwise (fallback condition)
</div>
)}
{/* Steps within this condition */}
<div className="mt-4 space-y-3">
<div className="space-y-3">
{activeStep.children && activeStep.children.length > 0 ? (
<SortableContext items={activeStep.children.map(child => child.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
@ -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({
</div>
)}
{/* Add step button */}
{/* Add step button - triggers special edit mode for adding child steps */}
<div className="flex justify-center pt-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit({
id: 'new',
name: 'New Step',
description: '',
type: 'instruction',
config: {},
order: activeStep.children?.length || 0
} as ConditionalStep)}
onClick={handleAddStepClick}
className="border-dashed text-xs"
>
<Plus className="h-3 w-3 mr-1" />

View File

@ -15,7 +15,7 @@ interface StepCardProps {
stepNumber: number;
isNested?: boolean;
onEdit: (step: ConditionalStep) => void;
onUpdate: (updates: Partial<ConditionalStep>) => void;
onUpdateStep: (updates: Partial<ConditionalStep>) => void;
agentTools?: any;
isLoadingTools?: boolean;
}
@ -25,7 +25,7 @@ export function StepCard({
stepNumber,
isNested = false,
onEdit,
onUpdate,
onUpdateStep,
agentTools,
isLoadingTools
}: StepCardProps) {
@ -44,12 +44,53 @@ export function StepCard({
};
const getStepIcon = (step: ConditionalStep) => {
const stepType = {
category: step.config?.tool_type === 'agentpress' || step.config?.tool_type === 'mcp' ? 'tools' :
step.type === 'condition' ? 'conditions' : 'actions',
config: step.config,
icon: step.type === 'condition' ? 'Settings' : 'FileText'
};
// Determine the proper step type based on the step configuration
let stepType;
if (step.type === 'condition') {
stepType = {
category: 'conditions',
config: step.config,
icon: 'Settings'
};
} else if (step.config?.tool_type === 'agentpress' || step.config?.tool_type === 'mcp') {
stepType = {
category: 'tools',
config: step.config,
icon: 'FileText'
};
} else if (step.config?.step_type === 'mcp_configuration') {
stepType = {
category: 'configuration',
config: { step_type: 'mcp_configuration' },
icon: 'Cog'
};
} else if (step.config?.step_type === 'credentials_profile') {
stepType = {
category: 'configuration',
config: { step_type: 'credentials_profile' },
icon: 'Key'
};
} else if (step.type === 'instruction') {
stepType = {
category: 'actions',
config: step.config,
icon: 'FileText'
};
} else if (step.type === 'sequence') {
stepType = {
category: 'actions',
config: step.config,
icon: 'GitBranch'
};
} else {
// Default fallback
stepType = {
category: 'actions',
config: step.config,
icon: 'FileText'
};
}
const { icon: IconComponent, color } = getStepIconAndColor(stepType);
return (
@ -90,7 +131,7 @@ export function StepCard({
</div>
)}
{step.config?.tool_name && (
<Badge variant="secondary" className="mt-1 text-xs">
<Badge variant="default" className="mt-1 text-xs">
{step.config.tool_name}
</Badge>
)}

View File

@ -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<ConditionalStep>) => 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({
<ConditionalGroup
conditionSteps={item}
groupKey={`condition-group-${index}`}
onUpdate={onUpdateStep}
onUpdateStep={onUpdateStep}
onAddElse={onAddElse}
onAddElseIf={onAddElseIf}
onRemove={onDeleteStep}
onEdit={onEditStep}
onAddStep={onAddStep}
agentTools={agentTools}
isLoadingTools={isLoadingTools}
/>
@ -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={() => { }}
/>
);
})()}

View File

@ -53,6 +53,7 @@ export function WorkflowBuilder({
const [selectedStep, setSelectedStep] = useState<ConditionalStep | null>(null);
const [insertIndex, setInsertIndex] = useState<number>(-1);
const [searchQuery, setSearchQuery] = useState('');
const [parentStepId, setParentStepId] = useState<string | null>(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}
>
<div className="h-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="min-h-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto max-w-3xl md:px-8 min-w-0 py-8">
{steps.length === 0 ? (
// Empty state
@ -130,6 +135,7 @@ export function WorkflowBuilder({
onDeleteStep={handleDeleteStep}
onAddElseIf={handleAddElseIf}
onAddElse={handleAddElse}
onStepsChange={onStepsChange}
agentTools={agentTools}
isLoadingTools={isLoadingTools}
/>

View File

@ -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<string, string> = {
export const ACTION_ICONS: Record<string, any> = {
'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' };

View File

@ -60,7 +60,7 @@ export function WorkflowLayout({
isSaving={isSaving}
/>
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-y-auto overflow-x-hidden">
{children}
</div>
</div>

View File

@ -159,7 +159,7 @@ export function WorkflowSidePanel({
<Input
id="step-name"
value={selectedStep.name}
onChange={(e) => 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({
<Textarea
id="step-description"
value={selectedStep.description}
onChange={(e) => onUpdateStep({ description: e.target.value })}
onChange={(e) => onUpdateStep({ id: selectedStep.id, description: e.target.value })}
placeholder="What should this step do?"
rows={3}
className="mt-1 resize-none"
@ -197,6 +197,7 @@ export function WorkflowSidePanel({
id="condition-expression"
value={selectedStep.conditions?.expression || ''}
onChange={(e) => onUpdateStep({
id: selectedStep.id,
conditions: {
...selectedStep.conditions,
type: 'if' as const,