jackstev's picture
Deploy HW3 explainer app
f915d94 verified
Raw History Blame Contribute Delete
8.44 kB
"""Shared visual language for the three 24-679 HW3 apps.
Warm light palette: apricot page, aubergine ink, hibiscus actions, marigold data.
The same file ships with every Space so the three apps read as one set.
"""
from html import escape
import gradio as gr
# Named palette tokens reused by the theme, CSS, and HTML renderers
P = {
"page": "#FBEEE2", # apricot tinted page, warm but not cream
"surface": "#FFF9F4", # panels sit a step lighter than the page
"line": "#EFD9C8", # hairlines, input borders, bar tracks
"track": "#F6E4D6", # empty part of score bars
"ink": "#3A2530", # warm aubergine body text
"muted": "#7A6170", # secondary text and captions
"accent": "#B83E62", # hibiscus, buttons and keyboard focus
"accent_soft": "#F7D9DF", # selected chips and soft highlights
"data": "#E9A03F", # marigold, bars and plotted curves
"good": "#4F7F55", # passing checks
"bad": "#A5352B", # failing checks
"warn": "#8A5A12", # cautions that are not failures
}
HEAD_FONT = "'Bricolage Grotesque', 'Figtree', system-ui, sans-serif"
def _same_in_dark(**tokens):
"""Duplicate each token into its _dark twin so the palette never flips."""
out = {}
for key, value in tokens.items():
out[key] = value
out[f"{key}_dark"] = value # lock light mode even if browser asks for dark
return out
def build_theme():
"""Gradio theme holding shared control defaults for all three apps."""
base = gr.themes.Base(
primary_hue="rose",
neutral_hue="stone",
font=[gr.themes.GoogleFont("Figtree"), "system-ui", "sans-serif"],
font_mono=["ui-monospace", "monospace"],
)
paired = _same_in_dark(
body_background_fill=P["page"],
body_text_color=P["ink"],
body_text_color_subdued=P["muted"],
background_fill_primary=P["surface"],
background_fill_secondary=P["page"],
border_color_primary=P["line"],
color_accent_soft=P["accent_soft"],
block_background_fill=P["surface"],
block_border_color=P["line"],
block_label_text_color=P["muted"],
block_label_background_fill=P["surface"],
block_title_text_color=P["ink"],
block_shadow="none",
input_background_fill="#FFFFFF",
input_border_color=P["line"],
input_border_color_focus=P["accent"],
input_shadow="none",
button_primary_background_fill=P["accent"],
button_primary_background_fill_hover="#A03455",
button_primary_text_color="#FFFFFF",
button_primary_border_color=P["accent"],
button_secondary_background_fill=P["surface"],
button_secondary_background_fill_hover=P["accent_soft"],
button_secondary_text_color=P["ink"],
button_secondary_border_color=P["line"],
slider_color=P["accent"],
checkbox_background_color_selected=P["accent"],
checkbox_border_color_selected=P["accent"],
checkbox_label_background_fill=P["surface"],
checkbox_label_background_fill_hover=P["accent_soft"],
checkbox_label_background_fill_selected=P["accent_soft"],
checkbox_label_text_color=P["ink"],
checkbox_label_text_color_selected=P["ink"],
link_text_color=P["accent"],
link_text_color_hover="#A03455",
table_even_background_fill=P["surface"],
table_odd_background_fill="#FFFFFF",
table_border_color=P["line"],
error_background_fill="#F6DEDA",
error_border_color=P["bad"],
error_text_color=P["bad"],
loader_color=P["accent"],
)
# Tokens without a _dark twin are set once
return base.set(color_accent=P["accent"], block_radius="14px", input_radius="10px",
block_border_width="1px", **paired)
CSS = f"""
@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,650&family=Figtree:wght@400;500;600&display=swap');
/* Page frame: one centred column that stops growing at 1120px */
.gradio-container {{ max-width: 1120px !important; margin: auto !important; }}
.hw3-head, .hw3-foot {{ padding: 0; border: 0; }}
/* Header: the title carries the personality, the lede stays quiet */
.hw3-head h1 {{
font-family: {HEAD_FONT}; font-weight: 650; color: {P['ink']};
font-size: clamp(34px, 5vw, 52px); line-height: 1.02; letter-spacing: -0.02em;
margin: 12px 0 8px;
}}
.hw3-head h2 {{
font-family: {HEAD_FONT}; font-weight: 650; color: {P['ink']};
font-size: 28px; line-height: 1.1; letter-spacing: -0.01em; margin: 18px 0 6px;
}}
.hw3-head p {{ max-width: 64ch; color: {P['muted']}; font-size: 16px; line-height: 1.55; margin: 0 0 8px; }}
/* Result card: verdict line set large, evidence underneath */
.verdict {{
background: {P['surface']}; border: 1px solid {P['line']}; border-radius: 14px;
padding: 22px 24px; color: {P['ink']}; font-variant-numeric: tabular-nums;
}}
.verdict .v-title {{
font-family: {HEAD_FONT}; font-weight: 650; font-size: clamp(30px, 4vw, 44px);
line-height: 1.05; letter-spacing: -0.015em; margin: 0;
}}
.verdict.empty .v-title {{ color: {P['muted']}; font-size: 26px; }}
.verdict .v-sub {{ color: {P['muted']}; font-size: 15px; margin: 8px 0 18px; line-height: 1.5; }}
.verdict .v-foot {{
color: {P['muted']}; font-size: 13px; line-height: 1.55;
border-top: 1px solid {P['line']}; padding-top: 12px; margin-top: 18px;
}}
/* Score bars: width comes from the model output, CSS only styles it */
.bar-row {{ margin: 0 0 14px; }}
.bar-row .bar-lab {{ display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 6px; }}
.bar-row .track {{ height: 10px; background: {P['track']}; border-radius: 999px; overflow: hidden; }}
.bar-row .fill {{ height: 100%; background: {P['data']}; border-radius: 999px; }}
.bar-row .fill.lead {{ background: {P['accent']}; }}
/* Status badges pair colour with text so meaning never relies on colour */
.badge {{ display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 600; }}
.badge.good {{ background: #E3EFE1; color: {P['good']}; }}
.badge.bad {{ background: #F6DEDA; color: {P['bad']}; }}
.badge.warn {{ background: #FBEBCB; color: {P['warn']}; }}
/* Keep keyboard focus obvious everywhere */
.gradio-container *:focus-visible {{ outline: 3px solid {P['accent']} !important; outline-offset: 2px; }}
.hw3-foot p {{ color: {P['muted']}; font-size: 13px; line-height: 1.55; margin: 6px 0; }}
@media (max-width: 640px) {{
.verdict {{ padding: 18px; }}
.hw3-head p {{ font-size: 15px; }}
}}
"""
def header_html(title, lede):
"""Page header: one strong title and a single sentence of purpose."""
return f'<header class="hw3-head"><h1>{escape(title)}</h1><p>{escape(lede)}</p></header>'
def section_html(title, lede):
"""Secondary heading for a major panel within the page."""
return f'<header class="hw3-head"><h2>{escape(title)}</h2><p>{escape(lede)}</p></header>'
def bars_html(scores, lead=None):
"""Horizontal score bars; the leading class gets the accent colour."""
rows = []
for label, value in scores.items():
cls = "fill lead" if label == lead else "fill" # highlight the predicted class
rows.append(
f'<div class="bar-row"><div class="bar-lab"><span>{escape(str(label))}</span>'
f'<strong>{value:.1%}</strong></div><div class="track" aria-hidden="true">'
f'<div class="{cls}" style="width:{100 * value:.2f}%"></div></div></div>'
)
return "".join(rows)
def verdict_html(title, sub="", body="", foot="", empty=False):
"""Result card shell shared by the tabular and image apps."""
cls = "verdict empty" if empty else "verdict"
sub_html = f'<p class="v-sub">{sub}</p>' if sub else ""
foot_html = f'<p class="v-foot">{foot}</p>' if foot else ""
return (f'<section class="{cls}" aria-live="polite"><p class="v-title">{escape(title)}</p>'
f"{sub_html}{body}{foot_html}</section>")
def badge(text, tone):
"""Small pill; tone is good, bad, or warn."""
return f'<span class="badge {tone}">{escape(text)}</span>'
def footer_html(lines):
"""Quiet footer paragraphs for credits and links (callers escape their own HTML)."""
return '<footer class="hw3-foot">' + "".join(f"<p>{line}</p>" for line in lines) + "</footer>"