Spaces:
Running
Running
| <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 & domain dimensions — every benchmark item counts equally, so a small bucket never outweighs a large one. <b>Knowledge & 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">🤗 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/⌘-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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> | |