import React, { useCallback, useEffect, useState } from 'react' import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react' import { useModel } from '../contexts/ModelContext' import { getModelInfo } from '../lib/huggingface' import { Heart, Download, ChevronDown, Check, ArrowDown, ArrowUp, Plus, Search, X } from 'lucide-react' import Tooltip from './Tooltip' import { ModelInfoResponse } from '@/types' type SortOption = 'likes' | 'downloads' | 'createdAt' | 'name' function ModelSelector() { const { models, setModelInfo, modelInfo, pipeline, isFetching, setIsFetching } = useModel() const [sortBy, setSortBy] = useState('createdAt') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') const [showCustomInput, setShowCustomInput] = useState(false) const [customModelName, setCustomModelName] = useState('') const [isLoadingCustomModel, setIsLoadingCustomModel] = useState(false) const [customModelError, setCustomModelError] = useState('') const [isCustomModel, setIsCustomModel] = useState(false) const formatNumber = (num: number) => { if (num >= 1000000000) { return (num / 1000000000).toFixed(1) + 'B' } else if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M' } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K' } return num.toString() } // Sort models based on current sort criteria const sortedModels = React.useMemo(() => { return [...models].sort((a, b) => { let comparison = 0 switch (sortBy) { case 'downloads': comparison = (a.downloads || 0) - (b.downloads || 0) break case 'createdAt': const dateA = new Date(a.createdAt || '').getTime() const dateB = new Date(b.createdAt || '').getTime() comparison = dateA - dateB break case 'name': comparison = a.id.localeCompare(b.id) break case 'likes': default: comparison = (a.likes || 0) - (b.likes || 0) break } return sortOrder === 'desc' ? -comparison : comparison }) }, [models, sortBy, sortOrder]) // Function to fetch detailed model info and set as selected const fetchAndSetModelInfo = useCallback( async (model: ModelInfoResponse, isCustom: boolean = false) => { try { const modelInfoResponse = await getModelInfo(model.id, pipeline) let parameters = 0 if (modelInfoResponse.safetensors) { const safetensors = modelInfoResponse.safetensors parameters = safetensors.parameters.BF16 || safetensors.parameters.F16 || safetensors.parameters.F32 || safetensors.parameters.total || 0 } const allTags = [...model.tags, ...modelInfoResponse.tags] const modelInfo = { id: model.id, name: modelInfoResponse.id || model.id, architecture: modelInfoResponse.config?.architectures?.[0] || 'Unknown', parameters, likes: modelInfoResponse.likes || 0, downloads: modelInfoResponse.downloads || 0, createdAt: modelInfoResponse.createdAt || '', isCompatible: modelInfoResponse.isCompatible, incompatibilityReason: modelInfoResponse.incompatibilityReason, supportedQuantizations: modelInfoResponse.supportedQuantizations, baseId: modelInfoResponse.baseId, readme: modelInfoResponse.readme, hasChatTemplate: Boolean( modelInfoResponse.config?.tokenizer_config?.chat_template ), isStyleTTS2: Boolean(allTags.includes('style_text_to_speech_2')), widgetData: modelInfoResponse.widgetData, voices: modelInfoResponse.voices } setModelInfo(modelInfo) setIsCustomModel(isCustom) setIsFetching(false) } catch (error) { console.error('Error fetching model info:', error) setIsFetching(false) throw error } }, [setModelInfo, pipeline, setIsFetching] ) useEffect(() => { // Reset custom model state when pipeline changes setIsCustomModel(false) setShowCustomInput(false) setCustomModelName('') setCustomModelError('') if (pipeline !== 'feature-extraction') { setSortBy('downloads') } }, [pipeline]) // Update modelInfo to first model when models are loaded and no custom model is selected useEffect(() => { if (models.length > 0 && !isCustomModel && !modelInfo) { const firstModel = sortedModels[0] fetchAndSetModelInfo(firstModel, false) } }, [models, sortedModels, fetchAndSetModelInfo, isCustomModel, modelInfo]) const handleModelSelect = (model: ModelInfoResponse) => { fetchAndSetModelInfo(model, false) } const handleSortChange = (newSortBy: SortOption) => { if (sortBy === newSortBy) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') } else { setSortBy(newSortBy) setSortOrder('desc') } } const handleCustomModelLoad = async () => { if (!customModelName.trim()) { setCustomModelError('Please enter a model name') return } setIsLoadingCustomModel(true) setCustomModelError('') try { await fetchAndSetModelInfo( { id: customModelName.trim(), tags: [] } as unknown as ModelInfoResponse, true ) setShowCustomInput(false) setCustomModelName('') } catch (error) { setCustomModelError( 'Failed to load model. Please check the model name and try again.' ) } finally { setIsLoadingCustomModel(false) } } const handleRemoveCustomModel = () => { setIsCustomModel(false) // Load the first model from the list if (sortedModels.length > 0) { fetchAndSetModelInfo(sortedModels[0], false) } } const handleCustomInputKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleCustomModelLoad() } else if (e.key === 'Escape') { setShowCustomInput(false) setCustomModelName('') setCustomModelError('') } } const selectedModel = models.find((model) => model.id === modelInfo?.id) || models[0] const SortIcon = ({ sortOrder }: { sortOrder: 'asc' | 'desc' }) => { return sortOrder === 'asc' ? ( ) : ( ) } if (isCustomModel) { return (
{modelInfo?.id || 'Custom model'}
{modelInfo && (modelInfo.likes > 0 || modelInfo.downloads > 0) && (
{modelInfo.likes > 0 && (
{formatNumber(modelInfo.likes)}
)} {modelInfo.downloads > 0 && (
{formatNumber(modelInfo.downloads)}
)}
)}
) } if (isFetching || models.length === 0) { return (
) } return (
handleModelSelect(model)} >
{modelInfo?.id || 'Select a model'}
{selectedModel && (selectedModel.likes > 0 || selectedModel.downloads > 0) && (
{selectedModel.likes > 0 && (
{formatNumber(selectedModel.likes)}
)} {selectedModel.downloads > 0 && (
{formatNumber(selectedModel.downloads)}
)}
)}
{/* Custom Model Input */} {showCustomInput ? (
setCustomModelName(e.target.value)} onKeyDown={handleCustomInputKeyPress} placeholder="onnx-community/Qwen3-0.6B-ONNX" className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded-sm focus:outline-hidden focus:ring-1 focus:ring-blue-500" autoFocus />
{customModelError && (

{customModelError}

)}

Press Enter to load or Escape to cancel

) : ( <>
{/* Load Custom Model Button */} {/* Sort Controls */}
Sort by:
)} {/* Model Options - Scrollable */} {!showCustomInput && (
{sortedModels.map((model) => { const hasStats = model.likes > 0 || model.downloads > 0 return ( `px-3 py-3 cursor-pointer border-b border-gray-100 last:border-b-0 ${ active ? 'bg-gray-50' : '' } ${selected ? 'bg-blue-50' : ''}` } > {({ selected }) => (
{model.id} {selected && ( )}
{/* Stats Display */} {hasStats && (
{model.likes > 0 && (
{formatNumber(model.likes)}
)} {model.downloads > 0 && (
{formatNumber(model.downloads)}
)} {model.createdAt && ( {model.createdAt.split('T')[0]} )}
)}
)}
) })}
)}
) } export default ModelSelector