enhancement: show readme on detail each model (#5705)

* 🧹cleanup: linter and log

* Update web-app/src/routes/hub/$modelId.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Faisal Amir 2025-07-07 09:54:16 +07:00 committed by GitHub
parent d4a3d6a0d6
commit a0be23b500
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 196 additions and 101 deletions

View File

@ -184,10 +184,10 @@ export const useHardware = create<HardwareStore>()(
set({
hardwareData: {
...data,
gpus: data.gpus.map(gpu => ({
gpus: data.gpus.map((gpu) => ({
...gpu,
activated: gpu.activated ?? false
}))
activated: gpu.activated ?? false,
})),
},
}),
@ -195,49 +195,50 @@ export const useHardware = create<HardwareStore>()(
set((state) => {
// If we have existing GPU data, preserve the order and activation state
if (state.hardwareData.gpus.length > 0) {
// Reorder fresh GPU data to match existing order, adding new GPUs at the end
const reorderedGpus: GPU[] = []
const processedUuids = new Set()
// First, add existing GPUs in their current order, preserving activation state
state.hardwareData.gpus.forEach(existingGpu => {
const freshGpu = data.gpus.find(gpu => gpu.uuid === existingGpu.uuid)
state.hardwareData.gpus.forEach((existingGpu) => {
const freshGpu = data.gpus.find(
(gpu) => gpu.uuid === existingGpu.uuid
)
if (freshGpu) {
reorderedGpus.push({
...freshGpu,
activated: existingGpu.activated ?? false
activated: existingGpu.activated ?? false,
})
processedUuids.add(freshGpu.uuid)
}
})
// Then, add any new GPUs that weren't in the existing order (default to inactive)
data.gpus.forEach(freshGpu => {
data.gpus.forEach((freshGpu) => {
if (!processedUuids.has(freshGpu.uuid)) {
reorderedGpus.push({
...freshGpu,
activated: false
activated: false,
})
}
})
return {
hardwareData: {
...data,
gpus: reorderedGpus
}
gpus: reorderedGpus,
},
}
} else {
// No existing GPU data, initialize all GPUs as inactive
return {
hardwareData: {
...data,
gpus: data.gpus.map(gpu => ({
gpus: data.gpus.map((gpu) => ({
...gpu,
activated: false
}))
}
activated: false,
})),
},
}
}
}),
@ -265,10 +266,10 @@ export const useHardware = create<HardwareStore>()(
const { pausePolling, resumePolling, setGpuLoading } = get()
pausePolling()
setGpuLoading(index, true)
try {
await new Promise((resolve) => setTimeout(resolve, 200)) // Simulate async operation
set((state) => {
const newGPUs = [...state.hardwareData.gpus]
if (index >= 0 && index < newGPUs.length) {
@ -277,7 +278,7 @@ export const useHardware = create<HardwareStore>()(
activated: !newGPUs[index].activated,
}
}
return {
hardwareData: {
...state.hardwareData,
@ -285,48 +286,41 @@ export const useHardware = create<HardwareStore>()(
},
}
})
// Update the device setting after state change
const updatedState = get()
// Import and get backend type
const { useModelProvider } = await import('./useModelProvider')
const { updateProvider, getProviderByName } = useModelProvider.getState()
const { updateProvider, getProviderByName } =
useModelProvider.getState()
const llamacppProvider = getProviderByName('llamacpp')
const backendType = llamacppProvider?.settings.find(s => s.key === 'version_backend')?.controller_props.value as string
const deviceString = updatedState.getActivatedDeviceString(backendType)
console.log(`GPU ${index} activation toggled. Backend: "${backendType}", New device string: "${deviceString}"`)
console.log('Activated GPUs:', updatedState.hardwareData.gpus.filter(gpu => gpu.activated).map((gpu, i) => ({
name: gpu.name,
nvidia: gpu.nvidia_info?.index,
vulkan: gpu.vulkan_info?.index,
activated: gpu.activated
})))
const backendType = llamacppProvider?.settings.find(
(s) => s.key === 'version_backend'
)?.controller_props.value as string
const deviceString =
updatedState.getActivatedDeviceString(backendType)
if (llamacppProvider) {
const updatedSettings = llamacppProvider.settings.map(setting => {
const updatedSettings = llamacppProvider.settings.map((setting) => {
if (setting.key === 'device') {
return {
...setting,
controller_props: {
...setting.controller_props,
value: deviceString
}
value: deviceString,
},
}
}
return setting
})
updateProvider('llamacpp', {
settings: updatedSettings
settings: updatedSettings,
})
console.log(`Updated llamacpp device setting to: "${deviceString}"`)
}
} finally {
setGpuLoading(index, false)
setTimeout(resumePolling, 1000) // Resume polling after 1s
@ -356,14 +350,14 @@ export const useHardware = create<HardwareStore>()(
getActivatedDeviceString: (backendType?: string) => {
const { hardwareData } = get()
// Get activated GPUs and generate appropriate device format based on backend
const activatedDevices = hardwareData.gpus
.filter(gpu => gpu.activated)
.map(gpu => {
.filter((gpu) => gpu.activated)
.map((gpu) => {
const isCudaBackend = backendType?.includes('cuda')
const isVulkanBackend = backendType?.includes('vulkan')
// Handle different backend scenarios
if (isCudaBackend && isVulkanBackend) {
// Mixed backend - prefer CUDA for NVIDIA GPUs, Vulkan for others
@ -388,8 +382,8 @@ export const useHardware = create<HardwareStore>()(
}
return null
})
.filter(device => device !== null) as string[]
.filter((device) => device !== null) as string[]
const deviceString = activatedDevices.join(',')
return deviceString
},
@ -397,27 +391,30 @@ export const useHardware = create<HardwareStore>()(
updateGPUActivationFromDeviceString: (deviceString: string) => {
set((state) => {
const newGPUs = [...state.hardwareData.gpus]
// Parse device string to get active device indices
const activeDevices = deviceString
.split(',')
.map(device => device.trim())
.filter(device => device.length > 0)
.map(device => {
.map((device) => device.trim())
.filter((device) => device.length > 0)
.map((device) => {
const match = device.match(/^(cuda|vulkan):(\d+)$/)
if (match) {
return {
type: match[1] as 'cuda' | 'vulkan',
index: parseInt(match[2])
index: parseInt(match[2]),
}
}
return null
})
.filter(device => device !== null) as Array<{type: 'cuda' | 'vulkan', index: number}>
.filter((device) => device !== null) as Array<{
type: 'cuda' | 'vulkan'
index: number
}>
// Update GPU activation states
newGPUs.forEach((gpu, gpuIndex) => {
const shouldBeActive = activeDevices.some(device => {
const shouldBeActive = activeDevices.some((device) => {
if (device.type === 'cuda' && gpu.nvidia_info) {
return gpu.nvidia_info.index === device.index
} else if (device.type === 'vulkan' && gpu.vulkan_info) {
@ -425,18 +422,18 @@ export const useHardware = create<HardwareStore>()(
}
return false
})
newGPUs[gpuIndex] = {
...gpu,
activated: shouldBeActive
activated: shouldBeActive,
}
})
return {
hardwareData: {
...state.hardwareData,
gpus: newGPUs
}
gpus: newGPUs,
},
}
})
},

View File

@ -10,7 +10,7 @@ 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 { useEffect, useMemo, useCallback, useState } from 'react'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useDownloadStore } from '@/hooks/useDownloadStore'
import { pullModel } from '@/services/models'
@ -31,6 +31,10 @@ function HubModelDetail() {
const { downloads, localDownloadingModels, addLocalDownloadingModel } =
useDownloadStore()
// State for README content
const [readmeContent, setReadmeContent] = useState<string>('')
const [isLoadingReadme, setIsLoadingReadme] = useState(false)
useEffect(() => {
fetchSources()
}, [fetchSources])
@ -112,6 +116,24 @@ function HubModelDetail() {
})
}, [modelData])
// Fetch README content when modelData.readme is available
useEffect(() => {
if (modelData?.readme) {
setIsLoadingReadme(true)
fetch(modelData.readme)
.then((response) => response.text())
.then((content) => {
setReadmeContent(content)
setIsLoadingReadme(false)
})
.catch((error) => {
console.error('Failed to fetch README:', error)
setIsLoadingReadme(false)
})
}
}, [modelData?.readme])
if (!modelData) {
return (
<div className="flex h-full w-full">
@ -358,6 +380,49 @@ function HubModelDetail() {
</div>
</div>
)}
{/* README Section */}
{modelData.readme && (
<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">
README
</h2>
</div>
{isLoadingReadme ? (
<div className="flex items-center justify-center py-8">
<span className="text-main-view-fg/60">
Loading README...
</span>
</div>
) : readmeContent ? (
<div className="prose prose-invert max-w-none">
<RenderMarkdown
enableRawHtml={true}
className="text-main-view-fg/80"
components={{
a: ({ ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
content={readmeContent}
/>
</div>
) : (
<div className="flex items-center justify-center py-8">
<span className="text-main-view-fg/60">
Failed to load README
</span>
</div>
)}
</div>
)}
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { useHardware } from '@/hooks/useHardware'
@ -13,22 +14,28 @@ import { useTranslation } from '@/i18n/react-i18next-compat'
import { toNumber } from '@/utils/number'
import { useModelProvider } from '@/hooks/useModelProvider'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.systemMonitor as any)({
component: SystemMonitor,
})
function SystemMonitor() {
const { t } = useTranslation()
const { hardwareData, systemUsage, updateHardwareDataPreservingGpuOrder, updateSystemUsage, updateGPUActivationFromDeviceString } =
useHardware()
const {
hardwareData,
systemUsage,
updateHardwareDataPreservingGpuOrder,
updateSystemUsage,
updateGPUActivationFromDeviceString,
} = useHardware()
const [activeModels, setActiveModels] = useState<string[]>([])
const { providers, getProviderByName } = useModelProvider()
const [isInitialized, setIsInitialized] = useState(false)
// Determine backend type and filter GPUs accordingly (same logic as hardware.tsx)
const llamacpp = providers.find((p) => p.provider === 'llamacpp')
const versionBackend = llamacpp?.settings.find((s) => s.key === "version_backend")?.controller_props.value
const versionBackend = llamacpp?.settings.find(
(s) => s.key === 'version_backend'
)?.controller_props.value
useEffect(() => {
// Initial data fetch - use updateHardwareDataPreservingGpuOrder like hardware.tsx
@ -52,53 +59,74 @@ function SystemMonitor() {
useEffect(() => {
if (hardwareData.gpus.length > 0 && !isInitialized) {
const llamacppProvider = getProviderByName('llamacpp')
const currentDeviceSetting = llamacppProvider?.settings.find(s => s.key === 'device')?.controller_props.value as string
const currentDeviceSetting = llamacppProvider?.settings.find(
(s) => s.key === 'device'
)?.controller_props.value as string
if (currentDeviceSetting) {
updateGPUActivationFromDeviceString(currentDeviceSetting)
}
setIsInitialized(true)
}
}, [hardwareData.gpus.length, isInitialized, getProviderByName, updateGPUActivationFromDeviceString])
}, [
hardwareData.gpus.length,
isInitialized,
getProviderByName,
updateGPUActivationFromDeviceString,
])
// Sync device setting when GPU activations change (only after initialization) - same logic as hardware.tsx
const { getActivatedDeviceString } = useHardware()
const { updateProvider } = useModelProvider()
const gpuActivationStates = hardwareData.gpus.map(gpu => gpu.activated)
const gpuActivationStates = hardwareData.gpus.map((gpu) => gpu.activated)
useEffect(() => {
if (isInitialized && hardwareData.gpus.length > 0) {
const llamacppProvider = getProviderByName('llamacpp')
const backendType = llamacppProvider?.settings.find(s => s.key === 'version_backend')?.controller_props.value as string
const backendType = llamacppProvider?.settings.find(
(s) => s.key === 'version_backend'
)?.controller_props.value as string
const deviceString = getActivatedDeviceString(backendType)
if (llamacppProvider) {
const currentDeviceSetting = llamacppProvider.settings.find(s => s.key === 'device')
const currentDeviceSetting = llamacppProvider.settings.find(
(s) => s.key === 'device'
)
// Sync device string when GPU activations change (only after initialization)
if (currentDeviceSetting && currentDeviceSetting.controller_props.value !== deviceString) {
const updatedSettings = llamacppProvider.settings.map(setting => {
if (
currentDeviceSetting &&
currentDeviceSetting.controller_props.value !== deviceString
) {
const updatedSettings = llamacppProvider.settings.map((setting) => {
if (setting.key === 'device') {
return {
...setting,
controller_props: {
...setting.controller_props,
value: deviceString
}
value: deviceString,
},
}
}
return setting
})
updateProvider('llamacpp', {
settings: updatedSettings
settings: updatedSettings,
})
}
}
}
}, [isInitialized, gpuActivationStates, versionBackend, getActivatedDeviceString, updateProvider, getProviderByName, hardwareData.gpus.length])
}, [
isInitialized,
gpuActivationStates,
versionBackend,
getActivatedDeviceString,
updateProvider,
getProviderByName,
hardwareData.gpus.length,
])
const stopRunningModel = (modelId: string) => {
stopModel(modelId)
@ -120,8 +148,10 @@ function SystemMonitor() {
) * 100
// Determine backend type and filter GPUs accordingly
const isCudaBackend = typeof versionBackend === 'string' && versionBackend.includes('cuda')
const isVulkanBackend = typeof versionBackend === 'string' && versionBackend.includes('vulkan')
const isCudaBackend =
typeof versionBackend === 'string' && versionBackend.includes('cuda')
const isVulkanBackend =
typeof versionBackend === 'string' && versionBackend.includes('vulkan')
// Check if GPU should be active based on backend compatibility
const isGPUCompatible = (gpu: any) => {
@ -144,7 +174,7 @@ function SystemMonitor() {
}
// Filter to show only active GPUs
const activeGPUs = hardwareData.gpus.filter(gpu => isGPUActive(gpu))
const activeGPUs = hardwareData.gpus.filter((gpu) => isGPUActive(gpu))
return (
<div className="flex flex-col h-full bg-main-view overflow-y-auto p-6">
@ -313,9 +343,10 @@ function SystemMonitor() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{activeGPUs.map((gpu, index) => {
// Find the corresponding system usage data for this GPU
const gpuUsage = systemUsage.gpus.find(usage => usage.uuid === gpu.uuid)
const gpuIndex = hardwareData.gpus.findIndex(hwGpu => hwGpu.uuid === gpu.uuid)
const gpuUsage = systemUsage.gpus.find(
(usage) => usage.uuid === gpu.uuid
)
return (
<div
key={gpu.uuid || index}
@ -337,13 +368,13 @@ function SystemMonitor() {
<span className="text-main-view-fg">
{gpuUsage ? (
<>
{formatMegaBytes(gpuUsage.used_memory)}{' '}
/ {formatMegaBytes(gpu.total_memory)}
{formatMegaBytes(gpuUsage.used_memory)} /{' '}
{formatMegaBytes(gpu.total_memory)}
</>
) : (
<>
{formatMegaBytes(0)}{' '}
/ {formatMegaBytes(gpu.total_memory)}
{formatMegaBytes(0)} /{' '}
{formatMegaBytes(gpu.total_memory)}
</>
)}
</span>
@ -362,14 +393,16 @@ function SystemMonitor() {
</span>
<span className="text-main-view-fg">
{gpu.nvidia_info?.compute_capability ||
gpu.vulkan_info?.api_version || '-'}
gpu.vulkan_info?.api_version ||
'-'}
</span>
</div>
<div className="mt-2">
<Progress
value={
gpuUsage ?
((gpuUsage.used_memory / gpu.total_memory) * 100) : 0
gpuUsage
? (gpuUsage.used_memory / gpu.total_memory) * 100
: 0
}
className="h-2 w-full"
/>
@ -385,7 +418,6 @@ function SystemMonitor() {
</div>
)}
</div>
</div>
)
}

View File

@ -20,6 +20,7 @@ export interface CatalogModel {
num_quants: number
quants: ModelQuant[]
created_at?: string
readme?: string
}
export type ModelCatalog = CatalogModel[]