quick connect option in model selector

This commit is contained in:
Saumya 2025-08-08 19:46:46 +05:30
parent 2975772806
commit 3e683de620
3 changed files with 345 additions and 180 deletions

View File

@ -45,13 +45,13 @@ class ToolkitService:
logger.error(f"Failed to list categories: {e}", exc_info=True)
raise
async def list_toolkits(self, limit: int = 100, category: Optional[str] = None) -> List[ToolkitInfo]:
async def list_toolkits(self, limit: int = 500, category: Optional[str] = None) -> List[ToolkitInfo]:
try:
logger.info(f"Fetching toolkits with limit: {limit}, category: {category}")
if category:
toolkits_response = self.client.toolkits.list(limit=limit, category=category)
toolkits_response = self.client.toolkits.list(limit=500, category=category, managed_by="composio")
else:
toolkits_response = self.client.toolkits.list(limit=limit)
toolkits_response = self.client.toolkits.list(limit=500, managed_by="composio")
items = getattr(toolkits_response, 'items', [])
if hasattr(toolkits_response, '__dict__'):

View File

@ -177,6 +177,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
selectedAgentId={selectedAgentId}
/>
</div>
);

View File

@ -6,7 +6,18 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuPortal,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Tooltip,
TooltipContent,
@ -14,7 +25,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, Search, AlertTriangle, Crown, ArrowUpRight, Brain, Plus, Edit, Trash, Cpu, Key, KeyRound } from 'lucide-react';
import { Check, Search, AlertTriangle, Crown, ArrowUpRight, Brain, Plus, Edit, Trash, Cpu, KeyRound, ExternalLink, Settings } from 'lucide-react';
import {
ModelOption,
SubscriptionStatus,
@ -24,15 +35,47 @@ import {
DEFAULT_PREMIUM_MODEL_ID,
formatModelName,
getCustomModels,
MODELS // Import the centralized MODELS constant
MODELS
} from './_use-model-selection';
import { PaywallDialog } from '@/components/payment/paywall-dialog';
import { BillingModal } from '@/components/billing/billing-modal';
import { cn } from '@/lib/utils';
import { useRouter } from 'next/navigation';
import { isLocalMode } from '@/lib/config';
import { CustomModelDialog, CustomModelFormData } from './custom-model-dialog';
import Link from 'next/link';
import { IntegrationsRegistry } from '@/components/agents/integrations-registry';
import { ComposioConnector } from '@/components/agents/composio/composio-connector';
import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
import { useAgent } from '@/hooks/react-query/agents/use-agents';
import { Skeleton } from '@/components/ui/skeleton';
import { useFeatureFlag } from '@/lib/feature-flags';
const PREDEFINED_APPS = [
{
id: 'googledrive',
name: 'Google Drive',
slug: 'googledrive',
description: 'Access and manage files in Google Drive'
},
{
id: 'slack',
name: 'Slack',
slug: 'slack',
description: 'Send messages and manage channels'
},
{
id: 'gmail',
name: 'Gmail',
slug: 'gmail',
description: 'Send and manage emails'
},
{
id: 'notion',
name: 'Notion',
slug: 'notion',
description: 'Create and manage Notion pages'
}
];
interface CustomModel {
id: string;
@ -50,6 +93,7 @@ interface ModelSelectorProps {
billingModalOpen: boolean;
setBillingModalOpen: (open: boolean) => void;
hasBorder?: boolean;
selectedAgentId?: string;
}
export const ModelSelector: React.FC<ModelSelectorProps> = ({
@ -62,6 +106,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
billingModalOpen,
setBillingModalOpen,
hasBorder = false,
selectedAgentId,
}) => {
const [paywallOpen, setPaywallOpen] = useState(false);
const [lockedModel, setLockedModel] = useState<string | null>(null);
@ -69,33 +114,65 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const [searchQuery, setSearchQuery] = useState('');
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const searchInputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
// Custom models state
const [customModels, setCustomModels] = useState<CustomModel[]>([]);
const [isCustomModelDialogOpen, setIsCustomModelDialogOpen] = useState(false);
const [dialogInitialData, setDialogInitialData] = useState<CustomModelFormData>({ id: '', label: '' });
const [dialogMode, setDialogMode] = useState<'add' | 'edit'>('add');
const [editingModelId, setEditingModelId] = useState<string | null>(null);
// Load custom models from localStorage on component mount
const [showIntegrationsManager, setShowIntegrationsManager] = useState(false);
const [selectedApp, setSelectedApp] = useState<typeof PREDEFINED_APPS[0] | null>(null);
const [showComposioConnector, setShowComposioConnector] = useState(false);
const { data: googleDriveIcon } = useComposioToolkitIcon('googledrive', { enabled: true });
const { data: slackIcon } = useComposioToolkitIcon('slack', { enabled: true });
const { data: gmailIcon } = useComposioToolkitIcon('gmail', { enabled: true });
const { data: notionIcon } = useComposioToolkitIcon('notion', { enabled: true });
const { data: selectedAppIcon } = useComposioToolkitIcon(selectedApp?.slug || '', {
enabled: !!selectedApp?.slug && showComposioConnector
});
const appIconMap = {
'googledrive': googleDriveIcon?.icon_url,
'slack': slackIcon?.icon_url,
'gmail': gmailIcon?.icon_url,
'notion': notionIcon?.icon_url,
};
const { data: agent } = useAgent(selectedAgentId || '');
const { data: profiles } = useComposioProfiles();
// Feature flag for custom agents
const { enabled: customAgentsEnabled } = useFeatureFlag('custom_agents');
const isAppConnectedToAgent = (appSlug: string): boolean => {
if (!selectedAgentId || !agent?.custom_mcps || !profiles) 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;
});
};
useEffect(() => {
if (isLocalMode()) {
setCustomModels(getCustomModels());
}
}, []);
// Save custom models to localStorage whenever they change
useEffect(() => {
if (isLocalMode() && customModels.length > 0) {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(customModels));
}
}, [customModels]);
// Enhance model options with capabilities - using a Map to ensure uniqueness
const modelMap = new Map();
// First add all standard models to the map
modelOptions.forEach(model => {
modelMap.set(model.id, {
...model,
@ -103,12 +180,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
});
});
// Then add custom models from the current customModels state (not from props)
// This ensures we're using the most up-to-date list of custom models
if (isLocalMode()) {
// Get current custom models from state (not from storage)
customModels.forEach(model => {
// Only add if it doesn't exist or mark it as a custom model if it does
if (!modelMap.has(model.id)) {
modelMap.set(model.id, {
id: model.id,
@ -118,7 +191,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
isCustom: true
});
} else {
// If it already exists (rare case), mark it as a custom model
const existingModel = modelMap.get(model.id);
modelMap.set(model.id, {
...existingModel,
@ -128,22 +200,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
});
}
// Convert map back to array
const enhancedModelOptions = Array.from(modelMap.values());
// Filter models based on search query
const filteredOptions = enhancedModelOptions.filter((opt) =>
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
opt.id.toLowerCase().includes(searchQuery.toLowerCase())
);
// Get free models from modelOptions (helper function)
const getFreeModels = () => modelOptions.filter(m => !m.requiresSubscription).map(m => m.id);
// No sorting needed - models are already sorted in the hook
const sortedModels = filteredOptions;
// Simplified premium models function - just filter without sorting
const getPremiumModels = () => {
return modelOptions
.filter(m => m.requiresSubscription)
@ -153,12 +219,10 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}));
}
// Make sure model IDs are unique for rendering
const getUniqueModelKey = (model: any, index: number): string => {
return `model-${model.id}-${index}`;
};
// Map models to ensure unique IDs for React keys
const uniqueModels = sortedModels.map((model, index) => ({
...model,
uniqueKey: getUniqueModelKey(model, index)
@ -179,17 +243,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
const handleSelect = (id: string) => {
// Check if it's a custom model
const isCustomModel = customModels.some(model => model.id === id);
// Custom models are always accessible in local mode
if (isCustomModel && isLocalMode()) {
onModelChange(id);
setIsOpen(false);
return;
}
// Otherwise use the regular canAccessModel check
if (canAccessModel(id)) {
onModelChange(id);
setIsOpen(false);
@ -233,38 +292,30 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const shouldDisplayAll = (!isLocalMode() && subscriptionStatus === 'no_subscription') && premiumModels.length > 0;
// Handle opening the custom model dialog
const openAddCustomModelDialog = (e?: React.MouseEvent) => {
e?.stopPropagation();
setDialogInitialData({ id: '', label: '' });
setDialogMode('add');
setIsCustomModelDialogOpen(true);
setIsOpen(false); // Close dropdown when opening modal
setIsOpen(false);
};
// Handle opening the edit model dialog
const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => {
e?.stopPropagation();
setDialogInitialData({ id: model.id, label: model.label });
setEditingModelId(model.id); // Keep the original ID with prefix for reference
setEditingModelId(model.id);
setDialogMode('edit');
setIsCustomModelDialogOpen(true);
setIsOpen(false); // Close dropdown when opening modal
setIsOpen(false);
};
// Handle saving a custom model
const handleSaveCustomModel = (formData: CustomModelFormData) => {
// Get model ID without automatically adding prefix
const modelId = formData.id.trim();
// Generate display name based on model ID (remove prefix if present for display name)
const displayId = modelId.startsWith('openrouter/') ? modelId.replace('openrouter/', '') : modelId;
const modelLabel = formData.label.trim() || formatModelName(displayId);
if (!modelId) return;
// Check for duplicates - only for new models or if ID changed during edit
const checkId = modelId;
if (customModels.some(model =>
model.id === checkId && (dialogMode === 'add' || model.id !== editingModelId))) {
@ -272,44 +323,32 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
return;
}
// First close the dialog to prevent UI issues
closeCustomModelDialog();
// Create the new model object
const newModel = { id: modelId, label: modelLabel };
// Update models array (add new or update existing)
const updatedModels = dialogMode === 'add'
? [...customModels, newModel]
: customModels.map(model => model.id === editingModelId ? newModel : model);
// Save to localStorage first
try {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels));
} catch (error) {
console.error('Failed to save custom models to localStorage:', error);
}
// Update state with new models
setCustomModels(updatedModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) {
refreshCustomModels();
}
// Handle model selection changes
if (dialogMode === 'add') {
// Always select newly added models
onModelChange(modelId);
// Also save the selection to localStorage
try {
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
} catch (error) {
console.warn('Failed to save selected model to localStorage:', error);
}
} else if (selectedModel === editingModelId) {
// For edits, only update if the edited model was selected
onModelChange(modelId);
try {
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
@ -317,23 +356,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
console.warn('Failed to save selected model to localStorage:', error);
}
}
// Force dropdown to close to ensure fresh data on next open
setIsOpen(false);
// Force a UI refresh by delaying the state update
setTimeout(() => {
setHighlightedIndex(-1);
}, 0);
};
// Handle closing the custom model dialog
const closeCustomModelDialog = () => {
setIsCustomModelDialogOpen(false);
setDialogInitialData({ id: '', label: '' });
setEditingModelId(null);
// Improved fix for pointer-events issue: ensure dialog closes properly
document.body.classList.remove('overflow-hidden');
const bodyStyle = document.body.style;
setTimeout(() => {
@ -342,15 +374,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}, 150);
};
// Handle deleting a custom model
const handleDeleteCustomModel = (modelId: string, e?: React.MouseEvent) => {
e?.stopPropagation();
e?.preventDefault();
// Filter out the model to delete
const updatedCustomModels = customModels.filter(model => model.id !== modelId);
// Update localStorage first to ensure data consistency
if (isLocalMode() && typeof window !== 'undefined') {
try {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedCustomModels));
@ -358,16 +386,10 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
console.error('Failed to update custom models in localStorage:', error);
}
}
// Update state with the new list
setCustomModels(updatedCustomModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) {
refreshCustomModels();
}
// Check if we need to change the selected model
if (selectedModel === modelId) {
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
onModelChange(defaultModel);
@ -377,17 +399,9 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
console.warn('Failed to update selected model in localStorage:', error);
}
}
// Force dropdown to close
setIsOpen(false);
// Update the modelMap and recreate enhancedModelOptions on next render
// This will force a complete refresh of the model list
setTimeout(() => {
// Force React to fully re-evaluate the component with fresh data
setHighlightedIndex(-1);
// Reopen dropdown with fresh data if it was open
if (isOpen) {
setIsOpen(false);
setTimeout(() => setIsOpen(true), 50);
@ -395,15 +409,28 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}, 10);
};
const handleAppSelect = (app: typeof PREDEFINED_APPS[0]) => {
setSelectedApp(app);
setShowComposioConnector(true);
setIsOpen(false);
};
const handleComposioComplete = (profileId: string, appName: string, appSlug: string) => {
console.log('Composio integration complete:', { profileId, appName, appSlug, selectedAgentId });
setShowComposioConnector(false);
setSelectedApp(null);
};
const handleOpenIntegrationsManager = () => {
setShowIntegrationsManager(true);
setIsOpen(false);
};
const renderModelOption = (opt: any, index: number) => {
// More accurate check for custom models - use the actual customModels array
// from both the opt.isCustom flag and by checking if it exists in customModels
const isCustom = Boolean(opt.isCustom) ||
(isLocalMode() && customModels.some(model => model.id === opt.id));
const accessible = isCustom ? true : canAccessModel(opt.id);
// Fix the highlighting logic to use the index parameter instead of searching in filteredOptions
const isHighlighted = index === highlightedIndex;
const isPremium = opt.requiresSubscription;
const isLowQuality = MODELS[opt.id]?.lowQuality || false;
@ -427,7 +454,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<span className="font-medium">{opt.label}</span>
</div>
<div className="flex items-center gap-2">
{/* Show capabilities */}
{isLowQuality && (
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
)}
@ -439,7 +465,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{isPremium && !accessible && (
<Crown className="h-3.5 w-3.5 text-blue-500" />
)}
{/* Custom model actions */}
{isLocalMode() && isCustom && (
<>
<button
@ -491,20 +516,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
);
};
// Update filtered options when customModels or search query changes
useEffect(() => {
// Force reset of enhancedModelOptions whenever customModels change
// The next render will regenerate enhancedModelOptions with the updated modelMap
setHighlightedIndex(-1);
setSearchQuery('');
// Force React to fully re-evaluate the component rendering
if (isOpen) {
// If dropdown is open, briefly close and reopen to force refresh
setIsOpen(false);
setTimeout(() => setIsOpen(true), 10);
}
}, [customModels, modelOptions]); // Also depend on modelOptions to refresh when parent changes
}, [customModels, modelOptions]);
return (
<div className="relative">
@ -532,22 +551,17 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent
align="end"
className="w-72 p-0 overflow-hidden"
sideOffset={4}
>
<div className="overflow-y-auto w-full scrollbar-hide relative">
{/* Completely separate views for subscribers and non-subscribers */}
{shouldDisplayAll ? (
/* No Subscription View */
<div>
{/* Available Models Section - ONLY hardcoded free models */}
<div className="px-3 py-3 text-xs font-medium text-muted-foreground">
Available Models
</div>
{/* Only show free models */}
{uniqueModels
.filter(m =>
!m.requiresSubscription &&
@ -561,7 +575,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<div className='w-full'>
<DropdownMenuItem
className={cn(
"text-sm mx-2 my-0.5 px-3 py-2 flex items-center justify-between cursor-pointer",
"text-sm mx-2 my-0.5 px-3 rounded-lg py-2 flex items-center justify-between cursor-pointer",
selectedModel === model.id && "bg-accent"
)}
onClick={() => onModelChange(model.id)}
@ -571,7 +585,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<span className="font-medium">{model.label}</span>
</div>
<div className="flex items-center gap-2">
{/* Show capabilities */}
{(MODELS[model.id]?.lowQuality || false) && (
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
)}
@ -596,15 +609,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipProvider>
))
}
{/* Premium Models Section */}
<div className="mt-4 border-t border-border pt-2">
<div className="px-3 py-1.5 text-xs font-medium text-blue-500 flex items-center">
{/* <Crown className="h-3.5 w-3.5 mr-1.5" /> */}
<Crown className="h-3.5 w-3.5 mr-1.5" />
Additional Models
</div>
{/* Premium models container with paywall overlay */}
<div className="relative h-40 overflow-hidden px-2">
{getPremiumModels()
.filter(m =>
@ -619,13 +628,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<TooltipTrigger asChild>
<div className='w-full'>
<DropdownMenuItem
className="text-sm px-3 py-2 flex items-center justify-between opacity-70 cursor-pointer pointer-events-none"
className="text-sm px-3 rounded-lg py-2 flex items-center justify-between opacity-70 cursor-pointer pointer-events-none"
>
<div className="flex items-center">
<span className="font-medium">{model.label}</span>
</div>
<div className="flex items-center gap-2">
{/* Show capabilities */}
{MODELS[model.id]?.recommended && (
<span className="text-xs px-1.5 py-0.5 rounded-sm bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 font-medium whitespace-nowrap">
Recommended
@ -643,8 +651,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipProvider>
))
}
{/* Absolute positioned paywall overlay with gradient fade */}
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/95 to-transparent flex items-end justify-center">
<div className="w-full p-3">
<div className="rounded-xl bg-gradient-to-br from-blue-50/80 to-blue-200/70 dark:from-blue-950/40 dark:to-blue-900/30 shadow-sm border border-blue-200/50 dark:border-blue-800/50 p-3">
@ -670,83 +676,211 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</div>
</div>
) : (
/* Subscription or other status view */
<div className='max-h-[320px] overflow-y-auto w-full'>
<div className="px-3 py-3 flex justify-between items-center">
<span className="text-xs font-medium text-muted-foreground">All Models</span>
{isLocalMode() && (
<div className="flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/settings/env-manager"
className="h-6 w-6 p-0 flex items-center justify-center"
>
<KeyRound className="h-3.5 w-3.5" />
</Link>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Local .Env Manager
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
openAddCustomModelDialog(e);
}}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Add a custom model
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className='max-h-[400px] overflow-y-auto w-full'>
<div className="p-2">
<DropdownMenuSub>
<DropdownMenuSubTrigger className="flex items-center rounded-lg gap-2 px-2 py-2">
<Cpu className="h-4 w-4" />
<span className="font-medium">Models</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="w-72">
<div className="px-3 py-2 flex justify-between items-center">
<span className="text-xs font-medium text-muted-foreground">All Models</span>
{isLocalMode() && (
<div className="flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/settings/env-manager"
className="h-6 w-6 p-0 flex items-center justify-center"
>
<KeyRound className="h-3.5 w-3.5" />
</Link>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Local .Env Manager
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
openAddCustomModelDialog(e);
}}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Add a custom model
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
<div className="px-2 py-1">
<div className="relative flex items-center">
<Search className="absolute left-2.5 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<input
ref={searchInputRef}
type="text"
placeholder="Search models..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchInputKeyDown}
className="w-full h-8 px-8 py-1 rounded-lg text-sm focus:outline-none bg-muted"
/>
</div>
</div>
{uniqueModels
.filter(m =>
m.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
m.id.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((model, index) => renderModelOption(model, index))}
{uniqueModels.length === 0 && (
<div className="text-sm text-center py-4 text-muted-foreground">
No models match your search
</div>
)}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{/* Quick Connect Apps Submenu - only show if custom_agents is enabled */}
{customAgentsEnabled && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="flex rounded-lg items-center gap-2 px-2 py-2">
<div className="flex items-center gap-2">
<Brain className="h-4 w-4" />
<span className="font-medium">Quick Connect</span>
</div>
<div className="flex items-center space-x-0.5">
{googleDriveIcon?.icon_url && slackIcon?.icon_url && notionIcon?.icon_url ? (
<>
<img src={googleDriveIcon.icon_url} className="w-5 h-5" alt="Google Drive" />
<img src={slackIcon.icon_url} className="w-4 h-4" alt="Slack" />
<img src={notionIcon.icon_url} className="w-4 h-4" alt="Notion" />
</>
) : (
<>
<Skeleton className="w-4 h-4 rounded-md" />
<Skeleton className="w-4 h-4 rounded-md" />
<Skeleton className="w-4 h-4 rounded-md" />
</>
)}
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="w-64 rounded-xl">
<div className="px-3 py-2 text-xs font-medium text-muted-foreground">
Popular Apps
</div>
<div className="px-1 space-y-1">
{!selectedAgentId || !agent || !profiles ? (
<>
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="px-3 py-2 mx-0 my-0.5 flex items-center justify-between">
<div className="flex items-center">
<Skeleton className="w-4 h-4 mr-2 rounded" />
<Skeleton className="w-20 h-4 rounded" />
</div>
<Skeleton className="w-4 h-4 rounded" />
</div>
))}
</>
) : (
PREDEFINED_APPS.map((app) => {
const isConnected = isAppConnectedToAgent(app.slug);
return (
<TooltipProvider key={app.id}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
className={cn(
"text-sm px-3 rounded-lg py-2 mx-0 my-0.5 flex items-center justify-between",
isConnected
? "opacity-60 cursor-not-allowed"
: "cursor-pointer hover:bg-accent/50"
)}
onClick={isConnected ? undefined : () => handleAppSelect(app)}
disabled={isConnected}
>
<div className="flex items-center">
{appIconMap[app.slug] ? (
<img src={appIconMap[app.slug]} alt={app.name} className="h-4 w-4 mr-2" />
) : (
<div className="w-4 h-4 mr-2 rounded bg-primary/10 flex items-center justify-center">
<span className="text-xs text-primary font-medium">{app.name.charAt(0)}</span>
</div>
)}
<span className="font-medium">{app.name}</span>
{isConnected && (
<span className="ml-2 text-xs px-1.5 py-0.5 rounded-sm bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300 font-medium">
Connected
</span>
)}
</div>
<div className="flex items-center gap-1">
{isConnected ? (
<Check className="h-3.5 w-3.5 text-green-500" />
) : (
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="left" className="text-xs max-w-xs">
<p>{isConnected ? `Manage ${app.name} tools` : app.description}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
})
)}
</div>
<div className="px-1 pt-2 border-t border-border/50 mt-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
className="text-sm px-3 rounded-lg py-2 mx-0 my-0.5 flex items-center justify-between cursor-pointer hover:bg-accent/50"
onClick={handleOpenIntegrationsManager}
>
<div className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<span className="font-medium">Discover more apps</span>
</div>
<ArrowUpRight className="h-3.5 w-3.5 text-muted-foreground" />
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="left" className="text-xs max-w-xs">
<p>Open full integrations manager</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
</div>
{uniqueModels
.filter(m =>
m.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
m.id.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((model, index) => renderModelOption(model, index))}
{uniqueModels.length === 0 && (
<div className="text-sm text-center py-4 text-muted-foreground">
No models match your search
</div>
)}
</div>
)}
</div>
{!shouldDisplayAll && <div className="px-3 py-2 border-t border-border">
<div className="relative flex items-center">
<Search className="absolute left-2.5 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<input
ref={searchInputRef}
type="text"
placeholder="Search models..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchInputKeyDown}
className="w-full h-8 px-8 py-1 rounded-lg text-sm focus:outline-none bg-muted"
/>
</div>
</div>}
</DropdownMenuContent>
</DropdownMenu>
{/* Custom Model Dialog - moved to separate component */}
<CustomModelDialog
isOpen={isCustomModelDialogOpen}
onClose={closeCustomModelDialog}
@ -754,7 +888,36 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
initialData={dialogInitialData}
mode={dialogMode}
/>
<Dialog open={showIntegrationsManager} onOpenChange={setShowIntegrationsManager}>
<DialogContent className="p-0 max-w-6xl h-[90vh] overflow-hidden">
<DialogHeader className="sr-only">
<DialogTitle>Integrations Manager</DialogTitle>
</DialogHeader>
<IntegrationsRegistry
showAgentSelector={true}
selectedAgentId={selectedAgentId}
onClose={() => setShowIntegrationsManager(false)}
/>
</DialogContent>
</Dialog>
{selectedApp && (
<ComposioConnector
app={{
slug: selectedApp.slug,
name: selectedApp.name,
description: selectedApp.description,
categories: ['productivity'],
tags: [],
auth_schemes: [],
logo: selectedAppIcon?.icon_url || ''
}}
agentId={selectedAgentId}
open={showComposioConnector}
onOpenChange={setShowComposioConnector}
onComplete={handleComposioComplete}
mode="full"
/>
)}
{paywallOpen && (
<PaywallDialog
open={true}
@ -773,4 +936,5 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
)}
</div>
);
};
};