mirror of https://github.com/kortix-ai/suna.git
custom agent preview
This commit is contained in:
parent
708a11d9fc
commit
99a2e9af18
|
@ -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]
|
||||
|
|
|
@ -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': '🎧'
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue