✨enhancement: add hub detail page
This commit is contained in:
parent
0343c09704
commit
c34291237f
@ -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',
|
||||
|
||||
@ -57,7 +57,7 @@ const mainMenus = [
|
||||
{
|
||||
title: 'common:hub',
|
||||
icon: IconAppsFilled,
|
||||
route: route.hub,
|
||||
route: route.hub.index,
|
||||
},
|
||||
{
|
||||
title: 'common:settings',
|
||||
|
||||
@ -29,7 +29,7 @@ function SetupScreen() {
|
||||
<Card
|
||||
header={
|
||||
<Link
|
||||
to={route.hub}
|
||||
to={route.hub.index}
|
||||
search={{
|
||||
...(!isProd ? { step: 'setup_local_provider' } : {}),
|
||||
}}
|
||||
|
||||
@ -78,7 +78,7 @@ export function DataProvider() {
|
||||
const resource = params.slice(1).join('/')
|
||||
// return { action, provider, resource }
|
||||
navigate({
|
||||
to: route.hub,
|
||||
to: route.hub.index,
|
||||
search: {
|
||||
repo: resource,
|
||||
},
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as SystemMonitorImport } from './routes/system-monitor'
|
||||
import { Route as LogsImport } from './routes/logs'
|
||||
import { Route as HubImport } from './routes/hub'
|
||||
import { Route as AssistantImport } from './routes/assistant'
|
||||
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 SettingsShortcutsImport } from './routes/settings/shortcuts'
|
||||
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 SettingsAppearanceImport } from './routes/settings/appearance'
|
||||
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 SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
|
||||
|
||||
@ -44,12 +45,6 @@ const LogsRoute = LogsImport.update({
|
||||
getParentRoute: () => 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"
|
||||
},
|
||||
|
||||
367
web-app/src/routes/hub/$modelId.tsx
Normal file
367
web-app/src/routes/hub/$modelId.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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<string, unknown>): 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<Record<string, boolean>>(
|
||||
@ -328,7 +323,7 @@ function Hub() {
|
||||
|
||||
if (status === STATUS.FINISHED) {
|
||||
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">
|
||||
{renderFilter()}
|
||||
</div>
|
||||
{filteredModels.map((model) => (
|
||||
<div key={model.model_name}>
|
||||
{filteredModels.map((model, i) => (
|
||||
<div key={`${model.model_name}-${i}`}>
|
||||
<Card
|
||||
header={
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<Link
|
||||
to={
|
||||
`https://huggingface.co/${model.model_name}` as string
|
||||
}
|
||||
target="_blank"
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
console.log(model.model_name)
|
||||
navigate({
|
||||
to: route.hub.model,
|
||||
params: {
|
||||
modelId: model.model_name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
className={cn(
|
||||
@ -495,7 +496,7 @@ function Hub() {
|
||||
>
|
||||
{extractModelName(model.model_name) || ''}
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="shrink-0 space-x-3 flex items-center">
|
||||
<span className="text-main-view-fg/70 font-medium text-xs">
|
||||
{
|
||||
@ -525,7 +525,7 @@ function ProviderDetail() {
|
||||
<p className="text-main-view-fg/70 mt-1 text-xs leading-relaxed">
|
||||
{t('providers:noModelFoundDesc')}
|
||||
|
||||
<Link to={route.hub}>{t('common:hub')}</Link>
|
||||
<Link to={route.hub.index}>{t('common:hub')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user