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:
parent
d4a3d6a0d6
commit
a0be23b500
@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export interface CatalogModel {
|
||||
num_quants: number
|
||||
quants: ModelQuant[]
|
||||
created_at?: string
|
||||
readme?: string
|
||||
}
|
||||
|
||||
export type ModelCatalog = CatalogModel[]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user