| | 'use client'; |
| |
|
| | import React, { useState, useRef, useEffect } from 'react'; |
| | import { |
| | DropdownMenu, |
| | DropdownMenuContent, |
| | DropdownMenuItem, |
| | DropdownMenuTrigger, |
| | } from '@/components/ui/dropdown-menu'; |
| | import { |
| | Tooltip, |
| | TooltipContent, |
| | TooltipProvider, |
| | TooltipTrigger, |
| | } from '@/components/ui/tooltip'; |
| | import { Button } from '@/components/ui/button'; |
| | import { Check, ChevronDown, Search, AlertTriangle, Crown, ArrowUpRight, Brain, Plus, Edit, Trash, Cpu } from 'lucide-react'; |
| | import { |
| | ModelOption, |
| | SubscriptionStatus, |
| | STORAGE_KEY_MODEL, |
| | STORAGE_KEY_CUSTOM_MODELS, |
| | DEFAULT_FREE_MODEL_ID, |
| | DEFAULT_PREMIUM_MODEL_ID, |
| | formatModelName, |
| | getCustomModels, |
| | 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'; |
| |
|
| | interface CustomModel { |
| | id: string; |
| | label: string; |
| | } |
| |
|
| |
|
| | interface ModelSelectorProps { |
| | selectedModel: string; |
| | onModelChange: (modelId: string) => void; |
| | modelOptions: ModelOption[]; |
| | canAccessModel: (modelId: string) => boolean; |
| | subscriptionStatus: SubscriptionStatus; |
| | refreshCustomModels?: () => void; |
| | billingModalOpen: boolean; |
| | setBillingModalOpen: (open: boolean) => void; |
| | hasBorder?: boolean; |
| | } |
| |
|
| | export const ModelSelector: React.FC<ModelSelectorProps> = ({ |
| | selectedModel, |
| | onModelChange, |
| | modelOptions, |
| | canAccessModel, |
| | subscriptionStatus, |
| | refreshCustomModels, |
| | billingModalOpen, |
| | setBillingModalOpen, |
| | hasBorder = false, |
| | }) => { |
| | const [paywallOpen, setPaywallOpen] = useState(false); |
| | const [lockedModel, setLockedModel] = useState<string | null>(null); |
| | const [isOpen, setIsOpen] = useState(false); |
| | const [searchQuery, setSearchQuery] = useState(''); |
| | const [highlightedIndex, setHighlightedIndex] = useState<number>(-1); |
| | const searchInputRef = useRef<HTMLInputElement>(null); |
| | const router = useRouter(); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | useEffect(() => { |
| | if (isLocalMode()) { |
| | setCustomModels(getCustomModels()); |
| | } |
| | }, []); |
| |
|
| | |
| | useEffect(() => { |
| | if (isLocalMode() && customModels.length > 0) { |
| | localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(customModels)); |
| | } |
| | }, [customModels]); |
| |
|
| | |
| | const modelMap = new Map(); |
| |
|
| | |
| | modelOptions.forEach(model => { |
| | modelMap.set(model.id, { |
| | ...model, |
| | isCustom: false |
| | }); |
| | }); |
| |
|
| | |
| | |
| | if (isLocalMode()) { |
| | |
| | customModels.forEach(model => { |
| | |
| | if (!modelMap.has(model.id)) { |
| | modelMap.set(model.id, { |
| | id: model.id, |
| | label: model.label || formatModelName(model.id), |
| | requiresSubscription: false, |
| | top: false, |
| | isCustom: true |
| | }); |
| | } else { |
| | |
| | const existingModel = modelMap.get(model.id); |
| | modelMap.set(model.id, { |
| | ...existingModel, |
| | isCustom: true |
| | }); |
| | } |
| | }); |
| | } |
| |
|
| | |
| | const enhancedModelOptions = Array.from(modelMap.values()); |
| |
|
| | |
| | const filteredOptions = enhancedModelOptions.filter((opt) => |
| | opt.label.toLowerCase().includes(searchQuery.toLowerCase()) || |
| | opt.id.toLowerCase().includes(searchQuery.toLowerCase()) |
| | ); |
| |
|
| | |
| | const getFreeModels = () => modelOptions.filter(m => !m.requiresSubscription).map(m => m.id); |
| |
|
| | |
| | const sortedModels = filteredOptions; |
| |
|
| | |
| | const getPremiumModels = () => { |
| | return modelOptions |
| | .filter(m => m.requiresSubscription) |
| | .map((m, index) => ({ |
| | ...m, |
| | uniqueKey: getUniqueModelKey(m, index) |
| | })); |
| | } |
| |
|
| | |
| | const getUniqueModelKey = (model: any, index: number): string => { |
| | return `model-${model.id}-${index}`; |
| | }; |
| |
|
| | |
| | const uniqueModels = sortedModels.map((model, index) => ({ |
| | ...model, |
| | uniqueKey: getUniqueModelKey(model, index) |
| | })); |
| |
|
| | useEffect(() => { |
| | if (isOpen && searchInputRef.current) { |
| | setTimeout(() => { |
| | searchInputRef.current?.focus(); |
| | }, 50); |
| | } else { |
| | setSearchQuery(''); |
| | setHighlightedIndex(-1); |
| | } |
| | }, [isOpen]); |
| |
|
| | const selectedLabel = |
| | enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model'; |
| |
|
| | const handleSelect = (id: string) => { |
| | |
| | const isCustomModel = customModels.some(model => model.id === id); |
| |
|
| | |
| | if (isCustomModel && isLocalMode()) { |
| | onModelChange(id); |
| | setIsOpen(false); |
| | return; |
| | } |
| |
|
| | |
| | if (canAccessModel(id)) { |
| | onModelChange(id); |
| | setIsOpen(false); |
| | } else { |
| | setLockedModel(id); |
| | setPaywallOpen(true); |
| | } |
| | }; |
| |
|
| | const handleUpgradeClick = () => { |
| | setBillingModalOpen(true); |
| | }; |
| |
|
| | const closeDialog = () => { |
| | setPaywallOpen(false); |
| | setLockedModel(null); |
| | }; |
| |
|
| | const handleSearchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { |
| | e.stopPropagation(); |
| | if (e.key === 'ArrowDown') { |
| | e.preventDefault(); |
| | setHighlightedIndex((prev) => |
| | prev < filteredOptions.length - 1 ? prev + 1 : 0 |
| | ); |
| | } else if (e.key === 'ArrowUp') { |
| | e.preventDefault(); |
| | setHighlightedIndex((prev) => |
| | prev > 0 ? prev - 1 : filteredOptions.length - 1 |
| | ); |
| | } else if (e.key === 'Enter' && highlightedIndex >= 0) { |
| | e.preventDefault(); |
| | const selectedOption = filteredOptions[highlightedIndex]; |
| | if (selectedOption) { |
| | handleSelect(selectedOption.id); |
| | } |
| | } |
| | }; |
| |
|
| | const premiumModels = sortedModels.filter(m => !getFreeModels().some(id => m.id.includes(id))); |
| |
|
| | const shouldDisplayAll = (!isLocalMode() && subscriptionStatus === 'no_subscription') && premiumModels.length > 0; |
| |
|
| | |
| | const openAddCustomModelDialog = (e?: React.MouseEvent) => { |
| | e?.stopPropagation(); |
| | setDialogInitialData({ id: '', label: '' }); |
| | setDialogMode('add'); |
| | setIsCustomModelDialogOpen(true); |
| | setIsOpen(false); |
| | }; |
| |
|
| | |
| | const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => { |
| | e?.stopPropagation(); |
| |
|
| | setDialogInitialData({ id: model.id, label: model.label }); |
| | setEditingModelId(model.id); |
| | setDialogMode('edit'); |
| | setIsCustomModelDialogOpen(true); |
| | setIsOpen(false); |
| | }; |
| |
|
| | |
| | const handleSaveCustomModel = (formData: CustomModelFormData) => { |
| | |
| | const modelId = formData.id.trim(); |
| |
|
| | |
| | const displayId = modelId.startsWith('openrouter/') ? modelId.replace('openrouter/', '') : modelId; |
| | const modelLabel = formData.label.trim() || formatModelName(displayId); |
| |
|
| | if (!modelId) return; |
| |
|
| | |
| | const checkId = modelId; |
| | if (customModels.some(model => |
| | model.id === checkId && (dialogMode === 'add' || model.id !== editingModelId))) { |
| | console.error('A model with this ID already exists'); |
| | return; |
| | } |
| |
|
| | |
| | closeCustomModelDialog(); |
| |
|
| | |
| | const newModel = { id: modelId, label: modelLabel }; |
| |
|
| | |
| | const updatedModels = dialogMode === 'add' |
| | ? [...customModels, newModel] |
| | : customModels.map(model => model.id === editingModelId ? newModel : model); |
| |
|
| | |
| | try { |
| | localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels)); |
| | } catch (error) { |
| | console.error('Failed to save custom models to localStorage:', error); |
| | } |
| |
|
| | |
| | setCustomModels(updatedModels); |
| |
|
| | |
| | if (refreshCustomModels) { |
| | refreshCustomModels(); |
| | } |
| |
|
| | |
| | if (dialogMode === 'add') { |
| | |
| | onModelChange(modelId); |
| | |
| | try { |
| | localStorage.setItem(STORAGE_KEY_MODEL, modelId); |
| | } catch (error) { |
| | console.warn('Failed to save selected model to localStorage:', error); |
| | } |
| | } else if (selectedModel === editingModelId) { |
| | |
| | onModelChange(modelId); |
| | try { |
| | localStorage.setItem(STORAGE_KEY_MODEL, modelId); |
| | } catch (error) { |
| | console.warn('Failed to save selected model to localStorage:', error); |
| | } |
| | } |
| |
|
| | |
| | setIsOpen(false); |
| |
|
| | |
| | setTimeout(() => { |
| | setHighlightedIndex(-1); |
| | }, 0); |
| | }; |
| |
|
| | |
| | const closeCustomModelDialog = () => { |
| | setIsCustomModelDialogOpen(false); |
| | setDialogInitialData({ id: '', label: '' }); |
| | setEditingModelId(null); |
| |
|
| | |
| | document.body.classList.remove('overflow-hidden'); |
| | const bodyStyle = document.body.style; |
| | setTimeout(() => { |
| | bodyStyle.pointerEvents = ''; |
| | bodyStyle.removeProperty('pointer-events'); |
| | }, 150); |
| | }; |
| |
|
| | |
| | const handleDeleteCustomModel = (modelId: string, e?: React.MouseEvent) => { |
| | e?.stopPropagation(); |
| | e?.preventDefault(); |
| |
|
| | |
| | const updatedCustomModels = customModels.filter(model => model.id !== modelId); |
| |
|
| | |
| | if (isLocalMode() && typeof window !== 'undefined') { |
| | try { |
| | localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedCustomModels)); |
| | } catch (error) { |
| | console.error('Failed to update custom models in localStorage:', error); |
| | } |
| | } |
| |
|
| | |
| | setCustomModels(updatedCustomModels); |
| |
|
| | |
| | if (refreshCustomModels) { |
| | refreshCustomModels(); |
| | } |
| |
|
| | |
| | if (selectedModel === modelId) { |
| | const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; |
| | onModelChange(defaultModel); |
| | try { |
| | localStorage.setItem(STORAGE_KEY_MODEL, defaultModel); |
| | } catch (error) { |
| | console.warn('Failed to update selected model in localStorage:', error); |
| | } |
| | } |
| |
|
| | |
| | setIsOpen(false); |
| |
|
| | |
| | |
| | setTimeout(() => { |
| | |
| | setHighlightedIndex(-1); |
| |
|
| | |
| | if (isOpen) { |
| | setIsOpen(false); |
| | setTimeout(() => setIsOpen(true), 50); |
| | } |
| | }, 10); |
| | }; |
| |
|
| | const renderModelOption = (opt: any, index: number) => { |
| | |
| | |
| | const isCustom = Boolean(opt.isCustom) || |
| | (isLocalMode() && customModels.some(model => model.id === opt.id)); |
| |
|
| | const accessible = isCustom ? true : canAccessModel(opt.id); |
| |
|
| | |
| | const isHighlighted = index === highlightedIndex; |
| | const isPremium = opt.requiresSubscription; |
| | const isLowQuality = MODELS[opt.id]?.lowQuality || false; |
| | const isRecommended = MODELS[opt.id]?.recommended || false; |
| |
|
| | return ( |
| | <TooltipProvider key={opt.uniqueKey || `model-${opt.id}-${index}`}> |
| | <Tooltip> |
| | <TooltipTrigger asChild> |
| | <div className='w-full'> |
| | <DropdownMenuItem |
| | className={cn( |
| | "text-sm px-3 py-2 mx-2 my-0.5 flex items-center justify-between cursor-pointer", |
| | isHighlighted && "bg-accent", |
| | !accessible && "opacity-70" |
| | )} |
| | onClick={() => handleSelect(opt.id)} |
| | onMouseEnter={() => setHighlightedIndex(index)} |
| | > |
| | <div className="flex items-center"> |
| | <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" /> |
| | )} |
| | {isRecommended && ( |
| | <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"> |
| | Recommended |
| | </span> |
| | )} |
| | {isPremium && !accessible && ( |
| | <Crown className="h-3.5 w-3.5 text-blue-500" /> |
| | )} |
| | {/* Custom model actions */} |
| | {isLocalMode() && isCustom && ( |
| | <> |
| | <button |
| | onClick={(e) => { |
| | e.stopPropagation(); |
| | openEditCustomModelDialog(opt, e); |
| | }} |
| | className="text-muted-foreground hover:text-foreground" |
| | > |
| | <Edit className="h-3.5 w-3.5" /> |
| | </button> |
| | <button |
| | onClick={(e) => { |
| | e.stopPropagation(); |
| | handleDeleteCustomModel(opt.id, e); |
| | }} |
| | className="text-muted-foreground hover:text-red-500" |
| | > |
| | <Trash className="h-3.5 w-3.5" /> |
| | </button> |
| | </> |
| | )} |
| | {selectedModel === opt.id && ( |
| | <Check className="h-4 w-4 text-blue-500" /> |
| | )} |
| | </div> |
| | </DropdownMenuItem> |
| | </div> |
| | </TooltipTrigger> |
| | {!accessible ? ( |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Requires subscription to access premium model</p> |
| | </TooltipContent> |
| | ) : isLowQuality ? ( |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Not recommended for complex tasks</p> |
| | </TooltipContent> |
| | ) : isRecommended ? ( |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Recommended for optimal performance</p> |
| | </TooltipContent> |
| | ) : isCustom ? ( |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Custom model</p> |
| | </TooltipContent> |
| | ) : null} |
| | </Tooltip> |
| | </TooltipProvider> |
| | ); |
| | }; |
| |
|
| | |
| | useEffect(() => { |
| | |
| | |
| | setHighlightedIndex(-1); |
| | setSearchQuery(''); |
| |
|
| | |
| | if (isOpen) { |
| | |
| | setIsOpen(false); |
| | setTimeout(() => setIsOpen(true), 10); |
| | } |
| | }, [customModels, modelOptions]); |
| |
|
| | return ( |
| | <div className="relative"> |
| | <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> |
| | <TooltipProvider> |
| | <Tooltip> |
| | <TooltipTrigger asChild> |
| | <DropdownMenuTrigger asChild> |
| | <Button |
| | variant="ghost" |
| | size="sm" |
| | className="h-8 px-2 py-2 bg-transparent border-0 rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2" |
| | > |
| | <div className="relative flex items-center justify-center"> |
| | <Cpu className="h-4 w-4" /> |
| | {MODELS[selectedModel]?.lowQuality && ( |
| | <AlertTriangle className="h-2.5 w-2.5 text-amber-500 absolute -top-1 -right-1" /> |
| | )} |
| | </div> |
| | </Button> |
| | </DropdownMenuTrigger> |
| | </TooltipTrigger> |
| | <TooltipContent side="top" className="text-xs"> |
| | <p>Choose a model</p> |
| | </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 && |
| | (m.label.toLowerCase().includes(searchQuery.toLowerCase()) || |
| | m.id.toLowerCase().includes(searchQuery.toLowerCase())) |
| | ) |
| | .map((model, index) => ( |
| | <TooltipProvider key={model.uniqueKey || `model-${model.id}-${index}`}> |
| | <Tooltip> |
| | <TooltipTrigger asChild> |
| | <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", |
| | selectedModel === model.id && "bg-accent" |
| | )} |
| | onClick={() => onModelChange(model.id)} |
| | onMouseEnter={() => setHighlightedIndex(filteredOptions.indexOf(model))} |
| | > |
| | <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]?.lowQuality || false) && ( |
| | <AlertTriangle className="h-3.5 w-3.5 text-amber-500" /> |
| | )} |
| | {(MODELS[model.id]?.recommended || false) && ( |
| | <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"> |
| | Recommended |
| | </span> |
| | )} |
| | {selectedModel === model.id && ( |
| | <Check className="h-4 w-4 text-blue-500" /> |
| | )} |
| | </div> |
| | </DropdownMenuItem> |
| | </div> |
| | </TooltipTrigger> |
| | {MODELS[model.id]?.lowQuality && ( |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Basic model with limited capabilities</p> |
| | </TooltipContent> |
| | )} |
| | </Tooltip> |
| | </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" /> */} |
| | Additional Models |
| | </div> |
| | |
| | {/* Premium models container with paywall overlay */} |
| | <div className="relative h-40 overflow-hidden px-2"> |
| | {getPremiumModels() |
| | .filter(m => |
| | m.requiresSubscription && |
| | (m.label.toLowerCase().includes(searchQuery.toLowerCase()) || |
| | m.id.toLowerCase().includes(searchQuery.toLowerCase())) |
| | ) |
| | .slice(0, 3) |
| | .map((model, index) => ( |
| | <TooltipProvider key={model.uniqueKey || `model-${model.id}-${index}`}> |
| | <Tooltip> |
| | <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" |
| | > |
| | <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 |
| | </span> |
| | )} |
| | <Crown className="h-3.5 w-3.5 text-blue-500" /> |
| | </div> |
| | </DropdownMenuItem> |
| | </div> |
| | </TooltipTrigger> |
| | <TooltipContent side="left" className="text-xs max-w-xs"> |
| | <p>Requires subscription to access premium model</p> |
| | </TooltipContent> |
| | </Tooltip> |
| | </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"> |
| | <div className="flex flex-col space-y-2"> |
| | <div className="flex items-center"> |
| | <Crown className="h-4 w-4 text-blue-500 mr-2 flex-shrink-0" /> |
| | <div> |
| | <p className="text-sm font-medium">Unlock all models + higher limits</p> |
| | </div> |
| | </div> |
| | <Button |
| | size="sm" |
| | className="w-full h-8 font-medium" |
| | onClick={handleUpgradeClick} |
| | > |
| | Upgrade now |
| | </Button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </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() && ( |
| | <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> |
| | {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} |
| | onSave={handleSaveCustomModel} |
| | initialData={dialogInitialData} |
| | mode={dialogMode} |
| | /> |
| | |
| | {paywallOpen && ( |
| | <PaywallDialog |
| | open={true} |
| | onDialogClose={closeDialog} |
| | title="Premium Model" |
| | description={ |
| | lockedModel |
| | ? `Subscribe to access ${modelOptions.find( |
| | (m) => m.id === lockedModel |
| | )?.label}` |
| | : 'Subscribe to access premium models with enhanced capabilities' |
| | } |
| | ctaText="Subscribe Now" |
| | cancelText="Maybe Later" |
| | /> |
| | )} |
| | </div> |
| | ); |
| | }; |