Spaces:
Sleeping
Sleeping
| """ | |
| Build Small Hackathon — Registration Dashboard | |
| ================================================ | |
| A sponsor-facing dashboard that reads the private org registration dataset and | |
| renders aggregate, anonymized charts. | |
| PRIVACY: This app NEVER displays names, emails, HF usernames, or project | |
| descriptions. Only counts and distributions are shown, so the link is safe to | |
| share with sponsors without exposing any registrant's personal information. | |
| DEPLOY (Hugging Face Spaces, Gradio SDK): | |
| 1. Add this app.py + requirements.txt + README.md to a new Space. | |
| 2. In the Space's Settings → Variables and secrets, add a secret: | |
| HF_TOKEN = <a token with READ access to the build-small-hackathon org> | |
| 3. (Optional) add DASHBOARD_PASSWORD = <some shared password> to gate access | |
| when the Space is public. Sponsors then log in with user `sponsor` + that | |
| password. Leave it unset for an open dashboard. | |
| """ | |
| import os | |
| import ast | |
| import time | |
| import tempfile | |
| import logging | |
| from collections import Counter | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import gradio as gr | |
| from huggingface_hub import HfApi | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| DATASET_NAME = "build-small-hackathon/build-small-hackathon-registrations" | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| DASHBOARD_PASSWORD = os.environ.get("DASHBOARD_PASSWORD") # optional gate | |
| # Columns we deliberately never surface in any aggregate (PII / free text). | |
| PII_COLUMNS = ["full_name", "email", "hf_username", "project_description"] | |
| # ---------------------------------------------------------------- brand tokens | |
| CREAM = "#fbf6e8" | |
| INK = "#2a1d0a" | |
| INK_SOFT = "#6b4423" | |
| GREEN = "#4a7c2e" | |
| GREEN_DEEP = "#2d5016" | |
| GREEN_MID = "#6b9039" | |
| GREEN_LIGHT = "#9bc466" | |
| AMBER = "#c9b072" | |
| RUST = "#b5651d" | |
| GRID = "rgba(139,111,71,0.16)" | |
| SEQ = [GREEN, GREEN_MID, GREEN_LIGHT, AMBER, RUST, INK_SOFT, GREEN_DEEP] | |
| BODY_FONT = "Spline Sans, -apple-system, BlinkMacSystemFont, sans-serif" | |
| DISPLAY_FONT = "Fraunces, Georgia, serif" | |
| HEAD = """ | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,900&family=Spline+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| """ | |
| # ---------------------------------------------------------------- data loading | |
| _CACHE = {"df": None, "ts": 0.0} | |
| CACHE_TTL = 300 # seconds — avoids hammering the Hub when several viewers open it | |
| def _strip_pii(df): | |
| """Drop PII so it can never leak into a chart, tooltip, or table.""" | |
| return df.drop(columns=[c for c in PII_COLUMNS if c in df.columns]) | |
| def fetch_dataframe(force=False): | |
| """Download the dataset's train parquet directly (force=True ignores cache). | |
| Mirrors how the registration Space itself reads the data, which is robust | |
| against stale dataset-script caches. PII is stripped on every return path. | |
| """ | |
| now = time.time() | |
| if not force and _CACHE["df"] is not None and (now - _CACHE["ts"]) < CACHE_TTL: | |
| return _strip_pii(_CACHE["df"]) | |
| api = HfApi(token=HF_TOKEN) | |
| files = api.list_repo_files(DATASET_NAME, repo_type="dataset") | |
| parquet_files = [f for f in files if f.endswith(".parquet") and "train" in f] | |
| if not parquet_files: | |
| raise RuntimeError("No train parquet file found in the dataset.") | |
| with tempfile.TemporaryDirectory() as tmp: | |
| path = api.hf_hub_download( | |
| repo_id=DATASET_NAME, | |
| filename=parquet_files[0], | |
| repo_type="dataset", | |
| cache_dir=tmp, | |
| force_download=True, | |
| token=HF_TOKEN, | |
| ) | |
| df = pd.read_parquet(path) | |
| df = _strip_pii(df) | |
| _CACHE["df"] = df | |
| _CACHE["ts"] = now | |
| return _strip_pii(df) | |
| # ---------------------------------------------------------------- small helpers | |
| def parse_list(val): | |
| """The app stores some fields as stringified lists, e.g. "['First timer']".""" | |
| if isinstance(val, list): | |
| return val | |
| if isinstance(val, str) and val.strip().startswith("["): | |
| try: | |
| parsed = ast.literal_eval(val) | |
| return parsed if isinstance(parsed, list) else [str(parsed)] | |
| except (ValueError, SyntaxError): | |
| return [] | |
| if val: | |
| return [str(val)] | |
| return [] | |
| def short(label): | |
| """Trim the long ' — explanation' tails off choice labels for chart axes.""" | |
| if not isinstance(label, str): | |
| return str(label) | |
| for sep in ("—", " - "): | |
| if sep in label: | |
| return label.split(sep)[0].strip() | |
| return label.strip() | |
| def track_label(label): | |
| s = str(label) | |
| if "Backyard" in s: | |
| return "🏡 Backyard AI" | |
| if "Thousand Token Wood" in s: | |
| return "🍄 Thousand Token Wood" | |
| if "Both" in s: | |
| return "Both tracks" | |
| return "Undecided" | |
| # ---------------------------------------------------------------- figure styling | |
| def style(fig, height=320, legend=False): | |
| fig.update_layout( | |
| height=height, | |
| paper_bgcolor=CREAM, | |
| plot_bgcolor=CREAM, | |
| font=dict(family=BODY_FONT, size=13, color=INK), | |
| title=dict(font=dict(family=DISPLAY_FONT, size=18, color=GREEN_DEEP), x=0.02, xanchor="left"), | |
| margin=dict(l=12, r=18, t=46, b=12), | |
| showlegend=legend, | |
| legend=dict(font=dict(size=11), bgcolor="rgba(0,0,0,0)"), | |
| hoverlabel=dict(bgcolor=GREEN_DEEP, font=dict(color=CREAM, family=BODY_FONT)), | |
| colorway=SEQ, | |
| ) | |
| fig.update_xaxes(showgrid=False, zeroline=False, linecolor=GRID, tickcolor=GRID) | |
| fig.update_yaxes(showgrid=True, gridcolor=GRID, zeroline=False, linecolor=GRID, tickcolor=GRID) | |
| return fig | |
| def empty_fig(msg="No data yet"): | |
| fig = go.Figure() | |
| fig.add_annotation(text=msg, showarrow=False, font=dict(family=DISPLAY_FONT, size=18, color=INK_SOFT)) | |
| fig.update_xaxes(visible=False) | |
| fig.update_yaxes(visible=False) | |
| return style(fig, height=260) | |
| def hbar(counts, title, color=GREEN, height=320): | |
| """Horizontal bar from a {label: count} mapping, largest on top.""" | |
| if not counts: | |
| return empty_fig() | |
| items = sorted(counts.items(), key=lambda kv: kv[1]) | |
| labels = [k for k, _ in items] | |
| values = [v for _, v in items] | |
| fig = go.Figure(go.Bar( | |
| x=values, y=labels, orientation="h", | |
| marker=dict(color=color, line=dict(color=GREEN_DEEP, width=0.5)), | |
| text=values, textposition="outside", | |
| cliponaxis=False, | |
| hovertemplate="%{y}: %{x}<extra></extra>", | |
| )) | |
| fig.update_layout(title=title) | |
| fig = style(fig, height=height) | |
| fig.update_xaxes(showgrid=True, gridcolor=GRID) | |
| fig.update_yaxes(showgrid=False) | |
| return fig | |
| def donut(counts, title, color_map=None, height=320): | |
| if not counts: | |
| return empty_fig() | |
| labels = list(counts.keys()) | |
| values = list(counts.values()) | |
| colors = [color_map.get(l) for l in labels] if color_map else SEQ | |
| fig = go.Figure(go.Pie( | |
| labels=labels, values=values, hole=0.58, | |
| marker=dict(colors=colors, line=dict(color=CREAM, width=2)), | |
| textinfo="percent", textfont=dict(family=BODY_FONT, size=12, color=CREAM), | |
| hovertemplate="%{label}: %{value} (%{percent})<extra></extra>", | |
| sort=False, | |
| )) | |
| fig.update_layout(title=title) | |
| return style(fig, height=height, legend=True) | |
| # ---------------------------------------------------------------- chart builders | |
| def fig_momentum(df): | |
| if df.empty or "timestamp" not in df.columns: | |
| return empty_fig() | |
| dt = pd.to_datetime(df["timestamp"], errors="coerce").dropna() | |
| if dt.empty: | |
| return empty_fig() | |
| daily = dt.dt.floor("D").value_counts().sort_index() | |
| cumulative = daily.cumsum() | |
| fig = make_subplots(specs=[[{"secondary_y": True}]]) | |
| fig.add_trace(go.Bar( | |
| x=daily.index, y=daily.values, name="Per day", | |
| marker=dict(color=GREEN_LIGHT), opacity=0.85, | |
| hovertemplate="%{x|%b %d}: %{y} registrations<extra></extra>", | |
| ), secondary_y=False) | |
| fig.add_trace(go.Scatter( | |
| x=cumulative.index, y=cumulative.values, name="Cumulative", | |
| mode="lines", line=dict(color=GREEN_DEEP, width=3, shape="spline"), | |
| fill="tozeroy", fillcolor="rgba(45,80,22,0.10)", | |
| hovertemplate="%{x|%b %d}: %{y} total<extra></extra>", | |
| ), secondary_y=True) | |
| fig.update_layout(title="Registration momentum", bargap=0.25) | |
| fig = style(fig, height=380, legend=True) | |
| fig.update_yaxes(title_text="Per day", secondary_y=False, showgrid=False) | |
| fig.update_yaxes(title_text="Cumulative", secondary_y=True, gridcolor=GRID) | |
| return fig | |
| def fig_heard(df): | |
| if df.empty or "how_heard" not in df.columns: | |
| return empty_fig() | |
| counts = df["how_heard"].dropna().map(short).value_counts().to_dict() | |
| return hbar(counts, "Where builders heard about us", color=GREEN, height=380) | |
| def fig_track(df): | |
| if df.empty or "track_interest" not in df.columns: | |
| return empty_fig() | |
| counts = df["track_interest"].dropna().map(track_label).value_counts().to_dict() | |
| cmap = { | |
| "🏡 Backyard AI": GREEN, | |
| "🍄 Thousand Token Wood": RUST, | |
| "Both tracks": GREEN_MID, | |
| "Undecided": AMBER, | |
| } | |
| return donut(counts, "Track interest", color_map=cmap) | |
| def fig_returning(df): | |
| if df.empty or "previous_participation" not in df.columns: | |
| return empty_fig() | |
| gradio_events = {"MCP 1st Birthday", "Agents & MCP Hackathon"} | |
| returning = first_time = other = 0 | |
| for val in df["previous_participation"]: | |
| items = set(parse_list(val)) | |
| if items & gradio_events: | |
| returning += 1 | |
| elif "First timer" in items: | |
| first_time += 1 | |
| else: | |
| other += 1 | |
| counts = { | |
| "Returning Gradio builders": returning, | |
| "First-timers": first_time, | |
| "Other hackathon vets": other, | |
| } | |
| counts = {k: v for k, v in counts.items() if v} | |
| cmap = { | |
| "Returning Gradio builders": GREEN_DEEP, | |
| "First-timers": GREEN_LIGHT, | |
| "Other hackathon vets": AMBER, | |
| } | |
| return donut(counts, "Community make-up", color_map=cmap) | |
| def fig_experience(df): | |
| if df.empty or "experience_level" not in df.columns: | |
| return empty_fig() | |
| order = ["Beginner", "Intermediate", "Advanced", "Expert"] | |
| raw = df["experience_level"].dropna().map(short) | |
| counts = raw.value_counts().to_dict() | |
| labels = [o for o in order if o in counts] | |
| values = [counts[o] for o in labels] | |
| fig = go.Figure(go.Bar( | |
| x=labels, y=values, | |
| marker=dict(color=[GREEN_LIGHT, GREEN_MID, GREEN, GREEN_DEEP][:len(labels)]), | |
| text=values, textposition="outside", cliponaxis=False, | |
| hovertemplate="%{x}: %{y}<extra></extra>", | |
| )) | |
| fig.update_layout(title="Developer experience") | |
| return style(fig, height=320) | |
| def fig_usage(df): | |
| if df.empty or "gradio_usage" not in df.columns: | |
| return empty_fig() | |
| counts = df["gradio_usage"].dropna().map(short).value_counts().to_dict() | |
| return hbar(counts, "How they use Gradio today", color=GREEN_MID, height=320) | |
| BADGE_ORDER = [ | |
| "🔌 Off the Grid", "🎯 Well-Tuned", "🎨 Off-Brand", | |
| "🦙 Llama Champion", "📡 Sharing is Caring", "📓 Field Notes", | |
| ] | |
| def fig_quests(df): | |
| if df.empty or "bonus_quests" not in df.columns: | |
| return empty_fig() | |
| counter = Counter() | |
| for val in df["bonus_quests"]: | |
| for item in parse_list(val): | |
| counter[short(item)] += 1 | |
| counts = {k: counter.get(k, 0) for k in BADGE_ORDER if counter.get(k, 0)} | |
| return hbar(counts, "Bonus-quest appetite", color=RUST, height=340) | |
| MODEL_FAMILIES = { | |
| "Qwen": ["qwen"], | |
| "Llama": ["llama"], | |
| "Gemma": ["gemma"], | |
| "SmolLM": ["smollm", "smol-lm", "smol lm"], | |
| "Phi": ["phi-", "phi3", "phi4", "phi-3", "phi-4", "phi2", "phi "], | |
| "Mistral": ["mistral", "ministral"], | |
| "MiniCPM": ["minicpm", "mini-cpm", "mini cpm"], | |
| "DeepSeek": ["deepseek"], | |
| "Granite": ["granite"], | |
| "Falcon": ["falcon"], | |
| "GPT-OSS": ["gpt-oss", "gptoss"], | |
| } | |
| def fig_models(df): | |
| if df.empty or "planned_small_model" not in df.columns: | |
| return empty_fig() | |
| counter = Counter() | |
| for val in df["planned_small_model"].dropna(): | |
| text = str(val).lower() | |
| if not text.strip(): | |
| continue | |
| for family, keys in MODEL_FAMILIES.items(): | |
| if any(k in text for k in keys): | |
| counter[family] += 1 | |
| counts = dict(counter) | |
| if not counts: | |
| return empty_fig("No models named yet") | |
| return hbar(counts, "Most-mentioned model families", color=GREEN, height=340) | |
| # ---------------------------------------------------------------- KPI + footer | |
| def kpi_card(label, value, sub, delay): | |
| return f""" | |
| <div class="bsh-kpi" style="animation-delay:{delay}ms"> | |
| <div class="bsh-kpi-label">{label}</div> | |
| <div class="bsh-kpi-value">{value}</div> | |
| <div class="bsh-kpi-sub">{sub}</div> | |
| </div>""" | |
| def pct(part, whole): | |
| return f"{round(100 * part / whole)}%" if whole else "—" | |
| def build_top(df): | |
| """Banner on the left + the four KPI tiles stacked in a column on the right.""" | |
| if df is None or len(df) == 0: | |
| cards = ( | |
| kpi_card("Total registrations", "—", "loading…", 0) | |
| + kpi_card("Returning Gradio builders", "—", "", 90) | |
| + kpi_card("Industry developers", "—", "", 180) | |
| + kpi_card("Advanced & expert", "—", "", 270) | |
| ) | |
| else: | |
| total = len(df) | |
| gradio_events = {"MCP 1st Birthday", "Agents & MCP Hackathon"} | |
| returning = sum(1 for v in df.get("previous_participation", []) if set(parse_list(v)) & gradio_events) | |
| usage = df.get("gradio_usage", pd.Series(dtype=str)).fillna("") | |
| industry = int(usage.str.startswith("Professional").sum()) | |
| exp = df.get("experience_level", pd.Series(dtype=str)).fillna("") | |
| advanced = int(exp.str.startswith(("Advanced", "Expert")).sum()) | |
| cards = ( | |
| kpi_card("Total registrations", f"{total:,}", "builders signed up", 0) | |
| + kpi_card("Returning Gradio builders", f"{returning:,}", f"{pct(returning, total)} came back for more", 90) | |
| + kpi_card("Industry developers", pct(industry, total), f"{industry:,} build with Gradio at work", 180) | |
| + kpi_card("Advanced & expert", pct(advanced, total), f"{advanced:,} seasoned AI devs", 270) | |
| ) | |
| banner = f'<div class="bsh-banner-wrap"><img src="{BANNER}" alt="Build Small Hackathon" /></div>' | |
| kpi_col = f'<div class="bsh-kpi-col">{cards}</div>' | |
| return f'<div class="bsh-top">{banner}{kpi_col}</div>' | |
| def build_footer(df): | |
| total = len(df) | |
| updated = time.strftime("%b %d, %Y · %H:%M UTC", time.gmtime()) | |
| return f""" | |
| <div class="bsh-footer"> | |
| Aggregated & anonymized — no personal information is shown · | |
| <b>N = {total:,}</b> registrations · Last refreshed {updated} | |
| </div>""" | |
| # ---------------------------------------------------------------- orchestration | |
| def build_everything(): | |
| try: | |
| df = fetch_dataframe() | |
| except Exception as e: | |
| logger.error(f"Data load failed: {e}") | |
| msg = empty_fig("Could not load data — check the HF_TOKEN secret") | |
| warn = ('<div class="bsh-footer" style="color:#8b2e25">' | |
| 'Could not load the dataset. Confirm the Space has an <code>HF_TOKEN</code> ' | |
| 'secret with read access to the org.</div>') | |
| return (build_top(None), msg, msg, msg, msg, msg, msg, msg, msg, warn) | |
| return ( | |
| build_top(df), | |
| fig_momentum(df), | |
| fig_heard(df), | |
| fig_track(df), | |
| fig_returning(df), | |
| fig_experience(df), | |
| fig_usage(df), | |
| fig_quests(df), | |
| fig_models(df), | |
| build_footer(df), | |
| ) | |
| # ---------------------------------------------------------------- styling (CSS) | |
| CUSTOM_CSS = """ | |
| .gradio-container { background: #f3ead6 !important; font-family: 'Spline Sans', sans-serif !important; } | |
| footer { display: none !important; } | |
| /* Top row: banner on the left, KPI tiles stacked on the right */ | |
| .bsh-top { | |
| display: flex; | |
| gap: 12px; | |
| align-items: stretch; | |
| margin-bottom: 4px; | |
| } | |
| .bsh-banner-wrap { | |
| flex: 2 1 0; | |
| min-width: 0; | |
| border-radius: 16px; | |
| overflow: hidden; | |
| border: 1px solid rgba(139,111,71,0.30); | |
| box-shadow: 0 4px 18px rgba(45,80,22,0.16); | |
| line-height: 0; | |
| } | |
| .bsh-banner-wrap img { width: 100%; height: auto; display: block; } | |
| .bsh-kpi-col { | |
| flex: 1 1 0; | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| /* Header text strip — sits below the banner/KPI row (reduced height) */ | |
| .bsh-strip { | |
| position: relative; overflow: hidden; | |
| background: #fbf6e8; border: 1px solid rgba(139,111,71,0.30); | |
| border-radius: 14px; padding: 8px 20px; margin: 8px 0 2px; | |
| box-shadow: 0 2px 8px rgba(45,80,22,0.08); | |
| } | |
| .bsh-strip::before { | |
| content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; | |
| background: linear-gradient(180deg, #4a7c2e, #2d5016); | |
| } | |
| .bsh-eyebrow { | |
| font-size: 10px; letter-spacing: 2px; text-transform: uppercase; | |
| color: #6b4423; font-weight: 600; margin-bottom: 1px; | |
| } | |
| .bsh-h1 { | |
| font-family: 'Fraunces', Georgia, serif; font-weight: 900; | |
| font-size: clamp(18px, 2.6vw, 23px); color: #2d5016; line-height: 1.04; | |
| margin-bottom: 2px; | |
| } | |
| .bsh-sub { color: #6b4423; font-size: 12px; line-height: 1.35; max-width: 80ch; } | |
| /* KPI cards (stacked in the right-hand column) */ | |
| .bsh-kpi { | |
| flex: 1 1 0; | |
| background: #fbf6e8; border: 1px solid rgba(139,111,71,0.30); | |
| border-radius: 14px; padding: 10px 16px; | |
| box-shadow: 0 2px 8px rgba(45,80,22,0.08); | |
| opacity: 0; transform: translateY(10px); | |
| animation: bshRise 0.55s cubic-bezier(.2,.7,.3,1) forwards; | |
| position: relative; overflow: hidden; | |
| display: flex; flex-direction: column; justify-content: center; | |
| } | |
| .bsh-kpi::before { | |
| content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; | |
| background: linear-gradient(180deg, #4a7c2e, #2d5016); | |
| } | |
| .bsh-kpi-label { | |
| font-size: 10px; letter-spacing: 1.4px; text-transform: uppercase; | |
| color: #6b4423; font-weight: 600; | |
| } | |
| .bsh-kpi-value { | |
| font-family: 'Fraunces', Georgia, serif; font-weight: 900; | |
| font-size: 30px; color: #2d5016; line-height: 1.05; margin: 1px 0; | |
| } | |
| .bsh-kpi-sub { font-size: 11.5px; color: #6b4423; opacity: 0.85; } | |
| @keyframes bshRise { to { opacity: 1; transform: translateY(0); } } | |
| .bsh-section { | |
| font-family: 'Fraunces', Georgia, serif; font-weight: 600; | |
| color: #2d5016; font-size: 15px; letter-spacing: 0.3px; | |
| margin: 14px 0 2px; padding-left: 2px; | |
| } | |
| .bsh-section::before { content: "❋ "; color: #6b9039; } | |
| .bsh-footer { | |
| text-align: center; font-size: 12px; color: #6b4423; | |
| margin: 16px 0 6px; opacity: 0.9; | |
| } | |
| .bsh-footer code { | |
| background: rgba(74,124,46,0.10); padding: 1px 6px; border-radius: 4px; font-size: 11px; | |
| } | |
| #bsh-refresh { | |
| background: linear-gradient(135deg, #4a7c2e, #2d5016) !important; | |
| color: #f5ecd9 !important; border: 1px solid #6b4423 !important; | |
| font-weight: 600 !important; border-radius: 10px !important; | |
| } | |
| #bsh-refresh:hover { filter: brightness(1.07); } | |
| @media (max-width: 820px) { | |
| .bsh-top { flex-direction: column; } | |
| .bsh-kpi-col { display: grid; grid-template-columns: 1fr 1fr; } | |
| .bsh-kpi { flex: none; } | |
| } | |
| """ | |
| BANNER = ("https://cdn-uploads.huggingface.co/production/uploads/" | |
| "60d2dc1007da9c17c72708f8/VhVvEN0e8oZKxjIzT9Qi0.png") | |
| STRIP_HTML = """ | |
| <div class="bsh-strip"> | |
| <div class="bsh-eyebrow">Registration Dashboard · Live Overview</div> | |
| <div class="bsh-h1">Build Small Hackathon</div> | |
| <div class="bsh-sub">Who's joining us in the woods — a live, anonymized read on the | |
| builders this event is reaching. June 5–15, 2026.</div> | |
| </div> | |
| """ | |
| # ---------------------------------------------------------------- UI | |
| with gr.Blocks(title="Build Small Hackathon — Dashboard") as demo: | |
| top = gr.HTML(build_top(None)) | |
| gr.HTML(STRIP_HTML) | |
| with gr.Row(): | |
| momentum = gr.Plot(label=None) | |
| with gr.Row(): | |
| heard = gr.Plot(label=None) | |
| gr.HTML('<div class="bsh-section">Engagement</div>') | |
| with gr.Row(): | |
| track = gr.Plot(label=None) | |
| returning = gr.Plot(label=None) | |
| gr.HTML('<div class="bsh-section">Audience quality</div>') | |
| with gr.Row(): | |
| experience = gr.Plot(label=None) | |
| usage = gr.Plot(label=None) | |
| gr.HTML('<div class="bsh-section">Technical appetite</div>') | |
| with gr.Row(): | |
| quests = gr.Plot(label=None) | |
| models = gr.Plot(label=None) | |
| footer = gr.HTML() | |
| with gr.Row(): | |
| refresh = gr.Button("↻ Refresh data", elem_id="bsh-refresh", scale=0) | |
| outputs = [top, momentum, heard, track, returning, experience, usage, quests, models, footer] | |
| demo.load(fn=build_everything, inputs=None, outputs=outputs) | |
| refresh.click(fn=lambda: build_everything(), inputs=None, outputs=outputs) | |
| if __name__ == "__main__": | |
| auth = ("sponsor", DASHBOARD_PASSWORD) if DASHBOARD_PASSWORD else None | |
| demo.launch( | |
| css=CUSTOM_CSS, | |
| head=HEAD, | |
| auth=auth, | |
| theme=gr.themes.Soft(primary_hue="green", secondary_hue="amber", neutral_hue="stone"), | |
| ) |