mirror of https://github.com/kortix-ai/suna.git
fix: workflow saving
This commit is contained in:
parent
762f18f96f
commit
1900bf9b60
|
@ -90,12 +90,14 @@ class UpcomingRunsResponse(BaseModel):
|
||||||
|
|
||||||
# Workflow models
|
# Workflow models
|
||||||
class WorkflowStepRequest(BaseModel):
|
class WorkflowStepRequest(BaseModel):
|
||||||
|
id: Optional[str] = None # CRITICAL: Accept ID from frontend
|
||||||
name: str
|
name: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
type: Optional[str] = "instruction"
|
type: Optional[str] = "instruction"
|
||||||
config: Dict[str, Any] = {}
|
config: Dict[str, Any] = {}
|
||||||
conditions: Optional[Dict[str, Any]] = None
|
conditions: Optional[Dict[str, Any]] = None
|
||||||
order: int
|
order: int
|
||||||
|
parentConditionalId: Optional[str] = None # CRITICAL: Accept parentConditionalId
|
||||||
children: Optional[List['WorkflowStepRequest']] = None
|
children: Optional[List['WorkflowStepRequest']] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -563,6 +565,13 @@ def convert_steps_to_json(steps: List[WorkflowStepRequest]) -> List[Dict[str, An
|
||||||
'conditions': step.conditions,
|
'conditions': step.conditions,
|
||||||
'order': step.order
|
'order': step.order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# CRITICAL: Preserve ID and parentConditionalId if they exist
|
||||||
|
if hasattr(step, 'id') and step.id:
|
||||||
|
step_dict['id'] = step.id
|
||||||
|
if hasattr(step, 'parentConditionalId') and step.parentConditionalId:
|
||||||
|
step_dict['parentConditionalId'] = step.parentConditionalId
|
||||||
|
|
||||||
if step.children:
|
if step.children:
|
||||||
step_dict['children'] = convert_steps_to_json(step.children)
|
step_dict['children'] = convert_steps_to_json(step.children)
|
||||||
result.append(step_dict)
|
result.append(step_dict)
|
||||||
|
|
|
@ -20,11 +20,9 @@ class ProviderError(TriggerError):
|
||||||
class WorkflowParser:
|
class WorkflowParser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.step_counter = 0
|
self.step_counter = 0
|
||||||
self.parsed_steps = []
|
|
||||||
|
|
||||||
def parse_workflow_steps(self, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def parse_workflow_steps(self, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
self.step_counter = 0
|
self.step_counter = 0
|
||||||
self.parsed_steps = []
|
|
||||||
|
|
||||||
start_node = next(
|
start_node = next(
|
||||||
(step for step in steps if step.get('name') == 'Start' and step.get('description') == 'Click to add steps or use the Add Node button'),
|
(step for step in steps if step.get('name') == 'Start' and step.get('description') == 'Click to add steps or use the Add Node button'),
|
||||||
|
@ -41,23 +39,98 @@ class WorkflowParser:
|
||||||
|
|
||||||
def _parse_steps_recursive(self, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _parse_steps_recursive(self, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
result = []
|
result = []
|
||||||
|
processed_ids = set()
|
||||||
|
|
||||||
for step in steps:
|
for step in steps:
|
||||||
parsed_step = self._parse_single_step(step)
|
step_id = step.get('id')
|
||||||
if parsed_step:
|
|
||||||
result.append(parsed_step)
|
# Skip if already processed as part of a conditional group
|
||||||
|
if step_id in processed_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
step_type = step.get('type')
|
||||||
|
parent_conditional_id = step.get('parentConditionalId')
|
||||||
|
|
||||||
|
if step_type == 'condition' and not parent_conditional_id:
|
||||||
|
# This is a root conditional step - find all its siblings
|
||||||
|
conditional_group = [step]
|
||||||
|
|
||||||
|
# Find all siblings (steps with parentConditionalId pointing to this step)
|
||||||
|
for other_step in steps:
|
||||||
|
if other_step.get('parentConditionalId') == step_id:
|
||||||
|
conditional_group.append(other_step)
|
||||||
|
|
||||||
|
# Sort by condition type (if, elseif, else)
|
||||||
|
conditional_group.sort(key=lambda s: self._get_condition_order(s))
|
||||||
|
|
||||||
|
# Parse the conditional group as an instruction step with conditions
|
||||||
|
parsed_group = self._parse_conditional_group(conditional_group)
|
||||||
|
if parsed_group:
|
||||||
|
result.append(parsed_group)
|
||||||
|
|
||||||
|
# Mark all steps in group as processed
|
||||||
|
for group_step in conditional_group:
|
||||||
|
processed_ids.add(group_step.get('id'))
|
||||||
|
|
||||||
|
elif step_type == 'condition' and parent_conditional_id:
|
||||||
|
# This step belongs to a parent conditional - skip it
|
||||||
|
# It will be processed when we encounter the parent
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Regular instruction step
|
||||||
|
parsed_step = self._parse_single_step(step)
|
||||||
|
if parsed_step:
|
||||||
|
result.append(parsed_step)
|
||||||
|
processed_ids.add(step_id)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _get_condition_order(self, step: Dict[str, Any]) -> int:
|
||||||
|
"""Sort order for conditions: if=0, elseif=1, else=2"""
|
||||||
|
condition_type = step.get('conditions', {}).get('type', 'if')
|
||||||
|
return {'if': 0, 'elseif': 1, 'else': 2}.get(condition_type, 0)
|
||||||
|
|
||||||
|
def _parse_conditional_group(self, conditional_group: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""Parse a group of related conditional steps (if/elseif/else) as one instruction step"""
|
||||||
|
if not conditional_group:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.step_counter += 1
|
||||||
|
|
||||||
|
# Use first step's name or generate one
|
||||||
|
first_step = conditional_group[0]
|
||||||
|
step_name = first_step.get('name', f'Conditional Step {self.step_counter}')
|
||||||
|
|
||||||
|
parsed_step = {
|
||||||
|
"step": step_name,
|
||||||
|
"step_number": self.step_counter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add description if meaningful
|
||||||
|
description = first_step.get('description', '').strip()
|
||||||
|
if description and description not in ['Add conditional logic', 'If/Then']:
|
||||||
|
parsed_step["description"] = description
|
||||||
|
|
||||||
|
# Parse all conditions in the group
|
||||||
|
conditions = []
|
||||||
|
for condition_step in conditional_group:
|
||||||
|
parsed_condition = self._parse_condition_step(condition_step)
|
||||||
|
if parsed_condition:
|
||||||
|
conditions.append(parsed_condition)
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
parsed_step["conditions"] = conditions
|
||||||
|
|
||||||
|
return parsed_step
|
||||||
|
|
||||||
def _parse_single_step(self, step: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
def _parse_single_step(self, step: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
step_type = step.get('type')
|
step_type = step.get('type')
|
||||||
step_name = step.get('name', '')
|
|
||||||
|
|
||||||
if step_type == 'instruction':
|
if step_type == 'instruction':
|
||||||
return self._parse_instruction_step(step)
|
return self._parse_instruction_step(step)
|
||||||
elif step_type == 'condition':
|
|
||||||
return self._parse_condition_step(step)
|
|
||||||
else:
|
else:
|
||||||
|
# For non-instruction steps, treat as instruction by default
|
||||||
return self._parse_instruction_step(step)
|
return self._parse_instruction_step(step)
|
||||||
|
|
||||||
def _parse_instruction_step(self, step: Dict[str, Any]) -> Dict[str, Any]:
|
def _parse_instruction_step(self, step: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
@ -69,7 +142,7 @@ class WorkflowParser:
|
||||||
}
|
}
|
||||||
|
|
||||||
description = step.get('description', '').strip()
|
description = step.get('description', '').strip()
|
||||||
if description:
|
if description and description not in ['Click to add steps or use the Add Node button', 'Add conditional logic', 'Add a custom instruction step']:
|
||||||
parsed_step["description"] = description
|
parsed_step["description"] = description
|
||||||
|
|
||||||
tool_name = step.get('config', {}).get('tool_name')
|
tool_name = step.get('config', {}).get('tool_name')
|
||||||
|
@ -82,14 +155,23 @@ class WorkflowParser:
|
||||||
|
|
||||||
children = step.get('children', [])
|
children = step.get('children', [])
|
||||||
if children:
|
if children:
|
||||||
condition_children = [child for child in children if child.get('type') == 'condition']
|
parsed_children = self._parse_steps_recursive(children)
|
||||||
instruction_children = [child for child in children if child.get('type') == 'instruction']
|
|
||||||
|
# Group children by type - conditions vs regular steps
|
||||||
|
condition_children = []
|
||||||
|
instruction_children = []
|
||||||
|
|
||||||
|
for child in parsed_children:
|
||||||
|
if child.get('condition'): # This is a condition
|
||||||
|
condition_children.append(child)
|
||||||
|
else: # This is a regular step
|
||||||
|
instruction_children.append(child)
|
||||||
|
|
||||||
if condition_children:
|
if condition_children:
|
||||||
parsed_step["conditions"] = self._parse_condition_branches(condition_children)
|
parsed_step["conditions"] = condition_children
|
||||||
|
|
||||||
if instruction_children:
|
if instruction_children:
|
||||||
parsed_step["then"] = self._parse_steps_recursive(instruction_children)
|
parsed_step["then"] = instruction_children
|
||||||
|
|
||||||
return parsed_step
|
return parsed_step
|
||||||
|
|
||||||
|
@ -113,16 +195,6 @@ class WorkflowParser:
|
||||||
|
|
||||||
return parsed_condition
|
return parsed_condition
|
||||||
|
|
||||||
def _parse_condition_branches(self, condition_steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
||||||
branches = []
|
|
||||||
|
|
||||||
for condition_step in condition_steps:
|
|
||||||
branch = self._parse_condition_step(condition_step)
|
|
||||||
if branch:
|
|
||||||
branches.append(branch)
|
|
||||||
|
|
||||||
return branches
|
|
||||||
|
|
||||||
def get_workflow_summary(self, steps: List[Dict[str, Any]]) -> Dict[str, Any]:
|
def get_workflow_summary(self, steps: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
def count_steps_recursive(steps_list):
|
def count_steps_recursive(steps_list):
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -152,6 +224,9 @@ class WorkflowParser:
|
||||||
|
|
||||||
total_steps, total_conditions, max_nesting_depth = count_steps_recursive(steps)
|
total_steps, total_conditions, max_nesting_depth = count_steps_recursive(steps)
|
||||||
|
|
||||||
|
# Parse the workflow to see the actual output
|
||||||
|
parsed_steps = self.parse_workflow_steps(steps)
|
||||||
|
|
||||||
# Large debug print to show inputs and parsed output
|
# Large debug print to show inputs and parsed output
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
print("WORKFLOW PARSER DEBUG - get_workflow_summary")
|
print("WORKFLOW PARSER DEBUG - get_workflow_summary")
|
||||||
|
@ -160,8 +235,11 @@ class WorkflowParser:
|
||||||
print(f"Type: {type(steps)}")
|
print(f"Type: {type(steps)}")
|
||||||
print(f"Length: {len(steps) if isinstance(steps, list) else 'N/A'}")
|
print(f"Length: {len(steps) if isinstance(steps, list) else 'N/A'}")
|
||||||
print(f"Raw steps: {steps}")
|
print(f"Raw steps: {steps}")
|
||||||
print("-" * 80)
|
print("-" * 40)
|
||||||
print(f"PARSED OUTPUT:")
|
print(f"PARSED STEPS:")
|
||||||
|
print(f"Parsed: {parsed_steps}")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"SUMMARY:")
|
||||||
print(f"Total steps: {total_steps}")
|
print(f"Total steps: {total_steps}")
|
||||||
print(f"Total conditions: {total_conditions}")
|
print(f"Total conditions: {total_conditions}")
|
||||||
print(f"Max nesting depth: {max_nesting_depth}")
|
print(f"Max nesting depth: {max_nesting_depth}")
|
||||||
|
|
|
@ -15,21 +15,32 @@ import { ConditionalStep } from '@/components/agents/workflows/conditional-workf
|
||||||
import { WorkflowBuilder } from '@/components/workflows/workflow-builder';
|
import { WorkflowBuilder } from '@/components/workflows/workflow-builder';
|
||||||
|
|
||||||
const convertToNestedJSON = (steps: ConditionalStep[]): any[] => {
|
const convertToNestedJSON = (steps: ConditionalStep[]): any[] => {
|
||||||
// Since we're using the old structure directly, just pass it through
|
// Clean, simple conversion - preserve the exact structure with order field for validation
|
||||||
let globalOrder = 1;
|
let globalOrder = 1;
|
||||||
|
|
||||||
const convertStepsWithNesting = (stepList: ConditionalStep[]): any[] => {
|
const convertStepsWithNesting = (stepList: ConditionalStep[]): any[] => {
|
||||||
return stepList.map((step) => {
|
return stepList.map((step) => {
|
||||||
|
// Build clean step object with required fields for backend validation
|
||||||
const jsonStep: any = {
|
const jsonStep: any = {
|
||||||
id: step.id,
|
id: step.id, // CRITICAL: Always include ID
|
||||||
name: step.name,
|
name: step.name,
|
||||||
description: step.description,
|
description: step.description,
|
||||||
type: step.type,
|
type: step.type,
|
||||||
config: step.config,
|
config: step.config || {},
|
||||||
order: globalOrder++
|
order: globalOrder++ // Required by backend validation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add conditional metadata if present
|
||||||
if (step.type === 'condition' && step.conditions) {
|
if (step.type === 'condition' && step.conditions) {
|
||||||
jsonStep.conditions = step.conditions;
|
jsonStep.conditions = step.conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add parent relationship if present
|
||||||
|
if (step.parentConditionalId) {
|
||||||
|
jsonStep.parentConditionalId = step.parentConditionalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add children if present
|
||||||
if (step.children && step.children.length > 0) {
|
if (step.children && step.children.length > 0) {
|
||||||
jsonStep.children = convertStepsWithNesting(step.children);
|
jsonStep.children = convertStepsWithNesting(step.children);
|
||||||
}
|
}
|
||||||
|
@ -37,6 +48,7 @@ const convertToNestedJSON = (steps: ConditionalStep[]): any[] => {
|
||||||
return jsonStep;
|
return jsonStep;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return convertStepsWithNesting(steps);
|
return convertStepsWithNesting(steps);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,12 +69,12 @@ const reconstructFromNestedJSON = (nestedSteps: any[]): ConditionalStep[] => {
|
||||||
const convertStepsFromNested = (stepList: any[]): ConditionalStep[] => {
|
const convertStepsFromNested = (stepList: any[]): ConditionalStep[] => {
|
||||||
return stepList.map((step) => {
|
return stepList.map((step) => {
|
||||||
const conditionalStep: ConditionalStep = {
|
const conditionalStep: ConditionalStep = {
|
||||||
id: step.id || Math.random().toString(36).substr(2, 9),
|
id: step.id, // Always use the existing ID - never regenerate
|
||||||
name: step.name,
|
name: step.name,
|
||||||
description: step.description || '',
|
description: step.description || '',
|
||||||
type: step.type || 'instruction',
|
type: step.type || 'instruction',
|
||||||
config: step.config || {},
|
config: step.config || {},
|
||||||
order: step.order || step.step_order || 0,
|
order: step.order || 0, // Preserve order from backend
|
||||||
enabled: step.enabled !== false,
|
enabled: step.enabled !== false,
|
||||||
hasIssues: step.hasIssues || false,
|
hasIssues: step.hasIssues || false,
|
||||||
children: [] // Initialize children array
|
children: [] // Initialize children array
|
||||||
|
@ -73,6 +85,11 @@ const reconstructFromNestedJSON = (nestedSteps: any[]): ConditionalStep[] => {
|
||||||
conditionalStep.conditions = step.conditions;
|
conditionalStep.conditions = step.conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle parentConditionalId for conditional grouping
|
||||||
|
if (step.parentConditionalId) {
|
||||||
|
conditionalStep.parentConditionalId = step.parentConditionalId;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle children - this is crucial for nested conditions
|
// Handle children - this is crucial for nested conditions
|
||||||
if (step.children && Array.isArray(step.children) && step.children.length > 0) {
|
if (step.children && Array.isArray(step.children) && step.children.length > 0) {
|
||||||
conditionalStep.children = convertStepsFromNested(step.children);
|
conditionalStep.children = convertStepsFromNested(step.children);
|
||||||
|
@ -110,7 +127,7 @@ const reconstructFromFlatJSON = (flatSteps: any[]): ConditionalStep[] => {
|
||||||
for (const flatStep of flatSteps) {
|
for (const flatStep of flatSteps) {
|
||||||
if (flatStep.type === 'condition') {
|
if (flatStep.type === 'condition') {
|
||||||
const conditionStep: ConditionalStep = {
|
const conditionStep: ConditionalStep = {
|
||||||
id: flatStep.id || Math.random().toString(36).substr(2, 9),
|
id: flatStep.id, // Always preserve existing ID
|
||||||
name: flatStep.name,
|
name: flatStep.name,
|
||||||
description: flatStep.description || '',
|
description: flatStep.description || '',
|
||||||
type: 'condition',
|
type: 'condition',
|
||||||
|
@ -127,7 +144,7 @@ const reconstructFromFlatJSON = (flatSteps: any[]): ConditionalStep[] => {
|
||||||
for (const [conditionId, conditionStep] of conditionSteps) {
|
for (const [conditionId, conditionStep] of conditionSteps) {
|
||||||
if (JSON.stringify(conditionStep.conditions) === JSON.stringify(flatStep.conditions)) {
|
if (JSON.stringify(conditionStep.conditions) === JSON.stringify(flatStep.conditions)) {
|
||||||
const childStep: ConditionalStep = {
|
const childStep: ConditionalStep = {
|
||||||
id: flatStep.id || Math.random().toString(36).substr(2, 9),
|
id: flatStep.id, // Always preserve existing ID
|
||||||
name: flatStep.name,
|
name: flatStep.name,
|
||||||
description: flatStep.description || '',
|
description: flatStep.description || '',
|
||||||
type: flatStep.type || 'instruction',
|
type: flatStep.type || 'instruction',
|
||||||
|
@ -165,7 +182,7 @@ const reconstructFromFlatJSON = (flatSteps: any[]): ConditionalStep[] => {
|
||||||
result.push(...conditionGroup);
|
result.push(...conditionGroup);
|
||||||
} else if (!flatStep.conditions) {
|
} else if (!flatStep.conditions) {
|
||||||
const step: ConditionalStep = {
|
const step: ConditionalStep = {
|
||||||
id: flatStep.id || Math.random().toString(36).substr(2, 9),
|
id: flatStep.id, // Always preserve existing ID
|
||||||
name: flatStep.name,
|
name: flatStep.name,
|
||||||
description: flatStep.description || '',
|
description: flatStep.description || '',
|
||||||
type: flatStep.type || 'instruction',
|
type: flatStep.type || 'instruction',
|
||||||
|
|
|
@ -97,33 +97,46 @@ export function ConditionalWorkflowBuilder({
|
||||||
order: 0,
|
order: 0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
const updateSteps = (items: ConditionalStep[]): ConditionalStep[] => {
|
|
||||||
|
const updateSteps = (items: ConditionalStep[]): { updatedItems: ConditionalStep[], found: boolean } => {
|
||||||
if (!parentId) {
|
if (!parentId) {
|
||||||
if (afterStepId) {
|
if (afterStepId) {
|
||||||
const index = items.findIndex(s => s.id === afterStepId);
|
const index = items.findIndex(s => s.id === afterStepId);
|
||||||
return [...items.slice(0, index + 1), newStep, ...items.slice(index + 1)];
|
return {
|
||||||
|
updatedItems: [...items.slice(0, index + 1), newStep, ...items.slice(index + 1)],
|
||||||
|
found: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return [...items, newStep];
|
return { updatedItems: [...items, newStep], found: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.map(step => {
|
let found = false;
|
||||||
|
const updatedItems = items.map(step => {
|
||||||
if (step.id === parentId) {
|
if (step.id === parentId) {
|
||||||
|
found = true;
|
||||||
return {
|
return {
|
||||||
...step,
|
...step,
|
||||||
children: [...(step.children || []), newStep]
|
children: [...(step.children || []), newStep]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (step.children) {
|
if (step.children && !found) {
|
||||||
return {
|
const result = updateSteps(step.children);
|
||||||
...step,
|
if (result.found) {
|
||||||
children: updateSteps(step.children)
|
found = true;
|
||||||
};
|
return {
|
||||||
|
...step,
|
||||||
|
children: result.updatedItems
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return step;
|
return step;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { updatedItems, found };
|
||||||
};
|
};
|
||||||
|
|
||||||
onStepsChange(updateSteps(steps));
|
const result = updateSteps(steps);
|
||||||
|
onStepsChange(result.updatedItems);
|
||||||
}, [steps, onStepsChange]);
|
}, [steps, onStepsChange]);
|
||||||
|
|
||||||
const addCondition = useCallback((afterStepId: string) => {
|
const addCondition = useCallback((afterStepId: string) => {
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, AlertTriangle, GripVertical, Settings } from 'lucide-react';
|
import { Plus, AlertTriangle, GripVertical, Settings, X } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { ConditionalStep } from '@/components/agents/workflows/conditional-workflow-builder';
|
import { ConditionalStep } from '@/components/agents/workflows/conditional-workflow-builder';
|
||||||
import { StepCard } from './step-card';
|
import { StepCard } from './step-card';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
@ -63,6 +71,7 @@ export function ConditionalGroup({
|
||||||
const [activeConditionTab, setActiveConditionTab] = useState<string>(conditionSteps[0]?.id || '');
|
const [activeConditionTab, setActiveConditionTab] = useState<string>(conditionSteps[0]?.id || '');
|
||||||
const [activeDragId, setActiveDragId] = useState<string | null>(null);
|
const [activeDragId, setActiveDragId] = useState<string | null>(null);
|
||||||
const [pendingElseStepAdd, setPendingElseStepAdd] = useState<boolean>(false);
|
const [pendingElseStepAdd, setPendingElseStepAdd] = useState<boolean>(false);
|
||||||
|
const [deleteConfirmStep, setDeleteConfirmStep] = useState<ConditionalStep | null>(null);
|
||||||
|
|
||||||
// Set up sensors for drag and drop within this conditional group
|
// Set up sensors for drag and drop within this conditional group
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
|
@ -197,6 +206,25 @@ export function ConditionalGroup({
|
||||||
}
|
}
|
||||||
}, [activeStep, onAddElse, conditionSteps, onAddStep]);
|
}, [activeStep, onAddElse, conditionSteps, onAddStep]);
|
||||||
|
|
||||||
|
// Handle delete confirmation
|
||||||
|
const handleDeleteConfirm = React.useCallback(() => {
|
||||||
|
if (deleteConfirmStep) {
|
||||||
|
// Switch to another tab before deleting if we're on the tab being deleted
|
||||||
|
if (activeConditionTab === deleteConfirmStep.id) {
|
||||||
|
const remainingSteps = conditionSteps.filter(s => s.id !== deleteConfirmStep.id);
|
||||||
|
if (remainingSteps.length > 0) {
|
||||||
|
setActiveConditionTab(remainingSteps[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRemove(deleteConfirmStep.id);
|
||||||
|
setDeleteConfirmStep(null);
|
||||||
|
}
|
||||||
|
}, [deleteConfirmStep, activeConditionTab, conditionSteps, onRemove]);
|
||||||
|
|
||||||
|
const handleDeleteCancel = React.useCallback(() => {
|
||||||
|
setDeleteConfirmStep(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} className={cn(isDragging && "opacity-50")}>
|
<div ref={setNodeRef} style={style} className={cn(isDragging && "opacity-50")}>
|
||||||
<div className="space-y-4 bg-zinc-50 dark:bg-zinc-900/50 rounded-2xl p-4 border border-zinc-200 dark:border-zinc-800 relative group">
|
<div className="space-y-4 bg-zinc-50 dark:bg-zinc-900/50 rounded-2xl p-4 border border-zinc-200 dark:border-zinc-800 relative group">
|
||||||
|
@ -232,24 +260,39 @@ export function ConditionalGroup({
|
||||||
step.conditions?.type === 'else' ? 'Else' : 'If';
|
step.conditions?.type === 'else' ? 'Else' : 'If';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<div key={step.id} className="relative group/tab">
|
||||||
key={step.id}
|
<Button
|
||||||
onClick={() => setActiveConditionTab(step.id)}
|
onClick={() => setActiveConditionTab(step.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 px-3 border border-dashed text-xs",
|
"h-9 px-3 border border-dashed text-xs",
|
||||||
isActive
|
isActive
|
||||||
? "bg-blue-500 hover:bg-blue-600 "
|
? "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"
|
: "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>
|
||||||
|
|
||||||
<span className="font-mono text-xs font-bold">{letter}</span>
|
{/* Delete button - only for else-if tabs */}
|
||||||
<span>•</span>
|
{step.conditions?.type === 'elseif' && (
|
||||||
<span>{conditionType}</span>
|
<Button
|
||||||
{step.hasIssues && (
|
variant="ghost"
|
||||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteConfirmStep(step);
|
||||||
|
}}
|
||||||
|
className="absolute -top-1 -right-1 h-4.5 w-4.5 !p-0 bg-primary hover:bg-primary hover:text-white hover:dark:text-black text-white dark:text-black rounded-full opacity-0 group-hover/tab:opacity-100 transition-opacity z-10 cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="!h-3 !w-3" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
@ -380,6 +423,8 @@ export function ConditionalGroup({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNestedAddStep = (index: number, parentStepId?: string) => {
|
const handleNestedAddStep = (index: number, parentStepId?: string) => {
|
||||||
|
// Call the parent's onAddStep which will open the side panel
|
||||||
|
// Pass the parentStepId correctly for nested context
|
||||||
onAddStep(index, parentStepId);
|
onAddStep(index, parentStepId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -505,6 +550,26 @@ export function ConditionalGroup({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={!!deleteConfirmStep} onOpenChange={(open) => !open && handleDeleteCancel()}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Condition</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this "Else If" condition? This action cannot be undone and will remove all steps within this condition.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleDeleteCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteConfirm}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Reference in New Issue