mirror of https://github.com/kortix-ai/suna.git
tasks page
This commit is contained in:
parent
5319a09476
commit
f7dc975acf
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { TasksPage } from '@/components/tasks/tasks-page';
|
||||
|
||||
export default function TasksRoute() {
|
||||
return <TasksPage />;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -96,6 +96,7 @@ export const useCreateComposioEventTrigger = () => {
|
|||
if (agentId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-triggers', agentId] });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['all-triggers'] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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';
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue