tasks page

This commit is contained in:
Saumya 2025-08-29 14:01:28 +05:30
parent 5319a09476
commit f7dc975acf
16 changed files with 1240 additions and 17 deletions

View File

@ -686,7 +686,7 @@ class TriggerTool(AgentBuilderBaseTool):
provider_id="composio",
name=name or slug,
config=suna_config,
description=f"Composio event: {slug}"
description=f"{slug}"
)
except Exception as e:
logger.error(f"Failed to create Suna trigger: {e}")

View File

@ -788,7 +788,7 @@ async def create_composio_trigger(req: CreateComposioTriggerRequest, current_use
provider_id="composio",
name=req.name or f"{req.slug}",
config=suna_config,
description=f"Composio event: {req.slug}"
description=f"{req.slug}"
)
# Immediately sync triggers to the current version config

View File

@ -269,6 +269,98 @@ async def get_agent_triggers(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/all", response_model=List[Dict[str, Any]])
async def get_all_user_triggers(
user_id: str = Depends(get_current_user_id_from_jwt)
):
if not await is_enabled("agent_triggers"):
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
try:
client = await db.client
agents_result = await client.table('agents').select(
'agent_id, name, description, current_version_id, config, icon_name, icon_color, icon_background, profile_image_url'
).eq('account_id', user_id).execute()
if not agents_result.data:
return []
agent_info = {}
for agent in agents_result.data:
agent_config = agent.get('config', {})
agent_name = agent.get('name', 'Untitled Agent')
agent_description = agent.get('description', '')
if isinstance(agent_config, dict):
if 'metadata' in agent_config and isinstance(agent_config['metadata'], dict):
agent_name = agent_config['metadata'].get('name', agent_name)
agent_description = agent_config['metadata'].get('description', agent_description)
elif 'name' in agent_config:
agent_name = agent_config.get('name', agent_name)
elif 'description' in agent_config:
agent_description = agent_config.get('description', agent_description)
agent_info[agent['agent_id']] = {
'agent_name': agent_name,
'agent_description': agent_description,
'icon_name': agent.get('icon_name'),
'icon_color': agent.get('icon_color'),
'icon_background': agent.get('icon_background'),
'profile_image_url': agent.get('profile_image_url')
}
agent_ids = [agent['agent_id'] for agent in agents_result.data]
triggers_result = await client.table('agent_triggers').select('*').in_('agent_id', agent_ids).execute()
if not triggers_result.data:
return []
base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000")
responses = []
for trigger in triggers_result.data:
agent_id = trigger['agent_id']
webhook_url = f"{base_url}/api/triggers/{trigger['trigger_id']}/webhook"
config = trigger.get('config', {})
if isinstance(config, str):
try:
import json
config = json.loads(config)
except json.JSONDecodeError:
config = {}
response_data = {
'trigger_id': trigger['trigger_id'],
'agent_id': agent_id,
'trigger_type': trigger['trigger_type'],
'provider_id': trigger.get('provider_id', ''),
'name': trigger['name'],
'description': trigger.get('description'),
'is_active': trigger.get('is_active', False),
'webhook_url': webhook_url,
'created_at': trigger['created_at'],
'updated_at': trigger['updated_at'],
'config': config,
'agent_name': agent_info.get(agent_id, {}).get('agent_name', 'Untitled Agent'),
'agent_description': agent_info.get(agent_id, {}).get('agent_description', ''),
'icon_name': agent_info.get(agent_id, {}).get('icon_name'),
'icon_color': agent_info.get(agent_id, {}).get('icon_color'),
'icon_background': agent_info.get(agent_id, {}).get('icon_background'),
'profile_image_url': agent_info.get(agent_id, {}).get('profile_image_url')
}
responses.append(response_data)
responses.sort(key=lambda x: x['updated_at'], reverse=True)
return responses
except Exception as e:
logger.error(f"Error getting all user triggers: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/agents/{agent_id}/upcoming-runs", response_model=UpcomingRunsResponse)
async def get_agent_upcoming_runs(
agent_id: str,

View File

@ -695,7 +695,7 @@ function AgentConfigurationContent() {
<button
onClick={() => handleStartTestWithPrompt("Enter agent builder mode. Help me configure you to be my perfect agent. Ask me detailed questions about what I want you to do, how you should behave, what tools you need, and what knowledge would be helpful. Then suggest improvements to your system prompt, tools, and configuration.")}
disabled={initiateAgentMutation.isPending}
className="w-full text-left p-2 rounded border border-border hover:bg-accent text-xs transition-colors"
className="w-full text-left p-2 rounded-xl border border-border hover:bg-accent text-xs transition-colors"
>
<span className="font-medium">Agent Builder Mode</span>
<br />
@ -705,7 +705,7 @@ function AgentConfigurationContent() {
<button
onClick={() => handleStartTestWithPrompt("Hi! I want to test your capabilities. Can you tell me who you are, what you can do, and what tools and knowledge you have access to? Then let's do a quick test to see how well you work.")}
disabled={initiateAgentMutation.isPending}
className="w-full text-left p-2 rounded border border-border hover:bg-accent text-xs transition-colors"
className="w-full text-left p-2 rounded-xl border border-border hover:bg-accent text-xs transition-colors"
>
<span className="font-medium">Capability Test</span>
<br />
@ -715,7 +715,7 @@ function AgentConfigurationContent() {
<button
onClick={() => handleStartTestWithPrompt("I need help with a specific task. Let me explain what I'm trying to accomplish and you can guide me through the process step by step.")}
disabled={initiateAgentMutation.isPending}
className="w-full text-left p-2 rounded border border-border hover:bg-accent text-xs transition-colors"
className="w-full text-left p-2 rounded-xl border border-border hover:bg-accent text-xs transition-colors"
>
<span className="font-medium">Task-Based Run</span>
<br />

View File

@ -0,0 +1,5 @@
import { TasksPage } from '@/components/tasks/tasks-page';
export default function TasksRoute() {
return <TasksPage />;
}

View File

@ -59,7 +59,7 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
triggerId: trigger.trigger_id,
agentId: trigger.agent_id
});
toast.success('Trigger deleted successfully');
toast.success('Task deleted successfully');
} catch (error) {
toast.error('Failed to delete trigger');
console.error('Error deleting trigger:', error);

View File

@ -212,7 +212,7 @@ export const ConfiguredTriggersList: React.FC<ConfiguredTriggersListProps> = ({
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive hover:bg-destructive/90"
className="bg-destructive hover:bg-destructive/90 text-white"
>
Delete Trigger
</AlertDialogAction>

View File

@ -25,6 +25,7 @@ interface EventBasedTriggerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
agentId: string;
onTriggerCreated?: (triggerId: string) => void;
}
type JSONSchema = {
@ -295,7 +296,7 @@ const DynamicConfigForm: React.FC<{
);
};
export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = ({ open, onOpenChange, agentId }) => {
export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = ({ open, onOpenChange, agentId, onTriggerCreated }) => {
const [step, setStep] = useState<'apps' | 'triggers' | 'config'>('apps');
const [search, setSearch] = useState('');
const [selectedApp, setSelectedApp] = useState<{ slug: string; name: string; logo?: string } | null>(null);
@ -412,8 +413,13 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
const payload = executionType === 'agent'
? { ...base, route: 'agent' as const, agent_prompt: (prompt || 'Read this') }
: { ...base, route: 'workflow' as const, workflow_id: selectedWorkflowId, workflow_input: workflowInput };
await createTrigger.mutateAsync(payload);
toast.success('Event trigger created');
const result = await createTrigger.mutateAsync(payload);
toast.success('Task created');
if (onTriggerCreated && result?.trigger_id) {
onTriggerCreated(result.trigger_id);
}
onOpenChange(false);
} catch (e: any) {
toast.error(e?.message || 'Failed to create trigger');

View File

@ -186,6 +186,21 @@ export function SidebarLeft({
</span>
</SidebarMenuButton>
</Link>
<Link href="/tasks">
<SidebarMenuButton
className={cn('touch-manipulation mt-1', {
'bg-accent text-accent-foreground font-medium': pathname === '/tasks',
})}
onClick={() => {
if (isMobile) setOpenMobile(false);
}}
>
<Zap className="h-4 w-4 mr-1" />
<span className="flex items-center justify-between w-full">
Tasks
</span>
</SidebarMenuButton>
</Link>
{!flagsLoading && customAgentsEnabled && (
<SidebarMenu>
<Collapsible

View File

@ -0,0 +1,326 @@
"use client";
import React, { useMemo, useState, useEffect } from 'react';
import { useAllTriggers, type TriggerWithAgent } from '@/hooks/react-query/triggers/use-all-triggers';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
MessageSquare,
Github,
Slack,
Clock,
AlertCircle,
Zap,
Hash,
Globe,
Sparkles,
Plus,
ChevronDown,
PlugZap,
Webhook,
Repeat
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { TriggerCreationDialog } from './trigger-creation-dialog';
import { TriggerDetailPanel } from './trigger-detail-panel';
const getTriggerIcon = (triggerType: string) => {
switch (triggerType.toLowerCase()) {
case 'schedule':
case 'scheduled':
return Repeat;
case 'telegram':
return MessageSquare;
case 'github':
return Github;
case 'slack':
return Slack;
case 'webhook':
return Webhook;
case 'discord':
return Hash;
case 'event':
return Sparkles;
default:
return Globe;
}
};
const getTriggerCategory = (triggerType: string): 'scheduled' | 'app' => {
const scheduledTypes = ['schedule', 'scheduled'];
return scheduledTypes.includes(triggerType.toLowerCase()) ? 'scheduled' : 'app';
};
const formatCronExpression = (cron?: string) => {
if (!cron) return 'Not configured';
const parts = cron.split(' ');
if (parts.length !== 5) return cron;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
if (minute === '0' && hour === '0' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return 'Daily at midnight';
}
if (minute === '0' && hour === '*/1' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return 'Every hour';
}
if (minute === '*/15' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return 'Every 15 minutes';
}
if (minute === '*/30' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return 'Every 30 minutes';
}
if (minute === '0' && hour === '9' && dayOfMonth === '*' && month === '*' && dayOfWeek === '1-5') {
return 'Weekdays at 9 AM';
}
if (minute === '0' && hour === String(hour) && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return `Daily at ${hour}:${minute.padStart(2, '0')}`;
}
return cron;
};
const TriggerListItem = ({
trigger,
onClick,
isSelected
}: {
trigger: TriggerWithAgent;
onClick: () => void;
isSelected: boolean;
}) => {
const Icon = getTriggerIcon(trigger.trigger_type);
const isScheduled = getTriggerCategory(trigger.trigger_type) === 'scheduled';
return (
<div
onClick={onClick}
className={cn(
"rounded-xl border group flex items-center justify-between px-4 py-3 cursor-pointer transition-all",
isSelected ? "bg-muted border-foreground/20" : "dark:bg-card hover:bg-muted/50"
)}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">{trigger.name}</span>
{!trigger.is_active && (
<Badge variant="secondary" className="text-xs">Inactive</Badge>
)}
</div>
{trigger.description && (
<p className="text-xs text-muted-foreground truncate mt-0.5">
{trigger.description}
</p>
)}
</div>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{isScheduled && trigger.config?.cron_expression && (
<span className="hidden sm:block">{formatCronExpression(trigger.config.cron_expression)}</span>
)}
<Repeat className="h-3 w-3" />
</div>
</div>
);
};
const EmptyState = () => (
<div className="bg-muted/20 rounded-3xl border flex flex-col items-center justify-center py-16 px-4">
<div className="w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4">
<Zap className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-base font-semibold text-foreground mb-2">Get started by adding a task</h3>
<p className="text-sm text-muted-foreground text-center max-w-sm mb-6">
Schedule a task to automate actions and get reminders when they complete.
</p>
</div>
);
const LoadingSkeleton = () => (
<div className="space-y-4 px-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="rounded-xl border dark:bg-card px-4 py-3">
<div className="flex items-center gap-3">
<Skeleton className="h-4 w-4 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
<Skeleton className="h-3 w-20" />
</div>
</div>
))}
</div>
);
export function TasksPage() {
const { data: triggers = [], isLoading, error } = useAllTriggers();
const [selectedTrigger, setSelectedTrigger] = useState<TriggerWithAgent | null>(null);
const [triggerDialogType, setTriggerDialogType] = useState<'schedule' | 'event' | null>(null);
const [pendingTriggerId, setPendingTriggerId] = useState<string | null>(null);
const sortedTriggers = useMemo(() => {
return [...triggers].sort((a, b) => {
if (a.is_active !== b.is_active) {
return a.is_active ? -1 : 1;
}
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
});
}, [triggers]);
useEffect(() => {
if (pendingTriggerId) {
const newTrigger = triggers.find(t => t.trigger_id === pendingTriggerId);
if (newTrigger) {
setSelectedTrigger(newTrigger);
setPendingTriggerId(null);
}
}
}, [triggers, pendingTriggerId]);
useEffect(() => {
if (selectedTrigger) {
const updatedTrigger = triggers.find(t => t.trigger_id === selectedTrigger.trigger_id);
if (updatedTrigger) {
setSelectedTrigger(updatedTrigger);
} else {
setSelectedTrigger(null);
}
}
}, [triggers, selectedTrigger?.trigger_id]);
const handleClosePanel = () => {
setSelectedTrigger(null);
};
const handleTriggerCreated = (triggerId: string) => {
setTriggerDialogType(null);
setPendingTriggerId(triggerId);
};
if (error) {
return (
<div className="h-screen flex flex-col">
<div className="max-w-4xl mx-auto w-full py-8 px-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Failed to load tasks. Please try refreshing the page.
</AlertDescription>
</Alert>
</div>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex justify-center">
<div className={cn(
"w-full px-4 transition-all duration-300 ease-in-out",
selectedTrigger ? "max-w-2xl" : "max-w-4xl"
)}>
<div className="flex items-center justify-between py-10">
<h1 className="text-xl font-semibold">Tasks</h1>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Plus className="h-4 w-4" />
New task
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72">
<DropdownMenuItem onClick={() => setTriggerDialogType('schedule')} className='rounded-lg'>
<Clock className="h-4 w-4 text-muted-foreground" />
<div className="flex flex-col">
<span>Scheduled Task</span>
<span className="text-xs text-muted-foreground">
Schedule a task to run at a specific time
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTriggerDialogType('event')} className='rounded-lg'>
<PlugZap className="h-4 w-4 text-muted-foreground" />
<div className="flex flex-col">
<span>Event-based Task</span>
<span className="text-xs text-muted-foreground">
Make a task to run when an event occurs
</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
<div className="flex justify-center">
<div className={cn(
"w-full px-4 pb-8 transition-all duration-300 ease-in-out",
selectedTrigger ? "max-w-2xl" : "max-w-4xl"
)}>
{isLoading ? (
<LoadingSkeleton />
) : sortedTriggers.length === 0 ? (
<EmptyState />
) : (
<div className="space-y-4">
{sortedTriggers.map(trigger => (
<TriggerListItem
key={trigger.trigger_id}
trigger={trigger}
isSelected={selectedTrigger?.trigger_id === trigger.trigger_id}
onClick={() => {
if (selectedTrigger?.trigger_id === trigger.trigger_id) {
setSelectedTrigger(null);
} else {
setSelectedTrigger(trigger);
}
}}
/>
))}
</div>
)}
</div>
</div>
</div>
</div>
<div className={cn(
"h-screen transition-all duration-300 ease-in-out overflow-hidden border-l",
selectedTrigger ? "w-2xl" : "w-0"
)}>
{selectedTrigger && (
<TriggerDetailPanel
trigger={selectedTrigger}
onClose={handleClosePanel}
/>
)}
</div>
{/* Trigger Creation Dialog */}
{triggerDialogType && (
<TriggerCreationDialog
open={!!triggerDialogType}
onOpenChange={(open) => {
if (!open) {
setTriggerDialogType(null);
}
}}
type={triggerDialogType}
onTriggerCreated={handleTriggerCreated}
/>
)}
</div>
);
}

View File

@ -0,0 +1,209 @@
"use client";
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { ArrowRight, Clock, PlugZap } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { createClient } from '@/lib/supabase/client';
import { TriggerConfigDialog } from '@/components/agents/triggers/trigger-config-dialog';
import { EventBasedTriggerDialog } from '@/components/agents/triggers/event-based-trigger-dialog';
import { useCreateTrigger } from '@/hooks/react-query/triggers';
import { toast } from 'sonner';
import { AgentIconAvatar } from '@/components/agents/config/agent-icon-avatar';
interface TriggerCreationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
type: 'schedule' | 'event';
onTriggerCreated?: (triggerId: string) => void;
}
export function TriggerCreationDialog({
open,
onOpenChange,
type,
onTriggerCreated
}: TriggerCreationDialogProps) {
const [step, setStep] = useState<'agent-selection' | 'trigger-config'>('agent-selection');
const [selectedAgent, setSelectedAgent] = useState<string>('');
const createTriggerMutation = useCreateTrigger();
// Fetch user's agents
const { data: agents, isLoading } = useQuery({
queryKey: ['user-agents'],
queryFn: async () => {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
const { data, error } = await supabase
.from('agents')
.select('agent_id, name, icon_name, icon_color, icon_background, profile_image_url')
.eq('account_id', user.id);
if (error) throw error;
return data || [];
},
enabled: open
});
const scheduleProvider = {
provider_id: 'schedule',
name: 'Schedule',
trigger_type: 'schedule',
webhook_enabled: true,
config_schema: {}
};
const handleAgentSelect = () => {
if (!selectedAgent) {
toast.error('Please select an agent');
return;
}
setStep('trigger-config');
};
const handleScheduleSave = async (config: any) => {
try {
const newTrigger = await createTriggerMutation.mutateAsync({
agentId: selectedAgent,
provider_id: 'schedule',
name: config.name || 'Scheduled Trigger',
description: config.description || 'Automatically scheduled trigger',
config: config.config,
});
toast.success('Schedule trigger created successfully');
// Call the callback with the new trigger ID
if (onTriggerCreated && newTrigger?.trigger_id) {
onTriggerCreated(newTrigger.trigger_id);
}
handleClose();
} catch (error: any) {
toast.error(error.message || 'Failed to create schedule trigger');
console.error('Error creating schedule trigger:', error);
}
};
const handleClose = () => {
setStep('agent-selection');
setSelectedAgent('');
onOpenChange(false);
};
const handleBack = () => {
setStep('agent-selection');
};
if (!open) return null;
if (step === 'agent-selection') {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{type === 'schedule' ? (
<>
<Clock className="h-5 w-5" />
Create Scheduled Task
</>
) : (
<>
<PlugZap className="h-5 w-5" />
Create App-based Task
</>
)}
</DialogTitle>
<DialogDescription>
First, select which agent should handle this task
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="agent">Select Agent</Label>
<Select value={selectedAgent} onValueChange={setSelectedAgent}>
<SelectTrigger id="agent">
<SelectValue placeholder="Choose an agent" />
</SelectTrigger>
<SelectContent>
{isLoading ? (
<div className="p-2 text-sm text-muted-foreground">Loading agents...</div>
) : agents?.length === 0 ? (
<div className="p-2 text-sm text-muted-foreground">
No agents found. Create an agent first.
</div>
) : (
agents?.map(agent => (
<SelectItem key={agent.agent_id} value={agent.agent_id}>
<div className="flex items-center gap-2">
<AgentIconAvatar
profileImageUrl={agent.profile_image_url}
iconName={agent.icon_name}
iconColor={agent.icon_color}
backgroundColor={agent.icon_background}
agentName={agent.name}
size={20}
/>
<span>{agent.name}</span>
</div>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleAgentSelect} disabled={!selectedAgent}>
Next
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
</div>
</DialogContent>
</Dialog>
);
}
if (type === 'schedule') {
return (
<Dialog open={open} onOpenChange={handleClose}>
<TriggerConfigDialog
provider={scheduleProvider}
existingConfig={undefined}
onSave={handleScheduleSave}
onCancel={handleBack}
isLoading={createTriggerMutation.isPending}
agentId={selectedAgent}
/>
</Dialog>
);
}
return (
<EventBasedTriggerDialog
open={open}
onOpenChange={handleClose}
agentId={selectedAgent}
onTriggerCreated={onTriggerCreated}
/>
);
}

View File

@ -0,0 +1,514 @@
"use client";
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog } from '@/components/ui/dialog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Clock,
X,
Trash2,
Power,
PowerOff,
ExternalLink,
Loader2,
Repeat,
Zap,
Edit2,
Activity,
Sparkles,
Mail,
MessageSquare,
Calendar as CalendarIcon,
FileText,
Users,
Globe,
Info
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import Link from 'next/link';
import { TriggerWithAgent } from '@/hooks/react-query/triggers/use-all-triggers';
import { useDeleteTrigger, useToggleTrigger, useUpdateTrigger } from '@/hooks/react-query/triggers';
import { TriggerConfigDialog } from '@/components/agents/triggers/trigger-config-dialog';
import { useAgentWorkflows } from '@/hooks/react-query/agents/use-agent-workflows';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { AgentIconAvatar } from '@/components/agents/config/agent-icon-avatar';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
interface TriggerDetailPanelProps {
trigger: TriggerWithAgent;
onClose: () => void;
}
const CRON_PRESETS = [
{ label: 'Every 15 minutes', value: '*/15 * * * *', emoji: '⚡' },
{ label: 'Every 30 minutes', value: '*/30 * * * *', emoji: '⏱️' },
{ label: 'Every hour', value: '0 * * * *', emoji: '🕐' },
{ label: 'Daily at 9 AM', value: '0 9 * * *', emoji: '☀️' },
{ label: 'Daily at midnight', value: '0 0 * * *', emoji: '🌙' },
{ label: 'Weekdays at 9 AM', value: '0 9 * * 1-5', emoji: '💼' },
{ label: 'Weekly on Monday', value: '0 9 * * 1', emoji: '📅' },
{ label: 'Monthly on 1st', value: '0 9 1 * *', emoji: '📆' },
];
const formatCronExpression = (cron?: string) => {
if (!cron) return 'Not configured';
const preset = CRON_PRESETS.find(p => p.value === cron);
if (preset) return preset.label;
const parts = cron.split(' ');
if (parts.length !== 5) return cron;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
if (minute === '0' && hour === String(hour) && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return `Daily at ${hour}:${minute.padStart(2, '0')}`;
}
return cron;
};
const getTriggerIcon = (triggerType: string) => {
switch (triggerType.toLowerCase()) {
case 'schedule':
case 'scheduled':
return Repeat;
default:
return Zap;
}
};
const getEventIcon = (triggerSlug: string) => {
if (!triggerSlug) return <Activity className="h-4 w-4" />;
const slug = triggerSlug.toLowerCase();
if (slug.includes('gmail') || slug.includes('email') || slug.includes('mail')) {
return <Mail className="h-4 w-4" />;
}
if (slug.includes('slack') || slug.includes('message') || slug.includes('chat')) {
return <MessageSquare className="h-4 w-4" />;
}
if (slug.includes('calendar') || slug.includes('event') || slug.includes('meeting')) {
return <CalendarIcon className="h-4 w-4" />;
}
if (slug.includes('document') || slug.includes('file') || slug.includes('doc')) {
return <FileText className="h-4 w-4" />;
}
if (slug.includes('user') || slug.includes('member') || slug.includes('contact')) {
return <Users className="h-4 w-4" />;
}
if (slug.includes('web') || slug.includes('http') || slug.includes('url')) {
return <Globe className="h-4 w-4" />;
}
return <Activity className="h-4 w-4" />;
};
const formatEventName = (triggerSlug: string) => {
if (!triggerSlug) return 'Event';
return triggerSlug
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
const getEventTypeFromSlug = (triggerSlug: string) => {
if (!triggerSlug) return 'Event';
const slug = triggerSlug.toLowerCase();
if (slug.includes('new')) return 'Creation';
if (slug.includes('update')) return 'Update';
if (slug.includes('delete')) return 'Deletion';
if (slug.includes('message')) return 'Message';
if (slug.includes('email')) return 'Email';
return 'Event';
};
export function TriggerDetailPanel({ trigger, onClose }: TriggerDetailPanelProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const deleteMutation = useDeleteTrigger();
const toggleMutation = useToggleTrigger();
const updateMutation = useUpdateTrigger();
const { data: workflows = [] } = useAgentWorkflows(trigger.agent_id);
const workflowName = workflows.find(w => w.id === trigger.config?.workflow_id)?.name;
const Icon = getTriggerIcon(trigger.trigger_type);
const isScheduled = trigger.trigger_type.toLowerCase() === 'schedule' || trigger.trigger_type.toLowerCase() === 'scheduled';
const currentPreset = CRON_PRESETS.find(p => p.value === trigger.config?.cron_expression);
const handleToggle = async () => {
try {
await toggleMutation.mutateAsync({
triggerId: trigger.trigger_id,
isActive: !trigger.is_active,
});
toast.success(`Task ${!trigger.is_active ? 'enabled' : 'disabled'}`);
} catch (error) {
toast.error('Failed to toggle task');
console.error('Error toggling task:', error);
}
};
const handleDelete = async () => {
try {
await deleteMutation.mutateAsync({
triggerId: trigger.trigger_id,
agentId: trigger.agent_id
});
toast.success('Task deleted successfully');
onClose();
} catch (error) {
toast.error('Failed to delete task');
console.error('Error deleting task:', error);
}
};
const handleEditSave = async (config: any) => {
try {
await updateMutation.mutateAsync({
triggerId: trigger.trigger_id,
name: config.name,
description: config.description,
config: config.config,
is_active: config.is_active,
});
toast.success('Task updated successfully');
setShowEditDialog(false);
} catch (error: any) {
toast.error(error.message || 'Failed to update task');
console.error('Error updating task:', error);
}
};
const isLoading = deleteMutation.isPending || toggleMutation.isPending || updateMutation.isPending;
const provider = {
provider_id: isScheduled ? 'schedule' : trigger.provider_id,
name: trigger.name,
description: trigger.description || '',
trigger_type: trigger.trigger_type,
webhook_enabled: !!trigger.webhook_url,
config_schema: {}
};
const triggerConfig = {
trigger_id: trigger.trigger_id,
agent_id: trigger.agent_id,
trigger_type: trigger.trigger_type,
provider_id: trigger.provider_id,
name: trigger.name,
description: trigger.description,
is_active: trigger.is_active,
webhook_url: trigger.webhook_url,
created_at: trigger.created_at,
updated_at: trigger.updated_at,
config: trigger.config
};
return (
<TooltipProvider>
<div className="h-full bg-background flex flex-col">
<div className="p-6 pb-4">
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<h2 className="text-xl font-semibold">{trigger.name}</h2>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 -mr-2"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-3 mb-4">
<div className="flex items-center gap-2">
<div className={cn(
"h-2 w-2 rounded-full",
trigger.is_active ? "bg-green-500" : "bg-muted-foreground"
)} />
<span className="text-sm text-muted-foreground">
{trigger.is_active ? "Active" : "Inactive"}
</span>
</div>
{isScheduled && trigger.config?.cron_expression && (
<>
<span className="text-muted-foreground"></span>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-3.5 w-3.5" />
<span>{formatCronExpression(trigger.config.cron_expression)}</span>
</div>
</>
)}
</div>
<div className="flex gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() => setShowEditDialog(true)}
>
<Edit2 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit task</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={handleToggle}
disabled={isLoading}
>
{trigger.is_active ? (
<PowerOff className="h-3.5 w-3.5" />
) : (
<Power className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{trigger.is_active ? 'Disable' : 'Enable'} task
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() => setShowDeleteDialog(true)}
disabled={isLoading}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete task</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
<div className="px-6 pb-6 space-y-6">
{isScheduled && trigger.config && (
<div className="rounded-lg space-y-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{trigger.config.execution_type === 'agent' ? (
<>
<span>Agent Prompt</span>
</>
) : (
<>
<Activity className="h-4 w-4 text-muted-foreground" />
<span>Run Workflow{workflowName ? `: ${workflowName}` : ''}</span>
</>
)}
</div>
{trigger.config.execution_type === 'agent' && trigger.config.agent_prompt && (
<div className="rounded-lg">
<p className="text-sm whitespace-pre-wrap font-mono">
{trigger.config.agent_prompt}
</p>
</div>
)}
{trigger.config.execution_type === 'workflow' && trigger.config.workflow_input && (
<div className="rounded-lg border p-3 bg-muted/30 mt-2">
<p className="text-xs text-muted-foreground mb-1">Workflow Input:</p>
<pre className="text-xs font-mono overflow-x-auto">
{JSON.stringify(trigger.config.workflow_input, null, 2)}
</pre>
</div>
)}
</div>
)}
{(trigger.provider_id === 'composio' || trigger.config?.trigger_slug || trigger.config?.composio_trigger_id) && trigger.config && (
<div className="space-y-4">
{trigger.config.trigger_slug && (
<Card className="shadow-none">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10 text-primary">
{getEventIcon(trigger.config.trigger_slug)}
</div>
<div>
<div className="font-medium">{formatEventName(trigger.config.trigger_slug)}</div>
<div className="text-sm text-muted-foreground font-normal">
{trigger.name} {getEventTypeFromSlug(trigger.config.trigger_slug)}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
<Clock className="h-3 w-3 mr-1" />
Real-time
</Badge>
<Badge variant="outline" className="text-xs">
Event-driven
</Badge>
</div>
<div className="text-sm text-muted-foreground">
This trigger will automatically activate whenever a <strong>{formatEventName(trigger.config.trigger_slug)}</strong> event occurs in your connected account.
</div>
{trigger.config.composio_trigger_id && (
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Event ID</span>
<code className="text-xs bg-muted px-2 py-1 rounded">
{trigger.config.trigger_slug}
</code>
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
<div className="rounded-lg space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
{trigger.config.execution_type === 'agent' ? (
<>
<Sparkles className="h-4 w-4" />
<span>Agent Prompt</span>
</>
) : (
<>
<Activity className="h-4 w-4" />
<span>Workflow Execution</span>
</>
)}
</div>
{trigger.config.execution_type === 'agent' && trigger.config.agent_prompt && (
<div className="rounded-lg border p-3 bg-muted/30">
<p className="text-sm whitespace-pre-wrap font-mono">
{trigger.config.agent_prompt}
</p>
</div>
)}
{trigger.config.execution_type === 'workflow' && trigger.config.workflow_id && (
<div className="rounded-lg border p-3 bg-muted/30">
<div className="space-y-1">
<p className="text-sm">
Workflow: <span className="font-medium">{workflowName || 'Unknown Workflow'}</span>
</p>
</div>
{trigger.config.workflow_input && (
<div className="mt-3 pt-3 border-t">
<p className="text-xs text-muted-foreground mb-1">Input:</p>
<pre className="text-xs font-mono overflow-x-auto">
{JSON.stringify(trigger.config.workflow_input, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
<div className="rounded-lg border p-4 bg-muted/30">
<h4 className="text-sm font-medium mb-2 flex items-center gap-2">
<Info className="h-4 w-4" />
How It Works
</h4>
<div className="space-y-2 text-sm text-muted-foreground">
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary mt-2 flex-shrink-0" />
<span>When the event occurs in your connected app, a webhook notification is sent instantly</span>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary mt-2 flex-shrink-0" />
<span>Your agent receives the event data and executes the configured action</span>
</div>
</div>
</div>
</div>
)}
<div className="rounded-2xl border bg-card p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<AgentIconAvatar
profileImageUrl={trigger.profile_image_url}
iconName={trigger.icon_name}
iconColor={trigger.icon_color}
backgroundColor={trigger.icon_background}
agentName={trigger.agent_name}
size={36}
/>
<div>
<p className="text-sm font-medium">
{trigger.agent_name || 'Unknown Agent'}
</p>
<p className="text-sm text-muted-foreground">Agent</p>
</div>
</div>
<Link
href={`/agents/config/${trigger.agent_id}`}
className="p-2 rounded-lg hover:bg-muted transition-colors"
>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
</Link>
</div>
</div>
</div>
</div>
{showEditDialog && (
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<TriggerConfigDialog
provider={provider}
existingConfig={triggerConfig}
onSave={handleEditSave}
onCancel={() => setShowEditDialog(false)}
isLoading={updateMutation.isPending}
agentId={trigger.agent_id}
/>
</Dialog>
)}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Task</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{trigger.name}"? This action cannot be undone and will stop all automated runs from this task.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive hover:bg-destructive/90 text-white"
>
Delete Task
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TooltipProvider>
);
}

View File

@ -96,6 +96,7 @@ export const useCreateComposioEventTrigger = () => {
if (agentId) {
queryClient.invalidateQueries({ queryKey: ['agent-triggers', agentId] });
}
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
}
});
};

View File

@ -1,8 +1,4 @@
export { useAgentTriggers, useCreateTrigger, useUpdateTrigger, useDeleteTrigger, useToggleTrigger } from './use-agent-triggers';
export { useTriggerProviders } from './use-trigger-providers';
export {
useAgentTriggers,
useCreateTrigger,
useUpdateTrigger,
useDeleteTrigger,
useToggleTrigger
} from './use-agent-triggers';
export { useInstallOAuthIntegration, useUninstallOAuthIntegration, useOAuthCallbackHandler } from './use-oauth-integrations';
export { useAllTriggers, type TriggerWithAgent } from './use-all-triggers';

View File

@ -114,6 +114,7 @@ export const useCreateTrigger = () => {
mutationFn: createTrigger,
onSuccess: (newTrigger) => {
queryClient.invalidateQueries({ queryKey: ['agent-upcoming-runs', newTrigger.agent_id] });
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
queryClient.setQueryData(
['agent-triggers', newTrigger.agent_id],
(old: TriggerConfiguration[] | undefined) => {
@ -131,6 +132,7 @@ export const useUpdateTrigger = () => {
mutationFn: updateTrigger,
onSuccess: (updatedTrigger) => {
queryClient.invalidateQueries({ queryKey: ['agent-upcoming-runs', updatedTrigger.agent_id] });
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
queryClient.setQueryData(
['agent-triggers', updatedTrigger.agent_id],
(old: TriggerConfiguration[] | undefined) => {
@ -152,6 +154,7 @@ export const useDeleteTrigger = () => {
onSuccess: (_, { triggerId, agentId }) => {
queryClient.invalidateQueries({ queryKey: ['agent-upcoming-runs', agentId] });
queryClient.invalidateQueries({ queryKey: ['agent-triggers'] });
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
},
});
};
@ -168,6 +171,7 @@ export const useToggleTrigger = () => {
},
onSuccess: (updatedTrigger) => {
queryClient.invalidateQueries({ queryKey: ['agent-upcoming-runs', updatedTrigger.agent_id] });
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
queryClient.setQueryData(
['agent-triggers', updatedTrigger.agent_id],
(old: TriggerConfiguration[] | undefined) => {

View File

@ -0,0 +1,55 @@
import { useQuery } from '@tanstack/react-query';
import { createClient } from '@/lib/supabase/client';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL;
export interface TriggerWithAgent {
trigger_id: string;
agent_id: string;
trigger_type: string;
provider_id: string;
name: string;
description?: string;
is_active: boolean;
webhook_url?: string;
created_at: string;
updated_at: string;
config?: Record<string, any>;
agent_name?: string;
agent_description?: string;
icon_name?: string;
icon_color?: string;
icon_background?: string;
profile_image_url?: string;
}
const fetchAllTriggers = async (): Promise<TriggerWithAgent[]> => {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('You must be logged in to fetch triggers');
}
const response = await fetch(`${API_URL}/triggers/all`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Failed to fetch triggers' }));
throw new Error(error.detail || 'Failed to fetch triggers');
}
return response.json();
};
export const useAllTriggers = () => {
return useQuery({
queryKey: ['all-triggers'],
queryFn: fetchAllTriggers,
staleTime: 1 * 60 * 1000,
refetchInterval: 30 * 1000,
});
};