fix(benchmark): Add settings nav link, fix submission bug, improve UX

- Add Benchmark to Settings sidebar navigation
- Fix Luxon DateTime bug when saving submission timestamp
- Add privacy explanation text before Share button
- Add error handling and display for failed submissions
- Show "Submitting..." state and success confirmation
- Add link to view leaderboard after successful submission

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-01-21 16:55:11 -08:00
parent 1eed81d18f
commit 45180a8bc3
3 changed files with 47 additions and 11 deletions

View File

@ -5,6 +5,7 @@ import Docker from 'dockerode'
import si from 'systeminformation' import si from 'systeminformation'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import axios from 'axios' import axios from 'axios'
import { DateTime } from 'luxon'
import BenchmarkResult from '#models/benchmark_result' import BenchmarkResult from '#models/benchmark_result'
import BenchmarkSetting from '#models/benchmark_setting' import BenchmarkSetting from '#models/benchmark_setting'
import { SystemService } from '#services/system_service' import { SystemService } from '#services/system_service'
@ -154,7 +155,7 @@ export class BenchmarkService {
if (response.data.success) { if (response.data.success) {
result.submitted_to_repository = true result.submitted_to_repository = true
result.submitted_at = new Date() as any result.submitted_at = DateTime.now()
result.repository_id = response.data.repository_id result.repository_id = response.data.repository_id
await result.save() await result.save()

View File

@ -1,4 +1,5 @@
import { import {
ChartBarIcon,
Cog6ToothIcon, Cog6ToothIcon,
CommandLineIcon, CommandLineIcon,
FolderIcon, FolderIcon,
@ -16,6 +17,7 @@ import { getServiceLink } from '~/lib/navigation'
const navigation = [ const navigation = [
{ name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false }, { name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
{ name: 'Benchmark', href: '/settings/benchmark', icon: ChartBarIcon, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false }, { name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false }, { name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
{ name: 'Models Manager', href: '/settings/models', icon: IconDatabaseStar, current: false }, { name: 'Models Manager', href: '/settings/models', icon: IconDatabaseStar, current: false },

View File

@ -128,18 +128,27 @@ export default function BenchmarkPage(props: {
}) })
// Submit to repository mutation // Submit to repository mutation
const [submitError, setSubmitError] = useState<string | null>(null)
const submitResult = useMutation({ const submitResult = useMutation({
mutationFn: async (benchmarkId?: string) => { mutationFn: async (benchmarkId?: string) => {
setSubmitError(null)
const res = await fetch('/api/benchmark/submit', { const res = await fetch('/api/benchmark/submit', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ benchmark_id: benchmarkId }), body: JSON.stringify({ benchmark_id: benchmarkId }),
}) })
return res.json() const data = await res.json()
if (!data.success) {
throw new Error(data.error || 'Failed to submit benchmark')
}
return data
}, },
onSuccess: () => { onSuccess: () => {
refetchLatest() refetchLatest()
}, },
onError: (error: Error) => {
setSubmitError(error.message)
},
}) })
// Simulate progress during sync benchmark (since we don't get SSE updates) // Simulate progress during sync benchmark (since we don't get SSE updates)
@ -336,21 +345,45 @@ export default function BenchmarkPage(props: {
Your NOMAD Score is a weighted composite of all benchmark results. Your NOMAD Score is a weighted composite of all benchmark results.
</p> </p>
{!latestResult.submitted_to_repository && ( {!latestResult.submitted_to_repository && (
<StyledButton <div className="space-y-3">
onClick={() => submitResult.mutate(latestResult.benchmark_id)} <p className="text-sm text-desert-stone-dark">
disabled={submitResult.isPending} Share your benchmark score anonymously with the NOMAD community. Only your hardware specs and scores are sent no identifying information.
leftIcon={<CloudArrowUpIcon className="w-5 h-5" />} </p>
> <StyledButton
Share with Community onClick={() => submitResult.mutate(latestResult.benchmark_id)}
</StyledButton> disabled={submitResult.isPending}
leftIcon={<CloudArrowUpIcon className="w-5 h-5" />}
>
{submitResult.isPending ? 'Submitting...' : 'Share with Community'}
</StyledButton>
{submitError && (
<Alert
type="error"
title="Submission Failed"
message={submitError}
variant="bordered"
dismissible
onDismiss={() => setSubmitError(null)}
/>
)}
</div>
)} )}
{latestResult.submitted_to_repository && ( {latestResult.submitted_to_repository && (
<Alert <Alert
type="success" type="success"
title="Shared with Community" title="Shared with Community"
message={`Repository ID: ${latestResult.repository_id}`} message="Your benchmark has been submitted to the community leaderboard. Thanks for contributing!"
variant="bordered" variant="bordered"
/> >
<a
href="https://benchmark.projectnomad.us"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-desert-green hover:underline mt-2 inline-block"
>
View the leaderboard
</a>
</Alert>
)} )}
</div> </div>
</div> </div>