chore(dev): workflow input nodes, xml examples

This commit is contained in:
Soumyadas15 2025-06-14 18:21:55 +05:30
parent 1ef38a3bb4
commit d13d6a55e6
17 changed files with 2424 additions and 226 deletions

View File

@ -42,3 +42,5 @@ aiohttp>=3.9.0
email-validator>=2.0.0
mailtrap>=2.0.1
cryptography>=41.0.0
apscheduler>=3.10.0
croniter>=1.4.0

View File

@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Test script to verify that XML tool examples are correctly included in workflow system prompts.
"""
import asyncio
import json
from workflows.converter import WorkflowConverter
from workflows.tool_examples import get_tools_xml_examples
def test_xml_examples_integration():
"""Test that XML tool examples are correctly included in workflow system prompts."""
print("🧪 Testing XML Tool Examples Integration")
print("=" * 50)
# Create a sample workflow with input node and tools
nodes = [
{
"id": "input-1",
"type": "inputNode",
"position": {"x": 100, "y": 100},
"data": {
"label": "Workflow Input",
"prompt": "Search for information about AI and create a summary file.",
"trigger_type": "MANUAL",
"variables": {
"topic": "artificial intelligence",
"output_format": "markdown"
}
}
},
{
"id": "agent-1",
"type": "agentNode",
"position": {"x": 400, "y": 100},
"data": {
"label": "Research Agent",
"instructions": "You are a research assistant that searches for information and creates summaries",
"model": "anthropic/claude-3-5-sonnet-latest",
"connectedTools": [
{"id": "tool-1", "name": "Web Search", "type": "web_search_tool"},
{"id": "tool-2", "name": "File Operations", "type": "sb_files_tool"}
]
}
},
{
"id": "tool-1",
"type": "toolConnectionNode",
"position": {"x": 100, "y": 200},
"data": {
"label": "Web Search",
"nodeId": "web_search_tool",
"description": "Search the web for information"
}
},
{
"id": "tool-2",
"type": "toolConnectionNode",
"position": {"x": 100, "y": 300},
"data": {
"label": "File Operations",
"nodeId": "sb_files_tool",
"description": "Create and manage files"
}
}
]
edges = [
{
"id": "e-input-1-agent-1",
"source": "input-1",
"target": "agent-1",
"sourceHandle": "output",
"targetHandle": "input"
},
{
"id": "e-tool-1-agent-1",
"source": "tool-1",
"target": "agent-1",
"sourceHandle": "tool-connection",
"targetHandle": "tools"
},
{
"id": "e-tool-2-agent-1",
"source": "tool-2",
"target": "agent-1",
"sourceHandle": "tool-connection",
"targetHandle": "tools"
}
]
metadata = {
"name": "Research and Summary Workflow",
"description": "A workflow that searches for information and creates summaries",
"project_id": "test-project-123"
}
# Convert flow to workflow definition
converter = WorkflowConverter()
workflow_def = converter.convert_flow_to_workflow(nodes, edges, metadata)
print(f"✅ Workflow Definition Created:")
print(f" Name: {workflow_def.name}")
print(f" Steps: {len(workflow_def.steps)}")
# Get the system prompt
main_step = workflow_def.steps[0]
system_prompt = main_step.config.get("system_prompt", "")
print(f"\n📝 System Prompt Analysis:")
print(f" System Prompt Length: {len(system_prompt)} characters")
# Check that input prompt is included
input_prompt = main_step.config.get("input_prompt", "")
if "Search for information about AI" in system_prompt:
print("✅ Input prompt correctly included in system prompt")
else:
print("❌ ERROR: Input prompt not found in system prompt!")
return False
# Check that tool examples section exists
if "## Tool Usage Examples" in system_prompt:
print("✅ Tool Usage Examples section found in system prompt")
else:
print("❌ ERROR: Tool Usage Examples section not found!")
return False
# Check for specific XML examples
if "<function_calls>" in system_prompt:
print("✅ XML function_calls format found in system prompt")
else:
print("❌ ERROR: XML function_calls format not found!")
return False
# Check for web search tool example
if "web_search" in system_prompt and "<invoke name=\"web_search\">" in system_prompt:
print("✅ Web search tool XML example found")
else:
print("❌ ERROR: Web search tool XML example not found!")
return False
# Check for file operations tool example
if "create_file" in system_prompt and "<invoke name=\"create_file\">" in system_prompt:
print("✅ File operations tool XML example found")
else:
print("❌ ERROR: File operations tool XML example not found!")
return False
# Test the tool examples function directly
print(f"\n🔧 Testing Tool Examples Function:")
tool_ids = ["web_search_tool", "sb_files_tool"]
xml_examples = get_tools_xml_examples(tool_ids)
if xml_examples:
print(f"✅ Tool examples generated successfully ({len(xml_examples)} characters)")
# Check that both tools are included
if "Web Search Tool" in xml_examples and "Sb Files Tool" in xml_examples:
print("✅ Both tool examples included in output")
else:
print("❌ ERROR: Not all tool examples included!")
return False
else:
print("❌ ERROR: No tool examples generated!")
return False
# Print a sample of the system prompt for verification
print(f"\n📄 System Prompt Sample (first 500 chars):")
print("-" * 50)
print(system_prompt[:500] + "..." if len(system_prompt) > 500 else system_prompt)
print("-" * 50)
# Look for the tool examples section specifically
if "## Tool Usage Examples" in system_prompt:
start_idx = system_prompt.find("## Tool Usage Examples")
end_idx = system_prompt.find("## Workflow Execution", start_idx)
if end_idx == -1:
end_idx = start_idx + 1000 # Show next 1000 chars if no end found
tool_examples_section = system_prompt[start_idx:end_idx]
print(f"\n🔧 Tool Examples Section:")
print("-" * 50)
print(tool_examples_section)
print("-" * 50)
print(f"\n🎉 All Tests Passed!")
print(f" ✓ Input prompt integration")
print(f" ✓ Tool Usage Examples section")
print(f" ✓ XML function_calls format")
print(f" ✓ Web search tool example")
print(f" ✓ File operations tool example")
print(f" ✓ Tool examples function")
return True
def main():
"""Run the test."""
try:
result = test_xml_examples_integration()
if result:
print(f"\n🎯 Test Summary: SUCCESS")
print(f" XML tool examples are now automatically included in workflow system prompts!")
print(f" When workflows are saved, agents will know exactly how to call tools using XML format.")
else:
print(f"\n❌ Test Summary: FAILED")
print(f" There are issues with XML tool examples integration.")
except Exception as e:
print(f"❌ Test failed with error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View File

@ -17,6 +17,7 @@ from .models import (
)
from .converter import WorkflowConverter, validate_workflow_flow
from .executor import WorkflowExecutor
from .scheduler import WorkflowScheduler
from services.supabase import DBConnection
from utils.logger import logger
from utils.auth_utils import get_current_user_id_from_jwt
@ -27,12 +28,14 @@ router = APIRouter()
db = DBConnection()
workflow_converter = WorkflowConverter()
workflow_executor = WorkflowExecutor(db)
workflow_scheduler = WorkflowScheduler(db, workflow_executor)
def initialize(database: DBConnection):
"""Initialize the workflow API with database connection."""
global db, workflow_executor
global db, workflow_executor, workflow_scheduler
db = database
workflow_executor = WorkflowExecutor(db)
workflow_scheduler = WorkflowScheduler(db, workflow_executor)
def _map_db_to_workflow_definition(data: dict) -> WorkflowDefinition:
"""Helper function to map database record to WorkflowDefinition."""
@ -197,7 +200,18 @@ async def update_workflow(
raise HTTPException(status_code=500, detail="Failed to update workflow")
data = result.data[0]
return _map_db_to_workflow_definition(data)
updated_workflow = _map_db_to_workflow_definition(data)
# Handle scheduling if workflow is active and has schedule triggers
if updated_workflow.state == 'ACTIVE':
has_schedule_trigger = any(trigger.type == 'SCHEDULE' for trigger in updated_workflow.triggers)
if has_schedule_trigger:
await workflow_scheduler.schedule_workflow(updated_workflow)
else:
# Unschedule if workflow is not active
await workflow_scheduler.unschedule_workflow(workflow_id)
return updated_workflow
except HTTPException:
raise
@ -219,6 +233,9 @@ async def delete_workflow(
if not existing.data:
raise HTTPException(status_code=404, detail="Workflow not found")
# Unschedule workflow before deleting
await workflow_scheduler.unschedule_workflow(workflow_id)
await client.table('workflows').delete().eq('id', workflow_id).execute()
return {"message": "Workflow deleted successfully"}
@ -441,7 +458,18 @@ async def update_workflow_flow(
raise HTTPException(status_code=500, detail="Failed to update workflow")
data = result.data[0]
return _map_db_to_workflow_definition(data)
updated_workflow = _map_db_to_workflow_definition(data)
# Handle scheduling if workflow is active and has schedule triggers
if updated_workflow.state == 'ACTIVE':
has_schedule_trigger = any(trigger.type == 'SCHEDULE' for trigger in updated_workflow.triggers)
if has_schedule_trigger:
await workflow_scheduler.schedule_workflow(updated_workflow)
else:
# Unschedule if workflow is not active
await workflow_scheduler.unschedule_workflow(workflow_id)
return updated_workflow
except HTTPException:
raise
@ -493,6 +521,114 @@ async def get_builder_nodes():
try:
# Return the available node types that can be used in workflows
nodes = [
{
"id": "inputNode",
"name": "Input",
"description": "Workflow input configuration with prompt and trigger settings",
"category": "input",
"icon": "Play",
"inputs": [],
"outputs": ["output"],
"required": True,
"config_schema": {
"prompt": {
"type": "textarea",
"label": "Workflow Prompt",
"description": "The main prompt that describes what this workflow should do",
"required": True,
"placeholder": "Describe what this workflow should accomplish..."
},
"trigger_type": {
"type": "select",
"label": "Trigger Type",
"description": "How this workflow should be triggered",
"required": True,
"options": [
{"value": "MANUAL", "label": "Manual"},
{"value": "WEBHOOK", "label": "Webhook"},
{"value": "SCHEDULE", "label": "Schedule"}
],
"default": "MANUAL"
},
"schedule_config": {
"type": "object",
"label": "Schedule Configuration",
"description": "Configure when the workflow runs automatically",
"conditional": {"field": "trigger_type", "value": "SCHEDULE"},
"properties": {
"interval_type": {
"type": "select",
"label": "Interval Type",
"options": [
{"value": "minutes", "label": "Minutes"},
{"value": "hours", "label": "Hours"},
{"value": "days", "label": "Days"},
{"value": "weeks", "label": "Weeks"}
]
},
"interval_value": {
"type": "number",
"label": "Interval Value",
"min": 1,
"placeholder": "e.g., 30 for every 30 minutes"
},
"cron_expression": {
"type": "text",
"label": "Cron Expression (Advanced)",
"description": "Use cron syntax for complex schedules",
"placeholder": "0 9 * * 1-5 (weekdays at 9 AM)"
},
"timezone": {
"type": "select",
"label": "Timezone",
"default": "UTC",
"options": [
{"value": "UTC", "label": "UTC"},
{"value": "America/New_York", "label": "Eastern Time"},
{"value": "America/Chicago", "label": "Central Time"},
{"value": "America/Denver", "label": "Mountain Time"},
{"value": "America/Los_Angeles", "label": "Pacific Time"},
{"value": "Europe/London", "label": "London"},
{"value": "Europe/Paris", "label": "Paris"},
{"value": "Asia/Tokyo", "label": "Tokyo"}
]
}
}
},
"webhook_config": {
"type": "object",
"label": "Webhook Configuration",
"description": "Configure webhook trigger settings",
"conditional": {"field": "trigger_type", "value": "WEBHOOK"},
"properties": {
"method": {
"type": "select",
"label": "HTTP Method",
"default": "POST",
"options": [
{"value": "POST", "label": "POST"},
{"value": "GET", "label": "GET"},
{"value": "PUT", "label": "PUT"}
]
},
"authentication": {
"type": "select",
"label": "Authentication",
"options": [
{"value": "none", "label": "None"},
{"value": "api_key", "label": "API Key"},
{"value": "bearer", "label": "Bearer Token"}
]
}
}
},
"variables": {
"type": "key_value",
"label": "Default Variables",
"description": "Set default values for workflow variables"
}
}
},
{
"id": "agentNode",
"name": "AI Agent",
@ -603,4 +739,51 @@ async def create_workflow_from_template(
raise
except Exception as e:
logger.error(f"Error creating workflow from template: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/workflows/scheduler/status")
async def get_scheduler_status(
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Get information about currently scheduled workflows."""
try:
scheduled_workflows = await workflow_scheduler.get_scheduled_workflows()
# Filter to only show workflows owned by the current user
client = await db.client
user_workflows = await client.table('workflows').select('id').eq('created_by', user_id).execute()
user_workflow_ids = {w['id'] for w in user_workflows.data}
filtered_scheduled = [
w for w in scheduled_workflows
if w['workflow_id'] in user_workflow_ids
]
return {
"scheduled_workflows": filtered_scheduled,
"total_scheduled": len(filtered_scheduled)
}
except Exception as e:
logger.error(f"Error getting scheduler status: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/workflows/scheduler/start")
async def start_scheduler():
"""Start the workflow scheduler."""
try:
await workflow_scheduler.start()
return {"message": "Workflow scheduler started"}
except Exception as e:
logger.error(f"Error starting scheduler: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/workflows/scheduler/stop")
async def stop_scheduler():
"""Stop the workflow scheduler."""
try:
await workflow_scheduler.stop()
return {"message": "Workflow scheduler stopped"}
except Exception as e:
logger.error(f"Error stopping scheduler: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,5 +1,6 @@
from typing import List, Dict, Any, Optional
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger, InputNodeConfig, ScheduleConfig
from .tool_examples import get_tools_xml_examples
import uuid
from utils.logger import logger
@ -23,8 +24,11 @@ class WorkflowConverter:
"""
logger.info(f"Converting workflow flow with {len(nodes)} nodes and {len(edges)} edges")
workflow_prompt = self._generate_workflow_prompt(nodes, edges)
# Find input node and extract configuration
input_config = self._extract_input_configuration(nodes)
workflow_prompt = self._generate_workflow_prompt(nodes, edges, input_config)
entry_point = self._find_entry_point(nodes, edges)
triggers = self._extract_triggers_from_input(input_config)
agent_step = WorkflowStep(
id="main_agent_step",
@ -36,22 +40,18 @@ class WorkflowConverter:
"system_prompt": workflow_prompt,
"agent_id": metadata.get("agent_id"),
"model": "anthropic/claude-3-5-sonnet-latest",
"max_iterations": 10
"max_iterations": 10,
"input_prompt": input_config.prompt if input_config else ""
},
next_steps=[]
)
trigger = WorkflowTrigger(
type="MANUAL",
config={}
)
workflow = WorkflowDefinition(
name=metadata.get("name", "Untitled Workflow"),
description=metadata.get("description", "Generated from visual workflow"),
steps=[agent_step],
entry_point="main_agent_step",
triggers=[trigger],
triggers=triggers,
project_id=metadata.get("project_id", ""),
agent_id=metadata.get("agent_id"),
is_template=metadata.get("is_template", False),
@ -61,16 +61,105 @@ class WorkflowConverter:
return workflow
def _generate_workflow_prompt(self, nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]) -> str:
def _extract_input_configuration(self, nodes: List[Dict[str, Any]]) -> Optional[InputNodeConfig]:
"""Extract input node configuration from the workflow nodes."""
for node in nodes:
if node.get('type') == 'inputNode':
data = node.get('data', {})
# Extract schedule configuration if present
schedule_config = None
if data.get('trigger_type') == 'SCHEDULE' and data.get('schedule_config'):
schedule_data = data.get('schedule_config', {})
schedule_config = ScheduleConfig(
cron_expression=schedule_data.get('cron_expression'),
interval_type=schedule_data.get('interval_type'),
interval_value=schedule_data.get('interval_value'),
timezone=schedule_data.get('timezone', 'UTC'),
start_date=schedule_data.get('start_date'),
end_date=schedule_data.get('end_date'),
enabled=schedule_data.get('enabled', True)
)
return InputNodeConfig(
prompt=data.get('prompt', ''),
trigger_type=data.get('trigger_type', 'MANUAL'),
webhook_config=data.get('webhook_config'),
schedule_config=schedule_config,
variables=data.get('variables')
)
return None
def _extract_triggers_from_input(self, input_config: Optional[InputNodeConfig]) -> List[WorkflowTrigger]:
"""Extract workflow triggers from input node configuration."""
if not input_config:
return [WorkflowTrigger(type="MANUAL", config={})]
triggers = []
if input_config.trigger_type == 'MANUAL':
triggers.append(WorkflowTrigger(type="MANUAL", config={}))
elif input_config.trigger_type == 'WEBHOOK':
webhook_config = input_config.webhook_config or {}
triggers.append(WorkflowTrigger(
type="WEBHOOK",
config={
"webhook_url": webhook_config.get('webhook_url'),
"method": webhook_config.get('method', 'POST'),
"headers": webhook_config.get('headers', {}),
"authentication": webhook_config.get('authentication')
}
))
elif input_config.trigger_type == 'SCHEDULE':
if input_config.schedule_config:
schedule_config = {
"cron_expression": input_config.schedule_config.cron_expression,
"interval_type": input_config.schedule_config.interval_type,
"interval_value": input_config.schedule_config.interval_value,
"timezone": input_config.schedule_config.timezone,
"start_date": input_config.schedule_config.start_date.isoformat() if input_config.schedule_config.start_date else None,
"end_date": input_config.schedule_config.end_date.isoformat() if input_config.schedule_config.end_date else None,
"enabled": input_config.schedule_config.enabled
}
triggers.append(WorkflowTrigger(type="SCHEDULE", config=schedule_config))
else:
# Default schedule trigger
triggers.append(WorkflowTrigger(
type="SCHEDULE",
config={
"interval_type": "hours",
"interval_value": 1,
"timezone": "UTC",
"enabled": True
}
))
return triggers
def _generate_workflow_prompt(self, nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]], input_config: Optional[InputNodeConfig] = None) -> str:
"""Generate a comprehensive system prompt that describes the workflow."""
prompt_parts = [
"You are an AI agent executing a workflow. Follow these instructions carefully:",
"",
]
# Add input prompt if available
if input_config and input_config.prompt:
prompt_parts.extend([
"## Workflow Input Prompt",
input_config.prompt,
"",
])
prompt_parts.extend([
"## Workflow Overview",
"This workflow was created visually and consists of the following components:",
""
]
])
node_descriptions = []
agent_nodes = []
@ -85,6 +174,9 @@ class WorkflowConverter:
tool_nodes.append(node)
desc = self._describe_tool_node(node, edges)
node_descriptions.append(desc)
elif node.get('type') == 'inputNode':
desc = self._describe_input_node(node, edges)
node_descriptions.append(desc)
else:
desc = self._describe_generic_node(node, edges)
node_descriptions.append(desc)
@ -92,6 +184,32 @@ class WorkflowConverter:
prompt_parts.extend(node_descriptions)
prompt_parts.append("")
# Add trigger information
if input_config:
prompt_parts.extend([
"## Trigger Configuration",
f"**Trigger Type**: {input_config.trigger_type}",
])
if input_config.trigger_type == 'SCHEDULE' and input_config.schedule_config:
schedule = input_config.schedule_config
if schedule.cron_expression:
prompt_parts.append(f"**Schedule**: {schedule.cron_expression} (cron)")
elif schedule.interval_type and schedule.interval_value:
prompt_parts.append(f"**Schedule**: Every {schedule.interval_value} {schedule.interval_type}")
prompt_parts.append(f"**Timezone**: {schedule.timezone}")
if input_config.variables:
prompt_parts.extend([
"",
"## Default Variables",
"The following default variables are configured:"
])
for key, value in input_config.variables.items():
prompt_parts.append(f"- **{key}**: {value}")
prompt_parts.append("")
prompt_parts.extend([
"## Execution Instructions",
"",
@ -107,18 +225,37 @@ class WorkflowConverter:
"You have access to the following tools based on the workflow configuration:"
])
# Extract tool IDs and generate tool descriptions
tool_ids = []
for tool_node in tool_nodes:
tool_data = tool_node.get('data', {})
tool_name = tool_data.get('nodeId', tool_data.get('label', 'Unknown Tool'))
tool_desc = tool_data.get('description', 'No description available')
prompt_parts.append(f"- **{tool_name}**: {tool_desc}")
# Collect tool ID for XML examples
tool_id = tool_data.get('nodeId')
if tool_id:
tool_ids.append(tool_id)
# Add XML tool examples if tools are available
if tool_ids:
xml_examples = get_tools_xml_examples(tool_ids)
if xml_examples:
prompt_parts.extend([
"",
"## Tool Usage Examples",
"Use the following XML format to call tools. Each tool call must be wrapped in <function_calls> tags:",
"",
xml_examples
])
prompt_parts.extend([
"",
"## Workflow Execution",
"When executing this workflow:",
"- Follow the logical flow defined by the visual connections",
"- Use tools in the order and manner specified",
"- Use tools in the order and manner specified using the XML format shown above",
"- Provide clear, step-by-step output",
"- If any step fails, explain what went wrong and suggest alternatives",
"- Complete the workflow by providing the expected output",
@ -128,6 +265,33 @@ class WorkflowConverter:
return "\n".join(prompt_parts)
def _describe_input_node(self, node: Dict[str, Any], edges: List[Dict[str, Any]]) -> str:
"""Describe an input node and its configuration."""
data = node.get('data', {})
prompt = data.get('prompt', 'No prompt specified')
trigger_type = data.get('trigger_type', 'MANUAL')
output_connections = self._find_node_outputs(node.get('id'), edges)
description = [
f"### Input Configuration",
f"**Prompt**: {prompt}",
f"**Trigger Type**: {trigger_type}",
]
if trigger_type == 'SCHEDULE':
schedule_config = data.get('schedule_config', {})
if schedule_config.get('cron_expression'):
description.append(f"**Schedule**: {schedule_config['cron_expression']} (cron)")
elif schedule_config.get('interval_type') and schedule_config.get('interval_value'):
description.append(f"**Schedule**: Every {schedule_config['interval_value']} {schedule_config['interval_type']}")
if output_connections:
description.append(f"**Connects to**: {', '.join(output_connections)}")
description.append("")
return "\n".join(description)
def _describe_agent_node(self, node: Dict[str, Any], edges: List[Dict[str, Any]]) -> str:
"""Describe an agent node and its role in the workflow."""
data = node.get('data', {})
@ -239,6 +403,28 @@ def validate_workflow_flow(nodes: List[Dict[str, Any]], edges: List[Dict[str, An
errors.append("Workflow must have at least one node")
return False, errors
# Check for required input node
has_input = any(node.get('type') == 'inputNode' for node in nodes)
if not has_input:
errors.append("Every workflow must have an input node")
# Validate input node configuration
for node in nodes:
if node.get('type') == 'inputNode':
data = node.get('data', {})
if not data.get('prompt'):
errors.append("Input node must have a prompt configured")
trigger_type = data.get('trigger_type', 'MANUAL')
if trigger_type == 'SCHEDULE':
schedule_config = data.get('schedule_config', {})
if not schedule_config.get('cron_expression') and not (schedule_config.get('interval_type') and schedule_config.get('interval_value')):
errors.append("Schedule trigger must have either cron expression or interval configuration")
elif trigger_type == 'WEBHOOK':
webhook_config = data.get('webhook_config', {})
if not webhook_config:
errors.append("Webhook trigger must have webhook configuration")
connected_nodes = set()
for edge in edges:
connected_nodes.add(edge.get('source'))

View File

@ -228,9 +228,20 @@ class WorkflowExecutor:
}
await client.table('threads').insert(thread_data).execute()
initial_message = f"Execute the workflow: {workflow.name}"
if workflow.description:
initial_message += f"\n\nDescription: {workflow.description}"
# Get the input prompt from the workflow step configuration
input_prompt = ""
if workflow.steps:
main_step = workflow.steps[0]
input_prompt = main_step.config.get("input_prompt", "")
# Use input prompt if available, otherwise fall back to workflow name/description
if input_prompt:
initial_message = input_prompt
else:
initial_message = f"Execute the workflow: {workflow.name}"
if workflow.description:
initial_message += f"\n\nDescription: {workflow.description}"
if variables:
initial_message += f"\n\nVariables: {json.dumps(variables, indent=2)}"

View File

@ -2,11 +2,50 @@ from pydantic import BaseModel
from typing import List, Dict, Any, Optional, Literal
from datetime import datetime
class ScheduleConfig(BaseModel):
"""Configuration for scheduled workflow triggers."""
cron_expression: Optional[str] = None
interval_type: Optional[Literal['minutes', 'hours', 'days', 'weeks']] = None
interval_value: Optional[int] = None
timezone: str = "UTC"
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
enabled: bool = True
class SlackWebhookConfig(BaseModel):
"""Configuration for Slack webhook integration."""
webhook_url: str
signing_secret: str
channel: Optional[str] = None
username: Optional[str] = None
class GenericWebhookConfig(BaseModel):
"""Configuration for generic webhook integration."""
url: str
headers: Optional[Dict[str, str]] = None
auth_token: Optional[str] = None
class WebhookConfig(BaseModel):
"""Configuration for webhook triggers."""
type: Literal['slack', 'generic'] = 'slack'
method: Optional[Literal['POST', 'GET', 'PUT']] = 'POST'
authentication: Optional[Literal['none', 'api_key', 'bearer']] = 'none'
slack: Optional[SlackWebhookConfig] = None
generic: Optional[GenericWebhookConfig] = None
class InputNodeConfig(BaseModel):
"""Configuration for workflow input nodes."""
prompt: str = ""
trigger_type: Literal['MANUAL', 'WEBHOOK', 'SCHEDULE'] = 'MANUAL'
webhook_config: Optional[WebhookConfig] = None
schedule_config: Optional[ScheduleConfig] = None
variables: Optional[Dict[str, Any]] = None
class WorkflowStep(BaseModel):
id: str
name: str
description: Optional[str] = None
type: Literal['TOOL', 'MCP_TOOL', 'CONDITION', 'LOOP', 'PARALLEL', 'WAIT', 'WEBHOOK', 'TRANSFORM']
type: Literal['TOOL', 'MCP_TOOL', 'CONDITION', 'LOOP', 'PARALLEL', 'WAIT', 'WEBHOOK', 'TRANSFORM', 'INPUT']
config: Dict[str, Any]
next_steps: List[str]
error_handler: Optional[str] = None

View File

@ -0,0 +1,220 @@
import asyncio
import uuid
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List
from croniter import croniter
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from .models import WorkflowDefinition, WorkflowTrigger, ScheduleConfig
from .executor import WorkflowExecutor
from services.supabase import DBConnection
from utils.logger import logger
class WorkflowScheduler:
"""Manages scheduled workflow executions."""
def __init__(self, db: DBConnection, workflow_executor: WorkflowExecutor):
self.db = db
self.workflow_executor = workflow_executor
self.scheduler = AsyncIOScheduler()
self.scheduled_jobs = {}
async def start(self):
"""Start the scheduler and load existing scheduled workflows."""
self.scheduler.start()
await self._load_scheduled_workflows()
logger.info("Workflow scheduler started")
async def stop(self):
"""Stop the scheduler."""
self.scheduler.shutdown()
logger.info("Workflow scheduler stopped")
async def _load_scheduled_workflows(self):
"""Load all active scheduled workflows from the database."""
try:
client = await self.db.client
result = await client.table('workflows').select('*').eq('status', 'active').execute()
for workflow_data in result.data:
definition = workflow_data.get('definition', {})
triggers = definition.get('triggers', [])
for trigger in triggers:
if trigger.get('type') == 'SCHEDULE':
workflow = self._map_db_to_workflow_definition(workflow_data)
await self._schedule_workflow(workflow, trigger)
except Exception as e:
logger.error(f"Error loading scheduled workflows: {e}")
async def schedule_workflow(self, workflow: WorkflowDefinition):
"""Schedule a workflow based on its triggers."""
for trigger in workflow.triggers:
if trigger.type == 'SCHEDULE':
await self._schedule_workflow(workflow, trigger.model_dump())
async def unschedule_workflow(self, workflow_id: str):
"""Remove a workflow from the schedule."""
if workflow_id in self.scheduled_jobs:
job_id = self.scheduled_jobs[workflow_id]
try:
self.scheduler.remove_job(job_id)
del self.scheduled_jobs[workflow_id]
logger.info(f"Unscheduled workflow {workflow_id}")
except Exception as e:
logger.error(f"Error unscheduling workflow {workflow_id}: {e}")
async def _schedule_workflow(self, workflow: WorkflowDefinition, trigger_config: Dict[str, Any]):
"""Schedule a single workflow with the given trigger configuration."""
try:
config = trigger_config.get('config', {})
if not config.get('enabled', True):
logger.info(f"Skipping disabled schedule for workflow {workflow.id}")
return
if workflow.id in self.scheduled_jobs:
await self.unschedule_workflow(workflow.id)
trigger = None
if config.get('cron_expression'):
cron_expr = config['cron_expression']
timezone_str = config.get('timezone', 'UTC')
if croniter.is_valid(cron_expr):
trigger = CronTrigger.from_crontab(cron_expr, timezone=timezone_str)
else:
logger.error(f"Invalid cron expression for workflow {workflow.id}: {cron_expr}")
return
elif config.get('interval_type') and config.get('interval_value'):
interval_type = config['interval_type']
interval_value = config['interval_value']
kwargs = {interval_type: interval_value}
trigger = IntervalTrigger(**kwargs)
else:
logger.error(f"No valid schedule configuration for workflow {workflow.id}")
return
job_id = f"workflow_{workflow.id}_{uuid.uuid4().hex[:8]}"
job = self.scheduler.add_job(
func=self._execute_scheduled_workflow,
trigger=trigger,
args=[workflow.id, workflow.project_id],
id=job_id,
name=f"Scheduled execution of {workflow.name}",
max_instances=1,
coalesce=True,
misfire_grace_time=300
)
self.scheduled_jobs[workflow.id] = job_id
next_run = job.next_run_time
logger.info(f"Scheduled workflow {workflow.name} (ID: {workflow.id}). Next run: {next_run}")
except Exception as e:
logger.error(f"Error scheduling workflow {workflow.id}: {e}")
async def _execute_scheduled_workflow(self, workflow_id: str, project_id: str):
"""Execute a scheduled workflow."""
try:
logger.info(f"Executing scheduled workflow {workflow_id}")
client = await self.db.client
result = await client.table('workflows').select('*').eq('id', workflow_id).execute()
if not result.data:
logger.error(f"Scheduled workflow {workflow_id} not found")
return
workflow_data = result.data[0]
if workflow_data.get('status') != 'active':
logger.info(f"Skipping execution of inactive workflow {workflow_id}")
await self.unschedule_workflow(workflow_id)
return
workflow = self._map_db_to_workflow_definition(workflow_data)
execution_id = str(uuid.uuid4())
execution_data = {
"id": execution_id,
"workflow_id": workflow_id,
"workflow_version": workflow_data.get('version', 1),
"workflow_name": workflow.name,
"execution_context": {},
"project_id": project_id,
"account_id": workflow_data.get('account_id'),
"triggered_by": "SCHEDULE",
"status": "pending",
"started_at": datetime.now(timezone.utc).isoformat()
}
await client.table('workflow_executions').insert(execution_data).execute()
thread_id = str(uuid.uuid4())
async for update in self.workflow_executor.execute_workflow(
workflow=workflow,
variables=None,
thread_id=thread_id,
project_id=project_id
):
logger.debug(f"Scheduled workflow {workflow_id} update: {update.get('type', 'unknown')}")
logger.info(f"Completed scheduled execution of workflow {workflow_id}")
except Exception as e:
logger.error(f"Error executing scheduled workflow {workflow_id}: {e}")
try:
client = await self.db.client
await client.table('workflow_executions').update({
"status": "failed",
"completed_at": datetime.now(timezone.utc).isoformat(),
"error": str(e)
}).eq('id', execution_id).execute()
except:
pass
def _map_db_to_workflow_definition(self, data: dict) -> WorkflowDefinition:
"""Helper function to map database record to WorkflowDefinition."""
definition = data.get('definition', {})
return WorkflowDefinition(
id=data['id'],
name=data['name'],
description=data.get('description'),
steps=definition.get('steps', []),
entry_point=definition.get('entry_point', ''),
triggers=definition.get('triggers', []),
state=data.get('status', 'draft').upper(),
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else None,
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else None,
created_by=data.get('created_by'),
project_id=data['project_id'],
agent_id=definition.get('agent_id'),
is_template=False,
max_execution_time=definition.get('max_execution_time', 3600),
max_retries=definition.get('max_retries', 3)
)
async def get_scheduled_workflows(self) -> List[Dict[str, Any]]:
"""Get information about currently scheduled workflows."""
scheduled_info = []
for workflow_id, job_id in self.scheduled_jobs.items():
try:
job = self.scheduler.get_job(job_id)
if job:
scheduled_info.append({
"workflow_id": workflow_id,
"job_id": job_id,
"name": job.name,
"next_run_time": job.next_run_time.isoformat() if job.next_run_time else None,
"trigger": str(job.trigger)
})
except Exception as e:
logger.error(f"Error getting job info for {job_id}: {e}")
return scheduled_info

View File

@ -0,0 +1,69 @@
TOOL_XML_EXAMPLES = {
"computer_use_tool": '<function_calls>\n <invoke name="move_to">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="click">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n <parameter name="button">left</parameter>\n <parameter name="num_clicks">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="scroll">\n <parameter name="amount">-3</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="typing">\n <parameter name="text">Hello World!</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="press">\n <parameter name="key">enter</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="wait">\n <parameter name="duration">1.5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="mouse_down">\n <parameter name="button">left</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="mouse_up">\n <parameter name="button">left</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="drag_to">\n <parameter name="x">500</parameter>\n <parameter name="y">50</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="hotkey">\n <parameter name="keys">ctrl+a</parameter>\n </invoke>\n </function_calls>',
"data_providers_tool": '<!-- \nThe get-data-provider-endpoints tool returns available endpoints for a specific data provider.\nUse this tool when you need to discover what endpoints are available.\n-->\n\n<!-- Example to get LinkedIn API endpoints -->\n<function_calls>\n<invoke name="get_data_provider_endpoints">\n<parameter name="service_name">linkedin</parameter>\n</invoke>\n</function_calls>\n\n<!-- \n The execute-data-provider-call tool makes a request to a specific data provider endpoint.\n Use this tool when you need to call an data provider endpoint with specific parameters.\n The route must be a valid endpoint key obtained from get-data-provider-endpoints tool!!\n -->\n \n <!-- Example to call linkedIn service with the specific route person -->\n <function_calls>\n <invoke name="execute_data_provider_call">\n <parameter name="service_name">linkedin</parameter>\n <parameter name="route">person</parameter>\n <parameter name="payload">{"link": "https://www.linkedin.com/in/johndoe/"}</parameter>\n </invoke>\n </function_calls>',
"expand_msg_tool": '<!-- Example 1: Expand a message that was truncated in the previous conversation -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">ecde3a4c-c7dc-4776-ae5c-8209517c5576</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Expand a message to create reports or analyze truncated data -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">f47ac10b-58cc-4372-a567-0e02b2c3d479</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Expand a message when you need the full content for analysis -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">550e8400-e29b-41d4-a716-446655440000</parameter>\n </invoke>\n </function_calls>',
"mcp_tool_wrapper": '<function_calls>\n <invoke name="call_mcp_tool">\n <parameter name="tool_name">mcp_exa_web_search_exa</parameter>\n <parameter name="arguments">{"query": "latest developments in AI", "num_results": 10}</parameter>\n </invoke>\n </function_calls>',
"message_tool": '<function_calls>\n <invoke name="ask">\n <parameter name="text">I\'m planning to bake the chocolate cake for your birthday party. The recipe mentions "rich frosting" but doesn\'t specify what type. Could you clarify your preferences? For example:\n1. Would you prefer buttercream or cream cheese frosting?\n2. Do you want any specific flavor added to the frosting (vanilla, coffee, etc.)?\n3. Should I add any decorative toppings like sprinkles or fruit?\n4. Do you have any dietary restrictions I should be aware of?\n\nThis information will help me make sure the cake meets your expectations for the celebration.</parameter>\n <parameter name="attachments">recipes/chocolate_cake.txt,photos/cake_examples.jpg</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="web_browser_takeover">\n <parameter name="text">I\'ve encountered a CAPTCHA verification on the page. Please:\n1. Solve the CAPTCHA puzzle\n2. Let me know once you\'ve completed it\n3. I\'ll then continue with the automated process\n\nIf you encounter any issues or need to take additional steps, please let me know.</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="complete">\n </invoke>\n </function_calls>',
"sb_browser_tool": '<function_calls>\n <invoke name="browser_navigate_to">\n <parameter name="url">https://example.com</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_go_back">\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_wait">\n <parameter name="seconds">5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_click_element">\n <parameter name="index">2</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_input_text">\n <parameter name="index">2</parameter>\n <parameter name="text">Hello, world!</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_send_keys">\n <parameter name="keys">Enter</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_switch_tab">\n <parameter name="page_id">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_close_tab">\n <parameter name="page_id">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_down">\n <parameter name="amount">500</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_up">\n <parameter name="amount">500</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_to_text">\n <parameter name="text">Contact Us</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_get_dropdown_options">\n <parameter name="index">2</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_select_dropdown_option">\n <parameter name="index">2</parameter>\n <parameter name="text">Option 1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_drag_drop">\n <parameter name="element_source">#draggable</parameter>\n <parameter name="element_target">#droppable</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_click_coordinates">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n </invoke>\n </function_calls>',
"sb_deploy_tool": '<!-- \n IMPORTANT: Only use this tool when:\n 1. The user explicitly requests permanent deployment to production\n 2. You have a complete, ready-to-deploy directory \n \n NOTE: If the same name is used, it will redeploy to the same project as before\n -->\n\n <function_calls>\n <invoke name="deploy">\n <parameter name="name">my-site</parameter>\n <parameter name="directory_path">website</parameter>\n </invoke>\n </function_calls>',
"sb_expose_tool": '<!-- Example 1: Expose a web server running on port 8000 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">8000</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Expose an API service running on port 3000 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">3000</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Expose a development server running on port 5173 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">5173</parameter>\n </invoke>\n </function_calls>',
"sb_files_tool": '<function_calls>\n <invoke name="create_file">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="file_contents">\n # This is the file content\n def main():\n print("Hello, World!")\n \n if __name__ == "__main__":\n main()\n </parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="str_replace">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="old_str">text to replace (must appear exactly once in the file)</parameter>\n <parameter name="new_str">replacement text that will be inserted instead</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="full_file_rewrite">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="file_contents">\n This completely replaces the entire file content.\n Use when making major changes to a file or when the changes\n are too extensive for str-replace.\n All previous content will be lost and replaced with this text.\n </parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="delete_file">\n <parameter name="file_path">src/main.py</parameter>\n </invoke>\n </function_calls>',
"sb_shell_tool": '<function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm run dev</parameter>\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Running in Specific Directory -->\n <function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm run build</parameter>\n <parameter name="folder">frontend</parameter>\n <parameter name="session_name">build_process</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Blocking command (wait for completion) -->\n <function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm install</parameter>\n <parameter name="blocking">true</parameter>\n <parameter name="timeout">300</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="check_command_output">\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n \n <!-- Example 2: Check final output and kill session -->\n <function_calls>\n <invoke name="check_command_output">\n <parameter name="session_name">build_process</parameter>\n <parameter name="kill_session">true</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="terminate_command">\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="list_commands">\n </invoke>\n </function_calls>',
"sb_vision_tool": '<!-- Example: Request to see an image named \'diagram.png\' inside the \'docs\' folder -->\n <function_calls>\n <invoke name="see_image">\n <parameter name="file_path">docs/diagram.png</parameter>\n </invoke>\n </function_calls>',
"update_agent_tool": '<function_calls>\n <invoke name="update_agent">\n <parameter name="name">Research Assistant</parameter>\n <parameter name="description">An AI assistant specialized in conducting research and providing comprehensive analysis</parameter>\n <parameter name="system_prompt">You are a research assistant with expertise in gathering, analyzing, and synthesizing information. Your approach is thorough and methodical...</parameter>\n <parameter name="agentpress_tools">{"web_search": {"enabled": true, "description": "Search the web for information"}, "sb_files": {"enabled": true, "description": "Read and write files"}}</parameter>\n <parameter name="avatar">🔬</parameter>\n <parameter name="avatar_color">#4F46E5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_current_agent_config">\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="search_mcp_servers">\n <parameter name="query">linear</parameter>\n <parameter name="limit">5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_mcp_server_tools">\n <parameter name="qualified_name">exa</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="configure_mcp_server">\n <parameter name="qualified_name">exa</parameter>\n <parameter name="display_name">Exa Search</parameter>\n <parameter name="enabled_tools">["search", "find_similar"]</parameter>\n <parameter name="config">{"exaApiKey": "user-api-key"}</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_popular_mcp_servers">\n <parameter name="category">AI & Search</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="test_mcp_server_connection">\n <parameter name="qualified_name">exa</parameter>\n <parameter name="config">{"exaApiKey": "user-api-key"}</parameter>\n </invoke>\n </function_calls>',
"web_search_tool": '<function_calls>\n <invoke name="web_search">\n <parameter name="query">what is Kortix AI and what are they building?</parameter>\n <parameter name="num_results">20</parameter>\n </invoke>\n </function_calls>\n \n <!-- Another search example -->\n <function_calls>\n <invoke name="web_search">\n <parameter name="query">latest AI research on transformer models</parameter>\n <parameter name="num_results">20</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="scrape_webpage">\n <parameter name="urls">https://www.kortix.ai/,https://github.com/kortix-ai/suna</parameter>\n </invoke>\n </function_calls>',
}
def get_tool_xml_example(tool_id: str) -> str:
"""
Get the XML example for a specific tool.
Args:
tool_id: The tool identifier
Returns:
XML example string for the tool, or empty string if not found
"""
return TOOL_XML_EXAMPLES.get(tool_id, "")
def get_tools_xml_examples(tool_ids: list) -> str:
"""
Get XML examples for multiple tools.
Args:
tool_ids: List of tool identifiers
Returns:
Combined XML examples for all tools
"""
examples = []
for tool_id in tool_ids:
example = get_tool_xml_example(tool_id)
if example:
examples.append(f"## {tool_id.replace('_', ' ').title()}")
examples.append(example)
examples.append("")
return "\n".join(examples)
def get_all_available_tools() -> list:
"""
Get a list of all available tool IDs.
Returns:
List of tool identifiers
"""
return list(TOOL_XML_EXAMPLES.keys())

View File

@ -4,9 +4,21 @@ import { useState, useEffect } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle, Check, X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { getWorkflows, executeWorkflow, deleteWorkflow, getProjects, createWorkflow, type Workflow } from "@/lib/api";
import { Input } from "@/components/ui/input";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { getWorkflows, executeWorkflow, deleteWorkflow, getProjects, createWorkflow, updateWorkflow, type Workflow } from "@/lib/api";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
@ -17,6 +29,10 @@ export default function WorkflowsPage() {
const [executingWorkflows, setExecutingWorkflows] = useState<Set<string>>(new Set());
const [projectId, setProjectId] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [editingWorkflowId, setEditingWorkflowId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [updatingWorkflows, setUpdatingWorkflows] = useState<Set<string>>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
const router = useRouter();
useEffect(() => {
@ -83,18 +99,23 @@ export default function WorkflowsPage() {
toast.error("No project selected");
return;
}
try {
setCreating(true);
const existingNames = workflows.map(w => w.name.toLowerCase());
let workflowName = "Untitled Workflow";
let counter = 1;
while (existingNames.includes(workflowName.toLowerCase())) {
workflowName = `Untitled Workflow ${counter}`;
counter++;
}
const newWorkflow = await createWorkflow({
name: "Untitled Workflow",
name: workflowName,
description: "A new workflow",
project_id: projectId,
nodes: [],
edges: [],
variables: {}
});
toast.success("Workflow created successfully!");
router.push(`/workflows/builder/${newWorkflow.id}`);
} catch (err) {
@ -105,18 +126,77 @@ export default function WorkflowsPage() {
}
};
const handleDeleteWorkflow = async (workflowId: string) => {
if (!confirm('Are you sure you want to delete this workflow?')) {
const handleStartEditName = (workflow: Workflow) => {
setEditingWorkflowId(workflow.id);
setEditingName(workflow.name);
};
const handleCancelEditName = () => {
setEditingWorkflowId(null);
setEditingName("");
};
const handleSaveEditName = async (workflowId: string) => {
if (!editingName.trim()) {
toast.error("Workflow name cannot be empty");
return;
}
// Check for duplicate names
const existingNames = workflows
.filter(w => w.id !== workflowId)
.map(w => w.name.toLowerCase());
if (existingNames.includes(editingName.toLowerCase())) {
toast.error("A workflow with this name already exists");
return;
}
try {
setUpdatingWorkflows(prev => new Set(prev).add(workflowId));
await updateWorkflow(workflowId, {
name: editingName.trim()
});
// Update local state
setWorkflows(prev => prev.map(w =>
w.id === workflowId
? { ...w, name: editingName.trim(), definition: { ...w.definition, name: editingName.trim() } }
: w
));
setEditingWorkflowId(null);
setEditingName("");
toast.success('Workflow name updated successfully');
} catch (err) {
console.error('Error updating workflow name:', err);
toast.error(err instanceof Error ? err.message : 'Failed to update workflow name');
} finally {
setUpdatingWorkflows(prev => {
const newSet = new Set(prev);
newSet.delete(workflowId);
return newSet;
});
}
};
const handleDeleteWorkflow = async (workflowId: string) => {
try {
setDeletingWorkflows(prev => new Set(prev).add(workflowId));
await deleteWorkflow(workflowId);
setWorkflows(prev => prev.filter(w => w.id !== workflowId));
toast.success('Workflow deleted successfully');
} catch (err) {
console.error('Error deleting workflow:', err);
toast.error(err instanceof Error ? err.message : 'Failed to delete workflow');
} finally {
setDeletingWorkflows(prev => {
const newSet = new Set(prev);
newSet.delete(workflowId);
return newSet;
});
}
};
@ -245,8 +325,55 @@ export default function WorkflowsPage() {
<Card key={workflow.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-1">
<CardTitle className="text-lg">{workflow.definition.name || workflow.name}</CardTitle>
<div className="space-y-1 flex-1 mr-2">
{editingWorkflowId === workflow.id ? (
<div className="flex items-center gap-2">
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
className="text-lg font-semibold h-8"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveEditName(workflow.id);
} else if (e.key === 'Escape') {
handleCancelEditName();
}
}}
autoFocus
/>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => handleSaveEditName(workflow.id)}
disabled={updatingWorkflows.has(workflow.id)}
>
{updatingWorkflows.has(workflow.id) ? (
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
) : (
<Check className="h-3 w-3 text-green-600" />
)}
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={handleCancelEditName}
disabled={updatingWorkflows.has(workflow.id)}
>
<X className="h-3 w-3 text-red-600" />
</Button>
</div>
</div>
) : (
<CardTitle
className="text-lg cursor-pointer hover:text-primary transition-colors"
onClick={() => handleStartEditName(workflow)}
>
{workflow.definition.name || workflow.name}
</CardTitle>
)}
<CardDescription>{workflow.definition.description || workflow.description}</CardDescription>
</div>
{getStatusBadge(workflow.status)}
@ -275,13 +402,39 @@ export default function WorkflowsPage() {
)}
Run
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteWorkflow(workflow.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={deletingWorkflows.has(workflow.id)}
>
{deletingWorkflows.has(workflow.id) ? (
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
) : (
<Trash2 className="h-3 w-3" />
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{workflow.definition.name || workflow.name}"?
This action cannot be undone and will permanently remove the workflow and all its data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteWorkflow(workflow.id)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete Workflow
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</CardContent>

View File

@ -0,0 +1,230 @@
"use client";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import InputNode from "./nodes/InputNode";
import { validateWorkflow } from "./WorkflowValidator";
const sampleInputNodeData = {
label: "Input",
prompt: "Generate daily sales report and email to stakeholders",
trigger_type: "SCHEDULE" as const,
schedule_config: {
interval_type: "hours" as const,
interval_value: 24,
timezone: "UTC",
enabled: true
},
variables: {
recipients: "manager@company.com,sales@company.com",
report_type: "daily_summary"
}
};
const sampleNodes = [
{
id: "input-1",
type: "inputNode",
position: { x: 100, y: 100 },
data: sampleInputNodeData
},
{
id: "agent-1",
type: "agentNode",
position: { x: 400, y: 100 },
data: {
label: "Report Generator",
instructions: "Generate and send reports"
}
}
];
const sampleEdges = [
{
id: "e1",
source: "input-1",
target: "agent-1"
}
];
export default function InputNodeDemo() {
const [nodeData, setNodeData] = useState(sampleInputNodeData);
const validation = validateWorkflow(sampleNodes, sampleEdges);
const updateNodeData = (updates: any) => {
setNodeData(prev => ({ ...prev, ...updates }));
};
return (
<div className="p-6 space-y-6 max-w-4xl mx-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Input Node Demo
<Badge variant="outline">Frontend Implementation</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Validation Status */}
<div className="p-4 rounded-lg bg-muted/50">
<h3 className="font-medium mb-2">Validation Status</h3>
<div className="flex items-center gap-2">
<Badge variant={validation.valid ? "default" : "destructive"}>
{validation.valid ? "Valid" : "Invalid"}
</Badge>
<span className="text-sm text-muted-foreground">
{validation.errors.length} issues found
</span>
</div>
{validation.errors.length > 0 && (
<div className="mt-2 space-y-1">
{validation.errors.map((error, index) => (
<div key={index} className="text-sm text-red-600">
{error.message}
</div>
))}
</div>
)}
</div>
{/* Input Node Preview */}
<div className="border rounded-lg p-4 bg-background">
<h3 className="font-medium mb-4">Input Node Component</h3>
<div className="flex justify-center">
<InputNode
id="demo-input"
data={nodeData}
selected={false}
type="inputNode"
isConnectable={true}
dragging={false}
zIndex={1}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
</div>
</div>
{/* Configuration Summary */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-sm">Current Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div>
<span className="font-medium">Prompt:</span>
<p className="text-muted-foreground mt-1">{nodeData.prompt}</p>
</div>
<div>
<span className="font-medium">Trigger:</span>
<Badge variant="outline" className="ml-2">{nodeData.trigger_type}</Badge>
</div>
{nodeData.trigger_type === 'SCHEDULE' && nodeData.schedule_config && (
<div>
<span className="font-medium">Schedule:</span>
<p className="text-muted-foreground mt-1">
Every {nodeData.schedule_config.interval_value} {nodeData.schedule_config.interval_type}
</p>
</div>
)}
<div>
<span className="font-medium">Variables:</span>
<div className="mt-1 space-y-1">
{Object.entries(nodeData.variables || {}).map(([key, value]) => (
<div key={key} className="text-xs">
<span className="font-mono bg-muted px-1 rounded">{key}</span>: {value}
</div>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Features Implemented</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Prompt configuration</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Trigger type selection</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Schedule configuration</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Webhook configuration</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Variables editor</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Workflow validation</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Connection rules</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Visual indicators</span>
</div>
</CardContent>
</Card>
</div>
{/* Test Actions */}
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => updateNodeData({ trigger_type: "MANUAL" })}
>
Set Manual Trigger
</Button>
<Button
variant="outline"
onClick={() => updateNodeData({
trigger_type: "SCHEDULE",
schedule_config: {
interval_type: "days",
interval_value: 1,
timezone: "UTC",
enabled: true
}
})}
>
Set Daily Schedule
</Button>
<Button
variant="outline"
onClick={() => updateNodeData({
trigger_type: "WEBHOOK",
webhook_config: {
method: "POST",
authentication: "api_key"
}
})}
>
Set Webhook Trigger
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,136 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { Trash2, Plus } from "lucide-react";
interface KeyValueEditorProps {
values: Record<string, any>;
onChange: (values: Record<string, any>) => void;
placeholder?: {
key?: string;
value?: string;
};
}
export default function KeyValueEditor({
values,
onChange,
placeholder = { key: "Variable name", value: "Variable value" }
}: KeyValueEditorProps) {
const [newKey, setNewKey] = useState("");
const [newValue, setNewValue] = useState("");
const handleAddVariable = () => {
if (newKey.trim() && !values.hasOwnProperty(newKey.trim())) {
onChange({
...values,
[newKey.trim()]: newValue.trim() || ""
});
setNewKey("");
setNewValue("");
}
};
const handleUpdateVariable = (key: string, value: string) => {
onChange({
...values,
[key]: value
});
};
const handleDeleteVariable = (key: string) => {
const newValues = { ...values };
delete newValues[key];
onChange(newValues);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleAddVariable();
}
};
return (
<div className="space-y-3">
{/* Existing Variables */}
{Object.entries(values).map(([key, value]) => (
<Card key={key} className="p-3">
<div className="flex items-center gap-2">
<div className="flex-1 grid grid-cols-2 gap-2">
<Input
value={key}
onChange={(e) => {
const newKey = e.target.value;
if (newKey !== key) {
const newValues = { ...values };
delete newValues[key];
newValues[newKey] = value;
onChange(newValues);
}
}}
placeholder={placeholder.key}
className="h-8 text-sm"
/>
<Input
value={value}
onChange={(e) => handleUpdateVariable(key, e.target.value)}
placeholder={placeholder.value}
className="h-8 text-sm"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteVariable(key)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</Card>
))}
{/* Add New Variable */}
<Card className="p-3 border-dashed">
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<Input
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
placeholder={placeholder.key}
onKeyPress={handleKeyPress}
className="h-8 text-sm"
/>
<Input
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
placeholder={placeholder.value}
onKeyPress={handleKeyPress}
className="h-8 text-sm"
/>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAddVariable}
disabled={!newKey.trim()}
className="w-full h-8"
>
<Plus className="h-4 w-4 mr-2" />
Add Variable
</Button>
</div>
</Card>
{Object.keys(values).length === 0 && (
<p className="text-xs text-muted-foreground text-center py-2">
No variables configured. Add variables to pass data to your workflow.
</p>
)}
</div>
);
}

View File

@ -21,9 +21,21 @@ import {
Send,
Settings,
Sparkles,
Zap
Zap,
Play
} from "lucide-react";
const inputNodes = [
{
id: "inputNode",
name: "Input",
description: "Workflow input configuration with prompt and trigger settings",
icon: Play,
category: "input",
required: true,
},
];
const agentNodes = [
{
id: "agent",
@ -140,11 +152,14 @@ function DraggableNode({ type, data, children }: DraggableNodeProps) {
export default function NodePalette() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>("agent");
const [selectedCategory, setSelectedCategory] = useState<string | null>("input");
const filteredNodes = useMemo(() => {
let nodes: any[] = [];
if (!selectedCategory || selectedCategory === "input") {
nodes = [...nodes, ...inputNodes];
}
if (!selectedCategory || selectedCategory === "agent") {
nodes = [...nodes, ...agentNodes];
}
@ -163,6 +178,7 @@ export default function NodePalette() {
}, [searchQuery, selectedCategory]);
const getNodesByCategory = (category: string) => {
if (category === "input") return inputNodes;
if (category === "agent") return agentNodes;
if (category === "tools") return toolNodes;
return [];
@ -195,7 +211,11 @@ export default function NodePalette() {
<div className="flex-1 flex flex-col overflow-hidden">
<div className="px-4 pt-2 pb-4">
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
<TabsList className="grid w-full grid-cols-2">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="input">
<Play className="h-4 w-4 mr-2" />
Input
</TabsTrigger>
<TabsTrigger value="agent">
<Bot className="h-4 w-4 mr-2" />
Agents
@ -209,6 +229,60 @@ export default function NodePalette() {
</div>
<div className="flex-1 overflow-hidden px-4">
{selectedCategory === "input" && (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-muted-foreground">Workflow Input</h4>
<Badge variant="outline" className="text-xs">
Required
</Badge>
</div>
<ScrollArea className="flex-1 overflow-y-auto">
<div className="space-y-3 pr-3 mb-4">
{getNodesByCategory("input").filter(node =>
!searchQuery ||
node.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
node.description.toLowerCase().includes(searchQuery.toLowerCase())
).map((node) => {
const Icon = node.icon;
return (
<DraggableNode
key={node.id}
type="inputNode"
data={{
label: node.name,
prompt: "",
trigger_type: "MANUAL",
variables: {}
}}
>
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5">
<CardHeader className="p-4 py-0">
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-primary/20 border border-primary/30 group-hover:bg-primary/30 transition-colors">
<Icon className="h-5 w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<CardTitle className="text-sm font-semibold leading-tight flex items-center gap-2">
{node.name}
<Badge variant="outline" className="text-xs">Required</Badge>
</CardTitle>
<CardDescription className="text-xs mt-1 line-clamp-2">
{node.description}
</CardDescription>
</div>
</div>
</CardHeader>
</Card>
</DraggableNode>
);
})}
</div>
</ScrollArea>
</div>
)}
{selectedCategory === "agent" && (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-3">

View File

@ -30,17 +30,24 @@ import {
Zap,
Workflow,
Eye,
EyeOff
EyeOff,
AlertTriangle,
CheckCircle,
XCircle
} from "lucide-react";
import NodePalette from "./NodePalette";
import WorkflowSettings from "./WorkflowSettings";
import AgentNode from "./nodes/AgentNode";
import ToolConnectionNode from "./nodes/ToolConnectionNode";
import InputNode from "./nodes/InputNode";
import { getProjects, createWorkflow, updateWorkflow, getWorkflow, executeWorkflow, type WorkflowNode, type WorkflowEdge } from "@/lib/api";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { validateWorkflow, canConnect, type ValidationResult } from "./WorkflowValidator";
import { WorkflowProvider } from "./WorkflowContext";
const nodeTypes = {
inputNode: InputNode,
agentNode: AgentNode,
toolConnectionNode: ToolConnectionNode,
};
@ -138,12 +145,12 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
const [saving, setSaving] = useState(false);
const [running, setRunning] = useState(false);
const [projectId, setProjectId] = useState<string | null>(null);
const [validationResult, setValidationResult] = useState<ValidationResult>({ valid: true, errors: [] });
const router = useRouter();
useEffect(() => {
const loadData = async () => {
try {
// Get user's first project
const projects = await getProjects();
if (projects.length === 0) {
toast.error("No projects found. Please create a project first.");
@ -152,12 +159,16 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
const firstProject = projects[0];
setProjectId(firstProject.id);
// If editing existing workflow, load it
if (workflowId) {
const workflow = await getWorkflow(workflowId);
setWorkflowName(workflow.definition.name || workflow.name);
setWorkflowDescription(workflow.definition.description || workflow.description);
console.log('Loaded workflow:', workflow);
const displayName = workflow.name || workflow.definition.name || "Untitled Workflow";
const displayDescription = workflow.description || workflow.definition.description || "";
console.log('Setting workflow name to:', displayName);
setWorkflowName(displayName);
setWorkflowDescription(displayDescription);
if (workflow.definition.nodes && workflow.definition.edges) {
setNodes(workflow.definition.nodes);
@ -173,15 +184,38 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
loadData();
}, [workflowId, setNodes, setEdges]);
useEffect(() => {
const result = validateWorkflow(nodes, edges);
setValidationResult(result);
}, [nodes, edges]);
const updateNodeData = useCallback((nodeId: string, updates: any) => {
setNodes((nds) =>
nds.map((node) =>
node.id === nodeId
? { ...node, data: { ...node.data, ...updates } }
: node
)
);
}, [setNodes]);
const handleSaveWorkflow = async () => {
if (!projectId) {
toast.error("No project selected");
return;
}
const validation = validateWorkflow(nodes, edges);
if (!validation.valid) {
const errorMessages = validation.errors
.filter(e => e.type === 'error')
.map(e => e.message)
.join(', ');
toast.error(`Cannot save workflow: ${errorMessages}`);
return;
}
try {
setSaving(true);
const workflowData = {
name: workflowName,
description: workflowDescription,
@ -465,9 +499,16 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
y: event.clientY - 100,
};
let nodeType = "toolConnectionNode";
if (nodeData.nodeId === "agent") {
nodeType = "agentNode";
} else if (type === "inputNode" || nodeData.nodeId === "inputNode") {
nodeType = "inputNode";
}
const newNode: Node = {
id: `${type}-${Date.now()}`,
type: nodeData.nodeId === "agent" ? "agentNode" : "toolConnectionNode",
type: nodeType,
position,
data: {
...nodeData,
@ -522,117 +563,123 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
)}
<div className="flex-1 flex flex-col">
<Card className="mb-0 rounded-none border-b shadow-none">
<CardHeader className="py-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-500/20">
<Workflow className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<CardTitle className="text-lg font-semibold">{workflowName}</CardTitle>
<p className="text-sm text-muted-foreground">{workflowDescription || "No description"}</p>
</div>
<div className="h-16 px-6 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center justify-between h-full">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-500/20">
<Workflow className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setShowNodePalette(!showNodePalette)}
className="text-muted-foreground hover:text-foreground"
>
{showNodePalette ? <EyeOff className="h-4 w-4 mr-2" /> : <Eye className="h-4 w-4 mr-2" />}
{showNodePalette ? "Hide" : "Show"} Palette
</Button>
<Separator orientation="vertical" className="h-6" />
<Button
variant="ghost"
onClick={() => setShowSettings(true)}
className="text-muted-foreground hover:text-foreground"
>
<Settings className="h-4 w-4" />
Settings
</Button>
<Button
variant="outline"
onClick={handleSaveWorkflow}
disabled={saving}
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
{saving ? "Saving..." : "Save"}
</Button>
<Button variant="outline">
<Share className="h-4 w-4 mr-2" />
Share
</Button>
<Button
onClick={handleRunWorkflow}
disabled={running || !workflowId}
>
{running ? (
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
) : (
<Play className="h-4 w-4 mr-2" />
)}
{running ? "Running..." : "Run Workflow"}
</Button>
<div>
<h1 className="text-lg font-semibold">{workflowName}</h1>
</div>
</div>
</CardHeader>
</Card>
<div className="flex-1 border border-border/50 bg-card/30 backdrop-blur-sm overflow-hidden">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={handleEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
defaultEdgeOptions={defaultEdgeOptions}
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineStyle={{
strokeWidth: 2,
stroke: '#6366f1',
}}
fitView
className="bg-transparent"
>
<Controls
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
showZoom={true}
showFitView={true}
showInteractive={true}
/>
<MiniMap
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
nodeColor={(node) => {
if (node.type === 'agentNode') return '#6366f1';
return '#9ca3af';
}}
/>
<Background
variant={BackgroundVariant.Dots}
gap={20}
size={1}
className="opacity-30 dark:opacity-20"
/>
</ReactFlow>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleSaveWorkflow}
disabled={saving}
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
) : (
<Save className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save"}
</Button>
<Button
onClick={handleRunWorkflow}
disabled={running || !workflowId}
>
{running ? (
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
) : (
<Play className="h-4 w-4" />
)}
{running ? "Running..." : "Run Workflow"}
</Button>
</div>
</div>
</div>
<Card className="rounded-none border-border/50 bg-card/80 backdrop-blur-sm">
{/* {validationResult.errors.length > 0 && (
<Card className="mx-4 mt-4 border-l-4 border-l-red-500">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
{validationResult.valid ? (
<AlertTriangle className="h-4 w-4 text-yellow-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className="text-sm font-medium">
{validationResult.valid ? 'Warnings' : 'Validation Errors'}
</span>
<Badge variant={validationResult.valid ? "secondary" : "destructive"} className="text-xs">
{validationResult.errors.length}
</Badge>
</div>
<div className="space-y-2">
{validationResult.errors.map((error, index) => (
<div key={index} className={`flex items-start gap-2 text-sm p-2 rounded ${
error.type === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-950/20 dark:text-red-300' : 'bg-yellow-50 text-yellow-700 dark:bg-yellow-950/20 dark:text-yellow-300'
}`}>
{error.type === 'error' ? (
<XCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
) : (
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
)}
<span>{error.message}</span>
</div>
))}
</div>
</CardContent>
</Card>
)} */}
<div className="flex-1 border border-border/50 bg-card/30 backdrop-blur-sm overflow-hidden">
<WorkflowProvider updateNodeData={updateNodeData}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={handleEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
defaultEdgeOptions={defaultEdgeOptions}
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineStyle={{
strokeWidth: 2,
stroke: '#6366f1',
}}
fitView
className="bg-transparent"
>
<Controls
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
showZoom={true}
showFitView={true}
showInteractive={true}
/>
<MiniMap
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
nodeColor={(node) => {
if (node.type === 'agentNode') return '#6366f1';
return '#9ca3af';
}}
/>
<Background
variant={BackgroundVariant.Dots}
gap={20}
size={1}
className="opacity-30 dark:opacity-20"
/>
</ReactFlow>
</WorkflowProvider>
</div>
{/* <Card className="h-13.5 rounded-none border-border/50 bg-card/80 backdrop-blur-sm">
<CardContent className="py-0">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-4">
@ -654,7 +701,7 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
</div>
</div>
</CardContent>
</Card>
</Card> */}
</div>
<WorkflowSettings

View File

@ -0,0 +1,31 @@
"use client";
import { createContext, useContext, ReactNode } from "react";
interface WorkflowContextType {
updateNodeData: (nodeId: string, updates: any) => void;
}
const WorkflowContext = createContext<WorkflowContextType | null>(null);
export function WorkflowProvider({
children,
updateNodeData
}: {
children: ReactNode;
updateNodeData: (nodeId: string, updates: any) => void;
}) {
return (
<WorkflowContext.Provider value={{ updateNodeData }}>
{children}
</WorkflowContext.Provider>
);
}
export function useWorkflow() {
const context = useContext(WorkflowContext);
if (!context) {
throw new Error("useWorkflow must be used within a WorkflowProvider");
}
return context;
}

View File

@ -0,0 +1,143 @@
"use client";
import { Node, Edge } from "@xyflow/react";
export interface ValidationError {
type: 'error' | 'warning';
message: string;
nodeId?: string;
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
export function validateWorkflow(nodes: Node[], edges: Edge[]): ValidationResult {
const errors: ValidationError[] = [];
// Check for required input node
const hasInputNode = nodes.some(node => node.type === 'inputNode');
if (!hasInputNode) {
errors.push({
type: 'error',
message: 'Every workflow must have an input node'
});
}
// Validate input node configuration
const inputNodes = nodes.filter(node => node.type === 'inputNode');
inputNodes.forEach(node => {
const data = node.data as any;
if (!data.prompt || (typeof data.prompt === 'string' && data.prompt.trim() === '')) {
errors.push({
type: 'error',
message: 'Input node must have a prompt configured',
nodeId: node.id
});
}
if (data.trigger_type === 'SCHEDULE') {
const scheduleConfig = data.schedule_config as any;
if (!scheduleConfig?.cron_expression &&
!(scheduleConfig?.interval_type && scheduleConfig?.interval_value)) {
errors.push({
type: 'error',
message: 'Schedule trigger must have either cron expression or interval configuration',
nodeId: node.id
});
}
} else if (data.trigger_type === 'WEBHOOK') {
const webhookConfig = data.webhook_config;
if (!webhookConfig) {
errors.push({
type: 'error',
message: 'Webhook trigger must have webhook configuration',
nodeId: node.id
});
}
}
});
// Check for multiple input nodes (warning)
if (inputNodes.length > 1) {
errors.push({
type: 'warning',
message: 'Multiple input nodes detected. Only one input node is recommended per workflow.'
});
}
// Check for disconnected nodes
const connectedNodeIds = new Set<string>();
edges.forEach(edge => {
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
});
const disconnectedNodes = nodes.filter(node =>
!connectedNodeIds.has(node.id) && nodes.length > 1
);
if (disconnectedNodes.length > 0) {
disconnectedNodes.forEach(node => {
errors.push({
type: 'warning',
message: `Node "${node.data.label || node.id}" is not connected to the workflow`,
nodeId: node.id
});
});
}
// Check for self-referencing edges
edges.forEach(edge => {
if (edge.source === edge.target) {
errors.push({
type: 'error',
message: 'Self-referencing connections are not allowed',
nodeId: edge.source
});
}
});
// Check that input nodes cannot receive connections
edges.forEach(edge => {
const targetNode = nodes.find(n => n.id === edge.target);
if (targetNode?.type === 'inputNode') {
errors.push({
type: 'error',
message: 'Input nodes cannot receive connections from other nodes',
nodeId: edge.target
});
}
});
// Check for at least one agent node
const hasAgentNode = nodes.some(node => node.type === 'agentNode');
if (!hasAgentNode && nodes.length > 1) {
errors.push({
type: 'warning',
message: 'Workflow should have at least one agent node'
});
}
return {
valid: errors.filter(e => e.type === 'error').length === 0,
errors
};
}
export function canConnect(source: Node, target: Node): boolean {
// Input nodes cannot receive connections
if (target.type === 'inputNode') {
return false;
}
// Input nodes can connect to any other node
if (source.type === 'inputNode') {
return true;
}
// Other connection rules can be added here
return true;
}

View File

@ -101,38 +101,6 @@ const AgentNode = memo(({ data, selected }: NodeProps) => {
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Connected Tools</label>
<Badge variant="outline" className="text-xs">
{connectedTools.length} tools
</Badge>
</div>
{connectedTools.length > 0 ? (
<div className="space-y-2">
{connectedTools.map((tool) => (
<div key={tool.id} className="flex items-center gap-2 p-2 bg-muted/30 rounded-lg border">
<div className="p-1 rounded bg-primary/10">
<Zap className="h-3 w-3 text-primary" />
</div>
<span className="text-sm font-medium flex-1">{tool.name}</span>
<Badge variant="outline" className="text-xs">
{tool.type.replace('_tool', '').replace('_', ' ')}
</Badge>
</div>
))}
</div>
) : (
<div className="bg-muted/30 rounded-lg p-3 border border-dashed">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Plus className="h-3 w-3" />
Connect tools from the palette
</div>
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Output Connections</label>

View File

@ -1,59 +1,551 @@
"use client";
import { memo } from "react";
import { memo, useState } from "react";
import { Handle, Position, NodeProps } from "@xyflow/react";
import { MessageSquare, Type, Upload, Webhook, Clock } from "lucide-react";
import { Play, Settings, Clock, Webhook, User, ChevronDown, ChevronUp } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import KeyValueEditor from "../KeyValueEditor";
import { useWorkflow } from "../WorkflowContext";
interface InputNodeData {
label: string;
nodeId: string;
config: any;
interface SlackWebhookConfig {
webhook_url: string;
signing_secret: string;
channel?: string;
username?: string;
}
const InputNode = memo(({ data, selected }: NodeProps) => {
const nodeData = data as unknown as InputNodeData;
const getIcon = () => {
switch (nodeData.nodeId) {
case "chat-input":
return <MessageSquare className="h-4 w-4 text-white" />;
case "text-input":
return <Type className="h-4 w-4 text-white" />;
case "file-input":
return <Upload className="h-4 w-4 text-white" />;
case "webhook-input":
return <Webhook className="h-4 w-4 text-white" />;
case "schedule-input":
return <Clock className="h-4 w-4 text-white" />;
interface WebhookConfig {
type: 'slack' | 'generic';
method?: 'POST' | 'GET' | 'PUT';
authentication?: 'none' | 'api_key' | 'bearer';
slack?: SlackWebhookConfig;
generic?: {
url: string;
headers?: Record<string, string>;
auth_token?: string;
};
}
interface InputNodeData {
label?: string;
prompt?: string;
trigger_type?: 'MANUAL' | 'WEBHOOK' | 'SCHEDULE';
schedule_config?: {
interval_type?: 'minutes' | 'hours' | 'days' | 'weeks';
interval_value?: number;
cron_expression?: string;
timezone?: string;
enabled?: boolean;
};
webhook_config?: WebhookConfig;
variables?: Record<string, any>;
}
const InputNode = memo(({ data, selected, id }: NodeProps) => {
const nodeData = data as InputNodeData;
const [isExpanded, setIsExpanded] = useState(false);
const [isConfigOpen, setIsConfigOpen] = useState(false);
const { updateNodeData } = useWorkflow();
const getTriggerIcon = () => {
switch (nodeData.trigger_type) {
case 'SCHEDULE':
return <Clock className="h-4 w-4" />;
case 'WEBHOOK':
return <Webhook className="h-4 w-4" />;
case 'MANUAL':
default:
return <MessageSquare className="h-4 w-4 text-white" />;
return <User className="h-4 w-4" />;
}
};
const getTriggerColor = () => {
switch (nodeData.trigger_type) {
case 'SCHEDULE':
return 'bg-orange-500';
case 'WEBHOOK':
return 'bg-purple-500';
case 'MANUAL':
default:
return 'bg-blue-500';
}
};
const getTriggerDescription = () => {
switch (nodeData.trigger_type) {
case 'SCHEDULE':
if (nodeData.schedule_config?.cron_expression) {
return `Cron: ${nodeData.schedule_config.cron_expression}`;
} else if (nodeData.schedule_config?.interval_type && nodeData.schedule_config?.interval_value) {
return `Every ${nodeData.schedule_config.interval_value} ${nodeData.schedule_config.interval_type}`;
}
return 'Scheduled execution';
case 'WEBHOOK':
if (nodeData.webhook_config?.type === 'slack') {
return 'Slack webhook';
}
return `${nodeData.webhook_config?.method || 'POST'} webhook`;
case 'MANUAL':
default:
return 'Manual execution';
}
};
return (
<div className={`relative bg-gray-800 rounded-lg border border-gray-600 min-w-[160px] ${selected ? "ring-2 ring-blue-400" : ""}`}>
{/* Icon section */}
<div className="flex items-center justify-center p-3 bg-blue-500 rounded-t-lg">
{getIcon()}
<div className={`relative bg-card rounded-lg border-2 min-w-[280px] max-w-[400px] ${
selected ? "border-primary shadow-lg" : "border-border"
}`}>
{/* Header */}
<div className={`flex items-center justify-between p-3 ${getTriggerColor()} rounded-t-lg text-white`}>
<div className="flex items-center gap-2">
<Play className="h-5 w-5" />
<span className="font-medium">Input</span>
</div>
<div className="flex items-center gap-2">
{getTriggerIcon()}
<Badge variant="secondary" className="text-xs bg-white/20 text-white border-white/30">
{nodeData.trigger_type || 'MANUAL'}
</Badge>
</div>
</div>
{/* Content section */}
<div className="p-3 text-center">
<h3 className="text-sm font-medium text-white">{nodeData.label}</h3>
<p className="text-xs text-gray-400 mt-1">
{nodeData.nodeId === "chat-input" && "Get chat inputs from the Playground."}
{nodeData.nodeId === "text-input" && "Static text input"}
{nodeData.nodeId === "file-input" && "Upload and read files"}
{nodeData.nodeId === "webhook-input" && "Receive webhook data"}
{nodeData.nodeId === "schedule-input" && "Scheduled trigger"}
</p>
</div>
{/* Output handle */}
{/* Content */}
<CardContent className="p-4 space-y-3">
{/* Prompt Preview */}
<div>
<Label className="text-xs font-medium text-muted-foreground">Prompt</Label>
<p className="text-sm mt-1 line-clamp-2 text-foreground">
{nodeData.prompt || "No prompt configured"}
</p>
</div>
{/* Trigger Info */}
<div>
<Label className="text-xs font-medium text-muted-foreground">Trigger</Label>
<p className="text-sm mt-1 text-foreground">
{getTriggerDescription()}
</p>
</div>
{/* Variables Preview */}
{nodeData.variables && Object.keys(nodeData.variables).length > 0 && (
<div>
<Label className="text-xs font-medium text-muted-foreground">Variables</Label>
<div className="flex flex-wrap gap-1 mt-1">
{Object.keys(nodeData.variables).slice(0, 3).map((key) => (
<Badge key={key} variant="outline" className="text-xs">
{key}
</Badge>
))}
{Object.keys(nodeData.variables).length > 3 && (
<Badge variant="outline" className="text-xs">
+{Object.keys(nodeData.variables).length - 3} more
</Badge>
)}
</div>
</div>
)}
<Separator />
{/* Configuration Toggle */}
<Collapsible open={isConfigOpen} onOpenChange={setIsConfigOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-full justify-between p-2">
<span className="flex items-center gap-2">
<Settings className="h-4 w-4" />
Configure
</span>
{isConfigOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-3">
{/* Prompt Configuration */}
<div className="space-y-2">
<Label htmlFor={`prompt-${id}`} className="text-sm font-medium">
Workflow Prompt *
</Label>
<Textarea
id={`prompt-${id}`}
placeholder="Describe what this workflow should accomplish..."
value={nodeData.prompt || ''}
onChange={(e) => updateNodeData(id, { prompt: e.target.value })}
className="min-h-[80px] text-sm"
/>
</div>
{/* <div className="space-y-2">
<Label className="text-sm font-medium">Trigger Type *</Label>
<Select
value={nodeData.trigger_type || 'MANUAL'}
onValueChange={(value: 'MANUAL' | 'WEBHOOK' | 'SCHEDULE') =>
updateNodeData(id, { trigger_type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="MANUAL">
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
Manual
</div>
</SelectItem>
<SelectItem value="WEBHOOK">
<div className="flex items-center gap-2">
<Webhook className="h-4 w-4" />
Webhook
</div>
</SelectItem>
<SelectItem value="SCHEDULE">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
Schedule
</div>
</SelectItem>
</SelectContent>
</Select>
</div> */}
{/* {nodeData.trigger_type === 'SCHEDULE' && (
<div className="space-y-3 p-3 bg-muted/50 rounded-lg">
<Label className="text-sm font-medium">Schedule Configuration</Label>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">Interval Type</Label>
<Select
value={nodeData.schedule_config?.interval_type || 'hours'}
onValueChange={(value: 'minutes' | 'hours' | 'days' | 'weeks') =>
updateNodeData(id, {
schedule_config: {
...nodeData.schedule_config,
interval_type: value
}
})
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="minutes">Minutes</SelectItem>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
<SelectItem value="weeks">Weeks</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Interval Value</Label>
<Input
type="number"
min="1"
placeholder="1"
value={nodeData.schedule_config?.interval_value || ''}
onChange={(e) =>
updateNodeData(id, {
schedule_config: {
...nodeData.schedule_config,
interval_value: parseInt(e.target.value) || 1
}
})
}
className="h-8"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Cron Expression (Advanced)</Label>
<Input
placeholder="0 9 * * 1-5 (weekdays at 9 AM)"
value={nodeData.schedule_config?.cron_expression || ''}
onChange={(e) =>
updateNodeData(id, {
schedule_config: {
...nodeData.schedule_config,
cron_expression: e.target.value
}
})
}
className="h-8 text-xs font-mono"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Timezone</Label>
<Select
value={nodeData.schedule_config?.timezone || 'UTC'}
onValueChange={(value) =>
updateNodeData(id, {
schedule_config: {
...nodeData.schedule_config,
timezone: value
}
})
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="UTC">UTC</SelectItem>
<SelectItem value="America/New_York">Eastern Time</SelectItem>
<SelectItem value="America/Chicago">Central Time</SelectItem>
<SelectItem value="America/Denver">Mountain Time</SelectItem>
<SelectItem value="America/Los_Angeles">Pacific Time</SelectItem>
<SelectItem value="Europe/London">London</SelectItem>
<SelectItem value="Europe/Paris">Paris</SelectItem>
<SelectItem value="Asia/Tokyo">Tokyo</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)} */}
{/* {nodeData.trigger_type === 'WEBHOOK' && (
<div className="space-y-3 p-3 bg-muted/50 rounded-lg">
<Label className="text-sm font-medium">Webhook Configuration</Label>
<div className="space-y-1">
<Label className="text-xs">Webhook Type</Label>
<Select
value={nodeData.webhook_config?.type || 'slack'}
onValueChange={(value: 'slack' | 'generic') =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
type: value
}
})
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="slack">Slack</SelectItem>
<SelectItem value="generic">Generic</SelectItem>
</SelectContent>
</Select>
</div>
{nodeData.webhook_config?.type === 'slack' && (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs">Slack Webhook URL *</Label>
<Input
placeholder="https://hooks.slack.com/services/..."
value={nodeData.webhook_config?.slack?.webhook_url || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
slack: {
...nodeData.webhook_config?.slack,
webhook_url: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Signing Secret *</Label>
<Input
type="password"
placeholder="Your Slack app signing secret"
value={nodeData.webhook_config?.slack?.signing_secret || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
slack: {
...nodeData.webhook_config?.slack,
signing_secret: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">Channel (Optional)</Label>
<Input
placeholder="#general"
value={nodeData.webhook_config?.slack?.channel || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
slack: {
...nodeData.webhook_config?.slack,
channel: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Username (Optional)</Label>
<Input
placeholder="WorkflowBot"
value={nodeData.webhook_config?.slack?.username || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
slack: {
...nodeData.webhook_config?.slack,
username: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
</div>
</div>
)}
{nodeData.webhook_config?.type === 'generic' && (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs">Webhook URL *</Label>
<Input
placeholder="https://your-webhook-endpoint.com"
value={nodeData.webhook_config?.generic?.url || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
generic: {
...nodeData.webhook_config?.generic,
url: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">HTTP Method</Label>
<Select
value={nodeData.webhook_config?.method || 'POST'}
onValueChange={(value: 'POST' | 'GET' | 'PUT') =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
method: value
}
})
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Authentication</Label>
<Select
value={nodeData.webhook_config?.authentication || 'none'}
onValueChange={(value: 'none' | 'api_key' | 'bearer') =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
authentication: value
}
})
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="api_key">API Key</SelectItem>
<SelectItem value="bearer">Bearer Token</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{nodeData.webhook_config?.authentication !== 'none' && (
<div className="space-y-1">
<Label className="text-xs">Auth Token</Label>
<Input
type="password"
placeholder="Your authentication token"
value={nodeData.webhook_config?.generic?.auth_token || ''}
onChange={(e) =>
updateNodeData(id, {
webhook_config: {
...nodeData.webhook_config,
generic: {
...nodeData.webhook_config?.generic,
auth_token: e.target.value
}
}
})
}
className="h-8 text-xs"
/>
</div>
)}
</div>
)}
</div>
)} */}
{/* <div className="space-y-2">
<Label className="text-sm font-medium">Default Variables</Label>
<KeyValueEditor
values={nodeData.variables || {}}
onChange={(variables) => updateNodeData(id, { variables })}
placeholder={{
key: "Variable name",
value: "Default value"
}}
/>
</div> */}
</CollapsibleContent>
</Collapsible>
</CardContent>
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 !bg-gray-400 !border-gray-300"
className="w-3 h-3 !bg-primary !border-primary-foreground"
style={{ right: -6 }}
/>
</div>