import { useEffect, useRef, useState } from 'react' import { IconChartBar, IconCpu, IconDatabase, IconRobot, IconServer } from '@tabler/icons-react' import CircularGauge from '~/components/systeminfo/CircularGauge' import StyledButton from '~/components/StyledButton' import { getScoreDisplay } from '~/lib/benchmarkScore' import BenchmarkResult from '#models/benchmark_result' interface ScoreRevealProps { result: BenchmarkResult scoreScale?: { max: number; caption: string } onDone: () => void } // Same thresholds as the NOMAD Score section on the benchmark page. const getScoreColor = (score: number) => { if (score >= 70) return 'text-green-600' if (score >= 40) return 'text-yellow-600' return 'text-red-600' } // AI tokens/sec normalized to 0-100 (30 tok/s = 50, 60 tok/s = 100), // mirroring the benchmark page's getAIScore. const getAIScore = (tokensPerSecond: number): number => Math.min(100, Math.max(0, (tokensPerSecond / 60) * 100)) /** rAF count-up from 0 to `target` with ease-out, for the big score number. */ function useCountUp(target: number, durationMs = 1200): number { const [value, setValue] = useState(0) useEffect(() => { let raf = 0 const start = performance.now() const tick = (now: number) => { const t = Math.min(1, (now - start) / durationMs) const eased = 1 - Math.pow(1 - t, 3) setValue(target * eased) if (t < 1) raf = requestAnimationFrame(tick) } raf = requestAnimationFrame(tick) return () => cancelAnimationFrame(raf) }, [target, durationMs]) return value } /** * End-of-run score reveal. Replaces the abrupt unmount of the live run view * with a deliberate report: big NOMAD score gauge + count-up number, then the * sub-score gauges cascading in. Dismisses via the Continue button or an * auto-dismiss timer. */ export default function ScoreReveal({ result, scoreScale = { max: 100, caption: 'out of 100' }, onDone, }: ScoreRevealProps) { const displayScore = useCountUp(result.nomad_score) // A partial (System/AI Only) run is not the NOMAD Score -- relabel + flag it. const scoreInfo = getScoreDisplay(result.benchmark_type) // Sub-score gauges, mirroring the System Performance / AI Performance grids. const gauges: { label: string value: number variant: 'cpu' | 'memory' | 'disk' icon: React.ReactNode }[] = [ { label: 'CPU', value: result.cpu_score * 100, variant: 'cpu', icon: }, { label: 'Memory', value: result.memory_score * 100, variant: 'memory', icon: }, { label: 'Disk Read', value: result.disk_read_score * 100, variant: 'disk', icon: }, { label: 'Disk Write', value: result.disk_write_score * 100, variant: 'disk', icon: }, ] if (result.ai_tokens_per_second) { gauges.push({ label: 'AI Score', value: getAIScore(result.ai_tokens_per_second), variant: 'cpu', icon: , }) } // Cascade: mount one gauge every 150ms; each CircularGauge animates 0→value // on mount, so staggered mounts produce the cascade. const [visibleCount, setVisibleCount] = useState(0) useEffect(() => { if (visibleCount >= gauges.length) return const id = setTimeout(() => setVisibleCount((n) => n + 1), 150) return () => clearTimeout(id) }, [visibleCount, gauges.length]) // Dismiss exactly once, whether via the button or the auto-dismiss timer. const doneRef = useRef(false) const onDoneRef = useRef(onDone) onDoneRef.current = onDone const fireDone = () => { if (doneRef.current) return doneRef.current = true onDoneRef.current() } useEffect(() => { const id = setTimeout(fireDone, 5000) return () => clearTimeout(id) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return (
Report {scoreInfo.isPartial && ( Partial )}
} />
{displayScore.toFixed(1)}

{scoreInfo.isPartial ? scoreInfo.cta : 'Your NOMAD Score is a weighted composite of all benchmark results.'}

{gauges.slice(0, visibleCount).map((g) => (
))}
Continue
) }