feat(benchmark): dashboard re-run banner prompting a Score v2 re-run

Shows a dismissible dashboard banner to users who submitted a benchmark under
v1 scoring but don't yet have a Score v2 result, nudging them to re-run so the
community leaderboard gets their v2 number.

- Show logic (benchmark_service.shouldShowRerunBanner): has a result with
  submitted_to_repository=true AND no result with nomad_score_v2 AND the
  dismiss KV isn't set. Self-clears two ways — dismiss sets the KV, and any v2
  run gives a result a nomad_score_v2 so the condition flips off on its own.
- New GET /api/benchmark/rerun-banner + useBenchmarkRerunBanner hook (mirrors
  useUpdateAvailable), rendered as a dismissible Alert on the dashboard with a
  "Re-run benchmark" CTA to /settings/benchmark.
- New KV key benchmark.rerunBannerDismissed (boolean) in both KV_STORE_SCHEMA
  and the SETTINGS_KEYS whitelist (the PATCH /system/settings validator enum);
  dismiss writes it via the existing updateSetting endpoint + invalidates the
  query.

Sibling to the Score v2 app client (#1094); stacked on it for the
nomad_score_v2 column. Browser-verified on the NOMAD3 dev env: show/dismiss/
reload-persist/self-clear-on-v2 all correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-07-13 11:20:10 -07:00
parent 651df80dc8
commit dbc49e92d4
8 changed files with 85 additions and 1 deletions

View File

@ -248,6 +248,13 @@ export default class BenchmarkController {
return this.benchmarkService.getStatus()
}
/**
* Whether to show the dashboard "re-run under Score v2" banner
*/
async rerunBanner({}: HttpContext) {
return { show: await this.benchmarkService.shouldShowRerunBanner() }
}
/**
* Get benchmark settings
*/

View File

@ -192,6 +192,25 @@ export class BenchmarkService {
return await BenchmarkResult.findBy('benchmark_id', benchmarkId)
}
/**
* Whether to show the dashboard "re-run under Score v2" banner. Shown to users
* who have a v1 leaderboard submission but no Score v2 result yet, and who
* haven't dismissed it. Self-clears two ways: dismiss sets the KV flag, and any
* result carrying a nomad_score_v2 (i.e. a v2 run happened) flips (b) false.
*/
async shouldShowRerunBanner(): Promise<boolean> {
const dismissed = await KVStore.getValue('benchmark.rerunBannerDismissed')
if (dismissed === true) return false
const hasV2Run = await BenchmarkResult.query().whereNotNull('nomad_score_v2').first()
if (hasV2Run) return false
const hasSubmittedV1 = await BenchmarkResult.query()
.where('submitted_to_repository', true)
.first()
return hasSubmittedV1 !== null
}
/**
* Submit benchmark results to central repository
*/

View File

@ -1,3 +1,3 @@
import { KVStoreKey } from "../types/kv_store.js";
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'system.internetStatusTestUrl', 'ai.assistantCustomName', 'ai.remoteOllamaUrl', 'ai.ollamaFlashAttention', 'rag.defaultIngestPolicy', 'autoUpdate.enabled', 'autoUpdate.windowStart', 'autoUpdate.windowEnd', 'autoUpdate.cooloffHours', 'appAutoUpdate.enabled', 'contentAutoUpdate.enabled', 'contentAutoUpdate.windowStart', 'contentAutoUpdate.windowEnd', 'contentAutoUpdate.cooloffHours', 'contentAutoUpdate.maxBytesPerWindow'];
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'system.internetStatusTestUrl', 'ai.assistantCustomName', 'ai.remoteOllamaUrl', 'ai.ollamaFlashAttention', 'rag.defaultIngestPolicy', 'autoUpdate.enabled', 'autoUpdate.windowStart', 'autoUpdate.windowEnd', 'autoUpdate.cooloffHours', 'appAutoUpdate.enabled', 'contentAutoUpdate.enabled', 'contentAutoUpdate.windowStart', 'contentAutoUpdate.windowEnd', 'contentAutoUpdate.cooloffHours', 'contentAutoUpdate.maxBytesPerWindow', 'benchmark.rerunBannerDismissed'];

