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) logger.error(f"Failed to list categories: {e}", exc_info=True)
raise 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: try:
logger.info(f"Fetching toolkits with limit: {limit}, category: {category}") logger.info(f"Fetching toolkits with limit: {limit}, category: {category}")
if 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: 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', []) items = getattr(toolkits_response, 'items', [])
if hasattr(toolkits_response, '__dict__'): if hasattr(toolkits_response, '__dict__'):

View File

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

View File

@ -6,7 +6,18 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuPortal,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -14,7 +25,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button'; 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 { import {
ModelOption, ModelOption,
SubscriptionStatus, SubscriptionStatus,
@ -24,15 +35,47 @@ import {
DEFAULT_PREMIUM_MODEL_ID, DEFAULT_PREMIUM_MODEL_ID,
formatModelName, formatModelName,
getCustomModels, getCustomModels,
MODELS // Import the centralized MODELS constant MODELS
} from './_use-model-selection'; } from './_use-model-selection';
import { PaywallDialog } from '@/components/payment/paywall-dialog'; import { PaywallDialog } from '@/components/payment/paywall-dialog';
import { BillingModal } from '@/components/billing/billing-modal';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useRouter } from 'next/navigation';
import { isLocalMode } from '@/lib/config'; import { isLocalMode } from '@/lib/config';
import { CustomModelDialog, CustomModelFormData } from './custom-model-dialog'; import { CustomModelDialog, CustomModelFormData } from './custom-model-dialog';
import Link from 'next/link'; 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 { interface CustomModel {
id: string; id: string;
@ -50,6 +93,7 @@ interface ModelSelectorProps {
billingModalOpen: boolean; billingModalOpen: boolean;
setBillingModalOpen: (open: boolean) => void; setBillingModalOpen: (open: boolean) => void;
hasBorder?: boolean; hasBorder?: boolean;
selectedAgentId?: string;
} }
export const ModelSelector: React.FC<ModelSelectorProps> = ({ export const ModelSelector: React.FC<ModelSelectorProps> = ({
@ -62,6 +106,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
billingModalOpen, billingModalOpen,
setBillingModalOpen, setBillingModalOpen,
hasBorder = false, hasBorder = false,
selectedAgentId,
}) => { }) => {
const [paywallOpen, setPaywallOpen] = useState(false); const [paywallOpen, setPaywallOpen] = useState(false);
const [lockedModel, setLockedModel] = useState<string | null>(null); const [lockedModel, setLockedModel] = useState<string | null>(null);
@ -69,33 +114,65 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1); const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
// Custom models state
const [customModels, setCustomModels] = useState<CustomModel[]>([]); const [customModels, setCustomModels] = useState<CustomModel[]>([]);
const [isCustomModelDialogOpen, setIsCustomModelDialogOpen] = useState(false); const [isCustomModelDialogOpen, setIsCustomModelDialogOpen] = useState(false);
const [dialogInitialData, setDialogInitialData] = useState<CustomModelFormData>({ id: '', label: '' }); const [dialogInitialData, setDialogInitialData] = useState<CustomModelFormData>({ id: '', label: '' });
const [dialogMode, setDialogMode] = useState<'add' | 'edit'>('add'); const [dialogMode, setDialogMode] = useState<'add' | 'edit'>('add');
const [editingModelId, setEditingModelId] = useState<string | null>(null); 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(() => { useEffect(() => {
if (isLocalMode()) { if (isLocalMode()) {
setCustomModels(getCustomModels()); setCustomModels(getCustomModels());
} }
}, []); }, []);
// Save custom models to localStorage whenever they change
useEffect(() => { useEffect(() => {
if (isLocalMode() && customModels.length > 0) { if (isLocalMode() && customModels.length > 0) {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(customModels)); localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(customModels));
} }
}, [customModels]); }, [customModels]);
// Enhance model options with capabilities - using a Map to ensure uniqueness
const modelMap = new Map(); const modelMap = new Map();
// First add all standard models to the map
modelOptions.forEach(model => { modelOptions.forEach(model => {
modelMap.set(model.id, { modelMap.set(model.id, {
...model, ...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()) { if (isLocalMode()) {
// Get current custom models from state (not from storage)
customModels.forEach(model => { 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)) { if (!modelMap.has(model.id)) {
modelMap.set(model.id, { modelMap.set(model.id, {
id: model.id, id: model.id,
@ -118,7 +191,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
isCustom: true isCustom: true
}); });
} else { } else {
// If it already exists (rare case), mark it as a custom model
const existingModel = modelMap.get(model.id); const existingModel = modelMap.get(model.id);
modelMap.set(model.id, { modelMap.set(model.id, {
...existingModel, ...existingModel,
@ -128,22 +200,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}); });
} }
// Convert map back to array
const enhancedModelOptions = Array.from(modelMap.values()); const enhancedModelOptions = Array.from(modelMap.values());
// Filter models based on search query
const filteredOptions = enhancedModelOptions.filter((opt) => const filteredOptions = enhancedModelOptions.filter((opt) =>
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) || opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
opt.id.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); const getFreeModels = () => modelOptions.filter(m => !m.requiresSubscription).map(m => m.id);
// No sorting needed - models are already sorted in the hook
const sortedModels = filteredOptions; const sortedModels = filteredOptions;
// Simplified premium models function - just filter without sorting
const getPremiumModels = () => { const getPremiumModels = () => {
return modelOptions return modelOptions
.filter(m => m.requiresSubscription) .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 => { const getUniqueModelKey = (model: any, index: number): string => {
return `model-${model.id}-${index}`; return `model-${model.id}-${index}`;
}; };
// Map models to ensure unique IDs for React keys
const uniqueModels = sortedModels.map((model, index) => ({ const uniqueModels = sortedModels.map((model, index) => ({
...model, ...model,
uniqueKey: getUniqueModelKey(model, index) uniqueKey: getUniqueModelKey(model, index)
@ -179,17 +243,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model'; enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
const handleSelect = (id: string) => { const handleSelect = (id: string) => {
// Check if it's a custom model
const isCustomModel = customModels.some(model => model.id === id); const isCustomModel = customModels.some(model => model.id === id);
// Custom models are always accessible in local mode
if (isCustomModel && isLocalMode()) { if (isCustomModel && isLocalMode()) {
onModelChange(id); onModelChange(id);
setIsOpen(false); setIsOpen(false);
return; return;
} }
// Otherwise use the regular canAccessModel check
if (canAccessModel(id)) { if (canAccessModel(id)) {
onModelChange(id); onModelChange(id);
setIsOpen(false); setIsOpen(false);
@ -233,38 +292,30 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const shouldDisplayAll = (!isLocalMode() && subscriptionStatus === 'no_subscription') && premiumModels.length > 0; const shouldDisplayAll = (!isLocalMode() && subscriptionStatus === 'no_subscription') && premiumModels.length > 0;
// Handle opening the custom model dialog
const openAddCustomModelDialog = (e?: React.MouseEvent) => { const openAddCustomModelDialog = (e?: React.MouseEvent) => {
e?.stopPropagation(); e?.stopPropagation();
setDialogInitialData({ id: '', label: '' }); setDialogInitialData({ id: '', label: '' });
setDialogMode('add'); setDialogMode('add');
setIsCustomModelDialogOpen(true); setIsCustomModelDialogOpen(true);
setIsOpen(false); // Close dropdown when opening modal setIsOpen(false);
}; };
// Handle opening the edit model dialog
const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => { const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => {
e?.stopPropagation(); e?.stopPropagation();
setDialogInitialData({ id: model.id, label: model.label }); setDialogInitialData({ id: model.id, label: model.label });
setEditingModelId(model.id); // Keep the original ID with prefix for reference setEditingModelId(model.id);
setDialogMode('edit'); setDialogMode('edit');
setIsCustomModelDialogOpen(true); setIsCustomModelDialogOpen(true);
setIsOpen(false); // Close dropdown when opening modal setIsOpen(false);
}; };
// Handle saving a custom model
const handleSaveCustomModel = (formData: CustomModelFormData) => { const handleSaveCustomModel = (formData: CustomModelFormData) => {
// Get model ID without automatically adding prefix
const modelId = formData.id.trim(); 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 displayId = modelId.startsWith('openrouter/') ? modelId.replace('openrouter/', '') : modelId;
const modelLabel = formData.label.trim() || formatModelName(displayId); const modelLabel = formData.label.trim() || formatModelName(displayId);
if (!modelId) return; if (!modelId) return;
// Check for duplicates - only for new models or if ID changed during edit
const checkId = modelId; const checkId = modelId;
if (customModels.some(model => if (customModels.some(model =>
model.id === checkId && (dialogMode === 'add' || model.id !== editingModelId))) { model.id === checkId && (dialogMode === 'add' || model.id !== editingModelId))) {
@ -272,44 +323,32 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
return; return;
} }
// First close the dialog to prevent UI issues
closeCustomModelDialog(); closeCustomModelDialog();
// Create the new model object
const newModel = { id: modelId, label: modelLabel }; const newModel = { id: modelId, label: modelLabel };
// Update models array (add new or update existing)
const updatedModels = dialogMode === 'add' const updatedModels = dialogMode === 'add'
? [...customModels, newModel] ? [...customModels, newModel]
: customModels.map(model => model.id === editingModelId ? newModel : model); : customModels.map(model => model.id === editingModelId ? newModel : model);
// Save to localStorage first
try { try {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels)); localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels));
} catch (error) { } catch (error) {
console.error('Failed to save custom models to localStorage:', error); console.error('Failed to save custom models to localStorage:', error);
} }
// Update state with new models
setCustomModels(updatedModels); setCustomModels(updatedModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) { if (refreshCustomModels) {
refreshCustomModels(); refreshCustomModels();
} }
// Handle model selection changes
if (dialogMode === 'add') { if (dialogMode === 'add') {
// Always select newly added models
onModelChange(modelId); onModelChange(modelId);
// Also save the selection to localStorage
try { try {
localStorage.setItem(STORAGE_KEY_MODEL, modelId); localStorage.setItem(STORAGE_KEY_MODEL, modelId);
} catch (error) { } catch (error) {
console.warn('Failed to save selected model to localStorage:', error); console.warn('Failed to save selected model to localStorage:', error);
} }
} else if (selectedModel === editingModelId) { } else if (selectedModel === editingModelId) {
// For edits, only update if the edited model was selected
onModelChange(modelId); onModelChange(modelId);
try { try {
localStorage.setItem(STORAGE_KEY_MODEL, modelId); 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); console.warn('Failed to save selected model to localStorage:', error);
} }
} }
// Force dropdown to close to ensure fresh data on next open
setIsOpen(false); setIsOpen(false);
// Force a UI refresh by delaying the state update
setTimeout(() => { setTimeout(() => {
setHighlightedIndex(-1); setHighlightedIndex(-1);
}, 0); }, 0);
}; };
// Handle closing the custom model dialog
const closeCustomModelDialog = () => { const closeCustomModelDialog = () => {
setIsCustomModelDialogOpen(false); setIsCustomModelDialogOpen(false);
setDialogInitialData({ id: '', label: '' }); setDialogInitialData({ id: '', label: '' });
setEditingModelId(null); setEditingModelId(null);
// Improved fix for pointer-events issue: ensure dialog closes properly
document.body.classList.remove('overflow-hidden'); document.body.classList.remove('overflow-hidden');
const bodyStyle = document.body.style; const bodyStyle = document.body.style;
setTimeout(() => { setTimeout(() => {
@ -342,15 +374,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}, 150); }, 150);
}; };
// Handle deleting a custom model
const handleDeleteCustomModel = (modelId: string, e?: React.MouseEvent) => { const handleDeleteCustomModel = (modelId: string, e?: React.MouseEvent) => {
e?.stopPropagation(); e?.stopPropagation();
e?.preventDefault(); e?.preventDefault();
// Filter out the model to delete
const updatedCustomModels = customModels.filter(model => model.id !== modelId); const updatedCustomModels = customModels.filter(model => model.id !== modelId);
// Update localStorage first to ensure data consistency
if (isLocalMode() && typeof window !== 'undefined') { if (isLocalMode() && typeof window !== 'undefined') {
try { try {
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedCustomModels)); 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); console.error('Failed to update custom models in localStorage:', error);
} }
} }
// Update state with the new list
setCustomModels(updatedCustomModels); setCustomModels(updatedCustomModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) { if (refreshCustomModels) {
refreshCustomModels(); refreshCustomModels();
} }
// Check if we need to change the selected model
if (selectedModel === modelId) { if (selectedModel === modelId) {
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
onModelChange(defaultModel); onModelChange(defaultModel);
@ -377,17 +399,9 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
console.warn('Failed to update selected model in localStorage:', error); console.warn('Failed to update selected model in localStorage:', error);
} }
} }
// Force dropdown to close
setIsOpen(false); setIsOpen(false);
// Update the modelMap and recreate enhancedModelOptions on next render
// This will force a complete refresh of the model list
setTimeout(() => { setTimeout(() => {
// Force React to fully re-evaluate the component with fresh data
setHighlightedIndex(-1); setHighlightedIndex(-1);
// Reopen dropdown with fresh data if it was open
if (isOpen) { if (isOpen) {
setIsOpen(false); setIsOpen(false);
setTimeout(() => setIsOpen(true), 50); setTimeout(() => setIsOpen(true), 50);
@ -395,15 +409,28 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}, 10); }, 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) => { 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) || const isCustom = Boolean(opt.isCustom) ||
(isLocalMode() && customModels.some(model => model.id === opt.id)); (isLocalMode() && customModels.some(model => model.id === opt.id));
const accessible = isCustom ? true : canAccessModel(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 isHighlighted = index === highlightedIndex;
const isPremium = opt.requiresSubscription; const isPremium = opt.requiresSubscription;
const isLowQuality = MODELS[opt.id]?.lowQuality || false; const isLowQuality = MODELS[opt.id]?.lowQuality || false;
@ -427,7 +454,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<span className="font-medium">{opt.label}</span> <span className="font-medium">{opt.label}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Show capabilities */}
{isLowQuality && ( {isLowQuality && (
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" /> <AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
)} )}
@ -439,7 +465,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
{isPremium && !accessible && ( {isPremium && !accessible && (
<Crown className="h-3.5 w-3.5 text-blue-500" /> <Crown className="h-3.5 w-3.5 text-blue-500" />
)} )}
{/* Custom model actions */}
{isLocalMode() && isCustom && ( {isLocalMode() && isCustom && (
<> <>
<button <button
@ -491,20 +516,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
); );
}; };
// Update filtered options when customModels or search query changes
useEffect(() => { useEffect(() => {
// Force reset of enhancedModelOptions whenever customModels change
// The next render will regenerate enhancedModelOptions with the updated modelMap
setHighlightedIndex(-1); setHighlightedIndex(-1);
setSearchQuery(''); setSearchQuery('');
// Force React to fully re-evaluate the component rendering
if (isOpen) { if (isOpen) {
// If dropdown is open, briefly close and reopen to force refresh
setIsOpen(false); setIsOpen(false);
setTimeout(() => setIsOpen(true), 10); setTimeout(() => setIsOpen(true), 10);
} }
}, [customModels, modelOptions]); // Also depend on modelOptions to refresh when parent changes }, [customModels, modelOptions]);
return ( return (
<div className="relative"> <div className="relative">
@ -532,22 +551,17 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="w-72 p-0 overflow-hidden" className="w-72 p-0 overflow-hidden"
sideOffset={4} sideOffset={4}
> >
<div className="overflow-y-auto w-full scrollbar-hide relative"> <div className="overflow-y-auto w-full scrollbar-hide relative">
{/* Completely separate views for subscribers and non-subscribers */}
{shouldDisplayAll ? ( {shouldDisplayAll ? (
/* No Subscription View */
<div> <div>
{/* Available Models Section - ONLY hardcoded free models */}
<div className="px-3 py-3 text-xs font-medium text-muted-foreground"> <div className="px-3 py-3 text-xs font-medium text-muted-foreground">
Available Models Available Models
</div> </div>
{/* Only show free models */}
{uniqueModels {uniqueModels
.filter(m => .filter(m =>
!m.requiresSubscription && !m.requiresSubscription &&
@ -561,7 +575,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<div className='w-full'> <div className='w-full'>
<DropdownMenuItem <DropdownMenuItem
className={cn( 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" selectedModel === model.id && "bg-accent"
)} )}
onClick={() => onModelChange(model.id)} onClick={() => onModelChange(model.id)}
@ -571,7 +585,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<span className="font-medium">{model.label}</span> <span className="font-medium">{model.label}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Show capabilities */}
{(MODELS[model.id]?.lowQuality || false) && ( {(MODELS[model.id]?.lowQuality || false) && (
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" /> <AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
)} )}
@ -596,15 +609,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipProvider> </TooltipProvider>
)) ))
} }
{/* Premium Models Section */}
<div className="mt-4 border-t border-border pt-2"> <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"> <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 Additional Models
</div> </div>
{/* Premium models container with paywall overlay */}
<div className="relative h-40 overflow-hidden px-2"> <div className="relative h-40 overflow-hidden px-2">
{getPremiumModels() {getPremiumModels()
.filter(m => .filter(m =>
@ -619,13 +628,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className='w-full'> <div className='w-full'>
<DropdownMenuItem <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"> <div className="flex items-center">
<span className="font-medium">{model.label}</span> <span className="font-medium">{model.label}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Show capabilities */}
{MODELS[model.id]?.recommended && ( {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"> <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 Recommended
@ -643,8 +651,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</TooltipProvider> </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="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="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"> <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>
</div> </div>
) : ( ) : (
/* Subscription or other status view */ <div className='max-h-[400px] overflow-y-auto w-full'>
<div className='max-h-[320px] overflow-y-auto w-full'> <div className="p-2">
<div className="px-3 py-3 flex justify-between items-center"> <DropdownMenuSub>
<span className="text-xs font-medium text-muted-foreground">All Models</span> <DropdownMenuSubTrigger className="flex items-center rounded-lg gap-2 px-2 py-2">
{isLocalMode() && ( <Cpu className="h-4 w-4" />
<div className="flex items-center gap-1"> <span className="font-medium">Models</span>
<TooltipProvider> </DropdownMenuSubTrigger>
<Tooltip> <DropdownMenuPortal>
<TooltipTrigger asChild> <DropdownMenuSubContent className="w-72">
<Link <div className="px-3 py-2 flex justify-between items-center">
href="/settings/env-manager" <span className="text-xs font-medium text-muted-foreground">All Models</span>
className="h-6 w-6 p-0 flex items-center justify-center" {isLocalMode() && (
> <div className="flex items-center gap-1">
<KeyRound className="h-3.5 w-3.5" /> <TooltipProvider>
</Link> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent side="bottom" className="text-xs"> <Link
Local .Env Manager href="/settings/env-manager"
</TooltipContent> className="h-6 w-6 p-0 flex items-center justify-center"
</Tooltip> >
</TooltipProvider> <KeyRound className="h-3.5 w-3.5" />
<TooltipProvider> </Link>
<Tooltip> </TooltipTrigger>
<TooltipTrigger asChild> <TooltipContent side="bottom" className="text-xs">
<Button Local .Env Manager
size="sm" </TooltipContent>
variant="ghost" </Tooltip>
className="h-6 w-6 p-0" </TooltipProvider>
onClick={(e) => { <TooltipProvider>
e.stopPropagation(); <Tooltip>
openAddCustomModelDialog(e); <TooltipTrigger asChild>
}} <Button
> size="sm"
<Plus className="h-3.5 w-3.5" /> variant="ghost"
</Button> className="h-6 w-6 p-0"
</TooltipTrigger> onClick={(e) => {
<TooltipContent side="bottom" className="text-xs"> e.stopPropagation();
Add a custom model openAddCustomModelDialog(e);
</TooltipContent> }}
</Tooltip> >
</TooltipProvider> <Plus className="h-3.5 w-3.5" />
</div> </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> </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>
)} )}
</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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Custom Model Dialog - moved to separate component */}
<CustomModelDialog <CustomModelDialog
isOpen={isCustomModelDialogOpen} isOpen={isCustomModelDialogOpen}
onClose={closeCustomModelDialog} onClose={closeCustomModelDialog}
@ -754,7 +888,36 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
initialData={dialogInitialData} initialData={dialogInitialData}
mode={dialogMode} 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 && ( {paywallOpen && (
<PaywallDialog <PaywallDialog
open={true} open={true}
@ -774,3 +937,4 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
</div> </div>
); );
}; };