"""First Contact — Gradio Space entrypoint (SPEC.md §0, §9). Teach an alien that knows *words* but has never lived a human life. The model is a stateless function; the alien's growing understanding lives in a plain-Python concept ledger (game/) injected into the prompt each turn. ZeroGPU contract (SPEC §0): * Gradio SDK, model ≤32B. * The model is loaded onto 'cuda' at MODULE level (here, at import time) — NOT lazily inside the GPU function. * Only inference runs inside @spaces.GPU; all state mutation / win-checking / learning happens outside it. * All per-user state lives in gr.State — never module globals. """ from __future__ import annotations import html import os # `spaces` must be imported before torch for ZeroGPU's CUDA emulation. Locally # (no `spaces` package) fall back to a no-op decorator so the app still runs on # the StubBrain with zero extra dependencies. try: import spaces GPU = spaces.GPU except Exception: # pragma: no cover - exercised only off-Space def GPU(*args, **kwargs): if args and callable(args[0]): return args[0] def _decorator(fn): return fn return _decorator import gradio as gr # css/theme live on Blocks() in Gradio <=5 (the pinned Space target) but moved to # launch() in Gradio 6. Detect so custom styling applies whichever version is # resolved locally vs. on the Space. _GR_MAJOR = int(gr.__version__.split(".")[0]) from game.brain import LocalBrain, make_brain from game.challenges import CHALLENGES from game.engine import ( advance_challenge, confirm_candidate, current_challenge, is_arc_complete, new_session, reject_candidate, run_turn, ) from game.models import Concept, GameSession, WorldState from game.world import check_win # --------------------------------------------------------------------------- # # Brain — created ONCE at module level. For BRAIN=local this loads the model to # 'cuda' here (startup), per SPEC §0. Only the inference call below is wrapped in # @spaces.GPU; run_turn() does all mutation outside it. # --------------------------------------------------------------------------- # try: _BRAIN = make_brain() except Exception as exc: # model download/load failed — keep the Space UP on stub print( f"[brain] could not build the configured brain: {exc!r}\n" "[brain] falling back to StubBrain. On the Space, set ZeroGPU hardware " "and BRAIN=local (HF token in secrets for gated models).", flush=True, ) from game.brain import StubBrain _BRAIN = StubBrain() _USE_GPU = isinstance(_BRAIN, LocalBrain) # Loud startup line so the Space logs show which alien is live — a StubBrain here # means visitors get canned lines (set BRAIN=local), not the real model. print( "[brain] active: " + type(_BRAIN).__name__ + (f" ({_BRAIN.model_id})" if _USE_GPU else " — zero-GPU stand-in, NOT the real model"), flush=True, ) # Sized from the 2026-06 bake-off: Qwen2.5-14B measured ~3.5s median / ~5s p90 # per call, so 20s covers the slow tail ~4x over while keeping ZeroGPU queue # priority high (shorter declared durations queue better; 120 was the # pre-measurement guess). Bump this if you switch to a slower model. @GPU(duration=20) def _generate_on_gpu(prompt: str) -> str: return _BRAIN.respond(prompt) class _TurnBrain: """Routes the model call through the @spaces.GPU function when (and only when) a real GPU brain is loaded. Stub/Modal go straight through. Infrastructure failures (ZeroGPU queue timeout, quota exhaustion, the duration cap) are swallowed into an empty reply: empty text fails parsing, which flows into the §4 retry -> safe-fallback path, so the player sees the alien 'not understanding' instead of an error toast (SPEC §4: never crash, never leak).""" def respond(self, prompt: str) -> str: try: return _generate_on_gpu(prompt) if _USE_GPU else _BRAIN.respond(prompt) except Exception as exc: print(f"[brain] respond failed: {exc}", flush=True) return "" _turn_brain = _TurnBrain() PLACEHOLDER = "Speak to the alien… (e.g. “hide the blue stone from the other one”)" # --------------------------------------------------------------------------- # # Rendering — pure functions over session state -> HTML # --------------------------------------------------------------------------- # def _chip(obj, concealed: bool = False) -> str: color = "blue" if "blue" in obj.id else "red" if "red" in obj.id else "neutral" if concealed: return ( f'' f'{html.escape(obj.name)}' ) return f'{html.escape(obj.name)}' def render_world(world: WorldState) -> str: held = {a_id: list(a.holding) for a_id, a in world.agents.items()} in_basket = [o for o in world.objects.values() if o.location == "basket"] on_ground = [o for o in world.objects.values() if o.location == "ground"] def agent_zone(agent_id: str, glyph: str, label: str) -> str: chips = "".join(_chip(world.objects[o]) for o in held.get(agent_id, [])) chips = chips or 'empty-handed' return ( f'
{glyph} {label}
' f'
{chips}
' ) basket_chips = "".join(_chip(o, concealed=o.hidden) for o in in_basket) or ( 'nothing inside' ) ground_chips = "".join(_chip(o) for o in on_ground) or 'bare' return ( '
' + agent_zone("alien", "◉", "the alien (you teach)") + agent_zone("other", "◎", "the other one") + f'
⬓ the basket
' f'
{basket_chips}
' + f'
· the ground
' f'
{ground_chips}
' + "
" ) def render_convo(history: list[dict]) -> str: # Each entry is one self-contained block; trim to the recent tail so the # latest turn (and any win beat) is what's on screen without inner scrolling. blocks = [] for entry in history[-12:]: who, text = entry["who"], html.escape(entry.get("text", "")) if who == "player": blocks.append(f'
you{text}
') elif who == "alien": gap = ( f'
…did not understand — {html.escape(entry["gap"])}
' if entry.get("gap") else "" ) act = ( f'
› {html.escape(entry["action"])}
' if entry.get("action") else "" ) blocks.append( f'
{text}
{act}{gap}
' ) else: # system beat kind = entry.get("kind") or "beat" blocks.append(f'
{text}
') if not blocks: blocks.append('
The alien waits. Tell it what to do.
') return f'
{"".join(blocks)}
' def _concept_card(c: Concept, highlight: set[str], label_of: dict[str, str]) -> str: klass = "concept " + ("innate" if c.taught_on_turn == 0 else "learned") if c.id in highlight: klass += " glow" applied = f'applied ×{c.times_applied}' if c.times_applied else "" if c.built_from: names = " + ".join(label_of.get(i, i) for i in c.built_from) built = f'from {html.escape(names)}' else: built = "" return ( f'
{html.escape(c.label)}
' f'
{html.escape(c.understanding)}
' f'
{built}{applied}
' ) def render_ledger(ledger: list[Concept], highlight: set[str] | None = None) -> str: # Learned concepts first (newest on top — the constellation that just grew), # innate primitives below a divider. The learned ones are the §9 screenshot. highlight = highlight or set() label_of = {c.id: c.label for c in ledger} learned = sorted( (c for c in ledger if c.taught_on_turn != 0), key=lambda c: c.taught_on_turn, reverse=True, ) innate = [c for c in ledger if c.taught_on_turn == 0] cards = [_concept_card(c, highlight, label_of) for c in learned] if learned: cards.append('
innate primitives
') cards.extend(_concept_card(c, highlight, label_of) for c in innate) return f'
{"".join(cards)}
' def render_header(session: GameSession) -> str: ch = current_challenge(session) n, total = session.challenge_index + 1, len(CHALLENGES) is_gen = ch.teaches is None and bool(ch.relies_on) kind = "generalize" if is_gen else ("teach" if ch.teaches else "warm-up") note = ( '
↯ No new teaching this round — just ask, and watch ' 'whether it understands on its own.
' if is_gen else "" ) return ( f'
CHALLENGE {n}/{total} · ' f'{kind}
' f'
{html.escape(ch.title)}
' f'
{html.escape(ch.setup_blurb)}
{note}
' ) def render_goal(session: GameSession) -> str: """A concrete objective + live status from the real win-predicate, so the player can see what actually counts as winning the stage.""" ch = current_challenge(session) is_gen = ch.teaches is None and bool(ch.relies_on) if check_win(session.world, ch): pill = ('✓ it understood!' if is_gen else '✓ done') else: pill = ('◌ watching…' if is_gen else '◌ not yet') return ( '
goal' f'{html.escape(ch.goal)}{pill}
' ) def _learn_offer_html(candidate: dict) -> str: return ( '
The alien thinks it learned ' f'something new: {html.escape(candidate.get("label", "?"))} — ' f'“{html.escape(candidate.get("understanding", ""))}”
' ) def _win_banner(ch, reapplied: tuple[str, ...]) -> str: if ch.teaches is None and ch.relies_on: used = " + ".join(reapplied) if reapplied else " + ".join(ch.relies_on) return ( '' ) return ( '' ) def render_finale(session: GameSession) -> str: """The end-of-arc payoff + shareable artifact: what the alien began with, what you taught it, and — set apart — what it reached on its own (SPEC §9, §12).""" learned = [c for c in session.ledger if c.taught_on_turn != 0] generalized = [c for c in learned if c.via_generalization] taught = [c for c in learned if not c.via_generalization] innate = [c for c in session.ledger if c.taught_on_turn == 0] label_of = {c.id: c.label for c in session.ledger} nt, ng = len(taught), len(generalized) if nt and ng: lead = (f"It began with {len(innate)} bare senses. You taught it {nt}. " f"From those, it reached {ng} more on its own.") elif ng: lead = (f"It began with {len(innate)} bare senses. On its own it reached for " f"{ng} idea{'s' if ng != 1 else ''} — though you taught it none to " f"build on.") elif nt: lead = (f"It began with {len(innate)} bare senses. You taught it {nt}, " f"and it carried them into new situations.") else: lead = f"It began with {len(innate)} bare senses, and there it remains — for now." def col(title: str, concepts: list[Concept], self_made: bool = False) -> str: if not concepts: return "" items = "" for c in concepts: lineage = "" if c.built_from: names = " + ".join(label_of.get(i, i) for i in c.built_from) lineage = f' (from {html.escape(names)})' items += (f"
  • {html.escape(c.label)} — " f"{html.escape(c.understanding)}{lineage}
  • ") cls = "finale-col self" if self_made else "finale-col" return f'
    {title}
    ' cols = col("you taught it", taught) + col("it understood alone", generalized, True) return ( '' ) # --------------------------------------------------------------------------- # # State -> outputs bundle # --------------------------------------------------------------------------- # def render_all(session: GameSession, highlight: set[str] | None = None) -> dict: return { state: session, header_html: render_header(session), goal_html: render_goal(session), world_html: render_world(session.world), convo_html: render_convo(session.history), ledger_html: render_ledger(session.ledger, highlight), } def _maybe_finale(session: GameSession, out: dict) -> bool: """If the arc is complete and no concept is awaiting confirmation, replace the per-stage banner with the finale and lock the input. Reachable from on_send (last stage won, nothing to confirm) and on_confirm/on_reject (last concept just resolved) — the old code only reached it via a continue button that the last stage never showed.""" if is_arc_complete(session) and not session.pending_candidate: out[success_banner] = gr.update(visible=True, value=render_finale(session)) out[continue_btn] = gr.update(visible=False) out[learn_row] = gr.update(visible=False) out[msg] = gr.update(interactive=False, value="", placeholder="— first contact complete · press restart —") return True return False # --------------------------------------------------------------------------- # # Handlers # --------------------------------------------------------------------------- # def on_send(session: GameSession, message: str) -> dict: message = (message or "").strip() if not message: return {msg: gr.update()} res = run_turn(session, message, _turn_brain) out = render_all(session, highlight=set(res.reapplied)) out[msg] = gr.update(value="") if res.learn_offer: out[learn_row] = gr.update(visible=True) out[learn_label] = _learn_offer_html(res.learn_offer) else: out[learn_row] = gr.update(visible=False) if res.won: is_last = session.challenge_index >= len(CHALLENGES) - 1 # If the alien proposed a concept, gate "continue" until the player # resolves it (Yes/No). Otherwise the prominent continue button silently # drops the concept, and the arc reaches the finale having taught nothing. out[continue_btn] = gr.update(visible=(not is_last and not res.learn_offer)) out[success_banner] = gr.update( visible=True, value=_win_banner(current_challenge(session), res.reapplied) ) else: out[continue_btn] = gr.update(visible=False) out[success_banner] = gr.update(visible=False) _maybe_finale(session, out) # last stage won with nothing to confirm -> finale return out def _reveal_continue(session: GameSession, out: dict) -> None: """After a pending concept is resolved, show continue if the stage is won (or the finale if the arc is done).""" if session.won_current and session.challenge_index < len(CHALLENGES) - 1: out[continue_btn] = gr.update(visible=True) _maybe_finale(session, out) def on_confirm(session: GameSession) -> dict: cid = (session.pending_candidate or {}).get("id") confirm_candidate(session) out = render_all(session, highlight={cid} if cid else set()) out[learn_row] = gr.update(visible=False) _reveal_continue(session, out) # concept resolved -> continue (or finale) return out def on_reject(session: GameSession) -> dict: reject_candidate(session) out = render_all(session) out[learn_row] = gr.update(visible=False) _reveal_continue(session, out) return out def on_continue(session: GameSession) -> dict: ok = advance_challenge(session) out = render_all(session) out[continue_btn] = gr.update(visible=False) out[learn_row] = gr.update(visible=False) if ok: out[success_banner] = gr.update(visible=False) out[msg] = gr.update(interactive=True, value="", placeholder=PLACEHOLDER) else: # safety net — the last stage shows no continue button, so this is rare out[success_banner] = gr.update(visible=True, value=render_finale(session)) out[msg] = gr.update(interactive=False, value="", placeholder="— first contact complete · press restart —") return out def on_restart() -> dict: session = new_session() out = render_all(session) out[continue_btn] = gr.update(visible=False) out[success_banner] = gr.update(visible=False) out[learn_row] = gr.update(visible=False) out[msg] = gr.update(interactive=True, value="", placeholder=PLACEHOLDER) return out # --------------------------------------------------------------------------- # # CSS — committed aesthetic: a xenolinguist's first-contact terminal. # Deep ink ground, bioluminescent accent for understanding, amber for confusion. # --------------------------------------------------------------------------- # CSS = """ @import url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,300;0,400;0,500;1,400&family=Space+Mono:wght@400;700&family=Major+Mono+Display&display=swap'); :root { --ink: #0b0e13; --ink2: #11151d; --panel: #141a24; --edge: #232c3a; --text: #d8dee9; --muted: #7c889c; --bio: #8be0c8; --bio-dim: #3f6b62; --amber: #e2b075; --blue: #6fa8dc; --red: #e0736f; } .gradio-container { background: radial-gradient(1200px 600px at 70% -10%, #16202c 0%, var(--ink) 55%) !important; color: var(--text) !important; font-family: 'Spectral', serif !important; } #masthead { text-align:center; padding: 10px 0 2px; } #masthead .title { font-family:'Major Mono Display', monospace; font-size: 1.9rem; letter-spacing: .12em; color: var(--bio); } #masthead .sub { color: var(--muted); font-style: italic; font-size: .95rem; } .fc-panel { background: var(--panel); border: 1px solid var(--edge); border-radius: 12px; padding: 14px; } .fc-panel .panel-label { font-family:'Space Mono', monospace; font-size:.72rem; letter-spacing:.18em; color: var(--muted); text-transform:uppercase; margin-bottom:8px; } /* header */ .ch-head { margin: 6px 0 2px; } .ch-meta { font-family:'Space Mono',monospace; font-size:.72rem; letter-spacing:.14em; color:var(--muted); } .ch-kind { padding:1px 7px; border:1px solid var(--edge); border-radius:999px; } .ch-kind.teach { color:var(--bio); border-color:var(--bio-dim); } .ch-kind.generalize { color:var(--amber); border-color:#6b5535; } .ch-title { font-size:1.5rem; color:var(--text); margin:3px 0; } .ch-blurb { color:var(--muted); font-size:.98rem; line-height:1.45; } /* world stage */ .stage { display:grid; grid-template-columns:1fr 1fr; gap:10px; } .zone { background: var(--ink2); border:1px solid var(--edge); border-radius:10px; padding:10px; min-height:64px; } .zone.ground, .zone.basket { grid-column: span 1; } .zone-head { font-family:'Space Mono',monospace; font-size:.72rem; color:var(--muted); letter-spacing:.1em; margin-bottom:8px; } .zone-body { display:flex; flex-wrap:wrap; gap:6px; } .zone.basket { background: repeating-linear-gradient(45deg,#10141c,#10141c 8px,#121822 8px,#121822 16px); } .chip { display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:999px; background:#1b2330; border:1px solid var(--edge); font-size:.9rem; } .chip .dot { width:9px; height:9px; border-radius:50%; background:var(--muted); } .chip.blue .dot { background:var(--blue); box-shadow:0 0 8px var(--blue); } .chip.red .dot { background:var(--red); box-shadow:0 0 8px var(--red); } .chip.concealed { border-style:dashed; opacity:.65; color:var(--amber); } .chip .lock { color:var(--amber); font-size:.8rem; } .empty { color:#4b5566; font-style:italic; font-size:.85rem; } /* conversation */ .convo { display:flex; flex-direction:column; gap:10px; padding-right:4px; } .turn.player { align-self:flex-end; background:#1a2230; border:1px solid var(--edge); border-radius:12px 12px 2px 12px; padding:7px 12px; max-width:85%; } .turn.player .tag { font-family:'Space Mono',monospace; font-size:.62rem; color:var(--muted); display:block; letter-spacing:.12em; } .turn.alien { align-self:flex-start; max-width:90%; } .voice { font-family:'Space Mono',monospace; font-size:.95rem; line-height:1.5; color:var(--bio); border-left:2px solid var(--bio-dim); padding:6px 12px; background:#101820; border-radius:2px 10px 10px 2px; } .gap { color:var(--amber); font-style:italic; font-size:.85rem; padding:2px 12px; opacity:.85; } .beat { text-align:center; font-family:'Space Mono',monospace; font-size:.74rem; letter-spacing:.14em; color:var(--muted); text-transform:uppercase; padding:4px 0; } .beat.win, .beat.learn { color:var(--bio); } .beat.hint { color:#4b5566; } /* ledger */ .ledger { display:flex; flex-direction:column; gap:8px; max-height:520px; overflow-y:auto; padding-right:4px; } .ledger-div { font-family:'Space Mono',monospace; font-size:.64rem; letter-spacing:.16em; text-transform:uppercase; color:#4b5566; text-align:center; margin:4px 0; border-top:1px dashed var(--edge); padding-top:8px; } .concept { border:1px solid var(--edge); border-radius:10px; padding:9px 11px; background:var(--ink2); } .concept.innate { opacity:.7; } .concept.learned { border-color:var(--bio-dim); background:linear-gradient(180deg,#13201c,#11151d); } .c-label { font-size:1.05rem; color:var(--text); } .concept.learned .c-label { color:var(--bio); } .c-gloss { color:var(--muted); font-size:.88rem; line-height:1.4; } .c-meta { display:flex; gap:10px; margin-top:5px; font-family:'Space Mono',monospace; font-size:.66rem; letter-spacing:.08em; } .built { color:var(--amber); } .applied { color:var(--bio); } @keyframes glowpulse { 0%{box-shadow:0 0 0 0 rgba(139,224,200,0);} 30%{box-shadow:0 0 22px 4px rgba(139,224,200,.55);} 100%{box-shadow:0 0 0 0 rgba(139,224,200,0);} } .concept.glow { animation: glowpulse 1.6s ease-out 1; border-color:var(--bio); } /* learn offer */ #learn_row { background:#15211d; border:1px solid var(--bio-dim); border-radius:12px; padding:6px 12px; } .offer-inner { color:var(--text); font-size:.95rem; } .offer-inner .spark { color:var(--bio); } /* banners */ .banner { text-align:center; border-radius:14px; padding:18px; margin-top:8px; } .banner.win { background:#13201c; border:1px solid var(--bio-dim); } .banner.generalize { background:radial-gradient(600px 200px at 50% 0%, #1d2a25, #11151d); border:1px solid var(--bio); box-shadow:0 0 40px rgba(139,224,200,.18); } .b-big { font-family:'Major Mono Display',monospace; font-size:1.4rem; color:var(--bio); letter-spacing:.1em; } .banner.generalize .b-big { animation: glowpulse 2s ease-out 1; } .b-sub { color:var(--muted); font-style:italic; margin-top:6px; } /* goal strip + generalization note */ .goal-strip { display:flex; align-items:center; gap:10px; margin:8px 0 2px; font-size:.95rem; color:var(--text); } .goal-label { font-family:'Space Mono',monospace; font-size:.6rem; letter-spacing:.18em; text-transform:uppercase; color:var(--muted); border:1px solid var(--edge); border-radius:4px; padding:2px 6px; } .goal-text { flex:1; } .goal-pill { font-family:'Space Mono',monospace; font-size:.72rem; letter-spacing:.05em; padding:2px 10px; border-radius:999px; white-space:nowrap; } .goal-pill.pending { color:var(--muted); border:1px solid var(--edge); } .goal-pill.done { color:#06120e; background:var(--bio); font-weight:700; } .ch-note { color:var(--amber); font-size:.9rem; font-style:italic; margin-top:5px; } /* mechanical action line under the alien's voice */ .act { font-family:'Space Mono',monospace; font-size:.78rem; color:var(--muted); padding:3px 12px 0; } /* capability hint */ .cap-hint { color:var(--muted); font-size:.82rem; text-align:center; margin:7px 0 0; opacity:.85; } .cap-hint b { color:var(--text); font-weight:400; } /* finale */ .banner.finale { background:radial-gradient(720px 280px at 50% 0%, #1d2a25, #0f1318); border:1px solid var(--bio); box-shadow:0 0 55px rgba(139,224,200,.22); } .banner.finale .b-big { animation: glowpulse 2.4s ease-out 1; } .finale-cols { display:flex; gap:16px; justify-content:center; margin-top:14px; text-align:left; flex-wrap:wrap; } .finale-col { flex:1; min-width:230px; background:var(--ink2); border:1px solid var(--edge); border-radius:10px; padding:10px 14px; } .finale-col.self { border-color:var(--bio); background:linear-gradient(180deg,#13201c,#11151d); } .finale-h { font-family:'Space Mono',monospace; font-size:.62rem; letter-spacing:.16em; text-transform:uppercase; color:var(--muted); margin-bottom:6px; } .finale-col.self .finale-h { color:var(--bio); } .finale-col ul { margin:0; padding-left:16px; } .finale-col li { font-size:.9rem; color:var(--text); margin:5px 0; line-height:1.4; } .finale-col .from { color:var(--amber); font-family:'Space Mono',monospace; font-size:.7rem; } .finale-foot { color:var(--muted); font-style:italic; margin-top:14px; font-size:.9rem; } /* inputs */ #send_btn { background:var(--bio-dim) !important; color:#06120e !important; border:none !important; } footer { display:none !important; } """ # --------------------------------------------------------------------------- # # Layout # --------------------------------------------------------------------------- # # On Gradio 5 (the Space's pinned sdk_version) css/theme belong on the Blocks # constructor; on Gradio 6 they belong on launch() (handled in __main__). _blocks_style = {} if _GR_MAJOR >= 6 else {"css": CSS, "theme": gr.themes.Base()} with gr.Blocks(title="First Contact", **_blocks_style) as demo: state = gr.State(new_session()) # Deliver the CSS via an inline ", container=False) gr.HTML( '
    first contact
    ' '
    teach an alien that knows words but has never lived a life
    ' ) header_html = gr.HTML() goal_html = gr.HTML() with gr.Row(equal_height=False): with gr.Column(scale=5): with gr.Group(elem_classes="fc-panel"): gr.HTML('
    the world
    ') world_html = gr.HTML() with gr.Group(elem_classes="fc-panel"): gr.HTML('
    first contact log
    ') convo_html = gr.HTML() with gr.Column(scale=4): with gr.Group(elem_classes="fc-panel"): gr.HTML('
    what the alien understands
    ') ledger_html = gr.HTML() success_banner = gr.HTML(visible=False) with gr.Row(visible=False, elem_id="learn_row") as learn_row: learn_label = gr.HTML() yes_btn = gr.Button("Yes — it learned that", scale=0, variant="primary") no_btn = gr.Button("No", scale=0) with gr.Row(): msg = gr.Textbox(placeholder=PLACEHOLDER, show_label=False, scale=8, autofocus=True) send_btn = gr.Button("speak", elem_id="send_btn", scale=1) gr.HTML( '
    the alien can move, pick up a thing, ' 'put it in the basket, give it, point, or wait — ' 'say it however you like; it will try to understand.
    ' ) with gr.Row(): continue_btn = gr.Button("continue →", visible=False, variant="primary") restart_btn = gr.Button("restart", scale=0) # outputs every handler may touch OUT = [state, header_html, goal_html, world_html, convo_html, ledger_html, learn_row, learn_label, continue_btn, success_banner, msg] send_btn.click(on_send, [state, msg], OUT) msg.submit(on_send, [state, msg], OUT) yes_btn.click(on_confirm, [state], OUT) no_btn.click(on_reject, [state], OUT) continue_btn.click(on_continue, [state], OUT) restart_btn.click(on_restart, None, OUT) demo.load(render_all, [state], [state, header_html, goal_html, world_html, convo_html, ledger_html]) if __name__ == "__main__": # Gradio 6 takes css/theme here; Gradio 5 already has them on Blocks above. _launch_style = {"css": CSS, "theme": gr.themes.Base()} if _GR_MAJOR >= 6 else {} demo.queue().launch(**_launch_style)