/* eslint-disable @typescript-eslint/no-explicit-any */ import { useVirtualizer } from '@tanstack/react-virtual' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' import { cn } from '@/lib/utils' import { PlatformGuard } from '@/lib/platform/PlatformGuard' import { PlatformFeature } from '@/lib/platform' import { useState, useMemo, useEffect, ChangeEvent, useCallback, useRef, } from 'react' import { Button } from '@/components/ui/button' import { useModelProvider } from '@/hooks/useModelProvider' import { Card, CardItem } from '@/containers/Card' import { RenderMarkdown } from '@/containers/RenderMarkdown' import { extractModelName, extractDescription } from '@/lib/models' import { IconDownload, IconFileCode, IconEye, IconSearch, IconTool, } from '@tabler/icons-react' import { Switch } from '@/components/ui/switch' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { ModelInfoHoverCard } from '@/containers/ModelInfoHoverCard' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useServiceHub } from '@/hooks/useServiceHub' import type { CatalogModel } from '@/services/models/types' import { useDownloadStore } from '@/hooks/useDownloadStore' import { Progress } from '@/components/ui/progress' import HeaderPage from '@/containers/HeaderPage' import { Loader } from 'lucide-react' import { useTranslation } from '@/i18n/react-i18next-compat' import Fuse from 'fuse.js' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { DownloadButtonPlaceholder } from '@/containers/DownloadButton' import { useShallow } from 'zustand/shallow' type SearchParams = { repo: string } const defaultModelQuantizations = ['iq4_xs', 'q4_k_m'] export const Route = createFileRoute(route.hub.index as any)({ component: Hub, validateSearch: (search: Record): SearchParams => ({ repo: search.repo as SearchParams['repo'], }), }) function Hub() { return ( ) } function HubContent() { const parentRef = useRef(null) const huggingfaceToken = useGeneralSetting((state) => state.huggingfaceToken) const serviceHub = useServiceHub() const { t } = useTranslation() const sortOptions = [ { value: 'newest', name: t('hub:sortNewest') }, { value: 'most-downloaded', name: t('hub:sortMostDownloaded') }, ] const searchOptions = useMemo(() => { return { includeScore: true, // Search in `author` and in `tags` array keys: ['model_name', 'quants.model_id'], } }, []) const { sources, fetchSources, loading } = useModelSources( useShallow((state) => ({ sources: state.sources, fetchSources: state.fetchSources, loading: state.loading, })) ) const [searchValue, setSearchValue] = useState('') const [sortSelected, setSortSelected] = useState('newest') const [expandedModels, setExpandedModels] = useState>( {} ) const [isSearching, setIsSearching] = useState(false) const [showOnlyDownloaded, setShowOnlyDownloaded] = useState(false) const [huggingFaceRepo, setHuggingFaceRepo] = useState( null ) const [modelSupportStatus, setModelSupportStatus] = useState< Record >({}) const addModelSourceTimeoutRef = useRef | null>( null ) const toggleModelExpansion = (modelId: string) => { setExpandedModels((prev) => ({ ...prev, [modelId]: !prev[modelId], })) } // Sorting functionality const sortedModels = useMemo(() => { return [...sources].sort((a, b) => { if (sortSelected === 'most-downloaded') { return (b.downloads || 0) - (a.downloads || 0) } else { return ( new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime() ) } }) }, [sortSelected, sources]) // Filtered models (debounced search) const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue) useEffect(() => { const handler = setTimeout(() => { setDebouncedSearchValue(searchValue) }, 300) return () => clearTimeout(handler) }, [searchValue]) const filteredModels = useMemo(() => { let filtered = sortedModels // Apply search filter if (debouncedSearchValue.length) { const fuse = new Fuse(filtered, searchOptions) // Remove domain from search value (e.g., "huggingface.co/author/model" -> "author/model") const cleanedSearchValue = debouncedSearchValue.replace( /^https?:\/\/[^/]+\//, '' ) filtered = fuse.search(cleanedSearchValue).map((result) => result.item) } // Apply downloaded filter if (showOnlyDownloaded) { filtered = filtered ?.map((model) => ({ ...model, quants: model.quants.filter((variant) => useModelProvider .getState() .getProviderByName('llamacpp') ?.models.some((m: { id: string }) => m.id === variant.model_id) ), })) .filter((model) => model.quants.length > 0) } // Add HuggingFace repo at the beginning if available if (huggingFaceRepo) { filtered = [huggingFaceRepo, ...filtered] } return filtered }, [ sortedModels, debouncedSearchValue, showOnlyDownloaded, huggingFaceRepo, searchOptions, ]) // The virtualizer const rowVirtualizer = useVirtualizer({ count: filteredModels.length, getScrollElement: () => parentRef.current, estimateSize: () => 35, }) useEffect(() => { fetchSources() }, [fetchSources]) const fetchHuggingFaceModel = async (searchValue: string) => { if ( !searchValue.length || (!searchValue.includes('/') && !searchValue.startsWith('http')) ) { return } setIsSearching(true) if (addModelSourceTimeoutRef.current) { clearTimeout(addModelSourceTimeoutRef.current) } addModelSourceTimeoutRef.current = setTimeout(async () => { try { const repoInfo = await serviceHub .models() .fetchHuggingFaceRepo(searchValue, huggingfaceToken) if (repoInfo) { const catalogModel = serviceHub .models() .convertHfRepoToCatalogModel(repoInfo) if ( !sources.some( (s) => catalogModel.model_name.trim().split('/').pop() === s.model_name.trim() && catalogModel.developer.trim() === s.developer?.trim() ) ) { setHuggingFaceRepo(catalogModel) } } } catch (error) { console.error('Error fetching repository info:', error) } finally { setIsSearching(false) } }, 500) } const handleSearchChange = (e: ChangeEvent) => { setIsSearching(false) setSearchValue(e.target.value) setHuggingFaceRepo(null) // Clear previous repo info if (!showOnlyDownloaded) { fetchHuggingFaceModel(e.target.value) } } const { downloads, localDownloadingModels, addLocalDownloadingModel } = useDownloadStore() const downloadProcesses = useMemo( () => Object.values(downloads).map((download) => ({ id: download.name, name: download.name, progress: download.progress, current: download.current, total: download.total, })), [downloads] ) const navigate = useNavigate() const isRecommendedModel = useCallback((modelId: string) => { return (extractModelName(modelId)?.toLowerCase() === 'jan-nano-gguf') as boolean }, []) const handleUseModel = useCallback( (modelId: string) => { navigate({ to: route.home, params: {}, search: { model: { id: modelId, provider: 'llamacpp', }, }, }) }, [navigate] ) const checkModelSupport = useCallback( async (variant: any) => { const modelKey = variant.model_id // Don't check again if already checking or checked if (modelSupportStatus[modelKey]) { return } // Set loading state setModelSupportStatus((prev) => ({ ...prev, [modelKey]: 'LOADING', })) try { // Use the HuggingFace path for the model const modelPath = variant.path const supportStatus = await serviceHub .models() .isModelSupported(modelPath, 8192) setModelSupportStatus((prev) => ({ ...prev, [modelKey]: supportStatus, })) } catch (error) { console.error('Error checking model support:', error) setModelSupportStatus((prev) => ({ ...prev, [modelKey]: 'RED', })) } }, [modelSupportStatus, serviceHub] ) // Check if we're on the last step const renderFilter = () => { return ( <> {searchValue.length === 0 && ( { sortOptions.find((option) => option.value === sortSelected) ?.name } {sortOptions.map((option) => ( setSortSelected(option.value)} > {option.name} ))} )}
{ setShowOnlyDownloaded(checked) if (checked) { setHuggingFaceRepo(null) } else { // Re-trigger HuggingFace search when switching back to "All models" fetchHuggingFaceModel(searchValue) } }} /> {t('hub:downloaded')}
) } return ( <>
{isSearching ? ( ) : ( )}
{renderFilter()}
{loading && !filteredModels.length ? (
{t('hub:loadingModels')}
) : filteredModels.length === 0 ? (
{t('hub:noModels')}
) : (
{renderFilter()}
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
{ navigate({ to: route.hub.model, params: { modelId: filteredModels[virtualItem.index] .model_name, }, }) }} >

{extractModelName( filteredModels[virtualItem.index].model_name ) || ''}

{ ( filteredModels[ virtualItem.index ].quants.find((m) => defaultModelQuantizations.some((e) => m.model_id.toLowerCase().includes(e) ) ) ?? filteredModels[virtualItem.index] .quants?.[0] )?.file_size } defaultModelQuantizations.some((e) => m.model_id.toLowerCase().includes(e) ) ) ?? filteredModels[virtualItem.index] .quants?.[0] } isDefaultVariant={true} modelSupportStatus={modelSupportStatus} onCheckModelSupport={checkModelSupport} />
} >
{t('hub:by')}{' '} {filteredModels[virtualItem.index]?.developer}
{filteredModels[virtualItem.index] .downloads || 0}
{filteredModels[virtualItem.index].quants ?.length || 0}
{filteredModels[virtualItem.index].num_mmproj > 0 && (

{t('vision')}

)} {filteredModels[virtualItem.index].tools && (

{t('tools')}

)}
{filteredModels[virtualItem.index].quants.length > 1 && (
toggleModelExpansion( filteredModels[virtualItem.index] .model_name ) } />

{t('hub:showVariants')}

)}
{expandedModels[ filteredModels[virtualItem.index].model_name ] && filteredModels[virtualItem.index].quants.length > 0 && (
{filteredModels[virtualItem.index].quants.map( (variant) => (
{variant.model_id} {filteredModels[virtualItem.index] .num_mmproj > 0 && (

{t('vision')}

)} {filteredModels[virtualItem.index] .tools && (

{t('tools')}

)}
} actions={

{variant.file_size}

{(() => { const isDownloading = localDownloadingModels.has( variant.model_id ) || downloadProcesses.some( (e) => e.id === variant.model_id ) const downloadProgress = downloadProcesses.find( (e) => e.id === variant.model_id )?.progress || 0 const isDownloaded = useModelProvider .getState() .getProviderByName('llamacpp') ?.models.some( (m: { id: string }) => m.id === variant.model_id ) if (isDownloading) { return ( <>
{Math.round( downloadProgress * 100 )} %
) } if (isDownloaded) { return (
) } return (
{ addLocalDownloadingModel( variant.model_id ) serviceHub .models() .pullModelWithMetadata( variant.model_id, variant.path, ( filteredModels[ virtualItem.index ].mmproj_models?.find( (e) => e.model_id.toLowerCase() === 'mmproj-f16' ) || filteredModels[ virtualItem.index ].mmproj_models?.[0] )?.path, huggingfaceToken ) }} >
) })()}
} /> ) )}
)}
))}
)}
) }