mcqueenmater's picture
Deploy Fusion Lab: mobile-friendly UI + bug fixes
2b21f2d verified
Raw History Blame Contribute Delete
14.3 kB
"""Fusion Lab design tokens: dark, clarity-first palette (Linear/Observable/Bloomberg,
not Palantir-dense) plus a Gradio theme + CSS injection built from the same tokens."""
import gradio as gr
BG_VOID = "#09090b"
BG_SURFACE = "#18181b"
BORDER_HAIRLINE = "#27272a"
TEXT_PRIMARY = "#a1a1aa"
TEXT_SECONDARY = "#71717a"
ACCENT = "#FF5722"
SIG_POSITIVE = "#3DDC97"
SIG_NEUTRAL = "#d4d4d8"
SIG_NEGATIVE = "#FF5722"
SIG_NOT_SIGNIFICANT = "#3f3f46"
CLASS_COLORS = {0: SIG_NEGATIVE, 1: SIG_NEUTRAL, 2: SIG_POSITIVE}
CLASS_NAMES = {0: "Negative", 1: "Neutral", 2: "Positive"}
FONT_MONO = 'ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace'
FONT_SERIF = '"Merriweather", "Georgia", "Times New Roman", serif'
FONT_UI = (
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
)
def build_theme() -> gr.themes.Base:
return gr.themes.Base(
primary_hue=gr.themes.colors.emerald,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), FONT_MONO],
).set(
body_background_fill=BG_VOID,
body_background_fill_dark=BG_VOID,
background_fill_primary=BG_SURFACE,
background_fill_primary_dark=BG_SURFACE,
background_fill_secondary=BG_VOID,
background_fill_secondary_dark=BG_VOID,
border_color_primary=BORDER_HAIRLINE,
border_color_primary_dark=BORDER_HAIRLINE,
body_text_color=TEXT_PRIMARY,
body_text_color_dark=TEXT_PRIMARY,
body_text_color_subdued=TEXT_SECONDARY,
body_text_color_subdued_dark=TEXT_SECONDARY,
color_accent=ACCENT,
color_accent_soft=ACCENT,
block_background_fill=BG_SURFACE,
block_background_fill_dark=BG_SURFACE,
block_border_color=BORDER_HAIRLINE,
block_border_color_dark=BORDER_HAIRLINE,
block_radius="4px",
button_primary_background_fill=ACCENT,
button_primary_background_fill_dark=ACCENT,
button_primary_text_color=BG_VOID,
button_primary_text_color_dark=BG_VOID,
input_background_fill=BG_VOID,
input_background_fill_dark=BG_VOID,
input_border_color=BORDER_HAIRLINE,
input_border_color_dark=BORDER_HAIRLINE,
)
CUSTOM_CSS = f"""
:root {{
--fl-bg-void: {BG_VOID};
--fl-bg-surface: {BG_SURFACE};
--fl-border: {BORDER_HAIRLINE};
--fl-text-primary: {TEXT_PRIMARY};
--fl-text-secondary: {TEXT_SECONDARY};
--fl-accent: {ACCENT};
--fl-positive: {SIG_POSITIVE};
--fl-neutral: {SIG_NEUTRAL};
--fl-negative: {SIG_NEGATIVE};
--fl-not-sig: {SIG_NOT_SIGNIFICANT};
}}
.gradio-container {{
max-width: 1320px !important;
font-family: {FONT_UI} !important;
}}
.fl-mono, .fl-mono * {{
font-family: {FONT_MONO} !important;
font-variant-numeric: tabular-nums;
}}
/* Scoped sharp-corner + hairline treatment on our own components — deliberately
NOT a blanket wildcard border-radius reset, which fights native Gradio
component chrome (dropdowns/sliders/tab pills) instead of composing with it. */
.gr-group, .fl-panel, .fl-stat-block, .fl-badge, .fl-turn, .fl-caveat, .fl-diagram-box {{
border-radius: 0px !important;
}}
.gr-group, .fl-panel {{
border: 1px solid var(--fl-border) !important;
background: var(--fl-bg-surface) !important;
}}
/* Persistent header ribbon (space/header.py) */
.fl-ribbon {{
display: flex;
align-items: center;
gap: 28px;
padding: 14px 20px;
margin-bottom: 4px;
border: 1px solid var(--fl-border);
background: linear-gradient(180deg, var(--fl-bg-surface) 0%, var(--fl-bg-void) 100%);
}}
.fl-wordmark {{
font-family: {FONT_UI};
font-weight: 800;
font-size: 16px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fl-text-primary);
white-space: nowrap;
}}
.fl-wordmark span {{ color: var(--fl-accent); }}
.fl-ribbon-divider {{
width: 1px;
align-self: stretch;
background: var(--fl-border);
}}
.fl-chip {{
display: flex;
flex-direction: column;
gap: 1px;
min-width: 108px;
}}
.fl-chip-label {{
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--fl-text-secondary);
}}
.fl-chip-value {{
font-family: {FONT_MONO};
font-variant-numeric: tabular-nums;
font-size: 18px;
font-weight: 700;
color: var(--fl-text-primary);
}}
.fl-chip-value.fl-accent {{ color: var(--fl-accent); }}
.fl-chip-dot {{
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 5px;
}}
.fl-stat-block {{
display: flex;
flex-direction: column;
gap: 4px;
padding: 16px 20px;
border: 1px solid var(--fl-border);
background: var(--fl-bg-surface);
}}
.fl-stat-label {{
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--fl-text-secondary);
}}
.fl-stat-value {{
font-family: {FONT_MONO};
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
font-size: 34px;
font-weight: 700;
color: var(--fl-text-primary);
}}
.fl-stat-value.fl-accent {{ color: var(--fl-accent); }}
.fl-stat-sub {{
font-size: 12px;
color: var(--fl-text-secondary);
}}
/* Architecture diagram (space/diagram.py) */
.fl-diagram {{
display: flex;
align-items: center;
gap: 10px;
overflow-x: auto;
padding: 18px 4px;
}}
.fl-diagram-box {{
flex: 0 0 auto;
border: 1px solid var(--fl-border);
background: var(--fl-bg-surface);
padding: 10px 14px;
text-align: center;
}}
.fl-diagram-box.fl-diagram-highlight {{
border-color: var(--fl-accent);
box-shadow: 0 0 0 1px rgba(255, 87, 34, 0.25);
}}
.fl-diagram-title {{
font-size: 12px;
font-weight: 600;
color: var(--fl-text-primary);
white-space: nowrap;
}}
.fl-diagram-dim {{
font-family: {FONT_MONO};
font-variant-numeric: tabular-nums;
font-size: 11px;
color: var(--fl-accent);
margin-top: 2px;
}}
.fl-diagram-sub {{
font-size: 10px;
color: var(--fl-text-secondary);
margin-top: 2px;
max-width: 120px;
}}
.fl-diagram-arrow {{
flex: 0 0 auto;
color: var(--fl-text-secondary);
font-size: 16px;
}}
.fl-diagram-col {{
display: flex;
flex-direction: column;
gap: 6px;
}}
/* Dataframe density + tabular numerals */
.fl-panel table, table.svelte-* {{ font-family: {FONT_UI}; }}
div[data-testid="dataframe"] td, div[data-testid="dataframe"] th {{
font-family: {FONT_MONO} !important;
font-variant-numeric: tabular-nums;
font-size: 12.5px !important;
padding: 5px 10px !important;
}}
.fl-class-badge {{
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
}}
.fl-class-dot {{
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
flex: 0 0 auto;
}}
.fl-badge {{
display: inline-block;
padding: 3px 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
border-radius: 3px;
}}
.fl-badge-significant {{ background: rgba(255, 87, 34, 0.12); color: var(--fl-accent); border: 1px solid rgba(255, 87, 34, 0.35); }}
.fl-badge-not-significant {{ background: rgba(74, 85, 104, 0.2); color: var(--fl-text-secondary); border: 1px solid var(--fl-border); }}
.fl-caveat {{
border-left: 3px solid var(--fl-neutral);
padding: 8px 14px;
background: rgba(201, 162, 39, 0.06);
font-size: 13px;
color: var(--fl-text-secondary);
overflow-wrap: break-word;
}}
.fl-caveat code {{
overflow-wrap: break-word;
word-break: break-word;
}}
.fl-turn {{
padding: 10px 14px;
margin-bottom: 6px;
border: 1px solid var(--fl-border);
border-left-width: 3px;
}}
.fl-turn-meta {{
font-size: 11px;
color: var(--fl-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 4px;
}}
.fl-turn-text {{
font-size: 14px;
color: var(--fl-text-primary);
line-height: 1.5;
}}
.fl-turn-weight {{
font-family: {FONT_MONO};
font-variant-numeric: tabular-nums;
color: var(--fl-text-secondary);
font-size: 12px;
}}
.fl-section-header {{
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fl-text-secondary);
margin: 18px 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid var(--fl-border);
}}
.fl-split-hero {{
display: flex;
flex-direction: row;
align-items: stretch;
background: var(--fl-bg-void);
border: 1px solid var(--fl-border);
margin-bottom: 32px;
}}
.fl-split-left {{
flex: 1;
padding: 48px;
border-right: 1px solid var(--fl-border);
}}
.fl-split-right {{
flex: 1;
padding: 48px;
background: rgba(255, 87, 34, 0.03); /* Subtle audio accent background */
}}
.fl-serif-text {{
font-family: {FONT_SERIF} !important;
font-size: 24px;
line-height: 1.6;
color: #f4f4f5;
font-style: italic;
}}
.fl-xray-highlight {{
border-left: 4px solid var(--fl-accent) !important;
background: rgba(255, 87, 34, 0.08) !important;
}}
.fl-xray-highlight .fl-turn-text {{
color: var(--fl-accent) !important;
font-family: {FONT_MONO} !important;
}}
/* --- Evidence Room: behavioral summary strip, timeline, evidence panel
(space/workspace.py) --- these classes are emitted by workspace.py/transcript.py
but were previously unstyled; restored here. */
.fl-summary-strip {{
display: flex;
gap: 1px;
background: var(--fl-border);
border: 1px solid var(--fl-border);
margin-bottom: 10px;
flex-wrap: wrap;
}}
.fl-summary-strip .fl-stat-block {{
flex: 1;
border: none;
padding: 12px 16px;
}}
.fl-summary-strip .fl-stat-value {{
font-size: 24px;
}}
.fl-timeline {{
border: 1px solid var(--fl-border);
background: var(--fl-bg-surface);
padding: 14px 16px;
}}
.fl-timeline-row {{
display: flex;
align-items: center;
gap: 10px;
height: 30px;
margin-bottom: 2px;
}}
.fl-timeline-label {{
width: 84px;
flex: 0 0 auto;
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--fl-text-secondary);
}}
.fl-timeline-strip {{
position: relative;
flex: 1 1 auto;
height: 22px;
background: var(--fl-bg-void);
border: 1px solid var(--fl-border);
}}
.fl-timeline-bar {{
position: absolute;
bottom: 0;
width: 3px;
background: var(--fl-accent);
cursor: pointer;
transition: opacity 0.1s;
}}
.fl-timeline-bar:hover {{ opacity: 0.7; }}
.fl-timeline-bar.fl-timeline-selected {{
background: var(--fl-text-primary);
box-shadow: 0 0 0 1px var(--fl-accent);
}}
.fl-timeline-divider {{
position: absolute;
top: -4px;
bottom: -4px;
width: 1px;
background: var(--fl-neutral);
}}
.fl-timeline-section-label {{
position: absolute;
top: -18px;
font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--fl-text-secondary);
}}
.fl-timeline-risk-row {{
position: relative;
height: 14px;
margin: 4px 0 2px 94px;
}}
.fl-risk-dot {{
position: absolute;
top: 2px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--fl-negative);
cursor: pointer;
transform: translateX(-50%);
}}
.fl-risk-dot.fl-timeline-selected {{
box-shadow: 0 0 0 2px var(--fl-text-primary);
}}
.fl-evidence-panel {{
border: 1px solid var(--fl-border);
background: var(--fl-bg-surface);
padding: 14px 16px;
min-height: 220px;
}}
.fl-evidence-empty {{
color: var(--fl-text-secondary);
font-size: 13px;
}}
.fl-evidence-quote {{
margin-bottom: 10px;
}}
.fl-evidence-bullets {{
margin: 0 0 10px 0;
padding-left: 18px;
}}
.fl-evidence-bullet {{
font-size: 12.5px;
}}
/* --- Verdict card (space/workspace.py: render_verdict_card) --- */
.fl-verdict-card {{
border: 1px solid var(--fl-border);
background: var(--fl-bg-surface);
margin-bottom: 10px;
overflow: hidden;
}}
.fl-verdict-banner {{
padding: 12px 18px;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
}}
.fl-verdict-banner-correct {{
background: rgba(61, 220, 151, 0.12);
color: var(--fl-positive);
border-bottom: 1px solid rgba(61, 220, 151, 0.35);
}}
.fl-verdict-banner-incorrect {{
background: rgba(255, 87, 34, 0.12);
color: var(--fl-accent);
border-bottom: 1px solid rgba(255, 87, 34, 0.35);
}}
.fl-verdict-tiles {{
display: flex;
flex-wrap: wrap;
}}
.fl-verdict-tiles .fl-stat-block {{
flex: 1;
border: none;
border-right: 1px solid var(--fl-border);
padding: 12px 16px;
}}
.fl-verdict-tiles .fl-stat-block:last-child {{ border-right: none; }}
.fl-verdict-tiles .fl-stat-value {{
font-size: 22px;
display: flex;
align-items: center;
gap: 8px;
}}
/* --- Mobile --- the layout above is built on Gradio's own responsive Row/Column
grid (columns already collapse to full width below ~320px on their own), but the
raw HTML/CSS blocks below (ribbon, split-hero, panels, timeline) bypass that grid
and need explicit narrow-viewport rules. */
@media (max-width: 768px) {{
.fl-ribbon {{
flex-wrap: wrap;
gap: 14px;
padding: 12px 16px;
}}
.fl-chip {{
min-width: 88px;
}}
.fl-split-hero {{
flex-direction: column;
}}
.fl-split-left {{
padding: 20px;
border-right: none;
border-bottom: 1px solid var(--fl-border);
}}
.fl-split-right {{
padding: 20px;
}}
.fl-serif-text {{
font-size: 19px;
}}
.fl-panel {{
padding: 16px !important;
}}
.fl-timeline-label {{
width: 60px;
font-size: 10px;
}}
.fl-timeline-row {{
gap: 6px;
}}
.fl-timeline-risk-row {{
margin-left: 66px;
}}
.fl-timeline-section-label {{
font-size: 9px;
}}
}}
@media (max-width: 480px) {{
.fl-wordmark {{
font-size: 13px;
}}
.fl-chip {{
min-width: 74px;
}}
.fl-chip-value {{
font-size: 15px;
}}
.fl-stat-value {{
font-size: 26px;
}}
}}
"""