feat: agent schedules

This commit is contained in:
Soumyadas15 2025-07-01 13:35:55 +05:30
parent debbb1246f
commit ff47404a89
11 changed files with 1155 additions and 65 deletions

View File

@ -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,

View File

@ -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"]
}
) )
] ]

View File

@ -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'
] ]

View File

@ -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 []

View File

@ -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">

View File

@ -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">

View File

@ -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>
); );
}; };

View File

@ -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>
);
};

View File

@ -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

View File

@ -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;
}

View File

@ -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" />;
} }