mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-07 01:06:15 +02:00
feat(benchmark): Add Builder Tag system for community leaderboard
- Add builder_tag column to benchmark_results table - Create BuilderTagSelector component with word dropdowns + randomize - Add 50 adjectives and 50 nouns for NOMAD-themed tags (e.g., Tactical-Llama-1234) - Add anonymous sharing option checkbox - Add builder tag display in Benchmark Details section - Add Benchmark History section showing all past benchmarks - Update submission API to accept anonymous flag - Add /api/benchmark/builder-tag endpoint to update tags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
82719188cd
commit
4d34b9a2f3
|
|
@ -169,9 +169,10 @@ export default class BenchmarkController {
|
||||||
*/
|
*/
|
||||||
async submit({ request, response }: HttpContext) {
|
async submit({ request, response }: HttpContext) {
|
||||||
const payload = await request.validateUsing(submitBenchmarkValidator)
|
const payload = await request.validateUsing(submitBenchmarkValidator)
|
||||||
|
const anonymous = request.input('anonymous') === true || request.input('anonymous') === 'true'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const submitResult = await this.benchmarkService.submitToRepository(payload.benchmark_id)
|
const submitResult = await this.benchmarkService.submitToRepository(payload.benchmark_id, anonymous)
|
||||||
return response.send({
|
return response.send({
|
||||||
success: true,
|
success: true,
|
||||||
repository_id: submitResult.repository_id,
|
repository_id: submitResult.repository_id,
|
||||||
|
|
@ -185,6 +186,48 @@ export default class BenchmarkController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update builder tag for a benchmark result
|
||||||
|
*/
|
||||||
|
async updateBuilderTag({ request, response }: HttpContext) {
|
||||||
|
const benchmarkId = request.input('benchmark_id')
|
||||||
|
const builderTag = request.input('builder_tag')
|
||||||
|
|
||||||
|
if (!benchmarkId) {
|
||||||
|
return response.status(400).send({
|
||||||
|
success: false,
|
||||||
|
error: 'benchmark_id is required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.benchmarkService.getResultById(benchmarkId)
|
||||||
|
if (!result) {
|
||||||
|
return response.status(404).send({
|
||||||
|
success: false,
|
||||||
|
error: 'Benchmark result not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate builder tag format if provided
|
||||||
|
if (builderTag) {
|
||||||
|
const tagPattern = /^[A-Za-z]+-[A-Za-z]+-\d{4}$/
|
||||||
|
if (!tagPattern.test(builderTag)) {
|
||||||
|
return response.status(400).send({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid builder tag format. Expected: Word-Word-0000',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.builder_tag = builderTag || null
|
||||||
|
await result.save()
|
||||||
|
|
||||||
|
return response.send({
|
||||||
|
success: true,
|
||||||
|
builder_tag: result.builder_tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get comparison stats from central repository
|
* Get comparison stats from central repository
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,9 @@ export default class BenchmarkResult extends BaseModel {
|
||||||
@column()
|
@column()
|
||||||
declare repository_id: string | null
|
declare repository_id: string | null
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare builder_tag: string | null
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true })
|
||||||
declare created_at: DateTime
|
declare created_at: DateTime
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export class BenchmarkService {
|
||||||
/**
|
/**
|
||||||
* Submit benchmark results to central repository
|
* Submit benchmark results to central repository
|
||||||
*/
|
*/
|
||||||
async submitToRepository(benchmarkId?: string): Promise<RepositorySubmitResponse> {
|
async submitToRepository(benchmarkId?: string, anonymous?: boolean): Promise<RepositorySubmitResponse> {
|
||||||
const result = benchmarkId
|
const result = benchmarkId
|
||||||
? await this.getResultById(benchmarkId)
|
? await this.getResultById(benchmarkId)
|
||||||
: await this.getLatestResult()
|
: await this.getLatestResult()
|
||||||
|
|
@ -145,6 +145,7 @@ export class BenchmarkService {
|
||||||
nomad_score: result.nomad_score,
|
nomad_score: result.nomad_score,
|
||||||
nomad_version: SystemService.getAppVersion(),
|
nomad_version: SystemService.getAppVersion(),
|
||||||
benchmark_version: '1.0.0',
|
benchmark_version: '1.0.0',
|
||||||
|
builder_tag: anonymous ? null : result.builder_tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'benchmark_results'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.string('builder_tag', 64).nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('builder_tag')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
131
admin/inertia/components/BuilderTagSelector.tsx
Normal file
131
admin/inertia/components/BuilderTagSelector.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { ArrowPathIcon } from '@heroicons/react/24/outline'
|
||||||
|
import {
|
||||||
|
ADJECTIVES,
|
||||||
|
NOUNS,
|
||||||
|
generateRandomNumber,
|
||||||
|
generateRandomBuilderTag,
|
||||||
|
parseBuilderTag,
|
||||||
|
buildBuilderTag,
|
||||||
|
} from '~/lib/builderTagWords'
|
||||||
|
|
||||||
|
interface BuilderTagSelectorProps {
|
||||||
|
value: string | null
|
||||||
|
onChange: (tag: string) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BuilderTagSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
}: BuilderTagSelectorProps) {
|
||||||
|
const [adjective, setAdjective] = useState<string>(ADJECTIVES[0])
|
||||||
|
const [noun, setNoun] = useState<string>(NOUNS[0])
|
||||||
|
const [number, setNumber] = useState<string>(generateRandomNumber())
|
||||||
|
|
||||||
|
// Parse existing value on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
const parsed = parseBuilderTag(value)
|
||||||
|
if (parsed) {
|
||||||
|
setAdjective(parsed.adjective)
|
||||||
|
setNoun(parsed.noun)
|
||||||
|
setNumber(parsed.number)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generate a random tag for new users
|
||||||
|
const randomTag = generateRandomBuilderTag()
|
||||||
|
const parsed = parseBuilderTag(randomTag)
|
||||||
|
if (parsed) {
|
||||||
|
setAdjective(parsed.adjective)
|
||||||
|
setNoun(parsed.noun)
|
||||||
|
setNumber(parsed.number)
|
||||||
|
onChange(randomTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Update parent when selections change
|
||||||
|
const updateTag = (newAdjective: string, newNoun: string, newNumber: string) => {
|
||||||
|
const tag = buildBuilderTag(newAdjective, newNoun, newNumber)
|
||||||
|
onChange(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdjectiveChange = (newAdjective: string) => {
|
||||||
|
setAdjective(newAdjective)
|
||||||
|
updateTag(newAdjective, noun, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNounChange = (newNoun: string) => {
|
||||||
|
setNoun(newNoun)
|
||||||
|
updateTag(adjective, newNoun, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRandomize = () => {
|
||||||
|
const newAdjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
|
||||||
|
const newNoun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
|
||||||
|
const newNumber = generateRandomNumber()
|
||||||
|
setAdjective(newAdjective)
|
||||||
|
setNoun(newNoun)
|
||||||
|
setNumber(newNumber)
|
||||||
|
updateTag(newAdjective, newNoun, newNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTag = buildBuilderTag(adjective, noun, number)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<select
|
||||||
|
value={adjective}
|
||||||
|
onChange={(e) => handleAdjectiveChange(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{ADJECTIVES.map((adj) => (
|
||||||
|
<option key={adj} value={adj}>
|
||||||
|
{adj}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<span className="text-desert-stone-dark font-bold">-</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={noun}
|
||||||
|
onChange={(e) => handleNounChange(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-medium focus:outline-none focus:ring-2 focus:ring-desert-green disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{NOUNS.map((n) => (
|
||||||
|
<option key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<span className="text-desert-stone-dark font-bold">-</span>
|
||||||
|
|
||||||
|
<span className="px-3 py-2 bg-desert-stone-lighter border border-desert-stone-light rounded-lg text-desert-green font-mono font-bold">
|
||||||
|
{number}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRandomize}
|
||||||
|
disabled={disabled}
|
||||||
|
className="p-2 text-desert-stone-dark hover:text-desert-green hover:bg-desert-stone-lighter rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
title="Randomize"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-desert-stone-dark">Your Builder Tag:</span>
|
||||||
|
<span className="font-mono font-bold text-desert-green">{currentTag}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
145
admin/inertia/lib/builderTagWords.ts
Normal file
145
admin/inertia/lib/builderTagWords.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Builder Tag word lists for generating unique, NOMAD-themed identifiers
|
||||||
|
// Format: [Adjective]-[Noun]-[4-digit number]
|
||||||
|
|
||||||
|
export const ADJECTIVES = [
|
||||||
|
'Tactical',
|
||||||
|
'Stealth',
|
||||||
|
'Rogue',
|
||||||
|
'Shadow',
|
||||||
|
'Ghost',
|
||||||
|
'Silent',
|
||||||
|
'Covert',
|
||||||
|
'Lone',
|
||||||
|
'Nomad',
|
||||||
|
'Digital',
|
||||||
|
'Cyber',
|
||||||
|
'Off-Grid',
|
||||||
|
'Remote',
|
||||||
|
'Arctic',
|
||||||
|
'Desert',
|
||||||
|
'Mountain',
|
||||||
|
'Urban',
|
||||||
|
'Bunker',
|
||||||
|
'Hidden',
|
||||||
|
'Secure',
|
||||||
|
'Armored',
|
||||||
|
'Fortified',
|
||||||
|
'Mobile',
|
||||||
|
'Solar',
|
||||||
|
'Nuclear',
|
||||||
|
'Storm',
|
||||||
|
'Thunder',
|
||||||
|
'Iron',
|
||||||
|
'Steel',
|
||||||
|
'Titanium',
|
||||||
|
'Carbon',
|
||||||
|
'Quantum',
|
||||||
|
'Neural',
|
||||||
|
'Alpha',
|
||||||
|
'Omega',
|
||||||
|
'Delta',
|
||||||
|
'Sigma',
|
||||||
|
'Apex',
|
||||||
|
'Prime',
|
||||||
|
'Elite',
|
||||||
|
'Midnight',
|
||||||
|
'Dawn',
|
||||||
|
'Dusk',
|
||||||
|
'Feral',
|
||||||
|
'Relic',
|
||||||
|
'Analog',
|
||||||
|
'Hardened',
|
||||||
|
'Vigilant',
|
||||||
|
'Outland',
|
||||||
|
'Frontier',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const NOUNS = [
|
||||||
|
'Llama',
|
||||||
|
'Wolf',
|
||||||
|
'Bear',
|
||||||
|
'Eagle',
|
||||||
|
'Falcon',
|
||||||
|
'Hawk',
|
||||||
|
'Raven',
|
||||||
|
'Fox',
|
||||||
|
'Coyote',
|
||||||
|
'Panther',
|
||||||
|
'Cobra',
|
||||||
|
'Viper',
|
||||||
|
'Phoenix',
|
||||||
|
'Dragon',
|
||||||
|
'Sentinel',
|
||||||
|
'Guardian',
|
||||||
|
'Ranger',
|
||||||
|
'Scout',
|
||||||
|
'Survivor',
|
||||||
|
'Prepper',
|
||||||
|
'Nomad',
|
||||||
|
'Wanderer',
|
||||||
|
'Drifter',
|
||||||
|
'Outpost',
|
||||||
|
'Shelter',
|
||||||
|
'Bunker',
|
||||||
|
'Vault',
|
||||||
|
'Cache',
|
||||||
|
'Haven',
|
||||||
|
'Fortress',
|
||||||
|
'Citadel',
|
||||||
|
'Node',
|
||||||
|
'Hub',
|
||||||
|
'Grid',
|
||||||
|
'Network',
|
||||||
|
'Signal',
|
||||||
|
'Beacon',
|
||||||
|
'Tower',
|
||||||
|
'Server',
|
||||||
|
'Cluster',
|
||||||
|
'Array',
|
||||||
|
'Matrix',
|
||||||
|
'Core',
|
||||||
|
'Nexus',
|
||||||
|
'Archive',
|
||||||
|
'Relay',
|
||||||
|
'Silo',
|
||||||
|
'Depot',
|
||||||
|
'Bastion',
|
||||||
|
'Homestead',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type Adjective = (typeof ADJECTIVES)[number]
|
||||||
|
export type Noun = (typeof NOUNS)[number]
|
||||||
|
|
||||||
|
export function generateRandomNumber(): string {
|
||||||
|
return String(Math.floor(Math.random() * 10000)).padStart(4, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomBuilderTag(): string {
|
||||||
|
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
|
||||||
|
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
|
||||||
|
const number = generateRandomNumber()
|
||||||
|
return `${adjective}-${noun}-${number}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBuilderTag(tag: string): {
|
||||||
|
adjective: Adjective
|
||||||
|
noun: Noun
|
||||||
|
number: string
|
||||||
|
} | null {
|
||||||
|
const match = tag.match(/^(.+)-(.+)-(\d{4})$/)
|
||||||
|
if (!match) return null
|
||||||
|
|
||||||
|
const [, adjective, noun, number] = match
|
||||||
|
if (!ADJECTIVES.includes(adjective as Adjective)) return null
|
||||||
|
if (!NOUNS.includes(noun as Noun)) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
adjective: adjective as Adjective,
|
||||||
|
noun: noun as Noun,
|
||||||
|
number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBuilderTag(adjective: string, noun: string, number: string): string {
|
||||||
|
return `${adjective}-${noun}-${number}`
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
import { Head, Link } from '@inertiajs/react'
|
import { Head, Link } from '@inertiajs/react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import CircularGauge from '~/components/systeminfo/CircularGauge'
|
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 InfoTooltip from '~/components/InfoTooltip'
|
||||||
|
import BuilderTagSelector from '~/components/BuilderTagSelector'
|
||||||
import {
|
import {
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
CpuChipIcon,
|
CpuChipIcon,
|
||||||
CircleStackIcon,
|
CircleStackIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
|
ClockIcon,
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { IconRobot } from '@tabler/icons-react'
|
import { IconRobot } from '@tabler/icons-react'
|
||||||
import { useTransmit } from 'react-adonis-transmit'
|
import { useTransmit } from 'react-adonis-transmit'
|
||||||
|
|
@ -29,10 +31,16 @@ export default function BenchmarkPage(props: {
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const { subscribe } = useTransmit()
|
const { subscribe } = useTransmit()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [progress, setProgress] = useState<BenchmarkProgressWithID | null>(null)
|
const [progress, setProgress] = useState<BenchmarkProgressWithID | null>(null)
|
||||||
const [isRunning, setIsRunning] = useState(props.benchmark.status !== 'idle')
|
const [isRunning, setIsRunning] = useState(props.benchmark.status !== 'idle')
|
||||||
const [showDetails, setShowDetails] = useState(false)
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showAIRequiredAlert, setShowAIRequiredAlert] = useState(false)
|
const [showAIRequiredAlert, setShowAIRequiredAlert] = useState(false)
|
||||||
|
const [shareAnonymously, setShareAnonymously] = useState(false)
|
||||||
|
const [currentBuilderTag, setCurrentBuilderTag] = useState<string | null>(
|
||||||
|
props.benchmark.latestResult?.builder_tag || null
|
||||||
|
)
|
||||||
|
|
||||||
// Check if AI Assistant is installed
|
// Check if AI Assistant is installed
|
||||||
const { data: aiInstalled } = useQuery({
|
const { data: aiInstalled } = useQuery({
|
||||||
|
|
@ -61,6 +69,16 @@ export default function BenchmarkPage(props: {
|
||||||
initialData: props.benchmark.latestResult,
|
initialData: props.benchmark.latestResult,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fetch all benchmark results for history
|
||||||
|
const { data: benchmarkHistory } = useQuery({
|
||||||
|
queryKey: ['benchmark', 'history'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fetch('/api/benchmark/results')
|
||||||
|
const data = await res.json()
|
||||||
|
return data.results as BenchmarkResult[]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Run benchmark mutation (uses sync mode by default for simpler local dev)
|
// Run benchmark mutation (uses sync mode by default for simpler local dev)
|
||||||
const runBenchmark = useMutation({
|
const runBenchmark = useMutation({
|
||||||
mutationFn: async (type: 'full' | 'system' | 'ai') => {
|
mutationFn: async (type: 'full' | 'system' | 'ai') => {
|
||||||
|
|
@ -118,15 +136,45 @@ export default function BenchmarkPage(props: {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update builder tag mutation
|
||||||
|
const updateBuilderTag = useMutation({
|
||||||
|
mutationFn: async ({ benchmarkId, builderTag }: { benchmarkId: string; builderTag: string }) => {
|
||||||
|
const res = await fetch('/api/benchmark/builder-tag', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ benchmark_id: benchmarkId, builder_tag: builderTag }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to update builder tag')
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchLatest()
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['benchmark', 'history'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Submit to repository mutation
|
// Submit to repository mutation
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||||
const submitResult = useMutation({
|
const submitResult = useMutation({
|
||||||
mutationFn: async (benchmarkId?: string) => {
|
mutationFn: async ({ benchmarkId, anonymous }: { benchmarkId: string; anonymous: boolean }) => {
|
||||||
setSubmitError(null)
|
setSubmitError(null)
|
||||||
|
|
||||||
|
// First, save the current builder tag to the benchmark
|
||||||
|
if (currentBuilderTag && !anonymous) {
|
||||||
|
await fetch('/api/benchmark/builder-tag', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ benchmark_id: benchmarkId, builder_tag: currentBuilderTag }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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, anonymous }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
|
|
@ -136,6 +184,7 @@ export default function BenchmarkPage(props: {
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
refetchLatest()
|
refetchLatest()
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['benchmark', 'history'] })
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
setSubmitError(error.message)
|
setSubmitError(error.message)
|
||||||
|
|
@ -395,12 +444,43 @@ export default function BenchmarkPage(props: {
|
||||||
|
|
||||||
{/* Share with Community - Only for full benchmarks with AI data */}
|
{/* Share with Community - Only for full benchmarks with AI data */}
|
||||||
{canShareBenchmark && (
|
{canShareBenchmark && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-4 mt-6 pt-6 border-t border-desert-stone-light">
|
||||||
|
<h3 className="font-semibold text-desert-green">Share with Community</h3>
|
||||||
<p className="text-sm text-desert-stone-dark">
|
<p className="text-sm text-desert-stone-dark">
|
||||||
Share your benchmark score anonymously with the NOMAD community. Only your hardware specs and scores are sent — no identifying information.
|
Share your benchmark on the community leaderboard. Choose a Builder Tag to claim your spot, or share anonymously.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Builder Tag Selector */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-desert-stone-dark">
|
||||||
|
Your Builder Tag
|
||||||
|
</label>
|
||||||
|
<BuilderTagSelector
|
||||||
|
value={currentBuilderTag}
|
||||||
|
onChange={setCurrentBuilderTag}
|
||||||
|
disabled={shareAnonymously || submitResult.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Anonymous checkbox */}
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={shareAnonymously}
|
||||||
|
onChange={(e) => setShareAnonymously(e.target.checked)}
|
||||||
|
disabled={submitResult.isPending}
|
||||||
|
className="w-4 h-4 rounded border-desert-stone-light text-desert-green focus:ring-desert-green"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-desert-stone-dark">
|
||||||
|
Share anonymously (no Builder Tag shown on leaderboard)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<StyledButton
|
<StyledButton
|
||||||
onClick={() => submitResult.mutate(latestResult.benchmark_id)}
|
onClick={() => submitResult.mutate({
|
||||||
|
benchmarkId: latestResult.benchmark_id,
|
||||||
|
anonymous: shareAnonymously
|
||||||
|
})}
|
||||||
disabled={submitResult.isPending}
|
disabled={submitResult.isPending}
|
||||||
icon='CloudArrowUpIcon'
|
icon='CloudArrowUpIcon'
|
||||||
>
|
>
|
||||||
|
|
@ -677,6 +757,10 @@ export default function BenchmarkPage(props: {
|
||||||
<span className="text-desert-stone-dark">Run Date</span>
|
<span className="text-desert-stone-dark">Run Date</span>
|
||||||
<span>{new Date(latestResult.created_at as unknown as string).toLocaleString()}</span>
|
<span>{new Date(latestResult.created_at as unknown as string).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-desert-stone-dark">Builder Tag</span>
|
||||||
|
<span className="font-mono">{latestResult.builder_tag || 'Not set'}</span>
|
||||||
|
</div>
|
||||||
{latestResult.ai_model_used && (
|
{latestResult.ai_model_used && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-desert-stone-dark">AI Model Used</span>
|
<span className="text-desert-stone-dark">AI Model Used</span>
|
||||||
|
|
@ -700,6 +784,83 @@ export default function BenchmarkPage(props: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Benchmark History */}
|
||||||
|
{benchmarkHistory && benchmarkHistory.length > 1 && (
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||||
|
<div className="w-1 h-6 bg-desert-green" />
|
||||||
|
Benchmark History
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-desert-white rounded-lg border border-desert-stone-light shadow-sm overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHistory(!showHistory)}
|
||||||
|
className="w-full p-4 flex items-center justify-between hover:bg-desert-stone-lighter/30 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ClockIcon className="w-5 h-5 text-desert-stone-dark" />
|
||||||
|
<span className="font-medium text-desert-green">
|
||||||
|
{benchmarkHistory.length} benchmark{benchmarkHistory.length !== 1 ? 's' : ''} recorded
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`w-5 h-5 text-desert-stone-dark transition-transform ${showHistory ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showHistory && (
|
||||||
|
<div className="border-t border-desert-stone-light">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-desert-stone-lighter/50">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left p-3 font-medium text-desert-stone-dark">Date</th>
|
||||||
|
<th className="text-left p-3 font-medium text-desert-stone-dark">Type</th>
|
||||||
|
<th className="text-left p-3 font-medium text-desert-stone-dark">Score</th>
|
||||||
|
<th className="text-left p-3 font-medium text-desert-stone-dark">Builder Tag</th>
|
||||||
|
<th className="text-left p-3 font-medium text-desert-stone-dark">Shared</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-desert-stone-lighter">
|
||||||
|
{benchmarkHistory.map((result) => (
|
||||||
|
<tr
|
||||||
|
key={result.benchmark_id}
|
||||||
|
className={`hover:bg-desert-stone-lighter/30 ${
|
||||||
|
result.benchmark_id === latestResult?.benchmark_id
|
||||||
|
? 'bg-desert-green/5'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="p-3">
|
||||||
|
{new Date(result.created_at as unknown as string).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 capitalize">{result.benchmark_type}</td>
|
||||||
|
<td className="p-3">
|
||||||
|
<span className="font-bold text-desert-green">
|
||||||
|
{result.nomad_score.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 font-mono text-xs">
|
||||||
|
{result.builder_tag || '—'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
{result.submitted_to_repository ? (
|
||||||
|
<span className="text-green-600">✓</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-desert-stone-dark">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ router
|
||||||
router.get('/results/latest', [BenchmarkController, 'latest'])
|
router.get('/results/latest', [BenchmarkController, 'latest'])
|
||||||
router.get('/results/:id', [BenchmarkController, 'show'])
|
router.get('/results/:id', [BenchmarkController, 'show'])
|
||||||
router.post('/submit', [BenchmarkController, 'submit'])
|
router.post('/submit', [BenchmarkController, 'submit'])
|
||||||
|
router.post('/builder-tag', [BenchmarkController, 'updateBuilderTag'])
|
||||||
router.get('/comparison', [BenchmarkController, 'comparison'])
|
router.get('/comparison', [BenchmarkController, 'comparison'])
|
||||||
router.get('/status', [BenchmarkController, 'status'])
|
router.get('/status', [BenchmarkController, 'status'])
|
||||||
router.get('/settings', [BenchmarkController, 'settings'])
|
router.get('/settings', [BenchmarkController, 'settings'])
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export type BenchmarkResultSlim = Pick<
|
||||||
| 'nomad_score'
|
| 'nomad_score'
|
||||||
| 'submitted_to_repository'
|
| 'submitted_to_repository'
|
||||||
| 'created_at'
|
| 'created_at'
|
||||||
|
| 'builder_tag'
|
||||||
> & {
|
> & {
|
||||||
cpu_model: string
|
cpu_model: string
|
||||||
gpu_model: string | null
|
gpu_model: string | null
|
||||||
|
|
@ -113,6 +114,7 @@ export type RepositorySubmission = Pick<
|
||||||
nomad_version: string
|
nomad_version: string
|
||||||
benchmark_version: string
|
benchmark_version: string
|
||||||
ram_gb: number
|
ram_gb: number
|
||||||
|
builder_tag: string | null // null = anonymous submission
|
||||||
}
|
}
|
||||||
|
|
||||||
// Central repository response types
|
// Central repository response types
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user