custom agent preview

This commit is contained in:
Saumya 2025-08-06 00:43:33 +05:30
parent 708a11d9fc
commit 99a2e9af18
7 changed files with 340 additions and 34 deletions

View File

@ -29,24 +29,12 @@ class ToolkitService:
popular_categories = [
{"id": "popular", "name": "Popular"},
{"id": "productivity", "name": "Productivity"},
{"id": "ai", "name": "AI"},
{"id": "crm", "name": "CRM"},
{"id": "marketing", "name": "Marketing"},
{"id": "email", "name": "Email"},
{"id": "analytics", "name": "Analytics"},
{"id": "automation", "name": "Automation"},
{"id": "communication", "name": "Communication"},
{"id": "project-management", "name": "Project Management"},
{"id": "e-commerce", "name": "E-commerce"},
{"id": "social-media", "name": "Social Media"},
{"id": "payments", "name": "Payments"},
{"id": "finance", "name": "Finance"},
{"id": "developer-tools", "name": "Developer Tools"},
{"id": "api", "name": "API"},
{"id": "notifications", "name": "Notifications"},
{"id": "scheduling", "name": "Scheduling"},
{"id": "data-analytics", "name": "Data Analytics"},
{"id": "customer-support", "name": "Customer Support"}
]
categories = [CategoryInfo(**cat) for cat in popular_categories]

View File

@ -18,24 +18,12 @@ import { AgentSelector } from '../../thread/chat-input/agent-selector';
const CATEGORY_EMOJIS: Record<string, string> = {
'popular': '🔥',
'productivity': '📊',
'ai': '🤖',
'crm': '👥',
'marketing': '📢',
'email': '📧',
'analytics': '📈',
'automation': '⚡',
'communication': '💬',
'project-management': '📋',
'e-commerce': '🛒',
'social-media': '📱',
'payments': '💳',
'finance': '💰',
'developer-tools': '🛠️',
'api': '🔌',
'notifications': '🔔',
'scheduling': '📅',
'data-analytics': '📊',
'customer-support': '🎧'
};

View File

@ -1,11 +1,12 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { SearchBar } from './search-bar';
import { MarketplaceSectionHeader } from './marketplace-section-header';
import { AgentCard } from './agent-card';
import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog';
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
interface MarketplaceTabProps {
@ -41,6 +42,25 @@ export const MarketplaceTab = ({
getItemStyling,
currentUserId
}: MarketplaceTabProps) => {
const [previewAgent, setPreviewAgent] = useState<MarketplaceTemplate | null>(null);
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const handleAgentClick = (item: MarketplaceTemplate) => {
setPreviewAgent(item);
setPreviewDialogOpen(true);
};
const handlePreviewClose = () => {
setPreviewDialogOpen(false);
setPreviewAgent(null);
};
const handleInstallFromPreview = (agent: MarketplaceTemplate) => {
onInstallClick(agent);
setPreviewDialogOpen(false);
setPreviewAgent(null);
};
return (
<div className="space-y-6 mt-8 flex flex-col min-h-full">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
@ -107,7 +127,7 @@ export const MarketplaceTab = ({
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => onInstallClick(item)}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
))}
@ -126,7 +146,7 @@ export const MarketplaceTab = ({
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => onInstallClick(item)}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
))}
@ -145,7 +165,7 @@ export const MarketplaceTab = ({
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => onInstallClick(item)}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
))}
@ -154,6 +174,14 @@ export const MarketplaceTab = ({
</div>
)}
</div>
<MarketplaceAgentPreviewDialog
agent={previewAgent}
isOpen={previewDialogOpen}
onClose={handlePreviewClose}
onInstall={handleInstallFromPreview}
isInstalling={installingItemId === previewAgent?.id}
/>
</div>
);
};

View File

