File size: 8,443 Bytes
f915d94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
"""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>"