Merge pull request #1779 from escapade-mckv/improve-installation-experience

add agent usage examples to preview dialog
This commit is contained in:
Bobbie 2025-10-06 16:46:58 +05:30 committed by GitHub
commit dd5b6c4eb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 623 additions and 250 deletions

View File

@ -28,10 +28,17 @@ router = APIRouter(tags=["templates"])
db: Optional[DBConnection] = None
class UsageExampleMessage(BaseModel):
role: str
content: str
tool_calls: Optional[List[Dict[str, Any]]] = None
class CreateTemplateRequest(BaseModel):
agent_id: str
make_public: bool = False
tags: Optional[List[str]] = None
usage_examples: Optional[List[UsageExampleMessage]] = None
class InstallTemplateRequest(BaseModel):
@ -44,6 +51,7 @@ class InstallTemplateRequest(BaseModel):
class PublishTemplateRequest(BaseModel):
tags: Optional[List[str]] = None
usage_examples: Optional[List[UsageExampleMessage]] = None
class TemplateResponse(BaseModel):
@ -65,6 +73,7 @@ class TemplateResponse(BaseModel):
icon_background: Optional[str] = None
metadata: Dict[str, Any]
creator_name: Optional[str] = None
usage_examples: Optional[List[UsageExampleMessage]] = None
class InstallationResponse(BaseModel):
@ -138,11 +147,16 @@ async def create_template_from_agent(
template_service = get_template_service(db)
usage_examples = None
if request.usage_examples:
usage_examples = [msg.dict() for msg in request.usage_examples]
template_id = await template_service.create_from_agent(
agent_id=request.agent_id,
creator_id=user_id,
make_public=request.make_public,
tags=request.tags
tags=request.tags,
usage_examples=usage_examples
)
logger.debug(f"Successfully created template {template_id} from agent {request.agent_id}")
@ -181,7 +195,15 @@ async def publish_template(
template_service = get_template_service(db)
success = await template_service.publish_template(template_id, user_id)
usage_examples = None
if request.usage_examples:
usage_examples = [msg.dict() for msg in request.usage_examples]
success = await template_service.publish_template(
template_id,
user_id,
usage_examples=usage_examples
)
if not success:
logger.warning(f"Failed to publish template {template_id} for user {user_id}")

View File

@ -48,6 +48,7 @@ class AgentTemplate:
icon_background: Optional[str] = None
metadata: ConfigType = field(default_factory=dict)
creator_name: Optional[str] = None
usage_examples: List[Dict[str, Any]] = field(default_factory=list)
def with_public_status(self, is_public: bool, published_at: Optional[datetime] = None) -> 'AgentTemplate':
return AgentTemplate(
@ -180,7 +181,8 @@ class TemplateService:
agent_id: str,
creator_id: str,
make_public: bool = False,
tags: Optional[List[str]] = None
tags: Optional[List[str]] = None,
usage_examples: Optional[List[Dict[str, Any]]] = None
) -> str:
logger.debug(f"Creating template from agent {agent_id} for user {creator_id}")
@ -211,7 +213,8 @@ class TemplateService:
icon_name=agent.get('icon_name'),
icon_color=agent.get('icon_color'),
icon_background=agent.get('icon_background'),
metadata=agent.get('metadata', {})
metadata=agent.get('metadata', {}),
usage_examples=usage_examples or []
)
await self._save_template(template)
@ -346,15 +349,26 @@ class TemplateService:
return templates
async def publish_template(self, template_id: str, creator_id: str) -> bool:
async def publish_template(
self,
template_id: str,
creator_id: str,
usage_examples: Optional[List[Dict[str, Any]]] = None
) -> bool:
logger.debug(f"Publishing template {template_id}")
client = await self._db.client
result = await client.table('agent_templates').update({
update_data = {
'is_public': True,
'marketplace_published_at': datetime.now(timezone.utc).isoformat(),
'updated_at': datetime.now(timezone.utc).isoformat()
}).eq('template_id', template_id)\
}
if usage_examples is not None:
update_data['usage_examples'] = usage_examples
result = await client.table('agent_templates').update(update_data)\
.eq('template_id', template_id)\
.eq('creator_id', creator_id)\
.execute()
@ -579,7 +593,8 @@ class TemplateService:
'icon_name': template.icon_name,
'icon_color': template.icon_color,
'icon_background': template.icon_background,
'metadata': template.metadata
'metadata': template.metadata,
'usage_examples': template.usage_examples
}
await client.table('agent_templates').insert(template_data).execute()
@ -587,6 +602,10 @@ class TemplateService:
def _map_to_template(self, data: Dict[str, Any]) -> AgentTemplate:
creator_name = data.get('creator_name')
usage_examples = data.get('usage_examples', [])
logger.debug(f"Mapping template {data.get('template_id')}: usage_examples from DB = {usage_examples}")
logger.debug(f"Raw data keys: {list(data.keys())}")
return AgentTemplate(
template_id=data['template_id'],
creator_id=data['creator_id'],
@ -603,7 +622,8 @@ class TemplateService:
icon_color=data.get('icon_color'),
icon_background=data.get('icon_background'),
metadata=data.get('metadata', {}),
creator_name=creator_name
creator_name=creator_name,
usage_examples=usage_examples
)
# Share link functionality removed - now using direct template ID URLs for simplicity

