From c34291237f5e8a5cdabf6b135d9958acfcfa5fa9 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 3 Jul 2025 13:36:00 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8enhancement:=20add=20hub=20detail=20pa?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-app/src/constants/routes.ts | 5 +- web-app/src/containers/LeftPanel.tsx | 2 +- web-app/src/containers/SetupScreen.tsx | 2 +- web-app/src/providers/DataProvider.tsx | 2 +- web-app/src/routeTree.gen.ts | 78 ++-- web-app/src/routes/hub/$modelId.tsx | 367 ++++++++++++++++++ web-app/src/routes/{hub.tsx => hub/index.tsx} | 35 +- .../settings/providers/$providerName.tsx | 2 +- 8 files changed, 445 insertions(+), 48 deletions(-) create mode 100644 web-app/src/routes/hub/$modelId.tsx rename web-app/src/routes/{hub.tsx => hub/index.tsx} (96%) diff --git a/web-app/src/constants/routes.ts b/web-app/src/constants/routes.ts index e9997590a..97f95631d 100644 --- a/web-app/src/constants/routes.ts +++ b/web-app/src/constants/routes.ts @@ -17,7 +17,10 @@ export const route = { https_proxy: '/settings/https-proxy', hardware: '/settings/hardware', }, - hub: '/hub', + hub: { + index: '/hub/', + model: '/hub/$modelId', + }, localApiServerlogs: '/local-api-server/logs', systemMonitor: '/system-monitor', threadsDetail: '/threads/$threadId', diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 748cb529f..82ceff643 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -57,7 +57,7 @@ const mainMenus = [ { title: 'common:hub', icon: IconAppsFilled, - route: route.hub, + route: route.hub.index, }, { title: 'common:settings', diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx index 807568073..4144e4e0a 100644 --- a/web-app/src/containers/SetupScreen.tsx +++ b/web-app/src/containers/SetupScreen.tsx @@ -29,7 +29,7 @@ function SetupScreen() { rootRoute, } as any) -const HubRoute = HubImport.update({ - id: '/hub', - path: '/hub', - getParentRoute: () => rootRoute, -} as any) - const AssistantRoute = AssistantImport.update({ id: '/assistant', path: '/assistant', @@ -62,6 +57,12 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const HubIndexRoute = HubIndexImport.update({ + id: '/hub/', + path: '/hub/', + getParentRoute: () => rootRoute, +} as any) + const ThreadsThreadIdRoute = ThreadsThreadIdImport.update({ id: '/threads/$threadId', path: '/threads/$threadId', @@ -128,6 +129,12 @@ const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({ getParentRoute: () => rootRoute, } as any) +const HubModelIdRoute = HubModelIdImport.update({ + id: '/hub/$modelId', + path: '/hub/$modelId', + getParentRoute: () => rootRoute, +} as any) + const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({ id: '/settings/providers/', path: '/settings/providers/', @@ -159,13 +166,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AssistantImport parentRoute: typeof rootRoute } - '/hub': { - id: '/hub' - path: '/hub' - fullPath: '/hub' - preLoaderRoute: typeof HubImport - parentRoute: typeof rootRoute - } '/logs': { id: '/logs' path: '/logs' @@ -180,6 +180,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SystemMonitorImport parentRoute: typeof rootRoute } + '/hub/$modelId': { + id: '/hub/$modelId' + path: '/hub/$modelId' + fullPath: '/hub/$modelId' + preLoaderRoute: typeof HubModelIdImport + parentRoute: typeof rootRoute + } '/local-api-server/logs': { id: '/local-api-server/logs' path: '/local-api-server/logs' @@ -257,6 +264,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ThreadsThreadIdImport parentRoute: typeof rootRoute } + '/hub/': { + id: '/hub/' + path: '/hub' + fullPath: '/hub' + preLoaderRoute: typeof HubIndexImport + parentRoute: typeof rootRoute + } '/settings/providers/$providerName': { id: '/settings/providers/$providerName' path: '/settings/providers/$providerName' @@ -279,9 +293,9 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute '/assistant': typeof AssistantRoute - '/hub': typeof HubRoute '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute + '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -293,6 +307,7 @@ export interface FileRoutesByFullPath { '/settings/privacy': typeof SettingsPrivacyRoute '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/hub': typeof HubIndexRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers': typeof SettingsProvidersIndexRoute } @@ -300,9 +315,9 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/assistant': typeof AssistantRoute - '/hub': typeof HubRoute '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute + '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -314,6 +329,7 @@ export interface FileRoutesByTo { '/settings/privacy': typeof SettingsPrivacyRoute '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/hub': typeof HubIndexRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers': typeof SettingsProvidersIndexRoute } @@ -322,9 +338,9 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/assistant': typeof AssistantRoute - '/hub': typeof HubRoute '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute + '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -336,6 +352,7 @@ export interface FileRoutesById { '/settings/privacy': typeof SettingsPrivacyRoute '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/hub/': typeof HubIndexRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers/': typeof SettingsProvidersIndexRoute } @@ -345,9 +362,9 @@ export interface FileRouteTypes { fullPaths: | '/' | '/assistant' - | '/hub' | '/logs' | '/system-monitor' + | '/hub/$modelId' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -359,15 +376,16 @@ export interface FileRouteTypes { | '/settings/privacy' | '/settings/shortcuts' | '/threads/$threadId' + | '/hub' | '/settings/providers/$providerName' | '/settings/providers' fileRoutesByTo: FileRoutesByTo to: | '/' | '/assistant' - | '/hub' | '/logs' | '/system-monitor' + | '/hub/$modelId' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -379,15 +397,16 @@ export interface FileRouteTypes { | '/settings/privacy' | '/settings/shortcuts' | '/threads/$threadId' + | '/hub' | '/settings/providers/$providerName' | '/settings/providers' id: | '__root__' | '/' | '/assistant' - | '/hub' | '/logs' | '/system-monitor' + | '/hub/$modelId' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -399,6 +418,7 @@ export interface FileRouteTypes { | '/settings/privacy' | '/settings/shortcuts' | '/threads/$threadId' + | '/hub/' | '/settings/providers/$providerName' | '/settings/providers/' fileRoutesById: FileRoutesById @@ -407,9 +427,9 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute AssistantRoute: typeof AssistantRoute - HubRoute: typeof HubRoute LogsRoute: typeof LogsRoute SystemMonitorRoute: typeof SystemMonitorRoute + HubModelIdRoute: typeof HubModelIdRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute @@ -421,6 +441,7 @@ export interface RootRouteChildren { SettingsPrivacyRoute: typeof SettingsPrivacyRoute SettingsShortcutsRoute: typeof SettingsShortcutsRoute ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute + HubIndexRoute: typeof HubIndexRoute SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute } @@ -428,9 +449,9 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AssistantRoute: AssistantRoute, - HubRoute: HubRoute, LogsRoute: LogsRoute, SystemMonitorRoute: SystemMonitorRoute, + HubModelIdRoute: HubModelIdRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute, SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsExtensionsRoute: SettingsExtensionsRoute, @@ -442,6 +463,7 @@ const rootRouteChildren: RootRouteChildren = { SettingsPrivacyRoute: SettingsPrivacyRoute, SettingsShortcutsRoute: SettingsShortcutsRoute, ThreadsThreadIdRoute: ThreadsThreadIdRoute, + HubIndexRoute: HubIndexRoute, SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute, SettingsProvidersIndexRoute: SettingsProvidersIndexRoute, } @@ -458,9 +480,9 @@ export const routeTree = rootRoute "children": [ "/", "/assistant", - "/hub", "/logs", "/system-monitor", + "/hub/$modelId", "/local-api-server/logs", "/settings/appearance", "/settings/extensions", @@ -472,6 +494,7 @@ export const routeTree = rootRoute "/settings/privacy", "/settings/shortcuts", "/threads/$threadId", + "/hub/", "/settings/providers/$providerName", "/settings/providers/" ] @@ -482,15 +505,15 @@ export const routeTree = rootRoute "/assistant": { "filePath": "assistant.tsx" }, - "/hub": { - "filePath": "hub.tsx" - }, "/logs": { "filePath": "logs.tsx" }, "/system-monitor": { "filePath": "system-monitor.tsx" }, + "/hub/$modelId": { + "filePath": "hub/$modelId.tsx" + }, "/local-api-server/logs": { "filePath": "local-api-server/logs.tsx" }, @@ -524,6 +547,9 @@ export const routeTree = rootRoute "/threads/$threadId": { "filePath": "threads/$threadId.tsx" }, + "/hub/": { + "filePath": "hub/index.tsx" + }, "/settings/providers/$providerName": { "filePath": "settings/providers/$providerName.tsx" }, diff --git a/web-app/src/routes/hub/$modelId.tsx b/web-app/src/routes/hub/$modelId.tsx new file mode 100644 index 000000000..2312f8e40 --- /dev/null +++ b/web-app/src/routes/hub/$modelId.tsx @@ -0,0 +1,367 @@ +import HeaderPage from '@/containers/HeaderPage' +import { createFileRoute, useParams, useNavigate } 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 } from 'react' +import { useModelProvider } from '@/hooks/useModelProvider' +import { useDownloadStore } from '@/hooks/useDownloadStore' +import { pullModel } from '@/services/models' +import { Progress } from '@/components/ui/progress' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +export const Route = createFileRoute('/hub/$modelId')({ + component: HubModelDetail, +}) + +function HubModelDetail() { + const { modelId } = useParams({ from: Route.id }) + const navigate = useNavigate() + const { sources, fetchSources } = useModelSources() + const { getProviderByName } = useModelProvider() + const llamaProvider = getProviderByName('llamacpp') + const { downloads, localDownloadingModels, addLocalDownloadingModel } = + useDownloadStore() + + useEffect(() => { + fetchSources() + }, [fetchSources]) + + // Find the model data from sources + const modelData = useMemo(() => { + return sources.find((model) => model.model_name === modelId) + }, [sources, modelId]) + + // 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]) + + 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 ( + + ) + })()} +
+
+
+ )} +
+
+
+
+
+ ) +} diff --git a/web-app/src/routes/hub.tsx b/web-app/src/routes/hub/index.tsx similarity index 96% rename from web-app/src/routes/hub.tsx rename to web-app/src/routes/hub/index.tsx index 8a812fcaf..e45bb476d 100644 --- a/web-app/src/routes/hub.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -1,10 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { - createFileRoute, - Link, - useNavigate, - useSearch, -} from '@tanstack/react-router' +import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' import { cn, fuzzySearch } from '@/lib/utils' @@ -46,7 +41,7 @@ type SearchParams = { } const defaultModelQuantizations = ['iq4_xs.gguf', 'q4_k_m.gguf'] -export const Route = createFileRoute(route.hub as any)({ +export const Route = createFileRoute(route.hub.index as any)({ component: Hub, validateSearch: (search: Record): SearchParams => ({ repo: search.repo as SearchParams['repo'], @@ -60,7 +55,7 @@ function Hub() { { value: 'most-downloaded', name: t('hub:sortMostDownloaded') }, ] const { sources, fetchSources, addSource, loading } = useModelSources() - const search = useSearch({ from: route.hub as any }) + const search = useSearch({ from: route.hub.index as any }) const [searchValue, setSearchValue] = useState('') const [sortSelected, setSortSelected] = useState('newest') const [expandedModels, setExpandedModels] = useState>( @@ -328,7 +323,7 @@ function Hub() { if (status === STATUS.FINISHED) { navigate({ - to: route.hub, + to: route.hub.index, }) } @@ -473,16 +468,22 @@ function Hub() {
{renderFilter()}
- {filteredModels.map((model) => ( -
+ {filteredModels.map((model, i) => ( +
- { + console.log(model.model_name) + navigate({ + to: route.hub.model, + params: { + modelId: model.model_name, + }, + }) + }} >

{extractModelName(model.model_name) || ''}

- +
{ diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 8331584b2..7d27e16be 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -525,7 +525,7 @@ function ProviderDetail() {

{t('providers:noModelFoundDesc')}   - {t('common:hub')} + {t('common:hub')}

)}