From 83527a7533404e8c11ef70c20e967be13a07e277 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 1 Aug 2025 13:32:41 +0700 Subject: [PATCH] fix: Jan hub repo detail and deep link --- web-app/src/providers/DataProvider.tsx | 2 +- web-app/src/routes/hub/$modelId.tsx | 40 ++++++++++-- web-app/src/routes/hub/index.tsx | 90 +++----------------------- web-app/src/services/models.ts | 41 ++++++++++++ 4 files changed, 86 insertions(+), 87 deletions(-) diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index 9fdb802ea..dcfacd536 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -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, }, diff --git a/web-app/src/routes/hub/$modelId.tsx b/web-app/src/routes/hub/$modelId.tsx index 245909174..679b265de 100644 --- a/web-app/src/routes/hub/$modelId.tsx +++ b/web-app/src/routes/hub/$modelId.tsx @@ -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): 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() // State for README content const [readmeContent, setReadmeContent] = useState('') @@ -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) { diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 7c904d1ee..66e079412 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -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 = { - includeScore: true, - // Search in `author` and in `tags` array - keys: ['model_name', 'quants.model_id'], - } + 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>( @@ -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 { diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 71911244f..12bf1997d 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -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.