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:
Chris Sherwood 2026-01-24 22:41:26 -08:00
parent a9b8a906a6
commit 5e514440c5
4 changed files with 74 additions and 15 deletions

View File

@ -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
if (graphics.controllers && graphics.controllers.length > 0) {
const discreteGpu = graphics.controllers.find(
(g) => !g.vendor?.toLowerCase().includes('intel') &&
!g.vendor?.toLowerCase().includes('amd') ||
(g.vram && g.vram > 0)
)
// First, look for discrete GPUs (NVIDIA, AMD discrete, or any with significant VRAM)
const discreteGpu = graphics.controllers.find((g) => {
const vendor = g.vendor?.toLowerCase() || ''
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
}

View 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>
)
}

View File

@ -31,23 +31,24 @@ export default function CircularGauge({
const displayValue = animated ? animatedValue : value
// Size configs: container size must match SVG size (2 * (radius + strokeWidth))
const sizes = {
sm: {
container: 'w-32 h-32',
container: 'w-28 h-28', // 112px = 2 * (48 + 8)
strokeWidth: 8,
radius: 48,
fontSize: 'text-xl',
labelSize: 'text-xs',
},
md: {
container: 'w-40 h-40',
container: 'w-[140px] h-[140px]', // 140px = 2 * (60 + 10)
strokeWidth: 10,
radius: 60,
fontSize: 'text-2xl',
labelSize: 'text-sm',
},
lg: {
container: 'w-60 h-60',
container: 'w-[244px] h-[244px]', // 244px = 2 * (110 + 12)
strokeWidth: 12,
radius: 110,
fontSize: 'text-4xl',
@ -60,10 +61,11 @@ export default function CircularGauge({
const offset = circumference - (displayValue / 100) * circumference
const getColor = () => {
if (value >= 90) return 'desert-red'
if (value >= 75) return 'desert-orange'
if (value >= 50) return 'desert-tan'
return 'desert-olive'
// For benchmarks: higher scores = better = green
if (value >= 75) return 'desert-green'
if (value >= 50) return 'desert-olive'
if (value >= 25) return 'desert-orange'
return 'desert-red'
}
const color = getColor()

View File

@ -6,6 +6,7 @@ import CircularGauge from '~/components/systeminfo/CircularGauge'
import InfoCard from '~/components/systeminfo/InfoCard'
import Alert from '~/components/Alert'
import StyledButton from '~/components/StyledButton'
import InfoTooltip from '~/components/InfoTooltip'
import {
ChartBarIcon,
CpuChipIcon,
@ -521,7 +522,10 @@ export default function BenchmarkPage(props: {
<div className="text-3xl font-bold text-desert-green">
{latestResult.ai_tokens_per_second.toFixed(1)}
</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>
@ -532,7 +536,10 @@ export default function BenchmarkPage(props: {
<div className="text-3xl font-bold text-desert-green">
{latestResult.ai_time_to_first_token?.toFixed(0) || 'N/A'} ms
</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>