/* eslint-disable @typescript-eslint/no-explicit-any */ import { useVirtualizer } from '@tanstack/react-virtual' import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' import { cn } from '@/lib/utils' 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, IconSearch, IconTool, } from '@tabler/icons-react' import { Switch } from '@/components/ui/switch' import Joyride, { CallBackProps, STATUS } from 'react-joyride' import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { CatalogModel, pullModel, fetchHuggingFaceRepo, convertHfRepoToCatalogModel, } from '@/services/models' 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' type ModelProps = { model: CatalogModel } 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() { const parentRef = useRef(null) const { huggingfaceToken } = useGeneralSetting() 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() 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 [joyrideReady, setJoyrideReady] = useState(false) const [currentStepIndex, setCurrentStepIndex] = useState(0) const addModelSourceTimeoutRef = useRef | null>( null ) const downloadButtonRef = useRef(null) const hasTriggeredDownload = useRef(false) const { getProviderByName } = useModelProvider() const llamaProvider = getProviderByName('llamacpp') 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?.filter((model) => model.quants.some((variant) => llamaProvider?.models.some( (m: { id: string }) => m.id === variant.model_id ) ) ) } // Add HuggingFace repo at the beginning if available if (huggingFaceRepo) { filtered = [huggingFaceRepo, ...filtered] } return filtered }, [ sortedModels, debouncedSearchValue, showOnlyDownloaded, huggingFaceRepo, searchOptions, llamaProvider?.models, ]) // The virtualizer const rowVirtualizer = useVirtualizer({ count: filteredModels.length, getScrollElement: () => parentRef.current, estimateSize: () => 35, }) useEffect(() => { fetchSources() }, [fetchSources]) const handleSearchChange = (e: ChangeEvent) => { setIsSearching(false) setSearchValue(e.target.value) setHuggingFaceRepo(null) // Clear previous repo info if (addModelSourceTimeoutRef.current) { clearTimeout(addModelSourceTimeoutRef.current) } if ( e.target.value.length && (e.target.value.includes('/') || e.target.value.startsWith('http')) ) { setIsSearching(true) addModelSourceTimeoutRef.current = setTimeout(async () => { try { // Fetch HuggingFace repository information const repoInfo = await fetchHuggingFaceRepo( e.target.value, huggingfaceToken ) if (repoInfo) { const catalogModel = convertHfRepoToCatalogModel(repoInfo) if ( !sources.some( (s) => catalogModel.model_name.trim().split('/').pop() === s.model_name.trim() ) ) { setHuggingFaceRepo(catalogModel) } } } catch (error) { console.error('Error fetching repository info:', error) } finally { setIsSearching(false) } }, 500) } } 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 DownloadButtonPlaceholder = useMemo(() => { return ({ model }: ModelProps) => { // Check if this is a HuggingFace repository (no quants) if (model.quants.length === 0) { return (
) } const quant = model.quants.find((e) => defaultModelQuantizations.some((m) => e.model_id.toLowerCase().includes(m) ) ) ?? model.quants[0] const modelId = quant?.model_id || model.model_name const modelUrl = quant?.path || modelId const isDownloading = localDownloadingModels.has(modelId) || downloadProcesses.some((e) => e.id === modelId) const downloadProgress = downloadProcesses.find((e) => e.id === modelId)?.progress || 0 const isDownloaded = llamaProvider?.models.some( (m: { id: string }) => m.id === modelId ) const isRecommended = isRecommendedModel(model.model_name) const handleDownload = () => { // Immediately set local downloading state addLocalDownloadingModel(modelId) const mmprojPath = model.mmproj_models?.[0]?.path pullModel(modelId, modelUrl, mmprojPath) } return (
{isDownloading && !isDownloaded && (
{Math.round(downloadProgress * 100)}%
)} {isDownloaded ? ( ) : ( )}
) } }, [ downloadProcesses, llamaProvider?.models, isRecommendedModel, downloadButtonRef, localDownloadingModels, addLocalDownloadingModel, t, handleUseModel, ]) const { step } = useSearch({ from: Route.id }) const isSetup = step === 'setup_local_provider' // Wait for DOM to be ready before starting Joyride useEffect(() => { if (!loading && filteredModels.length > 0 && isSetup) { const timer = setTimeout(() => { setJoyrideReady(true) }, 100) return () => clearTimeout(timer) } else { setJoyrideReady(false) } }, [loading, filteredModels.length, isSetup]) const handleJoyrideCallback = (data: CallBackProps) => { const { status, index } = data if ( status === STATUS.FINISHED && !isDownloading && isLastStep && !hasTriggeredDownload.current ) { const recommendedModel = filteredModels.find((model) => isRecommendedModel(model.model_name) ) if (recommendedModel && recommendedModel.quants[0]?.model_id) { if (downloadButtonRef.current) { hasTriggeredDownload.current = true downloadButtonRef.current.click() } return } } if (status === STATUS.FINISHED) { navigate({ to: route.hub.index, }) } // Track current step index setCurrentStepIndex(index) } // Check if any model is currently downloading const isDownloading = localDownloadingModels.size > 0 || downloadProcesses.length > 0 const steps = [ { target: '.hub-model-card-step', title: t('hub:joyride.recommendedModelTitle'), disableBeacon: true, content: t('hub:joyride.recommendedModelContent'), }, { target: '.hub-download-button-step', title: isDownloading ? t('hub:joyride.downloadInProgressTitle') : t('hub:joyride.downloadModelTitle'), disableBeacon: true, content: isDownloading ? t('hub:joyride.downloadInProgressContent') : t('hub:joyride.downloadModelContent'), }, ] // Check if we're on the last step const isLastStep = currentStepIndex === steps.length - 1 const renderFilter = () => { return ( <> { sortOptions.find((option) => option.value === sortSelected) ?.name } {sortOptions.map((option) => ( setSortSelected(option.value)} > {option.name} ))}
{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 }
} >
{t('hub:by')}{' '} {filteredModels[virtualItem.index]?.developer}
{filteredModels[virtualItem.index] .downloads || 0}
{filteredModels[virtualItem.index].quants ?.length || 0}
{filteredModels[virtualItem.index].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.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 = llamaProvider?.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 ) pullModel( variant.model_id, variant.path, filteredModels[ virtualItem.index ].mmproj_models?.[0]?.path ) }} >
) })()}
} /> ) )}
)}
))}
)}
) }