mirror of https://github.com/kortix-ai/suna.git
638 lines
25 KiB
TypeScript
638 lines
25 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Search, Zap, X, Settings, ChevronDown, ChevronUp } from 'lucide-react';
|
|
import { useComposioToolkits, useComposioCategories } from '@/hooks/react-query/composio/use-composio';
|
|
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
|
|
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
|
import { ComposioConnector } from './composio-connector';
|
|
import { ComposioToolsManager } from './composio-tools-manager';
|
|
import type { ComposioToolkit, ComposioProfile } from '@/hooks/react-query/composio/utils';
|
|
import { toast } from 'sonner';
|
|
import { cn } from '@/lib/utils';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { AgentSelector } from '../../thread/chat-input/agent-selector';
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
import { Switch } from '@/components/ui/switch';
|
|
|
|
const CATEGORY_EMOJIS: Record<string, string> = {
|
|
'popular': '🔥',
|
|
'productivity': '📊',
|
|
'crm': '👥',
|
|
'marketing': '📢',
|
|
'analytics': '📈',
|
|
'communication': '💬',
|
|
'project-management': '📋',
|
|
'scheduling': '📅',
|
|
};
|
|
|
|
interface ConnectedApp {
|
|
toolkit: ComposioToolkit;
|
|
profile: ComposioProfile;
|
|
mcpConfig: {
|
|
name: string;
|
|
type: string;
|
|
config: Record<string, any>;
|
|
enabledTools: string[];
|
|
};
|
|
}
|
|
|
|
interface ComposioRegistryProps {
|
|
onToolsSelected?: (profileId: string, selectedTools: string[], appName: string, appSlug: string) => void;
|
|
onAppSelected?: (app: ComposioToolkit) => void;
|
|
mode?: 'full' | 'profile-only';
|
|
onClose?: () => void;
|
|
showAgentSelector?: boolean;
|
|
selectedAgentId?: string;
|
|
onAgentChange?: (agentId: string | undefined) => void;
|
|
}
|
|
|
|
// Helper function to get agent-specific connected apps
|
|
const getAgentConnectedApps = (
|
|
agent: any,
|
|
profiles: ComposioProfile[],
|
|
toolkits: ComposioToolkit[]
|
|
): ConnectedApp[] => {
|
|
if (!agent?.custom_mcps || !profiles?.length || !toolkits?.length) return [];
|
|
|
|
const connectedApps: ConnectedApp[] = [];
|
|
|
|
agent.custom_mcps.forEach((mcpConfig: any) => {
|
|
// Check if this is a Composio MCP by looking for profile_id in config
|
|
if (mcpConfig.config?.profile_id) {
|
|
const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id);
|
|
const toolkit = toolkits.find(t => t.slug === profile?.toolkit_slug);
|
|
|
|
if (profile && toolkit) {
|
|
connectedApps.push({
|
|
toolkit,
|
|
profile,
|
|
mcpConfig
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return connectedApps;
|
|
};
|
|
|
|
// Helper function to check if an app is connected to the agent
|
|
const isAppConnectedToAgent = (
|
|
agent: any,
|
|
appSlug: string,
|
|
profiles: ComposioProfile[]
|
|
): boolean => {
|
|
if (!agent?.custom_mcps) return false;
|
|
|
|
return agent.custom_mcps.some((mcpConfig: any) => {
|
|
if (mcpConfig.config?.profile_id) {
|
|
const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id);
|
|
return profile?.toolkit_slug === appSlug;
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
const AppCardSkeleton = () => (
|
|
<div className="border border-border/50 rounded-xl p-4">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Skeleton className="w-10 h-10 rounded-lg" />
|
|
<div className="flex-1">
|
|
<Skeleton className="w-3/4 h-4 mb-2" />
|
|
<Skeleton className="w-full h-3" />
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
<Skeleton className="w-16 h-5" />
|
|
<Skeleton className="w-20 h-5" />
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<Skeleton className="w-24 h-6" />
|
|
<Skeleton className="w-20 h-8" />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ConnectedAppSkeleton = () => (
|
|
<div className="border border-border/50 rounded-2xl p-4">
|
|
<div className="flex items-start gap-3 mb-3">
|
|
<Skeleton className="w-10 h-10 rounded-lg" />
|
|
<div className="flex-1">
|
|
<Skeleton className="w-3/4 h-4 mb-2" />
|
|
<Skeleton className="w-full h-3" />
|
|
</div>
|
|
<Skeleton className="w-8 h-8 rounded" />
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<Skeleton className="w-32 h-4" />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ConnectedAppCard = ({
|
|
connectedApp,
|
|
onToggleTools,
|
|
onConfigure,
|
|
onManageTools,
|
|
isUpdating
|
|
}: {
|
|
connectedApp: ConnectedApp;
|
|
onToggleTools: (profileId: string, enabled: boolean) => void;
|
|
onConfigure: (app: ComposioToolkit, profile: ComposioProfile) => void;
|
|
onManageTools: (connectedApp: ConnectedApp) => void;
|
|
isUpdating: boolean;
|
|
}) => {
|
|
const { toolkit, profile, mcpConfig } = connectedApp;
|
|
const hasEnabledTools = mcpConfig.enabledTools && mcpConfig.enabledTools.length > 0;
|
|
|
|
return (
|
|
<div
|
|
className="group border bg-card rounded-2xl p-4 transition-all duration-200 cursor-pointer"
|
|
>
|
|
<div className="flex items-start gap-3 mb-3">
|
|
{toolkit.logo ? (
|
|
<img src={toolkit.logo} alt={toolkit.name} className="w-10 h-10 rounded-lg object-cover p-2 bg-muted rounded-xl border" />
|
|
) : (
|
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
<span className="text-primary text-sm font-medium">{toolkit.name.charAt(0)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-sm leading-tight truncate mb-1">{toolkit.name}</h3>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
|
Connected as "{profile.profile_name}"
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onManageTools(connectedApp)}
|
|
disabled={isUpdating}
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
|
{hasEnabledTools ? `${mcpConfig.enabledTools.length} tools enabled` : 'Connected (no tools)'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const AppCard = ({ app, profiles, onConnect, onConfigure, isConnectedToAgent, currentAgentId, mode }: {
|
|
app: ComposioToolkit;
|
|
profiles: ComposioProfile[];
|
|
onConnect: () => void;
|
|
onConfigure: (profile: ComposioProfile) => void;
|
|
isConnectedToAgent: boolean;
|
|
currentAgentId?: string;
|
|
mode?: 'full' | 'profile-only';
|
|
}) => {
|
|
const connectedProfiles = profiles.filter(p => p.is_connected);
|
|
const canConnect = mode === 'profile-only' ? true : (!isConnectedToAgent && currentAgentId);
|
|
|
|
return (
|
|
<div
|
|
onClick={canConnect ? (connectedProfiles.length > 0 ? () => onConfigure(connectedProfiles[0]) : onConnect) : undefined}
|
|
className={cn(
|
|
"group border bg-card rounded-2xl p-4 transition-all duration-200",
|
|
canConnect ? "hover:bg-muted cursor-pointer" : "opacity-60 cursor-not-allowed"
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3 mb-3">
|
|
{app.logo ? (
|
|
<img src={app.logo} alt={app.name} className="w-10 h-10 rounded-lg object-cover p-2 bg-muted rounded-xl border" />
|
|
) : (
|
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
<span className="text-primary text-sm font-medium">{app.name.charAt(0)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-sm leading-tight truncate mb-1">{app.name}</h3>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
|
{app.description || `Connect your ${app.name} account to access its features.`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{app.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
{app.tags.slice(0, 2).map((tag, index) => (
|
|
<Badge key={index} variant="secondary" className="text-xs px-1.5 py-0.5 h-auto">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
{app.tags.length > 2 && (
|
|
<Badge variant="outline" className="text-xs px-1.5 py-0.5 h-auto">
|
|
+{app.tags.length - 2}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-between items-center">
|
|
{mode === 'profile-only' ? (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50" />
|
|
{connectedProfiles.length > 0 ? `${connectedProfiles.length} existing profile${connectedProfiles.length !== 1 ? 's' : ''}` : 'Click to connect'}
|
|
</div>
|
|
</div>
|
|
) : isConnectedToAgent ? (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
|
Connected to this agent
|
|
</div>
|
|
</div>
|
|
) : connectedProfiles.length > 0 ? (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
|
Profile available ({connectedProfiles.length})
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50" />
|
|
Not connected
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ComposioRegistry: React.FC<ComposioRegistryProps> = ({
|
|
onToolsSelected,
|
|
onAppSelected,
|
|
mode = 'full',
|
|
onClose,
|
|
showAgentSelector = false,
|
|
selectedAgentId,
|
|
onAgentChange,
|
|
}) => {
|
|
const [search, setSearch] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
|
const [selectedApp, setSelectedApp] = useState<ComposioToolkit | null>(null);
|
|
const [showConnector, setShowConnector] = useState(false);
|
|
const [showConnectedApps, setShowConnectedApps] = useState(true);
|
|
const [showToolsManager, setShowToolsManager] = useState(false);
|
|
const [selectedConnectedApp, setSelectedConnectedApp] = useState<ConnectedApp | null>(null);
|
|
|
|
const [internalSelectedAgentId, setInternalSelectedAgentId] = useState<string | undefined>(selectedAgentId);
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: categoriesData, isLoading: isLoadingCategories } = useComposioCategories();
|
|
const { data: toolkits, isLoading } = useComposioToolkits(search, selectedCategory);
|
|
const { data: profiles, isLoading: isLoadingProfiles } = useComposioProfiles();
|
|
|
|
const currentAgentId = selectedAgentId ?? internalSelectedAgentId;
|
|
const { data: agent, isLoading: isLoadingAgent } = useAgent(currentAgentId || '');
|
|
const { mutate: updateAgent, isPending: isUpdatingAgent } = useUpdateAgent();
|
|
|
|
const handleAgentSelect = (agentId: string | undefined) => {
|
|
if (onAgentChange) {
|
|
onAgentChange(agentId);
|
|
} else {
|
|
setInternalSelectedAgentId(agentId);
|
|
}
|
|
};
|
|
|
|
const profilesByToolkit = useMemo(() => {
|
|
const grouped: Record<string, ComposioProfile[]> = {};
|
|
profiles?.forEach(profile => {
|
|
if (profile.is_connected) {
|
|
if (!grouped[profile.toolkit_slug]) {
|
|
grouped[profile.toolkit_slug] = [];
|
|
}
|
|
grouped[profile.toolkit_slug].push(profile);
|
|
}
|
|
});
|
|
return grouped;
|
|
}, [profiles]);
|
|
|
|
const connectedApps = useMemo(() => {
|
|
if (!currentAgentId || !agent) return [];
|
|
return getAgentConnectedApps(agent, profiles || [], toolkits?.toolkits || []);
|
|
}, [agent, profiles, toolkits, currentAgentId]);
|
|
|
|
const isLoadingConnectedApps = currentAgentId && (isLoadingAgent || isLoadingProfiles || isLoading);
|
|
|
|
const filteredToolkits = useMemo(() => {
|
|
if (!toolkits?.toolkits) return [];
|
|
return toolkits.toolkits;
|
|
}, [toolkits]);
|
|
|
|
const handleConnect = (app: ComposioToolkit) => {
|
|
if (mode !== 'profile-only' && !currentAgentId && showAgentSelector) {
|
|
toast.error('Please select an agent first');
|
|
return;
|
|
}
|
|
setSelectedApp(app);
|
|
setShowConnector(true);
|
|
};
|
|
|
|
const handleConfigure = (app: ComposioToolkit, profile: ComposioProfile) => {
|
|
if (mode !== 'profile-only' && !currentAgentId) {
|
|
toast.error('Please select an agent first');
|
|
return;
|
|
}
|
|
setSelectedApp(app);
|
|
setShowConnector(true);
|
|
};
|
|
|
|
const handleToggleTools = (profileId: string, enabled: boolean) => {
|
|
if (!currentAgentId || !agent) return;
|
|
|
|
const updatedCustomMcps = agent.custom_mcps?.map((mcpConfig: any) => {
|
|
if (mcpConfig.config?.profile_id === profileId) {
|
|
return {
|
|
...mcpConfig,
|
|
enabledTools: enabled ? mcpConfig.enabledTools || [] : []
|
|
};
|
|
}
|
|
return mcpConfig;
|
|
}) || [];
|
|
|
|
updateAgent({
|
|
agentId: currentAgentId,
|
|
custom_mcps: updatedCustomMcps
|
|
}, {
|
|
onSuccess: () => {
|
|
toast.success(enabled ? 'Tools enabled' : 'Tools disabled');
|
|
},
|
|
onError: (error: any) => {
|
|
toast.error(error.message || 'Failed to update tools');
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleManageTools = (connectedApp: ConnectedApp) => {
|
|
setSelectedConnectedApp(connectedApp);
|
|
setShowToolsManager(true);
|
|
};
|
|
|
|
const handleConnectionComplete = (profileId: string, appName: string, appSlug: string) => {
|
|
setShowConnector(false);
|
|
queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] });
|
|
if (onToolsSelected) {
|
|
onToolsSelected(profileId, [], appName, appSlug);
|
|
}
|
|
};
|
|
|
|
const categories = categoriesData?.categories || [];
|
|
|
|
return (
|
|
<div className="h-full w-full overflow-hidden flex">
|
|
<div className="w-64 h-full overflow-hidden border-r bg-muted/20">
|
|
<div className="h-full flex flex-col">
|
|
<div className="flex-shrink-0 p-4 border-b">
|
|
<h3 className="text-sm font-medium text-muted-foreground">Categories</h3>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
<ScrollArea className="h-full">
|
|
<div className="p-4 space-y-1">
|
|
<button
|
|
onClick={() => setSelectedCategory('')}
|
|
className={cn(
|
|
"w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors text-left",
|
|
selectedCategory === ''
|
|
? "bg-muted-foreground/20 text-muted-foreground"
|
|
: "hover:bg-muted/50 text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<span className="text-base">📁</span>
|
|
<span>All Apps</span>
|
|
</button>
|
|
|
|
{isLoadingCategories ? (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="flex items-center gap-3 px-3 py-2">
|
|
<Skeleton className="w-4 h-4 bg-muted rounded" />
|
|
<Skeleton className="flex-1 h-4 bg-muted rounded" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
categories.map((category) => (
|
|
<button
|
|
key={category.id}
|
|
onClick={() => setSelectedCategory(category.id)}
|
|
className={cn(
|
|
"w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors text-left",
|
|
selectedCategory === category.id
|
|
? "bg-muted-foreground/20 text-muted-foreground"
|
|
: "hover:bg-muted/50 text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<span className="text-base">{CATEGORY_EMOJIS[category.id] || '📁'}</span>
|
|
<span className="truncate">{category.name}</span>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 h-full overflow-hidden">
|
|
<div className="h-full flex flex-col">
|
|
<div className="flex-shrink-0 border-b p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h2 className="text-xl font-semibold">
|
|
{mode === 'profile-only' ? 'Connect New App' : 'App Integrations'}
|
|
</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
{mode === 'profile-only'
|
|
? 'Create a connection profile for your favorite apps'
|
|
: `Connect your favorite apps with ${currentAgentId ? 'this agent' : 'your agent'}`
|
|
}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{showAgentSelector && (
|
|
<AgentSelector
|
|
selectedAgentId={currentAgentId}
|
|
onAgentSelect={handleAgentSelect}
|
|
isSunaAgent={agent?.metadata?.is_suna_default}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search apps..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
{selectedCategory && (
|
|
<div className="flex items-center gap-2 mt-3">
|
|
<span className="text-xs text-muted-foreground">Filtered by:</span>
|
|
<Badge variant="outline" className="gap-1 bg-muted-foreground/20 text-muted-foreground">
|
|
<span>{CATEGORY_EMOJIS[selectedCategory] || '📁'}</span>
|
|
<span>{categories.find(c => c.id === selectedCategory)?.name}</span>
|
|
<button
|
|
onClick={() => setSelectedCategory('')}
|
|
className="ml-1 hover:bg-muted rounded-full p-0.5"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
<ScrollArea className="h-full">
|
|
<div className="p-6 space-y-6">
|
|
{currentAgentId && (
|
|
<Collapsible open={showConnectedApps} onOpenChange={setShowConnectedApps}>
|
|
<CollapsibleTrigger asChild>
|
|
<div className="w-full hover:underline flex items-center justify-between p-0 h-auto">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-lg font-medium">Connected to this agent</h3>
|
|
{isLoadingConnectedApps ? (
|
|
<Skeleton className="w-6 h-5 rounded ml-2" />
|
|
) : connectedApps.length > 0 && (
|
|
<Badge variant="outline" className="ml-2">
|
|
{connectedApps.length}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{showConnectedApps ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="mt-4">
|
|
{isLoadingConnectedApps ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<ConnectedAppSkeleton key={i} />
|
|
))}
|
|
</div>
|
|
) : connectedApps.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mb-4 mx-auto">
|
|
<Zap className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<h4 className="text-sm font-medium mb-2">No connected apps</h4>
|
|
<p className="text-xs">Connect apps below to manage tools for this agent.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{connectedApps.map((connectedApp) => (
|
|
<ConnectedAppCard
|
|
key={connectedApp.profile.profile_id}
|
|
connectedApp={connectedApp}
|
|
onToggleTools={handleToggleTools}
|
|
onConfigure={handleConfigure}
|
|
onManageTools={handleManageTools}
|
|
isUpdating={isUpdatingAgent}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
<div>
|
|
<h3 className="text-lg font-medium mb-4">
|
|
{currentAgentId ? 'Available Apps' : 'Browse Apps'}
|
|
</h3>
|
|
|
|
{isLoading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{Array.from({ length: 12 }).map((_, i) => (
|
|
<AppCardSkeleton key={i} />
|
|
))}
|
|
</div>
|
|
) : filteredToolkits.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mb-4">
|
|
<Search className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2">No apps found</h3>
|
|
<p className="text-muted-foreground">
|
|
{search ? `No apps match "${search}"` : 'No apps available in this category'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{filteredToolkits.map((app) => (
|
|
<AppCard
|
|
key={app.slug}
|
|
app={app}
|
|
profiles={profilesByToolkit[app.slug] || []}
|
|
onConnect={() => handleConnect(app)}
|
|
onConfigure={(profile) => handleConfigure(app, profile)}
|
|
isConnectedToAgent={isAppConnectedToAgent(agent, app.slug, profiles || [])}
|
|
currentAgentId={currentAgentId}
|
|
mode={mode}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{selectedApp && (
|
|
<ComposioConnector
|
|
app={selectedApp}
|
|
agentId={currentAgentId}
|
|
open={showConnector}
|
|
onOpenChange={setShowConnector}
|
|
onComplete={handleConnectionComplete}
|
|
mode={mode}
|
|
/>
|
|
)}
|
|
|
|
{selectedConnectedApp && currentAgentId && (
|
|
<ComposioToolsManager
|
|
agentId={currentAgentId}
|
|
open={showToolsManager}
|
|
onOpenChange={setShowToolsManager}
|
|
profileId={selectedConnectedApp.profile.profile_id}
|
|
profileInfo={{
|
|
profile_id: selectedConnectedApp.profile.profile_id,
|
|
profile_name: selectedConnectedApp.profile.profile_name,
|
|
toolkit_name: selectedConnectedApp.toolkit.name,
|
|
toolkit_slug: selectedConnectedApp.toolkit.slug,
|
|
}}
|
|
appLogo={selectedConnectedApp.toolkit.logo}
|
|
onToolsUpdate={() => {
|
|
queryClient.invalidateQueries({ queryKey: ['agents', currentAgentId] });
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|