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"}
|
||||
)
|
||||
|
||||
@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")
|
||||
async def handle_webhook(
|
||||
trigger_id: str,
|
||||
|
|
|
@ -291,6 +291,34 @@ class TriggerManager:
|
|||
response_template={
|
||||
"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 .slack_provider import SlackTriggerProvider
|
||||
from .schedule_provider import ScheduleTriggerProvider
|
||||
|
||||
__all__ = [
|
||||
'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 { state, setOpen, setOpenMobile } = useSidebar();
|
||||
|
||||
// Ref to track if initial layout has been applied (for sidebar closing)
|
||||
const initialLayoutAppliedRef = useRef(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
|
@ -395,6 +394,7 @@ export default function AgentConfigurationPage() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4" />
|
||||
Triggers
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<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');
|
||||
console.error('Error saving trigger:', error);
|
||||
}
|
||||
|
||||
setConfiguringProvider(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 =>
|
||||
['telegram', 'slack', 'webhook'].includes(provider.trigger_type)
|
||||
);
|
||||
|
@ -136,7 +134,7 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
|||
{/* <div>
|
||||
<h4 className="text-sm font-medium text-foreground mb-3">Manual Configuration</h4>
|
||||
<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>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{availableProviders.map((provider) => (
|
||||
|
@ -173,7 +171,6 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!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="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4">
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { getTriggerIcon } from './utils';
|
||||
import { TriggerConfigDialog } from './trigger-config-dialog';
|
||||
import { TriggerProvider, ScheduleTriggerConfig } from './types';
|
||||
import { Dialog } from '@/components/ui/dialog';
|
||||
import {
|
||||
useOAuthIntegrations,
|
||||
useInstallOAuthIntegration,
|
||||
useUninstallOAuthIntegration,
|
||||
useOAuthCallbackHandler
|
||||
} from '@/hooks/react-query/triggers/use-oauth-integrations';
|
||||
import {
|
||||
useAgentTriggers,
|
||||
useCreateTrigger,
|
||||
useDeleteTrigger
|
||||
} from '@/hooks/react-query/triggers';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface OneClickIntegrationsProps {
|
||||
agentId: string;
|
||||
|
@ -19,15 +28,13 @@ interface OneClickIntegrationsProps {
|
|||
const OAUTH_PROVIDERS = {
|
||||
slack: {
|
||||
name: 'Slack',
|
||||
icon: <SlackIcon className="h-4 w-4" />
|
||||
icon: <SlackIcon className="h-4 w-4" />,
|
||||
isOAuth: true
|
||||
},
|
||||
discord: {
|
||||
name: 'Discord',
|
||||
icon: <span className="">{getTriggerIcon('discord')}</span>
|
||||
},
|
||||
teams: {
|
||||
name: 'Microsoft Teams',
|
||||
icon: <span className="">{getTriggerIcon('teams')}</span>
|
||||
schedule: {
|
||||
name: 'Schedule',
|
||||
icon: <Clock className="h-4 w-4" color="#10b981" />,
|
||||
isOAuth: false
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
@ -36,9 +43,14 @@ type ProviderKey = keyof typeof OAUTH_PROVIDERS;
|
|||
export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
||||
agentId
|
||||
}) => {
|
||||
const [configuringSchedule, setConfiguringSchedule] = useState(false);
|
||||
|
||||
const { data: integrationStatus, isLoading, error } = useOAuthIntegrations(agentId);
|
||||
const { data: triggers = [] } = useAgentTriggers(agentId);
|
||||
const installMutation = useInstallOAuthIntegration();
|
||||
const uninstallMutation = useUninstallOAuthIntegration();
|
||||
const createTriggerMutation = useCreateTrigger();
|
||||
const deleteTriggerMutation = useDeleteTrigger();
|
||||
const { handleCallback } = useOAuthCallbackHandler();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -46,6 +58,11 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
|||
}, []);
|
||||
|
||||
const handleInstall = async (provider: ProviderKey) => {
|
||||
if (provider === 'schedule') {
|
||||
setConfiguringSchedule(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await installMutation.mutateAsync({
|
||||
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 {
|
||||
await uninstallMutation.mutateAsync(triggerId);
|
||||
await uninstallMutation.mutateAsync(triggerId!);
|
||||
} catch (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) => {
|
||||
if (provider === 'schedule') {
|
||||
return triggers.find(trigger => trigger.trigger_type === 'schedule');
|
||||
}
|
||||
return integrationStatus?.integrations.find(integration =>
|
||||
integration.provider === provider
|
||||
);
|
||||
|
@ -74,6 +122,14 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
|||
return !!getIntegrationForProvider(provider);
|
||||
};
|
||||
|
||||
const getTriggerId = (provider: ProviderKey) => {
|
||||
const integration = getIntegrationForProvider(provider);
|
||||
if (provider === 'schedule') {
|
||||
return integration?.trigger_id;
|
||||
}
|
||||
return integration?.trigger_id;
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<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 (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Object.entries(OAUTH_PROVIDERS).map(([providerId, config]) => {
|
||||
const provider = providerId as ProviderKey;
|
||||
const integration = getIntegrationForProvider(provider);
|
||||
const isInstalled = isProviderInstalled(provider);
|
||||
const isLoading = installMutation.isPending || uninstallMutation.isPending;
|
||||
return (
|
||||
<Button
|
||||
key={providerId}
|
||||
variant="outline"
|
||||
onClick={() => isInstalled ? handleUninstall(integration!.trigger_id) : handleInstall(provider)}
|
||||
disabled={isLoading}
|
||||
className="flex items-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
config.icon
|
||||
)}
|
||||
{isInstalled ? `Disconnect ${config.name}` : `Connect to ${config.name}`}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Object.entries(OAUTH_PROVIDERS).map(([providerId, config]) => {
|
||||
const provider = providerId as ProviderKey;
|
||||
const integration = getIntegrationForProvider(provider);
|
||||
const isInstalled = isProviderInstalled(provider);
|
||||
const isLoading = installMutation.isPending || uninstallMutation.isPending ||
|
||||
(provider === 'schedule' && (createTriggerMutation.isPending || deleteTriggerMutation.isPending));
|
||||
const triggerId = getTriggerId(provider);
|
||||
|
||||
const buttonText = provider === 'schedule'
|
||||
? config.name
|
||||
: (isInstalled ? `Disconnect ${config.name}` : `Connect ${config.name}`);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={providerId}
|
||||
variant="outline"
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
ExternalLink
|
||||
} 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 { SlackTriggerConfigForm } from './providers/slack-config';
|
||||
import { WebhookTriggerConfigForm } from './providers/webhook-config';
|
||||
import { ScheduleTriggerConfigForm } from './providers/schedule-config';
|
||||
import { getDialogIcon } from './utils';
|
||||
|
||||
|
||||
|
@ -42,27 +43,6 @@ interface TriggerConfigDialogProps {
|
|||
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> = ({
|
||||
provider,
|
||||
existingConfig,
|
||||
|
@ -95,6 +75,13 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
|||
if (!config.signing_secret) {
|
||||
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);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
|
@ -129,6 +116,15 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
|||
errors={errors}
|
||||
/>
|
||||
);
|
||||
case 'schedule':
|
||||
return (
|
||||
<ScheduleTriggerConfigForm
|
||||
provider={provider}
|
||||
config={config as ScheduleTriggerConfig}
|
||||
onChange={setConfig}
|
||||
errors={errors}
|
||||
/>
|
||||
);
|
||||
case 'webhook':
|
||||
case 'github_webhook':
|
||||
case 'discord':
|
||||
|
@ -242,7 +238,6 @@ export const TriggerConfigDialog: React.FC<TriggerConfigDialogProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
||||
Cancel
|
||||
|
|
|
@ -60,3 +60,9 @@ export interface DiscordTriggerConfig {
|
|||
allowed_channels?: 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 { SlackIcon } from "@/components/ui/icons/slack";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { Webhook, Clock } from "lucide-react";
|
||||
import { Zap } from "lucide-react";
|
||||
|
||||
export const getTriggerIcon = (triggerType: string) => {
|
||||
|
@ -11,6 +11,8 @@ export const getTriggerIcon = (triggerType: string) => {
|
|||
return <SlackIcon className="h-5 w-5" />;
|
||||
case 'webhook':
|
||||
return <Webhook className="h-5 w-5" />;
|
||||
case 'schedule':
|
||||
return <Clock className="h-5 w-5" color="#10b981" />;
|
||||
default:
|
||||
return <Zap className="h-5 w-5" />;
|
||||
}
|
||||
|
@ -24,6 +26,8 @@ export const getDialogIcon = (triggerType: string) => {
|
|||
return <SlackIcon className="h-6 w-6" />;
|
||||
case 'webhook':
|
||||
return <Webhook className="h-6 w-6" />;
|
||||
case 'schedule':
|
||||
return <Clock className="h-6 w-6" color="#10b981" />;
|
||||
default:
|
||||
return <Zap className="h-5 w-5" />;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue