fix: Jan hub repo detail and deep link

This commit is contained in:
Louis 2025-08-01 13:32:41 +07:00 committed by Ramon Perez
parent 026b21f779
commit 83527a7533
4 changed files with 86 additions and 87 deletions

View File

@ -79,7 +79,7 @@ export function DataProvider() {
const resource = params.slice(1).join('/')
// return { action, provider, resource }
navigate({
to: route.hub.index,
to: route.hub.model,
search: {
repo: resource,
},

View File

@ -1,5 +1,10 @@
import HeaderPage from '@/containers/HeaderPage'
import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router'
import {
createFileRoute,
useParams,
useNavigate,
useSearch,
} from '@tanstack/react-router'
import {
IconArrowLeft,
IconDownload,
@ -13,23 +18,38 @@ import { RenderMarkdown } from '@/containers/RenderMarkdown'
import { useEffect, useMemo, useCallback, useState } from 'react'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useDownloadStore } from '@/hooks/useDownloadStore'
import { pullModel } from '@/services/models'
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<string, unknown>): 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.hub.model as any })
const { getProviderByName } = useModelProvider()
const llamaProvider = getProviderByName('llamacpp')
const { downloads, localDownloadingModels, addLocalDownloadingModel } =
useDownloadStore()
const [repoData, setRepoData] = useState<CatalogModel | undefined>()
// State for README content
const [readmeContent, setReadmeContent] = useState<string>('')
@ -39,10 +59,21 @@ function HubModelDetail() {
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)
}, [sources, modelId])
return sources.find((model) => model.model_name === modelId) ?? repoData
}, [sources, modelId, repoData])
// Download processes
const downloadProcesses = useMemo(
@ -116,7 +147,6 @@ function HubModelDetail() {
})
}, [modelData])
// Fetch README content when modelData.readme is available
useEffect(() => {
if (modelData?.readme) {

View File

@ -31,7 +31,7 @@ import {
CatalogModel,
pullModel,
fetchHuggingFaceRepo,
HuggingFaceRepo,
convertHfRepoToCatalogModel,
} from '@/services/models'
import { useDownloadStore } from '@/hooks/useDownloadStore'
import { Progress } from '@/components/ui/progress'
@ -63,14 +63,16 @@ function Hub() {
{ value: 'newest', name: t('hub:sortNewest') },
{ value: 'most-downloaded', name: t('hub:sortMostDownloaded') },
]
const searchOptions = {
const searchOptions = useMemo(() => {
return {
includeScore: true,
// Search in `author` and in `tags` array
keys: ['model_name', 'quants.model_id'],
}
}, [])
const { sources, addSource, fetchSources, loading } = useModelSources()
const search = useSearch({ from: route.hub.index as any })
const [searchValue, setSearchValue] = useState('')
const [sortSelected, setSortSelected] = useState('newest')
const [expandedModels, setExpandedModels] = useState<Record<string, boolean>>(
@ -92,48 +94,6 @@ function Hub() {
const { getProviderByName } = useModelProvider()
const llamaProvider = getProviderByName('llamacpp')
// Convert HuggingFace repository to CatalogModel format
const convertHfRepoToCatalogModel = useCallback(
(repo: HuggingFaceRepo): CatalogModel => {
// Extract GGUF files from the repository siblings
const ggufFiles =
repo.siblings?.filter((file) =>
file.rfilename.toLowerCase().endsWith('.gguf')
) || []
// Convert GGUF files to quants format
const quants = ggufFiles.map((file) => {
// Format file size
const formatFileSize = (size?: number) => {
if (!size) return 'Unknown size'
if (size < 1024 ** 3) return `${(size / 1024 ** 2).toFixed(1)} MB`
return `${(size / 1024 ** 3).toFixed(1)} GB`
}
// Generate model_id from filename (remove .gguf extension, case-insensitive)
const modelId = file.rfilename.replace(/\.gguf$/i, '')
return {
model_id: modelId,
path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`,
file_size: formatFileSize(file.size),
}
})
return {
model_name: repo.modelId,
description: `**Metadata:** ${repo.pipeline_tag}\n\n **Tags**: ${repo.tags?.join(', ')}`,
developer: repo.author,
downloads: repo.downloads || 0,
num_quants: quants.length,
quants: quants,
created_at: repo.created_at,
readme: `https://huggingface.co/${repo.modelId}/resolve/main/README.md`,
}
},
[]
)
const toggleModelExpansion = (modelId: string) => {
setExpandedModels((prev) => ({
...prev,
@ -141,35 +101,6 @@ function Hub() {
}))
}
useEffect(() => {
if (search.repo) {
setSearchValue(search.repo || '')
setIsSearching(true)
addModelSourceTimeoutRef.current = setTimeout(async () => {
try {
// Fetch HuggingFace repository information
const repoInfo = await fetchHuggingFaceRepo(search.repo)
if (repoInfo) {
const catalogModel = convertHfRepoToCatalogModel(repoInfo)
if (
!sources.some((s) => s.model_name === catalogModel.model_name)
) {
setHuggingFaceRepo(catalogModel)
addSource(catalogModel)
}
}
await fetchSources()
} catch (error) {
console.error('Error fetching repository info:', error)
} finally {
setIsSearching(false)
}
}, 500)
}
}, [convertHfRepoToCatalogModel, fetchSources, addSource, search, sources])
// Sorting functionality
const sortedModels = useMemo(() => {
return [...sources].sort((a, b) => {
@ -264,9 +195,6 @@ function Hub() {
addSource(catalogModel)
}
}
// Original addSource logic (if needed)
await fetchSources()
} catch (error) {
console.error('Error fetching repository info:', error)
} finally {

View File

@ -134,6 +134,47 @@ export const fetchHuggingFaceRepo = async (
}
}
// Convert HuggingFace repository to CatalogModel format
export const convertHfRepoToCatalogModel = (
repo: HuggingFaceRepo
): CatalogModel => {
// Extract GGUF files from the repository siblings
const ggufFiles =
repo.siblings?.filter((file) =>
file.rfilename.toLowerCase().endsWith('.gguf')
) || []
// Convert GGUF files to quants format
const quants = ggufFiles.map((file) => {
// Format file size
const formatFileSize = (size?: number) => {
if (!size) return 'Unknown size'
if (size < 1024 ** 3) return `${(size / 1024 ** 2).toFixed(1)} MB`
return `${(size / 1024 ** 3).toFixed(1)} GB`
}
// Generate model_id from filename (remove .gguf extension, case-insensitive)
const modelId = file.rfilename.replace(/\.gguf$/i, '')
return {
model_id: modelId,
path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`,
file_size: formatFileSize(file.size),
}
})
return {
model_name: repo.modelId,
description: `**Tags**: ${repo.tags?.join(', ')}`,
developer: repo.author,
downloads: repo.downloads || 0,
num_quants: quants.length,
quants: quants,
created_at: repo.created_at,
readme: `https://huggingface.co/${repo.modelId}/resolve/main/README.md`,
}
}
/**
* Updates a model.
* @param model The model to update.