CellTriage / app /theme.py
Sarvarbek13's picture
CellTriage QC operator console - inference only, CPU-bound classical ML
749bffa verified
Raw
History Blame Contribute Delete
8.37 kB
"""Visual identity for the QC operator console.
DESIGN INTENT. This is instrument software, not a consumer app. The reference
points are laboratory and process-control interfaces: dark neutral ground,
high-contrast data, monospace for anything a reader might transcribe (cell IDs,
cycle counts, costs), and a clean sans-serif for prose.
THE COLOUR RULE, which is load-bearing rather than aesthetic. Green, amber and
red carry DECISION meaning -- accept, continue, reject -- and are used for
nothing else anywhere in the interface. A chart axis, a section header or a
hover state never uses them. An operator must be able to glance at a panel and
read its colour as a decision without checking what the colour refers to.
Data series therefore use the neutral/blue/violet ramp, and the one exception is
the campaign-shift alarm state, which is deliberately red because it IS a
decision: do not trust this result.
"""
from __future__ import annotations
import gradio as gr
# --- ground and structure ---------------------------------------------------
BG = "#0d1117" # page
PANEL = "#161b22" # cards
PANEL_RAISED = "#1c2430" # inputs, table headers
BORDER = "#30363d"
BORDER_STRONG = "#484f58"
# --- type -------------------------------------------------------------------
TEXT = "#e6edf3"
TEXT_MUTED = "#8b949e"
TEXT_DIM = "#6e7681"
# --- data (never used for decisions) ----------------------------------------
DATA = "#58a6ff" # primary series
DATA_ALT = "#a371f7" # secondary series
DATA_BAND = "#1f6feb" # uncertainty bands
GRID = "#21262d"
# --- decision semantics (used for NOTHING else) -----------------------------
ACCEPT = "#3fb950"
CONTINUE = "#d29922"
REJECT = "#f85149"
ALARM = "#f85149"
DECISION_COLOURS = {"A": ACCEPT, "B": CONTINUE, "C": REJECT, "CONTINUE": CONTINUE}
MONO = "'JetBrains Mono', 'SF Mono', 'Cascadia Mono', Consolas, monospace"
SANS = "'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif"
def build_theme() -> gr.Theme:
"""A restrained technical theme, not the Gradio default."""
return gr.themes.Base(
primary_hue=gr.themes.colors.blue,
secondary_hue=gr.themes.colors.slate,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "Consolas", "monospace"],
).set(
body_background_fill=BG,
body_text_color=TEXT,
background_fill_primary=PANEL,
background_fill_secondary=BG,
block_background_fill=PANEL,
block_border_color=BORDER,
block_border_width="1px",
block_label_background_fill=PANEL,
block_label_text_color=TEXT_MUTED,
block_title_text_color=TEXT,
border_color_primary=BORDER,
input_background_fill=PANEL_RAISED,
input_border_color=BORDER,
button_primary_background_fill=DATA_BAND,
button_primary_text_color="#ffffff",
button_secondary_background_fill=PANEL_RAISED,
button_secondary_text_color=TEXT,
panel_background_fill=PANEL,
table_odd_background_fill=PANEL,
table_even_background_fill=PANEL_RAISED,
)
CSS = f"""
:root {{
--ct-bg: {BG}; --ct-panel: {PANEL}; --ct-raised: {PANEL_RAISED};
--ct-border: {BORDER}; --ct-text: {TEXT}; --ct-muted: {TEXT_MUTED};
--ct-accept: {ACCEPT}; --ct-continue: {CONTINUE}; --ct-reject: {REJECT};
--ct-data: {DATA};
}}
.gradio-container {{ max-width: 1400px !important; }}
/* ---------- masthead ---------- */
.ct-masthead {{
border-bottom: 1px solid var(--ct-border);
padding: 0 0 14px 0; margin-bottom: 6px;
}}
.ct-masthead h1 {{
font-family: {MONO}; font-size: 1.45rem; font-weight: 600;
letter-spacing: 0.02em; color: var(--ct-text); margin: 0 0 4px 0;
}}
.ct-masthead .ct-sub {{
color: var(--ct-muted); font-size: 0.86rem; margin: 0;
}}
.ct-masthead .ct-build {{
font-family: {MONO}; font-size: 0.72rem; color: {TEXT_DIM}; margin-top: 6px;
}}
/* ---------- decision badge ---------- */
.ct-decision {{
border-radius: 6px; padding: 18px 22px; margin-bottom: 10px;
border-left: 5px solid var(--ct-border); background: var(--ct-raised);
}}
.ct-decision .ct-label {{
font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--ct-muted); margin-bottom: 6px;
}}
.ct-decision .ct-verdict {{
font-family: {MONO}; font-size: 1.9rem; font-weight: 700; line-height: 1.1;
}}
.ct-decision .ct-tier {{ color: var(--ct-muted); font-size: 0.9rem; margin-top: 4px; }}
.ct-accept {{ border-left-color: var(--ct-accept); }}
.ct-accept .ct-verdict {{ color: var(--ct-accept); }}
.ct-continue {{ border-left-color: var(--ct-continue); }}
.ct-continue .ct-verdict {{ color: var(--ct-continue); }}
.ct-reject {{ border-left-color: var(--ct-reject); }}
.ct-reject .ct-verdict {{ color: var(--ct-reject); }}
/* ---------- readouts ---------- */
.ct-readout {{
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1px; background: var(--ct-border); border: 1px solid var(--ct-border);
border-radius: 6px; overflow: hidden; margin-bottom: 10px;
}}
.ct-readout .ct-cell {{ background: var(--ct-panel); padding: 12px 14px; }}
.ct-readout .ct-k {{
font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--ct-muted); margin-bottom: 5px;
}}
.ct-readout .ct-v {{
font-family: {MONO}; font-size: 1.15rem; color: var(--ct-text); font-weight: 600;
}}
.ct-readout .ct-u {{ font-size: 0.72rem; color: {TEXT_DIM}; margin-left: 3px; }}
/* ---------- campaign-shift alarm: a state change, not a footnote ---------- */
.ct-alarm {{
border: 1px solid var(--ct-reject);
border-left: 5px solid var(--ct-reject);
background: rgba(248, 81, 73, 0.09);
border-radius: 6px; padding: 16px 20px; margin-bottom: 12px;
}}
.ct-alarm .ct-alarm-title {{
font-family: {MONO}; font-weight: 700; color: var(--ct-reject);
font-size: 0.95rem; letter-spacing: 0.04em; margin-bottom: 8px;
}}
.ct-alarm p {{ color: var(--ct-text); font-size: 0.85rem; margin: 4px 0; line-height: 1.5; }}
.ct-alarm code {{ font-family: {MONO}; color: var(--ct-reject); }}
.ct-nominal {{
border: 1px solid var(--ct-border); border-left: 5px solid var(--ct-accept);
background: rgba(63, 185, 80, 0.06);
border-radius: 6px; padding: 12px 18px; margin-bottom: 12px;
}}
.ct-nominal .ct-alarm-title {{
font-family: {MONO}; font-weight: 600; color: var(--ct-accept); font-size: 0.85rem;
}}
.ct-nominal p {{ color: var(--ct-muted); font-size: 0.8rem; margin: 4px 0; }}
/* ---------- takeaway strip under each figure ---------- */
.ct-takeaway {{
border-left: 3px solid var(--ct-data); background: var(--ct-raised);
padding: 10px 14px; margin: 4px 0 16px 0; border-radius: 0 4px 4px 0;
color: var(--ct-text); font-size: 0.84rem; line-height: 1.5;
}}
.ct-takeaway strong {{ color: var(--ct-data); }}
/* ---------- limitations ---------- */
.ct-limit {{
border: 1px solid var(--ct-border); border-radius: 6px;
padding: 16px 20px; background: var(--ct-panel); margin-bottom: 10px;
}}
.ct-limit h3 {{
font-family: {MONO}; font-size: 0.9rem; color: var(--ct-text);
margin: 0 0 10px 0; letter-spacing: 0.03em;
}}
.ct-limit ul {{ margin: 0; padding-left: 18px; }}
.ct-limit li {{ color: var(--ct-muted); font-size: 0.84rem; margin-bottom: 7px; line-height: 1.5; }}
.ct-limit li strong {{ color: var(--ct-text); }}
/* ---------- figure provenance line ---------- */
.ct-source {{
font-family: {MONO}; font-size: 0.7rem; color: {TEXT_DIM};
margin: -8px 0 22px 3px; letter-spacing: 0.02em;
}}
.ct-mono {{ font-family: {MONO}; }}
footer {{ display: none !important; }}
"""
def matplotlib_style() -> dict:
"""rcParams matching the console, so figures look native rather than pasted in."""
return {
"figure.facecolor": PANEL,
"axes.facecolor": PANEL,
"savefig.facecolor": PANEL,
"text.color": TEXT,
"axes.labelcolor": TEXT_MUTED,
"axes.edgecolor": BORDER_STRONG,
"xtick.color": TEXT_MUTED,
"ytick.color": TEXT_MUTED,
"grid.color": GRID,
"axes.grid": True,
"grid.alpha": 0.6,
"axes.titlecolor": TEXT,
"font.size": 9,
"axes.titlesize": 10,
"legend.frameon": False,
"legend.labelcolor": TEXT_MUTED,
"figure.dpi": 110,
}