enhancement: add hub detail page

This commit is contained in:
Faisal Amir 2025-07-03 13:36:00 +07:00
parent 0343c09704
commit c34291237f
8 changed files with 445 additions and 48 deletions

View File

@ -17,7 +17,10 @@ export const route = {
https_proxy: '/settings/https-proxy', https_proxy: '/settings/https-proxy',
hardware: '/settings/hardware', hardware: '/settings/hardware',
}, },
hub: '/hub', hub: {
index: '/hub/',
model: '/hub/$modelId',
},
localApiServerlogs: '/local-api-server/logs', localApiServerlogs: '/local-api-server/logs',
systemMonitor: '/system-monitor', systemMonitor: '/system-monitor',
threadsDetail: '/threads/$threadId', threadsDetail: '/threads/$threadId',

View File

@ -57,7 +57,7 @@ const mainMenus = [
{ {
title: 'common:hub', title: 'common:hub',
icon: IconAppsFilled, icon: IconAppsFilled,
route: route.hub, route: route.hub.index,
}, },
{ {
title: 'common:settings', title: 'common:settings',

View File

@ -29,7 +29,7 @@ function SetupScreen() {
<Card <Card
header={ header={
<Link <Link
to={route.hub} to={route.hub.index}
search={{ search={{
...(!isProd ? { step: 'setup_local_provider' } : {}), ...(!isProd ? { step: 'setup_local_provider' } : {}),
}} }}

View File

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

View File

@ -13,9 +13,9 @@
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as SystemMonitorImport } from './routes/system-monitor' import { Route as SystemMonitorImport } from './routes/system-monitor'
import { Route as LogsImport } from './routes/logs' import { Route as LogsImport } from './routes/logs'
import { Route as HubImport } from './routes/hub'
import { Route as AssistantImport } from './routes/assistant' import { Route as AssistantImport } from './routes/assistant'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as HubIndexImport } from './routes/hub/index'
import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId' import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId'
import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts' import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts'
import { Route as SettingsPrivacyImport } from './routes/settings/privacy' import { Route as SettingsPrivacyImport } from './routes/settings/privacy'
@ -27,6 +27,7 @@ import { Route as SettingsGeneralImport } from './routes/settings/general'
import { Route as SettingsExtensionsImport } from './routes/settings/extensions' import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
import { Route as SettingsAppearanceImport } from './routes/settings/appearance' import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs' import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
import { Route as HubModelIdImport } from './routes/hub/$modelId'
import { Route as SettingsProvidersIndexImport } from './routes/settings/providers/index' import { Route as SettingsProvidersIndexImport } from './routes/settings/providers/index'
import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName' import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
@ -44,12 +45,6 @@ const LogsRoute = LogsImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const HubRoute = HubImport.update({
id: '/hub',
path: '/hub',
getParentRoute: () => rootRoute,
} as any)
const AssistantRoute = AssistantImport.update({ const AssistantRoute = AssistantImport.update({
id: '/assistant', id: '/assistant',
path: '/assistant', path: '/assistant',
@ -62,6 +57,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const HubIndexRoute = HubIndexImport.update({
id: '/hub/',
path: '/hub/',
getParentRoute: () => rootRoute,
} as any)
const ThreadsThreadIdRoute = ThreadsThreadIdImport.update({ const ThreadsThreadIdRoute = ThreadsThreadIdImport.update({
id: '/threads/$threadId', id: '/threads/$threadId',
path: '/threads/$threadId', path: '/threads/$threadId',
@ -128,6 +129,12 @@ const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const HubModelIdRoute = HubModelIdImport.update({
id: '/hub/$modelId',
path: '/hub/$modelId',
getParentRoute: () => rootRoute,
} as any)
const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({ const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({
id: '/settings/providers/', id: '/settings/providers/',
path: '/settings/providers/', path: '/settings/providers/',
@ -159,13 +166,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AssistantImport preLoaderRoute: typeof AssistantImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/hub': {
id: '/hub'
path: '/hub'
fullPath: '/hub'
preLoaderRoute: typeof HubImport
parentRoute: typeof rootRoute
}
'/logs': { '/logs': {
id: '/logs' id: '/logs'
path: '/logs' path: '/logs'
@ -180,6 +180,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SystemMonitorImport preLoaderRoute: typeof SystemMonitorImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/hub/$modelId': {
id: '/hub/$modelId'
path: '/hub/$modelId'
fullPath: '/hub/$modelId'
preLoaderRoute: typeof HubModelIdImport
parentRoute: typeof rootRoute
}
'/local-api-server/logs': { '/local-api-server/logs': {
id: '/local-api-server/logs' id: '/local-api-server/logs'
path: '/local-api-server/logs' path: '/local-api-server/logs'
@ -257,6 +264,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ThreadsThreadIdImport preLoaderRoute: typeof ThreadsThreadIdImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/hub/': {
id: '/hub/'
path: '/hub'
fullPath: '/hub'
preLoaderRoute: typeof HubIndexImport
parentRoute: typeof rootRoute
}
'/settings/providers/$providerName': { '/settings/providers/$providerName': {
id: '/settings/providers/$providerName' id: '/settings/providers/$providerName'
path: '/settings/providers/$providerName' path: '/settings/providers/$providerName'
@ -279,9 +293,9 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/assistant': typeof AssistantRoute '/assistant': typeof AssistantRoute
'/hub': typeof HubRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/system-monitor': typeof SystemMonitorRoute '/system-monitor': typeof SystemMonitorRoute
'/hub/$modelId': typeof HubModelIdRoute
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
@ -293,6 +307,7 @@ export interface FileRoutesByFullPath {
'/settings/privacy': typeof SettingsPrivacyRoute '/settings/privacy': typeof SettingsPrivacyRoute
'/settings/shortcuts': typeof SettingsShortcutsRoute '/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute
'/hub': typeof HubIndexRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
'/settings/providers': typeof SettingsProvidersIndexRoute '/settings/providers': typeof SettingsProvidersIndexRoute
} }
@ -300,9 +315,9 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/assistant': typeof AssistantRoute '/assistant': typeof AssistantRoute
'/hub': typeof HubRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/system-monitor': typeof SystemMonitorRoute '/system-monitor': typeof SystemMonitorRoute
'/hub/$modelId': typeof HubModelIdRoute
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
@ -314,6 +329,7 @@ export interface FileRoutesByTo {
'/settings/privacy': typeof SettingsPrivacyRoute '/settings/privacy': typeof SettingsPrivacyRoute
'/settings/shortcuts': typeof SettingsShortcutsRoute '/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute
'/hub': typeof HubIndexRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
'/settings/providers': typeof SettingsProvidersIndexRoute '/settings/providers': typeof SettingsProvidersIndexRoute
} }
@ -322,9 +338,9 @@ export interface FileRoutesById {
__root__: typeof rootRoute __root__: typeof rootRoute
'/': typeof IndexRoute '/': typeof IndexRoute
'/assistant': typeof AssistantRoute '/assistant': typeof AssistantRoute
'/hub': typeof HubRoute
'/logs': typeof LogsRoute '/logs': typeof LogsRoute
'/system-monitor': typeof SystemMonitorRoute '/system-monitor': typeof SystemMonitorRoute
'/hub/$modelId': typeof HubModelIdRoute
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
@ -336,6 +352,7 @@ export interface FileRoutesById {
'/settings/privacy': typeof SettingsPrivacyRoute '/settings/privacy': typeof SettingsPrivacyRoute
'/settings/shortcuts': typeof SettingsShortcutsRoute '/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute
'/hub/': typeof HubIndexRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
'/settings/providers/': typeof SettingsProvidersIndexRoute '/settings/providers/': typeof SettingsProvidersIndexRoute
} }
@ -345,9 +362,9 @@ export interface FileRouteTypes {
fullPaths: fullPaths:
| '/' | '/'
| '/assistant' | '/assistant'
| '/hub'
| '/logs' | '/logs'
| '/system-monitor' | '/system-monitor'
| '/hub/$modelId'
| '/local-api-server/logs' | '/local-api-server/logs'
| '/settings/appearance' | '/settings/appearance'
| '/settings/extensions' | '/settings/extensions'
@ -359,15 +376,16 @@ export interface FileRouteTypes {
| '/settings/privacy' | '/settings/privacy'
| '/settings/shortcuts' | '/settings/shortcuts'
| '/threads/$threadId' | '/threads/$threadId'
| '/hub'
| '/settings/providers/$providerName' | '/settings/providers/$providerName'
| '/settings/providers' | '/settings/providers'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/assistant' | '/assistant'
| '/hub'
| '/logs' | '/logs'
| '/system-monitor' | '/system-monitor'
| '/hub/$modelId'
| '/local-api-server/logs' | '/local-api-server/logs'
| '/settings/appearance' | '/settings/appearance'
| '/settings/extensions' | '/settings/extensions'
@ -379,15 +397,16 @@ export interface FileRouteTypes {
| '/settings/privacy' | '/settings/privacy'
| '/settings/shortcuts' | '/settings/shortcuts'
| '/threads/$threadId' | '/threads/$threadId'
| '/hub'
| '/settings/providers/$providerName' | '/settings/providers/$providerName'
| '/settings/providers' | '/settings/providers'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/assistant' | '/assistant'
| '/hub'
| '/logs' | '/logs'
| '/system-monitor' | '/system-monitor'
| '/hub/$modelId'
| '/local-api-server/logs' | '/local-api-server/logs'
| '/settings/appearance' | '/settings/appearance'
| '/settings/extensions' | '/settings/extensions'
@ -399,6 +418,7 @@ export interface FileRouteTypes {
| '/settings/privacy' | '/settings/privacy'
| '/settings/shortcuts' | '/settings/shortcuts'
| '/threads/$threadId' | '/threads/$threadId'
| '/hub/'
| '/settings/providers/$providerName' | '/settings/providers/$providerName'
| '/settings/providers/' | '/settings/providers/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@ -407,9 +427,9 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AssistantRoute: typeof AssistantRoute AssistantRoute: typeof AssistantRoute
HubRoute: typeof HubRoute
LogsRoute: typeof LogsRoute LogsRoute: typeof LogsRoute
SystemMonitorRoute: typeof SystemMonitorRoute SystemMonitorRoute: typeof SystemMonitorRoute
HubModelIdRoute: typeof HubModelIdRoute
LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute
SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute
SettingsExtensionsRoute: typeof SettingsExtensionsRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute
@ -421,6 +441,7 @@ export interface RootRouteChildren {
SettingsPrivacyRoute: typeof SettingsPrivacyRoute SettingsPrivacyRoute: typeof SettingsPrivacyRoute
SettingsShortcutsRoute: typeof SettingsShortcutsRoute SettingsShortcutsRoute: typeof SettingsShortcutsRoute
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
HubIndexRoute: typeof HubIndexRoute
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute
} }
@ -428,9 +449,9 @@ export interface RootRouteChildren {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AssistantRoute: AssistantRoute, AssistantRoute: AssistantRoute,
HubRoute: HubRoute,
LogsRoute: LogsRoute, LogsRoute: LogsRoute,
SystemMonitorRoute: SystemMonitorRoute, SystemMonitorRoute: SystemMonitorRoute,
HubModelIdRoute: HubModelIdRoute,
LocalApiServerLogsRoute: LocalApiServerLogsRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute,
SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsAppearanceRoute: SettingsAppearanceRoute,
SettingsExtensionsRoute: SettingsExtensionsRoute, SettingsExtensionsRoute: SettingsExtensionsRoute,
@ -442,6 +463,7 @@ const rootRouteChildren: RootRouteChildren = {
SettingsPrivacyRoute: SettingsPrivacyRoute, SettingsPrivacyRoute: SettingsPrivacyRoute,
SettingsShortcutsRoute: SettingsShortcutsRoute, SettingsShortcutsRoute: SettingsShortcutsRoute,
ThreadsThreadIdRoute: ThreadsThreadIdRoute, ThreadsThreadIdRoute: ThreadsThreadIdRoute,
HubIndexRoute: HubIndexRoute,
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute, SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
SettingsProvidersIndexRoute: SettingsProvidersIndexRoute, SettingsProvidersIndexRoute: SettingsProvidersIndexRoute,
} }
@ -458,9 +480,9 @@ export const routeTree = rootRoute
"children": [ "children": [
"/", "/",
"/assistant", "/assistant",
"/hub",
"/logs", "/logs",
"/system-monitor", "/system-monitor",
"/hub/$modelId",
"/local-api-server/logs", "/local-api-server/logs",
"/settings/appearance", "/settings/appearance",
"/settings/extensions", "/settings/extensions",
@ -472,6 +494,7 @@ export const routeTree = rootRoute
"/settings/privacy", "/settings/privacy",
"/settings/shortcuts", "/settings/shortcuts",
"/threads/$threadId", "/threads/$threadId",
"/hub/",
"/settings/providers/$providerName", "/settings/providers/$providerName",
"/settings/providers/" "/settings/providers/"
] ]
@ -482,15 +505,15 @@ export const routeTree = rootRoute
"/assistant": { "/assistant": {
"filePath": "assistant.tsx" "filePath": "assistant.tsx"
}, },
"/hub": {
"filePath": "hub.tsx"
},
"/logs": { "/logs": {
"filePath": "logs.tsx" "filePath": "logs.tsx"
}, },
"/system-monitor": { "/system-monitor": {
"filePath": "system-monitor.tsx" "filePath": "system-monitor.tsx"
}, },
"/hub/$modelId": {
"filePath": "hub/$modelId.tsx"
},
"/local-api-server/logs": { "/local-api-server/logs": {
"filePath": "local-api-server/logs.tsx" "filePath": "local-api-server/logs.tsx"
}, },
@ -524,6 +547,9 @@ export const routeTree = rootRoute
"/threads/$threadId": { "/threads/$threadId": {
"filePath": "threads/$threadId.tsx" "filePath": "threads/$threadId.tsx"
}, },
"/hub/": {
"filePath": "hub/index.tsx"
},
"/settings/providers/$providerName": { "/settings/providers/$providerName": {
"filePath": "settings/providers/$providerName.tsx" "filePath": "settings/providers/$providerName.tsx"
}, },

View File

@ -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<string>()
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 (
<div className="flex h-full w-full">
<div className="flex flex-col h-full w-full">
<HeaderPage>
<button
className="relative z-20 flex items-center gap-2 cursor-pointer"
onClick={() => navigate({ to: route.hub.index })}
aria-label="Go back"
>
<div className="flex items-center justify-center size-5 rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconArrowLeft size={18} className="text-main-view-fg" />
</div>
<span className="text-main-view-fg">Back to Hub</span>
</button>
</HeaderPage>
<div className="flex-1 flex items-center justify-center">
<p className="text-main-view-fg/60">Model not found</p>
</div>
</div>
</div>
)
}
return (
<div className="flex h-full w-full">
<div className="flex flex-col h-full w-full ">
<HeaderPage>
<button
className="relative z-20 flex items-center gap-2 cursor-pointer"
onClick={() => navigate({ to: route.hub.index })}
aria-label="Go back"
>
<div className="flex items-center justify-center size-5 rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconArrowLeft size={18} className="text-main-view-fg" />
</div>
<span className="text-main-view-fg">Back to Hub</span>
</button>
</HeaderPage>
<div className="flex-1 overflow-y-auto ">
<div className="md:w-4/5 mx-auto">
<div className="max-w-4xl mx-auto p-6">
{/* Model Header */}
<div className="mb-8">
<h1
className="text-2xl font-semibold text-main-view-fg mb-4 capitalize break-words line-clamp-2"
title={
extractModelName(modelData.model_name) ||
modelData.model_name
}
>
{extractModelName(modelData.model_name) ||
modelData.model_name}
</h1>
{/* Stats */}
<div className="flex items-center gap-4 text-sm text-main-view-fg/60 mb-4 flex-wrap">
{modelData.developer && (
<>
<span>By {modelData.developer}</span>
</>
)}
<div className="flex items-center gap-2">
<IconDownload size={16} />
<span>{modelData.downloads || 0} Downloads</span>
</div>
{modelData.created_at && (
<div className="flex items-center gap-2">
<IconClock size={16} />
<span>Updated {formatDate(modelData.created_at)}</span>
</div>
)}
</div>
{/* Description */}
{modelData.description && (
<div className="text-main-view-fg/80 mb-4">
<RenderMarkdown
enableRawHtml={true}
className="select-none reset-heading"
components={{
a: ({ ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
content={
extractDescription(modelData.description) ||
modelData.description
}
/>
</div>
)}
{/* Tags */}
{tags.length > 0 && (
<div className="flex gap-2 flex-wrap">
{tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 text-sm bg-main-view-fg/10 text-main-view-fg rounded-md"
>
{tag}
</span>
))}
</div>
)}
</div>
{/* Variants Section */}
{modelData.quants && modelData.quants.length > 0 && (
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<IconFileCode size={20} className="text-main-view-fg/50" />
<h2 className="text-lg font-semibold text-main-view-fg">
Variants ({modelData.quants.length})
</h2>
</div>
<div className="w-full overflow-x-auto">
<table className="w-full min-w-[500px]">
<thead>
<tr className="border-b border-main-view-fg/10">
<th className="text-left py-3 px-2 text-sm font-medium text-main-view-fg/70">
Version
</th>
<th className="text-left py-3 px-2 text-sm font-medium text-main-view-fg/70">
Format
</th>
<th className="text-left py-3 px-2 text-sm font-medium text-main-view-fg/70">
Size
</th>
<th className="text-right py-3 px-2 text-sm font-medium text-main-view-fg/70">
Action
</th>
</tr>
</thead>
<tbody>
{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 (
<tr
key={variant.model_id}
className="border-b border-main-view-fg/5 hover:bg-main-view-fg/5"
>
<td className="py-3 px-2">
<span className="text-sm text-main-view-fg/80 font-medium">
{versionName}
</span>
</td>
<td className="py-3 px-2">
<span className="text-sm text-main-view-fg/60">
{format}
</span>
</td>
<td className="py-3 px-2">
<span className="text-sm text-main-view-fg/60">
{variant.file_size}
</span>
</td>
<td className="py-3 px-2 text-right">
{(() => {
if (isDownloading && !isDownloaded) {
return (
<div className="flex items-center justify-end gap-2">
<Progress
value={downloadProgress * 100}
className="w-12"
/>
<span className="text-xs text-main-view-fg/70 text-right">
{Math.round(downloadProgress * 100)}%
</span>
</div>
)
}
if (isDownloaded) {
return (
<Button
size="sm"
onClick={() =>
handleUseModel(variant.model_id)
}
>
Use
</Button>
)
}
return (
<Button
size="sm"
onClick={() => {
addLocalDownloadingModel(
variant.model_id
)
pullModel(
variant.model_id,
variant.path
)
}}
className={cn(isDownloading && 'hidden')}
>
Download
</Button>
)
})()}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,10 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
createFileRoute,
Link,
useNavigate,
useSearch,
} from '@tanstack/react-router'
import { route } from '@/constants/routes' import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources' import { useModelSources } from '@/hooks/useModelSources'
import { cn, fuzzySearch } from '@/lib/utils' import { cn, fuzzySearch } from '@/lib/utils'
@ -46,7 +41,7 @@ type SearchParams = {
} }
const defaultModelQuantizations = ['iq4_xs.gguf', 'q4_k_m.gguf'] 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, component: Hub,
validateSearch: (search: Record<string, unknown>): SearchParams => ({ validateSearch: (search: Record<string, unknown>): SearchParams => ({
repo: search.repo as SearchParams['repo'], repo: search.repo as SearchParams['repo'],
@ -60,7 +55,7 @@ function Hub() {
{ value: 'most-downloaded', name: t('hub:sortMostDownloaded') }, { value: 'most-downloaded', name: t('hub:sortMostDownloaded') },
] ]
const { sources, fetchSources, addSource, loading } = useModelSources() 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 [searchValue, setSearchValue] = useState('')
const [sortSelected, setSortSelected] = useState('newest') const [sortSelected, setSortSelected] = useState('newest')
const [expandedModels, setExpandedModels] = useState<Record<string, boolean>>( const [expandedModels, setExpandedModels] = useState<Record<string, boolean>>(
@ -328,7 +323,7 @@ function Hub() {
if (status === STATUS.FINISHED) { if (status === STATUS.FINISHED) {
navigate({ navigate({
to: route.hub, to: route.hub.index,
}) })
} }
@ -473,16 +468,22 @@ function Hub() {
<div className="flex items-center gap-2 justify-end sm:hidden"> <div className="flex items-center gap-2 justify-end sm:hidden">
{renderFilter()} {renderFilter()}
</div> </div>
{filteredModels.map((model) => ( {filteredModels.map((model, i) => (
<div key={model.model_name}> <div key={`${model.model_name}-${i}`}>
<Card <Card
header={ header={
<div className="flex items-center justify-between gap-x-2"> <div className="flex items-center justify-between gap-x-2">
<Link <div
to={ className="cursor-pointer"
`https://huggingface.co/${model.model_name}` as string onClick={() => {
} console.log(model.model_name)
target="_blank" navigate({
to: route.hub.model,
params: {
modelId: model.model_name,
},
})
}}
> >
<h1 <h1
className={cn( className={cn(
@ -495,7 +496,7 @@ function Hub() {
> >
{extractModelName(model.model_name) || ''} {extractModelName(model.model_name) || ''}
</h1> </h1>
</Link> </div>
<div className="shrink-0 space-x-3 flex items-center"> <div className="shrink-0 space-x-3 flex items-center">
<span className="text-main-view-fg/70 font-medium text-xs"> <span className="text-main-view-fg/70 font-medium text-xs">
{ {

View File

@ -525,7 +525,7 @@ function ProviderDetail() {
<p className="text-main-view-fg/70 mt-1 text-xs leading-relaxed"> <p className="text-main-view-fg/70 mt-1 text-xs leading-relaxed">
{t('providers:noModelFoundDesc')} {t('providers:noModelFoundDesc')}
&nbsp; &nbsp;
<Link to={route.hub}>{t('common:hub')}</Link> <Link to={route.hub.index}>{t('common:hub')}</Link>
</p> </p>
</div> </div>
)} )}