Spaces:
Running
Running
Download app.js from Valen-Team/JevalKit-Leaderboard: direct link, hf CLI and curl.
- Browser
- Download file 46.7 kB
-
https://huggingface.co/spaces/Valen-Team/JevalKit-Leaderboard/resolve/main/app.js
- Command line
-
hf download hf://spaces/Valen-Team/JevalKit-Leaderboard/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Valen-Team/JevalKit-Leaderboard/resolve/main/app.js
46.7 kB
| import {localize, translate} from './i18n.js'; | |
| import { GROUPS, METRICS, SHORT_NAMES, isDecisionIndex, statusOf, valueOf, formatValue, resultMap, sortedModels, ranksFor, filterModels, escapeHtml as esc, csvFor } from './ranking.js'; | |
| import { chartModels, chartAxis, plotValue, axisLabel, parameterSize } from './chart.js'; | |
| function readPreference(key, fallback) {try {return localStorage.getItem(key) || fallback;} catch {return fallback;}} | |
| let language = readPreference('jevalkit-language', 'en') === 'zh' ? 'zh' : 'en'; | |
| const requestedLanguage = new URLSearchParams(location.search).get('lang'); | |
| if (['en','zh'].includes(requestedLanguage)) language = requestedLanguage; | |
| let theme = readPreference('jevalkit-theme', 'light') === 'dark' ? 'dark' : 'light'; | |
| function applyPreferences() { | |
| document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'; | |
| document.documentElement.dataset.theme = theme; | |
| for (const link of document.querySelectorAll('a[href]')) { | |
| const url = new URL(link.href); | |
| if (url.hostname === 'valen-team.github.io' && /^\/(?:zh-Hans\/)?docs(?:\/|$)/.test(url.pathname)) { | |
| url.pathname = (language === 'zh' ? '/zh-Hans' : '') + url.pathname.replace(/^\/zh-Hans/, ''); | |
| link.href = url.href; | |
| } | |
| } | |
| document.title = language === 'zh' ? 'JevalKit · 决策模型排行榜' : 'JevalKit — The decision model leaderboard'; | |
| document.querySelectorAll('[data-language]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.language === language))); | |
| const toggle = document.querySelector('[data-theme-toggle]'); | |
| if (toggle) { | |
| const label = language === 'zh' ? (theme === 'dark' ? '切换浅色模式' : '切换深色模式') : (theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); | |
| toggle.setAttribute('aria-label', label); toggle.title = label; | |
| toggle.innerHTML = theme === 'dark' ? icon('sun', 16) : icon('moon', 16); | |
| } | |
| } | |
| function translatePage() {localize(document.body, language); applyPreferences();} | |
| function preferenceControls() {return `<div class="preference-switch" aria-label="Language and theme"><button type="button" data-language="en" lang="en" aria-label="Switch to English" data-no-translate>EN</button><button type="button" data-language="zh" lang="zh-CN" aria-label="切换到中文" data-no-translate>中</button><button type="button" data-theme-toggle></button></div>`;} | |
| const icons = { | |
| moon: '<path d="M20.9 13.1A9 9 0 0 1 10.9 3.1a9 9 0 1 0 10 10Z"/>', | |
| sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>', | |
| arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>', | |
| diagonal: '<path d="M7 17 17 7M7 7h10v10"/>', | |
| download: '<path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/>', | |
| search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>', | |
| compare: '<path d="M8 4v16m8-16v16M4 9h8m0 6h8"/>', | |
| close: '<path d="m6 6 12 12M6 18 18 6"/>', | |
| check: '<path d="m5 12 4 4L19 6"/>', | |
| shield: '<path d="m12 3 8 3v6c0 4-8 9-8 9S4 16 4 12V6z"/><path d="m8 12 3 3 5-6"/>', | |
| layers: '<path d="m12 3 10 5-10 5L2 8zm-9 10 9 5 9-5M3 18l9 5 9-5"/>', | |
| clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', | |
| info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7v1"/>', | |
| code: '<path d="m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18"/>', | |
| github: '<path d="M9 19c-5 2-5-3-7-3m14 6v-4a3.5 3.5 0 0 0-1-3c3 0 6-1.5 6-6a4.6 4.6 0 0 0-1-3 4.2 4.2 0 0 0 0-3s-1 0-3 1a13.4 13.4 0 0 0-7 0C8 3 7 3 7 3a4.2 4.2 0 0 0 0 3 4.6 4.6 0 0 0-1 3c0 4.5 3 6 6 6a3.5 3.5 0 0 0-1 3v4"/>', | |
| }; | |
| const icon = (name, size = 16) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name] || icons.arrow}</svg>`; | |
| const number = value => Number(value || 0).toLocaleString('en-US'); | |
| const safeUrl = value => /^https?:\/\//.test(value || '') ? esc(value) : '#'; | |
| const external = (url, label, className = 'inline-link') => `<a class="${className}" href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer">${label}</a>`; | |
| const prettyDate = value => value ? new Date(value).toLocaleDateString(language === 'zh' ? 'zh-CN' : 'en-GB', { day: '2-digit', month: 'short', year: 'numeric', timeZone: 'UTC' }) : 'Imported snapshot'; | |
| const groupNames = { choice: 'Choice', noul: 'Noul', score: 'Score' }; | |
| let data, map, benchmarks; | |
| let state = { group: 'text', metric: 'accuracy', rankBy: 'JevAltBench', reversed: false, family: 'all', query: '', fullCoverage: false, selected: [] }; | |
| let toastTimer; | |
| const chartState = { benchmark: 'DecisionIndex', metric: 'public_decision_index', min: 0, max: 30, includeUnknown: true, expanded: false }; | |
| let sizeLimit = 30; | |
| const sizePresets = [ | |
| { label: 'All sizes', min: 0, max: () => sizeLimit, unknown: true }, | |
| { label: '≤ 1B', min: 0, max: () => 1 }, | |
| { label: '1–4B', min: 1, max: () => 4 }, | |
| { label: '4–10B', min: 4, max: () => 10 }, | |
| { label: '≥ 10B', min: 10, max: () => sizeLimit }, | |
| ]; | |
| function activeNames() { return GROUPS[state.group].names; } | |
| function tableNames() { | |
| const names = activeNames(); | |
| return matchMedia('(max-width: 680px)').matches ? [state.rankBy, ...names.filter(name => name !== state.rankBy)] : names; | |
| } | |
| function visibleModels() { return sortedModels(filterModels(data.models, map, state), map, state.rankBy, state.metric, state.reversed); } | |
| function getRow(model, name) { return map.get(`${model}|${name}`); } | |
| function benchmark(name) { return benchmarks.get(name); } | |
| function currentRanks(name = state.rankBy) { return ranksFor(data.models, map, name, state.metric); } | |
| function measuredCount(name) { return data.results.filter(r => r.primary && r.eligible && r.benchmark === name).length; } | |
| function toast(message) { | |
| const element = document.querySelector('#toast'); | |
| element.textContent = translate(message, language); | |
| element.classList.add('visible'); | |
| clearTimeout(toastTimer); | |
| toastTimer = setTimeout(() => element.classList.remove('visible'), 2400); | |
| } | |
| function download(name, content, type = 'application/json') { | |
| const url = URL.createObjectURL(new Blob([content], { type })); | |
| const anchor = document.createElement('a'); | |
| anchor.href = url; anchor.download = name; document.body.append(anchor); anchor.click(); anchor.remove(); | |
| setTimeout(() => URL.revokeObjectURL(url), 1000); | |
| } | |
| function badge(row) { | |
| const status = statusOf(row); | |
| const label = { measured: 'Full measurement', missing: 'Not measured', unsupported: 'Unsupported', excluded: 'Excluded from ranking' }[status]; | |
| return `<span class="status-pill ${status}">${label}</span>`; | |
| } | |
| function scoreCell(model, name, rankMap) { | |
| const row = getRow(model.id, name), value = valueOf(row, state.metric); | |
| const active = name === state.rankBy ? ' active' : ''; | |
| if (value === null) { | |
| const status = statusOf(row); | |
| const label = status === 'unsupported' ? 'Unsupported' : status === 'excluded' ? 'Excluded' : '—'; | |
| const title = status === 'unsupported' ? `${model.id} does not support these inputs; no score is ranked.` : status === 'excluded' ? 'This measurement is ineligible. Inspect the run archive for details.' : `${model.id}: no ${METRICS[state.metric].label} measurement for ${name}.`; | |
| return `<td class="metric-cell${active}"><span class="${status === 'unsupported' ? 'unsupported-value' : status === 'excluded' ? 'excluded-value' : 'missing-value'}" title="${esc(title)}">${label}</span></td>`; | |
| } | |
| const best = rankMap.get(model.id) === 1; | |
| const partial = row.coverage < 1 - 1e-9; | |
| const title = `${name}: ${formatValue(value, state.metric)} · ${formatValue(row.coverage, 'coverage')} valid coverage. Click for measurement details.`; | |
| return `<td class="metric-cell${active}"><button class="score-button${best ? ' best' : ''}" data-run="${row.id}" title="${esc(title)}" aria-label="${esc(`${model.id}, ${name}: ${formatValue(value, state.metric)}. View measurement`)}">${formatValue(value, state.metric)}${partial ? '<i class="coverage-dot" aria-hidden="true"></i>' : ''}</button></td>`; | |
| } | |
| function renderRows() { | |
| const names = tableNames(), models = visibleModels(), ranks = currentRanks(); | |
| const rankMaps = new Map(names.map(name => [name, currentRanks(name)])); | |
| const highFirst = METRICS[state.metric].higher !== state.reversed; | |
| const header = `<thead><tr><th class="check-col"><span class="sr-only">Select</span></th><th class="rank-col">#</th><th class="model-col"><span>MODEL / FAMILY</span></th>${names.map(name => `<th class="benchmark-col${name === state.rankBy ? ' active' : ''}" aria-sort="${name === state.rankBy ? highFirst ? 'descending' : 'ascending' : 'none'}"><button class="col-button" data-rank="${name}" title="Rank by ${esc(name)}"><strong>${esc(SHORT_NAMES[name])}${name === state.rankBy ? highFirst ? ' ↓' : ' ↑' : ''}</strong><small>${number(benchmark(name).questions)} QUESTIONS</small></button></th>`).join('')}<th class="coverage-col" title="Valid question coverage on the selected ranking benchmark">VALID<br>COVERAGE</th></tr></thead>`; | |
| const rows = models.map(model => { | |
| const row = getRow(model.id, state.rankBy), rank = ranks.get(model.id); | |
| const coverage = row ? formatValue(row.coverage, 'coverage', 1) : '—'; | |
| return `<tr><td class="check-cell"><input type="checkbox" data-select="${esc(model.id)}" aria-label="Select ${esc(model.id)} for comparison" ${state.selected.includes(model.id) ? 'checked' : ''}></td><td class="rank-cell${rank && rank <= 3 ? ' top-rank' : ''}">${rank ?? '—'}</td><td class="model-cell"><button class="model-name" data-model="${esc(model.id)}">${esc(model.id)}</button><span class="model-sub">${esc(model.family)}${!model.evaluated ? '<span class="pending-label">· Not measured</span>' : model.transport === 'server' ? '<b>SERVER</b>' : ''}</span></td>${names.map(name => scoreCell(model, name, rankMaps.get(name))).join('')}<td class="coverage-cell${!row ? ' missing' : row.coverage < 1 ? ' partial' : ''}">${coverage}</td></tr>`; | |
| }).join(''); | |
| document.querySelector('#table-area').innerHTML = `<div class="table-scroll"><table aria-label="Model leaderboard, ranked by ${esc(SHORT_NAMES[state.rankBy])} ${METRICS[state.metric].label}">${header}<tbody>${models.length ? rows : `<tr><td colspan="${names.length + 4}" class="empty"><h3>No models match these filters</h3><p>Try another name or turn off the full-coverage filter.</p><button class="button" data-action="reset">Reset filters ${icon('arrow', 13)}</button></td></tr>`}</tbody></table></div>`; | |
| document.querySelector('#result-count').innerHTML = `<strong>${models.length} models</strong> · ranks use <strong>${esc(SHORT_NAMES[state.rankBy])}</strong> only`; | |
| document.querySelector('#metric-direction').textContent = `${METRICS[state.metric].label} ${METRICS[state.metric].higher ? '↑ higher' : '↓ lower'} is better`; | |
| document.querySelector('#metric-select').title = METRICS[state.metric].detail; | |
| const params = new URLSearchParams(); | |
| if (language === 'zh') params.set('lang', 'zh'); | |
| if (state.group !== 'text') params.set('view', state.group); | |
| if (state.metric !== 'accuracy') params.set('metric', state.metric); | |
| if (state.rankBy !== 'JevAltBench') params.set('rank', state.rankBy); | |
| if (state.query) params.set('q', state.query); | |
| try { history.replaceState(null, '', `${location.pathname}${params.size ? '?' + params.toString() : ''}${location.hash}`); } catch {} | |
| renderComparisonBar(); | |
| translatePage(); | |
| } | |
| function renderControls() { | |
| document.querySelector('#tabs').innerHTML = Object.entries(GROUPS).map(([id, group]) => `<button class="tab-button${state.group === id ? ' active' : ''}" data-group="${id}" aria-pressed="${state.group === id}">${group.label}<span class="tab-count">${group.names.length}</span></button>`).join('') + `<div class="snapshot-label"><span></span> SNAPSHOT · ${prettyDate(data.snapshot.last_run_at).toUpperCase()}</div>`; | |
| document.querySelector('#rank-select').innerHTML = activeNames().map(name => `<option value="${name}" ${state.rankBy === name ? 'selected' : ''}>${esc(SHORT_NAMES[name])}</option>`).join(''); | |
| document.querySelector('#metric-select').innerHTML = Object.entries(METRICS).filter(([id]) => id !== 'public_decision_index' || ['index', 'all'].includes(state.group)).map(([id, metric]) => `<option value="${id}" ${state.metric === id ? 'selected' : ''}>${metric.label}${metric.higher ? ' ↑' : ' ↓'}</option>`).join(''); | |
| document.querySelector('#family-select').value = state.family; | |
| document.querySelector('#model-search').value = state.query; | |
| document.querySelector('#full-coverage').checked = state.fullCoverage; | |
| } | |
| function renderComparisonBar() { | |
| const bar = document.querySelector('#compare-bar'); | |
| bar.hidden = state.selected.length === 0; | |
| bar.innerHTML = `<div class="selected-names">${state.selected.map(name => `<span>${esc(name)}</span>`).join('')}</div><button class="button primary" data-action="compare" ${state.selected.length < 2 ? 'disabled' : ''}>Compare <span class="count-label">${state.selected.length}/3</span> ${icon('arrow', 13)}</button><button class="clear-compare" data-action="clear-compare" aria-label="Clear selected models">${icon('close', 15)}</button>`; | |
| const compare = document.querySelector('#compare-top'); | |
| compare.disabled = state.selected.length < 2; | |
| compare.innerHTML = `${icon('compare', 14)} Compare${state.selected.length ? ' (' + state.selected.length + ')' : ''}`; | |
| } | |
| function renderChartSection() { | |
| return `<section class="performance-section" id="performance" aria-labelledby="performance-title"><div class="board-heading"><div><div class="section-label">COMPARE BY MODEL SIZE</div><h2 id="performance-title">Model performance</h2></div><a class="text-button" href="#leaderboard">View the full table ${icon('arrow', 13)}</a></div> | |
| <div class="performance-card"><div class="performance-controls"><label class="select-field"><span>BENCHMARK</span><select id="chart-benchmark" aria-label="Chart benchmark">${GROUPS.all.names.map(name => `<option value="${name}" ${name === chartState.benchmark ? 'selected' : ''}>${esc(SHORT_NAMES[name])}</option>`).join('')}</select></label><label class="select-field"><span>METRIC</span><select id="chart-metric" aria-label="Chart metric"></select></label><span class="performance-direction" id="chart-direction"></span></div> | |
| <div class="size-filter"><div class="size-filter-heading"><span class="size-filter-label">PARAMETERS <output id="size-readout" for="size-min size-max"></output></span><div class="size-presets" aria-label="Parameter size presets">${sizePresets.map((preset, i) => `<button class="size-preset" data-size-preset="${i}" aria-pressed="false">${preset.label}</button>`).join('')}</div></div><div class="size-sliders"><label>Minimum <strong id="size-min-label"></strong><input id="size-min" type="range" min="0" max="${sizeLimit}" step="0.1" value="0" aria-label="Minimum model parameters in billions"></label><label>Maximum <strong id="size-max-label"></strong><input id="size-max" type="range" min="0" max="${sizeLimit}" step="0.1" value="${sizeLimit}" aria-label="Maximum model parameters in billions"></label><label class="size-unknown"><input id="size-unknown" type="checkbox" checked> Include undisclosed sizes</label></div></div> | |
| <div class="performance-results"><div class="performance-summary" id="chart-summary" role="status"></div><div class="performance-axis" aria-hidden="true"><span></span><div id="chart-axis"></div><span>Score</span><span>Coverage</span></div><div id="chart-bars" role="list" aria-label="Models ranked by the selected benchmark and metric"></div><div class="chart-more" id="chart-more"></div></div> | |
| <div class="performance-notes"><p id="chart-note"></p><p>Sizes use published parameter counts or backbone sizes, including the base model for adapters. Click a size for its source; click a bar for the measurement. <span class="chart-partial-key"><i></i> Partial valid coverage</span></p></div></div></section>`; | |
| } | |
| function renderChart() { | |
| const s = chartState, metric = METRICS[s.metric]; | |
| const models = chartModels(data.models, map, s); | |
| const shown = s.expanded ? models : models.slice(0, 10); | |
| const axis = chartAxis(models.map(model => plotValue(valueOf(getRow(model.id, s.benchmark), s.metric), s.metric)), s.metric); | |
| document.querySelector('#chart-metric').innerHTML = Object.entries(METRICS).filter(([id]) => id !== 'public_decision_index' || isDecisionIndex(s.benchmark)).map(([id, m]) => `<option value="${id}" ${id === s.metric ? 'selected' : ''}>${m.label}</option>`).join(''); | |
| document.querySelector('#chart-direction').textContent = `${metric.higher ? '↑ Higher' : '↓ Lower'} is better`; | |
| document.querySelector('#chart-note').textContent = s.metric === 'public_decision_index' ? 'Public Decision Index · 0–100. This uses the official public-only formula; private components are excluded.' : metric.detail; | |
| document.querySelector('#size-readout').textContent = `${s.min.toFixed(1)}–${s.max.toFixed(1)}B`; | |
| for (const bound of ['min', 'max']) { | |
| document.querySelector(`#size-${bound}`).value = s[bound]; | |
| document.querySelector(`#size-${bound}-label`).textContent = s[bound].toFixed(1) + 'B'; | |
| } | |
| document.querySelector('#size-unknown').checked = s.includeUnknown; | |
| document.querySelectorAll('[data-size-preset]').forEach(button => { | |
| const preset = sizePresets[Number(button.dataset.sizePreset)]; | |
| button.setAttribute('aria-pressed', String(s.min === preset.min && s.max === preset.max() && s.includeUnknown === !!preset.unknown)); | |
| }); | |
| const measured = data.models.filter(model => valueOf(getRow(model.id, s.benchmark), s.metric) !== null).length; | |
| document.querySelector('#chart-summary').textContent = `${shown.length} of ${models.length} matching models · ${measured} with eligible scores on ${SHORT_NAMES[s.benchmark]}`; | |
| document.querySelector('#chart-axis').innerHTML = Array.from({ length: 6 }, (_, i) => `<span>${esc(axisLabel(axis * i / 5, s.metric))}</span>`).join(''); | |
| document.querySelector('#chart-bars').innerHTML = shown.length ? shown.map((model, i) => { | |
| const row = getRow(model.id, s.benchmark), value = valueOf(row, s.metric); | |
| const width = Math.max(0, Math.min(100, plotValue(value, s.metric) / axis * 100)); | |
| const partial = row.coverage < 1 - 1e-9; | |
| const size = parameterSize(model) === null ? '<span class="bar-size">Undisclosed</span>' : external(model.parameter_source, `${esc(model.parameter_label)} ${icon('diagonal', 9)}`, 'bar-size'); | |
| return `<div class="performance-row${i === 0 ? ' first' : ''}" role="listitem"><div class="bar-model"><button data-model="${esc(model.id)}" title="${esc(model.id)}">${esc(model.id)}</button>${size}</div><button class="bar-measure" data-run="${row.id}" aria-label="${esc(`${model.id}, ${SHORT_NAMES[s.benchmark]}: ${formatValue(value, s.metric)}; ${formatValue(row.coverage, 'coverage')} valid coverage. View measurement.`)}"><span class="bar-track"><i style="width:${width}%"></i></span><strong class="bar-value">${formatValue(value, s.metric)}${partial ? '<i class="chart-value-dot" aria-hidden="true"></i>' : ''}</strong></button><span class="bar-coverage${partial ? ' partial' : ''}" title="Valid question coverage">${partial ? '<i aria-hidden="true"></i>' : ''}${formatValue(row.coverage, 'coverage', 2)}</span></div>`; | |
| }).join('') : '<div class="chart-empty"><h3>No eligible scores in this size range</h3><p>Choose another range or benchmark.</p><button class="button" data-size-preset="0">All sizes</button></div>'; | |
| document.querySelector('#chart-more').innerHTML = models.length > 10 ? `<button class="text-button" data-action="chart-expand">${s.expanded ? 'Show top 10' : `Show all ${models.length} models`} ${icon('arrow', 12)}</button>` : ''; | |
| translatePage(); | |
| } | |
| function renderPage() { | |
| const c = data.snapshot.counts; | |
| const families = [...new Set(data.models.map(m => m.family))].sort(); | |
| document.querySelector('#app').innerHTML = `<div class="shell"> | |
| <header class="topbar"><a class="brand" href="./" aria-label="Valen leaderboard home"><svg width="30" height="26" viewBox="140 210 740 580" aria-hidden="true" style="color:var(--primary)"><defs><filter id="mark-filter" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 -.2126 -.7152 -.0722 1 0"/><feComponentTransfer><feFuncA type="table" tableValues="0 1 1"/></feComponentTransfer></filter><mask id="mark-mask" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000"><image href="./assets/branding/valen-mark-white.png" width="1000" height="1000" filter="url(#mark-filter)"/></mask></defs><path fill="currentColor" mask="url(#mark-mask)" d="M0 0h1000v1000H0z"/></svg>valen<small>leaderboard</small></a><label class="search-field">${icon('search', 15)}<input id="model-search" type="search" placeholder="Search models or families…" aria-label="Search models or families" autocomplete="off"></label><nav class="nav" aria-label="Project resources">${external('https://valen-team.github.io/docs/jevalkit/', 'Docs', 'external docs-link')}${external('https://github.com/Valen-Team/JevalKit', `GitHub ${icon('diagonal', 14)}`, 'external nav-secondary')}${external('https://huggingface.co/datasets/Valen-Team/JevalKit_Datasets', `Datasets ${icon('diagonal', 14)}`, 'dataset-link external')}${preferenceControls()}</nav></header> | |
| <nav class="section-nav" aria-label="Evaluation sections"><a class="active" href="#leaderboard">Leaderboard</a><button data-action="benchmarks">Benchmarks</button><button data-action="methodology">Methodology</button></nav> | |
| <main><section class="hero"><div class="hero-copy"><div class="eyebrow"><a href="https://valen-team.github.io/docs/jevalkit/">JevalKit</a><span aria-hidden="true">›</span>Leaderboard</div><h1>JevalKit</h1><div class="hero-description"><p class="hero-lead">A common ground for evaluating decision models.</p><p>Compare accuracy, calibration and coverage across text, images and video using verified measurements.</p><div class="type-chips"><span class="type-chip">Choice</span><span class="type-chip">Noul</span><span class="type-chip">Score</span></div></div></div></section> | |
| <section class="stats-strip" aria-label="Evaluation summary"><div class="stat"><span class="stat-number">${c.model_families}</span><div><div class="stat-label">Model families</div><div class="stat-note">${c.evaluated_families} evaluated · ${c.model_families - c.evaluated_families} API pending</div></div></div><div class="stat"><span class="stat-number">${c.benchmark_families}</span><div><div class="stat-label">Benchmark suites</div><div class="stat-note">Text, image & video</div></div></div><div class="stat"><span class="stat-number">${c.eligible_scores}</span><div><div class="stat-label">Verified full scores</div><div class="stat-note">${c.model_configurations} evaluated configurations</div></div></div><div class="stat"><span class="stat-number">${c.retained_runs}</span><div><div class="stat-label">Retained runs</div><div class="stat-note">Full evaluation history</div></div></div></section> | |
| ${renderChartSection()} | |
| <section class="board" id="leaderboard"><div class="board-heading"><div><div class="section-label">THE RESULTS, IN CONTEXT</div><h2>Model leaderboard</h2></div><div class="board-actions"><button class="button ghost archive-button" data-action="archive">${icon('clock', 14)} Run archive</button><button class="button compare-button" id="compare-top" data-action="compare" disabled>${icon('compare', 14)} Compare</button><button class="button primary" data-action="csv">${icon('download', 14)} Export CSV</button></div></div> | |
| <div class="table-card"><div class="table-tabs" id="tabs" role="group" aria-label="Benchmark category"></div><div class="toolbar"><label class="select-field"><span>METRIC</span><select id="metric-select" aria-label="Metric"></select></label><label class="select-field"><span>RANK WITHIN</span><select id="rank-select" aria-label="Rank within benchmark"></select></label><label class="select-field family-field"><span>MODEL FAMILY</span><select id="family-select" aria-label="Model family"><option value="all">All families</option>${families.map(name => `<option value="${esc(name)}">${esc(name)}</option>`).join('')}</select></label><label class="coverage-filter"><input type="checkbox" id="full-coverage"> Full coverage only</label></div><div class="table-meta"><p id="result-count"></p><p class="direction" id="metric-direction"></p></div><div id="table-area"></div><div class="table-footer"><div class="legend"><span><i class="legend-square"></i> Best in benchmark</span><span><i class="legend-dot"></i> Partial coverage</span><span>— Not measured</span></div><button class="text-button" data-action="methodology">How to read this table ${icon('info', 12)}</button></div></div> | |
| <div class="method-strip"><div class="method-item"><span class="method-icon">${icon('shield', 16)}</span><div><h3>Measured, then verified</h3><p>Only complete, eligible runs receive a rank. Historical errors remain visible in the archive.</p></div></div><div class="method-item"><span class="method-icon">${icon('layers', 16)}</span><div><h3>Compare like with like</h3><p>Each column uses a pinned dataset and protocol. No combined score across different benchmarks.</p></div></div><div class="method-item"><span class="method-icon">${icon('code', 16)}</span><div><h3>Open by design</h3><p>Explore every measurement, download the data, or reproduce results with JevalKit.</p></div></div></div></section></main> | |
| <footer class="site-footer"><a class="footer-brand" href="https://github.com/Valen-Team/JevalKit" target="_blank" rel="noopener noreferrer"><img src="./favicon.svg" alt="">JevalKit <span>· Open tools. Better decisions.</span></a>${external('https://valen-team.github.io/docs/jevalkit/', 'Documentation')}<span>Snapshot ${prettyDate(data.snapshot.last_run_at)} · UTC</span>${external(data.snapshot.source_url, `SOURCE ${esc(data.snapshot.source_commit.slice(0, 7))} ${icon('diagonal', 11)}`, 'source-link')}</footer> | |
| </div><div id="compare-bar" class="compare-bar" aria-label="Selected models" hidden></div>`; | |
| renderControls(); renderRows(); renderChart(); | |
| } | |
| function openDialog(title, kicker, subtitle, body) { | |
| const dialog = document.querySelector('#detail-dialog'); | |
| document.querySelector('#dialog-content').innerHTML = `<div class="dialog-head"><div><div class="eyebrow">${kicker}</div><h2 id="dialog-title">${title}</h2>${subtitle ? `<p>${subtitle}</p>` : ''}</div><button class="close-dialog" data-action="close" aria-label="Close dialog">${icon('close', 17)}</button></div><div class="dialog-body">${body}</div>`; | |
| translatePage(); | |
| if (!dialog.open) dialog.showModal(); | |
| document.body.style.overflow = 'hidden'; | |
| dialog.scrollTop = 0; | |
| } | |
| function showModel(id) { | |
| const model = data.models.find(m => m.id === id); | |
| if (!model) return; | |
| const full = data.results.filter(r => r.primary && r.eligible && r.model === id); | |
| const rows = GROUPS.all.names.map(name => { | |
| const row = getRow(id, name); | |
| return `<tr><td>${esc(SHORT_NAMES[name])}</td><td class="numeric">${formatValue(valueOf(row, 'accuracy'), 'accuracy')}</td><td class="numeric">${row ? formatValue(row.coverage, 'coverage', 1) : '—'}</td><td>${badge(row)}</td><td>${row ? `<button class="button" data-run="${row.id}">Details ${icon('diagonal', 11)}</button>` : ''}</td></tr>`; | |
| }).join(''); | |
| openDialog(esc(id), 'MODEL PROFILE', `${esc(model.family)} · ${full.length} eligible benchmark measurements`, `${!model.evaluated ? '<div class="no-rank-note">Benchmark evaluation is pending. No scores have been assigned to this configuration.</div>' : ''}<div class="dialog-table"><table><thead><tr><th>Benchmark</th><th class="numeric">Accuracy</th><th class="numeric">Coverage</th><th>Status</th><th></th></tr></thead><tbody>${rows}</tbody></table></div><p class="detail-note">Accuracy is shown per benchmark. Open a Decision Index measurement to view its separate public-only index score.</p><div class="dialog-actions">${external(model.source_url, `Model source ${icon('diagonal', 13)}`, 'button')}${model.checkpoint && !model.checkpoint.includes('://') && model.checkpoint.includes('/') ? external(`https://huggingface.co/${model.checkpoint}`, `Checkpoint ${icon('diagonal', 13)}`, 'button') : ''}<button class="button" data-add="${esc(id)}">${icon('compare', 13)} Add to comparison</button></div>`); | |
| } | |
| function showRun(id) { | |
| const row = data.results.find(r => r.id === id); | |
| if (!row) return; | |
| const overall = row.overall || {}, b = benchmark(row.benchmark); | |
| const counts = [['Total questions', row.questions], ['Valid answers', overall.valid ?? Math.round(row.coverage * row.questions)], ['Unsupported', row.unsupported], ['Runtime errors', row.failed]]; | |
| const stats = counts.map(([label, value]) => `<div class="summary-box"><span>${label}</span><strong>${number(value)}</strong></div>`).join(''); | |
| const metricRows = ['accuracy', 'brier', 'nll', 'ece', 'latency_s', ...(row.public_decision_index !== null ? ['public_decision_index'] : [])].map(metric => `<tr><td>${METRICS[metric].label}${metric === 'public_decision_index' ? ' · public-only' : ''}</td><td class="numeric">${formatValue(row[metric], metric)}</td><td class="model-note">${METRICS[metric].higher ? 'Higher' : 'Lower'} is better</td></tr>`).join(''); | |
| const types = Object.entries(row.by_type || {}).map(([type, metrics]) => `<tr><td>${groupNames[type] || esc(type)}</td><td class="numeric">${number(metrics.questions)}</td><td class="numeric">${formatValue(metrics.accuracy, 'accuracy')}</td><td class="numeric">${formatValue(metrics.coverage, 'coverage', 1)}</td></tr>`).join(''); | |
| const cleanStatus = row.primary ? 'CURRENT MEASUREMENT' : 'RETAINED HISTORICAL RUN'; | |
| openDialog(esc(row.model), cleanStatus, `${esc(SHORT_NAMES[row.benchmark] || row.benchmark)} · ${prettyDate(row.created_utc)}${row.created_utc ? ' UTC' : ''}`, `${badge(row)}<p class="detail-note">${row.primary ? 'The latest complete measurement selected by the toolkit for this dataset and protocol.' : 'This is a retained run, not the measurement currently selected for the main ranking.'} ${row.smoke ? 'This run uses a smoke-test sample.' : ''} ${row.origin?.startsWith('imported') ? 'This measurement was imported from retained upstream evaluation outputs.' : ''}</p><div class="dialog-summary" style="margin-top:20px">${stats}</div><div class="dialog-table"><table><thead><tr><th>Metric</th><th class="numeric">Value</th><th>Direction</th></tr></thead><tbody>${metricRows}</tbody></table></div>${types ? `<div class="result-types"><h3>By question type</h3><div class="dialog-table"><table><thead><tr><th>Type</th><th class="numeric">Questions</th><th class="numeric">Accuracy</th><th class="numeric">Coverage</th></tr></thead><tbody>${types}</tbody></table></div></div>` : ''}<p class="detail-note">Valid coverage: <strong>${formatValue(row.coverage, 'coverage')}</strong>. Accuracy uses ${typeof overall.hard_questions === 'number' ? number(overall.hard_questions) + ' hard-reference questions' : 'the benchmark’s hard-reference denominator'}; probability metrics use valid probability references. Unsupported answers remain in the accuracy denominator.${isDecisionIndex(row.benchmark) ? ' Public DI excludes private components and is not the full official index.' : ''}</p><dl class="provenance"><div><dt>Protocol</dt><dd>${esc(row.protocol)}</dd></div><div><dt>Hardware / origin</dt><dd>${esc(row.hardware || 'Not recorded')} · ${esc(row.origin)}</dd></div><div><dt>Dataset SHA-256</dt><dd>${esc(row.dataset_sha256)}</dd></div><div><dt>Prediction SHA-256</dt><dd>${esc(row.predictions_sha256 || 'See source snapshot')}</dd></div>${row.manifest_sha256 ? `<div><dt>Run manifest SHA-256</dt><dd>${esc(row.manifest_sha256)}</dd></div>` : ''}<div><dt>Reference questions</dt><dd>${number(b?.questions || row.questions)} · ${row.complete ? 'Complete' : 'Incomplete'} · ${row.smoke ? 'Smoke test' : 'Full dataset'}</dd></div></dl><div class="dialog-actions"><button class="button primary" data-download-run="${row.id}">${icon('download', 14)} Download run JSON</button>${external(data.snapshot.source_url, `Evaluation code ${icon('diagonal', 13)}`, 'button')}${b ? external(b.source_url, `Benchmark source ${icon('diagonal', 13)}`, 'button') : ''}</div>`); | |
| } | |
| function showComparison() { | |
| if (state.selected.length < 2) return; | |
| const colors = ['#b89c37', '#6e8d72', '#7c88a5']; | |
| const legend = state.selected.map((name, i) => `<span><i class="color-dot" style="background:${colors[i]}"></i>${esc(name)}</span>`).join(''); | |
| const sections = GROUPS.all.names.map(name => `<section class="comparison-bench"><h3>${esc(SHORT_NAMES[name])}<small>${number(benchmark(name).questions)} QUESTIONS</small></h3>${state.selected.map((id, i) => { | |
| const row = getRow(id, name), value = valueOf(row, 'accuracy'); | |
| const label = value === null ? statusOf(row) === 'unsupported' ? 'Unsupported' : '—' : formatValue(value, 'accuracy'); | |
| return `<div class="comparison-row"><span class="name">${esc(id)}</span><div class="spark-track">${value !== null ? `<i style="width:${value * 100}%;background:${colors[i]}"></i>` : ''}</div><span class="value" title="${row ? formatValue(row.coverage, 'coverage') + ' valid coverage' : 'Not measured'}">${label}</span></div>`; | |
| }).join('')}</section>`).join(''); | |
| openDialog('Side by side', 'MODEL COMPARISON', 'Accuracy on identical benchmark snapshots · higher is better', `<div class="compare-legend">${legend}</div><p class="detail-note" style="margin:0 0 17px">Bars show accuracy within each benchmark. Missing or unsupported measurements are left empty. Coverage is included in each bar’s tooltip; open a model profile for complete metrics.</p>${sections}<p class="detail-note">Decision Index bars show accuracy, not the public-only composite index. No average across benchmarks is calculated.</p>`); | |
| } | |
| function showBenchmarks() { | |
| const cards = data.benchmarks.map(b => `<article class="catalog-card"><h3>${external(b.source_url, `${esc(SHORT_NAMES[b.name])} ${icon('diagonal', 12)}`)}</h3><p>${number(b.records)} shared states · ${number(b.questions)} questions<br>${esc(b.modality)} · ${measuredCount(b.name)} eligible full results</p><code>${esc(b.protocol)}</code></article>`).join(''); | |
| openDialog('The benchmark collection', '8 FAMILIES · 9 SCORED VIEWS', 'Pinned data, native question types and benchmark-specific protocols', `<div class="catalog-grid">${cards}</div><p class="detail-note">VisualDecisionBench Image and Video are views of one family. The combined TSV is available in the dataset repository and is not counted as an additional suite. Public DI is an official public-only score, with private components excluded.</p><div class="dialog-actions">${external('https://huggingface.co/datasets/Valen-Team/JevalKit_Datasets', `Explore the datasets ${icon('diagonal', 13)}`, 'button primary')}${external('https://github.com/Valen-Team/JevalKit/blob/main/docs/benchmarks.md', `Protocols and scoring ${icon('diagonal', 13)}`, 'button')}</div>`); | |
| } | |
| function showMethodology() { | |
| openDialog('Read the result, not just the rank.', 'EVALUATION NOTES', 'Native inference. Pinned benchmarks. Explicit coverage.', `<div class="body-copy"><h3>One ranking per benchmark</h3><p>Ranks apply to the selected benchmark and metric only. Clicking a column changes the ranking. The table uses the latest complete measurement for each pinned dataset and protocol, rather than picking the highest repeat. No cross-benchmark aggregate is invented.</p><h3>What qualifies for a score?</h3><p>Only complete, non-smoke measurements with conformant inputs, no runtime failures and at least one valid answer are eligible. Unsupported questions stay in the hard-label accuracy denominator. The archive retains earlier errors, capability exclusions, smoke runs and repeated measurements.</p><h3>Accuracy and calibration answer different questions</h3><p>Accuracy uses hard references. Brier, NLL and ECE use valid references under the benchmark’s scoring protocol; inspect valid coverage alongside these metrics. Some suites include soft references that contribute probability metrics but not hard-label accuracy.</p><h3>Coverage matters</h3><p>A gold dot marks partial valid coverage. The coverage column refers to the benchmark selected under “Rank within.” “Full coverage only” filters that selected benchmark. A dash means no measurement. “Unsupported” indicates a complete run with no supported inputs and does not receive a rank.</p><h3>Decision Index is public-only</h3><p>The separate Decision Index tab can show its public-only official score. It excludes private components and should not be read as the complete official Decision Index.</p><h3>Latency needs matching conditions</h3><p>Latency is mean seconds per shared-state request. Hardware appears in each measurement. Author runtimes and serving environments differ, so latency alone is not a controlled hardware comparison.</p><h3>A reproducible snapshot</h3><p>This Space shows a static snapshot from ${prettyDate(data.snapshot.last_run_at)} UTC, with ${data.snapshot.counts.eligible_scores} eligible primary measurements. ${data.snapshot.counts.evaluated_families} of ${data.snapshot.counts.model_families} registered families have measurements; OpenAI-Decisions is still pending. Public data includes selection flags, dataset and prediction hashes, and retained run summaries.</p></div><div class="dialog-actions"><button class="button primary" data-action="json">${icon('download', 14)} Download all results</button><button class="button" data-action="archive">${icon('clock', 14)} Open run archive</button>${external(data.snapshot.source_url, `Evaluation code ${icon('diagonal', 13)}`, 'button')}</div>`); | |
| } | |
| function showArchive() { | |
| const rows = [...data.results].sort((a, b) => (b.created_utc || '').localeCompare(a.created_utc || '')).map(row => `<tr><td><button class="model-name" data-run="${row.id}">${esc(row.model)}</button><div class="archive-flag">${row.primary ? 'Current selected run' : 'Historical run'}${row.smoke ? ' · smoke' : ''}</div></td><td>${esc(SHORT_NAMES[row.benchmark] || row.benchmark)}</td><td>${badge(row)}</td><td class="numeric">${row.failed || '0'}</td><td><button class="button" data-run="${row.id}">Inspect ${icon('diagonal', 11)}</button></td></tr>`).join(''); | |
| openDialog('Every run has a place.', 'THE RUN ARCHIVE', `${data.results.length} retained evaluation artifacts · ${data.snapshot.counts.eligible_scores} current eligible scores`, `<p class="detail-note" style="margin:0 0 18px">Historical, incomplete and smoke measurements remain visible here. Only current eligible full runs enter the main ranking.</p><div class="dialog-table archive-wrap"><table><thead><tr><th>Model / selection</th><th>Benchmark</th><th>Run status</th><th class="numeric">Errors</th><th></th></tr></thead><tbody>${rows}</tbody></table></div><div class="dialog-actions"><button class="button primary" data-action="json">${icon('download', 14)} Download complete snapshot</button></div>`); | |
| } | |
| function changeGroup(group) { | |
| state.group = group; state.reversed = false; | |
| state.rankBy = group === 'text' ? 'JevAltBench' : GROUPS[group].names[0]; | |
| if (group === 'index') state.metric = 'public_decision_index'; | |
| else if (state.metric === 'public_decision_index') state.metric = 'accuracy'; | |
| renderControls(); renderRows(); | |
| } | |
| function addSelection(id) { | |
| if (state.selected.includes(id)) state.selected = state.selected.filter(name => name !== id); | |
| else if (state.selected.length >= 3) { toast('Choose up to 3 models for comparison.'); return false; } | |
| else state.selected.push(id); | |
| renderRows(); return true; | |
| } | |
| function resetFilters() { | |
| state.query = ''; state.family = 'all'; state.fullCoverage = false; | |
| renderControls(); renderRows(); | |
| } | |
| document.addEventListener('click', event => { | |
| const button = event.target.closest('button'); | |
| if (!button || !data) return; | |
| if (button.dataset.run) { showRun(button.dataset.run); return; } | |
| if (button.dataset.model) { showModel(button.dataset.model); return; } | |
| if (button.dataset.group) { changeGroup(button.dataset.group); return; } | |
| if (button.dataset.rank) { | |
| if (state.rankBy === button.dataset.rank) state.reversed = !state.reversed; | |
| else { state.rankBy = button.dataset.rank; state.reversed = false; } | |
| renderControls(); renderRows(); return; | |
| } | |
| if (button.dataset.add) { if (addSelection(button.dataset.add)) toast(state.selected.includes(button.dataset.add) ? 'Added to comparison.' : 'Removed from comparison.'); return; } | |
| if (button.dataset.downloadRun) { | |
| const row = data.results.find(r => r.id === button.dataset.downloadRun); | |
| download(`JevalKit-${row.model}-${row.benchmark}.json`, JSON.stringify({ source: data.snapshot.source_url, ...row }, null, 2)); return; | |
| } | |
| const action = button.dataset.action; | |
| if (button.dataset.sizePreset !== undefined) { | |
| const preset = sizePresets[Number(button.dataset.sizePreset)]; | |
| chartState.min = preset.min; chartState.max = preset.max(); chartState.includeUnknown = !!preset.unknown; | |
| renderChart(); return; | |
| } | |
| if (action === 'chart-expand') { chartState.expanded = !chartState.expanded; renderChart(); } | |
| if (action === 'close') document.querySelector('#detail-dialog').close(); | |
| if (action === 'methodology') showMethodology(); | |
| if (action === 'benchmarks') showBenchmarks(); | |
| if (action === 'archive') showArchive(); | |
| if (action === 'compare') showComparison(); | |
| if (action === 'clear-compare') { state.selected = []; renderRows(); } | |
| if (action === 'reset') resetFilters(); | |
| if (action === 'csv') download(`JevalKit-${state.group}-${state.metric}.csv`, csvFor(visibleModels(), map, tableNames(), state.metric), 'text/csv;charset=utf-8'); | |
| if (action === 'json') download('JevalKit-leaderboard.json', JSON.stringify(data, null, 2)); | |
| }); | |
| document.addEventListener('input', event => { | |
| if (event.target.id === 'model-search') { state.query = event.target.value; renderRows(); } | |
| if (!data) return; | |
| if (event.target.id === 'size-min' || event.target.id === 'size-max') { | |
| const bound = event.target.id === 'size-min' ? 'min' : 'max'; | |
| chartState[bound] = Number(event.target.value); | |
| if (chartState.min > chartState.max) chartState[bound === 'min' ? 'max' : 'min'] = chartState[bound]; | |
| chartState.includeUnknown = chartState.min === 0 && chartState.max === sizeLimit; | |
| renderChart(); | |
| } | |
| }); | |
| document.addEventListener('change', event => { | |
| const target = event.target; | |
| if (!data) return; | |
| if (target.id === 'chart-benchmark') { | |
| chartState.benchmark = target.value; | |
| if (isDecisionIndex(target.value)) chartState.metric = 'public_decision_index'; | |
| else if (chartState.metric === 'public_decision_index') chartState.metric = 'accuracy'; | |
| renderChart(); return; | |
| } | |
| if (target.id === 'chart-metric') { chartState.metric = target.value; renderChart(); return; } | |
| if (target.id === 'size-unknown') { chartState.includeUnknown = target.checked; renderChart(); return; } | |
| if (target.dataset.select) { if (!addSelection(target.dataset.select)) target.checked = false; return; } | |
| if (target.id === 'metric-select') { state.metric = target.value; state.reversed = false; } | |
| if (target.id === 'rank-select') { state.rankBy = target.value; state.reversed = false; } | |
| if (target.id === 'family-select') state.family = target.value; | |
| if (target.id === 'full-coverage') state.fullCoverage = target.checked; | |
| if (['metric-select', 'rank-select', 'family-select', 'full-coverage'].includes(target.id)) renderRows(); | |
| }); | |
| const dialog = document.querySelector('#detail-dialog'); | |
| matchMedia('(max-width: 680px)').addEventListener('change', () => { if (data) renderRows(); }); | |
| dialog.addEventListener('close', () => { document.body.style.overflow = ''; }); | |
| dialog.addEventListener('click', event => { if (event.target === dialog) { const rect = dialog.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close(); } }); | |
| async function boot() { | |
| try { | |
| const response = await fetch('./data/leaderboard.json'); | |
| if (!response.ok) throw new Error(`The data snapshot returned HTTP ${response.status}.`); | |
| data = await response.json(); | |
| if (data.schema_version !== 1 || !data.results.length || !data.models.length) throw new Error('The data snapshot has an unsupported format.'); | |
| map = resultMap(data); benchmarks = new Map(data.benchmarks.map(b => [b.name, b])); | |
| sizeLimit = Math.max(30, Math.ceil(Math.max(...data.models.map(model => parameterSize(model) || 0)) / 10) * 10); | |
| chartState.max = sizeLimit; | |
| const params = new URLSearchParams(location.search); | |
| if (GROUPS[params.get('view')]) state.group = params.get('view'); | |
| if (METRICS[params.get('metric')]) state.metric = params.get('metric'); | |
| else if (state.group === 'index') state.metric = 'public_decision_index'; | |
| if (activeNames().includes(params.get('rank'))) state.rankBy = params.get('rank'); | |
| else if (!activeNames().includes(state.rankBy)) state.rankBy = activeNames()[0]; | |
| if (state.metric === 'public_decision_index' && !['index', 'all'].includes(state.group)) state.metric = 'accuracy'; | |
| state.query = params.get('q') || ''; | |
| renderPage(); | |
| } catch (error) { | |
| document.querySelector('#app').innerHTML = `<main class="error-state"><a class="brand" href="./"><img src="./favicon.svg" alt="">JevalKit</a>${preferenceControls()}<h1>The results couldn’t load.</h1><p>${esc(error.message)} Please reload, or open the source snapshot on Hugging Face.</p><button class="button primary" id="retry-load">Try again ${icon('arrow', 13)}</button> ${external('https://huggingface.co/spaces/Valen-Team/JevalKit-Leaderboard/blob/main/data/leaderboard.json', 'View results snapshot', 'button')}</main>`; | |
| document.querySelector('#retry-load').onclick = boot; | |
| translatePage(); | |
| } | |
| } | |
| document.addEventListener('click', event => { | |
| const button = event.target.closest('button'); | |
| if (!button) return; | |
| if (button.dataset.language) { | |
| language = button.dataset.language === 'zh' ? 'zh' : 'en'; | |
| try {localStorage.setItem('jevalkit-language', language);} catch {} | |
| if (data) renderPage(); else translatePage(); | |
| } | |
| if (button.hasAttribute('data-theme-toggle')) { | |
| theme = theme === 'dark' ? 'light' : 'dark'; | |
| try {localStorage.setItem('jevalkit-theme', theme);} catch {} | |
| applyPreferences(); | |
| } | |
| }); | |
| applyPreferences(); | |
| boot(); | |