leaderboard / main.py
LennartPurucker's picture
Overview table gets a row per variant, and its cost columns follow the row
17d9c9b
Raw
History Blame Contribute Delete
93.4 kB
from __future__ import annotations
import gradio as gr
import json
from api import register_api
from data_loading import (
AXIS_NOTES,
CATEGORY_COMING_SOON,
DATA_DIR,
DATASET_LABELS,
DATASET_NOTES,
PROTOCOL_NOTES,
SYSTEM_CATEGORY_LABELS,
SYSTEM_CATEGORY_NOTES,
TASK_LABELS,
TASK_NOTES,
available_categories,
)
from pages import PAGES, render_page
from views import CARE_LABELS, CARE_NOTES, METRIC_LABELS, METRIC_NOTES
from website_texts import COI_HTML, TITLE
def _choice_hints() -> dict[str, str]:
"""Control label -> the hover text explaining it.
Keyed by the rendered label, because that is all the DOM gives `taStampTitles` to match on.
Covers the chip bars and the checkboxes alike: Gradio's own ``info`` renders as standing text
under the control and its markup shifts between versions, so nothing here uses it.
Built from the same dicts the controls are built from, so a new choice cannot end up without
an explanation by accident.
"""
hints: dict[str, str] = {}
for labels, notes in (
(TASK_LABELS, TASK_NOTES),
(DATASET_LABELS, DATASET_NOTES),
(CARE_LABELS, CARE_NOTES),
(METRIC_LABELS, METRIC_NOTES),
):
for key, label in labels.items():
if key in notes:
hints[label] = notes[key]
hints["🤖 Models"] = "Always on: TabArena is a model benchmark first."
hints["One per model"] = (
"Keep only each model's best-performing variant, one row per model — as the win-rate "
"matrix does. With it off, every variant gets its own row, so you can read what "
"tuning bought."
)
hints["Include imputed models"] = PROTOCOL_NOTES["imputed"]
hints["TabArena-Lite (single split)"] = PROTOCOL_NOTES["lite"]
# A category with no entrant yet says so, matching the disabled state the page renders.
available = available_categories(str(DATA_DIR))
for key, label in SYSTEM_CATEGORY_LABELS.items():
hints[label] = SYSTEM_CATEGORY_NOTES[key] if key in available else CATEGORY_COMING_SOON
return hints
CSS = """
/* Use the full page width instead of gradio's narrow centered column. */
.gradio-container { max-width: 100% !important; }
/* Leave room for the fixed selection bar (.ta-selbar sits 18px off the bottom and is ~42px
tall). Without this the page still scrolls to its end, but the end sits underneath the bar,
which reads as not being able to get to the bottom. */
.gradio-container { padding-bottom: 78px !important; }
/* And room at the top for the two corner chips, which are fixed and so reserve none of their
own. They sit side by side on the left and reach far enough across to meet the title, which
otherwise starts underneath them at every width. */
.gradio-container { padding-top: 46px !important; }
/* --- Colour emoji ----------------------------------------------------------
Gradio's own font stack ends "… "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"" — the colour emoji
fonts sit *after* the generic `sans-serif`, which matches everything, so the
browser never reaches them and resolves emoji through its own fallback
instead. On Linux that commonly lands on the monochrome Noto Emoji, which is
why 🧠⚡ / 🤖 / 🥇 render flat there and in colour on macOS and Windows.
Same stack in Gradio 5, so this is long-standing rather than new.
Naming the colour fonts before the generic fixes it for every emoji on the
page; Latin glyphs are unaffected because those fonts carry none.
Overriding the two variables rather than `font-family` directly is what makes
it stick for Gradio's components: its own rules set `font-family: var(--font)` /
`var(--font-sans)` on individual elements, which would beat a container-level
declaration. Raw HTML (`gr.HTML`) has no such rule and is handled by the
element-level block below. */
:root, .gradio-container, html body {
--font: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans",
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
--font-sans: var(--font);
font-family: var(--font);
}
/* A colour-emoji family restricted to emoji codepoints.
The emoji fonts have to lead the stack below to win over the browser's own
fallback, but they must not serve anything else: Noto Color Emoji carries a
SPACE glyph 1.25 em wide (2550/2048 units, i.e. emoji-width, against ~0.2 em
in a text font), so leading with the raw family names stretched every gap
between words wherever that font is installed — Latin letters fell through to
IBM Plex Sans, but U+0020 did not, because the emoji font *does* have it.
Binding the fonts through `unicode-range` keeps them first for emoji while
spaces and Latin resolve from the text font. */
@font-face {
font-family: "TA Color Emoji";
src: local("Apple Color Emoji"), local("Segoe UI Emoji"),
local("Noto Color Emoji"), local("Twemoji Mozilla");
unicode-range: U+00A9, U+00AE, U+203C, U+2049, U+2122, U+2139, U+2194-21AA,
U+231A-231B, U+2328, U+23CF, U+23E9-23FA, U+24C2, U+25AA-25FE,
U+2600-27BF, U+2934-2935, U+2B00-2BFF, U+3030, U+303D, U+3297, U+3299,
U+200D, U+20E3, U+FE0E-FE0F, U+1F000-1FAFF, U+E0020-E007F;
}
/* Overriding the variables above fixes Gradio's own components (buttons, tabs),
which resolve `font-family: var(--font)` per element. It does not reach content
inside `gr.HTML`, which has no per-element rule of its own — so the elements we
write by hand are listed here and get the colour fonts forced.
The emoji family leads and `!important` settles the cascade; every other
character, space included, resolves from IBM Plex Sans because the family
above answers only for emoji codepoints. Add a selector here if an emoji
shows up flat somewhere new. */
.ta-card-ico,
.ta-path-ico,
.ta-pill,
.ta-lb-title,
.ta-figbar-title,
.ta-section-head h2,
.ta-jump,
.ta-viewbtn,
.coi-badge,
.ta-legend,
.ta-lbtable td.ta-type-cell,
.ta-verified,
.ta-link-icon,
.ta-cap,
.coi-cta,
.markdown-text h1, .markdown-text h2, .markdown-text h3, .markdown-text h4 {
font-family: "TA Color Emoji", "IBM Plex Sans", ui-sans-serif, system-ui,
"Segoe UI", Roboto, sans-serif !important;
}
/* Gradio 6 pads every anchor inside a gr.HTML / gr.Markdown block:
.gradio-container-6-22-0 .gradio-style a { padding: 2px 8px; text-align: center }
That is a link-button style, and it lands on inline links too, which is where the
gaps around "(see paper)" / "(see code)" in the hero cards came from. Reset for
the links we write as running text — they are the ones with no class of their own
— at a specificity that beats Gradio's two-class selector whatever the stylesheet
order turns out to be. The pill-shaped links keep their own padding (below, where
the same Gradio rule is why they have to say `!important`). */
.gradio-container .gradio-style a:not([class]) { padding: 0; }
.markdown-text-box {
padding: 4px;
border-radius: 2px;
}
/* Intro tagline as a full-width card inside the hero group, above the stat boxes.
Tinted with the app accent (blue) so it stands out from the neutral stat cards. */
.ta-intro {
flex: 1 1 100%;
padding: 11px 16px;
border: 1px solid rgba(110, 140, 245, 0.5);
border-radius: 10px;
background: linear-gradient(135deg, rgba(110, 140, 245, 0.22), rgba(110, 140, 245, 0.07));
text-align: center;
font-size: 1.05em;
font-weight: 600;
line-height: 1.4;
color: #e6ebff;
}
/* --- Subset filter rows ----------------------------------------------------
The two content axes (task, dataset size) and the two view modifiers are one
control cluster, so they are drawn as three rows of the same panel: same frame,
same height, and a small uppercase label on the left saying which axis the row
switches. Before this the tab bars were bare text above a bordered toggle box,
which read as two unrelated controls.
Gradio 6 renders a Tabs as .tabs > .tab-wrapper > .tab-container > button and
marks the active tab .selected, styling it with a 2px underline. Here the row
frame lives on .tab-wrapper and the choices are flat chips with the active one
filled. Note .tab-wrapper holds a *second*, visually hidden .tab-container that
Gradio measures to decide which tabs go into its overflow menu, which is why the
chip rules deliberately hit every .tab-container: styling only the visible copy
would make it mis-measure. */
.tab-buttons {
gap: 0 !important;
/* Cancel the column gap the surrounding Blocks layout adds between the rows,
then set the row spacing here. */
margin: 0 0 calc(5px - var(--layout-gap)) !important;
}
.tab-buttons .tab-wrapper {
height: auto;
align-items: center;
margin-bottom: 0;
padding: 5px 12px 5px 14px;
background: #ffffff0a;
border: 1px solid #ffffff26;
border-radius: 10px;
}
/* The rule Gradio draws under a tab bar; the row frame replaces it. */
.tab-buttons .tab-container::after { display: none; }
.tab-buttons .tab-container { gap: 3px; height: auto; }
.tab-buttons .tab-container > button {
/* One neutral slate, and how strongly a chip is tinted says how deep in the axis
it sits: the row's "all" chip is the whole set, the next level is a subset of
it, and the lightest chips are subsets of a subset. Peers share a step, so
nothing in the row reads as ranked above anything else — Small and Medium are
the same tint, and so are Binary and Multiclass. Selection is the one place a
colour changes hue, and it is the app accent, the same as everywhere else. */
--depth: 0.62;
height: auto;
padding: 5px 12px;
font-size: 0.86em;
font-weight: 600;
color: #cbd5ef;
background: rgba(158, 176, 208, calc(0.3 * var(--depth)));
border: none;
border-radius: 8px;
transition: background .14s ease, color .14s ease;
}
.tab-buttons .tab-container > button:hover:not(.selected) {
background: rgba(158, 176, 208, calc(0.62 * var(--depth)));
color: #ffffff;
}
.tab-buttons .tab-container > button.selected {
color: #ffffff;
background: rgba(110, 140, 245, 0.92);
box-shadow: 0 1px 3px #0000005c;
}
/* The depth steps. Gradio gives the tabs no per-tab class, so they are addressed by
position — the nth-child order is the insertion order of TASK_LABELS /
DATASET_LABELS: all, classification, regression, binary, multiclass and all,
small, medium. Chips 4 and 5 of the task row are the two classification
flavours, one level further in than Classification itself. */
.axis-tasks .tab-container > button:nth-child(1),
.axis-datasets .tab-container > button:nth-child(1) { --depth: 1; }
.axis-tasks .tab-container > button:nth-child(n+4) { --depth: 0.34; }
/* The filled chip is the selection cue; Gradio's underline would sit inside it. */
.tab-buttons .tab-container > button.selected::after { display: none; }
/* The row labels: one style, one width, so the three rows line up on the left. */
.axis-tasks .tab-wrapper::before,
.axis-datasets .tab-wrapper::before,
.axis-care .tab-wrapper::before,
.axis-metric .tab-wrapper::before,
.view-toggles::before {
flex: 0 0 auto;
min-width: 74px;
font-size: 0.68em;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.5;
white-space: nowrap;
align-self: center;
}
.axis-tasks .tab-wrapper::before { content: "Task"; }
.axis-datasets .tab-wrapper::before { content: "Datasets"; }
.axis-care .tab-wrapper::before { content: "🎯 I care about"; }
.axis-metric .tab-wrapper::before { content: "Measured by"; }
/* Third row of the same panel: the view modifiers (imputation / Lite). */
.view-toggles {
margin: 0 0 16px 0;
padding: 7px 12px 7px 14px;
gap: 14px;
align-items: center;
background: #ffffff0a;
border: 1px solid #ffffff26;
border-radius: 10px;
}
.view-toggles::before { content: "View"; }
.view-toggles label span {
font-size: 0.86em !important;
}
/* --- The control band at the top of the page -------------------------------
"Who's competing?" and "I care about". These sit above everything because
they change every number below them: each entrant pool is its own evaluation
(see views/pages), not a filter over a shared table. Styled heavier than the
view toggles for that reason. */
.ta-controls {
margin: 4px 0 10px 0;
padding: 8px 14px;
gap: 0 !important;
background: #ffffff10;
border: 1px solid #ffffff33;
border-left: 3px solid rgba(240, 163, 90, 0.7);
border-radius: 12px;
}
.ta-controls .wrap { gap: 4px 10px !important; }
.ta-controls label span { font-size: 0.86em !important; }
/* One row per group. The caption column is a fixed width so every row's controls start on the
same vertical line: that is what makes the card scan as a list rather than a jumble. Rows are
separated by a hairline instead of whitespace, which keeps the whole card compact. */
.ta-controlrow {
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin: 0 !important;
min-height: 0 !important;
padding: 5px 0;
}
.ta-controlrow + .ta-controlrow,
.ta-controls .tab-buttons + .ta-controlrow,
.ta-controlrow + .tab-buttons,
.ta-controls .tab-buttons + .tab-buttons { border-top: 1px solid #ffffff14; }
/* Every row caption is drawn the same way: a ::before on the row for the checkbox rows, a
::before on the tab-wrapper for the chip bars. Same width, weight and opacity, so the four
captions form one column down the left of the card. */
.ta-row-entrants::before,
.ta-row-protocol::before {
content: "";
flex: 0 0 auto;
width: 150px;
align-self: center;
font-size: 0.7em;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.6;
white-space: nowrap;
}
.ta-row-entrants::before { content: "🥊 Who’s competing?"; }
.ta-row-protocol::before { content: "⚙️ Protocol"; }
/* Hints do not stand under the controls: five of them below a row of chips is more noise than
the chips themselves. `main.taStampTitles` moves each one into a native `title`, so it shows
on hover instead, and the ? badge is what says the text is there at all. */
.ta-controls span[data-testid="block-info"] { display: none !important; }
.ta-controlrow .ta-catchip label::after,
.metric-select .ta-catchip label::after {
content: "?";
flex: 0 0 auto;
width: 14px;
height: 14px;
margin-left: 2px;
border: 1px solid currentColor;
border-radius: 50%;
opacity: 0.45;
font-size: 0.66em;
font-weight: 700;
line-height: 12px;
text-align: center;
}
.ta-controlrow .ta-catchip label:hover::after,
.metric-select .ta-catchip label:hover::after { opacity: 0.85; }
/* Chip-bar buttons carry a tooltip too; a dotted underline hints at it without a badge on
every chip, which at five per row would be louder than the chips. */
.ta-controls .tab-buttons button[title] { text-decoration: underline dotted #ffffff40 1px; text-underline-offset: 3px; }
/* The chip bars sit inside the control card, which already draws a frame: drop their own
border so the card does not read as boxes within a box, and align their label column with
the other rows' captions. */
.ta-controls .tab-buttons { margin: 0 !important; }
.ta-controls .tab-buttons .tab-wrapper {
padding: 5px 0;
gap: 10px;
background: transparent;
border: none;
}
.ta-controls .tab-buttons .tab-container { gap: 4px; }
.ta-controls .axis-tasks .tab-wrapper::before,
.ta-controls .axis-datasets .tab-wrapper::before,
.ta-controls .axis-care .tab-wrapper::before,
.ta-controls .axis-metric .tab-wrapper::before {
width: 150px;
min-width: 150px;
font-size: 0.7em;
letter-spacing: 0.09em;
opacity: 0.6;
}
/* "I care about" and "Measured by" are two halves of one choice, so they read as one group:
the emoji sits on the first row only, the second caption is indented and quieter as a
continuation of it, and the hairline that separates every other row is dropped between them. */
.ta-controls .axis-care + .axis-metric { border-top: none !important; padding-top: 0 !important; }
.ta-controls .axis-care .tab-wrapper { padding-bottom: 1px; }
.ta-controls .axis-metric .tab-wrapper { padding-top: 1px; }
.ta-controls .axis-metric .tab-wrapper::before {
text-indent: 14px;
opacity: 0.42;
}
/* Phones. The caption column is 150px of a ~326px card, so the controls beside it get less room
than their own labels need: the entrant chips came out as unreadable slivers, three of the four
chip bars collapsed to an ellipsis, and the protocol checkboxes were clipped mid-word. Below
this width the caption takes a line of its own and the controls get the whole card; a chip bar
scrolls sideways rather than shrinking its chips, which is what it does anyway when it runs
out of room, only legibly. */
@media (max-width: 640px) {
.ta-row-entrants::before,
.ta-row-protocol::before,
.ta-controls .axis-tasks .tab-wrapper::before,
.ta-controls .axis-datasets .tab-wrapper::before,
.ta-controls .axis-care .tab-wrapper::before,
.ta-controls .axis-metric .tab-wrapper::before {
flex: 0 0 100%;
width: 100%;
min-width: 0;
margin-bottom: 3px;
}
.ta-controls .tab-buttons .tab-wrapper { flex-wrap: wrap; }
.ta-controls .tab-buttons .tab-container {
flex: 1 1 100%;
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
}
.ta-controls .tab-buttons .tab-container button { flex: 0 0 auto; white-space: nowrap; }
/* Two lines beat losing the second half of a label, and two columns beat four slivers:
four entrant checkboxes across a phone leave each one about forty pixels of text. */
.ta-controlrow .ta-catchip {
flex: 1 1 132px;
min-width: 132px;
}
.ta-controlrow .ta-catchip label { white-space: normal; }
.ta-controls { padding: 8px 10px; }
}
/* Each caption explains its axis on hover (main.taStampTitles). Say so: the same dotted
underline the chips use, a help cursor, and a brightening on hover, so "there is more here"
reads identically wherever a tooltip exists. `text-decoration` has to be set on the
pseudo-element itself; it does not inherit onto ::before from the row. */
.ta-row-entrants::before,
.ta-row-protocol::before,
.ta-controls .tab-buttons .tab-wrapper::before {
text-decoration: underline dotted #ffffff55 1px;
text-underline-offset: 3px;
cursor: help;
transition: opacity .14s ease;
}
.ta-row-entrants:hover::before,
.ta-row-protocol:hover::before,
.ta-controls .tab-buttons .tab-wrapper:hover::before { opacity: 0.95; }
/* No frame around these. Gradio draws the checkbox *label* as a bordered button -- its theme
literally calls it "the surrounding button of a checkbox" -- which on top of the checkbox
itself read as a second control. Overriding the theme variables rather than the rules is what
makes it stick: Gradio's own selectors carry the component's svelte hash, so a bare
`border: none` loses on specificity. The padding stays, so the whole label is the click
target rather than just the 12px box. */
.ta-controlrow .ta-catchip {
position: relative;
overflow: visible;
--checkbox-label-background-fill: transparent;
--checkbox-label-background-fill-hover: transparent;
--checkbox-label-background-fill-selected: transparent;
--checkbox-label-border-color: transparent;
--checkbox-label-border-color-hover: transparent;
--checkbox-label-border-color-selected: transparent;
--checkbox-label-border-width: 0px;
--checkbox-label-shadow: none;
--checkbox-label-shadow-hover: none;
--checkbox-label-shadow-active: none;
}
.ta-controlrow .ta-catchip label {
display: flex;
align-items: center;
gap: 7px;
padding: 4px 12px 4px 0 !important;
border: none !important;
border-radius: 0 !important;
background: none !important;
box-shadow: none !important;
color: #cfd6ea;
cursor: pointer;
transition: color .14s ease;
}
.ta-controlrow .ta-catchip label:hover { color: #eef2ff; }
.ta-controlrow .ta-catchip label span { cursor: pointer; }
.ta-controlrow .ta-catchip input { cursor: pointer; }
/* Checked reads as the accent fill the tab chips use for their selected state. */
/* Checked reads as brighter and heavier text; the box itself already carries the state. */
.ta-controlrow .ta-catchip label:has(input:checked) {
color: #eef2ff;
font-weight: 600;
}
/* The overview column matching a selector, washed orange across the whole cell.
Layered as a background-image rather than a background-color: each cell carries its heat
shade as an inline `background`, which encodes the value, and replacing it would throw that
away. A translucent flat gradient sits on top instead, so the column reads as selected and
still reads as hot or cold. `!important` is needed because the inline shorthand also resets
background-image. */
.ta-overview th.ta-col-sel {
border-top: 2px solid rgba(240, 163, 90, 0.95) !important;
background-image: linear-gradient(rgba(240, 163, 90, 0.2), rgba(240, 163, 90, 0.2)) !important;
color: #ffd9b0 !important;
}
.ta-overview td.ta-col-sel {
background-image: linear-gradient(rgba(240, 163, 90, 0.26), rgba(240, 163, 90, 0.26)) !important;
}
/* --- Where the reader is actually looking ----------------------------------
Everything pinned to a corner of the screen positions against these two, never against the
viewport directly. On a page that owns its scrollbar they are the viewport, so the rules
below come out exactly as `position: fixed` would have placed them anyway. Embedded in a
Hugging Face Space they are not: HF sizes the Space iframe to the app's whole content and
lets the *outer* page scroll, so our viewport is the entire six-thousand-pixel document and
a fixed element pins to the top of that instead of to the screen. `taTrackViewport` below
recovers the visible band and keeps these two up to date. */
:root {
--ta-vp-top: 0px;
--ta-vp-h: 100vh;
/* Where the COI chip starts: past the contents chip beside it. Replaced with the measured
width once the page has laid out; the default is a sensible width for its label. */
--ta-coi-left: 132px;
}
/* Sticky summary of the current selection, revealed once the control card scrolls away (see
main.taWatchControls). Bottom-centre rather than a corner: it is a status line for the whole
page, and the top-right corner already belongs to the COI badge. */
.ta-selbar {
position: fixed;
left: 50%;
top: calc(var(--ta-vp-h) - 18px);
z-index: 60;
display: flex;
align-items: center;
gap: 7px;
max-width: min(94vw, 1100px);
padding: 8px 14px;
border: 1px solid #ffffff2e;
border-radius: 999px;
background: rgba(24, 24, 28, 0.94);
box-shadow: 0 8px 26px #0009;
color: #dfe5f5;
font-family: inherit;
cursor: pointer;
overflow-x: auto;
/* Hidden until the observer says the controls are off screen. `top` anchors the bar's own
top edge, so it is pulled back up by its full height to sit on the line above. */
opacity: 0;
pointer-events: none;
transform: translate(-50%, calc(-100% + 12px)) translateY(var(--ta-vp-top));
/* The band offset rides in the same transform, so following the screen costs no layout;
only opacity and the reveal slide are transitioned, or every scroll would ease. */
transition: opacity .16s ease;
}
.ta-selbar.ta-show {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, -100%) translateY(var(--ta-vp-top));
}
.ta-selbar:hover { border-color: rgba(140, 168, 255, 0.8); }
.ta-selbar-lead {
flex: 0 0 auto;
font-size: 0.66em;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.5;
}
.ta-selchip {
flex: 0 0 auto;
padding: 3px 9px;
border-radius: 999px;
background: rgba(158, 176, 208, 0.16);
font-size: 0.8em;
font-weight: 600;
white-space: nowrap;
}
.ta-selbar-go {
flex: 0 0 auto;
margin-left: 3px;
padding: 3px 10px;
border-radius: 999px;
background: rgba(110, 140, 245, 0.34);
color: #eef2ff;
font-size: 0.78em;
font-weight: 700;
white-space: nowrap;
}
/* Contents row under the subset blurb: which figures follow, in the order they will appear.
The order changes with "I care about", so this doubles as a reminder of what that did. */
.ta-contents {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin: 2px 0 10px 0;
}
.ta-contents-lead {
margin-right: 2px;
font-size: 0.7em;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.55;
}
.ta-jumpchip {
padding: 4px 11px !important;
border: 1px solid #ffffff2e;
border-radius: 999px;
background: rgba(158, 176, 208, 0.14);
color: #cbd5ef !important;
font-size: 0.82em;
font-weight: 600;
text-decoration: none !important;
transition: background .14s ease, border-color .14s ease;
}
.ta-jumpchip:hover {
background: rgba(110, 140, 245, 0.3);
border-color: rgba(140, 168, 255, 0.85);
color: #eef2ff !important;
}
/* The models-vs-systems warning. It appears the moment a system joins the field, and it is the
one thing on the page a reader must not skim past, so it is styled as an alert rather than a
quote: red rule, red wash, and a heading at body-text weight instead of the muted grey the
info panels use. */
/* Nothing inside draws its own frame or fill; the single box is the wrapper below. Applies to
every descendant, not just the direct children: Gradio tints nested block elements, which
showed through as pale bands across the warning. */
.ta-warnbox *,
.ta-warnbox .prose {
border: none !important;
background: transparent !important;
background-color: transparent !important;
box-shadow: none !important;
}
.ta-warnbox > *,
.ta-warnbox .prose { padding: 0 !important; }
.ta-warnbox hr { display: none !important; }
.ta-warnbox {
margin: 6px 0 14px 0;
padding: 14px 18px;
/* The colour lives in the box rather than the text, but muted: a desaturated red-brown
that sits close to the page background instead of a saturated fill. The left rule and the
⚠️ carry the urgency, so the fill only has to mark the block as separate. */
border: 1px solid rgba(170, 140, 140, 0.22);
border-left: 5px solid #b05e62;
border-radius: 10px;
background: rgba(62, 54, 55, 0.5) !important;
}
.ta-warnbox h3 {
margin: 0 0 8px 0 !important;
color: #ffffff !important;
font-size: 1.08em !important;
font-weight: 700 !important;
}
.ta-warnbox p {
margin: 0 0 8px 0 !important;
color: #e8e2e2 !important;
font-size: 0.95em;
line-height: 1.5;
}
.ta-warnbox p:last-child { margin-bottom: 0 !important; }
.ta-warnbox strong { color: #ffffff !important; }
/* Models is locked on rather than unavailable, so it reads as the brightest thing in the row:
full strength, and a default cursor rather than the not-allowed one the dimmed categories
get. Both are non-interactive, and telling them apart at a glance is the point. */
.ta-controlrow .ta-catchip.ta-always label {
color: #eef2ff;
font-weight: 600;
opacity: 1;
cursor: default;
}
.ta-controlrow .ta-catchip.ta-always,
.ta-controlrow .ta-catchip.ta-always * { cursor: default !important; }
/* A category with no entrant yet. Dimmed and not-allowed rather than hidden, so the reader
can see the axis exists and that a submission is what is missing. */
.ta-soon { opacity: 0.42; }
.ta-soon, .ta-soon * { cursor: not-allowed !important; }
.ta-soon label:hover { color: #cfd6ea; }
/* The one-line consequence of each selection, so the effect is stated. */
.ta-controlnote {
display: flex;
flex-wrap: wrap;
gap: 4px 26px;
margin: 0 0 14px 2px;
font-size: 0.85em;
opacity: 0.72;
}
/* --- Figure panel with an interactive / static view switch -----------------
One card per figure: a header row (title + toggle) above two stacked views,
exactly one of which is visible. Both are in the DOM so the toggle is
instant; see main.taSwitchView and views._switchable_figure. */
.ta-figpanel {
margin: 18px 0 22px 0;
padding: 0;
overflow: hidden;
border: 1px solid #ffffff33;
border-left: 3px solid rgba(110, 140, 245, 0.55);
border-radius: 12px;
background: #ffffff14;
gap: 0 !important;
}
/* A titled header strip, full-bleed across the card: the earlier borderless
version left the three figure blocks running into one another, so it was hard
to see where one ended. The toggles sit right beside the title, where the eye
already is — pushed to the far edge they read as decoration and get missed. */
.ta-figbar {
display: flex;
align-items: center;
gap: 4px 12px;
flex-wrap: wrap;
margin: 0;
padding: 10px 15px;
background: #ffffff21;
border-bottom: 1px solid #ffffff38;
border-radius: 9px 9px 0 0;
}
.ta-figbar-title { font-weight: 650; font-size: 1.1em; letter-spacing: 0.01em; align-self: baseline; }
/* The subset qualifier: present, but it should not compete with the name. */
.ta-figbar-sub { font-size: 0.85em; opacity: 0.6; font-weight: 400; align-self: baseline; }
.ta-viewbtn {
flex: 0 0 auto;
cursor: pointer;
/* Gradio's stylesheet gives every button but the last a 4px bottom margin,
which under centre alignment lifts it 2px above its neighbours. */
margin: 0 !important;
padding: 5px 14px;
font-size: 0.85em;
font-weight: 600;
color: #cdd7ff;
background: rgba(110, 140, 245, 0.18);
border: 1px solid rgba(110, 140, 245, 0.55);
border-radius: 999px;
transition: background .15s ease, border-color .15s ease;
}
.ta-viewbtn:hover { background: rgba(110, 140, 245, 0.34); border-color: rgba(110, 140, 245, 0.95); }
/* The way into the controls, so it carries more weight than its neighbours. */
.ta-editbtn {
font-size: 0.95em;
font-weight: 700;
padding: 8px 18px;
color: #ffffff;
background: rgba(110, 140, 245, 0.9);
border-color: rgba(110, 140, 245, 1);
}
.ta-editbtn:hover { background: rgba(130, 158, 255, 1); border-color: rgba(150, 175, 255, 1); }
/* The chart / image itself, inset from the card edge. */
.ta-figview { padding: 0 13px 13px 13px; }
/* The per-dataset browser fills its panel: it is an application, not a figure, so the figure
inset -- and the padding Gradio puts on the HTML block inside it -- is dead width. */
.ta-figview-flush { padding: 0 3px 7px 3px !important; }
.ta-figview-flush .html-container { padding: 0 !important; }
/* Download controls: pinned to the right of the header and deliberately louder
than the view toggles, since exporting the figure is what paper view is for. */
.ta-exportgroup { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.ta-exportgroup[hidden] { display: none !important; }
.ta-exportlabel { font-size: 0.82em; font-weight: 700; letter-spacing: 0.04em; color: #7ee0b8; }
.ta-exportbtn { color: #063; background: #7ee0b8; border-color: #7ee0b8; }
.ta-exportbtn:hover { background: #a6efcd; border-color: #a6efcd; }
.ta-hidden { display: none !important; }
/* A Gradio button that exists only to carry a click to the server; the control the reader sees
is plain HTML inside a panel's title bar (see views.make_per_dataset_block). Moved out of
sight rather than `display: none`, which would take it out of the layout Gradio measures. */
.ta-offscreen-btn {
position: absolute !important;
width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
min-width: 0 !important; opacity: 0; pointer-events: none;
}
/* The static figure spans the panel and rescales with it — Gradio's default
image box would letterbox the very wide bar figures inside a fixed height. */
.ta-figpanel .image-container,
.ta-figpanel .image-frame { width: 100% !important; height: auto !important; max-height: none !important; }
.ta-figpanel .image-frame img {
width: 100% !important;
height: auto !important;
max-height: none !important;
object-fit: contain;
}
/* Row of external links/buttons (e.g. on a linked-leaderboard page).
Rendered as real <a target="_blank"> anchors so they open in a new tab
instead of navigating inside the embedded Hugging Face Space iframe. */
.link-row {
display: flex;
margin: 12px 0;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.ta-link-btn {
display: inline-block;
padding: 10px 20px !important;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
border: 1px solid transparent;
transition: background .15s ease, border-color .15s ease;
}
.ta-link-btn.primary {
background: rgba(110, 140, 245, 0.85);
border-color: rgba(110, 140, 245, 1);
color: #ffffff;
}
.ta-link-btn.primary:hover { background: rgba(110, 140, 245, 1); }
.ta-link-btn.secondary {
background: #ffffff14;
border-color: #ffffff3d;
color: #e6ebff;
}
.ta-link-btn.secondary:hover { background: #ffffff24; border-color: #ffffff5c; }
/* Compact "Metric:" selector panel above the overview table. */
.metric-select {
margin: -18px 0 0 0;
padding: 6px 14px;
gap: 12px;
align-items: center;
background: #ffffff0a;
border: 1px solid #ffffff26;
border-radius: 8px;
}
.metric-select::before {
content: "📈 Aggregation:";
font-size: 0.85em;
font-weight: 600;
opacity: 0.7;
white-space: nowrap;
}
/* Beside the metric dropdown, and on its own line once there is no room for both. Without a
floor it shrank instead of wrapping: on a phone the dropdown's 200px left this 84px, and a
one-line note became a 205px-tall ribbon one or two words wide. */
.metric-tldr {
flex: 1 1 260px;
min-width: 260px;
font-size: 0.85em;
opacity: 0.75;
font-style: italic;
}
.metric-tldr p { margin: 0; }
@media (max-width: 640px) {
.metric-tldr { min-width: 100%; }
}
/* Pull the overview (type legend + table) up tight under the aggregation box. */
.ta-overview-block { margin-top: -8px; }
/* "Jump to detailed results" pill link. */
.ta-jump {
display: inline-block;
margin: 0;
font-size: 0.9em;
padding: 4px 14px !important;
border: 1px solid #ffffff2e;
border-radius: 999px;
text-decoration: none;
opacity: 0.85;
}
.ta-jump:hover { opacity: 1; border-color: #ffffff5c; }
/* Section header: title (and optional jump link) on one row, summary directly below. */
.ta-section-head { margin: 10px 0 6px 0; }
.ta-section-head h2 { margin: 0; }
.ta-section-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.ta-section-sub { margin: 3px 0 0 0; font-size: 0.95em; opacity: 0.8; line-height: 1.4; }
/* Info topic pills + the single reveal panel (replaces the stacked accordions). */
.info-pills {
gap: 8px;
flex-wrap: wrap;
justify-content: center;
margin: -6px 0 2px 0;
}
.info-pills button {
border-radius: 999px !important;
flex: 0 0 auto !important;
font-weight: 600 !important;
font-size: 1em !important;
padding: 8px 20px !important;
color: #cdd7ff !important;
background: rgba(110, 140, 245, 0.18) !important;
border: 1px solid rgba(110, 140, 245, 0.55) !important;
}
.info-pills button:hover {
background: rgba(110, 140, 245, 0.32) !important;
border-color: rgba(110, 140, 245, 0.9) !important;
}
.info-pills button.primary {
background: rgba(110, 140, 245, 0.85) !important;
border-color: rgba(110, 140, 245, 1) !important;
color: #ffffff !important;
}
.info-panel {
margin: 6px 0 12px 0;
padding: 4px 18px;
border: 1px solid #ffffff1f;
border-radius: 12px;
background: #ffffff08;
}
/* --- The full leaderboard table, as one card -------------------------------
Title strip, then the filters, then the column key, then the table itself.
The controls and the key used to sit outside the table (the key in its own
floating panel, the filters inside the old third-party widget); keeping them
in the same bordered card is what makes them read as belonging to it. */
.ta-lb {
margin: 16px 0 10px 0;
padding: 0;
overflow: visible;
border: 1px solid #ffffff33;
border-left: 3px solid rgba(110, 140, 245, 0.55);
border-radius: 12px;
background: #ffffff10;
gap: 0 !important;
}
.ta-lb-bar {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 3px 12px;
padding: 11px 16px;
background: #ffffff21;
border-bottom: 1px solid #ffffff38;
border-radius: 9px 9px 0 0;
}
.ta-lb-title { font-weight: 650; font-size: 1.25em; letter-spacing: 0.01em; }
.ta-lb-sub { font-size: 0.85em; opacity: 0.6; }
/* One padded band for the filters, so four controls fit on a line without
looking cramped. Nothing here may set pointer-events, position or z-index:
these are live Gradio inputs and must stay clickable. */
.ta-lb-controls {
padding: 12px 16px 2px 16px;
gap: 18px;
align-items: flex-start;
}
.ta-lb .ta-lb-imputed { padding: 0 16px 6px 16px; }
.ta-lb .ta-legend { padding: 4px 16px 0 16px; }
.ta-lb .ta-scroll { margin: 4px 16px 0 16px; max-height: 720px; }
.ta-lb .ta-cap { padding: 0 16px 10px 16px; }
/* The line above the per-dataset browser, and the same line standing in for it while the
section is collapsed. */
.ta-pd-teaser { font-size: 0.92em; opacity: 0.78; margin: 2px 2px 10px; line-height: 1.5; }
/* --- Chip selectors, matching the plot explorers' edit view ------------------
The same shape, dot and pressed treatment as the method chips in the generated
*_explorer.html: a pill with a family-colored dot, no fill until selected, then
a tinted background and a matching border. Variables are the explorers' own
dark-theme values (the site forces dark).
These are real Gradio inputs underneath. The native box is visually hidden but
left in the layout and in the label, so a click on the chip still reaches it;
nothing here sets pointer-events, position or z-index on anything clickable.
Both `:has(input:checked)` and Gradio's own `.selected` drive the pressed look,
so a change to either survives. */
.ta-chips, .ta-btns, .ta-famchip {
--line: #2e2e33;
--muted: #9b9a92;
--pt-muted: #55555c;
--chip-bg: #232327;
--fam: #9b9a92;
}
.ta-btns-imputed { --fam: #e6c14d; }
.ta-chips label {
display: inline-flex !important;
align-items: center;
gap: 6px;
/* Spacing lives on the chips themselves rather than as a gap on Gradio's
internal container, so this does not depend on Gradio's DOM. */
margin: 0 5px 5px 0 !important;
padding: 5px 11px 5px 9px !important;
font: 500 12.5px/1 system-ui, sans-serif !important;
color: #f0efea;
background: none !important;
border: 1px solid var(--line) !important;
border-radius: 999px !important;
box-shadow: none !important;
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease;
}
.ta-chips label::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pt-muted);
flex: none;
}
.ta-chips label:hover { border-color: var(--muted) !important; }
.ta-chips label:has(input:checked),
.ta-chips label.selected {
border-color: var(--fam) !important;
background: color-mix(in srgb, var(--fam) 13%, transparent) !important;
font-weight: 650 !important;
}
.ta-chips label:has(input:checked)::before,
.ta-chips label.selected::before { background: var(--fam); }
/* Visually hidden, still hit-testable through its label. */
.ta-chips label input[type="checkbox"],
.ta-chips label input[type="radio"] {
width: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
opacity: 0;
flex: none;
}
/* The chip rows carry their own label above; keep Gradio's block label small. */
.ta-chips > .block-label, .ta-chips span[data-testid="block-info"] { font-size: 0.85em; }
/* The family chip that heads each row of model chips: the explorers' .famchip —
uppercase, dashed until selected, then solid in the family's colour. */
.ta-famchip label {
display: inline-flex !important;
align-items: center;
gap: 6px;
margin: 0 !important;
padding: 5px 11px !important;
font: 650 10.5px/1.3 system-ui, sans-serif !important;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
background: var(--chip-bg) !important;
border: 1px dashed var(--line) !important;
border-radius: 999px !important;
box-shadow: none !important;
cursor: pointer;
white-space: nowrap;
}
.ta-famchip label::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--fam);
flex: none;
}
.ta-famchip label:hover { border-color: var(--fam) !important; color: #f0efea; }
.ta-famchip label:has(input:checked),
.ta-famchip label.selected {
border: 1px solid var(--fam) !important;
background: color-mix(in srgb, var(--fam) 13%, transparent) !important;
color: #f0efea;
}
.ta-famchip label input { width: 0 !important; height: 0 !important; margin: 0 !important; opacity: 0; }
/* Family chip left, its models filling the rest of the row. */
.ta-chiprow { gap: 10px; align-items: flex-start; margin: 0 !important; padding: 0 16px; }
/* --- Button-style toggles, as in the Leaderboard Overview explorer -----------
That explorer uses buttons rather than chips for the variants ("since they
filter the series rather than the rows"), each carrying its variant colour from
--var-*; see constants.variant_color. Same shape here. */
.ta-btns label {
display: inline-flex !important;
align-items: center;
margin: 0 5px 5px 0 !important;
padding: 6px 11px !important;
font: 600 12.5px/1 system-ui, sans-serif !important;
color: #f0efea;
background: var(--chip-bg) !important;
border: 1px solid var(--line) !important;
border-radius: 7px !important;
box-shadow: none !important;
cursor: pointer;
white-space: nowrap;
transition: border-color 120ms ease, background-color 120ms ease;
}
.ta-btns label:hover { border-color: var(--muted) !important; }
.ta-btns label:has(input:checked),
.ta-btns label.selected {
border-color: var(--fam) !important;
background: color-mix(in srgb, var(--fam) 20%, #232327) !important;
}
.ta-btns label input { width: 0 !important; height: 0 !important; margin: 0 !important; opacity: 0; }
/* The CSV export button sits at the right end of the card's title strip. */
.ta-csvbtn { margin-left: auto !important; }
/* Sortable headers. The overview's info headers use a help cursor; here the same
header is also the sort control, so pointer wins and the dotted underline is
what still signals "hover me for a definition". */
.ta-lbtable thead th.ta-th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.ta-lbtable thead th.ta-th-sort:hover { background: #262632; }
.ta-lbtable thead th.ta-th-sort::after {
content: "↕";
font-size: 0.72em;
opacity: 0.28;
margin-left: 5px;
}
.ta-lbtable thead th[aria-sort="ascending"]::after { content: "▲"; opacity: 0.85; }
.ta-lbtable thead th[aria-sort="descending"]::after { content: "▼"; opacity: 0.85; }
.ta-lbtable th.ta-th-left { text-align: left; }
/* Two-emoji type symbols (🧠⚡, 🧠🔁) must never break across lines, in the table
cell or in the legend chip that explains it. */
.ta-pill { white-space: nowrap; }
.ta-lbtable td.ta-type-cell { white-space: nowrap; width: 1%; }
/* The Elo interval rides along with the value but stays visibly secondary. */
.ta-ci { opacity: 0.5; font-weight: 400; font-size: 0.82em; }
.ta-ci-head { opacity: 0.55; font-weight: 400; font-size: 0.82em; }
/* Hero stat cards shown above the info boxes (emoji on the left of the text, compact). */
.ta-hero {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: -8px 0 0 0;
}
.ta-hero .ta-card {
flex: 1 1 210px;
min-width: 210px;
display: flex;
align-items: center;
gap: 11px;
padding: 9px 13px;
border: 1px solid #ffffff1f;
border-radius: 10px;
background: linear-gradient(135deg, #ffffff12, #ffffff05);
text-align: left;
transition: transform .15s ease, border-color .15s ease;
}
.ta-hero .ta-card:hover {
transform: translateY(-2px);
border-color: #ffffff40;
}
.ta-hero .ta-card-ico { font-size: 1.5em; line-height: 1; flex: 0 0 auto; }
.ta-hero .ta-card-body { min-width: 0; }
.ta-hero .ta-card-num { font-size: 1.1em; font-weight: 700; line-height: 1.2; }
.ta-hero .ta-card-lbl { font-size: 0.8em; opacity: 0.75; margin-top: 1px; line-height: 1.3; }
.ta-hero .ta-card-lbl a { color: inherit; }
/* "Your Benchmark?" invite page: two "path" cards side by side. */
.ta-paths {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 18px 0 6px 0;
}
.ta-path {
flex: 1 1 320px;
min-width: 280px;
padding: 18px 22px;
border: 1px solid rgba(110, 140, 245, 0.45);
border-radius: 12px;
background: linear-gradient(135deg, rgba(110, 140, 245, 0.16), rgba(110, 140, 245, 0.04));
transition: transform .15s ease, border-color .15s ease;
}
.ta-path:hover { transform: translateY(-2px); border-color: rgba(110, 140, 245, 0.85); }
.ta-path-ico { font-size: 1.9em; line-height: 1; }
.ta-path h3 { margin: 8px 0 8px 0; font-size: 1.18em; }
.ta-path p { margin: 0; opacity: 0.88; line-height: 1.55; }
/* Soft "independent benchmarks" note on the invite page (mirrors the RamanBench callout). */
.ta-invite-note {
margin: 14px 0 4px 0;
padding: 11px 16px;
border: 1px solid rgba(110, 140, 245, 0.4);
border-left: 4px solid rgba(110, 140, 245, 0.85);
border-radius: 8px;
background: rgba(110, 140, 245, 0.08);
font-size: 0.95em;
line-height: 1.5;
opacity: 0.95;
}
/* --- Conflict-of-interest corner hint + CSS-only popup ---------------------
A small pill pinned to the top-right corner; clicking it reveals a modal. Opening and
closing it is the checkbox hack, so that much needs no JS; where the corner *is* comes from
`--ta-vp-top` / `--ta-vp-h` (see the top of this stylesheet). */
/* Jump menu, same CSS-only pattern as the COI badge below and pinned beside it. A menu rather
than a modal: every entry is one click and the page keeps its place until you pick one. */
/* Positioned with a transform rather than by moving `top`: the band is republished on every
scroll, and a transform is composited while a `top` change costs a layout pass, which is what
made the chips visibly trail the page. */
.toc-widget {
position: fixed;
top: 12px;
left: 14px;
z-index: 70;
transform: translateY(var(--ta-vp-top));
will-change: transform;
}
.toc-toggle { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.toc-badge {
display: inline-block;
padding: 5px 12px;
border: 1px solid rgba(110, 140, 245, 0.55);
border-radius: 999px;
background: rgba(110, 140, 245, 0.18);
color: #cdd7ff;
font-size: 0.82em;
font-weight: 600;
cursor: pointer;
user-select: none;
}
.toc-badge:hover { background: rgba(110, 140, 245, 0.34); border-color: rgba(110, 140, 245, 0.95); }
.toc-menu {
display: none;
position: absolute;
top: calc(100% + 6px);
left: 0;
min-width: 268px;
padding: 6px;
border: 1px solid #ffffff2e;
border-radius: 10px;
background: rgba(24, 24, 28, 0.97);
box-shadow: 0 10px 30px #000a;
}
.toc-toggle:checked ~ .toc-menu { display: block; }
/* Picking an entry closes the menu: while it is open this label covers the page, so the click
that lands on a link also reaches the label underneath and unchecks the toggle. CSS-only, so
it still works inside the Space iframe. */
.toc-toggle:checked ~ .toc-backdrop { display: block; }
/* Sized in multiples of the viewport rather than pinned to its edges: the transform on
`.toc-widget` makes that element the containing block for anything fixed inside it, so
`inset: 0` would shrink this to the chip and stop closing the menu. */
.toc-backdrop {
display: none;
position: fixed;
top: -150vh;
left: -150vw;
width: 400vw;
height: 400vh;
z-index: -1;
}
.toc-menu a {
display: block;
padding: 7px 10px !important;
border-radius: 7px;
color: #dfe5f5 !important;
font-size: 0.86em;
font-weight: 600;
text-align: left !important;
text-decoration: none !important;
}
.toc-menu a:hover { background: rgba(110, 140, 245, 0.28); }
/* Anchors sit under the fixed corner chips; scroll them clear. The figure panels are targets
too (the "Below:" row links to each one), and they are addressed by prefix because their ids
carry the subset they belong to. */
#ta-controls-anchor, #ta-overview-section, #lb-detailed, #ta-perdataset-section,
#ta-appendix-section, #ta-agentic-section, #ta-version-section,
[id^="ta-fig-"] { scroll-margin-top: 58px; }
/* Anchor navigation is the only jump that works inside the Space (a scripted scroll cannot move
a cross-origin parent), so the easing has to come from CSS rather than from `scrollIntoView`. */
html { scroll-behavior: smooth; }
/* Pinned to the same corner as the badge that toggles it, not left wherever in the document it
happens to sit. Clicking the badge focuses this checkbox, and a browser scrolls a focused
element into view: parked near the top of the page, opening the popup threw the reader back
to the top of the leaderboard. */
.coi-toggle {
position: fixed;
top: 10px;
left: var(--ta-coi-left);
transform: translateY(var(--ta-vp-top));
opacity: 0;
width: 0;
height: 0;
pointer-events: none;
}
.coi-badge {
position: fixed;
top: 10px;
/* Beside the contents chip, not in the opposite corner. `--ta-coi-left` is set from the
measured width of that chip (see `taPlaceChips`), since its label is not fixed. */
left: var(--ta-coi-left);
transform: translateY(var(--ta-vp-top));
will-change: transform;
z-index: 1000;
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 6px 13px;
font-size: 0.85em;
font-weight: 600;
color: #cdd7ff;
background: rgba(110, 140, 245, 0.2);
border: 1px solid rgba(110, 140, 245, 0.6);
border-radius: 999px;
user-select: none;
transition: background .15s ease, border-color .15s ease;
}
.coi-badge:hover { background: rgba(110, 140, 245, 0.34); border-color: rgba(110, 140, 245, 0.95); }
/* Two chips and a title share a 390px screen; give the chips less of it. Placed after both
badge rules rather than with the rest of the phone layout, which sits earlier in this sheet
and would lose to them on source order. */
@media (max-width: 640px) {
.toc-badge, .coi-badge { padding: 4px 9px; font-size: 0.74em; }
}
.coi-overlay {
display: none;
position: fixed;
top: var(--ta-vp-top);
left: 0;
right: 0;
height: var(--ta-vp-h);
z-index: 1001;
align-items: flex-start;
justify-content: center;
padding: 32px 20px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.66);
}
.coi-toggle:checked ~ .coi-overlay { display: flex; }
.coi-backdrop { position: absolute; inset: 0; cursor: default; }
.coi-modal {
position: relative;
width: 100%;
max-width: min(1040px, 94vw);
margin: 0 auto;
/* Fixed base font (in rem) so the popup does not inherit Gradio's smaller
in-app font; all sizes below scale from this. */
font-size: 1.06rem;
padding: 34px 44px 38px;
border-radius: 16px;
background: #1b1b24;
border: 1px solid rgba(110, 140, 245, 0.45);
box-shadow: 0 18px 55px rgba(0, 0, 0, 0.55);
color: #eef1ff;
line-height: 1.7;
}
.coi-modal h2 { margin: 0 0 6px 0; font-size: 1.85em; line-height: 1.25; }
.coi-modal h3 { margin: 26px 0 8px 0; font-size: 1.28em; color: #cdd7ff; }
.coi-modal p, .coi-modal li { font-size: 1em; opacity: 0.96; }
.coi-modal ul { margin: 8px 0; padding-left: 24px; }
.coi-modal li { margin: 7px 0; }
.coi-modal a { color: #9db4ff; }
.coi-lead { font-size: 1.18em !important; opacity: 1 !important; line-height: 1.55; }
.coi-tldr {
margin: 16px 0 4px 0;
padding: 16px 20px;
border: 1px solid rgba(110, 140, 245, 0.4);
border-left: 4px solid rgba(110, 140, 245, 0.85);
border-radius: 8px;
background: rgba(110, 140, 245, 0.1);
font-size: 1.05em;
line-height: 1.6;
}
.coi-x {
position: absolute;
top: 14px;
right: 20px;
font-size: 2em;
line-height: 1;
cursor: pointer;
opacity: 0.55;
user-select: none;
}
.coi-x:hover { opacity: 1; }
.coi-cta {
display: block;
width: fit-content;
margin: 28px auto 0;
text-align: center;
padding: 12px 24px !important;
border-radius: 8px;
font-weight: 600;
font-size: 1.02em;
text-decoration: none;
color: #ffffff !important;
background: rgba(110, 140, 245, 0.85);
border: 1px solid rgba(110, 140, 245, 1);
}
.coi-cta:hover { background: rgba(110, 140, 245, 1); }
/* On wider screens, give the prose a touch more size still. */
@media (min-width: 900px) {
.coi-modal { font-size: 1.12rem; }
}
/* BeyondArena "What do the subsets mean?" expander: larger label + blue highlight box. */
.beyond-subsets-accordion {
border: 1px solid #5aa9e6 !important;
border-radius: 10px !important;
background: rgba(90, 169, 230, 0.08) !important;
}
.beyond-subsets-accordion .label-wrap,
.beyond-subsets-accordion .label-wrap span,
.beyond-subsets-accordion > button {
font-size: 1.15rem !important;
font-weight: 600 !important;
color: #8cc4f0 !important;
}
"""
# Gradio 6 ships this inside `window.gradio_config` and the client appends it to
# <head> once the config is read — early, but *after* the bundle has picked a
# theme, which is why the block below overrides that choice rather than pre-empting
# it. (`THEME` in main() handles the frame before the bundle runs.)
HEAD = """
<style>
/* --- Dark, and only dark ---------------------------------------------------
The site has one theme: every colour in `CSS` above is a dark-theme value and
the figures in `data/` are exported for a dark background, so a light render
is broken rather than an alternative. `color-scheme` puts the browser's own
furniture — scrollbars, form controls, the canvas behind an over-scroll — on
the same side; the script below does the rest. */
:root { color-scheme: dark; }
html, body, gradio-app { background: #0f0f11; }
</style>
<script>
/* --- Dark, and only dark (see the <style> above) ---------------------------
Gradio 6 picks the theme in this order: the `theme-mode` attribute on
`<gradio-app>`, then the `__theme` query parameter, then `system` — which
follows the viewer's OS `prefers-color-scheme` and keeps a `matchMedia`
listener around to re-apply itself on every change. Setting `__theme=dark`
therefore only wins for a viewer who has neither of the other two against
them, which is how Hugging Face visitors on a light OS, or in a light-mode
embed, still landed in white mode. Reloading the page to append the
parameter (what this used to do) does not help: it loses the same race
again, one full page load later.
So don't negotiate with the theme picker — outlast it. Gradio's stylesheet
keys the whole dark palette off `:root.dark, :root .dark`, so adding the
class is enough on its own, and holding it is enough to survive the OS-theme
listener. Deliberately nothing else: the `theme-mode` attribute is a live
Svelte prop on the `<gradio-app>` custom element, so writing it re-enters the
boot it is meant to correct, and rewriting the URL moves ground the app is
standing on. A class is inert. */
(function () {
const DARK = "dark";
const observer = new MutationObserver(paint);
function hold(el) {
if (!el) return;
// Test before adding. `classList.add` re-serializes and re-sets the
// attribute whether or not the class is already there, and *that* queues
// a mutation record — so an unguarded add feeds this very observer and
// spins forever, freezing the page on Gradio's loading state.
if (!el.classList.contains(DARK)) el.classList.add(DARK);
// Same element, same options: re-observing replaces the registration
// rather than stacking another one.
observer.observe(el, { attributes: true, attributeFilter: ["class"] });
}
function paint() {
hold(document.documentElement);
hold(document.body);
// Embedded, Gradio marks the *parent* of <gradio-app> rather than
// <body> (`is_embed` in its bundle); holding both covers either mount.
for (const app of document.getElementsByTagName("gradio-app")) {
hold(app);
hold(app.parentElement);
}
}
// These few elements are the only ones Gradio ever marks, so watch them by
// name rather than subtree-watching a document this large.
paint();
document.addEventListener("DOMContentLoaded", paint);
window.addEventListener("load", paint);
})();
</script>
<script>
/* --- One decimal separator, everywhere ------------------------------------
Every number the site publishes uses a "." decimal separator and no
thousands grouping: the figures are rendered in Python, the CSVs are written
that way, and the interactive plots format with `toFixed`. Anything routed
through `toLocaleString` / `Intl.NumberFormat` would instead follow the
*viewer's* browser locale — a visitor in Germany would read "0,886" for a
score of 0.886, disagreeing with the figure beside it. Pin those two to
en-US, and default grouping off so the pin cannot introduce "1,765" either.
Number formatting only: dates are untouched, and an explicit locale or an
explicit `useGrouping` from the caller still wins. */
(function () {
const US = "en-US";
// Only fill in defaults when the caller asked for none — code that names a
// locale explicitly means it, and is left alone.
const pinned = (locales, options) =>
locales === undefined ? [US, { useGrouping: false, ...(options || {}) }] : [locales, options];
for (const proto of [Number.prototype, typeof BigInt !== "undefined" ? BigInt.prototype : null]) {
if (!proto) continue;
const original = proto.toLocaleString;
proto.toLocaleString = function (locales, options) {
return original.apply(this, pinned(locales, options));
};
}
const NumberFormat = Intl.NumberFormat;
function PinnedNumberFormat(locales, options) {
return new NumberFormat(...pinned(locales, options));
}
PinnedNumberFormat.prototype = NumberFormat.prototype;
PinnedNumberFormat.supportedLocalesOf = NumberFormat.supportedLocalesOf.bind(NumberFormat);
Intl.NumberFormat = PinnedNumberFormat;
})();
/* --- Figure panels: interactive <-> static view switch ---------------------
Each panel renders both views and flips them here, so switching costs no
server round trip. `uid` identifies the panel; `<uid>-i` / `<uid>-s` are the
interactive and static wrappers and `<uid>-btn` the toggle
(see views._switchable_figure). */
window.taSwitchView = function (uid) {
const interactive = document.getElementById(uid + "-i");
const staticView = document.getElementById(uid + "-s");
const btn = document.getElementById(uid + "-btn");
if (!interactive || !staticView || !btn) return;
interactive.classList.toggle("ta-hidden");
staticView.classList.toggle("ta-hidden");
const showingStatic = !staticView.classList.contains("ta-hidden");
btn.textContent = showingStatic ? "\\u26a1 Interactive view" : "\\u{1f5bc}\\ufe0f Static figure";
btn.setAttribute("aria-pressed", String(showingStatic));
};
/* Paper view — white background, chart + legend only — is the state a panel
*opens* in, since the figure is what a reader wants first. This flips to the
editing view and back. The panel header owns the control; the frame is
sandboxed and cross-origin, so the command travels over postMessage.
`aria-pressed` on the button tracks whether paper view is on. */
window.taPaperView = function (uid) {
const interactive = document.getElementById(uid + "-i");
const staticView = document.getElementById(uid + "-s");
const btn = document.getElementById(uid + "-paper");
const frame = interactive && interactive.querySelector("iframe.ta-explorer");
if (!frame || !btn) return;
// It only applies to the interactive chart, so bring that one back first.
if (staticView && !staticView.classList.contains("ta-hidden")) taSwitchView(uid);
const on = btn.getAttribute("aria-pressed") !== "true";
frame.contentWindow.postMessage({ type: "tabarena-explorer-paper", on: on }, "*");
btn.setAttribute("aria-pressed", String(on));
btn.textContent = on ? "\\u270f\\ufe0f Edit view" : "\\u{1f4c4} Paper view";
const exportGroup = document.getElementById(uid + "-export");
if (exportGroup) exportGroup.hidden = !on;
};
/* Metric preference — Elo or Improvability — broadcast to every embedded chart.
Both explorers already offer the two on their own y-axis selector, so this only decides
which one they open on. Sent to all frames rather than regenerating an artifact per
metric, which would double the published grid for a choice the charts can already make.
A panel whose wrapper carries .ta-pin-elo / .ta-pin-imp is told that one instead, so
whichever metric you did not pick stays visible somewhere on the page. */
/* Every selector in the control card explains itself on hover.
Two sources feed it. A checkbox already carries its text in Gradio's info span, which the CSS
hides -- that text moves into a `title` on the label, so a category's "coming soon" wording
follows whatever the server decided. A chip bar has no such span, so its text comes from
`taChoiceHints`, keyed by the chip's own label and injected from Python so the copy has one
home. Re-run on DOM changes because Gradio rebuilds these on every render. */
window.taChoiceHints = __TA_CHOICE_HINTS__;
window.taAxisHints = __TA_AXIS_HINTS__;
/* Reveal the selection bar once the control card has scrolled out of view, and hide it again
from the per-dataset section down. That section is a browser with its own contender, sort and
filter controls, so a second floating bar of selectors over it competes for the same job, and
the appendix below it is not about the selected leaderboard at all.
IntersectionObservers rather than a scroll handler: no work on frames where nothing crossed a
boundary. Re-attached after Gradio re-renders, since the elements are replaced. */
let taControlsSeen = null;
/* A CSS length variable as a number, but only once it is one. --ta-vp-h holds its declared
`100vh` until the tracker has measured a real band, and `parseFloat("100vh")` is 100. */
function taPxVar(name, fallback) {
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return raw.endsWith("px") ? parseFloat(raw) : fallback;
}
/* Has the reader reached the per-dataset section? Asked of the visible band rather than of an
IntersectionObserver, because the two environments move different things. Top-level the page
scrolls, so the section's own top moves and --ta-vp-top stays 0; embedded, the frame is as
tall as its content and never scrolls, so the section's top is a fixed document position and
the band moves instead. Comparing the two covers both -- where testing `top < 0` for "scrolled
past" only ever worked top-level, and let the bar reappear below the section inside a Space. */
function taPastPerDataset() {
const end = document.getElementById("ta-perdataset-section");
if (!end) return false;
const bandTop = taPxVar("--ta-vp-top", 0);
const bandH = taPxVar("--ta-vp-h", window.innerHeight);
return end.getBoundingClientRect().top <= bandTop + bandH * 0.5;
}
function taSyncSelbar() {
const bar = document.querySelector(".ta-selbar");
if (bar) bar.classList.toggle("ta-show", taControlsSeen === false && !taPastPerDataset());
}
function taWatchControls() {
const anchor = document.getElementById("ta-controls-anchor");
const bar = document.querySelector(".ta-selbar");
if (!anchor || !bar) return;
taSyncSelbar();
if (anchor.dataset.taWatched) return;
anchor.dataset.taWatched = "1";
new IntersectionObserver((entries) => {
taControlsSeen = entries[0].isIntersecting;
taSyncSelbar();
}, { rootMargin: "-40px 0px 0px 0px" }).observe(anchor);
}
/* --- Keep --ta-vp-top / --ta-vp-h on the visible band ----------------------
Only does anything when this page's viewport is not the screen. Embedded in a Hugging Face
Space it is not: HF sizes the Space iframe to the app's whole content, so the iframe never
scrolls, the outer page does, and our viewport is the entire document. Everything pinned to a
screen corner then pins to the top of a six-thousand-pixel page instead — the jump menu and
the COI badge scroll away with it, the selection bar waits at the very bottom, and opening the
COI modal blacks out the whole page with its content parked far above the screen.
The parent is cross-origin, so its scroll position is unreadable. IntersectionObserver's
implicit root is the top-level viewport though, and it reports the intersection in *our*
coordinates: a ladder of sentinels tiling the document therefore has the visible band as the
union of its intersection rectangles. Sentinels are tall enough that the one straddling each
edge is always partially visible, and its ratio changes continuously as the reader scrolls,
which is what keeps the updates coming (a single full-height probe would hold a constant
ratio through the middle of the page and never fire). */
const TA_SENTINEL_H = 300;
const TA_STEPS = Array.from({length: 101}, (_, i) => i / 100);
let taRailHeight = 0;
/* Add `?tadebug=1` to the URL for a live readout of what the tracker believes. There is no way
to reproduce a reader's own embedding from here — the Space page cannot be opened without
their session, and browsers differ on how they report intersections through a cross-origin
frame — so this is how a report comes back with numbers attached rather than a description. */
const TA_DEBUG = new URLSearchParams(location.search).has("tadebug");
function taReportViewport() {
if (!TA_DEBUG) return;
let box = document.getElementById("ta-debug-readout");
if (!box) {
box = document.createElement("div");
box.id = "ta-debug-readout";
box.style.cssText =
"position:fixed;top:calc(var(--ta-vp-top) + 46px);left:14px;z-index:2000;padding:6px 10px;" +
"border-radius:8px;background:#000d;color:#9fe;font:11px/1.5 ui-monospace,monospace;" +
"white-space:pre;pointer-events:none";
document.body.appendChild(box);
}
const cs = getComputedStyle(document.documentElement);
const rail = document.getElementById("ta-viewport-rail");
box.textContent = [
"framed " + (window.self !== window.top),
"innerH " + window.innerHeight,
"docH " + document.documentElement.scrollHeight,
"overflow " + (document.documentElement.scrollHeight - window.innerHeight),
"rail " + (rail ? rail.children.length + " sentinels" : "none"),
"resizer " + (window.parentIFrame ? "yes, asked for " + taRequestedHeight : "unavailable"),
"pageinfo " + (taPageInfoOn ? "on" : "off"),
"vp-top " + (cs.getPropertyValue("--ta-vp-top").trim() || "(unset)"),
"vp-h " + (cs.getPropertyValue("--ta-vp-h").trim() || "(unset)"),
// Not a backslash escape: HEAD is an ordinary Python string, so Python reads the escape
// and this script receives a real line break. Inside a JS string literal that is an
// unterminated string, which takes the whole script down with it.
].join(String.fromCharCode(10));
}
/* Ask the embedder for an iframe as tall as this page, when it has given us a shorter one.
Hugging Face sizes the Space with iframe-resizer, and its measurement can land well under the
real content -- a reader reported 4760px around a 6789px page. That leaves two scrollports:
the wheel scrolls the app inside its own frame until that runs out, and the page behind it
only moves after, which reads as scrolling getting stuck partway down. One ask per height, so
an embedder that declines is asked once and not again until the page itself changes. */
let taRequestedHeight = 0;
function taFitToParent() {
const parentFrame = window.parentIFrame;
if (!parentFrame || typeof parentFrame.size !== "function") return;
const height = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
if (height <= window.innerHeight + 8 || height === taRequestedHeight) return;
taRequestedHeight = height;
try {
parentFrame.size(height);
} catch (err) {
/* the embedder did not take it; the tracker below copes with the short frame anyway */
}
}
/* The embedder's own answer, when there is one. Hugging Face sizes the Space with
iframe-resizer, whose child half Gradio ships, and that half can ask the parent for its
geometry: `getPageInfo` reports the parent's scroll position, viewport and our offset within
it, and keeps reporting as the reader scrolls. That is the visible band directly, pushed to
us, with no sentinels and none of the lag of inferring it from intersections. The ladder
below stays for embedders that do not run iframe-resizer. */
let taPageInfoOn = false;
function taUsePageInfo() {
if (taPageInfoOn) return true;
const parentFrame = window.parentIFrame;
if (!parentFrame || typeof parentFrame.getPageInfo !== "function") return false;
taPageInfoOn = true;
parentFrame.getPageInfo((info) => {
// Our own viewport, clipped to what the parent's viewport shows of it. Client
// coordinates, which is what `position: fixed` measures from.
const top = Math.max(0, info.scrollTop - info.offsetTop);
const bottom = Math.min(info.iframeHeight, info.scrollTop + info.windowHeight - info.offsetTop);
const style = document.documentElement.style;
if (bottom - top >= window.innerHeight - 4 || bottom <= top) {
style.removeProperty("--ta-vp-top");
style.removeProperty("--ta-vp-h");
} else {
style.setProperty("--ta-vp-top", Math.round(top) + "px");
style.setProperty("--ta-vp-h", Math.round(bottom - top) + "px");
taSyncSelbar(); // the band moved, so what the reader has reached changed with it
}
taReportViewport();
});
// The ladder was only ever a stand-in for this.
document.getElementById("ta-viewport-rail")?.remove();
taRailHeight = 0;
return true;
}
/* Jumping to a section means moving whichever scrollport the reader is actually looking
through. Inside a Space that is usually the parent's, and a script in here may not touch it
-- following an anchor only works as far as this frame can scroll itself, which is why the
figure links did nothing while the menu, whose targets sat below this frame's own bottom,
did. iframe-resizer exposes `moveToAnchor` for exactly this. */
function taJumpToAnchor(event) {
// `composedPath`, not `event.target`: Gradio renders inside a custom element, and a click
// that started on a link is retargeted to the host by the time it reaches the document, so
// asking the target what it is closest to finds nothing.
const path = typeof event.composedPath === "function" ? event.composedPath() : [event.target];
const link = path.find((node) => node && node.matches && node.matches('a[href^="#"]'));
if (!link) return;
const target = document.getElementById(link.getAttribute("href").slice(1));
if (!target) return;
const parentFrame = window.parentIFrame;
if (!parentFrame) return;
event.preventDefault();
// Offset by the same margin the anchors reserve, so the heading clears the corner chips.
// `scrollToOffset` takes a position relative to this frame, which is what we can measure.
const y = Math.max(0, Math.round(target.getBoundingClientRect().top + window.scrollY) - 58);
if (typeof parentFrame.scrollToOffset === "function") parentFrame.scrollToOffset(0, y);
else if (typeof parentFrame.moveToAnchor === "function") parentFrame.moveToAnchor(target.id);
}
document.addEventListener("click", taJumpToAnchor);
function taTrackViewport() {
// Top-level, `position: fixed` already measures against the screen and the stylesheet's
// defaults say exactly that, so there is nothing to do and nothing to build.
if (window.self === window.top) return;
taFitToParent();
if (taUsePageInfo()) return;
// The ladder tiles our *viewport*, which in the case this exists for is the whole document.
// Never the document height: an absolutely positioned rail a few hundred pixels taller than
// the content (the page shrinks, the rail is only rebuilt past a sentinel) adds that much
// scrollable overflow, and an iframe with any overflow of its own stops handing the wheel to
// the page behind it -- scrolling died wherever the pointer happened to be. A fixed rail
// contributes no scrollable overflow at all, so it cannot come back.
const height = window.innerHeight;
// Rebuild only when the viewport has grown or shrunk past a sentinel (HF resizes the iframe
// around us as Gradio's lazy content settles).
if (Math.abs(height - taRailHeight) < TA_SENTINEL_H) return;
taRailHeight = height;
document.getElementById("ta-viewport-rail")?.remove();
const rail = document.createElement("div");
rail.id = "ta-viewport-rail";
rail.setAttribute("aria-hidden", "true");
rail.style.cssText =
"position:fixed;top:0;left:0;width:1px;height:100%;pointer-events:none;visibility:hidden";
const seen = new Map();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const rect = entry.intersectionRect;
if (rect.height > 0) seen.set(entry.target, rect);
else seen.delete(entry.target);
}
if (!seen.size) return; // scrolled clear of the Space; leave the last position
let top = Infinity, bottom = -Infinity;
for (const rect of seen.values()) {
top = Math.min(top, rect.top);
bottom = Math.max(bottom, rect.bottom);
}
const style = document.documentElement.style;
// Whether this page's viewport is the screen is answered here rather than by comparing
// heights up front: the band the reader can see *is* the screen, so a viewport taller
// than the band is one the embedder has stretched around the whole document. Comparing
// `innerHeight` against `scrollHeight` instead put the decision on a knife edge -- on
// the deployed Space those two differ by a single pixel.
if (bottom - top >= window.innerHeight - 4) {
style.removeProperty("--ta-vp-top");
style.removeProperty("--ta-vp-h");
return;
}
// Client coordinates, deliberately: everything that reads these is `position: fixed`,
// which measures from the top of *our* viewport. Adding `window.scrollY` to reach a
// document coordinate double-counts the moment this page has any scroll of its own --
// which happens whenever the embedder sizes the iframe smaller than the content, and
// then there are two scrollports rather than none. Both cases come out right here,
// because a page that cannot scroll has `scrollY === 0` anyway.
style.setProperty("--ta-vp-top", Math.round(top) + "px");
style.setProperty("--ta-vp-h", Math.round(bottom - top) + "px");
taSyncSelbar();
taReportViewport();
}, { threshold: TA_STEPS });
// Attached before the sentinels are observed: an element that is not in a document yet has
// no box to intersect, and the observer does not go back for it.
document.body.appendChild(rail);
for (let y = 0; y < height; y += TA_SENTINEL_H) {
const sentinel = document.createElement("div");
sentinel.style.cssText =
"position:absolute;left:0;width:1px;top:" + y + "px;height:" +
Math.min(TA_SENTINEL_H, height - y) + "px";
rail.appendChild(sentinel);
observer.observe(sentinel);
}
}
/* The COI chip sits beside the contents chip, whose width depends on its label and the reader's
font, so it is measured rather than guessed. */
function taPlaceChips() {
const contents = document.querySelector(".toc-widget");
if (!contents) return;
const width = Math.round(contents.getBoundingClientRect().width);
if (width > 0) {
document.documentElement.style.setProperty("--ta-coi-left", 14 + width + 10 + "px");
}
}
function taStampTitles() {
const hints = window.taChoiceHints || {};
const stamp = (el) => {
if (el.title) return;
const hint = hints[el.textContent.trim()];
if (hint) el.title = hint;
};
document.querySelectorAll(".ta-controls .ta-catchip label").forEach(stamp);
document.querySelectorAll(".ta-controls .tab-buttons button").forEach(stamp);
// The overview's own toggle sits outside the control card but explains itself the same way.
document.querySelectorAll(".metric-select .ta-catchip label").forEach(stamp);
// The row caption is a CSS ::before, which cannot hold a title, so it goes on the element
// the caption is drawn on. A chip inside has its own title, and the nearest one wins, so
// this only shows when the pointer is over the caption or the empty part of the row.
Object.entries(window.taAxisHints || {}).forEach(([cls, hint]) => {
document.querySelectorAll("." + cls).forEach((row) => {
const target = row.querySelector(".tab-wrapper") || row;
if (!target.title) target.title = hint;
});
});
}
new MutationObserver(() => {
// Coalesce a burst of mutations into one pass; the guards above make it idempotent.
clearTimeout(window._taStampTimer);
window._taStampTimer = setTimeout(() => {
taStampTitles();
taWatchControls();
taPlaceChips();
taTrackViewport();
}, 60);
}).observe(document.body, { childList: true, subtree: true });
// Top-level there is no band to track, so the section's own position is what moves.
window.addEventListener("scroll", taSyncSelbar, { passive: true });
document.addEventListener("DOMContentLoaded", () => {
taStampTitles();
taWatchControls();
taPlaceChips();
taTrackViewport();
taReportViewport();
});
// Which case we are in is decided by two heights, and neither one changing is a DOM mutation:
// the embedder resizing the iframe around us, and the page growing as Gradio's lazy content
// settles (an embedded explorer reporting its height moves the page without touching the DOM
// this observer watches).
window.addEventListener("resize", taTrackViewport);
new ResizeObserver(() => taTrackViewport()).observe(document.documentElement);
window.taMetric = "elo";
/* Send one frame the metric it should show: its pin if it has one, else the page-wide choice. */
window.taSendMetric = function (frame) {
const pinned = frame.closest(".ta-pin-elo, .ta-pin-imp");
const want = pinned ? (pinned.classList.contains("ta-pin-imp") ? "imp" : "elo") : window.taMetric;
try {
frame.contentWindow.postMessage({ type: "tabarena-explorer-metric", metric: want }, "*");
} catch (err) { /* not ready; it will ask again with its next height report */ }
};
window.taSetMetric = function (metric) {
window.taMetric = metric;
document.querySelectorAll("iframe.ta-explorer").forEach(window.taSendMetric);
};
/* The per-dataset browser covers every dataset, because a dataset's own numbers do not depend
on which others share its leaderboard. Tell it which slice the reader picked above, so it
opens on that; its own chips can widen it again. Frames without the attributes ignore this. */
window.taSendPerDatasetFilter = function (frame) {
const task = frame.dataset.task, size = frame.dataset.size;
if (task === undefined && size === undefined) return;
try {
frame.contentWindow.postMessage(
{ type: "tabarena-perdataset-filter", task: task || "all", size: size || "all" }, "*");
} catch (err) { /* not ready; it will ask again with its next height report */ }
};
/* How tall the reader's visible band is, for the per-dataset browser's "fit to screen" button.
The frame cannot work this out for itself: it is sized to its own content, so its
`innerHeight` is the length of its page rather than of the screen. We already track the band
for the pinned chips (--ta-vp-h), so send that. */
window.taSendPerDatasetViewport = function (frame) {
if (frame.dataset.task === undefined) return;
// The variable holds its declared value until the sentinel ladder has measured a real band,
// and that default is the token "100vh" -- which `parseFloat` happily reads as 100. Take it
// only once it is an actual pixel length.
const raw = getComputedStyle(document.documentElement).getPropertyValue("--ta-vp-h").trim();
const band = raw.endsWith("px") ? parseFloat(raw) : NaN;
const height = band > 200 ? band : window.innerHeight;
try {
frame.contentWindow.postMessage(
{ type: "tabarena-perdataset-viewport", height: Math.round(height) }, "*");
} catch (err) { /* not ready; it will ask again with its next height report */ }
};
/* A contents chip pointing at a collapsed section opens it as well as scrolling to it. The
anchor still does the scrolling on its own, so nothing breaks if the accordion markup does. */
window.taOpenSection = function (id) {
const section = document.getElementById(id);
if (!section) return;
const head = section.querySelector(".label-wrap");
if (head) {
if (!head.classList.contains("open")) head.click();
return;
}
// The per-dataset panel carries its own show/hide button in its title bar instead.
const show = section.querySelector('.ta-pdtoggle[aria-pressed="false"]');
if (show) show.click();
};
/* That button is HTML inside the panel's title bar, so it cannot be a Gradio component itself;
it forwards the click to the hidden one that actually reaches the server. */
window.taTogglePerDataset = function (uid) {
// Gradio puts `elem_id` on the <button> itself for gr.Button, and on a wrapper for most
// other components; accept either so this does not depend on which.
const node = document.getElementById(uid + "-toggle");
const real = node && (node.tagName === "BUTTON" ? node : node.querySelector("button"));
if (real) real.click();
};
document.addEventListener("click", (ev) => {
const chip = ev.target && ev.target.closest && ev.target.closest(".ta-jumpchip[data-open]");
if (chip) window.taOpenSection(chip.dataset.open);
});
/* --- Leaderboard table sorting ---------------------------------------------
Click a header to sort the rows beneath it; click again to reverse. Numeric
columns are marked `data-type="num"` and every cell carries the raw value in
`data-sort`, so the Elo column can print its confidence interval beside the
number without that text taking part in the comparison. Missing values ("–")
always sink to the bottom, whichever direction is active. Sorting is local to
the browser: the server sends the rows in published order and re-sending them
(when a filter changes) resets the sort. See views.leaderboard_table_html. */
window.taSortTable = function (th) {
const table = th.closest("table");
const body = table.tBodies[0];
if (!body) return;
const index = Array.prototype.indexOf.call(th.parentNode.children, th);
const numeric = th.dataset.type === "num";
const direction = th.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
table.querySelectorAll("th[aria-sort]").forEach((other) => other.removeAttribute("aria-sort"));
th.setAttribute("aria-sort", direction);
const sign = direction === "ascending" ? 1 : -1;
const keyOf = (row) => {
const cell = row.children[index];
if (!cell) return null;
const raw = cell.dataset.sort !== undefined ? cell.dataset.sort : cell.textContent.trim();
if (raw === "" || raw === "–") return null;
if (!numeric) return raw.toLowerCase();
const value = parseFloat(raw);
return isNaN(value) ? null : value;
};
const rows = Array.prototype.slice.call(body.rows);
rows.sort((a, b) => {
const ka = keyOf(a), kb = keyOf(b);
if (ka === null || kb === null) return ka === kb ? 0 : ka === null ? 1 : -1;
return ka < kb ? -sign : ka > kb ? sign : 0;
});
const fragment = document.createDocumentFragment();
rows.forEach((row) => fragment.appendChild(row));
body.appendChild(fragment);
};
/* Delegated so it keeps working after the server replaces the table's HTML. */
document.addEventListener("click", (ev) => {
const th = ev.target.closest && ev.target.closest("table.ta-lbtable th.ta-th-sort");
if (th) window.taSortTable(th);
});
/* --- Leaderboard CSV export -------------------------------------------------
Exports what the reader is actually looking at: the rows the filters left, the
columns they picked, in the order they sorted. Reading the rendered table is
what buys that last part — the server does not know the browser's sort. Values
come from `data-sort` where a cell has one (the raw unrounded number, and the
family name rather than its emoji), and the Elo cell's interval is split back
out into its own column. */
window.taExportTable = function (tableId) {
const table = document.getElementById(tableId);
if (!table) return;
const quote = (v) => '"' + String(v == null ? "" : v).replace(/"/g, '""') + '"';
const headers = [];
table.querySelectorAll("thead th").forEach((th) => {
const label = th.cloneNode(true);
label.querySelectorAll(".ta-ci-head").forEach((n) => n.remove());
headers.push(label.textContent.trim());
if (th.dataset.col === "elo") headers.push("Elo 95% CI");
});
const lines = [headers.map(quote).join(",")];
table.tBodies[0] && Array.prototype.forEach.call(table.tBodies[0].rows, (row) => {
if (row.hidden) return;
const values = [];
Array.prototype.forEach.call(row.cells, (cell) => {
if (cell.dataset.ci !== undefined) {
values.push(cell.dataset.sort);
values.push(cell.dataset.ci);
return;
}
values.push(cell.dataset.sort !== undefined && cell.dataset.export !== "text"
? cell.dataset.sort
: cell.textContent.trim());
});
lines.push(values.map(quote).join(","));
});
/* Escaped twice: this file's HEAD is a plain Python string, so a lone \n here
would reach the browser as a real newline inside a JS string literal and
break the whole script (as it once did). */
const blob = new Blob([lines.join("\\n") + "\\n"], { type: "text/csv;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = (table.dataset.name || tableId) + ".csv";
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};
/* The leaderboard table's CSV export. The button lives in the panel header, in
the same row as the title, and the download itself happens inside the frame —
only it knows the current filters, columns and sort order. Same channel as the
figure exports below. */
window.taLeaderboardCsv = function (button) {
const card = button.closest(".ta-lb");
const frame = card && card.querySelector("iframe.ta-explorer");
if (frame) frame.contentWindow.postMessage({ type: "tabarena-leaderboard-csv" }, "*");
};
/* Figure download, handed to the explorer over the same channel. */
window.taExport = function (uid, format) {
const frame = document.querySelector("#" + uid + "-i iframe.ta-explorer");
if (frame) frame.contentWindow.postMessage({ type: "tabarena-explorer-export", format: format }, "*");
};
/* The interactive explorer iframes (sandboxed srcdoc pages, see
views._interactive_plot_iframe) post their content height; resize each frame
to fit so it never shows an inner scrollbar. Height is clamped defensively —
the frames are sandboxed, but the message is still external input. */
window.addEventListener("message", (ev) => {
const d = ev.data;
if (!d || d.type !== "tabarena-explorer-height" || typeof d.height !== "number") return;
for (const frame of document.querySelectorAll("iframe.ta-explorer")) {
if (frame.contentWindow === ev.source) {
frame.style.height = Math.min(Math.max(Math.ceil(d.height), 300), 2600) + 8 + "px";
// A frame announcing its height has finished parsing, which is the first moment it
// can accept the metric choice. Answering here means a re-render's fresh iframes
// configure themselves; the host never has to detect that they appeared.
if (window.taSendMetric) window.taSendMetric(frame);
if (window.taSendPerDatasetFilter) window.taSendPerDatasetFilter(frame);
if (window.taSendPerDatasetViewport) window.taSendPerDatasetViewport(frame);
break;
}
}
});
</script>
"""
# The one frame `HEAD` cannot reach. Gradio's served `index.html` paints a splash
# from the theme's *light* body colours and only swaps to the dark pair when the
# viewer's OS asks for dark — so a light-mode visitor got a white flash before the
# bundle (and the dark-forcing block in `HEAD`) had loaded at all. Point the light
# pair at the dark values and that first frame is dark for everyone. Nothing else
# reads them: the app always renders under `.dark`, which has its own pair.
#
# Written out rather than referenced: `set()` rejects a `*…_dark` value, since it
# normally resolves the dark half of a pair by itself. These two are what
# `gr.themes.Default()` resolves `body_background_fill_dark` / `body_text_color_dark`
# to (`neutral_950` / `neutral_100`); the background also appears in `HEAD`'s <style>.
THEME = gr.themes.Default().set(
body_background_fill="#0f0f11",
body_text_color="#f4f4f5",
)
def main() -> None:
# Gradio 6 moved css / js / head from the Blocks constructor to launch(); passing
# them here would be ignored with a warning, silently dropping every style on the
# page. They are handed to launch() at the bottom of this function.
with gr.Blocks(title="TabArena", fill_width=True) as website:
gr.HTML(TITLE)
# Conflict-of-interest corner hint (🔍 pill, top-right) + CSS-only popup.
gr.HTML(COI_HTML)
# Top-level navigation: one tab per leaderboard (current + future + links).
with gr.Tabs(elem_id="top-tabs"):
for page in PAGES:
with gr.TabItem(page.name):
render_page(page)
# The agent-facing endpoints (no UI). These are what HF's generated
# agents.md points a coding agent at; see api.py.
register_api()
website.launch(
css=CSS,
# The hints are substituted here rather than baked into the constant, so the
# map stays derived from the label dicts at launch time.
head=(
HEAD.replace("__TA_CHOICE_HINTS__", json.dumps(_choice_hints())).replace(
"__TA_AXIS_HINTS__", json.dumps(AXIS_NOTES)
)
),
theme=THEME,
show_error=True,
ssr_mode=False,
debug=True,
# Serves the api.py endpoints as MCP tools at /gradio_api/mcp/, so an MCP
# client can attach to the Space itself instead of driving the REST API.
mcp_server=True,
)
if __name__ == "__main__":
main()