mirror of https://github.com/kortix-ai/suna.git
feat: agent schedules
This commit is contained in:
parent
debbb1246f
commit
ff47404a89
|
@ -437,6 +437,191 @@ async def universal_slack_webhook(request: Request):
|
||||||
content={"error": "Internal server error"}
|
content={"error": "Internal server error"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.post("/qstash/webhook")
|
||||||
|
async def handle_qstash_webhook(request: Request):
|
||||||
|
try:
|
||||||
|
logger.info("QStash webhook received")
|
||||||
|
body = await request.body()
|
||||||
|
headers = dict(request.headers)
|
||||||
|
|
||||||
|
logger.debug(f"QStash webhook body: {body[:500]}...")
|
||||||
|
logger.debug(f"QStash webhook headers: {headers}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if body:
|
||||||
|
data = await request.json()
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to parse JSON body: {e}")
|
||||||
|
data = {
|
||||||
|
"raw_body": body.decode('utf-8', errors='ignore'),
|
||||||
|
"content_type": headers.get('content-type', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger_id = data.get('trigger_id')
|
||||||
|
|
||||||
|
if not trigger_id:
|
||||||
|
logger.error("No trigger_id in QStash webhook payload")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "trigger_id is required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
data["headers"] = headers
|
||||||
|
data["qstash_message_id"] = headers.get('upstash-message-id')
|
||||||
|
data["qstash_schedule_id"] = headers.get('upstash-schedule-id')
|
||||||
|
|
||||||
|
logger.info(f"Processing QStash trigger event for {trigger_id}")
|
||||||
|
manager = await get_trigger_manager()
|
||||||
|
result = await manager.process_trigger_event(trigger_id, data)
|
||||||
|
|
||||||
|
logger.info(f"QStash trigger processing result: success={result.success}, should_execute={result.should_execute_agent}, error={result.error_message}")
|
||||||
|
|
||||||
|
if result.success and result.should_execute_agent:
|
||||||
|
executor = AgentTriggerExecutor(db)
|
||||||
|
trigger_config = await manager.get_trigger(trigger_id)
|
||||||
|
if trigger_config:
|
||||||
|
from .core import TriggerEvent, TriggerType
|
||||||
|
trigger_type = trigger_config.trigger_type
|
||||||
|
if isinstance(trigger_type, str):
|
||||||
|
trigger_type = TriggerType(trigger_type)
|
||||||
|
|
||||||
|
trigger_event = TriggerEvent(
|
||||||
|
trigger_id=trigger_id,
|
||||||
|
agent_id=trigger_config.agent_id,
|
||||||
|
trigger_type=trigger_type,
|
||||||
|
raw_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
execution_result = await executor.execute_triggered_agent(
|
||||||
|
agent_id=trigger_config.agent_id,
|
||||||
|
trigger_result=result,
|
||||||
|
trigger_event=trigger_event
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"QStash agent execution result: {execution_result}")
|
||||||
|
return JSONResponse(content={
|
||||||
|
"message": "QStash webhook processed and agent execution started",
|
||||||
|
"trigger_id": trigger_id,
|
||||||
|
"agent_id": trigger_config.agent_id,
|
||||||
|
"thread_id": execution_result.get("thread_id"),
|
||||||
|
"agent_run_id": execution_result.get("agent_run_id")
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.response_data:
|
||||||
|
return JSONResponse(content=result.response_data)
|
||||||
|
elif result.success:
|
||||||
|
return {"message": "QStash webhook processed successfully"}
|
||||||
|
else:
|
||||||
|
logger.warning(f"QStash webhook processing failed for {trigger_id}: {result.error_message}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": result.error_message}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing QStash webhook: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": "Internal server error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/schedule/webhook")
|
||||||
|
async def handle_schedule_webhook(request: Request):
|
||||||
|
try:
|
||||||
|
logger.info("Schedule webhook received from Pipedream")
|
||||||
|
body = await request.body()
|
||||||
|
headers = dict(request.headers)
|
||||||
|
|
||||||
|
logger.debug(f"Schedule webhook body: {body[:500]}...")
|
||||||
|
logger.debug(f"Schedule webhook headers: {headers}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if body:
|
||||||
|
data = await request.json()
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to parse JSON body: {e}")
|
||||||
|
data = {
|
||||||
|
"raw_body": body.decode('utf-8', errors='ignore'),
|
||||||
|
"content_type": headers.get('content-type', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger_id = data.get('trigger_id')
|
||||||
|
agent_id = data.get('agent_id')
|
||||||
|
|
||||||
|
if not trigger_id:
|
||||||
|
logger.error("No trigger_id in schedule webhook payload")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "trigger_id is required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Processing scheduled trigger event for {trigger_id}")
|
||||||
|
manager = await get_trigger_manager()
|
||||||
|
|
||||||
|
trigger_config = await manager.get_trigger(trigger_id)
|
||||||
|
if trigger_config:
|
||||||
|
data['trigger_config'] = trigger_config.config
|
||||||
|
|
||||||
|
result = await manager.process_trigger_event(trigger_id, data)
|
||||||
|
|
||||||
|
logger.info(f"Schedule trigger processing result: success={result.success}, should_execute={result.should_execute_agent}, error={result.error_message}")
|
||||||
|
|
||||||
|
if result.success and result.should_execute_agent:
|
||||||
|
executor = AgentTriggerExecutor(db)
|
||||||
|
if trigger_config:
|
||||||
|
from .core import TriggerEvent, TriggerType
|
||||||
|
trigger_type = trigger_config.trigger_type
|
||||||
|
if isinstance(trigger_type, str):
|
||||||
|
trigger_type = TriggerType(trigger_type)
|
||||||
|
|
||||||
|
trigger_event = TriggerEvent(
|
||||||
|
trigger_id=trigger_id,
|
||||||
|
agent_id=trigger_config.agent_id,
|
||||||
|
trigger_type=trigger_type,
|
||||||
|
raw_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
execution_result = await executor.execute_triggered_agent(
|
||||||
|
agent_id=trigger_config.agent_id,
|
||||||
|
trigger_result=result,
|
||||||
|
trigger_event=trigger_event
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Scheduled agent execution result: {execution_result}")
|
||||||
|
return JSONResponse(content={
|
||||||
|
"message": "Schedule webhook processed and agent execution started",
|
||||||
|
"trigger_id": trigger_id,
|
||||||
|
"agent_id": trigger_config.agent_id,
|
||||||
|
"thread_id": execution_result.get("thread_id"),
|
||||||
|
"agent_run_id": execution_result.get("agent_run_id")
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.response_data:
|
||||||
|
return JSONResponse(content=result.response_data)
|
||||||
|
elif result.success:
|
||||||
|
return {"message": "Schedule webhook processed successfully"}
|
||||||
|
else:
|
||||||
|
logger.warning(f"Schedule webhook processing failed for {trigger_id}: {result.error_message}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": result.error_message}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing schedule webhook: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": "Internal server error"}
|
||||||
|
)
|
||||||
|
|
||||||
@router.post("/{trigger_id}/webhook")
|
@router.post("/{trigger_id}/webhook")
|
||||||
async def handle_webhook(
|
async def handle_webhook(
|
||||||
trigger_id: str,
|
trigger_id: str,
|
||||||
|
|
|
@ -291,6 +291,34 @@ class TriggerManager:
|
||||||
response_template={
|
response_template={
|
||||||
"agent_prompt": "GitHub {github_event} event in {github_repo} by {github_sender}"
|
"agent_prompt": "GitHub {github_event} event in {github_repo} by {github_sender}"
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
ProviderDefinition(
|
||||||
|
provider_id="schedule",
|
||||||
|
name="Schedule",
|
||||||
|
description="Schedule agent execution using Cloudflare Workers and cron expressions",
|
||||||
|
trigger_type="schedule",
|
||||||
|
provider_class="triggers.providers.schedule_provider.ScheduleTriggerProvider",
|
||||||
|
webhook_enabled=True,
|
||||||
|
config_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cron_expression": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Cron expression for scheduling",
|
||||||
|
"pattern": r"^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$"
|
||||||
|
},
|
||||||
|
"agent_prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The prompt to run the agent with when triggered"
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Timezone for schedule execution (default: UTC)",
|
||||||
|
"default": "UTC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cron_expression", "agent_prompt"]
|
||||||
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from .telegram_provider import TelegramTriggerProvider
|
from .telegram_provider import TelegramTriggerProvider
|
||||||
from .slack_provider import SlackTriggerProvider
|
from .slack_provider import SlackTriggerProvider
|
||||||
|
from .schedule_provider import ScheduleTriggerProvider
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'TelegramTriggerProvider',
|
'TelegramTriggerProvider',
|
||||||
'SlackTriggerProvider'
|
'SlackTriggerProvider',
|
||||||
|
'ScheduleTriggerProvider'
|
||||||
]
|
]
|
|
@ -0,0 +1,259 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from qstash.client import QStash
|
||||||
|
from utils.logger import logger
|
||||||
|
from ..core import TriggerProvider, TriggerType, TriggerEvent, TriggerResult, TriggerConfig, ProviderDefinition
|
||||||
|
|
||||||
|
class ScheduleTriggerProvider(TriggerProvider):
|
||||||
|
"""Schedule trigger provider using Upstash QStash."""
|
||||||
|
|
||||||
|
def __init__(self, provider_definition: Optional[ProviderDefinition] = None):
|
||||||
|
super().__init__(TriggerType.SCHEDULE, provider_definition)
|
||||||
|
|
||||||
|
self.qstash_token = os.getenv("QSTASH_TOKEN")
|
||||||
|
self.webhook_base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
if not self.qstash_token:
|
||||||
|
logger.warning("QSTASH_TOKEN not found. QStash provider will not work without it.")
|
||||||
|
self.qstash = None
|
||||||
|
else:
|
||||||
|
self.qstash = QStash(token=self.qstash_token)
|
||||||
|
|
||||||
|
async def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validate schedule configuration."""
|
||||||
|
if not self.qstash:
|
||||||
|
raise ValueError("QSTASH_TOKEN environment variable is required for QStash scheduling")
|
||||||
|
|
||||||
|
if 'cron_expression' not in config:
|
||||||
|
raise ValueError("cron_expression is required for QStash schedule triggers")
|
||||||
|
|
||||||
|
if 'agent_prompt' not in config:
|
||||||
|
raise ValueError("agent_prompt is required for schedule triggers")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import croniter
|
||||||
|
croniter.croniter(config['cron_expression'])
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError("croniter package is required for cron expressions. Please install it with: pip install croniter")
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Invalid cron expression: {str(e)}")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
async def setup_trigger(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Set up scheduled trigger using QStash."""
|
||||||
|
try:
|
||||||
|
webhook_url = f"{self.webhook_base_url}/api/triggers/qstash/webhook"
|
||||||
|
webhook_payload = {
|
||||||
|
"trigger_id": trigger_config.trigger_id,
|
||||||
|
"agent_id": trigger_config.agent_id,
|
||||||
|
"agent_prompt": trigger_config.config['agent_prompt'],
|
||||||
|
"schedule_name": trigger_config.name,
|
||||||
|
"cron_expression": trigger_config.config['cron_expression'],
|
||||||
|
"event_type": "scheduled",
|
||||||
|
"provider": "qstash"
|
||||||
|
}
|
||||||
|
schedule_id = await asyncio.to_thread(
|
||||||
|
self.qstash.schedule.create,
|
||||||
|
destination=webhook_url,
|
||||||
|
cron=trigger_config.config['cron_expression'],
|
||||||
|
body=json.dumps(webhook_payload),
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Schedule-Provider": "qstash",
|
||||||
|
"X-Trigger-ID": trigger_config.trigger_id,
|
||||||
|
"X-Agent-ID": trigger_config.agent_id
|
||||||
|
},
|
||||||
|
retries=3,
|
||||||
|
delay="5s"
|
||||||
|
)
|
||||||
|
trigger_config.config['qstash_schedule_id'] = schedule_id
|
||||||
|
logger.info(f"Successfully created QStash schedule {schedule_id} for trigger {trigger_config.trigger_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting up QStash scheduled trigger {trigger_config.trigger_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def teardown_trigger(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Remove scheduled trigger from QStash."""
|
||||||
|
try:
|
||||||
|
schedule_id = trigger_config.config.get('qstash_schedule_id')
|
||||||
|
if not schedule_id:
|
||||||
|
logger.warning(f"No QStash schedule ID found for trigger {trigger_config.trigger_id}")
|
||||||
|
return True
|
||||||
|
await asyncio.to_thread(
|
||||||
|
self.qstash.schedule.delete,
|
||||||
|
schedule_id=schedule_id
|
||||||
|
)
|
||||||
|
logger.info(f"Successfully deleted QStash schedule {schedule_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing QStash scheduled trigger {trigger_config.trigger_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def process_event(self, event: TriggerEvent) -> TriggerResult:
|
||||||
|
"""Process scheduled trigger event from QStash."""
|
||||||
|
try:
|
||||||
|
raw_data = event.raw_data
|
||||||
|
agent_prompt = raw_data.get('agent_prompt', 'Execute scheduled task')
|
||||||
|
execution_variables = {
|
||||||
|
'scheduled_at': event.timestamp.isoformat(),
|
||||||
|
'trigger_id': event.trigger_id,
|
||||||
|
'agent_id': event.agent_id,
|
||||||
|
'schedule_name': raw_data.get('schedule_name', 'Scheduled Task'),
|
||||||
|
'execution_source': 'qstash',
|
||||||
|
'cron_expression': raw_data.get('cron_expression'),
|
||||||
|
'qstash_message_id': raw_data.get('messageId')
|
||||||
|
}
|
||||||
|
return TriggerResult(
|
||||||
|
success=True,
|
||||||
|
should_execute_agent=True,
|
||||||
|
agent_prompt=agent_prompt,
|
||||||
|
execution_variables=execution_variables
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return TriggerResult(
|
||||||
|
success=False,
|
||||||
|
error_message=f"Error processing QStash scheduled trigger event: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def health_check(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Check if the QStash scheduled trigger is healthy."""
|
||||||
|
try:
|
||||||
|
schedule_id = trigger_config.config.get('qstash_schedule_id')
|
||||||
|
if not schedule_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
schedule = await asyncio.to_thread(
|
||||||
|
self.qstash.schedule.get,
|
||||||
|
schedule_id=schedule_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return getattr(schedule, 'is_active', False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health check failed for QStash scheduled trigger {trigger_config.trigger_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def pause_trigger(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Pause a QStash schedule."""
|
||||||
|
try:
|
||||||
|
schedule_id = trigger_config.config.get('qstash_schedule_id')
|
||||||
|
if not schedule_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await asyncio.to_thread(
|
||||||
|
self.qstash.schedules.pause,
|
||||||
|
schedule_id=schedule_id
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Successfully paused QStash schedule {schedule_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error pausing QStash schedule: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def resume_trigger(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Resume a QStash schedule."""
|
||||||
|
try:
|
||||||
|
schedule_id = trigger_config.config.get('qstash_schedule_id')
|
||||||
|
if not schedule_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await asyncio.to_thread(
|
||||||
|
self.qstash.schedules.resume,
|
||||||
|
schedule_id=schedule_id
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Successfully resumed QStash schedule {schedule_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error resuming QStash schedule: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def update_trigger(self, trigger_config: TriggerConfig) -> bool:
|
||||||
|
"""Update a QStash schedule by recreating it."""
|
||||||
|
try:
|
||||||
|
schedule_id = trigger_config.config.get('qstash_schedule_id')
|
||||||
|
webhook_url = f"{self.webhook_base_url}/api/triggers/qstash/webhook"
|
||||||
|
|
||||||
|
webhook_payload = {
|
||||||
|
"trigger_id": trigger_config.trigger_id,
|
||||||
|
"agent_id": trigger_config.agent_id,
|
||||||
|
"agent_prompt": trigger_config.config['agent_prompt'],
|
||||||
|
"schedule_name": trigger_config.name,
|
||||||
|
"cron_expression": trigger_config.config['cron_expression'],
|
||||||
|
"event_type": "scheduled",
|
||||||
|
"provider": "qstash"
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.qstash_base_url}/schedules",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.qstash_token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"scheduleId": schedule_id,
|
||||||
|
"destination": webhook_url,
|
||||||
|
"cron": trigger_config.config['cron_expression'],
|
||||||
|
"body": webhook_payload,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Schedule-Provider": "qstash",
|
||||||
|
"X-Trigger-ID": trigger_config.trigger_id,
|
||||||
|
"X-Agent-ID": trigger_config.agent_id
|
||||||
|
},
|
||||||
|
"retries": 3,
|
||||||
|
"delay": "5s"
|
||||||
|
},
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info(f"Successfully updated QStash schedule {schedule_id}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to update QStash schedule: {response.status_code} - {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating QStash schedule: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_webhook_url(self, trigger_id: str, base_url: str) -> Optional[str]:
|
||||||
|
"""Return webhook URL for QStash schedules."""
|
||||||
|
return f"{base_url}/api/triggers/qstash/webhook"
|
||||||
|
|
||||||
|
async def list_schedules(self) -> list:
|
||||||
|
"""List all QStash schedules."""
|
||||||
|
try:
|
||||||
|
schedules_data = await asyncio.to_thread(
|
||||||
|
self.qstash.schedules.list
|
||||||
|
)
|
||||||
|
|
||||||
|
schedules = []
|
||||||
|
for schedule in schedules_data:
|
||||||
|
schedules.append({
|
||||||
|
'id': getattr(schedule, 'schedule_id', None),
|
||||||
|
'destination': getattr(schedule, 'destination', None),
|
||||||
|
'cron': getattr(schedule, 'cron', None),
|
||||||
|
'is_active': getattr(schedule, 'is_active', False),
|
||||||
|
'created_at': getattr(schedule, 'created_at', None),
|
||||||
|
'next_delivery': getattr(schedule, 'next_delivery', None)
|
||||||
|
})
|
||||||
|
|
||||||
|
return schedules
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing QStash schedules: {e}")
|
||||||
|
return []
|
|
@ -34,7 +34,6 @@ export default function AgentConfigurationPage() {
|
||||||
const updateAgentMutation = useUpdateAgent();
|
const updateAgentMutation = useUpdateAgent();
|
||||||
const { state, setOpen, setOpenMobile } = useSidebar();
|
const { state, setOpen, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
// Ref to track if initial layout has been applied (for sidebar closing)
|
|
||||||
const initialLayoutAppliedRef = useRef(false);
|
const initialLayoutAppliedRef = useRef(false);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
@ -395,6 +394,7 @@ export default function AgentConfigurationPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="h-4 w-4" />
|
<Zap className="h-4 w-4" />
|
||||||
Triggers
|
Triggers
|
||||||
|
<Badge variant='new'>New</Badge>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||||
|
|
|
@ -89,7 +89,6 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
||||||
toast.error(error.message || 'Failed to save trigger');
|
toast.error(error.message || 'Failed to save trigger');
|
||||||
console.error('Error saving trigger:', error);
|
console.error('Error saving trigger:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfiguringProvider(null);
|
setConfiguringProvider(null);
|
||||||
setEditingTrigger(null);
|
setEditingTrigger(null);
|
||||||
};
|
};
|
||||||
|
@ -107,7 +106,6 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get available providers that can be directly configured
|
|
||||||
const availableProviders = providers.filter(provider =>
|
const availableProviders = providers.filter(provider =>
|
||||||
['telegram', 'slack', 'webhook'].includes(provider.trigger_type)
|
['telegram', 'slack', 'webhook'].includes(provider.trigger_type)
|
||||||
);
|
);
|
||||||
|
@ -136,7 +134,7 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
||||||
{/* <div>
|
{/* <div>
|
||||||
<h4 className="text-sm font-medium text-foreground mb-3">Manual Configuration</h4>
|
<h4 className="text-sm font-medium text-foreground mb-3">Manual Configuration</h4>
|
||||||
<p className="text-xs text-muted-foreground mb-4">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
For advanced users who want to configure triggers manually with custom settings.
|
Configure triggers manually with custom settings for advanced use cases.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
{availableProviders.map((provider) => (
|
{availableProviders.map((provider) => (
|
||||||
|
@ -173,7 +171,6 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{!isLoading && triggers.length === 0 && (
|
{!isLoading && triggers.length === 0 && (
|
||||||
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
|
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
|
||||||
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4">
|
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4">
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader2, ExternalLink, AlertCircle } from 'lucide-react';
|
import { Loader2, ExternalLink, AlertCircle, Clock } from 'lucide-react';
|
||||||
import { SlackIcon } from '@/components/ui/icons/slack';
|
import { SlackIcon } from '@/components/ui/icons/slack';
|
||||||
import { getTriggerIcon } from './utils';
|
import { getTriggerIcon } from './utils';
|
||||||
|
import { TriggerConfigDialog } from './trigger-config-dialog';
|
||||||
|
import { TriggerProvider, ScheduleTriggerConfig } from './types';
|
||||||
|
import { Dialog } from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
useOAuthIntegrations,
|
useOAuthIntegrations,
|
||||||
useInstallOAuthIntegration,
|
useInstallOAuthIntegration,
|
||||||
useUninstallOAuthIntegration,
|
useUninstallOAuthIntegration,
|
||||||
useOAuthCallbackHandler
|
useOAuthCallbackHandler
|
||||||
} from '@/hooks/react-query/triggers/use-oauth-integrations';
|
} from '@/hooks/react-query/triggers/use-oauth-integrations';
|
||||||
|
import {
|
||||||
|
useAgentTriggers,
|
||||||
|
useCreateTrigger,
|
||||||
|
useDeleteTrigger
|
||||||
|
} from '@/hooks/react-query/triggers';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface OneClickIntegrationsProps {
|
interface OneClickIntegrationsProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
@ -19,15 +28,13 @@ interface OneClickIntegrationsProps {
|
||||||
const OAUTH_PROVIDERS = {
|
const OAUTH_PROVIDERS = {
|
||||||
slack: {
|
slack: {
|
||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
icon: <SlackIcon className="h-4 w-4" />
|
icon: <SlackIcon className="h-4 w-4" />,
|
||||||
|
isOAuth: true
|
||||||
},
|
},
|
||||||
discord: {
|
schedule: {
|
||||||
name: 'Discord',
|
name: 'Schedule',
|
||||||
icon: <span className="">{getTriggerIcon('discord')}</span>
|
icon: <Clock className="h-4 w-4" color="#10b981" />,
|
||||||
},
|
isOAuth: false
|
||||||
teams: {
|
|
||||||
name: 'Microsoft Teams',
|
|
||||||
icon: <span className="">{getTriggerIcon('teams')}</span>
|
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -36,9 +43,14 @@ type ProviderKey = keyof typeof OAUTH_PROVIDERS;
|
||||||
export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||||
agentId
|
agentId
|
||||||
}) => {
|
}) => {
|
||||||
|
const [configuringSchedule, setConfiguringSchedule] = useState(false);
|
||||||
|
|
||||||
const { data: integrationStatus, isLoading, error } = useOAuthIntegrations(agentId);
|
const { data: integrationStatus, isLoading, error } = useOAuthIntegrations(agentId);
|
||||||
|
const { data: triggers = [] } = useAgentTriggers(agentId);
|
||||||
const installMutation = useInstallOAuthIntegration();
|
const installMutation = useInstallOAuthIntegration();
|
||||||
const uninstallMutation = useUninstallOAuthIntegration();
|
const uninstallMutation = useUninstallOAuthIntegration();
|
||||||
|
const createTriggerMutation = useCreateTrigger();
|
||||||
|
const deleteTriggerMutation = useDeleteTrigger();
|
||||||
const { handleCallback } = useOAuthCallbackHandler();
|
const { handleCallback } = useOAuthCallbackHandler();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -46,6 +58,11 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInstall = async (provider: ProviderKey) => {
|
const handleInstall = async (provider: ProviderKey) => {
|
||||||
|
if (provider === 'schedule') {
|
||||||
|
setConfiguringSchedule(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await installMutation.mutateAsync({
|
await installMutation.mutateAsync({
|
||||||
agent_id: agentId,
|
agent_id: agentId,
|
||||||
|
@ -56,15 +73,46 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUninstall = async (triggerId: string) => {
|
const handleUninstall = async (provider: ProviderKey, triggerId?: string) => {
|
||||||
|
if (provider === 'schedule' && triggerId) {
|
||||||
|
try {
|
||||||
|
await deleteTriggerMutation.mutateAsync(triggerId);
|
||||||
|
toast.success('Schedule trigger removed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to remove schedule trigger');
|
||||||
|
console.error('Error removing schedule trigger:', error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await uninstallMutation.mutateAsync(triggerId);
|
await uninstallMutation.mutateAsync(triggerId!);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uninstalling integration:', error);
|
console.error('Error uninstalling integration:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScheduleSave = async (config: any) => {
|
||||||
|
try {
|
||||||
|
await createTriggerMutation.mutateAsync({
|
||||||
|
agentId,
|
||||||
|
provider_id: 'schedule',
|
||||||
|
name: config.name || 'Scheduled Trigger',
|
||||||
|
description: config.description || 'Automatically scheduled trigger',
|
||||||
|
config: config.config,
|
||||||
|
});
|
||||||
|
toast.success('Schedule trigger created successfully');
|
||||||
|
setConfiguringSchedule(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Failed to create schedule trigger');
|
||||||
|
console.error('Error creating schedule trigger:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getIntegrationForProvider = (provider: ProviderKey) => {
|
const getIntegrationForProvider = (provider: ProviderKey) => {
|
||||||
|
if (provider === 'schedule') {
|
||||||
|
return triggers.find(trigger => trigger.trigger_type === 'schedule');
|
||||||
|
}
|
||||||
return integrationStatus?.integrations.find(integration =>
|
return integrationStatus?.integrations.find(integration =>
|
||||||
integration.provider === provider
|
integration.provider === provider
|
||||||
);
|
);
|
||||||
|
@ -74,6 +122,14 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||||
return !!getIntegrationForProvider(provider);
|
return !!getIntegrationForProvider(provider);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTriggerId = (provider: ProviderKey) => {
|
||||||
|
const integration = getIntegrationForProvider(provider);
|
||||||
|
if (provider === 'schedule') {
|
||||||
|
return integration?.trigger_id;
|
||||||
|
}
|
||||||
|
return integration?.trigger_id;
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 border border-destructive/20 bg-destructive/5 rounded-lg">
|
<div className="p-4 border border-destructive/20 bg-destructive/5 rounded-lg">
|
||||||
|
@ -90,30 +146,68 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scheduleProvider: TriggerProvider = {
|
||||||
|
provider_id: 'schedule',
|
||||||
|
name: 'Schedule',
|
||||||
|
description: 'Schedule agent execution using cron expressions',
|
||||||
|
trigger_type: 'schedule',
|
||||||
|
webhook_enabled: true,
|
||||||
|
config_schema: {}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="space-y-4">
|
||||||
{Object.entries(OAUTH_PROVIDERS).map(([providerId, config]) => {
|
<div className="flex flex-wrap gap-3">
|
||||||
const provider = providerId as ProviderKey;
|
{Object.entries(OAUTH_PROVIDERS).map(([providerId, config]) => {
|
||||||
const integration = getIntegrationForProvider(provider);
|
const provider = providerId as ProviderKey;
|
||||||
const isInstalled = isProviderInstalled(provider);
|
const integration = getIntegrationForProvider(provider);
|
||||||
const isLoading = installMutation.isPending || uninstallMutation.isPending;
|
const isInstalled = isProviderInstalled(provider);
|
||||||
return (
|
const isLoading = installMutation.isPending || uninstallMutation.isPending ||
|
||||||
<Button
|
(provider === 'schedule' && (createTriggerMutation.isPending || deleteTriggerMutation.isPending));
|
||||||
key={providerId}
|
const triggerId = getTriggerId(provider);
|
||||||
variant="outline"
|
|
||||||
onClick={() => isInstalled ? handleUninstall(integration!.trigger_id) : handleInstall(provider)}
|
const buttonText = provider === 'schedule'
|
||||||
disabled={isLoading}
|
? config.name
|
||||||
className="flex items-center"
|
: (isInstalled ? `Disconnect ${config.name}` : `Connect ${config.name}`);
|
||||||
>
|
|
||||||
{isLoading ? (
|
return (
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Button
|
||||||
) : (
|
key={providerId}
|
||||||
config.icon
|
variant="outline"
|
||||||
)}
|
size='sm'
|
||||||
{isInstalled ? `Disconnect ${config.name}` : `Connect to ${config.name}`}
|
onClick={() => {
|
||||||
</Button>
|
if (provider === 'schedule') {
|
||||||
);
|
handleInstall(provider);
|
||||||
})}
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
isInstalled ? handleUninstall(provider, triggerId) : handleInstall(provider);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
config.icon
|
||||||
|
)}
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{configuringSchedule && (
|
||||||
|
<Dialog open={configuringSchedule} onOpenChange={setConfiguringSchedule}>
|
||||||
|
<TriggerConfigDialog
|
||||||
|
provider={scheduleProvider}
|
||||||
|
existingConfig={null}
|
||||||
|
onSave={handleScheduleSave}
|
||||||
|
onCancel={() => setConfiguringSchedule(false)}
|
||||||
|
isLoading={createTriggerMutation.isPending}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -0,0 +1,520 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Clock, Calendar as CalendarIcon, Info, Zap, Repeat, Timer, Target } from 'lucide-react';
|
||||||
|
import { format, startOfDay } from 'date-fns';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { TriggerProvider, ScheduleTriggerConfig } from '../types';
|
||||||
|
|
||||||
|
interface ScheduleTriggerConfigFormProps {
|
||||||
|
provider: TriggerProvider;
|
||||||
|
config: ScheduleTriggerConfig;
|
||||||
|
onChange: (config: ScheduleTriggerConfig) => void;
|
||||||
|
errors: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScheduleType = 'quick' | 'recurring' | 'advanced' | 'one-time';
|
||||||
|
|
||||||
|
interface QuickPreset {
|
||||||
|
name: string;
|
||||||
|
cron: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
category: 'frequent' | 'daily' | 'weekly' | 'monthly';
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUICK_PRESETS: QuickPreset[] = [
|
||||||
|
{ name: 'Every minute', cron: '* * * * *', description: 'Every minute', icon: <Zap className="h-4 w-4" />, category: 'frequent' },
|
||||||
|
{ name: 'Every 5 minutes', cron: '*/5 * * * *', description: 'Every 5 minutes', icon: <Timer className="h-4 w-4" />, category: 'frequent' },
|
||||||
|
{ name: 'Every 15 minutes', cron: '*/15 * * * *', description: 'Every 15 minutes', icon: <Timer className="h-4 w-4" />, category: 'frequent' },
|
||||||
|
{ name: 'Every 30 minutes', cron: '*/30 * * * *', description: 'Every 30 minutes', icon: <Timer className="h-4 w-4" />, category: 'frequent' },
|
||||||
|
{ name: 'Every hour', cron: '0 * * * *', description: 'At the start of every hour', icon: <Clock className="h-4 w-4" />, category: 'frequent' },
|
||||||
|
|
||||||
|
{ name: 'Daily at 9 AM', cron: '0 9 * * *', description: 'Every day at 9:00 AM', icon: <Target className="h-4 w-4" />, category: 'daily' },
|
||||||
|
{ name: 'Daily at 12 PM', cron: '0 12 * * *', description: 'Every day at 12:00 PM', icon: <Target className="h-4 w-4" />, category: 'daily' },
|
||||||
|
{ name: 'Daily at 6 PM', cron: '0 18 * * *', description: 'Every day at 6:00 PM', icon: <Target className="h-4 w-4" />, category: 'daily' },
|
||||||
|
{ name: 'Twice daily', cron: '0 9,17 * * *', description: 'Every day at 9 AM and 5 PM', icon: <Repeat className="h-4 w-4" />, category: 'daily' },
|
||||||
|
|
||||||
|
{ name: 'Weekdays at 9 AM', cron: '0 9 * * 1-5', description: 'Monday-Friday at 9:00 AM', icon: <Target className="h-4 w-4" />, category: 'weekly' },
|
||||||
|
{ name: 'Monday mornings', cron: '0 9 * * 1', description: 'Every Monday at 9:00 AM', icon: <CalendarIcon className="h-4 w-4" />, category: 'weekly' },
|
||||||
|
{ name: 'Friday evenings', cron: '0 17 * * 5', description: 'Every Friday at 5:00 PM', icon: <CalendarIcon className="h-4 w-4" />, category: 'weekly' },
|
||||||
|
{ name: 'Weekend mornings', cron: '0 10 * * 0,6', description: 'Saturday & Sunday at 10:00 AM', icon: <CalendarIcon className="h-4 w-4" />, category: 'weekly' },
|
||||||
|
|
||||||
|
{ name: 'Monthly on 1st', cron: '0 9 1 * *', description: 'First day of month at 9:00 AM', icon: <CalendarIcon className="h-4 w-4" />, category: 'monthly' },
|
||||||
|
{ name: 'Monthly on 15th', cron: '0 9 15 * *', description: '15th of month at 9:00 AM', icon: <CalendarIcon className="h-4 w-4" />, category: 'monthly' },
|
||||||
|
{ name: 'End of month', cron: '0 9 28-31 * *', description: 'Last few days of month at 9:00 AM', icon: <CalendarIcon className="h-4 w-4" />, category: 'monthly' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TIMEZONES = [
|
||||||
|
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
||||||
|
{ value: 'America/New_York', label: 'Eastern Time (ET)' },
|
||||||
|
{ value: 'America/Chicago', label: 'Central Time (CT)' },
|
||||||
|
{ value: 'America/Denver', label: 'Mountain Time (MT)' },
|
||||||
|
{ value: 'America/Los_Angeles', label: 'Pacific Time (PT)' },
|
||||||
|
{ value: 'Europe/London', label: 'Greenwich Mean Time (GMT)' },
|
||||||
|
{ value: 'Europe/Paris', label: 'Central European Time (CET)' },
|
||||||
|
{ value: 'Europe/Berlin', label: 'Central European Time (CET)' },
|
||||||
|
{ value: 'Asia/Tokyo', label: 'Japan Standard Time (JST)' },
|
||||||
|
{ value: 'Asia/Shanghai', label: 'China Standard Time (CST)' },
|
||||||
|
{ value: 'Australia/Sydney', label: 'Australian Eastern Time (AET)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const WEEKDAYS = [
|
||||||
|
{ value: '1', label: 'Monday', short: 'Mon' },
|
||||||
|
{ value: '2', label: 'Tuesday', short: 'Tue' },
|
||||||
|
{ value: '3', label: 'Wednesday', short: 'Wed' },
|
||||||
|
{ value: '4', label: 'Thursday', short: 'Thu' },
|
||||||
|
{ value: '5', label: 'Friday', short: 'Fri' },
|
||||||
|
{ value: '6', label: 'Saturday', short: 'Sat' },
|
||||||
|
{ value: '0', label: 'Sunday', short: 'Sun' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MONTHS = [
|
||||||
|
{ value: '1', label: 'January' },
|
||||||
|
{ value: '2', label: 'February' },
|
||||||
|
{ value: '3', label: 'March' },
|
||||||
|
{ value: '4', label: 'April' },
|
||||||
|
{ value: '5', label: 'May' },
|
||||||
|
{ value: '6', label: 'June' },
|
||||||
|
{ value: '7', label: 'July' },
|
||||||
|
{ value: '8', label: 'August' },
|
||||||
|
{ value: '9', label: 'September' },
|
||||||
|
{ value: '10', label: 'October' },
|
||||||
|
{ value: '11', label: 'November' },
|
||||||
|
{ value: '12', label: 'December' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ScheduleTriggerConfigForm: React.FC<ScheduleTriggerConfigFormProps> = ({
|
||||||
|
provider,
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
errors,
|
||||||
|
}) => {
|
||||||
|
const [scheduleType, setScheduleType] = useState<ScheduleType>('quick');
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<string>('');
|
||||||
|
|
||||||
|
const [recurringType, setRecurringType] = useState<'daily' | 'weekly' | 'monthly'>('daily');
|
||||||
|
const [selectedWeekdays, setSelectedWeekdays] = useState<string[]>(['1', '2', '3', '4', '5']);
|
||||||
|
const [selectedMonths, setSelectedMonths] = useState<string[]>(['*']);
|
||||||
|
const [dayOfMonth, setDayOfMonth] = useState<string>('1');
|
||||||
|
const [scheduleTime, setScheduleTime] = useState<{ hour: string; minute: string }>({ hour: '09', minute: '00' });
|
||||||
|
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date>();
|
||||||
|
const [oneTimeTime, setOneTimeTime] = useState<{ hour: string; minute: string }>({ hour: '09', minute: '00' });
|
||||||
|
|
||||||
|
const generateCronExpression = () => {
|
||||||
|
if (scheduleType === 'quick' && selectedPreset) {
|
||||||
|
return selectedPreset;
|
||||||
|
}
|
||||||
|
if (scheduleType === 'recurring') {
|
||||||
|
const { hour, minute } = scheduleTime;
|
||||||
|
switch (recurringType) {
|
||||||
|
case 'daily':
|
||||||
|
return `${minute} ${hour} * * *`;
|
||||||
|
case 'weekly':
|
||||||
|
const weekdayStr = selectedWeekdays.join(',');
|
||||||
|
return `${minute} ${hour} * * ${weekdayStr}`;
|
||||||
|
case 'monthly':
|
||||||
|
const monthStr = selectedMonths.includes('*') ? '*' : selectedMonths.join(',');
|
||||||
|
return `${minute} ${hour} ${dayOfMonth} ${monthStr} *`;
|
||||||
|
default:
|
||||||
|
return `${minute} ${hour} * * *`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scheduleType === 'one-time' && selectedDate) {
|
||||||
|
const { hour, minute } = oneTimeTime;
|
||||||
|
const day = selectedDate.getDate();
|
||||||
|
const month = selectedDate.getMonth() + 1;
|
||||||
|
const year = selectedDate.getFullYear();
|
||||||
|
return `${minute} ${hour} ${day} ${month} *`;
|
||||||
|
}
|
||||||
|
return config.cron_expression || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newCron = generateCronExpression();
|
||||||
|
if (newCron && newCron !== config.cron_expression) {
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
cron_expression: newCron,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [scheduleType, selectedPreset, recurringType, selectedWeekdays, selectedMonths, dayOfMonth, scheduleTime, selectedDate, oneTimeTime]);
|
||||||
|
|
||||||
|
const handlePresetSelect = (preset: QuickPreset) => {
|
||||||
|
setSelectedPreset(preset.cron);
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
cron_expression: preset.cron,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentPromptChange = (value: string) => {
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
agent_prompt: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimezoneChange = (value: string) => {
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
timezone: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWeekdayToggle = (weekday: string) => {
|
||||||
|
setSelectedWeekdays(prev =>
|
||||||
|
prev.includes(weekday)
|
||||||
|
? prev.filter(w => w !== weekday)
|
||||||
|
: [...prev, weekday].sort()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthToggle = (month: string) => {
|
||||||
|
if (month === '*') {
|
||||||
|
setSelectedMonths(['*']);
|
||||||
|
} else {
|
||||||
|
setSelectedMonths(prev => {
|
||||||
|
const filtered = prev.filter(m => m !== '*');
|
||||||
|
return filtered.includes(month)
|
||||||
|
? filtered.filter(m => m !== month)
|
||||||
|
: [...filtered, month].sort((a, b) => parseInt(a) - parseInt(b));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedPresets = QUICK_PRESETS.reduce((acc, preset) => {
|
||||||
|
if (!acc[preset.category]) acc[preset.category] = [];
|
||||||
|
acc[preset.category].push(preset);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, QuickPreset[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="border-none bg-transparent shadow-none p-0">
|
||||||
|
<CardHeader className='p-0 -mt-2'>
|
||||||
|
<CardDescription>
|
||||||
|
Configure when your agent should be triggered automatically. Choose from quick presets, recurring schedules, or set up advanced cron expressions.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Tabs value={scheduleType} onValueChange={(value) => setScheduleType(value as ScheduleType)} className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="quick" className="flex items-center gap-2">
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
Quick
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="recurring" className="flex items-center gap-2">
|
||||||
|
<Repeat className="h-4 w-4" />
|
||||||
|
Recurring
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="one-time" className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
One-time
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced" className="flex items-center gap-2">
|
||||||
|
<Target className="h-4 w-4" />
|
||||||
|
Advanced
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="quick" className="space-y-4 mt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(groupedPresets).map(([category, presets]) => (
|
||||||
|
<div key={category}>
|
||||||
|
<h4 className="text-sm font-medium mb-3 capitalize">{category} Schedules</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{presets.map((preset) => (
|
||||||
|
<Card
|
||||||
|
key={preset.cron}
|
||||||
|
className={cn(
|
||||||
|
"p-0 cursor-pointer transition-colors hover:bg-accent",
|
||||||
|
selectedPreset === preset.cron && "ring-2 ring-primary bg-accent"
|
||||||
|
)}
|
||||||
|
onClick={() => handlePresetSelect(preset)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-primary">{preset.icon}</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-sm">{preset.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{preset.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="recurring" className="space-y-6 mt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Schedule Type</Label>
|
||||||
|
<RadioGroup value={recurringType} onValueChange={(value) => setRecurringType(value as any)}>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="daily" id="daily" />
|
||||||
|
<Label htmlFor="daily">Daily</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="weekly" id="weekly" />
|
||||||
|
<Label htmlFor="weekly">Weekly</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="monthly" id="monthly" />
|
||||||
|
<Label htmlFor="monthly">Monthly</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recurringType === 'weekly' && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Days of Week</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{WEEKDAYS.map((day) => (
|
||||||
|
<Button
|
||||||
|
key={day.value}
|
||||||
|
variant={selectedWeekdays.includes(day.value) ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleWeekdayToggle(day.value)}
|
||||||
|
>
|
||||||
|
{day.short}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recurringType === 'monthly' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Day of Month</Label>
|
||||||
|
<Select value={dayOfMonth} onValueChange={setDayOfMonth}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 31 }, (_, i) => (
|
||||||
|
<SelectItem key={i + 1} value={(i + 1).toString()}>
|
||||||
|
{i + 1}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Months</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant={selectedMonths.includes('*') ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMonthToggle('*')}
|
||||||
|
>
|
||||||
|
All Months
|
||||||
|
</Button>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{MONTHS.map((month) => (
|
||||||
|
<Button
|
||||||
|
key={month.value}
|
||||||
|
variant={selectedMonths.includes(month.value) ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMonthToggle(month.value)}
|
||||||
|
disabled={selectedMonths.includes('*')}
|
||||||
|
>
|
||||||
|
{month.label.slice(0, 3)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Time</Label>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Select value={scheduleTime.hour} onValueChange={(value) => setScheduleTime(prev => ({ ...prev, hour: value }))}>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => (
|
||||||
|
<SelectItem key={i} value={i.toString().padStart(2, '0')}>
|
||||||
|
{i.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span>:</span>
|
||||||
|
<Select value={scheduleTime.minute} onValueChange={(value) => setScheduleTime(prev => ({ ...prev, minute: value }))}>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 60 }, (_, i) => (
|
||||||
|
<SelectItem key={i} value={i.toString().padStart(2, '0')}>
|
||||||
|
{i.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="one-time" className="space-y-6 mt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Date</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-left font-normal",
|
||||||
|
!selectedDate && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
{selectedDate ? format(selectedDate, "PPP") : "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={setSelectedDate}
|
||||||
|
disabled={(date) => date < startOfDay(new Date())}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium mb-3 block">Time</Label>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Select value={oneTimeTime.hour} onValueChange={(value) => setOneTimeTime(prev => ({ ...prev, hour: value }))}>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => (
|
||||||
|
<SelectItem key={i} value={i.toString().padStart(2, '0')}>
|
||||||
|
{i.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span>:</span>
|
||||||
|
<Select value={oneTimeTime.minute} onValueChange={(value) => setOneTimeTime(prev => ({ ...prev, minute: value }))}>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 60 }, (_, i) => (
|
||||||
|
<SelectItem key={i} value={i.toString().padStart(2, '0')}>
|
||||||
|
{i.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="advanced" className="space-y-4 mt-6">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="cron_expression" className="text-sm font-medium">
|
||||||
|
Cron Expression *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="cron_expression"
|
||||||
|
type="text"
|
||||||
|
value={config.cron_expression || ''}
|
||||||
|
onChange={(e) => onChange({ ...config, cron_expression: e.target.value })}
|
||||||
|
placeholder="0 9 * * 1-5"
|
||||||
|
className={errors.cron_expression ? 'border-destructive' : ''}
|
||||||
|
/>
|
||||||
|
{errors.cron_expression && (
|
||||||
|
<p className="text-xs text-destructive mt-1">{errors.cron_expression}</p>
|
||||||
|
)}
|
||||||
|
<Card className="mt-3 p-0 py-4">
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Info className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Cron Format</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
|
<div>Format: <code className="bg-muted px-1 rounded text-xs">minute hour day month weekday</code></div>
|
||||||
|
<div>Example: <code className="bg-muted px-1 rounded text-xs">0 9 * * 1-5</code> = Weekdays at 9 AM</div>
|
||||||
|
<div>Use <code className="bg-muted px-1 rounded text-xs">*</code> for any value, <code className="bg-muted px-1 rounded text-xs">*/5</code> for every 5 units</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-none bg-transparent shadow-none p-0">
|
||||||
|
<CardContent className="space-y-4 p-0">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="timezone" className="text-sm font-medium">
|
||||||
|
Timezone
|
||||||
|
</Label>
|
||||||
|
<Select value={config.timezone || 'UTC'} onValueChange={handleTimezoneChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select timezone" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIMEZONES.map((tz) => (
|
||||||
|
<SelectItem key={tz.value} value={tz.value}>
|
||||||
|
{tz.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="agent_prompt" className="text-sm font-medium">
|
||||||
|
Agent Prompt *
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="agent_prompt"
|
||||||
|
value={config.agent_prompt || ''}
|
||||||
|
onChange={(e) => handleAgentPromptChange(e.target.value)}
|
||||||
|
placeholder="Enter the prompt that will be sent to your agent when triggered..."
|
||||||
|
rows={4}
|
||||||
|
className={errors.agent_prompt ? 'border-destructive' : ''}
|
||||||
|
/>
|
||||||
|
{errors.agent_prompt && (
|
||||||
|
<p className="text-xs text-destructive mt-1">{errors.agent_prompt}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
This prompt will be sent to your agent each time the schedule triggers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -27,10 +27,11 @@ import {
|
||||||
Copy,
|
Copy,
|
||||||
ExternalLink
|
ExternalLink
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { TriggerProvider, TriggerConfiguration, TelegramTriggerConfig, SlackTriggerConfig } from './types';
|
import { TriggerProvider, TriggerConfiguration, TelegramTriggerConfig, SlackTriggerConfig, ScheduleTriggerConfig } from './types';
|
||||||
import { TelegramTriggerConfigForm } from './providers/telegram-config';
|
import { TelegramTriggerConfigForm } from './providers/telegram-config';
|
||||||
import { SlackTriggerConfigForm } from './providers/slack-config';
|
import { SlackTriggerConfigForm } from './providers/slack-config';
|
||||||
import { WebhookTriggerConfigForm } from './providers/webhook-config';
|
import { WebhookTriggerConfigForm } from './providers/webhook-config';
|
||||||
|
import { ScheduleTriggerConfigForm } from './providers/schedule-config';
|
||||||
import { getDialogIcon } from './utils';
|
import { getDialogIcon } from './utils';
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,27 +43,6 @@ interface TriggerConfigDialogProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTriggerIcon = (triggerType: string) => {
|
|
||||||
switch (triggerType) {
|
|
||||||
case 'telegram':
|
|
||||||
return <MessageSquare className="h-5 w-5" />;
|
|
||||||
case 'slack':
|
|
||||||
return <MessageSquare className="h-5 w-5" />;
|
|
||||||
case 'webhook':
|
|
||||||
return <Webhook className="h-5 w-5" />;
|
|
||||||
case 'schedule':
|
|
||||||
return <Clock className="h-5 w-5" />;
|
|
||||||
case 'email':
|
|
||||||
return <Mail className="h-5 w-5" />;
|
|
||||||
case 'github':
|
|
||||||
return <Github className="h-5 w-5" />;
|
|
||||||
case 'discord':
|
|
||||||
return <Gamepad2 className="h-5 w-5" />;
|
|
||||||
default:
|
|
||||||
return <Activity className="h-5 w-5" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
||||||
provider,
|
provider,
|
||||||
existingConfig,
|
existingConfig,
|
||||||
|
@ -95,6 +75,13 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
||||||
if (!config.signing_secret) {
|
if (!config.signing_secret) {
|
||||||
newErrors.signing_secret = 'Signing secret is required';
|
newErrors.signing_secret = 'Signing secret is required';
|
||||||
}
|
}
|
||||||
|
} else if (provider.provider_id === 'schedule') {
|
||||||
|
if (!config.cron_expression) {
|
||||||
|
newErrors.cron_expression = 'Cron expression is required';
|
||||||
|
}
|
||||||
|
if (!config.agent_prompt) {
|
||||||
|
newErrors.agent_prompt = 'Agent prompt is required';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
|
@ -129,6 +116,15 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
||||||
errors={errors}
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'schedule':
|
||||||
|
return (
|
||||||
|
<ScheduleTriggerConfigForm
|
||||||
|
provider={provider}
|
||||||
|
config={config as ScheduleTriggerConfig}
|
||||||
|
onChange={setConfig}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
case 'github_webhook':
|
case 'github_webhook':
|
||||||
case 'discord':
|
case 'discord':
|
||||||
|
@ -242,7 +238,6 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -60,3 +60,9 @@ export interface DiscordTriggerConfig {
|
||||||
allowed_channels?: string[];
|
allowed_channels?: string[];
|
||||||
trigger_keywords?: string[];
|
trigger_keywords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScheduleTriggerConfig {
|
||||||
|
cron_expression: string;
|
||||||
|
agent_prompt: string;
|
||||||
|
timezone?: string;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { FaTelegram } from "react-icons/fa";
|
import { FaTelegram } from "react-icons/fa";
|
||||||
import { SlackIcon } from "@/components/ui/icons/slack";
|
import { SlackIcon } from "@/components/ui/icons/slack";
|
||||||
import { Webhook } from "lucide-react";
|
import { Webhook, Clock } from "lucide-react";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
|
|
||||||
export const getTriggerIcon = (triggerType: string) => {
|
export const getTriggerIcon = (triggerType: string) => {
|
||||||
|
@ -11,6 +11,8 @@ export const getTriggerIcon = (triggerType: string) => {
|
||||||
return <SlackIcon className="h-5 w-5" />;
|
return <SlackIcon className="h-5 w-5" />;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
return <Webhook className="h-5 w-5" />;
|
return <Webhook className="h-5 w-5" />;
|
||||||
|
case 'schedule':
|
||||||
|
return <Clock className="h-5 w-5" color="#10b981" />;
|
||||||
default:
|
default:
|
||||||
return <Zap className="h-5 w-5" />;
|
return <Zap className="h-5 w-5" />;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +26,8 @@ export const getDialogIcon = (triggerType: string) => {
|
||||||
return <SlackIcon className="h-6 w-6" />;
|
return <SlackIcon className="h-6 w-6" />;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
return <Webhook className="h-6 w-6" />;
|
return <Webhook className="h-6 w-6" />;
|
||||||
|
case 'schedule':
|
||||||
|
return <Clock className="h-6 w-6" color="#10b981" />;
|
||||||
default:
|
default:
|
||||||
return <Zap className="h-5 w-5" />;
|
return <Zap className="h-5 w-5" />;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue