mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-09 18:26:15 +02:00
fix(benchmark): UI improvements and GPU detection fix
- Fix GPU detection to properly identify AMD discrete GPUs - Fix gauge colors (high scores now green, low scores red) - Fix gauge centering (SVG size matches container) - Add info tooltips for Tokens/sec and Time to First Token Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a9b8a906a6
commit
5e514440c5
|
|
@ -223,14 +223,29 @@ export class BenchmarkService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get GPU model (prefer discrete GPU)
|
// Get GPU model (prefer discrete GPU with dedicated VRAM)
|
||||||
let gpuModel: string | null = null
|
let gpuModel: string | null = null
|
||||||
if (graphics.controllers && graphics.controllers.length > 0) {
|
if (graphics.controllers && graphics.controllers.length > 0) {
|
||||||
const discreteGpu = graphics.controllers.find(
|
// First, look for discrete GPUs (NVIDIA, AMD discrete, or any with significant VRAM)
|
||||||
(g) => !g.vendor?.toLowerCase().includes('intel') &&
|
const discreteGpu = graphics.controllers.find((g) => {
|
||||||
!g.vendor?.toLowerCase().includes('amd') ||
|
const vendor = g.vendor?.toLowerCase() || ''
|
||||||
(g.vram && g.vram > 0)
|
const model = g.model?.toLowerCase() || ''
|
||||||
)
|
// NVIDIA GPUs are always discrete
|
||||||
|
if (vendor.includes('nvidia') || model.includes('geforce') || model.includes('rtx') || model.includes('quadro')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// AMD discrete GPUs (Radeon, not integrated APU graphics)
|
||||||
|
if ((vendor.includes('amd') || vendor.includes('ati')) &&
|
||||||
|
(model.includes('radeon') || model.includes('rx ') || model.includes('vega')) &&
|
||||||
|
!model.includes('graphics')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Any GPU with dedicated VRAM > 512MB is likely discrete
|
||||||
|
if (g.vram && g.vram > 512) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
gpuModel = discreteGpu?.model || graphics.controllers[0]?.model || null
|
gpuModel = discreteGpu?.model || graphics.controllers[0]?.model || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
35
admin/inertia/components/InfoTooltip.tsx
Normal file
35
admin/inertia/components/InfoTooltip.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { InformationCircleIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface InfoTooltipProps {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InfoTooltip({ text, className = '' }: InfoTooltipProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`relative inline-flex items-center ${className}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-desert-stone-dark hover:text-desert-green transition-colors p-0.5"
|
||||||
|
onMouseEnter={() => setIsVisible(true)}
|
||||||
|
onMouseLeave={() => setIsVisible(false)}
|
||||||
|
onFocus={() => setIsVisible(true)}
|
||||||
|
onBlur={() => setIsVisible(false)}
|
||||||
|
aria-label="More information"
|
||||||
|
>
|
||||||
|
<InformationCircleIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{isVisible && (
|
||||||
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50">
|
||||||
|
<div className="bg-desert-stone-dark text-white text-xs rounded-lg px-3 py-2 max-w-xs whitespace-normal shadow-lg">
|
||||||
|
{text}
|
||||||
|
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-desert-stone-dark" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -31,23 +31,24 @@ export default function CircularGauge({
|
||||||
|
|
||||||
const displayValue = animated ? animatedValue : value
|
const displayValue = animated ? animatedValue : value
|
||||||
|
|
||||||
|
// Size configs: container size must match SVG size (2 * (radius + strokeWidth))
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: {
|
sm: {
|
||||||
container: 'w-32 h-32',
|
container: 'w-28 h-28', // 112px = 2 * (48 + 8)
|
||||||
strokeWidth: 8,
|
strokeWidth: 8,
|
||||||
radius: 48,
|
radius: 48,
|
||||||
fontSize: 'text-xl',
|
fontSize: 'text-xl',
|
||||||
labelSize: 'text-xs',
|
labelSize: 'text-xs',
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
container: 'w-40 h-40',
|
container: 'w-[140px] h-[140px]', // 140px = 2 * (60 + 10)
|
||||||
strokeWidth: 10,
|
strokeWidth: 10,
|
||||||
radius: 60,
|
radius: 60,
|
||||||
fontSize: 'text-2xl',
|
fontSize: 'text-2xl',
|
||||||
labelSize: 'text-sm',
|
labelSize: 'text-sm',
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
container: 'w-60 h-60',
|
container: 'w-[244px] h-[244px]', // 244px = 2 * (110 + 12)
|
||||||
strokeWidth: 12,
|
strokeWidth: 12,
|
||||||
radius: 110,
|
radius: 110,
|
||||||
fontSize: 'text-4xl',
|
fontSize: 'text-4xl',
|
||||||
|
|
@ -60,10 +61,11 @@ export default function CircularGauge({
|
||||||
const offset = circumference - (displayValue / 100) * circumference
|
const offset = circumference - (displayValue / 100) * circumference
|
||||||
|
|
||||||
const getColor = () => {
|
const getColor = () => {
|
||||||
if (value >= 90) return 'desert-red'
|
// For benchmarks: higher scores = better = green
|
||||||
if (value >= 75) return 'desert-orange'
|
if (value >= 75) return 'desert-green'
|
||||||
if (value >= 50) return 'desert-tan'
|
if (value >= 50) return 'desert-olive'
|
||||||
return 'desert-olive'
|
if (value >= 25) return 'desert-orange'
|
||||||
|
return 'desert-red'
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = getColor()
|
const color = getColor()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import CircularGauge from '~/components/systeminfo/CircularGauge'
|
||||||
import InfoCard from '~/components/systeminfo/InfoCard'
|
import InfoCard from '~/components/systeminfo/InfoCard'
|
||||||
import Alert from '~/components/Alert'
|
import Alert from '~/components/Alert'
|
||||||
import StyledButton from '~/components/StyledButton'
|
import StyledButton from '~/components/StyledButton'
|
||||||
|
import InfoTooltip from '~/components/InfoTooltip'
|
||||||
import {
|
import {
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
CpuChipIcon,
|
CpuChipIcon,
|
||||||
|
|
@ -521,7 +522,10 @@ export default function BenchmarkPage(props: {
|
||||||
<div className="text-3xl font-bold text-desert-green">
|
<div className="text-3xl font-bold text-desert-green">
|
||||||
{latestResult.ai_tokens_per_second.toFixed(1)}
|
{latestResult.ai_tokens_per_second.toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-desert-stone-dark">Tokens per Second</div>
|
<div className="text-sm text-desert-stone-dark flex items-center gap-1">
|
||||||
|
Tokens per Second
|
||||||
|
<InfoTooltip text="How fast the AI generates text. Higher is better. 30+ tokens/sec feels responsive, 60+ feels instant." />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -532,7 +536,10 @@ export default function BenchmarkPage(props: {
|
||||||
<div className="text-3xl font-bold text-desert-green">
|
<div className="text-3xl font-bold text-desert-green">
|
||||||
{latestResult.ai_time_to_first_token?.toFixed(0) || 'N/A'} ms
|
{latestResult.ai_time_to_first_token?.toFixed(0) || 'N/A'} ms
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-desert-stone-dark">Time to First Token</div>
|
<div className="text-sm text-desert-stone-dark flex items-center gap-1">
|
||||||
|
Time to First Token
|
||||||
|
<InfoTooltip text="How quickly the AI starts responding after you send a message. Lower is better. Under 500ms feels instant." />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user