standardise ui

This commit is contained in:
marko-kraemer 2025-08-10 18:47:41 -07:00
parent 6796216a4f
commit 4d1e11267f
8 changed files with 386 additions and 300 deletions

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Switch } from '@/components/ui/switch';
import { Search } from 'lucide-react';
import { AGENTPRESS_TOOL_DEFINITIONS, getToolDisplayName } from './tools';
import { toast } from 'sonner';
@ -59,49 +60,57 @@ export const AgentToolsConfiguration = ({ tools, onToolsChange, disabled = false
};
return (
<div className="h-full flex flex-col">
<div className="flex-shrink-0 mb-4">
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-muted-foreground">
{getSelectedToolsCount()} selected
</span>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-foreground">Default Tools</h3>
<p className="text-xs text-muted-foreground">Configure default agentpress tools {getSelectedToolsCount()} selected</p>
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div className="gap-4 grid grid-cols-1 md:grid-cols-2">
<div>
<div className="space-y-2">
{getFilteredTools().map(([toolName, toolInfo]) => (
<div
key={toolName}
className="flex items-center gap-3 p-3 bg-muted/50 rounded-lg border hover:border-border/80 transition-colors"
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors"
>
<div className={`w-10 h-10 rounded-lg ${toolInfo.color} flex items-center justify-center flex-shrink-0`}>
<span className="text-lg">{toolInfo.icon}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h4 className="font-medium text-sm">
{getToolDisplayName(toolName)}
</h4>
<Switch
checked={isToolEnabled(tools[toolName])}
onCheckedChange={(checked) => handleToolToggle(toolName, checked)}
className="flex-shrink-0"
disabled={disabled}
/>
<div className="flex items-center space-x-4 flex-1">
<div className={`w-10 h-10 rounded-lg ${toolInfo.color} border flex items-center justify-center flex-shrink-0`}>
<span className="text-lg">{toolInfo.icon}</span>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{toolInfo.description}
</p>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-sm font-medium truncate">
{getToolDisplayName(toolName)}
</h4>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{toolInfo.description}
</p>
</div>
</div>
<div className="flex items-center flex-shrink-0">
<Switch
checked={isToolEnabled(tools[toolName])}
onCheckedChange={(checked) => handleToolToggle(toolName, checked)}
disabled={disabled}
/>
</div>
</div>
))}
</div>
{getFilteredTools().length === 0 && (
<div className="text-center py-8">
<div className="text-4xl mb-3">🔍</div>
<h3 className="text-sm font-medium mb-1">No tools found</h3>
<p className="text-xs text-muted-foreground">Try adjusting your search criteria</p>
<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 border">
<Search className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground mb-2">
No tools found
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Try adjusting your search criteria
</p>
</div>
)}
</div>

View File

@ -393,7 +393,6 @@ export function ConfigurationTab({
<div className="bg-muted rounded-xl h-10 w-10 flex items-center justify-center transition-all duration-300 group-hover:scale-105">
<Zap className="h-5 w-5 text-muted-foreground" />
</div>
</div>
<div className="text-left flex-1">
<h4 className="text-sm font-semibold text-foreground mb-1 group-hover:text-primary transition-colors duration-300">Triggers</h4>

View File

@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Plus,
Edit2,
@ -621,7 +621,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
return (
<div
className="space-y-6"
className="space-y-4"
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
@ -639,27 +639,33 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
</div>
)}
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search knowledge entries..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
<div>
<h3 className="text-sm font-medium text-foreground">Knowledge Base</h3>
<p className="text-xs text-muted-foreground">Upload and manage knowledge for the agent</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 w-48"
/>
</div>
<Button onClick={() => handleOpenAddDialog()} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
Add Knowledge
</Button>
</div>
<Button onClick={() => handleOpenAddDialog()} className="gap-2 ml-4">
<Plus className="h-4 w-4" />
Add Knowledge
</Button>
</div>
{entries.length === 0 ? (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4 border">
<Bot className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-sm font-semibold mb-2">No Agent Knowledge Entries</h3>
<p className="text-muted-foreground mb-6 max-w-sm mx-auto">
<h4 className="text-sm font-semibold text-foreground mb-2">No knowledge entries</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Add knowledge entries to provide <span className="font-medium">{agentName}</span> with specialized context,
guidelines, and information it should always remember.
</p>
@ -667,9 +673,16 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
) : (
<div className="space-y-3">
{filteredEntries.length === 0 ? (
<div className="text-center py-8">
<Search className="h-8 w-8 mx-auto text-muted-foreground/50 mb-2" />
<p className="text-sm text-muted-foreground">No entries match your search</p>
<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 border">
<Search className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground mb-2">
No entries match your search
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Try adjusting your search criteria or add new knowledge entries
</p>
</div>
) : (
filteredEntries.map((entry) => {
@ -678,106 +691,70 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const SourceIcon = getSourceIcon(entry.source_type || 'manual', entry.source_metadata?.filename);
return (
<Card
<div
key={entry.entry_id}
className={cn(
"group transition-all p-0",
entry.is_active
? "bg-card"
: "bg-muted/30 opacity-70"
)}
className="flex items-start justify-between p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors group"
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center gap-2">
<SourceIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<h3 className="font-medium truncate">{entry.name}</h3>
{!entry.is_active && (
<Badge variant="outline" className="text-xs">
<EyeOff className="h-3 w-3 mr-1" />
Disabled
</Badge>
)}
{entry.source_type && entry.source_type !== 'manual' && (
<Badge variant="outline" className="text-xs">
{entry.source_type === 'git_repo' ? 'Git' :
entry.source_type === 'zip_extracted' ? 'ZIP' : 'File'}
</Badge>
)}
</div>
{entry.description && (
<p className="text-sm text-muted-foreground line-clamp-1">
{entry.description}
</p>
)}
<p className="text-sm text-foreground/80 line-clamp-2 leading-relaxed">
{entry.content}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge variant="outline" className={cn("text-xs gap-1", contextConfig.color)}>
<ContextIcon className="h-3 w-3" />
{contextConfig.label}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(entry.created_at).toLocaleDateString()}
</span>
{entry.file_size && (
<span className="text-xs text-muted-foreground">
{(entry.file_size / 1024).toFixed(1)}KB
</span>
)}
</div>
{entry.content_tokens && (
<span className="text-xs text-muted-foreground">
~{entry.content_tokens.toLocaleString()} tokens
</span>
)}
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36">
<DropdownMenuItem onClick={() => handleOpenEditDialog(entry)}>
<Edit2 className="h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleToggleActive(entry)}>
{entry.is_active ? (
<>
<EyeOff className="h-4 w-4" />
Disable
</>
) : (
<>
<Eye className="h-4 w-4" />
Enable
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setDeleteEntryId(entry.entry_id)}
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
>
<Trash2 className="h-4 w-4 text-destructive" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-sm font-medium truncate">{entry.name}</h4>
{entry.source_type && entry.source_type !== 'manual' && (
<Badge variant="outline" className="text-xs">
{entry.source_type === 'git_repo' ? 'Git' :
entry.source_type === 'zip_extracted' ? 'ZIP' : 'File'}
</Badge>
)}
</div>
</CardContent>
</Card>
{entry.description && (
<p className="text-xs text-muted-foreground line-clamp-1">
{entry.description}
</p>
)}
<p className="text-xs text-foreground/80 line-clamp-2 leading-relaxed">
{entry.content}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge variant="outline" className={cn("text-xs gap-1", contextConfig.color)}>
<ContextIcon className="h-3 w-3" />
{contextConfig.label}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(entry.created_at).toLocaleDateString()}
</span>
{entry.file_size && (
<span className="text-xs text-muted-foreground">
{(entry.file_size / 1024).toFixed(1)}KB
</span>
)}
</div>
{entry.content_tokens && (
<span className="text-xs text-muted-foreground">
~{entry.content_tokens.toLocaleString()} tokens
</span>
)}
</div>
</div>
<div className="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => handleOpenEditDialog(entry)}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={() => setDeleteEntryId(entry.entry_id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
);
})
)}

View File

@ -2,7 +2,17 @@ import React from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Settings, X, Sparkles, Key, AlertTriangle } from 'lucide-react';
import { Settings, X, Sparkles, Key, AlertTriangle, Trash2 } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { MCPConfiguration } from './types';
import { useCredentialProfilesForMcp } from '@/hooks/react-query/mcp/use-credential-profiles';
@ -50,7 +60,7 @@ const MCPLogo: React.FC<{ mcp: MCPConfiguration }> = ({ mcp }) => {
const firstLetter = mcp.name.charAt(0).toUpperCase();
return (
<div className="w-6 h-6 flex items-center justify-center flex-shrink-0 overflow-hidden">
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 overflow-hidden">
{logoUrl ? (
<img
src={logoUrl}
@ -87,49 +97,51 @@ const MCPConfigurationItem: React.FC<{
const hasCredentialProfile = !!profileId && !!selectedProfile;
return (
<Card className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors">
<div className="flex items-center space-x-4 flex-1">
<div className="p-2 rounded-lg bg-muted border">
<MCPLogo mcp={mcp} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className="font-medium text-sm truncate">{mcp.name}</div>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span>{mcp.enabledTools?.length || 0} tools enabled</span>
{hasCredentialProfile && (
<div className="flex items-center gap-1">
<Key className="h-3 w-3 text-green-600" />
<span className="text-green-600 font-medium truncate max-w-24">
{selectedProfile.profile_name}
</span>
</div>
)}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-sm font-medium truncate">{mcp.name}</h4>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span>{mcp.enabledTools?.length || 0} tools enabled</span>
{hasCredentialProfile && (
<div className="flex items-center gap-1">
<Key className="h-3 w-3 text-green-600" />
<span className="text-green-600 font-medium truncate max-w-24">
{selectedProfile.profile_name}
</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{onConfigureTools && (
<Button
size="sm"
variant="ghost"
onClick={() => onConfigureTools(index)}
title="Configure tools"
>
<Settings className="h-4 w-4" />
</Button>
)}
</div>
<div className="flex items-center space-x-2 flex-shrink-0">
{onConfigureTools && (
<Button
size="sm"
variant="ghost"
onClick={() => onRemove(index)}
title="Remove integration"
className="h-8 w-8 p-0"
onClick={() => onConfigureTools(index)}
title="Configure tools"
>
<X className="h-4 w-4" />
<Settings className="h-4 w-4" />
</Button>
</div>
)}
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={() => onRemove(index)}
title="Remove integration"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</Card>
</div>
);
};
@ -139,20 +151,58 @@ export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
onRemove,
onConfigureTools,
}) => {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [mcpToDelete, setMcpToDelete] = React.useState<{ mcp: MCPConfiguration; index: number } | null>(null);
const handleDeleteClick = (mcp: MCPConfiguration, index: number) => {
setMcpToDelete({ mcp, index });
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (mcpToDelete) {
onRemove(mcpToDelete.index);
setMcpToDelete(null);
setDeleteDialogOpen(false);
}
};
if (configuredMCPs.length === 0) return null;
return (
<div className="space-y-2">
{configuredMCPs.map((mcp, index) => (
<MCPConfigurationItem
key={index}
mcp={mcp}
index={index}
onEdit={onEdit}
onRemove={onRemove}
onConfigureTools={onConfigureTools}
/>
))}
</div>
<>
<div className="space-y-2">
{configuredMCPs.map((mcp, index) => (
<MCPConfigurationItem
key={index}
mcp={mcp}
index={index}
onEdit={onEdit}
onRemove={(idx) => handleDeleteClick(mcp, idx)}
onConfigureTools={onConfigureTools}
/>
))}
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove Integration</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to remove the "{mcpToDelete?.mcp.name}" integration? This will disconnect all associated tools and cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive hover:bg-destructive/90"
>
Remove Integration
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};

View File

@ -110,56 +110,46 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
};
return (
<div className="h-full flex flex-col">
<div className="flex-1 overflow-y-auto">
{configuredMCPs.length === 0 && (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4">
<Zap className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-medium text-foreground mb-2">
No integrations configured
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Browse the app registry to connect your apps through Composio or add custom MCP servers
</p>
<div className="flex gap-2 justify-center">
<Button onClick={() => setShowRegistryDialog(true)} variant="default">
<Store className="h-4 w-4" />
Browse Apps
</Button>
<Button onClick={() => setShowCustomDialog(true)} variant="outline">
<Server className="h-4 w-4" />
Custom MCP
</Button>
</div>
</div>
)}
{configuredMCPs.length > 0 && (
<div className="space-y-4">
<ConfiguredMcpList
configuredMCPs={configuredMCPs}
onEdit={handleEditMCP}
onRemove={handleRemoveMCP}
onConfigureTools={handleConfigureTools}
/>
</div>
)}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-foreground">Integrations</h3>
<p className="text-xs text-muted-foreground">Connect external services via MCPs</p>
</div>
<div className="flex gap-2">
<Button onClick={() => setShowRegistryDialog(true)} size="sm" variant="default" className="gap-2">
<Store className="h-4 w-4" />
Browse Apps
</Button>
<Button onClick={() => setShowCustomDialog(true)} size="sm" variant="outline" className="gap-2">
<Server className="h-4 w-4" />
Custom MCP
</Button>
</div>
</div>
{configuredMCPs.length > 0 && (
<div className="flex-shrink-0 pt-4">
<div className="flex gap-2 justify-center">
<Button onClick={() => setShowRegistryDialog(true)} variant="default">
<Store className="h-4 w-4" />
Browse Apps
</Button>
<Button onClick={() => setShowCustomDialog(true)} variant="outline">
<Server className="h-4 w-4" />
Custom MCP
</Button>
{configuredMCPs.length === 0 && (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4 border">
<Server className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground mb-2">
No integrations configured
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Browse the app registry to connect your apps through Composio or add custom MCP servers
</p>
</div>
)}
{configuredMCPs.length > 0 && (
<div className="space-y-4">
<ConfiguredMcpList
configuredMCPs={configuredMCPs}
onEdit={handleEditMCP}
onRemove={handleRemoveMCP}
onConfigureTools={handleConfigureTools}
/>
</div>
)}

View File

@ -59,47 +59,59 @@ export function AgentPlaybooksConfiguration({ agentId, agentName }: AgentPlayboo
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BookOpen className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium">Playbooks</div>
<div className="text-xs text-muted-foreground">Lightweight, variable-driven instructions stored via workflows</div>
</div>
<div>
<h3 className="text-sm font-medium text-foreground">Playbooks</h3>
<p className="text-xs text-muted-foreground">Simple variable-driven runs</p>
</div>
<Button onClick={() => { setEditing(null); setIsCreateOpen(true); }}>
<Plus className="h-4 w-4 mr-1" /> New Playbook
<Button onClick={() => { setEditing(null); setIsCreateOpen(true); }} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
New Playbook
</Button>
</div>
{isLoading ? (
<div className="text-sm text-muted-foreground">Loading...</div>
) : playbooks.length === 0 ? (
<Card className="p-6 text-center text-sm text-muted-foreground shadow-none">No playbooks yet.</Card>
<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 border">
<BookOpen className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground mb-2">
No playbooks yet
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Create your first playbook to automate common workflows with variable-driven runs
</p>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="space-y-2">
{playbooks.map((pb) => (
<Card key={pb.id} className="w-full p-4 shadow-none">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="font-medium truncate">{pb.name}</div>
{pb.description ? (
<div className="text-xs text-muted-foreground truncate">{pb.description}</div>
) : null}
<div key={pb.id} className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors group">
<div className="flex items-center space-x-4 flex-1">
<div className="p-2 rounded-lg bg-muted border">
<BookOpen className="h-4 w-4" />
</div>
<div className="flex items-center gap-1">
<Button size="icon" variant="outline" className="h-8 w-8" onClick={() => { setEditing(pb); setIsCreateOpen(true); }} aria-label="Edit playbook">
<Pencil className="h-4 w-4" />
</Button>
<Button size="icon" variant="destructive" className="h-8 w-8" onClick={() => handleDelete(pb)} aria-label="Delete playbook">
<Trash2 className="h-4 w-4" />
</Button>
<Button size="icon" className="h-8 w-8" onClick={() => setExecuting(pb)} aria-label="Run playbook">
<Play className="h-4 w-4" />
</Button>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-sm font-medium truncate">{pb.name}</h4>
</div>
{pb.description && (
<p className="text-xs text-muted-foreground truncate">{pb.description}</p>
)}
</div>
</div>
</Card>
<div className="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<Button size="sm" variant="ghost" className="h-8 w-8 p-0" onClick={() => setExecuting(pb)} aria-label="Run playbook">
<Play className="h-4 w-4" />
</Button>
<Button size="sm" variant="ghost" className="h-8 w-8 p-0" onClick={() => { setEditing(pb); setIsCreateOpen(true); }} aria-label="Edit playbook">
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="ghost" className="h-8 w-8 p-0 text-destructive hover:text-destructive" onClick={() => handleDelete(pb)} aria-label="Delete playbook">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}

View File

@ -127,35 +127,38 @@ export const AgentTriggersConfiguration: React.FC<AgentTriggersConfigurationProp
}
return (
<div className="h-full flex flex-col">
<div className="flex-1 overflow-y-auto space-y-4">
<OneClickIntegrations agentId={agentId} />
{triggers.length > 0 && (
<ConfiguredTriggersList
triggers={triggers}
onEdit={handleEditTrigger}
onRemove={handleRemoveTrigger}
onToggle={handleToggleTrigger}
isLoading={deleteTriggerMutation.isPending || toggleTriggerMutation.isPending}
/>
)}
{!isLoading && triggers.length === 0 && (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4 border">
<Zap className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground">
No triggers configured
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Click on a trigger provider above to get started
</p>
</div>
)}
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-foreground">Triggers</h3>
<p className="text-xs text-muted-foreground mb-4">Set up automated agent runs</p>
</div>
<OneClickIntegrations agentId={agentId} />
{triggers.length > 0 && (
<ConfiguredTriggersList
triggers={triggers}
onEdit={handleEditTrigger}
onRemove={handleRemoveTrigger}
onToggle={handleToggleTrigger}
isLoading={deleteTriggerMutation.isPending || toggleTriggerMutation.isPending}
/>
)}
{!isLoading && triggers.length === 0 && (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
<div className="mx-auto w-12 h-12 bg-muted rounded-full flex items-center justify-center mb-4 border">
<Zap className="h-6 w-6 text-muted-foreground" />
</div>
<h4 className="text-sm font-semibold text-foreground">
No triggers configured
</h4>
<p className="text-sm text-muted-foreground mb-6 max-w-sm mx-auto">
Click on a trigger provider above to get started
</p>
</div>
)}
{configuringProvider && (
<Dialog open={!!configuringProvider} onOpenChange={() => setConfiguringProvider(null)}>
<TriggerConfigDialog

View File

@ -19,6 +19,16 @@ import {
} from 'lucide-react';
import { TriggerConfiguration } from './types';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { getTriggerIcon } from './utils';
import { truncateString } from '@/lib/utils';
@ -65,6 +75,22 @@ export const ConfiguredTriggersList: React.FC<ConfiguredTriggersListProps> = ({
onToggle,
isLoading = false,
}) => {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [triggerToDelete, setTriggerToDelete] = React.useState<TriggerConfiguration | null>(null);
const handleDeleteClick = (trigger: TriggerConfiguration) => {
setTriggerToDelete(trigger);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (triggerToDelete) {
onRemove(triggerToDelete);
setTriggerToDelete(null);
setDeleteDialogOpen(false);
}
};
return (
<TooltipProvider>
<div className="space-y-2">
@ -147,7 +173,7 @@ export const ConfiguredTriggersList: React.FC<ConfiguredTriggersListProps> = ({
)}
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 flex-shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
@ -185,7 +211,7 @@ export const ConfiguredTriggersList: React.FC<ConfiguredTriggersListProps> = ({
<Button
size="sm"
variant="ghost"
onClick={() => onRemove(trigger)}
onClick={() => handleDeleteClick(trigger)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled={isLoading}
>
@ -200,6 +226,26 @@ export const ConfiguredTriggersList: React.FC<ConfiguredTriggersListProps> = ({
</div>
))}
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Trigger</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{triggerToDelete?.name}"? This action cannot be undone and will stop all automated runs from this trigger.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive hover:bg-destructive/90"
>
Delete Trigger
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TooltipProvider>
);
};