View File

@ -0,0 +1,15 @@
import api from "~/lib/api"
import { useQuery } from "@tanstack/react-query"
export const BENCHMARK_RERUN_BANNER_QUERY_KEY = ['benchmark-rerun-banner']
export const useBenchmarkRerunBanner = () => {
const queryData = useQuery<{ show: boolean } | undefined>({
queryKey: BENCHMARK_RERUN_BANNER_QUERY_KEY,
queryFn: () => api.checkBenchmarkRerunBanner(),
refetchInterval: Infinity, // Disable automatic refetching
refetchOnWindowFocus: false,
})
return queryData.data
}

View File

@ -374,6 +374,13 @@ class API {
})()
}
async checkBenchmarkRerunBanner() {
return catchInternal(async () => {
const response = await this.client.get<{ show: boolean }>('/benchmark/rerun-banner')
return response.data
})()
}
async getChatSessions() {
return catchInternal(async () => {
const response = await this.client.get<

View File

@ -13,6 +13,12 @@ import { ServiceSlim } from '../../types/services'
import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
import { useUpdateAvailable } from '~/hooks/useUpdateAvailable'
import { useSystemSetting } from '~/hooks/useSystemSetting'
import {
useBenchmarkRerunBanner,
BENCHMARK_RERUN_BANNER_QUERY_KEY,
} from '~/hooks/useBenchmarkRerunBanner'
import { useQueryClient } from '@tanstack/react-query'
import api from '~/lib/api'
import Alert from '~/components/Alert'
import { SERVICE_NAMES } from '../../constants/service_names'
@ -91,8 +97,15 @@ export default function Home(props: {
}) {
const items: DashboardItem[] = []
const updateInfo = useUpdateAvailable();
const rerunBanner = useBenchmarkRerunBanner()
const queryClient = useQueryClient()
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const handleDismissRerunBanner = async () => {
await api.updateSetting('benchmark.rerunBannerDismissed', true)
queryClient.invalidateQueries({ queryKey: BENCHMARK_RERUN_BANNER_QUERY_KEY })
}
// Check if user has visited Easy Setup
const { data: easySetupVisited } = useSystemSetting({
key: 'ui.hasVisitedEasySetup'
@ -155,6 +168,27 @@ export default function Home(props: {
</div>
)
}
{
rerunBanner?.show && (
<div className='flex justify-center items-center px-4 pt-4 w-full'>
<Alert
title="Your benchmark can be re-scored with Score v2"
message="We've upgraded the benchmark scoring system. Re-run your benchmark to get an updated Score v2 result on the community leaderboard."
type="info-inverted"
variant="solid"
className="w-full"
dismissible
onDismiss={handleDismissRerunBanner}
buttonProps={{
variant: 'primary',
children: 'Re-run benchmark',
icon: 'IconRefresh',
onClick: () => router.visit('/settings/benchmark'),
}}
/>
</div>
)
}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{items.map((item) => {
const isEasySetup = item.label === 'Easy Setup'

View File

@ -237,6 +237,7 @@ router
router.post('/builder-tag', [BenchmarkController, 'updateBuilderTag'])
router.get('/comparison', [BenchmarkController, 'comparison'])
router.get('/status', [BenchmarkController, 'status'])
router.get('/rerun-banner', [BenchmarkController, 'rerunBanner'])
router.get('/settings', [BenchmarkController, 'settings'])
router.post('/settings', [BenchmarkController, 'updateSettings'])
})

View File

@ -42,6 +42,7 @@ export const KV_STORE_SCHEMA = {
'ai.amdHsaOverride': 'string',
'ai.autoFixGpuPassthrough': 'boolean',
'gpu.autoRemediatedAt': 'string',
'benchmark.rerunBannerDismissed': 'boolean',
} as const
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string