From 99a2e9af181b2d05d8e64d8544ee4719ecbbeea4 Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 6 Aug 2025 00:43:33 +0530 Subject: [PATCH] custom agent preview --- .../composio_integration/toolkit_service.py | 12 - .../agents/composio/composio-registry.tsx | 12 - .../custom-agents-page/marketplace-tab.tsx | 36 ++- .../components/agents/installation/types.ts | 1 + .../marketplace-agent-preview-dialog.tsx | 282 ++++++++++++++++++ .../react-query/composio/use-composio.ts | 14 + frontend/src/lib/api-client.ts | 17 +- 7 files changed, 340 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/agents/marketplace-agent-preview-dialog.tsx diff --git a/backend/composio_integration/toolkit_service.py b/backend/composio_integration/toolkit_service.py index 64870dca..91253cd2 100644 --- a/backend/composio_integration/toolkit_service.py +++ b/backend/composio_integration/toolkit_service.py @@ -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] diff --git a/frontend/src/components/agents/composio/composio-registry.tsx b/frontend/src/components/agents/composio/composio-registry.tsx index a5c07134..0a16dcbb 100644 --- a/frontend/src/components/agents/composio/composio-registry.tsx +++ b/frontend/src/components/agents/composio/composio-registry.tsx @@ -18,24 +18,12 @@ import { AgentSelector } from '../../thread/chat-input/agent-selector'; const CATEGORY_EMOJIS: Record = { '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': '🎧' }; diff --git a/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx b/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx index 1231604a..fc14e74f 100644 --- a/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx +++ b/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx @@ -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(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 (
@@ -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 = ({
)}
+ + ); }; \ No newline at end of file diff --git a/frontend/src/components/agents/installation/types.ts b/frontend/src/components/agents/installation/types.ts index 43e75fc9..f67b4655 100644 --- a/frontend/src/components/agents/installation/types.ts +++ b/frontend/src/components/agents/installation/types.ts @@ -12,6 +12,7 @@ export interface MarketplaceTemplate { avatar_color?: string; template_id: string; is_kortix_team?: boolean; + agentpress_tools?: Record; mcp_requirements?: Array<{ qualified_name: string; display_name: string; diff --git a/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx b/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx new file mode 100644 index 00000000..4aac6e75 --- /dev/null +++ b/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx @@ -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 ( +
+ {logoUrl ? ( + {displayName} { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + target.nextElementSibling?.classList.remove('hidden'); + }} + /> + ) : null} +
+ {firstLetter} +
+
+ ); +}; + +export const MarketplaceAgentPreviewDialog: React.FC = ({ + 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 ( + + + + Agent Preview +
+
+ {avatar || '🤖'} +
+
+
+ +
+
+
+
+

+ {agent.name} +

+
+
+ + {agent.creator_name || 'Unknown'} +
+
+ + {agent.download_count} downloads +
+
+ + {formatDate(agent.created_at)} +
+
+
+
+

+ {agent.description || 'No description available'} +

+ {agent.tags && agent.tags.length > 0 && ( +
+ +
+ {agent.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} +
+
+ {integrations.length > 0 && ( + + +
+ +

Integrations

+
+
+ {integrations.map((integration, index) => ( + + + + {integration.display_name || getAppDisplayName(integration.qualified_name)} + + + ))} +
+
+
+ )} + {customTools.length > 0 && ( + + +
+ +

Custom Tools

+
+
+ {customTools.map((tool, index) => ( + + + + {tool.display_name || getAppDisplayName(tool.qualified_name)} + + + ))} +
+
+
+ )} + {agentpressTools.length === 0 && tools.length === 0 && ( + + + +

+ This agent uses basic functionality without external integrations or specialized tools. +

+
+
+ )} +
+
+
+ + +
+
+
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/react-query/composio/use-composio.ts b/frontend/src/hooks/react-query/composio/use-composio.ts index e89f0333..2e6f0108 100644 --- a/frontend/src/hooks/react-query/composio/use-composio.ts +++ b/frontend/src/hooks/react-query/composio/use-composio.ts @@ -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(); diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts index 717aaa50..720ebe5a 100644 --- a/frontend/src/lib/api-client.ts +++ b/frontend/src/lib/api-client.ts @@ -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); }