suna/frontend/src/components/dashboard/custom-agents-section.tsx

268 lines
10 KiB
TypeScript

'use client';
import React from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Ripple } from '../ui/ripple';
import { useKortixTeamTemplates, useInstallTemplate } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
import { Skeleton } from '@/components/ui/skeleton';
import { useRouter } from 'next/navigation';
import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog';
import { StreamlinedInstallDialog } from '@/components/agents/installation/streamlined-install-dialog';
import { toast } from 'sonner';
import { AgentCountLimitDialog } from '@/components/agents/agent-count-limit-dialog';
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
import { AgentCountLimitError } from '@/lib/api';
interface CustomAgentsSectionProps {
onAgentSelect?: (templateId: string) => void;
}
const TitleSection = () => (
<div className="mb-6 text-center">
<h3 className="text-lg font-medium text-foreground/90 mb-1">
Choose specialised agent
</h3>
<p className="text-sm text-muted-foreground/70">
Ready-to-use AI agents for specific tasks
</p>
</div>
);
export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps) {
const router = useRouter();
const { data: templates, isLoading, error } = useKortixTeamTemplates();
const installTemplate = useInstallTemplate();
const [selectedTemplate, setSelectedTemplate] = React.useState<MarketplaceTemplate | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = React.useState(false);
const [showInstallDialog, setShowInstallDialog] = React.useState(false);
const [showAgentLimitDialog, setShowAgentLimitDialog] = React.useState(false);
const [agentLimitError, setAgentLimitError] = React.useState<any>(null);
const [installingItemId, setInstallingItemId] = React.useState<string | null>(null);
const handleCardClick = (template: any) => {
// Map the template to MarketplaceTemplate format
const marketplaceTemplate: MarketplaceTemplate = {
id: template.template_id,
template_id: template.template_id,
creator_id: template.creator_id,
name: template.name,
description: template.description,
tags: template.tags || [],
download_count: template.download_count || 0,
is_kortix_team: template.is_kortix_team || false,
creator_name: template.creator_name,
created_at: template.created_at,
profile_image_url: template.profile_image_url,
avatar: template.avatar,
avatar_color: template.avatar_color,
mcp_requirements: template.mcp_requirements || [],
agentpress_tools: template.agentpress_tools || {},
model: template.metadata?.model,
marketplace_published_at: template.marketplace_published_at,
};
setSelectedTemplate(marketplaceTemplate);
setIsPreviewOpen(true);
};
// Handle clicking Install from the preview dialog - opens the streamlined install dialog
const handlePreviewInstall = (agent: MarketplaceTemplate) => {
setIsPreviewOpen(false);
setSelectedTemplate(agent);
setShowInstallDialog(true);
};
// Handle the actual installation from the streamlined dialog
const handleInstall = async (
item: MarketplaceTemplate,
instanceName: string,
profileMappings: Record<string, string>,
customServerConfigs: Record<string, any>
) => {
if (!item) return;
setInstallingItemId(item.id);
try {
const result = await installTemplate.mutateAsync({
template_id: item.id,
instance_name: instanceName,
profile_mappings: profileMappings,
custom_mcp_configs: customServerConfigs,
});
if (result.status === 'installed' && result.instance_id) {
toast.success(`Agent "${instanceName}" installed successfully!`);
setShowInstallDialog(false);
router.push(`/agents/config/${result.instance_id}`);
} else if (result.status === 'configs_required') {
toast.error('Please provide all required configurations');
return;
} else {
toast.error('Unexpected response from server. Please try again.');
return;
}
} catch (error: any) {
console.error('Installation error:', error);
if (error instanceof AgentCountLimitError) {
setAgentLimitError(error.detail);
setShowAgentLimitDialog(true);
setShowInstallDialog(false);
return;
}
if (error.message?.includes('already in your library')) {
toast.error('This agent is already in your library');
} else if (error.message?.includes('Credential profile not found')) {
toast.error('One or more selected credential profiles could not be found. Please refresh and try again.');
} else if (error.message?.includes('Missing credential profile')) {
toast.error('Please select credential profiles for all required services');
} else if (error.message?.includes('Invalid credential profile')) {
toast.error('One or more selected credential profiles are invalid. Please select valid profiles.');
} else if (error.message?.includes('inactive')) {
toast.error('One or more selected credential profiles are inactive. Please select active profiles.');
} else if (error.message?.includes('Template not found')) {
toast.error('This agent template is no longer available');
} else if (error.message?.includes('Access denied')) {
toast.error('You do not have permission to install this agent');
} else {
toast.error(error.message || 'Failed to install agent. Please try again.');
}
} finally {
setInstallingItemId(null);
}
};
if (isLoading) {
return (
<div className="w-full">
<TitleSection />
<div className="grid gap-4 pb-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<div key={i} className="bg-muted/30 rounded-3xl p-4 h-[180px] w-full">
<Skeleton className="h-12 w-12 rounded-2xl mb-3" />
<Skeleton className="h-5 w-3/4 mb-2" />
<Skeleton className="h-10 w-full mb-3" />
<Skeleton className="h-8 w-full mt-auto" />
</div>
))}
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="w-full">
<TitleSection />
<div className="text-center py-8">
<p className="text-muted-foreground">Failed to load custom agents</p>
</div>
</div>
);
}
// No agents found
if (!templates || templates.length === 0) {
return (
<div className="w-full">
<TitleSection />
<div className="text-center py-8">
<p className="text-muted-foreground">No custom agents available yet</p>
</div>
</div>
);
}
return (
<>
<div className="w-full">
<TitleSection />
<div className="grid gap-4 pb-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{templates.slice(0, 4).map((template) => (
<div
key={template.template_id}
className={cn(
'group relative bg-muted/30 rounded-3xl overflow-hidden transition-all duration-300 border cursor-pointer flex flex-col h-[180px] w-full border-border/50',
'hover:border-primary/20'
)}
onClick={() => handleCardClick(template)}
>
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="h-full relative flex flex-col overflow-hidden w-full p-4">
<div className="flex items-center gap-3 mb-3">
{template.profile_image_url ? (
<img
src={template.profile_image_url}
alt={template.name}
className="h-10 w-10 object-cover rounded-xl flex-shrink-0"
/>
) : (
<div
className="relative h-10 w-10 flex items-center justify-center rounded-xl text-lg shadow-md flex-shrink-0"
style={{
backgroundColor: template.avatar_color || '#3b82f6',
}}
>
<span>{template.avatar || '🤖'}</span>
<div
className="absolute inset-0 rounded-xl pointer-events-none opacity-0 dark:opacity-100 transition-opacity"
style={{
boxShadow: `0 12px 36px -6px ${template.avatar_color || '#3b82f6'}70, 0 6px 18px -3px ${template.avatar_color || '#3b82f6'}50`
}}
/>
</div>
)}
<h3 className="text-base font-semibold text-foreground line-clamp-1 flex-1">
{template.name}
</h3>
</div>
<p className="text-sm text-muted-foreground line-clamp-4 leading-relaxed flex-1">
{template.description || 'No description available'}
</p>
</div>
</div>
))}
</div>
</div>
{/* Marketplace Preview Dialog */}
<MarketplaceAgentPreviewDialog
agent={selectedTemplate}
isOpen={isPreviewOpen}
onClose={() => {
setIsPreviewOpen(false);
setSelectedTemplate(null);
}}
onInstall={handlePreviewInstall}
isInstalling={installingItemId === selectedTemplate?.id}
/>
{/* Streamlined Install Dialog */}
<StreamlinedInstallDialog
item={selectedTemplate}
open={showInstallDialog}
onOpenChange={setShowInstallDialog}
onInstall={handleInstall}
isInstalling={installingItemId === selectedTemplate?.id}
/>
{/* Agent Limit Dialog */}
{showAgentLimitDialog && agentLimitError && (
<AgentCountLimitDialog
open={showAgentLimitDialog}
onOpenChange={setShowAgentLimitDialog}
currentCount={agentLimitError.current_count}
limit={agentLimitError.limit}
tierName={agentLimitError.tier_name}
/>
)}
</>
);
}