View File

@ -110,6 +110,10 @@ def is_suna_default_agent(agent_data: Dict[str, Any]) -> bool:
def format_template_for_response(template: AgentTemplate) -> Dict[str, Any]:
from core.utils.logger import logger
logger.debug(f"Formatting template {template.template_id}: usage_examples = {template.usage_examples}")
response = {
'template_id': template.template_id,
'creator_id': template.creator_id,
@ -129,8 +133,12 @@ def format_template_for_response(template: AgentTemplate) -> Dict[str, Any]:
'icon_color': template.icon_color,
'icon_background': template.icon_background,
'metadata': template.metadata,
'creator_name': template.creator_name
'creator_name': template.creator_name,
'usage_examples': template.usage_examples
}
logger.debug(f"Response for {template.template_id} includes usage_examples: {response.get('usage_examples')}")
return response

View File

@ -0,0 +1,12 @@
BEGIN;
ALTER TABLE agent_templates
ADD COLUMN IF NOT EXISTS usage_examples JSONB DEFAULT '[]'::jsonb;
CREATE INDEX IF NOT EXISTS idx_agent_templates_usage_examples
ON agent_templates USING gin(usage_examples);
COMMENT ON COLUMN agent_templates.usage_examples IS
'Example conversation demonstrating agent usage. Array of message objects with role (user/assistant), content, and optional tool_calls array';
COMMIT;

View File

@ -0,0 +1,12 @@
BEGIN;
ALTER TABLE agent_templates
ADD COLUMN IF NOT EXISTS usage_examples JSONB DEFAULT '[]'::jsonb;
CREATE INDEX IF NOT EXISTS idx_agent_templates_usage_examples
ON agent_templates USING gin(usage_examples);
COMMENT ON COLUMN agent_templates.usage_examples IS
'Example conversation demonstrating agent usage. Array of message objects with role (user/assistant), content, and optional tool_calls array';
COMMIT;

View File

@ -197,6 +197,7 @@ export default function AgentsPage() {
creator_id: template.creator_id,
name: template.name,
description: template.description,
system_prompt: template.system_prompt,
tags: template.tags || [],
download_count: template.download_count || 0,
creator_name: template.creator_name || 'Anonymous',
@ -209,6 +210,7 @@ export default function AgentsPage() {
is_kortix_team: template.is_kortix_team,
mcp_requirements: template.mcp_requirements,
metadata: template.metadata,
usage_examples: template.usage_examples,
};
items.push(item);
@ -506,7 +508,7 @@ export default function AgentsPage() {
});
};
const handlePublish = async () => {
const handlePublish = async (usageExamples: any[]) => {
if (!publishDialog) return;
try {
@ -517,7 +519,8 @@ export default function AgentsPage() {
const result = await createTemplateMutation.mutateAsync({
agent_id: publishDialog.templateId,
make_public: true
make_public: true,
usage_examples: usageExamples
});
toast.success(`${publishDialog.templateName} has been published to the marketplace`);
@ -525,7 +528,8 @@ export default function AgentsPage() {
setTemplatesActioningId(publishDialog.templateId);
await publishMutation.mutateAsync({
template_id: publishDialog.templateId
template_id: publishDialog.templateId,
usage_examples: usageExamples
});
toast.success(`${publishDialog.templateName} has been published to the marketplace`);

View File

@ -59,6 +59,14 @@ interface MarketplaceTemplate {
icon_background: string | null;
metadata: Record<string, any>;
creator_name: string | null;
usage_examples?: Array<{
role: string;
content: string;
tool_calls?: Array<{
name: string;
arguments?: Record<string, any>;
}>;
}>;
}
const IntegrationIcon: React.FC<{

View File

@ -1,10 +1,14 @@
'use client';
import React from 'react';
import { Globe, Loader2, AlertTriangle } from 'lucide-react';
import React, { useState } from 'react';
import { Globe, Loader2, Plus, Trash2, User, Bot, Wrench } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { UsageExampleMessage } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
interface PublishDialogData {
templateId: string;
@ -15,7 +19,7 @@ interface PublishDialogProps {
publishDialog: PublishDialogData | null;
templatesActioningId: string | null;
onClose: () => void;
onPublish: () => void;
onPublish: (usageExamples: UsageExampleMessage[]) => void;
}
export const PublishDialog = ({
@ -24,25 +28,197 @@ export const PublishDialog = ({
onClose,
onPublish
}: PublishDialogProps) => {
const [examples, setExamples] = useState<UsageExampleMessage[]>([]);
const handleAddMessage = () => {
setExamples([...examples, { role: 'user', content: '' }]);
};
const handleRemoveMessage = (index: number) => {
setExamples(examples.filter((_, i) => i !== index));
};
const handleUpdateMessage = (index: number, field: keyof UsageExampleMessage, value: any) => {
const updated = [...examples];
updated[index] = { ...updated[index], [field]: value };
setExamples(updated);
};
const handleAddToolCall = (messageIndex: number) => {
const updated = [...examples];
if (!updated[messageIndex].tool_calls) {
updated[messageIndex].tool_calls = [];
}
updated[messageIndex].tool_calls!.push({ name: '', arguments: {} });
setExamples(updated);
};
const handleRemoveToolCall = (messageIndex: number, toolIndex: number) => {
const updated = [...examples];
updated[messageIndex].tool_calls = updated[messageIndex].tool_calls?.filter((_, i) => i !== toolIndex);
setExamples(updated);
};
const handleUpdateToolCall = (messageIndex: number, toolIndex: number, field: string, value: any) => {
const updated = [...examples];
if (updated[messageIndex].tool_calls) {
updated[messageIndex].tool_calls![toolIndex] = {
...updated[messageIndex].tool_calls![toolIndex],
[field]: value
};
}
setExamples(updated);
};
const handlePublish = () => {
const validExamples = examples.filter(ex => ex.content.trim());
onPublish(validExamples);
setExamples([]);
};
const handleClose = () => {
setExamples([]);
onClose();
};
return (
<Dialog open={!!publishDialog} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<Dialog open={!!publishDialog} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Publish Template to Marketplace</DialogTitle>
<DialogDescription>
Make "{publishDialog?.templateName}" available for the community to discover and install.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-base font-semibold">Usage Examples (Optional)</Label>
<p className="text-xs text-muted-foreground mt-1">
Add example messages to help users understand how your agent works
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAddMessage}
className="gap-2"
>
<Plus className="h-4 w-4" />
Add Message
</Button>
</div>
{examples.length === 0 && (
<div className="text-center py-8 px-4 border-2 border-dashed rounded-lg">
<p className="text-sm text-muted-foreground">
No example messages yet. Click "Add Message" to create an example conversation.
</p>
</div>
)}
<div className="space-y-3">
{examples.map((message, index) => (
<div
key={index}
className="border rounded-lg p-4 space-y-3"
>
<div className="flex items-start gap-3">
<div className="flex-1 space-y-3">
<div className="flex items-center gap-3">
<Select
value={message.role}
onValueChange={(value) => handleUpdateMessage(index, 'role', value)}
>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
<span>User</span>
</div>
</SelectItem>
<SelectItem value="assistant">
<div className="flex items-center gap-2">
<Bot className="h-4 w-4" />
<span>Assistant</span>
</div>
</SelectItem>
</SelectContent>
</Select>
{message.role === 'assistant' && (
<Button
variant="outline"
size="sm"
onClick={() => handleAddToolCall(index)}
className="gap-2"
>
<Wrench className="h-3 w-3" />
Add Tool Call
</Button>
)}
</div>
<Textarea
placeholder="Message content..."
value={message.content}
onChange={(e) => handleUpdateMessage(index, 'content', e.target.value)}
rows={3}
/>
{message.tool_calls && message.tool_calls.length > 0 && (
<div className="space-y-2 pl-4 border-l-2">
<Label className="text-xs text-muted-foreground">Tool Calls</Label>
{message.tool_calls.map((toolCall, toolIndex) => (
<div key={toolIndex} className="flex items-center gap-2">
<Input
placeholder="Tool name"
value={toolCall.name}
onChange={(e) => handleUpdateToolCall(index, toolIndex, 'name', e.target.value)}
className="flex-1"
/>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveToolCall(index, toolIndex)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveMessage(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={onClose}
onClick={handleClose}
disabled={!!templatesActioningId}
>
Cancel
</Button>
<Button
onClick={onPublish}
onClick={handlePublish}
disabled={!!templatesActioningId}
>
{templatesActioningId ? (
@ -61,4 +237,4 @@ export const PublishDialog = ({
</DialogContent>
</Dialog>
);
};
};

View File

@ -1,8 +1,18 @@
export interface UsageExampleMessage {
role: 'user' | 'assistant';
content: string;
tool_calls?: Array<{
name: string;
arguments?: Record<string, any>;
}>;
}
export interface MarketplaceTemplate {
id: string;
creator_id: string;
name: string;
description: string;
system_prompt?: string;
tags: string[];
download_count: number;
creator_name: string;
@ -26,6 +36,7 @@ export interface MarketplaceTemplate {
source?: 'trigger' | 'tool';
trigger_index?: number;
}>;
usage_examples?: UsageExampleMessage[];
metadata?: {
source_agent_id?: string;
source_version_id?: string;

View File

@ -5,14 +5,16 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Bot, Download, Wrench, Plug, Tag, User, Calendar, Loader2, Share, Cpu, Eye, Zap } from 'lucide-react';
import { Bot, Download, Wrench, Plug, Tag, User, Calendar, Loader2, Share, Cpu, Eye, Zap, MessageSquare, ArrowRight, Sparkles, FileText } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
import { toast } from 'sonner';
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
import type { MarketplaceTemplate, UsageExampleMessage } from '@/components/agents/installation/types';
import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
import { useRouter } from 'next/navigation';
import { backendApi } from '@/lib/api-client';
import { AgentAvatar } from '@/components/thread/content/agent-avatar';
import { useTheme } from 'next-themes';
import { Markdown } from '@/components/ui/markdown';
interface MarketplaceAgentPreviewDialogProps {
agent: MarketplaceTemplate | null;
@ -47,7 +49,8 @@ const IntegrationLogo: React.FC<{
displayName: string;
customType?: string;
toolkitSlug?: string;
}> = ({ qualifiedName, displayName, customType, toolkitSlug }) => {
size?: 'sm' | 'md' | 'lg';
}> = ({ qualifiedName, displayName, customType, toolkitSlug, size = 'sm' }) => {
let appInfo = extractAppInfo(qualifiedName, customType);
if (!appInfo && toolkitSlug) {
@ -66,8 +69,14 @@ const IntegrationLogo: React.FC<{
const firstLetter = displayName.charAt(0).toUpperCase();
const sizeClasses = {
sm: 'w-5 h-5',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
return (
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0 overflow-hidden rounded-sm">
<div className={`${sizeClasses[size]} flex items-center justify-center flex-shrink-0 overflow-hidden rounded-md`}>
{logoUrl ? (
<img
src={logoUrl}
@ -80,7 +89,7 @@ const IntegrationLogo: React.FC<{
}}
/>
) : null}
<div className={logoUrl ? "hidden" : "flex w-full h-full items-center justify-center bg-muted rounded-sm text-xs font-medium text-muted-foreground"}>
<div className={logoUrl ? "hidden" : "flex w-full h-full items-center justify-center bg-muted rounded-md text-xs font-medium text-muted-foreground"}>
{firstLetter}
</div>
</div>
@ -95,6 +104,7 @@ export const MarketplaceAgentPreviewDialog: React.FC<MarketplaceAgentPreviewDial
isInstalling = false
}) => {
const router = useRouter();
const { theme } = useTheme();
const [isGeneratingShareLink, setIsGeneratingShareLink] = React.useState(false);
if (!agent) return null;
@ -149,214 +159,248 @@ export const MarketplaceAgentPreviewDialog: React.FC<MarketplaceAgentPreviewDial
return qualifiedName.replace(/\b\w/g, l => l.toUpperCase());
};
console.log('agent', agent);
const hasUsageExamples = agent.usage_examples && agent.usage_examples.length > 0;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-lg max-h-[90vh] p-0 overflow-hidden">
<DialogHeader className='p-6'>
<DialogTitle className='sr-only'>Agent Preview</DialogTitle>
<div className="flex-shrink-0">
<AgentAvatar
iconName={agent.icon_name}
iconColor={agent.icon_color}
backgroundColor={agent.icon_background}
agentName={agent.name}
size={80}
/>
</div>
<DialogContent className={`${hasUsageExamples ? 'max-w-6xl' : 'max-w-2xl'} h-[85vh] p-0 overflow-hidden flex flex-col`}>
<DialogHeader className='sr-only'>
<DialogTitle>Agent Preview</DialogTitle>
</DialogHeader>
<div className="-mt-4 flex flex-col max-h-[calc(90vh-8rem)] overflow-hidden">
<div className="p-6 py-0 pb-2">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<h2 className="text-2xl font-bold text-foreground mb-2">
{agent.name}
</h2>
{/* <div className="flex items-center gap-4 text-sm text-muted-foreground mb-3">
<div className="flex items-center gap-1">
<User className="h-4 w-4" />
{agent.creator_name || 'Unknown'}
</div>
<div className="flex items-center gap-1">
<Download className="h-4 w-4" />
{agent.download_count} downloads
</div>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{formatDate(agent.created_at)}
</div>
</div> */}
<div className={`flex ${hasUsageExamples ? 'flex-row' : 'flex-col'} flex-1 min-h-0`}>
<div className={`${hasUsageExamples ? 'w-1/2' : 'w-full'} flex flex-col min-h-0`}>
<div className="flex-shrink-0 p-8 pb-6">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 relative">
<AgentAvatar
iconName={agent.icon_name}
iconColor={agent.icon_color}
backgroundColor={agent.icon_background}
agentName={agent.name}
size={56}
/>
<div
className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-30 h-10 blur-2xl opacity-100"
style={{
background: `radial-gradient(ellipse at center, ${agent.icon_color}, transparent)`
}}
/>
</div>
<div className="flex-1 min-w-0">
<h1 className="text-2xl font-semibold text-foreground mb-2">
{agent.name}
</h1>
</div>
</div>
</div>
{agent.tags && agent.tags.length > 0 && (
<div className="flex items-center gap-2 mb-2">
<Tag className="h-4 w-4 text-muted-foreground" />
<div className="flex flex-wrap gap-1">
{agent.tags.map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{agent.system_prompt && (
<div className="flex-1 overflow-y-auto px-8 min-h-0 scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-muted-foreground/10">
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<FileText className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-muted-foreground">System Prompt</h3>
</div>
<div className="rounded-lg">
<Markdown className="text-xs [&>*]:text-xs [&>*]:opacity-50 [&>*]:leading-relaxed select-text">
{agent.system_prompt}
</Markdown>
</div>
</div>
</div>
)}
</div>
<div className="flex-1 gap-4 overflow-y-auto p-6 pt-2 space-y-3">
{agent.model && (
<Card className='p-0 border-none bg-transparent shadow-none'>
<CardContent className="p-0">
<div className="flex items-center gap-2 mb-2">
<Cpu className="h-4 w-4 text-primary" />
<h3 className="font-semibold">Model Configuration</h3>
</div>
<div className="flex flex-wrap gap-2">
<Badge
variant="secondary"
className="flex items-center px-3 py-1.5 bg-muted/50 hover:bg-muted border"
>
<span className="text-sm font-medium">
{agent.model.replace('openrouter/', '').replace('anthropic/', '')}
</span>
</Badge>
</div>
</CardContent>
</Card>
)}
{integrations.length > 0 && (
<Card className='p-0 border-none bg-transparent shadow-none'>
<CardContent className="p-0">
<div className="flex items-center gap-2 mb-2">
<Plug className="h-4 w-4 text-primary" />
<h3 className="font-semibold">Integrations</h3>
</div>
<div className="flex flex-wrap gap-2">
{integrations.map((integration, index) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1.5 px-3 py-1.5 bg-muted/50 hover:bg-muted border"
>
<IntegrationLogo
qualifiedName={integration.qualified_name}
displayName={integration.display_name || getAppDisplayName(integration.qualified_name)}
customType={integration.custom_type}
toolkitSlug={integration.toolkit_slug}
/>
<span className="text-sm font-medium ml-1">
{integration.display_name || getAppDisplayName(integration.qualified_name)}
</span>
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{triggerRequirements.length > 0 && (
<Card className='p-0 border-none bg-transparent shadow-none'>
<CardContent className="p-0">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4 text-primary" />
<h3 className="font-semibold">Event Triggers</h3>
</div>
<div className="flex flex-wrap gap-2">
{triggerRequirements.map((trigger, index) => {
const appName = trigger.display_name?.split(' (')[0] || trigger.display_name;
const triggerName = trigger.display_name?.match(/\(([^)]+)\)/)?.[1] || trigger.display_name;
return (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1.5 px-3 py-1.5 bg-muted/50 hover:bg-muted border"
<div className="flex-shrink-0 bg-background p-6 space-y-6">
{(integrations.length > 0 || customTools.length > 0 || triggerRequirements.length > 0) && (
<div>
<div className="flex items-center gap-2 mb-3">
<Plug className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-muted-foreground">Integrations</h3>
</div>
<div className="flex flex-wrap gap-3">
{integrations.map((integration, index) => (
<div
key={`int-${index}`}
className="flex items-center gap-2 p-2 rounded-lg border bg-card"
>
<div className="flex items-center gap-1">
<IntegrationLogo
qualifiedName={integration.qualified_name}
displayName={integration.display_name || getAppDisplayName(integration.qualified_name)}
customType={integration.custom_type}
toolkitSlug={integration.toolkit_slug}
size="sm"
/>
<span className="text-sm font-medium pr-2">
{integration.display_name || getAppDisplayName(integration.qualified_name)}
</span>
</div>
))}
{triggerRequirements.map((trigger, index) => {
const appName = trigger.display_name?.split(' (')[0] || trigger.display_name;
const triggerName = trigger.display_name?.match(/\(([^)]+)\)/)?.[1] || trigger.display_name;
return (
<div
key={`trig-${index}`}
className="flex items-center gap-2 p-2 rounded-lg border bg-card"
>
<IntegrationLogo
qualifiedName={trigger.qualified_name}
displayName={appName || getAppDisplayName(trigger.qualified_name)}
customType={trigger.custom_type || (trigger.qualified_name?.startsWith('composio.') ? 'composio' : undefined)}
toolkitSlug={trigger.toolkit_slug}
size="sm"
/>
<span className="text-sm font-medium ml-1">
{triggerName || trigger.display_name}
</span>
<div className="flex items-center gap-1">
<Zap className="h-3 w-3 text-muted-foreground" />
<span className="text-sm font-medium pr-2">
{triggerName || trigger.display_name}
</span>
</div>
</div>
</Badge>
);
})}
);
})}
{customTools.map((tool, index) => (
<div
key={`tool-${index}`}
className="flex items-center gap-2 p-2 rounded-lg border bg-card"
>
<IntegrationLogo
qualifiedName={tool.qualified_name}
displayName={tool.display_name || getAppDisplayName(tool.qualified_name)}
customType={tool.custom_type}
toolkitSlug={tool.toolkit_slug}
size="md"
/>
<span className="text-sm font-medium pr-2">
{tool.display_name || getAppDisplayName(tool.qualified_name)}
</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
)}
{customTools.length > 0 && (
<Card className='p-0 border-none bg-transparent shadow-none'>
<CardContent className="p-0">
<div className="flex items-center gap-2 mb-2">
<Wrench className="h-4 w-4 text-primary" />
<h3 className="font-semibold">Custom Tools</h3>
</div>
<div className="flex flex-wrap gap-2">
{customTools.map((tool, index) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1.5 px-3 py-1.5 bg-muted/50 hover:bg-muted border"
>
<IntegrationLogo
qualifiedName={tool.qualified_name}
displayName={tool.display_name || getAppDisplayName(tool.qualified_name)}
customType={tool.custom_type}
toolkitSlug={tool.toolkit_slug}
/>
<span className="text-sm font-medium ml-1">
{tool.display_name || getAppDisplayName(tool.qualified_name)}
</span>
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{agentpressTools.length === 0 && toolRequirements.length === 0 && triggerRequirements.length === 0 && (
<Card>
<CardContent className="p-4 text-center">
<Bot className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
)}
{agentpressTools.length === 0 && toolRequirements.length === 0 && triggerRequirements.length === 0 && (
<div className="rounded-lg border bg-muted/20 p-6 text-center">
<Sparkles className="h-8 w-8 text-muted-foreground mx-auto mb-3" />
<p className="text-muted-foreground">
This agent uses basic functionality without external integrations or specialized tools.
This agent operates with core AI capabilities without external integrations
</p>
</CardContent>
</Card>
)}
</div>
<div className="p-6 pt-3">
<div className="flex gap-3">
</div>
)}
<Button
onClick={handleInstall}
disabled={isInstalling}
className="flex-1"
className="w-full"
>
{isInstalling ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="h-5 w-5 animate-spin" />
Installing...
</>
) : (
<>
<Download className="h-4 w-4" />
<Download className="h-5 w-5" />
Install Agent
</>
)}
</Button>
{/* <Button variant="outline" onClick={handleShare} disabled={isGeneratingShareLink}>
{isGeneratingShareLink ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Share className="h-4 w-4" />
)}
Share
</Button> */}
</div>
</div>
{hasUsageExamples && (
<div className="w-1/2 flex flex-col p-4 overflow-hidden">
<div className="px-0 py-4 flex-shrink-0">
<div className="flex items-center gap-2 mb-1">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-muted-foreground">Usage</h3>
</div>
</div>
<div className="bg-white dark:bg-black p-3 rounded-xl flex-1 overflow-y-auto space-y-4 min-h-0">
<div
className="p-3 rounded-xl flex-1 overflow-y-auto space-y-4 h-full"
style={{
background: theme === 'dark'
? `linear-gradient(to bottom right, ${agent.icon_background}14, ${agent.icon_background}0A)`
: `linear-gradient(to bottom right, ${agent.icon_background}33, ${agent.icon_background}60)`
}}
>
{(() => {
const messages = agent.usage_examples || [];
const groupedMessages: Array<{ role: string; messages: UsageExampleMessage[] }> = [];
messages.forEach((message) => {
const lastGroup = groupedMessages[groupedMessages.length - 1];
if (lastGroup && lastGroup.role === message.role) {
lastGroup.messages.push(message);
} else {
groupedMessages.push({ role: message.role, messages: [message] });
}
});
return groupedMessages.map((group, groupIndex) => {
const isUser = group.role === 'user';
return (
<div key={groupIndex} className='flex'>
<div className={`group relative max-w-[85%]`}>
<div className="flex items-start gap-3">
{!isUser && (
<div className="flex-shrink-0 mt-1">
<AgentAvatar
iconName={agent.icon_name}
iconColor={agent.icon_color}
backgroundColor={agent.icon_background}
agentName={agent.name}
size={32}
/>
</div>
)}
{isUser && (
<div className="flex-shrink-0 mt-1">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<User className="h-4 w-4 text-muted" />
</div>
</div>
)}
<div className='rounded-xl space-y-3'>
{group.messages.map((message, msgIndex) => (
<div key={msgIndex}>
<p className="text-sm mt-1 font-medium leading-relaxed whitespace-pre-wrap">{message.content}</p>
{message.tool_calls && message.tool_calls.length > 0 && (
<div className="mt-2 space-y-1.5">
{message.tool_calls.map((tool, toolIndex) => (
<div key={toolIndex} className="flex items-center gap-2 p-1 bg-white dark:bg-muted border rounded-md">
<div className="w-6 h-6 rounded-md bg-gradient-to-br from-muted-foreground/10 to-muted-foreground/5 border-2 flex items-center justify-center">
<Wrench className="h-3 w-3" />
</div>
<span className="text-xs">{tool.name}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
);
});
})()}
</div>
{!agent.usage_examples && (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-3">
<MessageSquare className="h-12 w-12 text-muted-foreground/30 mx-auto" />
<p className="text-sm text-muted-foreground">No example conversations available</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>

View File

@ -320,11 +320,21 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
const [showComposioConnector, setShowComposioConnector] = useState(false);
const { data: appsData, isLoading: loadingApps } = useComposioAppsWithTriggers();
const { data: triggersData, isLoading: loadingTriggers } = useComposioAppTriggers(selectedApp?.slug, !!selectedApp);
const { data: triggersData, isLoading: loadingTriggers, error: triggersError } = useComposioAppTriggers(selectedApp?.slug, !!selectedApp);
const { data: profiles, isLoading: loadingProfiles, refetch: refetchProfiles } = useComposioProfiles(selectedApp?.slug ? { toolkit_slug: selectedApp.slug } : undefined);
const { data: allProfiles } = useComposioProfiles(); // Get all profiles for connection status
const { data: toolkitDetails } = useComposioToolkitDetails(selectedApp?.slug || '', { enabled: !!selectedApp });
// Debug logging for trigger data
useEffect(() => {
if (isEditMode) {
console.log('Edit mode - selectedApp changed:', selectedApp);
console.log('Edit mode - loadingTriggers:', loadingTriggers);
console.log('Edit mode - triggersData:', triggersData);
console.log('Edit mode - triggersError:', triggersError);
}
}, [isEditMode, selectedApp, loadingTriggers, triggersData, triggersError]);
const apps = useMemo(() => (appsData?.items || []).filter((a) => a.name.toLowerCase().includes(search.toLowerCase()) || a.slug.toLowerCase().includes(search.toLowerCase())), [appsData, search]);
// Helper function to check if an app has connected profiles
@ -375,61 +385,74 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
}
}, [profiles, profileId]);
// Initialize form for edit mode
useEffect(() => {
if (isEditMode && existingTrigger && open) {
console.log('Edit mode - existingTrigger:', existingTrigger);
const triggerConfig = existingTrigger.config || {};
console.log('Edit mode - triggerConfig:', triggerConfig);
// Set basic info
setName(existingTrigger.name || '');
setPrompt(triggerConfig.agent_prompt || '');
setProfileId(triggerConfig.profile_id || '');
// Set trigger config (excluding execution-specific fields)
const { agent_prompt, profile_id, ...triggerSpecificConfig } = triggerConfig;
const { agent_prompt, profile_id, provider_id, trigger_slug, qualified_name, ...triggerSpecificConfig } = triggerConfig;
setConfig(triggerSpecificConfig);
// For composio triggers, we need to reconstruct the app and trigger selection
if (triggerConfig.provider_id === 'composio' && triggerConfig.trigger_slug) {
console.log('Edit mode - setting up composio trigger for:', triggerConfig.qualified_name, triggerConfig.trigger_slug);
// Extract toolkit slug from qualified_name (e.g., "composio.googledocs" -> "googledocs")
const toolkitSlug = triggerConfig.qualified_name?.replace('composio.', '') || '';
const isComposioTrigger = triggerConfig.provider_id === 'composio' || existingTrigger.provider_id === 'composio';
if (isComposioTrigger && triggerConfig.trigger_slug) {
let toolkitSlug = '';
if (triggerConfig.qualified_name) {
toolkitSlug = triggerConfig.qualified_name.replace(/^composio\./, '').toLowerCase();
console.log('Edit mode - extracted from qualified_name:', toolkitSlug);
}
if (!toolkitSlug && triggerConfig.trigger_slug) {
const slugParts = triggerConfig.trigger_slug.toLowerCase().split('_');
if (slugParts.length > 0) {
toolkitSlug = slugParts[0];
}
}
if (toolkitSlug) {
// Create app object to trigger the API call
const app = {
slug: toolkitSlug,
name: toolkitSlug,
logo: undefined
};
setSelectedApp(app);
}
}
}
}, [isEditMode, existingTrigger, open]);
// Find the matching trigger for edit mode once triggers are fetched
useEffect(() => {
if (isEditMode && existingTrigger && selectedApp && triggersData?.items) {
const triggerConfig = existingTrigger.config || {};
console.log('Edit mode - looking for trigger with slug:', triggerConfig.trigger_slug);
console.log('Edit mode - available triggers:', triggersData.items.map(t => t.slug));
if (triggerConfig.trigger_slug) {
// Find the matching trigger from the fetched data
const matchingTrigger = triggersData.items.find(t => t.slug === triggerConfig.trigger_slug);
if (matchingTrigger) {
console.log('Edit mode - found matching trigger:', matchingTrigger);
setSelectedTrigger(matchingTrigger);
} else {
console.log('Edit mode - no matching trigger found for slug:', triggerConfig.trigger_slug);
toast.error('Could not determine the app for this trigger');
onOpenChange(false);
}
} else if (isComposioTrigger && !triggerConfig.trigger_slug) {
toast.error('Invalid trigger configuration: missing trigger information');
onOpenChange(false);
}
}
}, [isEditMode, existingTrigger, open, onOpenChange]);
useEffect(() => {
if (isEditMode && existingTrigger && selectedApp) {
const triggerConfig = existingTrigger.config || {};
if (triggersData !== undefined) {
if (triggersData?.items && triggersData.items.length > 0) {
if (triggerConfig.trigger_slug) {
const searchSlug = triggerConfig.trigger_slug.toLowerCase();
const matchingTrigger = triggersData.items.find(t => t.slug.toLowerCase() === searchSlug);
if (matchingTrigger) {
setSelectedTrigger(matchingTrigger);
} else {
toast.error('Could not find the trigger type. It may have been removed or renamed.');
setTimeout(() => onOpenChange(false), 2000);
}
} else {
toast.error('Invalid trigger configuration: missing trigger_slug');
setTimeout(() => onOpenChange(false), 2000);
}
} else {
toast.error('No triggers available for this app');
setTimeout(() => onOpenChange(false), 2000);
}
}
}
}, [isEditMode, existingTrigger, selectedApp, triggersData]);
}, [isEditMode, existingTrigger, selectedApp, triggersData, onOpenChange]);
const isConfigValid = useMemo(() => {
@ -493,17 +516,7 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
onOpenChange(false);
} catch (e: any) {
// Handle nested error structure from API
let errorMessage = isEditMode ? 'Failed to update trigger' : 'Failed to create trigger';
console.error('Error creating trigger:', e);
console.error('Error details:', e?.details);
console.error('Error keys:', Object.keys(e || {}));
if (e?.details) {
console.error('Details keys:', Object.keys(e.details));
console.error('Details content:', JSON.stringify(e.details, null, 2));
}
// Check for details property from api-client.ts error structure
if (e?.details?.detail?.error?.message) {
errorMessage = e.details.detail.error.message;
} else if (e?.details?.message) {
@ -679,11 +692,32 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
{step === 'config' && (
<div className="h-full flex flex-col">
{/* Loading state for edit mode while waiting for trigger data */}
{isEditMode && !selectedTrigger ? (
{isEditMode && !selectedTrigger && (loadingTriggers || !selectedApp) ? (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center space-y-3">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
<p className="text-sm text-muted-foreground">Loading trigger configuration...</p>
{triggersError && (
<p className="text-xs text-destructive">Error: {String(triggersError)}</p>
)}
</div>
</div>
) : isEditMode && !selectedTrigger && !loadingTriggers ? (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center space-y-3">
<div className="w-12 h-12 rounded-lg bg-muted flex items-center justify-center mb-3 mx-auto">
<Info className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="font-medium">Unable to load trigger</h3>
<p className="text-sm text-muted-foreground max-w-sm">
The trigger configuration could not be loaded. It may have been removed or is no longer available.
</p>
{triggersError && (
<p className="text-xs text-destructive mt-2">Error: {String(triggersError)}</p>
)}
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</div>
</div>
) : selectedTrigger ? (

View File

@ -51,6 +51,7 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
creator_id: template.creator_id,
name: template.name,
description: template.description,
system_prompt: template.system_prompt,
tags: template.tags || [],
download_count: template.download_count || 0,
is_kortix_team: template.is_kortix_team || false,
@ -63,8 +64,9 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
agentpress_tools: template.agentpress_tools || {},
model: template.metadata?.model,
marketplace_published_at: template.marketplace_published_at,
usage_examples: template.usage_examples,
};
console.log('usage examples', template.usage_examples);
setSelectedTemplate(marketplaceTemplate);
setIsPreviewOpen(true);
};

View File

@ -26,11 +26,21 @@ export interface TestCredentialResponse {
error_details?: string;
}
export interface UsageExampleMessage {
role: 'user' | 'assistant';
content: string;
tool_calls?: Array<{
name: string;
arguments?: Record<string, any>;
}>;
}
export interface AgentTemplate {
template_id: string;
creator_id: string;
name: string;
description?: string;
system_prompt?: string;
mcp_requirements: MCPRequirement[];
agentpress_tools: Record<string, any>;
tags: string[];
@ -43,6 +53,7 @@ export interface AgentTemplate {
icon_color?: string;
icon_background?: string;
is_kortix_team?: boolean;
usage_examples?: UsageExampleMessage[];
metadata?: {
source_agent_id?: string;
source_version_id?: string;
@ -92,6 +103,7 @@ export interface CreateTemplateRequest {
agent_id: string;
make_public?: boolean;
tags?: string[];
usage_examples?: UsageExampleMessage[];
}
// =====================================================
@ -355,7 +367,15 @@ export function usePublishTemplate() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ template_id, tags }: { template_id: string; tags?: string[] }): Promise<{ message: string }> => {
mutationFn: async ({
template_id,
tags,
usage_examples
}: {
template_id: string;
tags?: string[];
usage_examples?: UsageExampleMessage[];
}): Promise<{ message: string }> => {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -369,7 +389,7 @@ export function usePublishTemplate() {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({ tags }),
body: JSON.stringify({ tags, usage_examples }),
});
if (!response.ok) {