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',
hardware: '/settings/hardware',
},
hub: '/hub',
hub: {
index: '/hub/',
model: '/hub/$modelId',
},
localApiServerlogs: '/local-api-server/logs',
systemMonitor: '/system-monitor',
threadsDetail: '/threads/$threadId',

View File

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

View File

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

View File

@ -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,
},

View File

@ -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"
},

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 */
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">
{

View File

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