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({ set({
hardwareData: { hardwareData: {
...data, ...data,
gpus: data.gpus.map(gpu => ({ gpus: data.gpus.map((gpu) => ({
...gpu, ...gpu,
activated: gpu.activated ?? false activated: gpu.activated ?? false,
})) })),
}, },
}), }),
@ -195,29 +195,30 @@ export const useHardware = create<HardwareStore>()(
set((state) => { set((state) => {
// If we have existing GPU data, preserve the order and activation state // If we have existing GPU data, preserve the order and activation state
if (state.hardwareData.gpus.length > 0) { if (state.hardwareData.gpus.length > 0) {
// Reorder fresh GPU data to match existing order, adding new GPUs at the end // Reorder fresh GPU data to match existing order, adding new GPUs at the end
const reorderedGpus: GPU[] = [] const reorderedGpus: GPU[] = []
const processedUuids = new Set() const processedUuids = new Set()
// First, add existing GPUs in their current order, preserving activation state // First, add existing GPUs in their current order, preserving activation state
state.hardwareData.gpus.forEach(existingGpu => { state.hardwareData.gpus.forEach((existingGpu) => {
const freshGpu = data.gpus.find(gpu => gpu.uuid === existingGpu.uuid) const freshGpu = data.gpus.find(
(gpu) => gpu.uuid === existingGpu.uuid
)
if (freshGpu) { if (freshGpu) {
reorderedGpus.push({ reorderedGpus.push({
...freshGpu, ...freshGpu,
activated: existingGpu.activated ?? false activated: existingGpu.activated ?? false,
}) })
processedUuids.add(freshGpu.uuid) processedUuids.add(freshGpu.uuid)
} }
}) })
// Then, add any new GPUs that weren't in the existing order (default to inactive) // 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)) { if (!processedUuids.has(freshGpu.uuid)) {
reorderedGpus.push({ reorderedGpus.push({
...freshGpu, ...freshGpu,
activated: false activated: false,
}) })
} }
}) })
@ -225,19 +226,19 @@ export const useHardware = create<HardwareStore>()(
return { return {
hardwareData: { hardwareData: {
...data, ...data,
gpus: reorderedGpus gpus: reorderedGpus,
} },
} }
} else { } else {
// No existing GPU data, initialize all GPUs as inactive // No existing GPU data, initialize all GPUs as inactive
return { return {
hardwareData: { hardwareData: {
...data, ...data,
gpus: data.gpus.map(gpu => ({ gpus: data.gpus.map((gpu) => ({
...gpu, ...gpu,
activated: false activated: false,
})) })),
} },
} }
} }
}), }),
@ -291,42 +292,35 @@ export const useHardware = create<HardwareStore>()(
// Import and get backend type // Import and get backend type
const { useModelProvider } = await import('./useModelProvider') const { useModelProvider } = await import('./useModelProvider')
const { updateProvider, getProviderByName } = useModelProvider.getState() const { updateProvider, getProviderByName } =
useModelProvider.getState()
const llamacppProvider = getProviderByName('llamacpp') 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 = updatedState.getActivatedDeviceString(backendType) 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
})))
if (llamacppProvider) { if (llamacppProvider) {
const updatedSettings = llamacppProvider.settings.map(setting => { const updatedSettings = llamacppProvider.settings.map((setting) => {
if (setting.key === 'device') { if (setting.key === 'device') {
return { return {
...setting, ...setting,
controller_props: { controller_props: {
...setting.controller_props, ...setting.controller_props,
value: deviceString value: deviceString,
} },
} }
} }
return setting return setting
}) })
updateProvider('llamacpp', { updateProvider('llamacpp', {
settings: updatedSettings settings: updatedSettings,
}) })
console.log(`Updated llamacpp device setting to: "${deviceString}"`)
} }
} finally { } finally {
setGpuLoading(index, false) setGpuLoading(index, false)
setTimeout(resumePolling, 1000) // Resume polling after 1s setTimeout(resumePolling, 1000) // Resume polling after 1s
@ -359,8 +353,8 @@ export const useHardware = create<HardwareStore>()(
// Get activated GPUs and generate appropriate device format based on backend // Get activated GPUs and generate appropriate device format based on backend
const activatedDevices = hardwareData.gpus const activatedDevices = hardwareData.gpus
.filter(gpu => gpu.activated) .filter((gpu) => gpu.activated)
.map(gpu => { .map((gpu) => {
const isCudaBackend = backendType?.includes('cuda') const isCudaBackend = backendType?.includes('cuda')
const isVulkanBackend = backendType?.includes('vulkan') const isVulkanBackend = backendType?.includes('vulkan')
@ -388,7 +382,7 @@ export const useHardware = create<HardwareStore>()(
} }
return null return null
}) })
.filter(device => device !== null) as string[] .filter((device) => device !== null) as string[]
const deviceString = activatedDevices.join(',') const deviceString = activatedDevices.join(',')
return deviceString return deviceString
@ -401,23 +395,26 @@ export const useHardware = create<HardwareStore>()(
// Parse device string to get active device indices // Parse device string to get active device indices
const activeDevices = deviceString const activeDevices = deviceString
.split(',') .split(',')
.map(device => device.trim()) .map((device) => device.trim())
.filter(device => device.length > 0) .filter((device) => device.length > 0)
.map(device => { .map((device) => {
const match = device.match(/^(cuda|vulkan):(\d+)$/) const match = device.match(/^(cuda|vulkan):(\d+)$/)
if (match) { if (match) {
return { return {
type: match[1] as 'cuda' | 'vulkan', type: match[1] as 'cuda' | 'vulkan',
index: parseInt(match[2]) index: parseInt(match[2]),
} }
} }
return null 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 // Update GPU activation states
newGPUs.forEach((gpu, gpuIndex) => { newGPUs.forEach((gpu, gpuIndex) => {
const shouldBeActive = activeDevices.some(device => { const shouldBeActive = activeDevices.some((device) => {
if (device.type === 'cuda' && gpu.nvidia_info) { if (device.type === 'cuda' && gpu.nvidia_info) {
return gpu.nvidia_info.index === device.index return gpu.nvidia_info.index === device.index
} else if (device.type === 'vulkan' && gpu.vulkan_info) { } else if (device.type === 'vulkan' && gpu.vulkan_info) {
@ -428,15 +425,15 @@ export const useHardware = create<HardwareStore>()(
newGPUs[gpuIndex] = { newGPUs[gpuIndex] = {
...gpu, ...gpu,
activated: shouldBeActive activated: shouldBeActive,
} }
}) })
return { return {
hardwareData: { hardwareData: {
...state.hardwareData, ...state.hardwareData,
gpus: newGPUs gpus: newGPUs,
} },
} }
}) })
}, },

View File

@ -10,7 +10,7 @@ import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources' import { useModelSources } from '@/hooks/useModelSources'
import { extractModelName, extractDescription } from '@/lib/models' import { extractModelName, extractDescription } from '@/lib/models'
import { RenderMarkdown } from '@/containers/RenderMarkdown' import { RenderMarkdown } from '@/containers/RenderMarkdown'
import { useEffect, useMemo, useCallback } from 'react' import { useEffect, useMemo, useCallback, useState } from 'react'
import { useModelProvider } from '@/hooks/useModelProvider' import { useModelProvider } from '@/hooks/useModelProvider'
import { useDownloadStore } from '@/hooks/useDownloadStore' import { useDownloadStore } from '@/hooks/useDownloadStore'
import { pullModel } from '@/services/models' import { pullModel } from '@/services/models'
@ -31,6 +31,10 @@ function HubModelDetail() {
const { downloads, localDownloadingModels, addLocalDownloadingModel } = const { downloads, localDownloadingModels, addLocalDownloadingModel } =
useDownloadStore() useDownloadStore()
// State for README content
const [readmeContent, setReadmeContent] = useState<string>('')
const [isLoadingReadme, setIsLoadingReadme] = useState(false)
useEffect(() => { useEffect(() => {
fetchSources() fetchSources()
}, [fetchSources]) }, [fetchSources])
@ -112,6 +116,24 @@ function HubModelDetail() {
}) })
}, [modelData]) }, [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) { if (!modelData) {
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
@ -358,6 +380,49 @@ function HubModelDetail() {
</div> </div>
</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> </div>
</div> </div>

View File

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

View File

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