Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import base64 | |
| import html | |
| import json | |
| import mimetypes | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Any | |
| import gradio as gr | |
| from lifechoice_engine import ( | |
| METRICS, | |
| MODEL_ID, | |
| SimulationSession, | |
| choose, | |
| current_node, | |
| character_expression, | |
| environment_image, | |
| environment_state, | |
| prefetch_node, | |
| split_dilemma, | |
| start_session, | |
| ) | |
| ROOT = Path(__file__).parent | |
| PERSONA_ASSETS = { | |
| "Family": ROOT / "assets" / "personas" / "family.webp", | |
| "Friend": ROOT / "assets" / "personas" / "friend.webp", | |
| "Mentor": ROOT / "assets" / "personas" / "mentor.webp", | |
| "Partner": ROOT / "assets" / "personas" / "friend.webp", | |
| "Inner Voice": ROOT / "assets" / "personas" / "mentor.webp", | |
| } | |
| CSS = """ | |
| :root { | |
| --night: #070a12; | |
| --panel: rgba(9, 13, 24, .90); | |
| --line: rgba(255, 255, 255, .14); | |
| --cream: #fff7df; | |
| --muted: #bbb7aa; | |
| --gold: #ffc857; | |
| --coral: #ff6b5f; | |
| --cyan: #5ce1e6; | |
| --green: #74e59b; | |
| } | |
| * { box-sizing: border-box; } | |
| body, .gradio-container { | |
| background: | |
| radial-gradient(circle at 12% -8%, rgba(65, 71, 150, .34), transparent 32rem), | |
| linear-gradient(180deg, #11162a 0%, var(--night) 48%, #05070c 100%) !important; | |
| color: var(--cream) !important; | |
| } | |
| .gradio-container { | |
| max-width: 1180px !important; | |
| padding: 22px 22px 34px !important; | |
| font-family: Inter, ui-sans-serif, system-ui, sans-serif !important; | |
| } | |
| footer { display: none !important; } | |
| .app-header { | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: space-between; | |
| gap: 24px; | |
| margin: 5px 2px 18px; | |
| } | |
| .brand-lockup h1 { | |
| color: #fff !important; | |
| font-family: "Arial Black", Impact, sans-serif; | |
| font-size: clamp(2rem, 6vw, 4.7rem); | |
| line-height: .84; | |
| letter-spacing: -.075em; | |
| margin: 7px 0 9px; | |
| text-transform: uppercase; | |
| } | |
| .brand-lockup h1 span { color: var(--gold); } | |
| .brand-lockup p { color: var(--muted) !important; margin: 0; max-width: 670px; } | |
| .eyebrow { | |
| color: var(--cyan) !important; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace; | |
| font-size: .72rem; | |
| font-weight: 800; | |
| letter-spacing: .14em; | |
| text-transform: uppercase; | |
| } | |
| .model-chip { | |
| border: 1px solid rgba(92, 225, 230, .4); | |
| border-radius: 999px; | |
| color: var(--cyan); | |
| flex: 0 0 auto; | |
| font: 700 .72rem ui-monospace, monospace; | |
| padding: 9px 12px; | |
| } | |
| .setup-card { | |
| background: linear-gradient(145deg, rgba(22, 27, 48, .98), rgba(8, 11, 21, .98)); | |
| border: 1px solid var(--line); | |
| border-radius: 22px; | |
| box-shadow: 0 24px 80px rgba(0, 0, 0, .36); | |
| overflow: hidden; | |
| padding: 10px; | |
| } | |
| .setup-intro { | |
| min-height: 100%; | |
| padding: 26px 22px; | |
| background: | |
| linear-gradient(180deg, rgba(7, 10, 18, .08), rgba(7, 10, 18, .92)), | |
| url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80'%3E%3Cpath d='M0 40h80M40 0v80' stroke='%23ffffff' stroke-opacity='.035'/%3E%3C/svg%3E"); | |
| border-radius: 15px; | |
| } | |
| .setup-intro h2 { | |
| color: #fff !important; | |
| font-family: "Arial Black", Impact, sans-serif; | |
| font-size: clamp(1.8rem, 4vw, 3.2rem); | |
| letter-spacing: -.05em; | |
| line-height: .95; | |
| margin: 24px 0 14px; | |
| text-transform: uppercase; | |
| } | |
| .setup-intro p { color: var(--muted) !important; line-height: 1.65; } | |
| .feature-list { display: grid; gap: 11px; margin-top: 26px; } | |
| .feature { | |
| align-items: center; | |
| border-top: 1px solid rgba(255,255,255,.09); | |
| color: #ddd8ca; | |
| display: flex; | |
| font-size: .88rem; | |
| gap: 10px; | |
| padding-top: 11px; | |
| } | |
| .feature b { color: var(--gold); font: 800 .72rem ui-monospace, monospace; } | |
| .form-panel { padding: 18px 14px 12px; } | |
| .gradio-container label span { color: #d9d5c8 !important; font-weight: 700 !important; } | |
| .gradio-container textarea, | |
| .gradio-container input { | |
| color: #fff !important; | |
| } | |
| .gradio-container .block { | |
| border-color: rgba(255,255,255,.12) !important; | |
| } | |
| #begin-button, #commit-button { | |
| background: linear-gradient(135deg, var(--gold), #ff9f43) !important; | |
| border: 0 !important; | |
| box-shadow: 0 10px 30px rgba(255, 159, 67, .22); | |
| color: #17120a !important; | |
| font-family: "Arial Black", Impact, sans-serif !important; | |
| letter-spacing: .04em; | |
| min-height: 50px; | |
| text-transform: uppercase; | |
| } | |
| #begin-button:hover, #commit-button:hover { filter: brightness(1.08); transform: translateY(-1px); } | |
| .microcopy { color: #8f8b80 !important; font-size: .75rem; line-height: 1.5; margin: 9px 2px 0; } | |
| .game-frame { | |
| aspect-ratio: 16 / 9; | |
| background: #111; | |
| border: 1px solid rgba(255,255,255,.18); | |
| border-radius: 22px; | |
| box-shadow: 0 26px 90px rgba(0,0,0,.52); | |
| isolation: isolate; | |
| min-height: 530px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .world-bg { | |
| background-position: center; | |
| background-size: cover; | |
| filter: saturate(.9) contrast(1.04); | |
| inset: 0; | |
| position: absolute; | |
| transform: scale(1.015); | |
| z-index: -3; | |
| } | |
| .world-shade { | |
| background: | |
| linear-gradient(90deg, rgba(3,5,10,.88) 0%, rgba(3,5,10,.34) 57%, rgba(3,5,10,.72) 100%), | |
| linear-gradient(0deg, rgba(3,5,10,.94) 0%, transparent 38%, rgba(3,5,10,.25) 100%); | |
| inset: 0; | |
| position: absolute; | |
| z-index: -2; | |
| } | |
| .scanlines { | |
| background: repeating-linear-gradient(0deg, transparent 0, transparent 3px, rgba(0,0,0,.08) 4px); | |
| inset: 0; | |
| pointer-events: none; | |
| position: absolute; | |
| z-index: 5; | |
| } | |
| .stage-top { | |
| align-items: flex-start; | |
| display: flex; | |
| justify-content: space-between; | |
| padding: clamp(16px, 3vw, 28px); | |
| } | |
| .scenario-card { | |
| backdrop-filter: blur(12px); | |
| background: var(--panel); | |
| border: 1px solid rgba(255,255,255,.17); | |
| border-left: 4px solid var(--gold); | |
| border-radius: 6px 16px 16px 6px; | |
| box-shadow: 0 18px 55px rgba(0,0,0,.34); | |
| max-width: 64%; | |
| padding: clamp(15px, 2.5vw, 25px); | |
| } | |
| .scene-meta { | |
| color: var(--cyan); | |
| font: 800 .68rem ui-monospace, monospace; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| } | |
| .scenario-card h2 { | |
| color: #fff !important; | |
| font-family: "Arial Black", Impact, sans-serif; | |
| font-size: clamp(1.25rem, 3vw, 2.15rem); | |
| letter-spacing: -.035em; | |
| line-height: 1; | |
| margin: 9px 0 12px; | |
| text-transform: uppercase; | |
| } | |
| .scenario-card p { color: #f0ecdf !important; font-size: clamp(.87rem, 1.5vw, 1rem); line-height: 1.58; margin: 0; } | |
| .source-line { color: #8f8b80; display: block; font: .64rem ui-monospace, monospace; margin-top: 14px; } | |
| .hud { | |
| backdrop-filter: blur(10px); | |
| background: rgba(7,10,18,.84); | |
| border: 1px solid rgba(255,255,255,.14); | |
| border-radius: 14px; | |
| min-width: 210px; | |
| padding: 13px; | |
| width: 25%; | |
| } | |
| .hud-title { color: var(--gold); font: 900 .68rem ui-monospace, monospace; letter-spacing: .12em; margin-bottom: 10px; } | |
| .metric-row { margin: 8px 0; } | |
| .metric-label { color: #d6d2c7; display: flex; font: 700 .62rem ui-monospace, monospace; justify-content: space-between; text-transform: uppercase; } | |
| .metric-track { background: rgba(255,255,255,.11); height: 5px; margin-top: 5px; overflow: hidden; } | |
| .metric-fill { background: linear-gradient(90deg, var(--cyan), var(--green)); height: 100%; } | |
| .metric-row.stress .metric-fill { background: linear-gradient(90deg, #ffb347, var(--coral)); } | |
| .stage-bottom { bottom: 0; display: flex; justify-content: space-between; left: 0; padding: 0 27px 24px; position: absolute; right: 0; } | |
| .persona-bubble { | |
| align-self: flex-end; | |
| backdrop-filter: blur(10px); | |
| background: rgba(10,13,22,.91); | |
| border: 1px solid rgba(255,255,255,.16); | |
| border-radius: 14px; | |
| display: flex; | |
| gap: 12px; | |
| max-width: 58%; | |
| padding: 10px 13px; | |
| } | |
| .persona-bubble img { border: 2px solid var(--cyan); border-radius: 8px; height: 58px; object-fit: cover; width: 58px; } | |
| .persona-copy b { color: var(--cyan); display: block; font: 800 .68rem ui-monospace, monospace; letter-spacing: .08em; text-transform: uppercase; } | |
| .persona-copy p { color: #eee9dc !important; font-size: .8rem; line-height: 1.4; margin: 5px 0 0; } | |
| .player-wrap { align-items: flex-end; display: flex; gap: 12px; } | |
| .state-pill { | |
| background: rgba(7,10,18,.82); | |
| border: 1px solid rgba(255,255,255,.15); | |
| border-radius: 999px; | |
| color: var(--cream); | |
| font: 800 .63rem ui-monospace, monospace; | |
| margin-bottom: 8px; | |
| padding: 7px 10px; | |
| text-transform: uppercase; | |
| } | |
| .player-sprite { | |
| background-repeat: no-repeat; | |
| background-size: 400% 100%; | |
| filter: drop-shadow(0 16px 9px rgba(0,0,0,.55)); | |
| height: 142px; | |
| image-rendering: pixelated; | |
| width: 126px; | |
| } | |
| .cascade-banner { | |
| animation: slide-in .28s ease-out; | |
| background: linear-gradient(90deg, rgba(255,107,95,.96), rgba(118,52,83,.94)); | |
| border: 1px solid rgba(255,255,255,.2); | |
| box-shadow: 0 10px 35px rgba(0,0,0,.35); | |
| color: #fff; | |
| left: 50%; | |
| max-width: 70%; | |
| padding: 10px 18px; | |
| position: absolute; | |
| text-align: center; | |
| top: 49%; | |
| transform: translate(-50%, -50%); | |
| z-index: 4; | |
| } | |
| .loading-stage { | |
| align-items: center; | |
| background: | |
| radial-gradient(circle at 50% 35%, rgba(92,225,230,.10), transparent 30%), | |
| linear-gradient(145deg, #11182a, #070a12); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| justify-content: center; | |
| min-height: 530px; | |
| text-align: center; | |
| } | |
| .loading-orbit { | |
| animation: orbit 1.05s linear infinite; | |
| border: 3px solid rgba(255,255,255,.11); | |
| border-radius: 50%; | |
| border-top-color: var(--gold); | |
| height: 52px; | |
| width: 52px; | |
| } | |
| .loading-stage h2 { color: #fff !important; font: 1.7rem "Arial Black", Impact, sans-serif; margin: 0; text-transform: uppercase; } | |
| .loading-stage p { color: var(--muted) !important; margin: 0; max-width: 460px; } | |
| .loading-steps { color: var(--cyan); font: 700 .67rem ui-monospace, monospace; letter-spacing: .09em; text-transform: uppercase; } | |
| .cascade-banner b { display: block; font: 900 .7rem ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; } | |
| .cascade-banner span { font-size: .76rem; } | |
| .report-card { | |
| backdrop-filter: blur(15px); | |
| background: rgba(7,10,18,.94); | |
| border: 1px solid rgba(255,255,255,.18); | |
| border-top: 4px solid var(--gold); | |
| border-radius: 16px; | |
| inset: 6%; | |
| overflow: auto; | |
| padding: clamp(20px, 4vw, 42px); | |
| position: absolute; | |
| z-index: 4; | |
| } | |
| .report-card h2 { color: var(--gold) !important; font: clamp(1.8rem, 5vw, 3.7rem)/.95 "Arial Black", Impact, sans-serif; letter-spacing: -.055em; margin: 8px 0 18px; text-transform: uppercase; } | |
| .report-card p { color: #e7e2d5 !important; line-height: 1.62; max-width: 850px; } | |
| .report-card details { color: #aaa59a; margin-top: 18px; } | |
| .report-card pre { white-space: pre-wrap; } | |
| .choices-shell { | |
| background: rgba(8,11,20,.78); | |
| border: 1px solid rgba(255,255,255,.12); | |
| border-radius: 18px; | |
| margin-top: 14px; | |
| padding: 14px; | |
| } | |
| .choice-heading { align-items: center; display: flex; justify-content: space-between; margin: 2px 2px 11px; } | |
| .choice-heading b { color: #fff; font-family: "Arial Black", Impact, sans-serif; letter-spacing: .02em; text-transform: uppercase; } | |
| .choice-heading span { color: #827e74; font: .66rem ui-monospace, monospace; } | |
| #choice-cards label { | |
| background: rgba(255,255,255,.035) !important; | |
| border: 1px solid rgba(255,255,255,.12) !important; | |
| border-radius: 10px !important; | |
| margin: 6px 0 !important; | |
| padding: 11px 12px !important; | |
| transition: .16s ease; | |
| } | |
| #choice-cards label:hover { background: rgba(255,200,87,.08) !important; border-color: rgba(255,200,87,.55) !important; transform: translateX(3px); } | |
| #choice-cards label:has(input:checked) { background: rgba(255,200,87,.13) !important; border-color: var(--gold) !important; } | |
| #path-options label, #persona-options label { | |
| background: rgba(255,255,255,.045) !important; | |
| border: 1px solid rgba(255,255,255,.11) !important; | |
| color: #e6e1d4 !important; | |
| } | |
| #path-options label:has(input:checked), #persona-options label:has(input:checked) { | |
| background: rgba(255,200,87,.12) !important; | |
| border-color: rgba(255,200,87,.7) !important; | |
| } | |
| #detected-paths { color: var(--cyan) !important; opacity: .74; } | |
| #reset-button { | |
| background: transparent !important; | |
| border: 1px solid rgba(255,255,255,.15) !important; | |
| color: #aaa59a !important; | |
| margin-top: 11px; | |
| } | |
| #reset-button:hover { border-color: rgba(255,200,87,.45) !important; color: var(--gold) !important; } | |
| @keyframes slide-in { from { opacity: 0; transform: translate(-50%, -42%); } to { opacity: 1; transform: translate(-50%, -50%); } } | |
| @keyframes orbit { to { transform: rotate(360deg); } } | |
| @media (max-width: 760px) { | |
| .gradio-container { padding: 12px !important; } | |
| .app-header { align-items: flex-start; flex-direction: column; } | |
| .game-frame { aspect-ratio: auto; min-height: 660px; } | |
| .stage-top { display: block; } | |
| .scenario-card { max-width: 100%; } | |
| .hud { margin-top: 10px; width: 100%; } | |
| .metric-row { display: inline-block; margin: 6px 1.2%; width: 46%; } | |
| .stage-bottom { padding: 0 15px 17px; } | |
| .persona-bubble { max-width: 74%; } | |
| .player-sprite { height: 105px; width: 93px; } | |
| .state-pill { display: none; } | |
| } | |
| """ | |
| def image_data_uri(path_text: str) -> str: | |
| path = Path(path_text) | |
| mime = mimetypes.guess_type(path.name)[0] or "image/png" | |
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") | |
| return f"data:{mime};base64,{encoded}" | |
| def parse_paths(dilemma: str) -> tuple[gr.Radio, str]: | |
| path_a, path_b = split_dilemma(dilemma) | |
| return ( | |
| gr.Radio(choices=[(path_a, "A"), (path_b, "B")], value="A", label="Choose the future to enter"), | |
| f"PATHS DETECTED: {path_a.upper()} / {path_b.upper()}", | |
| ) | |
| def begin(dilemma: str, chosen_key: str, calibration: str, persona: str): | |
| if len(dilemma.strip()) < 5: | |
| raise gr.Error("Enter a real dilemma first.") | |
| if not calibration.strip(): | |
| raise gr.Error("Add one concrete pressure, constraint, or proof point.") | |
| yield ( | |
| None, | |
| loading_html(dilemma, calibration), | |
| gr.Radio(choices=[], value=None), | |
| "", | |
| gr.Group(visible=False), | |
| gr.Column(visible=False), | |
| gr.Group(visible=False), | |
| gr.Group(visible=True), | |
| gr.Button(interactive=False, value="Building your world..."), | |
| ) | |
| session = start_session(dilemma, chosen_key or "A", calibration, persona, prefetch=False) | |
| node = current_node(session) | |
| yield ( | |
| session, | |
| game_html(session, node), | |
| gr.Radio(choices=_choice_options(node), value=None, label="Choose your move"), | |
| "", | |
| gr.Group(visible=False), | |
| gr.Column(visible=True), | |
| gr.Group(visible=False), | |
| gr.Group(visible=True), | |
| gr.Button(interactive=True, value="Commit choice"), | |
| ) | |
| prefetch_node(session, 1) | |
| def make_choice(session: SimulationSession, choice_value: str | None, custom_choice: str): | |
| if session is None: | |
| raise gr.Error("Start a simulation first.") | |
| custom = custom_choice.strip() | |
| if not custom and choice_value is None: | |
| raise gr.Error("Choose an option or write your own move.") | |
| result = choose(session, int(choice_value or 0), custom) | |
| if result["complete"]: | |
| return ( | |
| session, | |
| game_html( | |
| session, | |
| reaction=result.get("reaction", ""), | |
| cascade=result.get("cascade"), | |
| report=result["report"], | |
| ), | |
| gr.Radio(choices=[], value=None), | |
| "", | |
| gr.Group(visible=True), | |
| gr.Column(visible=False), | |
| gr.Group(visible=False), | |
| gr.Button(interactive=False), | |
| ) | |
| node = result["node"] | |
| return ( | |
| session, | |
| game_html( | |
| session, | |
| node, | |
| reaction=result.get("reaction", ""), | |
| cascade=result.get("cascade"), | |
| ), | |
| gr.Radio(choices=_choice_options(node), value=None, label="Choose your move"), | |
| "", | |
| gr.Group(visible=False), | |
| gr.Column(visible=True), | |
| gr.Group(visible=False), | |
| gr.Button(interactive=True), | |
| ) | |
| def reset(): | |
| return ( | |
| None, | |
| "", | |
| gr.Radio(choices=[], value=None), | |
| "", | |
| gr.Group(visible=False), | |
| gr.Column(visible=False), | |
| gr.Group(visible=True), | |
| gr.Group(visible=False), | |
| gr.Button(interactive=True), | |
| ) | |
| def loading_html(dilemma: str, calibration: str) -> str: | |
| path_a, path_b = split_dilemma(dilemma) | |
| detail = " ".join(calibration.split())[:150] | |
| return f""" | |
| <div class="game-frame loading-stage"> | |
| <div class="loading-orbit"></div> | |
| <div class="eyebrow">Building a personalized causal world</div> | |
| <h2>{html.escape(path_a)} / {html.escape(path_b)}</h2> | |
| <p>Connecting your real constraint to the opening scene: {html.escape(detail)}</p> | |
| <div class="loading-steps">Calibrating state / placing consequences / preparing choices</div> | |
| </div> | |
| """ | |
| def game_html( | |
| session: SimulationSession, | |
| node: dict[str, Any] | None = None, | |
| reaction: str = "", | |
| cascade: dict[str, str] | None = None, | |
| report: dict[str, Any] | None = None, | |
| ) -> str: | |
| state = environment_state(session.world_state) | |
| expression = character_expression(session.world_state) | |
| background = image_data_uri(environment_image(session)) | |
| sprite = image_data_uri(str(ROOT / "assets" / "characters" / "character.png")) | |
| portrait = image_data_uri(str(PERSONA_ASSETS[session.persona])) | |
| sprite_position = {"neutral": "0%", "stressed": "33.333%", "confident": "66.666%"}[expression] | |
| node = node or { | |
| "month_label": "Future report", | |
| "node_theme": "reflection", | |
| "scenario": "", | |
| "generation_source": "causal ledger", | |
| } | |
| cascade_markup = "" | |
| if cascade: | |
| cascade_markup = f""" | |
| <div class="cascade-banner"> | |
| <b>{html.escape(cascade['label'])}</b> | |
| <span>{html.escape(cascade['memory'])} {html.escape(cascade['effect'])}</span> | |
| </div> | |
| """ | |
| persona_text = reaction or ( | |
| f"I'll stay with you through this version of {session.chosen_path}. " | |
| "Pay attention to what each choice costs, not only what it promises." | |
| ) | |
| persona_markup = f""" | |
| <div class="persona-bubble"> | |
| <img src="{portrait}" alt="{html.escape(session.persona)}"> | |
| <div class="persona-copy"> | |
| <b>{html.escape(session.persona)}</b> | |
| <p>{html.escape(persona_text)}</p> | |
| </div> | |
| </div> | |
| """ | |
| report_markup = report_html(report) if report else "" | |
| return f""" | |
| <div class="game-frame"> | |
| <div class="world-bg" style="background-image:url('{background}')"></div> | |
| <div class="world-shade"></div> | |
| <div class="stage-top"> | |
| <section class="scenario-card"> | |
| <div class="scene-meta">{html.escape(node['month_label'])} / {html.escape(node['node_theme'])}</div> | |
| <h2>{html.escape(session.chosen_path)}</h2> | |
| <p>{html.escape(node['scenario'])}</p> | |
| <small class="source-line">SOURCE: {html.escape(node.get('generation_source', 'deterministic')).upper()}</small> | |
| </section> | |
| {metrics_html(session)} | |
| </div> | |
| {cascade_markup} | |
| <div class="stage-bottom"> | |
| {persona_markup} | |
| <div class="player-wrap"> | |
| <span class="state-pill">World: {state} / You: {expression}</span> | |
| <div class="player-sprite" style="background-image:url('{sprite}');background-position:{sprite_position} 0"></div> | |
| </div> | |
| </div> | |
| {report_markup} | |
| <div class="scanlines"></div> | |
| </div> | |
| """ | |
| def metrics_html(session: SimulationSession) -> str: | |
| labels = { | |
| "financial_security": "Financial", | |
| "creative_fulfillment": "Fulfillment", | |
| "social_validation": "Validation", | |
| "stress": "Stress", | |
| "family_satisfaction": "Family", | |
| } | |
| rows = [] | |
| for key in METRICS: | |
| value = session.world_state[key] | |
| rows.append( | |
| f""" | |
| <div class="metric-row {'stress' if key == 'stress' else ''}"> | |
| <div class="metric-label"><span>{labels[key]}</span><span>{value}</span></div> | |
| <div class="metric-track"><div class="metric-fill" style="width:{value}%"></div></div> | |
| </div> | |
| """ | |
| ) | |
| return f"<aside class='hud'><div class='hud-title'>LIVE WORLD STATE</div>{''.join(rows)}</aside>" | |
| def report_html(report: dict[str, Any]) -> str: | |
| ledger = html.escape( | |
| json.dumps( | |
| {"facts": report["facts"], "obligations": report["obligations"]}, | |
| indent=2, | |
| ) | |
| ) | |
| return f""" | |
| <section class="report-card"> | |
| <div class="eyebrow">Simulation complete / causal reflection</div> | |
| <h2>{html.escape(report['archetype'])}</h2> | |
| <p>{html.escape(report['summary'])}</p> | |
| <p>{html.escape(report['honest_mirror'])}</p> | |
| <details><summary>Open the causal ledger</summary><pre>{ledger}</pre></details> | |
| </section> | |
| """ | |
| def _choice_options(node: dict[str, Any]) -> list[tuple[str, str]]: | |
| return [(choice["text"], str(index)) for index, choice in enumerate(node["choices"])] | |
| theme = gr.themes.Base( | |
| primary_hue="orange", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui"], | |
| ).set( | |
| body_background_fill="#070a12", | |
| block_background_fill="#0d1120", | |
| block_border_color="rgba(255,255,255,.12)", | |
| input_background_fill="#080b15", | |
| input_border_color="rgba(255,255,255,.14)", | |
| ) | |
| with gr.Blocks(css=CSS, theme=theme, title="LifeChoice Simulator") as demo: | |
| session_state = gr.State() | |
| gr.HTML( | |
| f""" | |
| <header class="app-header"> | |
| <div class="brand-lockup"> | |
| <div class="eyebrow">A causal future simulator</div> | |
| <h1>Life<span>Choice</span></h1> | |
| <p>Enter one road not taken. Make decisions inside it. Watch the world remember.</p> | |
| </div> | |
| <div class="model-chip">{MODEL_ID} / 7B</div> | |
| </header> | |
| """ | |
| ) | |
| with gr.Group(elem_classes="setup-card") as setup_group: | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=5): | |
| gr.HTML( | |
| """ | |
| <section class="setup-intro"> | |
| <div class="eyebrow">New simulation</div> | |
| <h2>Try the future<br>before it happens.</h2> | |
| <p>This is not a chatbot. It is an eight-node causal simulation with persistent metrics, delayed consequences, and a world that changes with your decisions.</p> | |
| <div class="feature-list"> | |
| <div class="feature"><b>01</b> Immediate first scene</div> | |
| <div class="feature"><b>02</b> Choices produce distinct state changes</div> | |
| <div class="feature"><b>03</b> Your persona remembers what happened</div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| with gr.Column(scale=6, elem_classes="form-panel"): | |
| dilemma = gr.Textbox( | |
| label="The fork in the road", | |
| placeholder="Artist vs Software Engineer", | |
| value="MTech vs Software Job", | |
| ) | |
| path_choice = gr.Radio( | |
| choices=[("MTech", "A"), ("Software Job", "B")], | |
| value="A", | |
| label="Choose the future to enter", | |
| elem_id="path-options", | |
| ) | |
| detected = gr.Markdown( | |
| "PATHS DETECTED: MTECH / SOFTWARE JOB", | |
| elem_classes="eyebrow", | |
| elem_id="detected-paths", | |
| ) | |
| calibration = gr.Textbox( | |
| label="What makes this decision real?", | |
| placeholder="A pressure, deadline, responsibility, or proof point...", | |
| value="My family needs income soon, but a professor is already interested in my research.", | |
| lines=2, | |
| ) | |
| persona = gr.Radio( | |
| choices=["Family", "Friend", "Mentor", "Partner", "Inner Voice"], | |
| value="Mentor", | |
| label="Choose the voice that follows you", | |
| elem_id="persona-options", | |
| ) | |
| begin_btn = gr.Button("Enter this future", variant="primary", elem_id="begin-button") | |
| gr.HTML( | |
| "<p class='microcopy'>Reflective simulation only. Not medical, legal, financial, or professional advice. The first scene starts immediately while later scenes prepare in the background.</p>" | |
| ) | |
| with gr.Group(visible=False) as game_group: | |
| game_board = gr.HTML() | |
| with gr.Column(visible=False, elem_classes="choices-shell") as play_column: | |
| gr.HTML("<div class='choice-heading'><b>What do you do?</b><span>YOUR DECISION CHANGES THE STATE</span></div>") | |
| choices = gr.Radio(choices=[], show_label=False, elem_id="choice-cards") | |
| custom = gr.Textbox( | |
| label="Or make your own move", | |
| placeholder="Describe one specific action...", | |
| lines=1, | |
| ) | |
| choose_btn = gr.Button("Commit choice", variant="primary", elem_id="commit-button") | |
| with gr.Group(visible=False) as report_group: | |
| gr.Markdown( | |
| "The report reflects patterns in your causal ledger. It does not predict your future or tell you what to do." | |
| ) | |
| reset_btn = gr.Button("Start a different simulation", variant="secondary", elem_id="reset-button") | |
| dilemma.change(parse_paths, dilemma, [path_choice, detected], show_progress="hidden") | |
| begin_btn.click( | |
| begin, | |
| [dilemma, path_choice, calibration, persona], | |
| [session_state, game_board, choices, custom, report_group, play_column, setup_group, game_group, choose_btn], | |
| show_progress="hidden", | |
| ) | |
| choose_btn.click( | |
| make_choice, | |
| [session_state, choices, custom], | |
| [session_state, game_board, choices, custom, report_group, play_column, setup_group, choose_btn], | |
| ) | |
| reset_btn.click( | |
| reset, | |
| outputs=[session_state, game_board, choices, custom, report_group, play_column, setup_group, game_group, choose_btn], | |
| show_progress="hidden", | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=8).launch() | |