benchmark / index.html
Mushari440's picture
v8 item weights recounted to 32,890 (generation was 1,000 light)
39c661d verified
Raw
History Blame Contribute Delete
73.9 kB
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Arabic LLM Leaderboard</title>
<meta name="description" content="A hard, discriminating benchmark for Modern Standard Arabic — models graded by strong LLM judges." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Amiri:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root{
--emerald-900:#04231e; --emerald-800:#063f37; --emerald-700:#0b5d50; --emerald-600:#0f766e;
--emerald-500:#10b981; --emerald-400:#34d399; --emerald-300:#6ee7b7;
--gold-600:#c99a3f; --gold-500:#dcb15a; --gold-400:#e7c877; --gold-300:#efd08a;
}
:root[data-theme="dark"]{
--bg:#071411; --bg2:#0a1a16; --surface:#0e211c; --surface2:#123029; --border:rgba(120,190,170,.16);
--ink:#eaf3ef; --ink-soft:#b7cec6; --ink-mute:#7f9a91;
--accent:var(--emerald-400); --accent-strong:var(--emerald-300); --gold:var(--gold-400);
--row-hover:rgba(16,185,129,.10); --head-bg:rgba(16,185,129,.13); --head-bd:var(--emerald-500);
--cell-heat:16,185,129;
--freeze:#0e211c; --freeze-hover:#15302a; --head-solid:#0e3529; --scroll-track:#0a1a16;
}
:root[data-theme="light"]{
--bg:#f6f3ea; --bg2:#efeadd; --surface:#fffdf7; --surface2:#f3efe4; --border:rgba(20,80,66,.16);
--ink:#122a23; --ink-soft:#3a544c; --ink-mute:#6d857c;
--accent:var(--emerald-600); --accent-strong:var(--emerald-700); --gold:var(--gold-600);
--row-hover:rgba(15,118,110,.07); --head-bg:rgba(15,118,110,.10); --head-bd:var(--emerald-600);
--cell-heat:5,150,105;
--freeze:#fffdf7; --freeze-hover:#eef4ed; --head-solid:#e7f0e9; --scroll-track:#e7e0d0;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;overscroll-behavior-x:none}
body{
background:
radial-gradient(120% 90% at 88% -8%, rgba(201,154,63,.10), transparent 55%),
radial-gradient(120% 90% at 8% -6%, rgba(16,185,129,.10), transparent 55%),
var(--bg);
color:var(--ink);
font-family:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif;
font-size:15px; line-height:1.5; -webkit-font-smoothing:antialiased;
}
.wrap{max-width:1360px; margin:0 auto; padding:22px 22px 80px}
a{color:var(--accent); text-decoration:none}
a:hover{text-decoration:underline; text-decoration-style:dotted}
/* ---------------- Masthead ---------------- */
.masthead{
position:relative; overflow:hidden; border-radius:22px; padding:36px 42px 32px;
color:#f3efe4; margin-bottom:20px;
background:radial-gradient(130% 150% at 12% 8%, #0c6355 0%, #074237 46%, #04231e 100%);
border:1px solid rgba(201,154,63,.32);
box-shadow:0 26px 60px -30px rgba(0,0,0,.75), inset 0 1px 0 rgba(255,255,255,.05);
}
.masthead::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.15;
background-image:radial-gradient(circle at 88% 16%, rgba(231,200,119,.55) 0, transparent 42%),
repeating-linear-gradient(135deg, rgba(255,255,255,.05) 0 2px, transparent 2px 16px);
-webkit-mask-image:linear-gradient(90deg,transparent 42%,#000 100%); mask-image:linear-gradient(90deg,transparent 42%,#000 100%);}
.masthead::after{content:"";position:absolute;left:42px;right:42px;bottom:0;height:2px;
background:linear-gradient(90deg,transparent,#c99a3f 22%,#efd08a 50%,#c99a3f 78%,transparent);opacity:.75}
.mh-top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
.eyebrow{font:600 11.5px/1 "IBM Plex Sans";letter-spacing:.3em;text-transform:uppercase;color:#ecd08a}
.titles{display:flex;align-items:baseline;gap:20px;flex-wrap:wrap;margin:13px 0 8px}
.title-en{margin:0;font:600 clamp(30px,4.6vw,48px)/1 "Fraunces",Georgia,serif;color:#fdfbf4;letter-spacing:-.012em}
.title-ar{font:700 clamp(23px,3.4vw,37px)/1.2 "Amiri",serif;color:#ecd08a;direction:rtl}
.tagline{max-width:66ch;margin:2px 0 0;font:400 15px/1.6 "IBM Plex Sans";color:#cfe6dd}
.stats{margin-top:17px;display:flex;gap:11px;flex-wrap:wrap;align-items:center;font:500 12.5px/1 "IBM Plex Sans";color:#b6d4ca}
.stats b{color:#fff;font-weight:700}.stats .dot{color:#c99a3f;font-size:8px;opacity:.85}
.theme-btn{flex:none;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);color:#f3efe4;
border-radius:10px;padding:8px 12px;cursor:pointer;font:600 13px/1 "IBM Plex Sans";transition:background .15s}
.theme-btn:hover{background:rgba(255,255,255,.16)}
.mh-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
.mh-who{color:#cfe6dd;font-size:12.5px;white-space:nowrap}
.mh-who b{color:#fff}.mh-who a{color:var(--gold-300)}
.btn-submit-top{box-shadow:0 10px 24px -12px rgba(16,185,129,.8)}
/* ---------------- Controls ---------------- */
.controls{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:16px}
/* Version picker sits OUTSIDE #controls on purpose: #controls is hidden whenever a
version has no rows, and a picker inside it would strand you on an empty board. */
.verbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}
.verbar label{font:600 12px "IBM Plex Sans";letter-spacing:.02em;color:var(--ink-soft)}
.verbar select{padding:8px 12px;border-radius:10px;border:1px solid var(--border);
background:var(--bg2);color:var(--ink);font:500 13.5px "IBM Plex Sans"}
.verbar select:focus{outline:2px solid var(--accent);outline-offset:0;border-color:transparent}
.verbar .vnote{font-size:12px;color:var(--ink-mute)}
.search{flex:1 1 260px;position:relative}
.search input{width:100%;padding:11px 14px 11px 38px;border-radius:12px;border:1px solid var(--border);
background:var(--surface);color:var(--ink);font:500 14px "IBM Plex Sans"}
.search input::placeholder{color:var(--ink-mute)}
.search svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);opacity:.5}
.chips{display:flex;gap:8px;flex-wrap:wrap}
.chip{border:1px solid var(--border);background:var(--surface);color:var(--ink-soft);border-radius:999px;
padding:8px 14px;font:600 12.5px "IBM Plex Sans";cursor:pointer;transition:all .12s;user-select:none}
.chip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:#04231e}
.range{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--border);
border-radius:12px;padding:8px 14px;font:600 12.5px "IBM Plex Sans";color:var(--ink-soft)}
.range input[type=range]{accent-color:var(--accent);width:110px}
.range .rv{font-family:"IBM Plex Mono";color:var(--ink);min-width:22px;text-align:right}
/* ---------------- Table ---------------- */
.board-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;
box-shadow:0 16px 40px -30px rgba(0,0,0,.5)}
.board-scroll{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;
overscroll-behavior-x:none;scrollbar-width:thin;scrollbar-color:var(--ink-mute) var(--scroll-track)}
.board-scroll::-webkit-scrollbar{height:12px}
.board-scroll::-webkit-scrollbar-track{background:var(--scroll-track)}
.board-scroll::-webkit-scrollbar-thumb{background:var(--ink-mute);border-radius:8px;border:3px solid var(--scroll-track)}
.board-scroll::-webkit-scrollbar-thumb:hover{background:var(--accent)}
table.board{border-collapse:separate;border-spacing:0;width:100%;font-variant-numeric:tabular-nums}
table.board th,table.board td{padding:12px 12px;white-space:nowrap;text-align:right}
table.board th:nth-child(-n+2),table.board td:nth-child(-n+2){text-align:left}
table.board thead th{position:sticky;top:0;z-index:4;background:var(--head-solid);
border-bottom:2px solid var(--head-bd);
font:700 12px "IBM Plex Sans";letter-spacing:.02em;color:var(--ink);cursor:pointer;user-select:none}
table.board thead th .arrow{color:var(--accent);font-size:10px;margin-left:3px;opacity:.9}
table.board thead th.sub{font-size:11px;color:var(--ink-soft);font-weight:600}
table.board tbody tr{border-bottom:1px solid var(--border)}
table.board tbody tr:hover{background:var(--row-hover)}
table.board tbody td{font-size:13.5px}
/* Frozen identity columns — keep Rank + Model pinned while scrolling the task columns. */
table.board th:nth-child(1),table.board td:nth-child(1){position:sticky;left:0;z-index:3;
background:var(--freeze);width:46px;min-width:46px;max-width:46px;padding-left:14px;padding-right:4px}
table.board th:nth-child(2),table.board td:nth-child(2){position:sticky;left:46px;z-index:3;background:var(--freeze)}
table.board thead th:nth-child(1),table.board thead th:nth-child(2){z-index:5;background:var(--head-solid)}
table.board tbody tr:hover td:nth-child(1),table.board tbody tr:hover td:nth-child(2){background:var(--freeze-hover)}
/* subtle shadow on the pinned block's edge, shown only once scrolled right */
.board-scroll.scrolled table.board th:nth-child(2),.board-scroll.scrolled table.board td:nth-child(2){
box-shadow:13px 0 15px -12px rgba(0,0,0,.6)}
.rank{font-family:"IBM Plex Mono";font-weight:700;color:var(--ink-mute)}
.rank.top{color:#04231e;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;font-size:13px}
.rank.r1{background:linear-gradient(135deg,#f4d980,#c99a3f)}
.rank.r2{background:linear-gradient(135deg,#e6e6ea,#b8bcc6)}
.rank.r3{background:linear-gradient(135deg,#e3b489,#c0824e)}
.model-cell{display:flex;flex-direction:column;gap:2px;max-width:260px}
.model-cell .name{font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis}
.model-cell .org{font-size:11.5px;color:var(--ink-mute)}
.type-ico{font-size:13px;margin-right:2px}
.avg-cell{min-width:118px}
.avg-num{font:700 15px "IBM Plex Mono";color:var(--accent-strong)}
.avg-bar{height:5px;border-radius:3px;background:var(--surface2);margin-top:5px;overflow:hidden}
.avg-bar>i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,var(--emerald-600),var(--emerald-400))}
.score{font-family:"IBM Plex Mono";font-weight:500;border-radius:7px;padding:5px 8px;display:inline-block;min-width:48px;text-align:right}
/* best score in each task column (across all shown models) */
.score.na{color:var(--muted);opacity:.55;font-weight:400}
.score.best{font-weight:800;color:var(--ink);box-shadow:inset 0 0 0 2px var(--gold),0 4px 12px -6px rgba(201,154,63,.5)}
.meta-num{font-family:"IBM Plex Mono";color:var(--ink-soft);font-size:12.5px}
.note{color:var(--ink-mute);font-size:12.5px;margin:12px 4px 0;line-height:1.55}
.note b{color:var(--ink-soft)}
/* queue + submit */
/* Author `display` rules (e.g. .grid2 below) outrank the UA stylesheet's
[hidden]{display:none}, so an element with class + hidden stays visible.
This restores the attribute as the last word. */
[hidden]{display:none!important}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:26px}
.grid2.one{grid-template-columns:1fr}
.panel{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:20px 22px}
.panel h3{margin:0 0 12px;font:700 15px "IBM Plex Sans";color:var(--ink);display:flex;align-items:center;gap:8px}
.panel h3 .k{font:600 11px "IBM Plex Sans";letter-spacing:.06em;color:var(--gold);text-transform:uppercase}
.qrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 0;border-bottom:1px dashed var(--border);font-size:13px}
.qrow:last-child{border-bottom:0}
.mrow{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px dashed var(--border);font-size:13px}
.mrow:last-child{border-bottom:0}
.mrow .mname{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mbtn{font:600 11px "IBM Plex Sans";padding:4px 10px;border-radius:999px;border:1px solid var(--border);
background:transparent;color:var(--ink-soft);cursor:pointer}
.mbtn:hover{border-color:var(--accent);color:var(--accent)}
.mbtn.danger:hover{border-color:#e06666;color:#e06666}
.b-private{background:rgba(120,120,200,.16);color:#9aa4e0}
.vchip{font:600 10px "IBM Plex Mono";padding:2px 7px;border-radius:6px;border:1px solid var(--border);
color:var(--ink-mute);white-space:nowrap}
.vchip.on{border-color:var(--accent);color:var(--accent)}
.vchip.here{box-shadow:inset 0 0 0 1px var(--accent)}
.mbar{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:9px 0 11px;
border-bottom:1px solid var(--border);margin-bottom:6px}
.mbar .grow{flex:1 1 auto}
.prog{margin:2px 0 12px;padding:11px 13px;border-radius:12px;background:rgba(var(--cell-heat),.07);
border:1px solid var(--border)}
.prog .ptop{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;font-size:13px}
.prog .pmodel{font-weight:700}
.prog .pphase{font:600 10.5px "IBM Plex Sans";letter-spacing:.04em;padding:2px 8px;border-radius:999px;
background:rgba(16,185,129,.16);color:var(--accent)}
.prog .pnums{margin-left:auto;font-family:"IBM Plex Mono";font-size:12px;color:var(--ink-soft)}
.pbar{height:7px;border-radius:999px;background:rgba(255,255,255,.07);margin-top:9px;overflow:hidden}
.pbar i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#6ee7b7);transition:width .5s}
.pmeta{margin-top:7px;font-size:11.5px;color:var(--ink-mute);font-family:"IBM Plex Mono"}
.qcount{float:right;font-family:"IBM Plex Mono";font-size:12px;color:var(--ink-mute);font-weight:400}
.qnum{font-family:"IBM Plex Mono";font-size:11.5px;color:var(--ink-mute);min-width:22px;text-align:right;flex:0 0 auto}
.qrow a{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.badge{font:600 10.5px "IBM Plex Sans";letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:999px}
.b-pending{background:rgba(201,154,63,.16);color:var(--gold)}
.b-running{background:rgba(16,185,129,.16);color:var(--accent)}
.b-failed{background:rgba(229,90,90,.16);color:#e88}
.panel ol{margin:0;padding-left:20px}.panel li{margin:6px 0;color:var(--ink-soft);font-size:13.5px}
.panel code{font-family:"IBM Plex Mono";font-size:12px;background:var(--surface2);padding:2px 6px;border-radius:6px;color:var(--ink)}
.state{padding:60px 20px;text-align:center;color:var(--ink-mute)}
.spinner{width:34px;height:34px;border-radius:50%;border:3px solid var(--border);border-top-color:var(--accent);
animation:spin .8s linear infinite;margin:0 auto 14px}
@keyframes spin{to{transform:rotate(360deg)}}
footer{margin-top:30px;text-align:center;color:var(--ink-mute);font-size:12px}
/* ---------------- Submit button + auth ---------------- */
.btn{border:0;border-radius:12px;padding:11px 18px;font:700 14px "IBM Plex Sans";cursor:pointer;
display:inline-flex;align-items:center;gap:8px;justify-content:center;transition:filter .15s,background .15s}
.btn-primary{background:linear-gradient(135deg,var(--emerald-500),var(--emerald-600));color:#04231e}
.btn-primary:hover{filter:brightness(1.07)}
.btn-ghost{background:var(--surface2);color:var(--ink);border:1px solid var(--border)}
.btn-ghost:hover{background:var(--row-hover)}
.btn:disabled{opacity:.55;cursor:not-allowed}
.who{font-size:12.5px;color:var(--ink-mute)}.who b{color:var(--ink-soft)}
.submit-intro{margin:0 0 14px;color:var(--ink-soft);font-size:13.5px;line-height:1.55}
/* ---------------- Modal ---------------- */
.modal{position:fixed;inset:0;z-index:60;display:none;align-items:center;justify-content:center;padding:20px;
background:rgba(3,12,10,.62);backdrop-filter:blur(3px)}
.modal.open{display:flex}
.modal-card{width:min(520px,100%);max-height:90vh;overflow:auto;background:var(--surface);border:1px solid var(--border);
border-radius:18px;padding:24px 26px 20px;box-shadow:0 30px 80px -30px rgba(0,0,0,.7)}
.modal-card h2{margin:0 0 3px;font:700 21px "Fraunces",Georgia,serif;color:var(--ink)}
.modal-card .sub{margin:0 0 18px;color:var(--ink-mute);font-size:13px}
.x-close{float:right;background:none;border:0;color:var(--ink-mute);font-size:24px;cursor:pointer;line-height:1;margin:-4px -6px 0 0}
.x-close:hover{color:var(--ink)}
.field{margin-bottom:14px}
.field label{display:block;font:600 12px "IBM Plex Sans";letter-spacing:.02em;color:var(--ink-soft);margin-bottom:6px}
.field input,.field select{width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);
background:var(--bg2);color:var(--ink);font:500 14px "IBM Plex Sans"}
.field input:focus,.field select:focus{outline:2px solid var(--accent);outline-offset:0;border-color:transparent}
.field .hint{font-size:11.5px;color:var(--ink-mute);margin-top:5px}
.field .hint code{font-family:"IBM Plex Mono";font-size:11px;background:var(--surface2);padding:1px 5px;border-radius:5px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.modal-actions{display:flex;gap:10px;align-items:center;margin-top:10px}
.msg{margin-top:12px;padding:11px 13px;border-radius:10px;font-size:13px;display:none;line-height:1.5}
.msg.show{display:block}
.msg.ok{background:rgba(16,185,129,.14);color:var(--accent-strong);border:1px solid rgba(16,185,129,.32)}
.msg.err{background:rgba(229,90,90,.14);color:#f0a3a3;border:1px solid rgba(229,90,90,.32)}
.signin-box{text-align:center;padding:8px 0 6px}
.signin-box p{color:var(--ink-soft);font-size:13.5px;margin:0 0 16px;line-height:1.55}
.hf-btn{background:#ff9d00;color:#141414;border:0;border-radius:11px;padding:12px 20px;
font:700 14px "IBM Plex Sans";cursor:pointer;display:inline-flex;gap:9px;align-items:center}
.hf-btn:hover{filter:brightness(1.06)}
@media(max-width:720px){.wrap{padding:14px 12px 60px}.masthead{padding:24px 22px}.grid2{grid-template-columns:1fr}.row2{grid-template-columns:1fr}
.model-cell{max-width:150px}}
</style>
</head>
<body>
<div class="wrap">
<header class="masthead">
<div class="mh-top">
<div class="eyebrow">Modern Standard Arabic · LLM Evaluation</div>
<div class="mh-actions">
<span class="mh-who" id="whoami"></span>
<button class="btn btn-primary btn-submit-top" id="openSubmit">+ Submit your model</button>
<button class="theme-btn" id="themeBtn" title="Toggle light / dark">◐ Theme</button>
</div>
</div>
<div class="titles">
<h1 class="title-en">Arabic LLM Leaderboard</h1>
<span class="title-ar">لوحة صدارة نماذج اللغة العربية</span>
</div>
<p class="tagline">A hard, discriminating benchmark for Modern Standard Arabic — models graded by strong LLM judges across generation, reasoning, knowledge, safety, and multi-turn dialogue.</p>
<div class="stats">
<span><b id="itemCount">34,137</b> items</span><i class="dot"></i>
<span><b>70</b> subtasks</span><i class="dot"></i>
<span><b>13</b> categories</span><i class="dot"></i>
<span><b>5</b> formats</span><i class="dot"></i>
<span><b id="modelCount"></b> models</span><i class="dot"></i>
<span>LLM-as-Judge</span>
</div>
</header>
<div class="verbar">
<label for="verSel">Benchmark version</label>
<select id="verSel"></select>
<span class="vnote" id="verNote"></span>
</div>
<div class="controls" id="controls" hidden>
<div class="search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input id="search" type="search" placeholder="Search models…" autocomplete="off" />
</div>
<div class="chips" id="typeChips"></div>
<div class="chips" id="precChips"></div>
<label class="range">Params ≤ <input id="paramMax" type="range" min="0" max="72" step="1" value="72"><span class="rv" id="paramRV">72</span>B</label>
</div>
<div class="board-card">
<div class="board-scroll">
<div class="state" id="state"><div class="spinner"></div>Loading the leaderboard…</div>
<table class="board" id="board" hidden><thead id="thead"></thead><tbody id="tbody"></tbody></table>
</div>
</div>
<p class="note" id="note" hidden>The <b>Average</b> is the <b>item-weighted</b> mean across 13 capability &amp; domain dimensions — every benchmark item counts equally, so a small bucket never outweighs a large one. <b>Knowledge &amp; STEM</b> includes medicine and biology. Category cells are shaded by score, and the <b>gold-outlined</b> cell is the top score in each task column. Click any column header to sort.</p>
<div class="grid2 one" id="managePanel" hidden>
<div class="panel">
<h3><span class="k">Manage</span> Your submitted models<span id="manageCount" class="qcount"></span></h3>
<div class="mbar" id="manageBar" hidden>
<span class="vchip here" id="mbarVer"></span>
<span class="grow" style="font-size:12px;color:var(--ink-mute)" id="mbarNote"></span>
<button class="mbtn" id="btnQueueMissing">Queue models missing here</button>
<button class="mbtn" id="btnQueueAll">Re-queue all, scored included</button>
</div>
<div id="manageList"><div class="mrow" style="color:var(--ink-mute)">Loading…</div></div>
<div class="msg" id="manageMsg"></div>
</div>
</div>
<div class="grid2 one">
<div class="panel">
<h3><span class="k">Queue</span> Evaluation status<span id="queueCount" class="qcount"></span></h3>
<div id="progress" hidden></div>
<div id="queue"><div class="qrow" style="color:var(--ink-mute)">Loading queue…</div></div>
</div>
</div>
<footer>
Data read live from <a href="https://huggingface.co/datasets/Mushari440/results" target="_blank" rel="noopener">Mushari440/results</a> ·
<span id="updated"></span> · Built for Modern Standard Arabic.
</footer>
</div>
<!-- ---------------- Submit modal ---------------- -->
<div class="modal" id="submitModal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="smTitle">
<button class="x-close" id="closeSubmit" aria-label="Close">×</button>
<h2 id="smTitle">Submit a model</h2>
<p class="sub">Evaluate a public Hugging Face model on the Arabic benchmark.</p>
<div id="signinView" class="signin-box" hidden>
<p>Sign in with your Hugging Face account to submit a model. Your username is recorded with the
request — your access token is never used and never leaves your browser.</p>
<button class="hf-btn" id="hfLogin">🤗 &nbsp;Sign in with Hugging Face</button>
</div>
<form id="submitForm" hidden>
<div class="field">
<label for="f-model">Model repository</label>
<input id="f-model" placeholder="org/model-name" autocomplete="off" spellcheck="false" required />
<div class="hint" id="f-model-hint">A public model repo on the Hub, loadable with <code>AutoModel</code> (must have a <code>config.json</code>).</div>
</div>
<div class="field" id="sourceField" hidden>
<label for="f-source">Model source</label>
<select id="f-source">
<option value="open">Open-weight — runs on our GPUs</option>
<option value="closed">Closed-source — evaluated via OpenRouter API</option>
</select>
<div class="hint">Closed-source models are billed per token, so this option is limited to the leaderboard maintainer.</div>
</div>
<div class="field" id="visField" hidden>
<label for="f-vis">Leaderboard visibility</label>
<select id="f-vis">
<option value="public">Public — show on the leaderboard</option>
<option value="private">Private — only visible to me</option>
</select>
<div class="hint">Private scores go to a separate private dataset; nobody else can read them, on the board or on the Hub.</div>
</div>
<div class="field" id="maxField" hidden>
<label for="f-max">Items per subtask <span style="font-weight:400;opacity:.7">— blank = every item</span></label>
<input id="f-max" type="number" min="1" max="1000" placeholder="e.g. 5 for a smoke test" autocomplete="off" />
<div class="hint">Caps every subtask. A capped run is for testing — its score is not comparable with the full-corpus board.</div>
</div>
<div class="field" id="subtaskField" hidden>
<label for="f-subtasks">Subtasks <span style="font-weight:400;opacity:.7">— none selected = all 70</span></label>
<select id="f-subtasks" multiple size="9">
<optgroup label="Arabic">
<option value="rare_words_lexicons">rare_words_lexicons (381)</option>
</optgroup>
<optgroup label="Arabic Language Understanding">
<option value="arabic_diacritization">arabic_diacritization (296)</option>
<option value="arabic_figurative">arabic_figurative (306)</option>
<option value="arabic_grammar_correction">arabic_grammar_correction (242)</option>
<option value="arabic_irab">arabic_irab (336)</option>
<option value="arabic_morphology">arabic_morphology (354)</option>
<option value="arabic_semantics">arabic_semantics (316)</option>
<option value="contextual_error">contextual_error (383)</option>
<option value="general_aptitude_tahsili_verbal">general_aptitude_tahsili_verbal (1,095)</option>
<option value="odd_one_out">odd_one_out (314)</option>
<option value="text_correction_msa_mixed_ar_en">text_correction_msa_mixed_ar_en (1,300)</option>
<option value="verbal_analogy">verbal_analogy (394)</option>
</optgroup>
<optgroup label="Completion">
<option value="completion">completion (366)</option>
</optgroup>
<optgroup label="Generation & Instruction">
<option value="gen_constrained">gen_constrained (273)</option>
<option value="gen_creative_writing">gen_creative_writing (407)</option>
<option value="gen_formal_documents">gen_formal_documents (364)</option>
<option value="gen_rewriting">gen_rewriting (244)</option>
<option value="gen_structured_output">gen_structured_output (364)</option>
<option value="gen_summarization">gen_summarization (645)</option>
<option value="gen_translation">gen_translation (388)</option>
</optgroup>
<optgroup label="Information Extraction">
<option value="core_entities_persons_orgs_locations">core_entities_persons_orgs_locations (400)</option>
<option value="instructed_extraction_arbitrary_targets_scoping">instructed_extraction_arbitrary_targets_scoping (800)</option>
<option value="temporal_numeric_expressions">temporal_numeric_expressions (300)</option>
</optgroup>
<optgroup label="Islam">
<option value="arabic_islamic_studies">arabic_islamic_studies (15)</option>
<option value="creed">creed (380)</option>
<option value="fiqh_sciences_maxims">fiqh_sciences_maxims (1,086)</option>
<option value="hadith_commentaries">hadith_commentaries (1,084)</option>
<option value="hadith_defects_questions">hadith_defects_questions (376)</option>
<option value="hadith_sciences">hadith_sciences (1,085)</option>
<option value="hanafi_fiqh">hanafi_fiqh (1,083)</option>
<option value="history">history (382)</option>
<option value="logic">logic (27)</option>
<option value="sects_refutations">sects_refutations (377)</option>
<option value="shariah_politics_judiciary">shariah_politics_judiciary (1,083)</option>
<option value="takhrij_atraf">takhrij_atraf (1,081)</option>
<option value="usul_al_fiqh">usul_al_fiqh (1,084)</option>
</optgroup>
<optgroup label="Knowledge & STEM">
<option value="biology">biology (261)</option>
<option value="chemistry">chemistry (230)</option>
<option value="knowledge_humanities">knowledge_humanities (370)</option>
<option value="knowledge_islamic">knowledge_islamic (232)</option>
<option value="knowledge_qa_rag">knowledge_qa_rag (268)</option>
<option value="knowledge_qa_short">knowledge_qa_short (228)</option>
<option value="knowledge_social_sciences">knowledge_social_sciences (296)</option>
<option value="knowledge_stem">knowledge_stem (456)</option>
<option value="mathematics">mathematics (165)</option>
<option value="medicine_bio">medicine_bio (415)</option>
<option value="physics">physics (224)</option>
</optgroup>
<optgroup label="Multi-Turn Dialogue">
<option value="dialogue_debate">dialogue_debate (380)</option>
<option value="dialogue_info_seeking">dialogue_info_seeking (458)</option>
<option value="dialogue_instruction_revision">dialogue_instruction_revision (447)</option>
<option value="dialogue_task_oriented">dialogue_task_oriented (403)</option>
</optgroup>
<optgroup label="Reasoning & Logic">
<option value="reasoning_causal">reasoning_causal (360)</option>
<option value="reasoning_commonsense">reasoning_commonsense (500)</option>
<option value="reasoning_logical">reasoning_logical (612)</option>
<option value="reasoning_math">reasoning_math (788)</option>
<option value="reasoning_reading_comprehension">reasoning_reading_comprehension (550)</option>
<option value="reasoning_word_problems">reasoning_word_problems (500)</option>
</optgroup>
<optgroup label="Safety, Trust & Bias">
<option value="safety_bias_detection">safety_bias_detection (582)</option>
<option value="safety_hallucination">safety_hallucination (438)</option>
<option value="safety_refusal">safety_refusal (558)</option>
<option value="safety_toxicity">safety_toxicity (456)</option>
</optgroup>
<optgroup label="Sentiment & Meaning">
<option value="sentiment_analysis">sentiment_analysis (827)</option>
<option value="sentiment_emotion">sentiment_emotion (667)</option>
<option value="sentiment_implicature">sentiment_implicature (541)</option>
<option value="sentiment_sarcasm">sentiment_sarcasm (514)</option>
</optgroup>
<optgroup label="Translation">
<option value="expository_analytical_english_arabic">expository_analytical_english_arabic (450)</option>
<option value="expository_literary_msa_arabic_english">expository_literary_msa_arabic_english (450)</option>
<option value="specialized_technical_arabic_english">specialized_technical_arabic_english (350)</option>
<option value="specialized_technical_english_arabic">specialized_technical_english_arabic (350)</option>
<option value="translation_fidelity_mcq_both_directions">translation_fidelity_mcq_both_directions (400)</option>
</optgroup>
</select>
<div class="hint">Ctrl/&#8984;-click to pick several. Restricting subtasks produces a partial score, so use it for testing only.</div>
</div>
<div class="row2">
<div class="field">
<label for="f-type">Model type</label>
<select id="f-type">
<option value="fine-tuned">🔶 Fine-tuned</option>
<option value="instruction-tuned">⭕ Instruction-tuned</option>
<option value="pretrained">🟢 Pretrained</option>
<option value="RL-tuned">🟦 RL-tuned</option>
</select>
</div>
<div class="field">
<label for="f-prec">Precision</label>
<select id="f-prec">
<option value="bfloat16">bfloat16</option>
<option value="float16">float16</option>
</select>
</div>
</div>
<div class="row2">
<div class="field">
<label for="f-rev">Revision</label>
<input id="f-rev" value="main" autocomplete="off" spellcheck="false" />
</div>
<div class="field">
<label for="f-weight">Weights</label>
<select id="f-weight">
<option value="Original">Original</option>
<option value="Adapter">Adapter (LoRA)</option>
<option value="Delta">Delta</option>
</select>
</div>
</div>
<div class="msg" id="submitMsg"></div>
<div class="modal-actions">
<span class="who" id="whoamiModal" style="margin-right:auto"></span>
<button type="button" class="btn btn-ghost" id="cancelSubmit">Cancel</button>
<button type="submit" class="btn btn-primary" id="doSubmit">Submit for evaluation</button>
</div>
</form>
</div>
</div>
<script>
const RESULTS="Mushari440/results", REQUESTS="Mushari440/requests";
const TREE=r=>`https://huggingface.co/api/datasets/${r}/tree/main?recursive=1`;
const FILE=(r,p)=>`https://huggingface.co/datasets/${r}/resolve/main/${p.split('/').map(encodeURIComponent).join('/')}`;
// 13 dimensions: [key, short label, item count (weight)].
// Weights are item counts in the v2 corpus (combined_34137, 2026-07-29): they sum
// to 34,137. Translation and Information Extraction arrived with v2. Knowledge &
// STEM absorbs the standalone Medicine (400) + Biology (15) buckets: 3825+400+15=4240.
// Item counts in the combined_34137 corpus (2026-07-29). They sum to 34,137.
const DIMS_34137=[
["reasoning_logic","Reasoning",3310],["generation_instruction","Generation",2685],
["sentiment_emotion_meaning","Sentiment",2549],["safety_trust_bias","Safety",2034],
["arabic_language_understanding","Arabic Lang.",3531],["knowledge_stem","Knowledge",4240],
["multi_turn_dialogue","Multi-Turn",1688],["translation","Translation",2000],
["information_extraction","Extraction",1500],["islam","Islam",9143],
["completion","Completion",366],["contextual","Contextual",383],
["semantic","Semantic",708]
];
// ── Benchmark versions ────────────────────────────────────────────────────────
// Each version is a separate results namespace in the dataset repo:
// <version>/<org>/results_<model>.json
// `legacy:true` also admits the un-prefixed files at the repo root, which is where
// every result lived before versioning; a model found in both is taken from the
// prefixed copy. Weights are the corpus item counts for THAT version — v8 runs on
// the revised corpus, so its counts must be updated once that corpus is final.
// Item counts in the revised corpus (benchmarkDataset, 2026-08-19). They sum to
// The live corpus. Recounted from benchmarkDataset on 2026-08-28.
// Derived by running the worker's own bucket_for() over the corpus, not by hand.
// Item counts in the live corpus (benchmarkDataset, recounted 2026-08-28 with the
// same bucket_for() the results builder uses, so these weights match the buckets
// the scores land in). They sum to 32,890. Previously these still held the
// 31,925 figures: generation_instruction was 1,000 items light, so every
// item-weighted Average was computed against a corpus that no longer existed.
const DIMS_V8=[
["reasoning_logic","Reasoning",3161],["generation_instruction","Generation",3475],
["sentiment_emotion_meaning","Sentiment",2549],["safety_trust_bias","Safety",2034],
["arabic_language_understanding","Arabic Lang.",3531],["knowledge_stem","Knowledge",3732],
["multi_turn_dialogue","Multi-Turn",1679],["translation","Translation",2000],
["information_extraction","Extraction",1500],["islam","Islam",7838],
["completion","Completion",365],["contextual","Contextual",343],
["semantic","Semantic",683]
];
const KS_34137=[["knowledge_stem",3825],["medicine",400],["biology",15]];
const KS_V8=[["knowledge_stem",3318],["medicine",400],["biology",14]];
let KS_PARTS=KS_34137;
const VERSIONS=[
{id:"v7", label:"v7 — corpus 34,137", dims:DIMS_34137, ks:KS_34137, legacy:true},
{id:"v8", label:"v8 — corpus 32,890", dims:DIMS_V8, ks:KS_V8}
];
const DEFAULT_VERSION="v7";
function versionCfg(id){return VERSIONS.find(v=>v.id===id)||VERSIONS[0];}
let CURRENT=versionCfg(DEFAULT_VERSION);
let DIMS=CURRENT.dims;
let KS=CURRENT.ks;
let WSUM=DIMS.reduce((s,d)=>s+d[2],0);
// Results published BEFORE the merge carry medicine/biology as separate keys; fold them back
// into Knowledge & STEM by item count. Results published after the fix already contain the
// merged value under knowledge_stem alone, so this degrades gracefully to a plain read.
function dimScore(res,key){
if(key!=="knowledge_stem"){
const v=res?.[key]?.acc; return typeof v==="number"?v*100:null;
}
let s=0,w=0;
for(const [k,n] of KS_PARTS){const v=res?.[k]?.acc; if(typeof v==="number"){s+=v*n;w+=n;}}
return w?(s/w)*100:null;
}
const TYPE_ICON={"fine-tuned":"🔶","instruction-tuned":"⭕","pretrained":"🟢","RL-tuned":"🟦"};
function typeKey(t){t=(t||"").toLowerCase();
if(t.includes("fine"))return"fine-tuned";if(t.includes("instruction"))return"instruction-tuned";
if(t.includes("pretrain"))return"pretrained";if(t.includes("rl"))return"RL-tuned";return"other";}
function precKey(p){p=(p||"").toLowerCase();return p.includes("float16")&&!p.includes("b")?"float16":p.includes("bfloat")?"bfloat16":(p||"other");}
const state={models:[],sort:"avg",dir:-1,q:"",types:new Set(),precs:new Set(),pmax:72};
async function getJSON(u){const r=await fetch(u);if(!r.ok)throw new Error(r.status);return r.json();}
async function loadDir(repo,version){
const tree=await getJSON(TREE(repo));
let files=tree.filter(f=>f.type==="file"&&f.path.endsWith(".json"));
if(version){
const pre=version.id+"/";
// A prefixed file is that version's. Un-prefixed files at the repo root predate
// versioning and belong to the legacy version only. Anything under some OTHER
// version's prefix is ignored — the tree is fetched recursively.
files=files.filter(f=>{
if(f.path.startsWith(pre))return true;
if(!version.legacy)return false;
return f.path.split("/").length===2&&!VERSIONS.some(v=>f.path.startsWith(v.id+"/"));
});
// Prefer the prefixed copy while a migration is only half-done, so a model that
// exists at both paths is not counted twice.
files.sort((a,b)=>(b.path.startsWith(pre)?1:0)-(a.path.startsWith(pre)?1:0));
}
const out=await Promise.all(files.map(async f=>{try{return await getJSON(FILE(repo,f.path));}catch(e){return null;}}));
const seen=new Set(),uniq=[];
for(const r of out.filter(Boolean)){
const k=r?.config?.model_name;
if(k&&seen.has(k))continue;
if(k)seen.add(k);
uniq.push(r);
}
return uniq;
}
function itemWeightedAvg(res){
let s=0,w=0;
for(const [k,,wt] of DIMS){const v=dimScore(res,k);if(v!==null){s+=v*wt;w+=wt;}}
return w?s/w:0;
}
async function init(){
DIMS=CURRENT.dims; KS_PARTS=CURRENT.ks; WSUM=DIMS.reduce((s,d)=>s+d[2],0);
// The corpus size belongs to the version: v7 is 34,137 items and v8 is 32,890.
const ic=document.getElementById("itemCount");
if(ic)ic.textContent=WSUM.toLocaleString("en-US");
const st=document.getElementById("state");
st.hidden=false; st.innerHTML='<div class="spinner"></div>Loading '+CURRENT.id+'…';
document.getElementById("board").hidden=true;
document.getElementById("controls").hidden=true;
document.getElementById("note").hidden=true;
let results,requests=[];
try{results=await loadDir(RESULTS,CURRENT);}
catch(e){st.textContent="⚠️ Could not load leaderboard data ("+e+").";return;}
// The queue/metadata is non-essential — if it fails, still render the board from results.
try{requests=await loadDir(REQUESTS);}catch(e){requests=[];}
// Private scores are NOT in the public dataset; the server hands them over only
// to the owner. Merged here so the maintainer sees their own hidden rows.
try{
const pr=await fetch('/api/private-results?version='+encodeURIComponent(CURRENT.id),{credentials:'same-origin'});
if(pr.ok){const pd=await pr.json();
if(Array.isArray(pd.results)&&pd.results.length)results=results.concat(pd.results);}
}catch(e){/* not the owner, or not signed in — public board only */}
// Owner-only extra columns (policy A / policy B / judged timestamp). Served from
// the PRIVATE dataset, so a non-owner gets 401 here and the columns never appear.
state.extra={};
try{
const em=await fetch('/api/private-metrics?version='+encodeURIComponent(CURRENT.id),{credentials:'same-origin'});
if(em.ok){const ed=await em.json(); state.extra=(ed&&ed.models)||{};}
}catch(e){/* not the owner — no extra columns */}
// the progress card cross-checks against these to see if a run is really live
state.requests=requests;
if(typeof loadProgress==='function')loadProgress();
// index request metadata by model name
const meta={};
for(const q of requests){if(q&&q.model)meta[q.model]={params:q.params,likes:q.likes,type:q.model_type,prec:q.precision,status:q.status};}
const KEYS=DIMS.map(d=>d[0]);
const V2_ONLY=["translation","information_extraction"];
const REQUIRED=KEYS.filter(k=>!V2_ONLY.includes(k));
const models=[];
for(const r of results){
const name=r?.config?.model_name;if(!name)continue;
const res=r.results||{};
// Models scored before the v2 corpus have no translation / information_extraction
// results. Requiring those would drop every pre-v2 row from the board, so they are
// exempt; itemWeightedAvg already renormalises over whichever dimensions exist.
if(!REQUIRED.every(k=>dimScore(res,k)!==null))continue;
const m=meta[name]||{};
models.push({
name, org:name.split("/")[0], short:name.split("/").slice(1).join("/")||name,
avg:itemWeightedAvg(res),
scores:Object.fromEntries(DIMS.map(d=>[d[0],dimScore(res,d[0])])),
params:typeof m.params==="number"?m.params:null,
likes:typeof m.likes==="number"?m.likes:null,
type:typeKey(m.type), prec:precKey(m.prec),
dtype:r?.config?.model_dtype,
polA:(state.extra&&state.extra[name])?state.extra[name].overall_accuracy*100:null,
polB:(state.extra&&state.extra[name])?state.extra[name].overall_strict*100:null,
mcqFmt:(state.extra&&state.extra[name])?state.extra[name].mcq_format_ok*100:null,
judged:(state.extra&&state.extra[name])?state.extra[name].judged_at:null,
isPrivate:r?._private===true
});
}
state.models=models;
if(!models.length){
st.hidden=false;
st.innerHTML='<div style="text-align:center;padding:26px 12px">'
+'<div style="font-size:15px;font-weight:600;margin-bottom:6px">No models evaluated on '+CURRENT.id+' yet.</div>'
+'<div class="muted" style="font-size:13px">Results land here as each model finishes. '
+'Use the version picker above to see an earlier board.</div></div>';
document.getElementById("board").hidden=true;
document.getElementById("tbody").innerHTML=""; // drop the previous version's rows
document.getElementById("modelCount").textContent="0";
document.getElementById("updated").textContent="0 models";
renderQueue(requests);
return;
}
// Params slider: default range 0–72B, but never hide a larger model — grow the cap to fit.
const maxP=Math.max(72,Math.ceil(Math.max(0,...models.map(m=>m.params||0))));
const slider=document.getElementById("paramMax");
slider.max=maxP; slider.value=maxP; state.pmax=maxP;
document.getElementById("paramRV").textContent=maxP;
document.getElementById("modelCount").textContent=models.length;
document.getElementById("updated").textContent=`${models.length} models`;
// build filter chips
const types=[...new Set(models.map(m=>m.type))];
const precs=[...new Set(models.map(m=>m.prec))];
state.types=new Set(types); state.precs=new Set(precs);
const tc=document.getElementById("typeChips"), pc=document.getElementById("precChips");
tc.innerHTML=""; pc.innerHTML=""; // init() re-runs on every version switch
types.forEach(t=>tc.appendChild(mkChip(t,(TYPE_ICON[t]?TYPE_ICON[t]+" ":"")+t,()=>toggle(state.types,t))));
precs.forEach(p=>pc.appendChild(mkChip(p,p,()=>toggle(state.precs,p))));
document.getElementById("controls").hidden=false;
document.getElementById("state").hidden=true;
document.getElementById("board").hidden=false;
document.getElementById("note").hidden=false;
renderQueue(requests);
render();
}
function mkChip(val,label,onclick){const b=document.createElement("button");b.className="chip";b.textContent=label;
b.setAttribute("aria-pressed","true");b.onclick=()=>{b.setAttribute("aria-pressed",b.getAttribute("aria-pressed")==="true"?"false":"true");onclick();render();};return b;}
function toggle(set,v){set.has(v)?set.delete(v):set.add(v);}
function heat(v){ // 0..100 -> subtle emerald tint + readable text
const t=Math.max(0,Math.min(1,v/100));
return `background:rgba(var(--cell-heat),${(0.05+t*0.30).toFixed(3)})`;
}
const BASE_HEADS=[["rank","#"],["model","Model"],["avg","Average"],...DIMS.map(d=>[d[0],d[1]])];
// Owner-only columns. `state.extra` is populated only when /api/private-metrics
// answered, which it does only for the leaderboard owner — so a public viewer
// sees exactly the columns they saw before.
const OWNER_HEADS=[["polA","A: Acc"],["polB","B: Strict"],["mcqFmt","MCQ Fmt"],["judged","Judged"]];
function heads(){
const hasExtra=state.extra&&Object.keys(state.extra).length;
return hasExtra?BASE_HEADS.concat(OWNER_HEADS):BASE_HEADS;
}
function extraCells(m){
if(!(state.extra&&Object.keys(state.extra).length))return "";
const pct=v=>v==null?'<span class="score na">n/a</span>':`<span class="score" style="${heat(v)}">${v.toFixed(2)}%</span>`;
const drop=(m.polA!=null&&m.polB!=null)?(m.polA-m.polB):null;
const bTitle=drop!=null?` title="${drop.toFixed(2)} points lost to instruction-following"`:"";
const ts=m.judged?String(m.judged).replace("T"," ").slice(0,16):"—";
return `<td>${pct(m.polA)}</td><td${bTitle}>${pct(m.polB)}</td>`
+ `<td>${pct(m.mcqFmt)}</td>`
+ `<td><span class="score na" style="font-size:11px;white-space:nowrap">${ts}</span></td>`;
}
function render(){
let rows=state.models.filter(m=>
(!state.q||m.name.toLowerCase().includes(state.q))&&
state.types.has(m.type)&&state.precs.has(m.prec)&&
(m.params==null||m.params<=state.pmax));
const k=state.sort,dir=state.dir;
rows.sort((a,b)=>{
let va,vb;
if(k==="model"){va=a.name.toLowerCase();vb=b.name.toLowerCase();return va<vb?-dir:va>vb?dir:0;}
if(k==="avg"){va=a.avg;vb=b.avg;}
else if(k==="rank"){va=a.avg;vb=b.avg;dir*1;}
else if(k==="judged"){va=a.judged||"";vb=b.judged||"";return va<vb?-dir:va>vb?dir:0;}
else if(k==="polA"||k==="polB"||k==="mcqFmt"){va=a[k];vb=b[k];}
else {va=a.scores[k];vb=b.scores[k];}
// Unscored cells sort last in both directions — (null-60) is NaN, which makes the
// comparator inconsistent and scrambles the order of every other row.
if(va==null&&vb==null)return 0;
if(va==null)return 1;
if(vb==null)return -1;
return (va-vb)*dir;
});
const thead=document.getElementById("thead");
const HEADS=heads();
thead.innerHTML="<tr>"+HEADS.map(([key,label])=>{
const active=(state.sort===key)||(key==="rank"&&state.sort==="avg");
const arrow=active?`<span class="arrow">${state.dir<0?"▼":"▲"}</span>`:"";
const cls=(key!=="rank"&&key!=="model"&&key!=="avg")?' class="sub"':"";
return `<th data-k="${key}"${cls}>${label}${arrow}</th>`;}).join("")+"</tr>";
thead.querySelectorAll("th").forEach(th=>th.onclick=()=>{
const key=th.dataset.k;
if(state.sort===key||(key==="rank"&&state.sort==="avg")){state.dir*=-1;}
else{state.sort=key==="rank"?"avg":key;state.dir=(key==="model")?1:-1;}
render();
});
const tb=document.getElementById("tbody");
// Best score in each task column across the shown models — highlight the leader(s).
const best={};
if(rows.length>1){for(const d of DIMS){const vs=rows.map(m=>m.scores[d[0]]).filter(v=>v!=null);
best[d[0]]=vs.length?Math.max(...vs):null;}}
tb.innerHTML=rows.map((m,i)=>{
const rank=i+1;
const rk=rank<=3?`<span class="rank top r${rank}">${rank}</span>`:`<span class="rank">${rank}</span>`;
const cats=DIMS.map(d=>{const v=m.scores[d[0]];
// Not every model has every dimension: anything scored before the v2 corpus has
// no translation / information_extraction result. Show "n/a" rather than a made-up
// number — and never call .toFixed on it, which is what emptied the whole board.
if(v==null)return `<td><span class="score na" title="Not evaluated on this task (scored before the v2 corpus)">n/a</span></td>`;
const isBest=(best[d[0]]!=null)&&(best[d[0]]>0)&&(best[d[0]]-v<0.05);
return `<td><span class="score${isBest?' best':''}" style="${heat(v)}"${isBest?' title="Best in this task"':''}>${v.toFixed(1)}</span></td>`;}).join("");
return `<tr>
<td>${rk}</td>
<td><div class="model-cell">
<a class="name" href="https://huggingface.co/${m.name}" target="_blank" rel="noopener" title="${m.name}"><span class="type-ico">${TYPE_ICON[m.type]||"·"}</span>${m.short}</a>${m.isPrivate?'<span class="badge b-private" title="Private — only you can see this row">PRIVATE</span>':""}
<span class="org">${m.org}${m.params!=null?` · ${m.params.toFixed(1)}B`:""}${m.prec?` · ${m.prec}`:""}${m.likes?` · ♥ ${m.likes}`:""}</span>
</div></td>
<td class="avg-cell"><span class="avg-num">${m.avg.toFixed(2)}%</span><div class="avg-bar"><i style="width:${Math.min(100,m.avg)}%"></i></div></td>
${cats}${extraCells(m)}</tr>`;
}).join("")||`<tr><td colspan="${HEADS.length}" class="state" style="padding:40px">No models match the filters.</td></tr>`;
}
function renderQueue(requests){
const q=document.getElementById("queue");
const active=requests.filter(r=>r&&["PENDING","RUNNING","RERUN","FAILED","REJECTED"].includes((r.status||"").toUpperCase()));
if(!active.length){
const c0=document.getElementById("queueCount"); if(c0)c0.textContent="";
q.innerHTML='<div class="qrow" style="color:var(--ink-mute)">No pending or running evaluations.</div>';return;}
const order={RUNNING:0,QUEUED:1,PENDING:1,RERUN:1,FAILED:2,REJECTED:3};
// Within a status group, order by submitted_time — that is precisely how the
// worker chooses its next job (auto_hf_queue.list_requests sorts oldest-first
// and next_todo takes todo[0]), so the panel reads as the real run order.
// Without this tiebreak every PENDING row ties and the list falls back to
// repo file order, which is alphabetical and tells you nothing.
active.sort((a,b)=>(order[a.status?.toUpperCase()]??3)-(order[b.status?.toUpperCase()]??3)
||String(a.submitted_time||"").localeCompare(String(b.submitted_time||"")));
const cnt=document.getElementById("queueCount");
if(cnt)cnt.textContent=`${active.length} in queue`;
// No cap: truncating here once hid 5 of 17 queued models and read as "they
// were never queued". A long list is better than a silently short one.
q.innerHTML=active.map((r,i)=>{
let s=(r.status||"").toUpperCase();
// The worker marks every model it has claimed as RUNNING, but it works
// through them one at a time. Only the model the progress feed names is
// really running; the rest are waiting their turn, so say so.
if(s==="RUNNING"&&state.activeModel){
const m=r.model||"";
const isActive=(m===state.activeModel)||m.endsWith("/"+state.activeModel);
if(!isActive)s="QUEUED";
}
const cls=s==="RUNNING"?"b-running":(s==="FAILED"||s==="REJECTED")?"b-failed":"b-pending";
const tip=s==="REJECTED"?' title="Closed-source models may only be submitted by the leaderboard maintainer."':"";
return `<div class="qrow"><span class="qnum">${i+1}</span><a href="https://huggingface.co/${r.model}" target="_blank" rel="noopener" title="${r.model}">${r.model}</a><span class="badge ${cls}"${tip}>${s}</span></div>`;
}).join("");
}
// controls
document.getElementById("search").addEventListener("input",e=>{state.q=e.target.value.trim().toLowerCase();render();});
const pm=document.getElementById("paramMax");
pm.addEventListener("input",e=>{state.pmax=+e.target.value;document.getElementById("paramRV").textContent=e.target.value;render();});
// theme toggle (persist)
const root=document.documentElement, tbtn=document.getElementById("themeBtn");
const saved=localStorage.getItem("all-theme"); if(saved)root.setAttribute("data-theme",saved);
else if(window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches)root.setAttribute("data-theme","light");
tbtn.onclick=()=>{const n=root.getAttribute("data-theme")==="dark"?"light":"dark";root.setAttribute("data-theme",n);localStorage.setItem("all-theme",n);};
// ---------------- submission + Hugging Face sign-in ----------------
const modal=document.getElementById('submitModal');
const signinView=document.getElementById('signinView');
const submitForm=document.getElementById('submitForm');
const submitMsg=document.getElementById('submitMsg');
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function setUser(u,isOwner){
const who=document.getElementById('whoami');
const whoM=document.getElementById('whoamiModal');
if(u){
who.innerHTML=`Signed in as <b>@${esc(u)}</b> · <a href="/logout">sign out</a>`;
whoM.innerHTML=`Signed in as <b>@${esc(u)}</b>`;
signinView.hidden=true; submitForm.hidden=false;
// Owner-only: reveal the closed-source option. Cosmetic — /api/submit enforces it.
const sf=document.getElementById('sourceField');
if(sf){sf.hidden=!isOwner; if(!isOwner)document.getElementById('f-source').value='open';}
for(const id of ['visField','maxField','subtaskField']){
const el=document.getElementById(id); if(el)el.hidden=!isOwner;
}
// Signing in after page load must reveal the panel without a hard reload.
if(isOwner&&typeof loadManage==='function')loadManage();
if(!isOwner){
const vs=document.getElementById('f-vis'); if(vs)vs.value='public';
const mx=document.getElementById('f-max'); if(mx)mx.value='';
const st=document.getElementById('f-subtasks');
if(st)[...st.options].forEach(o=>o.selected=false);
}
}else{
who.textContent=''; whoM.textContent='';
for(const id of ['sourceField','visField','maxField','subtaskField']){
const el0=document.getElementById(id); if(el0)el0.hidden=true;
}
const mp=document.getElementById('managePanel'); if(mp)mp.hidden=true;
signinView.hidden=false; submitForm.hidden=true;
}
}
async function refreshMe(){
try{const r=await fetch('/api/me',{credentials:'same-origin'});const d=await r.json();setUser(d.user||null,!!d.is_owner);}
catch(e){setUser(null,false);}
}
function smsg(t,kind){submitMsg.className='msg'+(t?(' show '+kind):'');submitMsg.textContent=t||'';}
function openModal(){modal.classList.add('open');modal.setAttribute('aria-hidden','false');smsg('');refreshMe();}
function closeModal(){modal.classList.remove('open');modal.setAttribute('aria-hidden','true');smsg('');}
document.getElementById('openSubmit').onclick=openModal;
document.getElementById('closeSubmit').onclick=closeModal;
document.getElementById('cancelSubmit').onclick=closeModal;
modal.addEventListener('click',e=>{if(e.target===modal)closeModal();});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&modal.classList.contains('open'))closeModal();});
document.getElementById('hfLogin').onclick=()=>{
// If embedded in an iframe (huggingface.co Space page), open sign-in in a new tab so
// the HF authorize page loads top-level and cookies work; otherwise redirect in place.
if(window.top!==window.self){window.open('/login','_blank','noopener');}
else{window.location.href='/login';}
};
// pick up a sign-in that completed in another tab / after returning
window.addEventListener('focus',()=>{if(modal.classList.contains('open'))refreshMe();});
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&modal.classList.contains('open'))refreshMe();});
submitForm.addEventListener('submit',async e=>{
e.preventDefault();
const btn=document.getElementById('doSubmit');
const payload={
model:document.getElementById('f-model').value.trim(),
model_type:document.getElementById('f-type').value,
precision:document.getElementById('f-prec').value,
revision:(document.getElementById('f-rev').value.trim()||'main'),
weight_type:document.getElementById('f-weight').value,
source:(document.getElementById('f-source')||{value:'open'}).value,
};
// Owner-only extras: omitted entirely when blank/unselected, so a normal
// submission sends exactly what it sent before this feature existed.
const vsEl=document.getElementById('f-vis');
if(vsEl&&vsEl.value==='private')payload.visibility='private';
const mxEl=document.getElementById('f-max');
if(mxEl&&mxEl.value.trim()!=='')payload.max_examples=parseInt(mxEl.value,10);
const stEl=document.getElementById('f-subtasks');
if(stEl){
const picked=[...stEl.selectedOptions].map(o=>o.value);
if(picked.length)payload.subtasks=picked;
}
if(!/^[\w.-]+\/[\w.-]+$/.test(payload.model)){smsg('Enter a model id like org/name.','err');return;}
btn.disabled=true;btn.textContent='Submitting…';smsg('');
try{
const r=await fetch('/api/submit',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const d=await r.json().catch(()=>({}));
if(r.ok){
smsg(d.message||'Submitted!','ok');
document.getElementById('f-model').value='';
setTimeout(()=>{closeModal();location.reload();},2000);
}else if(r.status===401){smsg('Please sign in with Hugging Face first.','err');refreshMe();}
else{smsg(d.detail||d.message||('Error '+r.status),'err');}
}catch(err){smsg('Network error — please try again.','err');}
finally{btn.disabled=false;btn.textContent='Submit for evaluation';}
});
// ---------------- Live progress ----------------
// Published by the worker to a small dataset (one commit only when the numbers
// change), so the board can show how far the running evaluation has got.
const PROGRESS_URL="https://huggingface.co/datasets/Mushari440/progress/raw/main/progress.json";
// Remembers when the run last made progress, so a frozen run can be called
// stalled. It must track BOTH counters: during the judge phase `inferred` is
// frozen by design (inference finished), and watching it alone reported a
// healthy judging run as "no new answers for 15 min — STALLED".
let _prog={model:null,inferred:-1,scored:-1,since:0};
async function loadProgress(){
const el=document.getElementById('progress');
try{
const r=await fetch(PROGRESS_URL+"?t="+Date.now(),{cache:"no-store"});
if(!r.ok){el.hidden=true;return;}
const p=await r.json();
// Which model the worker is actually on. The worker claims every queued model
// as RUNNING up front but works through them one at a time, so without this
// the queue showed three simultaneous RUNNING rows. Recorded before the
// liveness gates below: even a stale feed names the one job that was active,
// which beats implying all of them are.
if(p&&p.model){
const wasActive=state.activeModel;
state.activeModel=p.model;
if(wasActive!==state.activeModel&&state.requests)renderQueue(state.requests);
}
if(!p||!p.model||p.idle){el.hidden=true;return;} // nothing running
// Freshness of the FILE is not liveness of the RUN. The worker republishes every
// 60s regardless, so a killed run stayed "fresh" forever: a stopped ox-alpha sat
// here reading "INFERENCE 16,177 / 31,925" while the queue underneath correctly
// showed FAILED. Three checks now, in order of authority.
const age=(Date.now()-Date.parse(p.updated_at||0))/1000;
if(!isFinite(age)||age>900){el.hidden=true;return;}
// 1. The worker says so outright (published once the fix is running).
if(p.live===false){el.hidden=true;return;}
// 2. The queue is the authority on whether this model is being worked on.
// progress carries the run NAME ("ox-alpha"); the queue carries the model id
// ("stealth/ox-alpha"), so match on either.
const ACTIVE_ST=new Set(['RUNNING','PENDING','RERUN']);
// The queue has not loaded yet on the first pass -- loadProgress runs before
// init() finishes. Painting now would show an unverified card for up to a full
// poll interval, which is exactly how the dead ox-alpha run stayed on screen.
// init() calls this again the moment the queue is in.
if(!state.requests){el.hidden=true;return;}
const q=(state.requests||[]).find(r=>{
const m=r&&r.model; if(!m)return false;
return m===p.model||m.endsWith('/'+p.model);
});
if(q&&!ACTIVE_ST.has(String(q.status||'').toUpperCase())){el.hidden=true;return;}
// 3. Nothing has been answered for a while -> say stalled rather than "INFERENCE".
// ox-alpha sat rate-limited at 429 for hours still captioned as running.
const now=Date.now();
if(_prog.model!==p.model||_prog.inferred!==p.inferred||_prog.scored!==p.scored){
_prog={model:p.model,inferred:p.inferred,scored:p.scored,since:now};
}
const stalledFor=(now-_prog.since)/1000;
const stalled=stalledFor>600;
const stalledWhat=String(p.phase||"").toUpperCase().includes("JUDGE")?"judgements":"answers";
const pct=p.total?Math.min(100,100*p.inferred/p.total):0;
const spct=p.total?Math.min(100,100*p.scored/p.total):0;
el.hidden=false;
el.innerHTML=`<div class="prog">
<div class="ptop">
<span class="pmodel">${esc(p.model)}</span>
<span class="pphase"${stalled?' style="background:rgba(224,102,102,.18);color:#e06666"':''}>${
stalled?'STALLED':esc(p.phase||'')}</span>
${p.max_per_subtask?`<span class="pphase" style="background:rgba(201,154,63,.16);color:var(--gold)">MAX ${p.max_per_subtask}/subtask</span>`:''}
<span class="pnums">${(p.inferred||0).toLocaleString()} / ${(p.total||0).toLocaleString()} answered · ${(p.scored||0).toLocaleString()} judged</span>
</div>
<div class="pbar"><i style="width:${pct.toFixed(1)}%"></i></div>
<div class="pmeta">subtasks ${p.subtasks_done}/${p.subtasks_total} done · ${p.subtasks_active} running · ${p.subtasks_left} not started · judged ${spct.toFixed(0)}%</div>
${p.current?`<div class="pmeta">now: ${esc(p.current)}${p.current_done}/${p.current_total}</div>`:''}
${stalled?`<div class="pmeta" style="color:#e06666">no new ${stalledWhat} for ${Math.round(stalledFor/60)} min — the run may be rate-limited or stopped</div>`:''}
</div>`;
}catch(e){el.hidden=true;}
}
loadProgress(); setInterval(loadProgress,30000);
// ---------------- Manage panel (owner only) ----------------
function mmsg(t,kind){const e=document.getElementById('manageMsg');
e.className='msg'+(t?(' show '+kind):'');e.textContent=t||'';}
// loadManage runs on page load, on sign-in and on every version switch, so several
// can be in flight at once. Without a guard the last response to arrive wins even if
// it is the oldest, which left the panel showing one version's counts while its
// delete buttons named another. Only the newest call may paint.
let _manageSeq=0;
async function loadManage(){
const seq=++_manageSeq;
const panel=document.getElementById('managePanel');
const list=document.getElementById('manageList');
try{
const want=CURRENT.id;
// /api/manage lists three repos and reads every request file, so it takes a good
// few seconds. Until it lands the rows on screen still belong to the version we
// just left, and their buttons are labelled and wired for it -- a "Delete v7
// score" button sitting live on the v8 panel. Say which version is loading and
// freeze the controls until the new rows replace them.
const bar=document.getElementById('mbarNote');
if(bar)bar.textContent='loading '+want+'…';
for(const b of [document.getElementById('btnQueueMissing'),
document.getElementById('btnQueueAll'),
...list.querySelectorAll('button[data-act]')]){
if(b)b.disabled=true;
}
const r=await fetch('/api/manage?version='+encodeURIComponent(want),{credentials:'same-origin'});
if(seq!==_manageSeq)return; // a newer load started while we waited
if(!r.ok){
// 401/403 = not signed in, or not the maintainer. Hide rather than explain:
// this panel simply does not exist for anyone else.
panel.hidden=true;
list.innerHTML='<div class="mrow" style="color:var(--ink-mute)">Sign in as the maintainer to manage models.</div>';
return;
}
const d=await r.json();
if(seq!==_manageSeq)return;
panel.hidden=false;
const models=Array.isArray(d.models)?d.models:[];
const versions=Array.isArray(d.versions)&&d.versions.length?d.versions:VERSIONS.map(v=>v.id);
document.getElementById('manageCount').textContent=`${models.length} submitted`;
const missing=models.length-models.filter(m=>((m.results_by_version||{})[CURRENT.id]||[]).length).length;
document.getElementById('manageBar').hidden=false;
document.getElementById('mbarVer').textContent='managing '+CURRENT.id;
document.getElementById('mbarNote').textContent=
`${models.length-missing} scored on ${CURRENT.id}, ${missing} missing`;
if(!models.length){list.innerHTML='<div class="mrow" style="color:var(--ink-mute)">Nothing submitted yet.</div>';return;}
list.innerHTML=models.map(m=>{
const s=(m.status||'').toUpperCase();
const cls=s==='RUNNING'?'b-running':(s==='FAILED'||s==='REJECTED')?'b-failed':'b-pending';
const tags=[`<span class="badge ${cls}">${s}</span>`];
if(m.visibility==='private')tags.push('<span class="badge b-private">PRIVATE</span>');
if(m.source==='closed')tags.push('<span class="badge b-pending">API</span>');
// Where this model actually has a score. The status badge above is global —
// one request serves every version — so it says nothing about which board the
// model is on. These chips do.
const byv=m.results_by_version||{};
const chips=versions.map(v=>{
const on=Array.isArray(byv[v])&&byv[v].length;
const priv=on&&byv[v].includes('private')&&!byv[v].includes('public');
return `<span class="vchip${on?' on':''}${v===CURRENT.id?' here':''}" title="${
on?`scored on ${v}${priv?' (private)':''}`:`no score on ${v}`}">${v}${on?(priv?' ●':' ✓'):' —'}</span>`;
}).join('');
const hasHere=Array.isArray(byv[CURRENT.id])&&byv[CURRENT.id].length;
return `<div class="mrow" data-model="${esc(m.model)}">
<span class="mname" title="${esc(m.model)}">${esc(m.model)}</span>
${tags.join('')}${chips}
<button class="mbtn" data-act="rerun">Re-run</button>
<button class="mbtn danger" data-act="delete"${hasHere?'':' disabled title="no score on this version"'}>Delete ${esc(CURRENT.id)} score</button>
<button class="mbtn danger" data-act="purge">Remove entirely</button>
</div>`;}).join('');
list.querySelectorAll('button[data-act]').forEach(b=>b.onclick=()=>manageAct(b));
const bm=document.getElementById('btnQueueMissing'), ba=document.getElementById('btnQueueAll');
bm.textContent=`Queue ${missing} missing on ${CURRENT.id}`;
bm.disabled=!missing;
bm.onclick=()=>requeue(true,bm);
ba.onclick=()=>requeue(false,ba);
}catch(e){
panel.hidden=true;
list.innerHTML=`<div class="mrow" style="color:var(--ink-mute)">Could not load (${esc(String(e))}).</div>`;
}
}
async function manageAct(btn){
const row=btn.closest('.mrow'), model=row.dataset.model, act=btn.dataset.act;
const label=btn.textContent;
// 'delete' drops only the score on the version being viewed. 'purge' removes the
// queue entry and every version's score — the request is shared across versions,
// so purging while looking at v8 also takes the v7 row down.
if(act==='delete'&&!confirm(
`Remove ${model}'s ${CURRENT.id} score?\n\nIts queue entry stays, so it can be evaluated again. Scores on other versions are untouched.`))return;
if(act==='purge'&&!confirm(
`Remove ${model} entirely?\n\nThis deletes its queue entry AND its score on EVERY version, ${CURRENT.id} included. It cannot be undone — only a fresh submission would bring it back.`))return;
row.querySelectorAll('button').forEach(b=>b.disabled=true);
btn.textContent=act==='rerun'?'Queuing…':'Removing…';
const url=act==='rerun'?'/api/model/rerun':'/api/model/delete';
const body={model,version:CURRENT.id};
if(act!=='rerun')body.scope=(act==='purge')?'all':'version';
try{
const r=await fetch(url,{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await r.json().catch(()=>({}));
if(r.ok){mmsg(d.message||'Done.','ok');await loadManage();
if(act!=='rerun')setTimeout(()=>location.reload(),1200);}
else{mmsg(d.detail||d.message||('Error '+r.status),'err');
row.querySelectorAll('button').forEach(b=>b.disabled=false);
btn.textContent=label;}
}catch(e){mmsg('Network error — please try again.','err');
row.querySelectorAll('button').forEach(b=>b.disabled=false);
btn.textContent=label;}
}
async function requeue(onlyMissing,btn){
const what=onlyMissing?`every model with no ${CURRENT.id} score`:`ALL models, including the ${CURRENT.id} ones already scored`;
if(!confirm(`Queue ${what} for re-evaluation?\n\nOnly finished and failed models are queued. Anything HELD, CANCELLED, SUPERSEDED or REJECTED is left exactly as it is.\n\nThey run on whichever version the worker has as its RUN_ID — make sure that is ${CURRENT.id} before you start it.`))return;
const label=btn.textContent; btn.disabled=true; btn.textContent='Queuing…';
try{
const r=await fetch('/api/requeue-all',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({version:CURRENT.id,only_missing:onlyMissing})});
const d=await r.json().catch(()=>({}));
mmsg(r.ok?(d.message||'Queued.'):(d.detail||d.message||('Error '+r.status)),r.ok?'ok':'err');
if(r.ok)await loadManage();
}catch(e){mmsg('Network error — please try again.','err');}
btn.disabled=false; btn.textContent=label;
}
// show the pinned-column edge shadow only while the table is scrolled right
const boardScroll=document.querySelector('.board-scroll');
if(boardScroll)boardScroll.addEventListener('scroll',
()=>boardScroll.classList.toggle('scrolled',boardScroll.scrollLeft>2),{passive:true});
// Windows mouse-wheel support: a plain vertical scroll wheel (no horizontal wheel on most PC
// mice) is translated into horizontal table scrolling. Trackpads send a horizontal component
// (deltaX) or small pixel deltas and are left to native handling, so Mac / precision-touchpad
// behaviour is unchanged. At the table's left/right edge we release to normal page scrolling.
if(boardScroll)boardScroll.addEventListener('wheel',e=>{
const el=boardScroll, max=el.scrollWidth-el.clientWidth;
if(max<=0) return; // nothing to scroll sideways
if(e.deltaX!==0) return; // already horizontal (trackpad / shift+wheel)
const isMouseWheel = e.deltaMode===1 || Math.abs(e.deltaY)>=50;
if(!isMouseWheel) return; // small pixel deltas = trackpad vertical → page
const dy = e.deltaMode===1 ? e.deltaY*40 : e.deltaY; // scale line-mode (Firefox) to pixels
if((dy<0 && el.scrollLeft<=0) || (dy>0 && el.scrollLeft>=max)) return; // at edge → let page scroll
el.scrollLeft = Math.max(0, Math.min(max, el.scrollLeft+dy));
e.preventDefault();
},{passive:false});
refreshMe();
if(new URLSearchParams(location.search).get('signedin')) openModal();
function initVersionPicker(){
const sel=document.getElementById("verSel");
for(const v of VERSIONS){
const o=document.createElement("option");o.value=v.id;o.textContent=v.label;sel.appendChild(o);
}
// URL wins over the remembered choice, so a shared #v=v8 link opens on v8.
const fromHash=(location.hash.match(/(?:^|[#&])v=([^&]+)/)||[])[1];
let want=fromHash;
if(!want){try{want=localStorage.getItem("benchVersion");}catch(e){}}
CURRENT=versionCfg(want||DEFAULT_VERSION);
sel.value=CURRENT.id;
document.getElementById("verNote").textContent=
CURRENT.legacy?"the published board":"a separate board — results appear as models finish";
sel.addEventListener("change",()=>{
CURRENT=versionCfg(sel.value);
try{localStorage.setItem("benchVersion",CURRENT.id);}catch(e){}
history.replaceState(null,"","#v="+CURRENT.id);
document.getElementById("verNote").textContent=
CURRENT.legacy?"the published board":"a separate board — results appear as models finish";
init();
// The manage panel is scoped to the selected version -- its counts, its queue
// button and its delete button all name it. Without this it kept the version it
// was built with, so switching to v8 left every control still acting on v7.
if(typeof loadManage==='function')loadManage();
});
}
initVersionPicker(); // resolves CURRENT from #v= / localStorage before anything reads it
init();
loadManage(); // must follow initVersionPicker: it is scoped to CURRENT
</script>
</body>
</html>