@ -12,6 +12,7 @@ export interface MarketplaceTemplate {
avatar_color?: string;
template_id: string;
is_kortix_team?: boolean;
agentpress_tools?: Record<string, any>;
mcp_requirements?: Array<{
qualified_name: string;
display_name: string;

View File

@ -0,0 +1,282 @@
'use client';
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Bot, Download, Wrench, Plug, Tag, User, Calendar, Loader2 } from 'lucide-react';
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
import { AGENTPRESS_TOOL_DEFINITIONS } from '@/components/agents/tools';
import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
import { usePipedreamAppIcon } from '@/hooks/react-query/pipedream/use-pipedream';
interface MarketplaceAgentPreviewDialogProps {
agent: MarketplaceTemplate | null;
isOpen: boolean;
onClose: () => void;
onInstall: (agent: MarketplaceTemplate) => void;
isInstalling?: boolean;
}
const extractAppInfo = (qualifiedName: string, customType?: string) => {
if (customType === 'pipedream') {
const qualifiedMatch = qualifiedName.match(/^pipedream_([^_]+)_/);
if (qualifiedMatch) {
return { type: 'pipedream', slug: qualifiedMatch[1] };
}
}
if (customType === 'composio') {
if (qualifiedName.startsWith('composio.')) {
const extractedSlug = qualifiedName.substring(9);
if (extractedSlug) {
return { type: 'composio', slug: extractedSlug };
}
}
}
return null;
};
const IntegrationLogo: React.FC<{
qualifiedName: string;
displayName: string;
customType?: string;
}> = ({ qualifiedName, displayName, customType }) => {
const appInfo = extractAppInfo(qualifiedName, customType);
const { data: pipedreamIconData } = usePipedreamAppIcon(
appInfo?.type === 'pipedream' ? appInfo.slug : '',
{ enabled: appInfo?.type === 'pipedream' }
);
const { data: composioIconData } = useComposioToolkitIcon(
appInfo?.type === 'composio' ? appInfo.slug : '',
{ enabled: appInfo?.type === 'composio' }
);
let logoUrl: string | undefined;
if (appInfo?.type === 'pipedream') {
logoUrl = pipedreamIconData?.icon_url;
} else if (appInfo?.type === 'composio') {
logoUrl = composioIconData?.icon_url;
}
const firstLetter = displayName.charAt(0).toUpperCase();
return (
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0 overflow-hidden rounded-sm">
{logoUrl ? (
<img
src={logoUrl}
alt={displayName}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
) : 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"}>
{firstLetter}
</div>
</div>
);
};
export const MarketplaceAgentPreviewDialog: React.FC<MarketplaceAgentPreviewDialogProps> = ({
agent,
isOpen,
onClose,
onInstall,
isInstalling = false
}) => {
if (!agent) return null;
const { avatar, avatar_color } = agent;
const isSunaAgent = agent.is_kortix_team || false;
const tools = agent.mcp_requirements || [];
const integrations = tools.filter(tool => !tool.custom_type || tool.custom_type !== 'sse');
const customTools = tools.filter(tool => tool.custom_type === 'sse');
const agentpressTools = Object.entries(agent.agentpress_tools || {})
.filter(([_, enabled]) => enabled)
.map(([toolName]) => toolName);
const handleInstall = () => {
onInstall(agent);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const getAppDisplayName = (qualifiedName: string) => {
if (qualifiedName.includes('_')) {
const parts = qualifiedName.split('_');
return parts[parts.length - 1].replace(/\b\w/g, l => l.toUpperCase());
}
return qualifiedName.replace(/\b\w/g, l => l.toUpperCase());
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[90vh] p-0 overflow-hidden">
<DialogHeader className='p-6'>
<DialogTitle className='sr-only'>Agent Preview</DialogTitle>
<div className='relative h-20 w-20 aspect-square bg-muted rounded-2xl flex items-center justify-center' style={{ backgroundColor: avatar_color }}>
<div className="text-4xl drop-shadow-lg">
{avatar || '🤖'}
</div>
<div
className="absolute inset-0 rounded-2xl pointer-events-none opacity-0 dark:opacity-100 transition-opacity"
style={{
boxShadow: `0 16px 48px -8px ${avatar_color}70, 0 8px 24px -4px ${avatar_color}50`
}}
/>
</div>
</DialogHeader>
<div className="-mt-4 flex flex-col max-h-[calc(90vh-8rem)] overflow-hidden">
<div className="p-6 py-0 pb-4">
<div className="flex items-start justify-between mb-4">
<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>
</div>
<p className="text-muted-foreground leading-relaxed mb-4">
{agent.description || 'No description available'}
</p>
{agent.tags && agent.tags.length > 0 && (
<div className="flex items-center gap-2 mb-4">
<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>
))}
</div>
</div>
)}
</div>
<div className="flex-1 gap-4 overflow-y-auto p-6 pt-4 space-y-4">
{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-3">
<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 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}
/>
<span className="text-sm font-medium">
{integration.display_name || getAppDisplayName(integration.qualified_name)}
</span>
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{customTools.length > 0 && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<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}
/>
<span className="text-sm font-medium">
{tool.display_name || getAppDisplayName(tool.qualified_name)}
</span>
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{agentpressTools.length === 0 && tools.length === 0 && (
<Card>
<CardContent className="p-4 text-center">
<Bot className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-muted-foreground">
This agent uses basic functionality without external integrations or specialized tools.
</p>
</CardContent>
</Card>
)}
</div>
<div className="p-6 pt-4">
<div className="flex gap-3">
<Button
onClick={handleInstall}
disabled={isInstalling}
className="flex-1"
>
{isInstalling ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Installing...
</>
) : (
<>
<Download className="h-4 w-4" />
Install Agent
</>
)}
</Button>
<Button variant="outline" onClick={onClose}>
Close
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -37,6 +37,20 @@ export const useComposioToolkits = (search?: string, category?: string) => {
});
};
export const useComposioToolkitIcon = (toolkitSlug: string, options?: { enabled?: boolean }) => {
return useQuery({
queryKey: ['composio', 'toolkit-icon', toolkitSlug],
queryFn: async (): Promise<{ success: boolean; icon_url?: string }> => {
const result = await composioApi.getToolkitIcon(toolkitSlug);
console.log(`🎨 Composio Icon for ${toolkitSlug}:`, result);
return result;
},
enabled: options?.enabled !== undefined ? options.enabled : !!toolkitSlug,
staleTime: 60 * 60 * 1000,
retry: 2,
});
};
export const useCreateComposioProfile = () => {
const queryClient = useQueryClient();

View File

@ -85,19 +85,24 @@ export const apiClient = {
clearTimeout(timeoutId);
if (!response.ok) {
const error: ApiError = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
let errorData: any = null;
try {
const errorData = await response.json();
error.details = errorData;
errorData = await response.json();
if (errorData.message) {
error.message = errorData.message;
errorMessage = errorData.message;
}
} catch {
}
const error: ApiError = new Error(errorMessage);
error.status = response.status;
error.response = response;
if (errorData) {
error.details = errorData;
}
if (showErrors) {
handleApiError(error, errorContext);
}