techfreakworm's picture
feat(ui): lock Score button while scoring + persist/download curves + label fix
2c55ab4 unverified
Raw
History Blame Contribute Delete
15.9 kB
"""Cortical Observatory theme — a calm, clinical-but-warm scientific instrument.
The visual identity for the TRIBE v2 brain-score Space (PLAN.md §6): a deep
slate surface read like an fMRI viewer, with the two ends of a BOLD activation
colormap (hot amber / cool cyan) used with restraint as accents. Metric curves
themselves use a colorblind-safe scientific ramp (see ``plotting.py``).
This module exposes the same three things ``qwen_theme.py`` does, so ``ui.py``
and ``app.py`` consume it identically:
* ``PALETTE`` -- the single source of truth for colors + a couple of metrics.
* ``build_theme()`` -- a configured :class:`gradio.themes.Base`.
* ``CSS`` -- the touches Gradio's theme tokens can't express alone (the
wordmark, the synchronized-readout chrome, the state panels, the seek
hint, and the licensing footer).
Type roles (three, per §6):
* display = **Space Grotesk** -- wordmark + section headers, used sparingly.
* body = **Inter** -- Gradio's default chain (the bulk of the UI text).
* data = **JetBrains Mono** -- tabular figures for timestamps / metric
values, where aligned numerals matter (the timeline is a clock).
Fonts are pulled from Google Fonts via :class:`gradio.themes.GoogleFont`; the
``@import`` in :data:`CSS` is a belt-and-suspenders fallback so the display +
mono faces are present even if the theme font chain is overridden.
"""
from __future__ import annotations
import gradio as gr
# Single source of truth for the palette. Variant: "Cortical Observatory" --
# deep slate panels + the hot/cool ends of an fMRI activation colormap.
PALETTE: dict[str, str] = {
# Surfaces
"body_bg": "#0E1116", # page: near-black slate
"panel_bg": "#161B22", # cards / blocks
"input_bg": "#0E1116", # inputs sit flush with the page
"elev_bg": "#1B2230", # hover / raised state
# Ink
"ink": "#E8EDF2", # primary text
"dim": "#8A93A0", # secondary / labels / info
"faint": "#5A626E", # tertiary / separators-as-text
# Lines
"border": "#232A33",
"border_strong": "#313B47",
# Accents -- the two ends of a BOLD activation colormap.
"accent": "#FFB454", # activation hot (amber) -- primary action
"accent_text": "#0E1116", # ink ON the amber button
"cool": "#36D1C4", # cool cyan -- counter-accent / live indicators
"cool_dim": "#1F7A73",
# Status
"error": "#FF6B6B",
"warn": "#FFB454",
"ok": "#36D1C4",
"radius": "10px",
}
def build_theme() -> gr.themes.Base:
"""Return a Gradio theme matching the Cortical Observatory palette.
Three font roles are registered (display / body / mono). The neutral and
primary hue ramps are derived from :data:`PALETTE` so Gradio's own
components (sliders, focus rings, primary buttons) stay on-palette without
per-component CSS overrides.
"""
return gr.themes.Base(
primary_hue=gr.themes.Color(
# Amber "activation hot" ramp around PALETTE["accent"].
c50="#FFF6E9",
c100="#FFE9C7",
c200="#FFD79A",
c300="#FFC774",
c400="#FFBE63",
c500=PALETTE["accent"],
c600="#E69A3C",
c700="#BE7C2C",
c800="#8C5A1E",
c900="#5A3A12",
c950="#2E1E09",
),
secondary_hue=gr.themes.Color(
# Cool cyan counter-accent ramp around PALETTE["cool"].
c50="#E7FBF8",
c100="#C2F4ED",
c200="#8DE9DE",
c300="#5BDCCE",
c400="#42D6C7",
c500=PALETTE["cool"],
c600="#23A99D",
c700="#1F7A73",
c800="#155350",
c900="#0E3634",
c950="#071E1D",
),
neutral_hue=gr.themes.Color(
# Slate ramp: light ink -> near-black page.
c50="#E8EDF2",
c100="#CDD5DE",
c200="#AAB4C0",
c300="#8A93A0",
c400="#5A626E",
c500="#3B434E",
c600="#313B47",
c700="#232A33",
c800="#161B22",
c900="#0E1116",
c950="#080B0F",
),
font=(
gr.themes.GoogleFont("Inter"),
"ui-sans-serif",
"system-ui",
"sans-serif",
),
font_mono=(
gr.themes.GoogleFont("JetBrains Mono"),
"ui-monospace",
"SFMono-Regular",
"Menlo",
"monospace",
),
radius_size=gr.themes.sizes.radius_md,
spacing_size=gr.themes.sizes.spacing_md,
text_size=gr.themes.sizes.text_md,
).set(
# --- Surfaces -------------------------------------------------------
body_background_fill=PALETTE["body_bg"],
body_text_color=PALETTE["ink"],
body_text_color_subdued=PALETTE["dim"],
background_fill_primary=PALETTE["panel_bg"],
background_fill_secondary=PALETTE["body_bg"],
block_background_fill=PALETTE["panel_bg"],
block_border_color=PALETTE["border"],
block_border_width="1px",
block_label_background_fill=PALETTE["panel_bg"],
block_label_text_color=PALETTE["dim"],
block_title_text_color=PALETTE["ink"],
block_radius=PALETTE["radius"],
panel_background_fill=PALETTE["panel_bg"],
panel_border_color=PALETTE["border"],
# --- Inputs ---------------------------------------------------------
input_background_fill=PALETTE["input_bg"],
input_background_fill_focus=PALETTE["elev_bg"],
input_border_color=PALETTE["border"],
input_border_color_focus=PALETTE["accent"],
input_placeholder_color=PALETTE["faint"],
# --- Primary button (activation hot) --------------------------------
button_primary_background_fill=PALETTE["accent"],
button_primary_background_fill_hover="#FFC774",
button_primary_text_color=PALETTE["accent_text"],
button_primary_border_color=PALETTE["accent"],
# --- Secondary button (quiet, bordered) -----------------------------
button_secondary_background_fill=PALETTE["elev_bg"],
button_secondary_background_fill_hover=PALETTE["border_strong"],
button_secondary_text_color=PALETTE["ink"],
button_secondary_border_color=PALETTE["border_strong"],
# --- Accents / focus ------------------------------------------------
slider_color=PALETTE["accent"],
color_accent=PALETTE["accent"],
color_accent_soft="rgba(255,180,84,0.12)",
checkbox_background_color=PALETTE["input_bg"],
checkbox_background_color_selected=PALETTE["accent"],
checkbox_border_color=PALETTE["border_strong"],
checkbox_border_color_focus=PALETTE["accent"],
checkbox_label_background_fill=PALETTE["panel_bg"],
checkbox_label_background_fill_selected=PALETTE["elev_bg"],
# --- Links ----------------------------------------------------------
link_text_color=PALETTE["cool"],
link_text_color_hover="#5BDCCE",
link_text_color_active=PALETTE["cool"],
)
CSS: str = """
/* Cortical Observatory -- chrome Gradio's theme tokens can't express alone. */
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--co-bg: #0E1116;
--co-panel: #161B22;
--co-elev: #1B2230;
--co-ink: #E8EDF2;
--co-dim: #8A93A0;
--co-faint: #5A626E;
--co-border: #232A33;
--co-border-strong: #313B47;
--co-amber: #FFB454;
--co-cyan: #36D1C4;
--co-error: #FF6B6B;
--co-display: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
--co-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* Keep the heavy hero contained on very wide monitors. */
.gradio-container { max-width: 1320px !important; margin: 0 auto !important; }
/* ---- Wordmark / masthead ------------------------------------------------ */
.co-masthead {
display: flex;
align-items: baseline;
gap: 14px;
flex-wrap: wrap;
padding: 4px 2px 2px;
margin-bottom: 2px;
}
.co-wordmark {
font-family: var(--co-display);
font-weight: 700;
font-size: 26px;
letter-spacing: -0.01em;
color: var(--co-ink);
line-height: 1.1;
}
/* The "·" between the two activation-colormap ends. */
.co-wordmark .co-hot { color: var(--co-amber); }
.co-wordmark .co-cool { color: var(--co-cyan); }
.co-tagline {
font-family: var(--co-mono);
font-size: 12px;
color: var(--co-dim);
letter-spacing: 0.02em;
}
/* Section headers use the display face, sparingly. */
.co-section-title {
font-family: var(--co-display);
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--co-dim);
margin: 2px 2px 6px;
}
/* ---- Live / quota banner (Space-only) ----------------------------------- */
.co-quota {
margin: 2px 0 10px;
padding: 9px 14px;
font-size: 12.5px;
line-height: 1.5;
color: var(--co-dim);
background: linear-gradient(180deg, rgba(54,209,196,0.06), rgba(54,209,196,0.02));
border: 1px solid var(--co-border);
border-radius: 10px;
}
.co-quota strong { color: var(--co-ink); font-weight: 600; }
.co-quota .co-dot::before {
content: "";
display: inline-block;
width: 7px; height: 7px; border-radius: 50%;
background: var(--co-cyan);
box-shadow: 0 0 0 0 rgba(54,209,196,0.5);
margin-right: 7px;
vertical-align: middle;
animation: co-pulse 2.4s ease-out infinite;
}
@keyframes co-pulse {
0% { box-shadow: 0 0 0 0 rgba(54,209,196,0.45); }
70% { box-shadow: 0 0 0 6px rgba(54,209,196,0); }
100% { box-shadow: 0 0 0 0 rgba(54,209,196,0); }
}
/* ---- Synchronized readout (the hero / signature element) ----------------- */
.co-readout {
border: 1px solid var(--co-border);
border-radius: 12px;
background:
radial-gradient(1200px 280px at 20% -10%, rgba(255,180,84,0.05), transparent 60%),
radial-gradient(900px 260px at 90% -20%, rgba(54,209,196,0.05), transparent 55%),
var(--co-panel);
padding: 10px;
}
/* The custom <video id="tm-video"> mount (the seek fallback target, §6). */
.co-video-wrap { position: relative; }
#tm-video {
width: 100%;
display: block;
border-radius: 10px;
background: #000;
border: 1px solid var(--co-border);
aspect-ratio: 16 / 9;
object-fit: contain;
}
.co-video-empty {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
border: 1px dashed var(--co-border-strong);
border-radius: 10px;
color: var(--co-faint);
font-family: var(--co-mono);
font-size: 13px;
background:
repeating-linear-gradient(45deg, rgba(255,255,255,0.012) 0 12px, transparent 12px 24px),
var(--co-bg);
}
/* The "click a spike -> jump there" hint sits under the timeline. */
.co-seek-hint {
font-family: var(--co-mono);
font-size: 11.5px;
color: var(--co-faint);
margin-top: 4px;
text-align: right;
}
.co-seek-hint kbd {
font-family: var(--co-mono);
color: var(--co-dim);
border: 1px solid var(--co-border-strong);
border-bottom-width: 2px;
border-radius: 5px;
padding: 0 5px;
background: var(--co-elev);
}
/* ---- Summary strip (peak / mean per metric) ----------------------------- */
.co-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 10px;
}
.co-stat {
border: 1px solid var(--co-border);
border-left: 3px solid var(--co-border-strong);
border-radius: 8px;
padding: 9px 12px;
background: var(--co-bg);
}
.co-stat .co-stat-name {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--co-dim);
}
.co-stat .co-stat-val {
font-family: var(--co-mono);
font-size: 19px;
font-weight: 500;
color: var(--co-ink);
line-height: 1.3;
}
.co-stat .co-stat-sub {
font-family: var(--co-mono);
font-size: 11px;
color: var(--co-faint);
}
/* ---- State panels: empty / loading / error ------------------------------ */
.co-state {
border: 1px solid var(--co-border);
border-radius: 12px;
padding: 30px 26px;
background: var(--co-panel);
text-align: center;
}
.co-state-icon {
font-size: 26px;
line-height: 1;
margin-bottom: 12px;
opacity: 0.9;
}
.co-state-title {
font-family: var(--co-display);
font-weight: 600;
font-size: 17px;
color: var(--co-ink);
margin-bottom: 6px;
}
.co-state-body {
font-size: 13.5px;
color: var(--co-dim);
line-height: 1.6;
max-width: 460px;
margin: 0 auto;
}
.co-state.co-state-error {
border-color: rgba(255,107,107,0.35);
background: linear-gradient(180deg, rgba(255,107,107,0.06), transparent), var(--co-panel);
}
.co-state.co-state-error .co-state-title { color: var(--co-error); }
/* Loading: a DETERMINATE in-card progress bar (Option T). Gradio's native
gr.Progress bar does not render in this loading-card layout (smoke-test
confirmed), so a gr.Timer re-renders the card HTML with this amber fill bar
driven by the live per-clip encode state. */
.co-prog-track {
height: 6px;
border-radius: 4px;
background: var(--co-border);
margin: 16px auto 8px;
max-width: 460px;
overflow: hidden;
}
.co-prog-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, var(--co-amber), #FFD9A0);
box-shadow: 0 0 10px rgba(255,180,84,0.5);
transition: width 0.3s linear;
}
.co-prog-pct {
font-family: var(--co-mono, ui-monospace, monospace);
font-size: 12.5px;
color: var(--co-amber);
letter-spacing: 0.02em;
}
/* Download-curves link appended to the summary (finished runs persist). */
.co-dl {
margin-top: 12px;
font-size: 13px;
}
.co-dl a {
color: var(--co-amber);
text-decoration: none;
font-weight: 600;
border-bottom: 1px solid rgba(255,180,84,0.4);
}
.co-dl a:hover { border-bottom-color: var(--co-amber); }
.co-dl-note { color: var(--co-dim); font-weight: 400; }
/* ---- Proxy / honesty caveat (the value-region disclaimer, §5) ----------- */
.co-caveat {
font-size: 12px;
line-height: 1.55;
color: var(--co-dim);
border-left: 2px solid var(--co-amber);
padding: 2px 0 2px 12px;
margin: 8px 2px;
}
.co-caveat strong { color: var(--co-ink); }
/* ---- Footer (license posture) ------------------------------------------- */
.co-footer {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--co-border);
font-size: 11.5px;
color: var(--co-faint);
line-height: 1.6;
}
.co-footer a { color: var(--co-dim); text-decoration: underline; text-underline-offset: 2px; }
.co-footer a:hover { color: var(--co-ink); }
/* Monospace numerals anywhere we explicitly tag tabular data. */
.co-mono, .co-tabnum { font-family: var(--co-mono); font-variant-numeric: tabular-nums; }
/* ---- Accessibility ------------------------------------------------------ */
/* Visible keyboard focus everywhere (don't rely on the faint default ring). */
:focus-visible {
outline: 2px solid var(--co-amber) !important;
outline-offset: 2px !important;
border-radius: 4px;
}
@media (prefers-reduced-motion: reduce) {
.co-quota .co-dot::before,
.co-stat { animation: none !important; }
}
/* ---- Responsive: zones stack on small screens --------------------------- */
@media (max-width: 820px) {
.co-wordmark { font-size: 22px; }
.co-readout { padding: 7px; }
.co-summary { grid-template-columns: repeat(2, 1fr); }
.co-state { padding: 22px 16px; }
}
@media (max-width: 480px) {
.co-summary { grid-template-columns: 1fr; }
.co-tagline { display: block; }
}
""".strip()