mirror of https://github.com/kortix-ai/suna.git
quick connect option in model selector
This commit is contained in:
parent
2975772806
commit
3e683de620
|
@ -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__'):
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue