"""WitGym Gradio demo — dark gym landing, ivory practice screen.""" import html import os import threading from pathlib import Path from dotenv import load_dotenv load_dotenv() if os.getenv("SPACE_ID"): os.environ.setdefault("LLM_BACKEND", "hf_api") import gradio as gr from loguru import logger from witgym.conversation import ConversationManager from witgym.engine import WitGymEngine, get_shared_resources from witgym.debug_render import ( format_transcript_html, thinking_turn_html, StreamingTurnState, apply_stream_event, format_transcript_with_streaming, ) from witgym import config from witgym.avatars import char_avatar_url, char_avatar_svg INDEX_PATH = os.getenv("WITGYM_INDEX_PATH", config.INDEX_PATH) _FAVICON = Path(__file__).parent / "assets" / "favicon.png" # Character data: (name, role-label, card-bg, avatar-url, bio-title, bio-desc) CHARACTERS = [ ("Michael", "comedian", "#5a1a0a", char_avatar_url("Michael"), "Regional Manager", "Needs to be the funniest person in the room — always. Even at funerals."), ("Dwight", "contrarian", "#2d3d1a", char_avatar_url("Dwight"), "Assistant (to the) Regional Manager", "Treats every situation as a threat to be neutralised through superior preparation."), ("Jim", "wit", "#1a2d4a", char_avatar_url("Jim"), "Sales Representative", "Deflects chaos with a raised eyebrow and impeccable comedic timing."), ("Pam", "empath", "#5a1a4a", char_avatar_url("Pam"), "Receptionist → Office Administrator", "Finds the kindest possible way to say the unsayable thing everyone else is thinking."), ("Kevin", "literalist", "#3d1a5a", char_avatar_url("Kevin"), "Accountant", "Cuts to the literal truth everyone else is too sophisticated to say out loud."), ("Andy", "overclaimer", "#7a3010", char_avatar_url("Andy"), "Sales Representative", "Overclaims, overshares, and somehow — through sheer confidence — lands it."), ("Stanley", "cynic", "#0f2d1a", char_avatar_url("Stanley"), "Sales Representative", "Has seen it all. Cares about essentially none of it. Will now return to his crossword."), ("Angela", "moralist", "#2a2a0a", char_avatar_url("Angela"), "Head of Accounting", "Holds the line on decorum, propriety and cats while everything collapses around her."), ("Ryan", "hustler", "#1f0a2d", char_avatar_url("Ryan"), "Temp → VP → Temp → Temp", "Dresses up insecurity as strategy. The hustle is the product."), ("Kelly", "enthusiast", "#6a0a3a", char_avatar_url("Kelly"), "Customer Service Representative", "Turns raw enthusiasm into an overwhelming and surprisingly effective force of nature."), ] STARTERS = [ ("Status", "I just got promoted to manager and I have no idea what I'm doing."), ("Social", "My coworker keeps stealing my lunch from the fridge."), ("Delusion", "I'm pretending to understand cryptocurrency at dinner parties."), ("Anxiety", "I've been ignoring a voicemail so long it feels like a legal risk."), ("Self-aware", "I sent a complaint about my manager to my manager."), ("Coach me", "Help me respond when someone asks about my job and I don't know what to say."), ] from witgym.prompts import DRILL_KEYS as _DRILL_KEYS _drill_text = {v: k for k, v in _DRILL_KEYS.items()} DRILL_ACTIONS = [ ("sharpen it", _drill_text["sharpen"]), ("different angle", _drill_text["angle"]), ("explain the joke", _drill_text["explain"]), ] TRANSCRIPT_MIN_HEIGHT = 440 TRANSCRIPT_MAX_HEIGHT = 580 # ── Mascot SVG ──────────────────────────────────────────────────────────────── _MASCOT = """""" APP_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap'); /* ── Global dark base ──────────────────────────────────────────────────── */ body, .gradio-container, .main { background: #141414 !important; } footer { display: none !important; height: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important; } /* ── Light mode (activated on START TRAINING) ──────────────────────────── */ body.wg-light-mode, body.wg-light-mode .gradio-container, body.wg-light-mode .main { background: #f5f0e8 !important; } body.wg-light-mode footer { display: none !important; } :root { --wg-bg: #141414; --wg-surf: #1e1e1e; --wg-surf2: #252525; --wg-border: #2e2e2e; --wg-yellow: #f5c518; --wg-green: #2d6a4f; --wg-white: #f0f0f0; --wg-muted: #777; --wg-r: 10px; } /* ── Light-mode CSS variable scope — ALL var(--wg-*) inside #wg-practice resolve to light values without needing per-class overrides. This is the structural fix: dark :root vars don't cascade into the light practice screen. We also override Gradio's dark-theme vars (--body-text-color, etc.) so that Gradio's own .prose * { color: var(--body-text-color) } rule renders dark text, not near-white (#f0f0f0) on our ivory background. */ #wg-practice { --wg-bg: #fffff8; --wg-surf: #faf9f6; --wg-surf2: #f0ece4; --wg-border: #e0d8cc; --wg-white: #2a2118; --wg-muted: #9e9288; --wg-yellow: #b45309; /* --wg-green and --wg-r stay the same across modes */ /* Gradio dark-theme CSS vars — override so .prose * inherits dark text */ --body-text-color: #2a2118; --body-text-color-subdued: #6b6258; --block-text-color: #2a2118; --block-label-text-color: #6b6258; --input-text: #2a2118; --color-text-body: #2a2118; } /* ── Landing / Hero ─────────────────────────────────────────────────────── */ #wg-landing { background: transparent !important; border: none !important; } /* Collapse Gradio's default gap between HTML components in landing */ #wg-landing > .svelte-1plpy97, #wg-landing > div { gap: 0 !important; } #wg-landing .gap-4 { gap: 0 !important; } #wg-landing .block { padding: 0 !important; margin: 0 !important; min-height: 0 !important; box-shadow: none !important; border: none !important; } .wg-hero { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; /* Dot grid scoped to hero only — footer gap below START TRAINING stays solid dark */ background: radial-gradient(circle, rgba(245,197,24,0.18) 1px, transparent 2px) center/24px 24px, var(--wg-bg); padding: 0.75rem 1rem 0.75rem; text-align: center; } /* ● REC indicator — bigger, more visible flicker */ .wg-rec { position: absolute; top: 1.5rem; right: 1.75rem; display: flex; align-items: center; gap: 0.5rem; font-family: 'Courier New', monospace; font-size: 0.8rem; font-weight: 700; color: #e53e3e; letter-spacing: 0.22em; z-index: 1; } .wg-rec-dot { width: 12px; height: 12px; border-radius: 50%; background: #e53e3e; box-shadow: 0 0 8px rgba(229,62,62,0.6); animation: wg-pulse 1.4s ease-in-out infinite; } @keyframes wg-pulse { 0%,100% { opacity: 1; box-shadow: 0 0 8px rgba(229,62,62,0.6); } 50% { opacity: 0.1; box-shadow: 0 0 2px rgba(229,62,62,0.1); } } /* Kicker */ .wg-kicker { font-family: 'EB Garamond', Georgia, serif; font-style: italic; font-size: 0.78rem; letter-spacing: 0.22em; color: var(--wg-yellow); text-transform: uppercase; margin-bottom: 0.25rem; position: relative; z-index: 1; } /* Vertical logo: mascot → WIT → GYM stacked, all centered */ .wg-logo-row { display: flex; flex-direction: column; align-items: center; gap: 0; position: relative; z-index: 1; } /* Mascot */ .wg-mascot { position: relative; z-index: 1; margin-bottom: 0.25rem; filter: drop-shadow(0 4px 24px rgba(45,106,79,0.4)); } /* WIT and GYM stacked vertically, each on its own line */ .wg-wordmark { display: flex; flex-direction: column; align-items: center; line-height: 0.9; position: relative; z-index: 1; } /* Huge font — fills ~260px of the 720px viewport so content organically fills height, eliminating the empty footer gap without fighting Svelte's height binding. */ .wg-wordmark-wit, .wg-wordmark-gym { font-family: 'Bebas Neue', Impact, 'Arial Black', sans-serif; font-size: clamp(5rem, 12vw, 8.5rem); letter-spacing: 0.03em; display: block; } /* Hardcoded hex + !important: Gradio 6 SSR on HF Spaces injects theme CSS after APP_CSS, causing same-specificity cascade override of var(--wg-white/yellow). */ .wg-wordmark-wit { color: #f0f0f0 !important; text-shadow: 0 0 40px rgba(0,0,0,0.6); } .wg-wordmark-gym { color: #f5c518 !important; text-shadow: 0 2px 20px rgba(0,0,0,0.8), 0 0 60px rgba(0,0,0,0.5); } .wg-hero-tagline { font-family: 'EB Garamond', Georgia, serif; font-style: italic; font-size: 1rem; color: rgba(240,240,240,0.7); margin-bottom: 1.75rem; position: relative; z-index: 1; } .wg-start-hint { font-size: 0.72rem; color: var(--wg-muted); margin: 0.15rem 0 0 !important; font-style: italic; text-align: center; background: transparent !important; } /* ── Scroll cue: clickable circle button between tagline and CTA ─────────── */ .wg-scroll-cue { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0.3rem; padding: 0.5rem 1rem 0.75rem; background: transparent !important; cursor: pointer; transition: transform .22s ease; position: relative; z-index: 1; } .wg-scroll-cue:hover { transform: translateY(5px); } .wg-scroll-circle { width: 46px; height: 46px; border-radius: 50%; border: 1.5px solid rgba(74,222,128,0.35); background: rgba(45,106,79,0.08); display: flex; align-items: center; justify-content: center; transition: border-color .2s, box-shadow .2s; } .wg-scroll-cue:hover .wg-scroll-circle { border-color: rgba(74,222,128,0.85); box-shadow: 0 0 18px rgba(74,222,128,0.28); } .wg-scroll-arrow-svg { animation: wg-arrow-fall 1.9s ease-in-out infinite; filter: drop-shadow(0 0 4px rgba(74,222,128,0.45)); } .wg-scroll-label { font-family: 'EB Garamond', Georgia, serif; font-style: italic; font-size: 0.68rem; color: rgba(74,222,128,0.45); letter-spacing: 0.2em; text-transform: uppercase; transition: color .2s; } .wg-scroll-cue:hover .wg-scroll-label { color: rgba(74,222,128,0.8); } @keyframes wg-arrow-fall { 0%,100% { transform: translateY(0); opacity: 0.55; } 50% { transform: translateY(7px); opacity: 1; } } /* ── Real Gradio START TRAINING button ──────────────────────────────────── */ #wg-start-btn { justify-content: center !important; background: transparent !important; padding: 0 !important; } #wg-start-btn button { font-family: 'Bebas Neue', Impact, sans-serif !important; font-size: 1.1rem !important; letter-spacing: 0.22em !important; background: var(--wg-green) !important; color: #fff !important; border: none !important; border-radius: 50px !important; padding: 0.55rem 3rem !important; transition: background .2s, transform .15s !important; } #wg-start-btn button:hover { background: #235a40 !important; transform: translateY(-2px) !important; } /* ── Coaching panel ─────────────────────────────────────────────────────── */ .wg-coach-panel { width: 100%; padding: 0.2rem 1rem 0.15rem; background: var(--wg-bg); border-top: none; } .wg-coach-divider { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.2rem; } .wg-coach-div-line { flex: 1; height: 1px; background: #3a3a3a; } .wg-coach-div-text { font-family: 'Bebas Neue', Impact, sans-serif; font-size: 0.82rem; letter-spacing: 0.3em; color: var(--wg-yellow); white-space: nowrap; } .wg-char-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.65rem; max-width: 900px; margin: 0 auto; } .wg-char-card { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; background: var(--wg-surf2); border-radius: var(--wg-r); padding: 0.65rem 0.4rem 0.55rem; width: 82px; border: 1px solid var(--wg-border); cursor: pointer; transition: border-color .15s, transform .15s, box-shadow .15s; } .wg-char-card:hover { border-color: var(--wg-yellow); transform: translateY(-4px); box-shadow: 0 8px 24px rgba(245,197,24,0.15); } .wg-char-card img { width: 58px; height: 58px; border-radius: 8px; background: transparent; } .wg-char-name { font-family: 'Bebas Neue', sans-serif; font-size: 0.73rem; letter-spacing: 0.06em; color: var(--wg-white); text-align: center; } .wg-char-role { font-family: 'EB Garamond', serif; font-style: italic; font-size: 0.58rem; color: var(--wg-muted); text-align: center; } /* ── Practice screen header (compact, centered) ─────────────────────────── */ @keyframes wg-header-drop { 0% { transform: translateY(-10px) scaleY(0.82); opacity: 0; } 65% { transform: translateY(2px) scaleY(1.04); opacity: 1; } 100% { transform: translateY(0) scaleY(1); } } .wg-practice-bar { display: flex; align-items: center; justify-content: center; gap: 0.75rem; padding: 0.75rem 1.25rem; border-bottom: 1px solid #d8d0c4; background: #faf9f6; animation: wg-header-drop 0.42s cubic-bezier(.22,.68,0,1.35) both; } .wg-practice-logo { font-family: 'Bebas Neue', Impact, sans-serif; font-size: 1.5rem; letter-spacing: 0.1em; color: #3d3429; line-height: 1; } .wg-practice-logo span { color: var(--wg-green); } .wg-practice-sub { font-family: 'EB Garamond', Georgia, serif; font-style: italic; font-size: 0.8rem; color: #9e9288; } /* ── Practice screen: ivory/light override ──────────────────────────────── */ #wg-practice { background: #fffff8 !important; } /* Target Gradio's internal wrappers that carry the dark theme background */ #wg-practice .block, #wg-practice .gap, #wg-practice .form, #wg-practice .scroll-hide, #wg-practice .styler, #wg-practice > div, #wg-practice .gradio-group { background: #faf9f6 !important; border-color: #e0d8cc !important; } #wg-practice #wg-chat-shell, #wg-practice #wg-chat-shell .block, #wg-practice #wg-chat-shell .gap { background: #faf9f6 !important; border-color: #e0d8cc !important; } #wg-practice .wg-transcript { color: #2a2118 !important; } #wg-practice .wg-user { color: #2d6a4f !important; } #wg-practice .wg-thinking { background: #f5f2eb !important; border-color: rgba(200,190,175,0.5) !important; color: #6b6258 !important; } #wg-practice .wg-coach-reply { background: #f0fdf4 !important; border-color: #4ade80 !important; border-left-color: #2d6a4f !important; } #wg-practice .wg-coach-reply-header { color: #2d6a4f !important; } #wg-practice .wg-coach-reply-body { color: #14532d !important; font-size: 1.55rem !important; font-weight: 600 !important; } #wg-practice .wg-panel-yellow { background: #fffbeb !important; border-color: #fbbf24 !important; color: #78350f !important; } #wg-practice .wg-panel-yellow .wg-panel-title { color: #b45309 !important; } #wg-practice .wg-panel-blue { background: #eff6ff !important; border-color: #60a5fa !important; color: #1e3a5f !important; } #wg-practice .wg-panel-blue .wg-panel-title { color: #2563eb !important; } #wg-practice .wg-panel-green { background: #f0fdf4 !important; border-color: #4ade80 !important; color: #14532d !important; } #wg-practice .wg-panel-green .wg-panel-title { color: #16a34a !important; } #wg-practice .wg-panel-dim { background: #f5f5f4 !important; border-color: #d6d3d1 !important; color: #78716c !important; } #wg-practice .wg-dim { color: #9e9288 !important; } #wg-practice .wg-cyan { color: #0891b2 !important; } #wg-practice .wg-dim-italic { color: #9e9288 !important; font-style: italic; } #wg-practice .wg-trace-block { background: #faf9f6 !important; border-color: #e0d8cc !important; } #wg-practice .wg-trace-title { color: #9e9288 !important; } #wg-practice .wg-trace-json { color: #3d3429 !important; } #wg-practice .wg-debug-toggle-line { background: #e0d8cc !important; } #wg-practice .wg-debug-toggle-label { border-color: #e0d8cc !important; background: #f5f2eb !important; color: #9e9288 !important; } #wg-practice .wg-debug-toggle-label:hover { color: #6b6258 !important; } #wg-practice .wg-rule { border-color: #e0d8cc !important; } #wg-practice .wg-empty { color: #9e9288 !important; } #wg-practice #wg-sidebar { background: #faf9f6 !important; border-color: #e0d8cc !important; } #wg-practice .wg-sidebar-label { color: #9e9288 !important; } #wg-practice .wg-starter-btn button { background: #fff !important; border-color: #e0d8cc !important; color: #3d3429 !important; } #wg-practice .wg-starter-btn button:hover { border-color: var(--wg-green) !important; background: #f0fdf4 !important; } #wg-practice textarea, #wg-practice input, #wg-practice input[type="text"], #wg-practice .gradio-container textarea, #wg-practice .gradio-container input[type="text"] { background: #fff !important; color: #2a2118 !important; border-color: #e0d8cc !important; } #wg-practice textarea::placeholder, #wg-practice input::placeholder { color: #9e9288 !important; } /* Hide native placeholder in chat shell — flicker overlay replaces it */ #wg-chat-shell textarea::placeholder { color: transparent !important; } #wg-practice button.secondary, #wg-practice .gradio-container button.secondary { background: #f5f2eb !important; border-color: #e0d8cc !important; color: #3d3429 !important; } #wg-practice label span, #wg-practice .gradio-container label span { color: #6b6258 !important; } /* Main layout */ #witgym-main { max-width: 1200px; margin: 0 auto; padding: 0.75rem 0.5rem 1rem; } /* Submit button */ #wg-submit-btn button { background: var(--wg-green) !important; font-family: 'Bebas Neue', sans-serif !important; letter-spacing: 0.18em !important; font-size: 1rem !important; transition: background .2s; } #wg-submit-btn button:hover { background: #235a40 !important; } /* Sidebar */ #wg-sidebar { background: var(--wg-surf) !important; border: 1px solid var(--wg-border) !important; border-radius: var(--wg-r) !important; padding: 0.85rem !important; } .wg-sidebar-label { font-family: 'Bebas Neue', sans-serif; font-size: 0.88rem; letter-spacing: 0.15em; color: var(--wg-muted); margin-bottom: 0.4rem; } .wg-starter-btn button { width: 100%; text-align: left; white-space: normal; height: auto !important; min-height: 2.1rem; line-height: 1.3; padding: 0.38rem 0.55rem !important; font-size: 0.82rem !important; font-family: 'EB Garamond', serif !important; border-radius: 7px !important; background: var(--wg-surf2) !important; border: 1px solid var(--wg-border) !important; color: rgba(240,240,240,.82) !important; transition: border-color .15s; } .wg-starter-btn button:hover { border-color: var(--wg-yellow) !important; } /* Chat shell */ #wg-chat-shell { background: var(--wg-surf) !important; border: 1px solid var(--wg-border) !important; border-radius: var(--wg-r) !important; overflow: hidden; } /* Transcript (dark default, overridden in #wg-practice) */ .wg-transcript { font-size: 16px; line-height: 1.65; color: var(--wg-white); } .wg-empty { color: var(--wg-muted); font-style: italic; font-family: 'EB Garamond', serif; padding: 2.5rem 1.5rem; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; } .wg-empty-icon { font-size: 2rem; opacity: 0.4; } .wg-empty-text { max-width: 280px; line-height: 1.55; } .wg-turn { margin-bottom: 1.75rem; } .wg-user { color: #4ade80; font-weight: 700; margin-bottom: 0.65rem; font-size: 17px; } .wg-label { font-weight: 700; margin-right: 0.3rem; } .wg-thinking { display: flex; align-items: center; gap: 0.5rem; color: var(--wg-muted); font-style: italic; font-size: 0.95rem; padding: 0.6rem 0.85rem; margin-top: 0.25rem; background: var(--wg-surf2); border-radius: 8px; border: 1px solid var(--wg-border); } .wg-thinking-icon { flex-shrink: 0; animation: wg-spin .9s linear infinite; } @keyframes wg-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .wg-thinking-icon { animation: none; } } @media (max-width: 640px) { #wg-practice #wg-chat-shell .html-container { min-height: 180px !important; max-height: none !important; } #wg-practice .wg-trace-json { font-size: 0.72rem; line-height: 1.45; } #wg-practice .wg-reply-actions { top: 0.4rem; right: 0.45rem; gap: 0.35rem; } } .wg-coach-reply { margin-top: 0.85rem; padding: 0.9rem 1.1rem; background: #051a0a; border: 1px solid var(--wg-green); border-left: 3px solid var(--wg-yellow); border-radius: var(--wg-r); } .wg-coach-reply-header { font-family: 'Bebas Neue', sans-serif; font-size: 0.8rem; letter-spacing: 0.15em; color: var(--wg-yellow); margin-bottom: 0.4rem; } .wg-coach-reply-body { font-family: 'EB Garamond', Georgia, serif; font-size: 1.55rem; line-height: 1.55; color: #c6f6d5; font-weight: 600; } .wg-coach-reply--compact { margin-top: 0.5rem; } .wg-mode-badge { font-family: 'Bebas Neue', sans-serif; font-size: 0.72rem; letter-spacing: 0.18em; padding: 0.15rem 0.55rem; border-radius: 20px; display: inline-block; margin-bottom: 0.4rem; } .wg-mode-banter { background: #1a3d2b; color: #4ade80; border: 1px solid #2d6a4f; } .wg-mode-wit { background: #3d2a00; color: #fcd34d; border: 1px solid #92400e; } .wg-mode-coach { background: #050e1e; color: #93c5fd; border: 1px solid #1e3558; } /* Debug toggle */ .wg-debug-toggle { display: flex; align-items: center; gap: 0.6rem; margin: 0.8rem 0 0.4rem; cursor: pointer; user-select: none; } .wg-debug-toggle-line { flex: 1; height: 1px; background: var(--wg-border); } .wg-debug-toggle-label { font-family: 'EB Garamond', serif; font-style: italic; font-size: 0.75rem; color: var(--wg-muted); white-space: nowrap; padding: 0.12rem 0.45rem; border: 1px solid var(--wg-border); border-radius: 20px; background: var(--wg-surf2); transition: color .15s; } .wg-debug-toggle-label:hover { color: var(--wg-yellow); } .wg-debug-chevron { font-size: 0.6rem; margin-left: 0.2rem; } .wg-debug-body.wg-collapsed { display: none; } /* Debug panels */ .wg-panel { border-radius: 7px; padding: 0.6rem 0.8rem; margin: 0.35rem 0; border: 1px solid; font-size: 14px; } .wg-panel-title { font-family: 'Bebas Neue', sans-serif; font-size: 0.82rem; letter-spacing: 0.08em; margin-bottom: 0.35rem; } .wg-trace-block { border-radius: 10px; padding: 0.75rem 0.9rem; margin: 0.35rem 0; border: 1px solid var(--wg-border); background: var(--wg-surf2); } .wg-trace-title { font-family: 'Bebas Neue', sans-serif; font-size: 0.82rem; letter-spacing: 0.14em; color: var(--wg-muted); margin-bottom: 0.45rem; } .wg-trace-json { margin: 0; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.8rem; line-height: 1.55; color: var(--wg-white); } /* Clickable chips */ .wg-chip-clickable { cursor: pointer; transition: filter .15s, transform .1s; } .wg-chip-clickable:hover { filter: brightness(1.25); transform: scale(1.06); } .wg-panel-yellow { background: #1a1000; border-color: #6b3a0a; color: #fcd34d; } .wg-panel-yellow .wg-panel-title { color: #fbbf24; } .wg-panel-blue { background: #050e1e; border-color: #1e3558; color: #93c5fd; } .wg-panel-blue .wg-panel-title { color: #60a5fa; } .wg-panel-green { background: #041409; border-color: #145228; color: #86efac; } .wg-panel-green .wg-panel-title { color: #4ade80; } .wg-panel-dim { background: #111; border-color: var(--wg-border); color: var(--wg-muted); } .wg-clickable { cursor: pointer; transition: opacity .15s, transform .1s; } .wg-clickable:hover { opacity: 0.85; transform: scale(1.01); } /* Scene card: gentle border-pulse + arrow float to signal interactivity */ @keyframes wg-scene-beckon { 0%,100% { box-shadow: 0 0 0 0 rgba(96,165,250,0); border-color: #1e3558; } 50% { box-shadow: 0 0 0 5px rgba(96,165,250,0.2); border-color: #60a5fa; } } @keyframes wg-arrow-float { 0%,100% { transform: translate(0,0); opacity: 0.6; } 50% { transform: translate(2px,-2px); opacity: 1; } } .wg-panel-blue.wg-clickable { animation: wg-scene-beckon 2.8s ease-in-out infinite; } .wg-scene-arrow { display: inline-block; animation: wg-arrow-float 2.8s ease-in-out infinite; } /* Pause animations when user hovers — reduces distraction mid-read */ .wg-panel-blue.wg-clickable:hover { animation: none; } .wg-panel-blue.wg-clickable:hover .wg-scene-arrow { animation: none; opacity: 1; } .wg-meta { border-collapse: collapse; width: 100%; } .wg-meta td { padding: 0.1rem 0.5rem 0.1rem 0; vertical-align: top; } .wg-rule { border-top: 1px solid var(--wg-border); margin: 0.75rem 0; } /* ── Coaching notes toggle attention flicker (plays 4× on new turns) ────── */ @keyframes wg-toggle-beckon { 0%,100% { color: var(--wg-muted); box-shadow: none; } 40%,60% { color: var(--wg-yellow); box-shadow: 0 0 10px rgba(245,197,24,0.35); border-color: rgba(245,197,24,0.5); } } .wg-debug-toggle--new .wg-debug-toggle-label { animation: wg-toggle-beckon 1.1s ease-in-out 0.4s 4 forwards; } #wg-practice .wg-debug-toggle--new .wg-debug-toggle-label { animation: wg-toggle-beckon-light 1.1s ease-in-out 0.4s 4 forwards; } @keyframes wg-toggle-beckon-light { 0%,100% { color: #9e9288; box-shadow: none; } 40%,60% { color: #b45309; box-shadow: 0 0 8px rgba(180,83,9,0.25); border-color: rgba(180,83,9,0.4); } } /* ── Metadata chips (dark-mode defaults) ────────────────────────────────── */ .wg-chip-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.45rem 0 0.35rem; } .wg-chip { font-family: 'Bebas Neue', sans-serif; font-size: 0.69rem; letter-spacing: 0.1em; padding: 0.18rem 0.55rem; border-radius: 20px; display: inline-block; line-height: 1.5; } .wg-chip-cyan { background: #0d2d33; color: #67e8f9; border: 1px solid #164e63; } .wg-chip-purple { background: #1e1030; color: #c4b5fd; border: 1px solid #4c1d95; } .wg-chip-orange { background: #2d1a00; color: #fbbf24; border: 1px solid #92400e; } .wg-chip-green { background: #0a2018; color: #4ade80; border: 1px solid #1a3d2b; } .wg-chip-label { font-size: 0.68rem; color: var(--wg-muted); align-self: center; font-family: 'EB Garamond', serif; font-style: italic; } .wg-avoided { font-size: 0.82rem; color: var(--wg-muted); margin: 0.3rem 0 0.45rem; padding: 0.25rem 0; border-top: 1px dashed rgba(255,255,255,0.07); } /* Expandable capsules (dark-mode) */ .wg-capsule { border: 1px solid var(--wg-border); border-radius: 7px; margin-top: 0.38rem; overflow: hidden; } .wg-capsule-head { cursor: pointer; padding: 0.38rem 0.65rem; font-family: 'Bebas Neue', sans-serif; font-size: 0.69rem; letter-spacing: 0.1em; color: #888; user-select: none; display: flex; justify-content: space-between; align-items: center; transition: color .15s, background .15s; } .wg-capsule-head:hover { color: var(--wg-white); background: rgba(255,255,255,0.04); } .wg-capsule-body { padding: 0.5rem 0.65rem; font-size: 0.92rem; color: rgba(240,240,240,0.88); line-height: 1.55; border-top: 1px solid var(--wg-border); background: var(--wg-surf2); } .wg-capsule-body.wg-collapsed { display: none; } .wg-cap-chev { font-size: 0.6rem; transition: transform .18s; display: inline-block; } .wg-capsule--open .wg-cap-chev { transform: rotate(90deg); } /* ── Shared shimmer + attention keyframes ────────────────────────────────── */ /* Shimmer: light highlight sweeps L→R — signals "this surface has depth" */ @keyframes wg-shimmer-slide { 0% { transform: translateX(-160%); } 100% { transform: translateX(160%); } } /* Chevron bobs toward hidden content — pure motion affordance */ @keyframes wg-chev-beckon { 0%,100% { transform: translateX(0); } 40% { transform: translateX(6px); } } /* Border breathes with a warm glow — signals "this boundary is crossable" */ @keyframes wg-capsule-glow { 0%,100% { border-color: var(--wg-border); box-shadow: none; } 50% { border-color: rgba(245,197,24,0.7); box-shadow: 0 0 0 2.5px rgba(245,197,24,0.18); } } /* Chip pop-in: slight scale bounce then settles — signals "I'm interactive" */ @keyframes wg-chip-pop { 0% { transform: scale(0.88); opacity: 0.6; } 60% { transform: scale(1.08); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } /* Chip shimmer: same sweep but stronger amber */ @keyframes wg-chip-shimmer { 0% { transform: translateX(-180%); } 100% { transform: translateX(180%); } } /* ── Capsule attention (border glow + head shimmer + chevron bob) ─────────── */ .wg-capsule--new { animation: wg-capsule-glow 1.2s ease-in-out 0.4s 3 both; } .wg-capsule--new .wg-capsule-head { position: relative; overflow: hidden; } .wg-capsule--new .wg-capsule-head::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, transparent 15%, rgba(245,197,24,0.55) 50%, transparent 85%); transform: translateX(-160%); animation: wg-shimmer-slide 1.1s ease-in-out 0.7s 3 both; } .wg-capsule--new .wg-cap-chev { animation: wg-chev-beckon 0.5s ease-in-out 0.3s 6 both; } /* Kill all animations once the user engages */ .wg-capsule--new.wg-capsule--open, .wg-capsule--new.wg-capsule--open .wg-capsule-head::after, .wg-capsule--new.wg-capsule--open .wg-cap-chev { animation: none; } /* Light-mode capsule overrides */ @keyframes wg-capsule-glow-light { 0%,100% { border-color: #e0d8cc; box-shadow: none; } 50% { border-color: rgba(180,83,9,0.6); box-shadow: 0 0 0 2.5px rgba(180,83,9,0.14); } } #wg-practice .wg-capsule--new { animation: wg-capsule-glow-light 1.2s ease-in-out 0.4s 3 both; } #wg-practice .wg-capsule--new .wg-capsule-head::after { background: linear-gradient(90deg, transparent 15%, rgba(180,83,9,0.45) 50%, transparent 85%); } /* ── Chip attention: pop-in scale bounce + shimmer sweep ─────────────────── */ .wg-chip-clickable { position: relative; overflow: hidden; animation: wg-chip-pop 0.45s cubic-bezier(.22,.68,0,1.4) both; } /* Stagger the three chips */ .wg-chip-row .wg-chip-clickable:nth-child(1) { animation-delay: 0.05s; } .wg-chip-row .wg-chip-clickable:nth-child(2) { animation-delay: 0.18s; } .wg-chip-row .wg-chip-clickable:nth-child(3) { animation-delay: 0.31s; } /* Shimmer on each chip after its pop-in */ .wg-chip-clickable::after { content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; background: linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.55) 50%, transparent 90%); transform: translateX(-180%); animation: wg-chip-shimmer 0.9s ease-in-out 0.55s 2 both; } .wg-chip-row .wg-chip-clickable:nth-child(2)::after { animation-delay: 0.68s; } .wg-chip-row .wg-chip-clickable:nth-child(3)::after { animation-delay: 0.81s; } /* ── Coaching notes toggle shimmer ──────────────────────────────────────── */ .wg-debug-toggle--new .wg-debug-toggle-label { position: relative; overflow: hidden; } .wg-debug-toggle--new .wg-debug-toggle-label::after { content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; background: linear-gradient(90deg, transparent 15%, rgba(245,197,24,0.5) 50%, transparent 85%); transform: translateX(-160%); animation: wg-shimmer-slide 1.3s ease-in-out 0.2s 3 both; } #wg-practice .wg-debug-toggle--new .wg-debug-toggle-label::after { background: linear-gradient(90deg, transparent 15%, rgba(180,83,9,0.38) 50%, transparent 85%); } /* ── Light-mode overrides for chips + capsules ──────────────────────────── */ #wg-practice .wg-chip-cyan { background: #ecfeff; color: #0e7490; border-color: #a5f3fc; } #wg-practice .wg-chip-purple { background: #f5f3ff; color: #7c3aed; border-color: #ddd6fe; } #wg-practice .wg-chip-orange { background: #fffbeb; color: #b45309; border-color: #fde68a; } #wg-practice .wg-chip-green { background: #f0fdf4; color: #16a34a; border-color: #bbf7d0; } #wg-practice .wg-chip-label { color: #9e9288; } #wg-practice .wg-avoided { color: #9e9288; border-top-color: rgba(0,0,0,0.07); } #wg-practice .wg-capsule { border-color: #e0d8cc; } #wg-practice .wg-capsule-head { color: #9e9288 !important; } #wg-practice .wg-capsule-head:hover { color: #3d3429 !important; background: rgba(0,0,0,0.025) !important; } #wg-practice .wg-capsule-body { background: #faf9f6 !important; color: #3d3429 !important; border-top-color: #e0d8cc !important; } /* ── Light-mode overrides for mode badges ───────────────────────────────── */ #wg-practice .wg-mode-banter { background: #dcfce7 !important; color: #15803d !important; border-color: #86efac !important; } #wg-practice .wg-mode-wit { background: #fef9c3 !important; color: #92400e !important; border-color: #fde047 !important; } #wg-practice .wg-mode-coach { background: #dbeafe !important; color: #1d4ed8 !important; border-color: #93c5fd !important; } .wg-dim { color: var(--wg-muted); } .wg-dim-italic { color: var(--wg-muted); font-style: italic; } .wg-cyan { color: #22d3ee; font-weight: 500; } .wg-bold { font-weight: 600; } /* ── Slide-in animation on new turns ───────────────────────────────────── */ @keyframes wg-slide-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .wg-turn { animation: wg-slide-in 0.35s ease-out both; } /* ── Reveal animation on winning coach reply ────────────────────────────── */ @keyframes wg-reply-reveal { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } .wg-coach-reply--new .wg-coach-reply-body { animation: wg-reply-reveal 0.6s ease-out 0.15s both; } /* Flash on "another take" swap */ @keyframes wg-alt-flash { 0%,100% { opacity: 1; } 40% { opacity: 0.3; } } .wg-coach-reply--alt .wg-coach-reply-body { animation: wg-alt-flash 0.35s ease-out; } /* ── Twist potential meter ──────────────────────────────────────────────── */ .wg-twist-meter { display: flex; align-items: center; gap: 0.5rem; margin: 0.6rem 0 0.75rem; font-family: 'Bebas Neue', sans-serif; font-size: 0.72rem; letter-spacing: 0.12em; } .wg-twist-label { color: var(--wg-muted); white-space: nowrap; } .wg-twist-bar { flex: 1; height: 4px; background: var(--wg-border); border-radius: 2px; overflow: hidden; } .wg-twist-fill { height: 100%; border-radius: 2px; background: linear-gradient(to right, var(--wg-green), var(--wg-yellow)); transition: width 0.8s ease-out; } .wg-twist-score { color: var(--wg-yellow); min-width: 2.5rem; text-align: right; } /* Light-mode overrides for meter */ #wg-practice .wg-twist-bar { background: #e0d8cc !important; } #wg-practice .wg-twist-score { color: #b45309 !important; } #wg-practice .wg-twist-label { color: #9e9288 !important; } /* ── Drill chips ─────────────────────────────────────────────────────────── */ .wg-drill-chips { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0.6rem 0 0.2rem; } .wg-drill-chip { font-family: 'EB Garamond', serif; font-style: italic; font-size: 0.82rem; cursor: pointer; user-select: none; padding: 0.22rem 0.7rem; border-radius: 20px; border: 1px solid var(--wg-border); color: var(--wg-muted); background: var(--wg-surf2); transition: border-color .15s, color .15s, background .15s; } .wg-drill-chip:hover { border-color: var(--wg-yellow); color: var(--wg-yellow); background: rgba(245,197,24,0.06); } #wg-practice .wg-drill-chip { border-color: #e0d8cc !important; color: #9e9288 !important; background: #f5f2eb !important; } #wg-practice .wg-drill-chip:hover { border-color: #b45309 !important; color: #b45309 !important; background: rgba(180,83,9,0.06) !important; } /* ── Persona label + another-take ──────────────────────────────────────── */ .wg-persona-label { font-style: italic; font-family: 'EB Garamond', serif; font-size: 0.78rem; color: var(--wg-yellow); letter-spacing: 0; font-weight: 400; background: transparent !important; border: none !important; padding: 0 !important; cursor: pointer; box-shadow: none !important; display: inline; vertical-align: baseline; } #wg-practice .wg-persona-label { color: #b45309 !important; } .wg-insight-strip { margin-top: 0.85rem; padding: 0.8rem 0.95rem; border-radius: 14px; background: linear-gradient(180deg, rgba(255,250,241,0.98), rgba(245,240,232,0.98)); border: 1px solid rgba(180,83,9,0.16); } .wg-insight-title { font-family: 'Bebas Neue', sans-serif; font-size: 0.9rem; letter-spacing: 0.12em; color: #92400e !important; } .wg-insight-sub { margin-top: 0.2rem; font-size: 0.88rem; color: #6b6258 !important; } .wg-insight-buttons { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-top: 0.65rem; } .wg-insight-btn { -webkit-appearance: none !important; appearance: none !important; background: #fffaf1 !important; border: 1px solid rgba(180,83,9,0.22) !important; color: #9a3412 !important; border-radius: 999px !important; padding: 0.42rem 0.72rem !important; font-family: 'Bebas Neue', sans-serif; font-size: 0.8rem; letter-spacing: 0.09em; cursor: pointer; line-height: 1; box-shadow: none !important; } .wg-insight-btn:hover { background: #ffffff !important; border-color: rgba(180,83,9,0.42) !important; transform: translateY(-1px); } .wg-another-take { float: right; cursor: pointer; font-family: 'EB Garamond', serif; font-size: 0.75rem; font-style: italic; letter-spacing: 0; color: rgba(245,197,24,0.6); transition: color .15s; user-select: none; } .wg-another-take:hover { color: var(--wg-yellow); } #wg-practice .wg-another-take { color: #b4960a !important; } #wg-practice .wg-another-take:hover { color: #78350f !important; } /* ── Step-cycle loading messages ────────────────────────────────────────── */ .wg-step-cycle { position: relative; display: inline-block; height: 1.4em; min-width: 200px; overflow: hidden; vertical-align: middle; } .wg-step-cycle span { position: absolute; left: 0; top: 0; opacity: 0; animation: wg-step-show 6s linear infinite; white-space: nowrap; } .wg-step-cycle span:nth-child(2) { animation-delay: 2s; } .wg-step-cycle span:nth-child(3) { animation-delay: 4s; } @keyframes wg-step-show { 0% { opacity: 0; transform: translateY(4px); } 6% { opacity: 0.8; transform: translateY(0); } 28% { opacity: 0.8; transform: translateY(0); } 34% { opacity: 0; transform: translateY(-4px); } 100% { opacity: 0; } } /* ── Subtle stage spotlight on practice bg ──────────────────────────────── */ #wg-practice::before { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0; background: radial-gradient(ellipse 70% 35% at 50% 0%, rgba(45,106,79,0.05) 0%, transparent 70%); } #wg-practice { position: relative; } /* ── Comic-style modal ──────────────────────────────────────────────────── */ /* Same structural fix as #wg-practice: Gradio injects .prose * { color: var(--body-text-color) } which resolves to #f0f0f0 (dark theme) on every child element. The modal is outside #wg-practice so that scope doesn't apply. Override the same Gradio CSS vars here so child text inherits dark-on-ivory correctly — no per-element !important hacks needed. */ #wg-modal { --body-text-color: #2a2118; --body-text-color-subdued: #6b6258; --block-text-color: #2a2118; --block-label-text-color: #6b6258; --input-text: #2a2118; --color-text-body: #2a2118; color: #2a2118; } #wg-modal-overlay { position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.65); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; padding: 1rem; } #wg-modal { background: #fffff8; background-image: radial-gradient(rgba(0,0,0,0.04) 1px, transparent 1px); background-size: 20px 20px; border-radius: 18px; max-width: 680px; width: 100%; padding: 1.75rem 1.75rem 1.5rem; position: relative; box-shadow: 0 24px 64px rgba(0,0,0,0.45), 0 0 0 2px rgba(0,0,0,0.08); font-family: 'EB Garamond', Georgia, serif; max-height: 90vh; overflow-y: auto; } .wg-modal-x { position: absolute; top: 1rem; right: 1rem; width: 32px; height: 32px; border-radius: 50%; background: #1a1a1a; color: #fff; border: none; font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background .15s; } .wg-modal-x:hover { background: #333; } .wg-pop-show { font-family: 'Bebas Neue', sans-serif; font-size: 1rem; letter-spacing: 0.2em; color: #2d6a4f !important; margin-bottom: 1rem; border-bottom: 2px solid #e0d8cc; padding-bottom: 0.5rem; } .wg-pop-row { display: flex; gap: 1.25rem; margin-bottom: 1rem; align-items: flex-start; } .wg-pop-char { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; flex-shrink: 0; } .wg-pop-avatar { width: 110px; height: 110px; border-radius: 12px; background: #f5f0e6; } .wg-pop-name { font-family: 'Bebas Neue', sans-serif; font-size: 1rem; letter-spacing: 0.08em; color: #2a2118 !important; text-align: center; } .wg-pop-title { font-size: 0.72rem; color: #9e9288 !important; text-align: center; font-style: italic; max-width: 110px; line-height: 1.3; } .wg-pop-right { flex: 1; display: flex; flex-direction: column; gap: 0.65rem; } .wg-pop-setup { font-style: italic; color: #6b6258 !important; font-size: 0.95rem; line-height: 1.5; } .wg-pop-bubble { background: #fff; border: 2.5px solid #1a1a1a; border-radius: 14px; padding: 0.85rem 1rem; font-family: 'Bebas Neue', Impact, sans-serif; font-size: 1.15rem; line-height: 1.3; letter-spacing: 0.02em; color: #1a1a1a !important; position: relative; } .wg-pop-bubble::before { content: ''; position: absolute; left: -14px; top: 50%; transform: translateY(-50%); border: 7px solid transparent; border-right-color: #1a1a1a; } .wg-pop-bubble::after { content: ''; position: absolute; left: -10px; top: 50%; transform: translateY(-50%); border: 6px solid transparent; border-right-color: #fff; } .wg-pop-why { background: #eff6ff; border: 1px solid #60a5fa; border-radius: 10px; padding: 0.75rem 1rem; margin-top: 0.25rem; } .wg-pop-why-title { font-family: 'Bebas Neue', sans-serif; font-size: 0.82rem; letter-spacing: 0.12em; color: #2563eb !important; margin-bottom: 0.35rem; } .wg-pop-why-body { font-size: 0.95rem; color: #1e3a5f !important; line-height: 1.55; } /* Bio modal (character card click — no scene context) */ .wg-pop-bio { font-size: 1.05rem; color: #3d3429 !important; line-height: 1.6; font-style: italic; background: #f5f0e6; border-left: 3px solid #2d6a4f; padding: 0.75rem 1rem; border-radius: 0 10px 10px 0; } .wg-pop-minihead { font-family: 'Bebas Neue', sans-serif; font-size: 0.8rem; letter-spacing: 0.12em; color: #2563eb !important; margin: 0.9rem 0 0.35rem; } /* ── Arcade Character Selector ─────────────────────────────────────────────── */ .wg-arcade { display: flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.05rem 0 0; max-width: 700px; margin: 0 auto; } .wg-arcade-stage { display: flex; align-items: center; justify-content: center; gap: 0.75rem; flex: 1; } .wg-arcade-arrow { background: rgba(10,20,14,0.72) !important; border: 1.5px solid #4ade80 !important; color: #4ade80 !important; font-size: 2rem !important; border-radius: 50% !important; width: 48px !important; height: 48px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; transition: background .15s, box-shadow .15s, transform .1s !important; flex-shrink: 0 !important; line-height: 1 !important; padding: 0 !important; box-shadow: 0 0 12px rgba(74,222,128,0.35), inset 0 0 8px rgba(74,222,128,0.08) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; outline: none !important; } .wg-arcade-arrow:hover { background: rgba(74,222,128,0.15) !important; border-color: #86efac !important; box-shadow: 0 0 22px rgba(74,222,128,0.6), inset 0 0 12px rgba(74,222,128,0.15) !important; transform: scale(1.12) !important; color: #86efac !important; } .wg-arcade-center { background: var(--wg-surf2); border: 2px solid var(--wg-yellow); border-radius: 14px; padding: 0.35rem 0.8rem 0.35rem; display: flex; flex-direction: column; align-items: center; gap: 0.15rem; cursor: pointer; min-width: 150px; max-width: 175px; box-shadow: 0 0 28px rgba(245,197,24,0.22); transition: box-shadow .2s, border-color .2s; animation: wg-arcade-glow 2.4s ease-in-out infinite; } .wg-arcade-center:hover { box-shadow: 0 0 45px rgba(245,197,24,0.45); animation: none; } @keyframes wg-arcade-glow { 0%,100% { box-shadow: 0 0 18px rgba(245,197,24,0.18); border-color: var(--wg-yellow); } 50% { box-shadow: 0 0 36px rgba(245,197,24,0.4); border-color: #fde68a; } } .wg-arcade-avatar-wrap { position: relative; } .wg-arcade-img { width: 76px; height: 76px; border-radius: 10px; background: rgba(255,255,255,0.05); object-fit: cover; transition: opacity .18s; } .wg-arcade-name { font-family: 'Bebas Neue', sans-serif; font-size: 1.15rem; letter-spacing: 0.12em; color: var(--wg-yellow) !important; text-align: center; margin-top: 0.1rem; } .wg-arcade-role { font-family: 'EB Garamond', serif; font-style: italic; font-size: 0.7rem; color: var(--wg-muted); text-align: center; letter-spacing: 0.1em; } .wg-arcade-bio { display: none; } .wg-arcade-select-hint { font-family: 'Bebas Neue', sans-serif; font-size: 0.55rem; letter-spacing: 0.22em; color: #4ade80; margin-top: 0.1rem; opacity: 0.7; } /* Peek (adjacent) cards */ .wg-arcade-peek { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; opacity: 0.42; filter: blur(1.5px); transform: scale(0.78); transition: opacity .2s, filter .2s, transform .2s; pointer-events: none; } .wg-arcade-peek img { width: 52px; height: 52px; border-radius: 8px; } .wg-arcade-peek-name { font-family: 'Bebas Neue', sans-serif; font-size: 0.65rem; letter-spacing: 0.1em; color: var(--wg-muted); text-align: center; } /* Dot indicators */ .wg-arcade-dots { display: flex; justify-content: center; gap: 0.5rem; margin: 0.6rem 0 0.25rem; } .wg-arcade-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.2); transition: background .2s, transform .2s; } .wg-arcade-dot--active { background: var(--wg-yellow); transform: scale(1.4); } /* Slide animation */ @keyframes wg-arcade-slide-in-right { 0% { opacity: 0; transform: translateX(40px) scale(0.92); } 100% { opacity: 1; transform: translateX(0) scale(1); } } @keyframes wg-arcade-slide-in-left { 0% { opacity: 0; transform: translateX(-40px) scale(0.92); } 100% { opacity: 1; transform: translateX(0) scale(1); } } .wg-arcade-center.wg-arcade--slide-right { animation: wg-arcade-slide-in-right 0.22s ease-out both; } .wg-arcade-center.wg-arcade--slide-left { animation: wg-arcade-slide-in-left 0.22s ease-out both; } /* ── Practice header coach identity ─────────────────────────────────────── */ .wg-coach-avatar-wrap { display: flex; align-items: center; flex-shrink: 0; } .wg-coach-avatar-img { width: 42px; height: 42px; border-radius: 8px; object-fit: cover; } .wg-coach-id { display: flex; flex-direction: column; justify-content: center; } /* ── Coach reply: avatar + NAME SAYS ────────────────────────────────────── */ .wg-coach-reply { position: relative; } .wg-coach-reply-header--char { display: flex; align-items: center; gap: 0.45rem; } .wg-coach-reply-avatar { width: 28px; height: 28px; border-radius: 6px; flex-shrink: 0; background: rgba(255,255,255,0.08); } #wg-practice .wg-coach-reply-avatar { background: rgba(0,0,0,0.05); } /* ── Reply action buttons ────────────────────────────────────────────────── */ .wg-reply-actions { position: absolute; top: 0.5rem; right: 0.6rem; display: inline-flex; align-items: center; gap: 0.5rem; z-index: 3; } .wg-action-btn { -webkit-appearance: none !important; appearance: none !important; display: inline-flex; align-items: center; justify-content: center; width: 2.15rem; height: 2.15rem; min-width: 2.15rem; background: rgba(245,240,232,0.88) !important; border: 1px solid rgba(45,106,79,0.12) !important; border-radius: 0.72rem !important; box-shadow: none !important; cursor: pointer; color: rgba(94, 84, 72, 0.88); opacity: 0.92; transition: opacity .15s, color .15s, transform .15s, border-color .15s, background-color .15s; padding: 0 !important; line-height: 1; font: inherit; outline: none !important; } .wg-action-btn:hover { opacity: 1; color: #9c5c19; transform: translateY(-1px); border-color: rgba(180,83,9,0.22) !important; background: rgba(255, 250, 241, 0.98) !important; } #wg-practice .wg-action-btn { color: rgba(94, 84, 72, 0.88); background: rgba(245,240,232,0.9) !important; } #wg-practice .wg-action-btn:hover { color: #b45309; background: rgba(255,250,241,0.98) !important; } .wg-action-btn:focus, .wg-action-btn:focus-visible { outline: none !important; box-shadow: 0 0 0 2px rgba(45,106,79,0.14) !important; } .wg-action-icon { width: 0.86rem; height: 0.86rem; display: block; flex-shrink: 0; pointer-events: none; } /* ── Dedicated trace launcher + modal post-mortem ───────────────────────── */ .wg-trace-launch-wrap { display: flex; justify-content: flex-start; margin-top: 0.75rem; } .wg-trace-launch { display: inline-flex; align-items: center; gap: 0.55rem; border: 1.5px solid rgba(180,83,9,0.34); background: linear-gradient(180deg, rgba(255,250,241,0.95), rgba(245,240,232,0.98)); color: #b45309; border-radius: 999px; padding: 0.55rem 0.95rem; cursor: pointer; font-family: 'Bebas Neue', sans-serif; font-size: 0.88rem; letter-spacing: 0.12em; box-shadow: 0 8px 24px rgba(180,83,9,0.08); transition: transform .15s, box-shadow .15s, border-color .15s, color .15s; } .wg-trace-launch:hover { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(180,83,9,0.13); border-color: rgba(180,83,9,0.55); color: #92400e; } .wg-trace-launch-icon { width: 1.9rem; height: 1.9rem; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.95), rgba(255,241,220,0.92) 42%, rgba(255,231,189,0.88)); border: 1px solid rgba(180,83,9,0.2); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.65), 0 4px 14px rgba(180,83,9,0.14); } .wg-trace-launch-icon svg { width: 1.08rem; height: 1.08rem; display: block; } .wg-trace-launch-text { display: inline-flex; align-items: center; } .wg-trace-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 2px solid #e0d8cc; } .wg-trace-modal-title { font-family: 'Bebas Neue', sans-serif; font-size: 1.05rem; letter-spacing: 0.18em; color: #2d6a4f !important; } .wg-trace-modal-sub { color: #9e9288 !important; font-size: 0.92rem; font-style: italic; } .wg-trace-summary { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-bottom: 1rem; } .wg-trace-pill { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.28rem 0.62rem; border-radius: 999px; border: 1px solid #e0d8cc; background: #fff; color: #3d3429 !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.76rem; } .wg-trace-logbox { background: #171717; color: #f5f0e8; border-radius: 14px; padding: 0.9rem 1rem; margin-bottom: 1rem; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); } .wg-trace-logtitle { font-family: 'Bebas Neue', sans-serif; font-size: 0.82rem; letter-spacing: 0.16em; color: #f5c518 !important; margin-bottom: 0.55rem; } .wg-trace-logline { display: grid; grid-template-columns: 118px 64px 1fr; gap: 0.7rem; padding: 0.24rem 0; align-items: start; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.78rem; line-height: 1.45; } .wg-trace-step { color: #93c5fd !important; } .wg-trace-status { color: #86efac !important; text-transform: uppercase; } .wg-trace-detail { color: #f5f0e8 !important; opacity: 0.92; } .wg-trace-jsonbox { background: #faf9f6; border: 1px solid #e0d8cc; border-radius: 14px; overflow: hidden; } .wg-trace-jsonhead { font-family: 'Bebas Neue', sans-serif; font-size: 0.82rem; letter-spacing: 0.15em; color: #9e9288 !important; padding: 0.75rem 0.95rem; border-bottom: 1px solid #e0d8cc; background: rgba(255,255,255,0.7); } .wg-trace-jsonpre { margin: 0; padding: 0.95rem 1rem 1.1rem; max-height: 48vh; overflow: auto; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.78rem; line-height: 1.55; color: #3d3429 !important; } /* ── Hidden char state textbox ───────────────────────────────────────────── */ #wg-char-hidden { position: absolute; width: 0; height: 0; overflow: hidden; opacity: 0; pointer-events: none; } /* ── Floating roast messages — JS-spawned, multi-directional ─────────────── */ .wg-roast-chip { position: fixed; z-index: 50; pointer-events: none; font-family: 'EB Garamond', Georgia, serif; font-style: italic; font-size: 0.75rem; color: rgba(245,197,24,0.9); background: rgba(20,20,20,0.78); border: 1px solid rgba(245,197,24,0.28); border-radius: 20px; padding: 0.28rem 0.8rem; opacity: 0; transition: none; animation: wg-roast-float-in 4s ease-out forwards; } @keyframes wg-roast-float-in { 0% { opacity: 0; } 15% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } } /* ── Sidebar bubble head ─────────────────────────────────────────────────── */ @keyframes wg-bubble-bob { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-6px) rotate(0.4deg); } } .wg-bubble-head { display: flex; flex-direction: column; align-items: center; padding: 1.4rem 0.5rem 0.6rem; user-select: none; } .wg-bubble-head-inner { animation: wg-bubble-bob 2.8s ease-in-out infinite; transition: transform 0.22s ease; cursor: pointer; } .wg-bubble-head-inner:hover { animation-play-state: paused; transform: scale(1.1) rotate(5deg) !important; } .wg-bubble-avatar-large { width: 90px; height: 90px; border-radius: 50%; border: 2.5px solid var(--wg-green); box-shadow: 0 0 18px rgba(74,222,128,0.22), 0 3px 10px rgba(0,0,0,0.35); object-fit: cover; display: block; } .wg-bubble-char-name { font-family: 'Bebas Neue', sans-serif; letter-spacing: 0.18em; font-size: 0.68rem; color: var(--wg-muted); margin-top: 0.5rem; text-align: center; } #wg-practice .wg-bubble-avatar-large { border-color: #2d6a4f; box-shadow: 0 0 16px rgba(45,106,79,0.18), 0 2px 8px rgba(0,0,0,0.1); } #wg-practice .wg-bubble-char-name { color: #9e9288; } /* ── Sci-fi flicker input overlay ────────────────────────────────────────── */ .wg-flicker-wrap { position: relative; } .wg-flicker-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; padding: 0.55rem 0.75rem; font-family: 'EB Garamond', serif; font-size: 1rem; color: #2a2118; opacity: 0.5; line-height: 1.5; display: flex; align-items: center; z-index: 2; } .wg-flicker-cursor { display: inline-block; width: 2px; height: 1.1em; background: #2a2118; margin-left: 1px; vertical-align: middle; animation: wg-cursor-blink 0.9s step-end infinite; } @keyframes wg-cursor-blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } } /* Glow border on idle textarea */ #wg-chat-shell textarea:not(:focus) { box-shadow: 0 0 0 1.5px rgba(45,106,79,0.25) !important; transition: box-shadow .4s; } #wg-chat-shell textarea:focus { box-shadow: 0 0 0 2px rgba(45,106,79,0.55) !important; } #wg-practice #wg-chat-shell textarea:not(:focus) { box-shadow: 0 0 0 1.5px rgba(45,106,79,0.18) !important; } #wg-practice #wg-chat-shell textarea:focus { box-shadow: 0 0 0 2px rgba(45,106,79,0.4) !important; } """ # ── Global JS — injected into page via gr.HTML(head=...) ────────────── # Using head= instead of value= because " # ── App state & engine ──────────────────────────────────────────────────────── _shared = None _warmup_error: str | None = None def _get_shared(): global _shared if _shared is None: from witgym.retriever import load_index load_index(INDEX_PATH) _shared = get_shared_resources(index_path=INDEX_PATH) return _shared def _format_warmup_error(exc: Exception) -> str: from witgym.hub_data import get_startup_status lines = [str(exc)] status = get_startup_status() if status: lines += ["", "Diagnostics:"] + [f"• {e}" for e in status] return "\n".join(lines) def _bg_warmup(): global _warmup_error try: _get_shared() pass # TTS warms up on first use via HF Inference API except Exception as e: logger.exception("Background warmup failed") _warmup_error = _format_warmup_error(e) threading.Thread(target=_bg_warmup, daemon=True).start() def _new_session(): return {"conversation": ConversationManager(), "traces": [], "last_wit_response": None, "selected_char": "AI"} def _on_page_load(): if _warmup_error: return ( '
' f'
⚠️
{html.escape(_warmup_error)}
' '
' ) return format_transcript_html([], show_debug=False) # ── HTML generators ─────────────────────────────────────────────────────────── def _coaching_panel_html() -> str: import json as _json # Build JS character data array — includes "AI" as zeroth card mascot_data_uri = char_avatar_svg("AI") # reuse existing SVG helper with generic key chars_js = [ { "name": "AI", "role": "optimized", "avatarUrl": mascot_data_uri, "bio": "Let the engine pick the sharpest voice for your situation.", "title": "AI-Optimized Coach", } ] for name, role, _bg, avatar_url, bio_title, bio_desc in CHARACTERS: fallback = char_avatar_svg(name) chars_js.append({ "name": name, "role": role, "avatarUrl": avatar_url, "fallbackUrl": fallback, "bio": bio_desc, "title": bio_title, }) chars_json = _json.dumps(chars_js) return ( f'
' f'
' f'' f'— CHOOSE YOUR COACH —' f'' f'
' # Arcade carousel scaffold — all state/animation driven by wgInitArcade() JS f'
' f'' f'
' f'
' f'
' f'
' f'
' f'
' f'
' f'
PRESS ENTER OR CLICK TO SELECT
' f'
' f'
' f'
' f'' f'
' # Dot indicators f'
' # Embed character data for JS f'' f'
' ) _SCROLL_CUE_HTML = ( '
' '
' '' '' '' '' '
' 'begin' '
' ) def _landing_html() -> str: return ( f'
' f'
REC
' f'
Paste awkward — get one line that lands
' f'
' f'{_MASCOT}' f'
' f'
WIT
' f'
GYM
' f'
' f'
' f'
' ) _MUG_SVG = ( '' ) _ROAST_FLOAT_HTML = '' def _sidebar_bubble_head_html() -> str: ai_src = char_avatar_svg("AI") return ( '
' '
' f'AI Coach' '
' '
AI COACH
' '
' ) def _practice_header_html() -> str: # IDs wg-coach-avatar, wg-coach-name, wg-coach-role are injected by wgUpdateCoachHeader() JS # on start_btn click — no Gradio re-render needed. return ( '
' '
' + _MUG_SVG + '' '
' '
' '' '
Comedy Coaching Engine
' '
' '' '
' ) # ── Single modal scaffold — rendered at top DOM level (outside any Column) ──── # position:fixed so parent visibility doesn't matter. # JS is injected via head= param on the gr.HTML that renders this. _MODAL_SCAFFOLD = ( '
' '
' '' '
' '
' '
' ) def fill_starter(text: str) -> str: return text def practice(user_input: str, session, show_debug: bool, progress=gr.Progress()): if not isinstance(session, dict): session = _new_session() user_input = (user_input or "").strip() selected_char = session.get("selected_char", "AI") if not user_input: yield format_transcript_html(session["traces"], show_debug=show_debug, selected_char=selected_char), gr.update(value="", interactive=True), session return logger.info(f"Practice: {user_input[:80]!r} | char={selected_char}") yield ( format_transcript_html(session["traces"], append_html=thinking_turn_html(user_input), show_debug=show_debug, selected_char=selected_char), gr.update(value="", interactive=False), session, ) progress(0.05, desc="That's what she said — analysing…") engine = WitGymEngine( resources=_get_shared(), conversation=session["conversation"], last_wit_response=session.get("last_wit_response"), character=selected_char, ) stream_state = StreamingTurnState(user_input=user_input) try: for event in engine.respond_stream(user_input): apply_stream_event(stream_state, event) if event.phase == "metadata": progress(0.2, desc="Reading the room like Jim reads Dwight…") elif event.phase == "banter": progress(0.5, desc="Banter mode activated…") elif event.phase == "coaching_ask": progress(0.5, desc="Consulting the coaching panel…") elif event.phase == "scenes": progress(0.35, desc="Checking the beet farm playbook…") elif event.phase == "candidate_start": progress(0.5, desc=f"Channeling {event.persona}…") elif event.phase == "ranked": progress(0.85, desc="Picking the sharpest line (not that one, Toby)…") elif event.phase == "final_start": progress(0.92, desc="Polishing — almost there…") elif event.phase == "done" and event.response: session["traces"].append((user_input, event.response)) session["traces"] = session["traces"][-5:] session["last_wit_response"] = engine._last_wit_response yield ( format_transcript_html(session["traces"], show_debug=show_debug, selected_char=selected_char), gr.update(value="", interactive=True), session, ) return yield ( format_transcript_with_streaming(session["traces"], stream_state, show_debug=show_debug, selected_char=selected_char), gr.update(value="", interactive=False), session, ) except Exception as e: logger.exception("Engine error") err = ( f'
You ' f'{html.escape(user_input)}
' f'
Error: {html.escape(str(e))}
' ) yield format_transcript_html(session["traces"], append_html=err, show_debug=show_debug, selected_char=selected_char), gr.update(value="", interactive=True), session return def clear_session(show_debug: bool): return format_transcript_html([], show_debug=show_debug), gr.update(value="", interactive=True), _new_session() def _theme(): return ( gr.themes.Base( primary_hue=gr.themes.colors.green, secondary_hue=gr.themes.colors.yellow, neutral_hue=gr.themes.colors.zinc, ) .set( body_background_fill="#141414", block_background_fill="#1e1e1e", button_primary_background_fill="#2d6a4f", button_primary_background_fill_hover="#235a40", button_primary_text_color="#ffffff", body_text_color="#f0f0f0", input_background_fill="#1a1a1a", input_border_color="#2e2e2e", border_color_primary="#2e2e2e", ) ) def _set_char_in_session(char_name: str, session): """Called when arcade SELECT fires — stores chosen character in session state.""" if not isinstance(session, dict): session = _new_session() session = dict(session) session["selected_char"] = char_name or "AI" logger.info(f"Coach selected: {session['selected_char']}") return session def build_ui(): with gr.Blocks(title="WitGym", css=APP_CSS, theme=_theme(), head=_GLOBAL_JS_SCRIPT_TAG) as demo: # Modal scaffold at top DOM level — position:fixed, never hidden by Column visibility toggling. # JS is injected via launch(head=...) below, not here, for SSR compatibility. gr.HTML(value=_MODAL_SCAFFOLD) session_state = gr.State(_new_session()) show_debug_state = gr.State(False) # punchline first; user can expand coaching notes # ── Landing screen ──────────────────────────────────────── with gr.Column(visible=True, elem_id="wg-landing") as landing_col: gr.HTML(_landing_html()) gr.HTML(_coaching_panel_html()) with gr.Row(elem_id="wg-start-btn"): start_btn = gr.Button("START TRAINING →", variant="primary", size="lg") # Hidden Textbox: JS writes selected char name here before arcade confirm fires char_hidden = gr.Textbox(visible=True, elem_id="wg-char-hidden", value="AI") # ── Practice screen ─────────────────────────────────────── with gr.Column(visible=False, elem_id="wg-practice") as practice_col: gr.HTML(_practice_header_html()) # Floating roast messages (fixed bottom-right) gr.HTML(_ROAST_FLOAT_HTML) with gr.Column(elem_id="witgym-main"): with gr.Row(equal_height=True): with gr.Column(scale=3): with gr.Group(elem_id="wg-chat-shell"): transcript = gr.HTML( value=format_transcript_html([], show_debug=False), min_height=TRANSCRIPT_MIN_HEIGHT, max_height=TRANSCRIPT_MAX_HEIGHT, autoscroll=True, show_label=False, padding=True, ) user_input = gr.Textbox( show_label=False, placeholder="Describe a situation… (Enter to send)", lines=1, max_lines=3, elem_id="wg-user-input", ) with gr.Row(): submit_btn = gr.Button( "Flex Your Wit →", variant="primary", scale=4, elem_id="wg-submit-btn", ) clear_btn = gr.Button("Start over →", size="sm", variant="secondary", scale=1) gr.HTML( '
' '' '
' ) with gr.Column(scale=1, elem_id="wg-sidebar"): gr.HTML('
Try a situation
') for tag, text in STARTERS: sb = gr.Button( f"{tag.lower()} · {text}", size="sm", variant="secondary", elem_classes=["wg-starter-btn"], ) sb.click( fn=fill_starter, inputs=[gr.State(text)], outputs=user_input, queue=False, ).then( fn=practice, inputs=[user_input, session_state, show_debug_state], outputs=[transcript, user_input, session_state], show_progress="full", show_progress_on=submit_btn, ) gr.HTML(_sidebar_bubble_head_html()) # ── Event wiring ────────────────────────────────────────── start_btn.click( fn=lambda char, s: (gr.update(visible=False), gr.update(visible=True), {**s, "selected_char": char or "AI"}), inputs=[char_hidden, session_state], outputs=[landing_col, practice_col, session_state], queue=False, js=( "(char, sess) => {" " document.body.classList.add('wg-light-mode');" " wgPlayBell && wgPlayBell();" " (function _poll(n){" " setTimeout(function(){" " var el = document.getElementById('wg-bubble-char-name');" " if (el) {" " wgUpdateCoachHeader && wgUpdateCoachHeader();" " wgInitFlicker && wgInitFlicker();" " wgInitRoasts && wgInitRoasts();" " } else if (n > 0) { _poll(n - 1); }" " }, 120);" " })(40);" " return [char, sess];" "}" ), ) submit_btn.click( fn=practice, inputs=[user_input, session_state, show_debug_state], outputs=[transcript, user_input, session_state], show_progress="full", show_progress_on=submit_btn, ) user_input.submit( fn=practice, inputs=[user_input, session_state, show_debug_state], outputs=[transcript, user_input, session_state], show_progress="full", show_progress_on=submit_btn, ) clear_btn.click( fn=clear_session, inputs=[show_debug_state], outputs=[transcript, user_input, session_state], ) demo.load(fn=_on_page_load, outputs=transcript, show_progress="hidden") return demo demo = build_ui() demo.queue(default_concurrency_limit=1) demo.favicon_path = _FAVICON if __name__ == "__main__": kwargs = dict( favicon_path=_FAVICON, css=APP_CSS, theme=_theme(), head=_GLOBAL_JS_SCRIPT_TAG, ) if os.getenv("SPACE_ID"): kwargs["server_name"] = "0.0.0.0" demo.launch(**kwargs)