Spaces:
Running
Running
| 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() | |