tsx import React, { useState, useRef, useEffect } from 'react' import { useAppStore } from '@/store/useAppStore' import { AI_MODELS, CATEGORY_NAMES, CATEGORY_DESCRIPTIONS, getModelsByCategory } from '@/utils/constants' import { AIModel, AICategory } from '@/types' import { ChevronDown, Search, Zap } from 'lucide-react' /** * Component for selecting AI models with categorized dropdown */ export function ModelSelector() { const { selectedModel, setSelectedModel } = useAppStore() const [isOpen, setIsOpen] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [selectedCategory, setSelectedCategory] = useState(null) const dropdownRef = useRef(null) const searchRef = useRef(null) /** * Filter models based on search term and category */ const filteredModels = AI_MODELS.filter(model => { const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase()) || model.description.toLowerCase().includes(searchTerm.toLowerCase()) const matchesCategory = !selectedCategory || model.category === selectedCategory return matchesSearch && matchesCategory }) /** * Close dropdown when clicking outside */ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, []) /** * Focus search input when dropdown opens */ useEffect(() => { if (isOpen && searchRef.current) { searchRef.current.focus() } }, [isOpen]) /** * Handle keyboard navigation */ const handleKeyDown = (event: React.KeyboardEvent) => { if (!isOpen && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault() setIsOpen(true) } else if (isOpen && event.key === 'Escape') { setIsOpen(false) } } /** * Handle model selection */ const handleModelSelect = (model: AIModel) => { setSelectedModel(model) setIsOpen(false) setSearchTerm('') setSelectedCategory(null) } /** * Handle category selection */ const handleCategorySelect = (category: AICategory) => { setSelectedCategory(selectedCategory === category ? null : category) } /** * Get selected model info */ const selectedModelInfo = selectedModel ? AI_MODELS.find(m => m.id === selectedModel) : null /** * Get category icon */ const getCategoryIcon = (category: AICategory) => { switch (category) { case 'llm': return case 'code': return case 'image': return case 'special': return } } return (
{/* Selected Model Display */}
{/* Dropdown Menu */} {isOpen && (
{/* Search Input */}
setSearchTerm(e.target.value)} className="w-full pl-9 pr-3 py-2 text-sm input-field" />
{/* Categories */}
{Object.entries(CATEGORY_NAMES).map(([categoryKey, categoryName]) => { const category = categoryKey as AICategory const categoryModels = getModelsByCategory(category) const filteredCategoryModels = categoryModels.filter(model => filteredModels.includes(model) ) if (filteredCategoryModels.length === 0) return null return (
{/* Category Header */} {/* Models in Category */} {(selectedCategory === category || selectedCategory === null) && (
{filteredCategoryModels.map((model) => ( ))}
)}
) })}
)}
{/* Selected Model Info */} {selectedModelInfo && (
{getCategoryIcon(selectedModelInfo.category)}

{selectedModelInfo.name}

{selectedModelInfo.description}

{CATEGORY_NAMES[selectedModelInfo.category]} {selectedModelInfo.maxTokens && ( Max: {(selectedModelInfo.maxTokens / 1000).toFixed(0)}k Tokens )} {selectedModelInfo.supportsStreaming && ( Streaming )}
)}
) }