import HeaderPage from '@/containers/HeaderPage' import { createFileRoute, useParams, useNavigate, useSearch, } from '@tanstack/react-router' import { IconArrowLeft, IconDownload, IconClock, IconFileCode, } from '@tabler/icons-react' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' import { extractModelName, extractDescription } from '@/lib/models' import { RenderMarkdown } from '@/containers/RenderMarkdown' import { useEffect, useMemo, useCallback, useState } from 'react' import { useModelProvider } from '@/hooks/useModelProvider' import { useDownloadStore } from '@/hooks/useDownloadStore' import { CatalogModel, convertHfRepoToCatalogModel, fetchHuggingFaceRepo, pullModel, } from '@/services/models' import { Progress } from '@/components/ui/progress' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' type SearchParams = { repo: string } export const Route = createFileRoute('/hub/$modelId')({ component: HubModelDetail, validateSearch: (search: Record): SearchParams => ({ repo: search.repo as SearchParams['repo'], }), }) function HubModelDetail() { const { modelId } = useParams({ from: Route.id }) const navigate = useNavigate() const { sources, fetchSources } = useModelSources() // eslint-disable-next-line @typescript-eslint/no-explicit-any const search = useSearch({ from: Route.id as any }) const { getProviderByName } = useModelProvider() const llamaProvider = getProviderByName('llamacpp') const { downloads, localDownloadingModels, addLocalDownloadingModel } = useDownloadStore() const [repoData, setRepoData] = useState() // State for README content const [readmeContent, setReadmeContent] = useState('') const [isLoadingReadme, setIsLoadingReadme] = useState(false) useEffect(() => { fetchSources() }, [fetchSources]) const fetchRepo = useCallback(async () => { const repoInfo = await fetchHuggingFaceRepo(search.repo || modelId) if (repoInfo) { const repoDetail = convertHfRepoToCatalogModel(repoInfo) setRepoData(repoDetail) } }, [modelId, search]) useEffect(() => { fetchRepo() }, [modelId, fetchRepo]) // Find the model data from sources const modelData = useMemo(() => { return sources.find((model) => model.model_name === modelId) ?? repoData }, [sources, modelId, repoData]) // Download processes const downloadProcesses = useMemo( () => Object.values(downloads).map((download) => ({ id: download.name, name: download.name, progress: download.progress, current: download.current, total: download.total, })), [downloads] ) // Handle model use const handleUseModel = useCallback( (modelId: string) => { navigate({ to: route.home, params: {}, search: { model: { id: modelId, provider: 'llamacpp', }, }, }) }, [navigate] ) // Format the date const formatDate = (dateString: string) => { const date = new Date(dateString) const now = new Date() const diffTime = Math.abs(now.getTime() - date.getTime()) const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) if (diffDays < 7) { return `${diffDays} days ago` } else if (diffDays < 30) { const weeks = Math.floor(diffDays / 7) return `${weeks} week${weeks > 1 ? 's' : ''} ago` } else if (diffDays < 365) { const months = Math.floor(diffDays / 30) return `${months} month${months > 1 ? 's' : ''} ago` } else { const years = Math.floor(diffDays / 365) return `${years} year${years > 1 ? 's' : ''} ago` } } // Extract tags from quants (model variants) const tags = useMemo(() => { if (!modelData?.quants) return [] // Extract unique size indicators from quant names const sizePattern = /(\d+b)/i const uniqueSizes = new Set() modelData.quants.forEach((quant) => { const match = quant.model_id.match(sizePattern) if (match) { uniqueSizes.add(match[1].toLowerCase()) } }) return Array.from(uniqueSizes).sort((a, b) => { const numA = parseInt(a) const numB = parseInt(b) return numA - numB }) }, [modelData]) // Fetch README content when modelData.readme is available useEffect(() => { if (modelData?.readme) { setIsLoadingReadme(true) fetch(modelData.readme) .then((response) => response.text()) .then((content) => { setReadmeContent(content) setIsLoadingReadme(false) }) .catch((error) => { console.error('Failed to fetch README:', error) setIsLoadingReadme(false) }) } }, [modelData?.readme]) if (!modelData) { return (

Model not found

) } return (
{/* Model Header */}

{extractModelName(modelData.model_name) || modelData.model_name}

{/* Stats */}
{modelData.developer && ( <> By {modelData.developer} )}
{modelData.downloads || 0} Downloads
{modelData.created_at && (
Updated {formatDate(modelData.created_at)}
)}
{/* Description */} {modelData.description && ( )} {/* Tags */} {tags.length > 0 && (
{tags.map((tag) => ( {tag} ))}
)}
{/* Variants Section */} {modelData.quants && modelData.quants.length > 0 && (

Variants ({modelData.quants.length})

{modelData.quants.map((variant) => { 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 ) // Extract format from model_id const format = variant.model_id .toLowerCase() .includes('tensorrt') ? 'TensorRT' : 'GGUF' // Extract version name (remove format suffix) const versionName = variant.model_id .replace(/_GGUF$/i, '') .replace(/-GGUF$/i, '') .replace(/_TensorRT$/i, '') .replace(/-TensorRT$/i, '') return ( ) })}
Version Format Size Action
{versionName} {format} {variant.file_size} {(() => { if (isDownloading && !isDownloaded) { return (
{Math.round(downloadProgress * 100)}%
) } if (isDownloaded) { return ( ) } return ( ) })()}
)} {/* README Section */} {modelData.readme && (
)}
) }