Spaces:
Running on Zero
Running on Zero
| import hashlib | |
| import random | |
| from datetime import date | |
| from html import escape | |
| from urllib.parse import quote | |
| import gradio as gr | |
| import requests | |
| import spaces | |
| WIKIPEDIA_API = "https://en.wikipedia.org/w/api.php" | |
| CHALLENGES = [ | |
| ("Motorcycle", "Black hole"), | |
| ("Coffee", "Moon"), | |
| ("Rubber duck", "Artificial intelligence"), | |
| ("Pizza", "Quantum mechanics"), | |
| ("Harley-Davidson", "Computer science"), | |
| ("Volcano", "Internet"), | |
| ("Shark", "Electricity"), | |
| ("Chess", "Space exploration"), | |
| ("New York City", "Dinosaur"), | |
| ("Heavy metal music", "Ancient Egypt"), | |
| ("Video game", "Solar System"), | |
| ("Submarine", "World Wide Web"), | |
| ] | |
| def zerogpu_compatibility(): | |
| """Allows this lightweight app to start on ZeroGPU hardware.""" | |
| return True | |
| def wikipedia_request(params: dict) -> dict: | |
| response = requests.get( | |
| WIKIPEDIA_API, | |
| params={ | |
| **params, | |
| "format": "json", | |
| "formatversion": 2, | |
| "origin": "*", | |
| }, | |
| timeout=20, | |
| headers={ | |
| "User-Agent": "WikiQuest/1.0 (Hugging Face educational game)" | |
| }, | |
| ) | |
| response.raise_for_status() | |
| return response.json() | |
| def normalize_title(title: str) -> str: | |
| return " ".join(title.strip().split()) | |
| def resolve_article(title: str) -> str: | |
| data = wikipedia_request( | |
| { | |
| "action": "query", | |
| "titles": title, | |
| "redirects": 1, | |
| } | |
| ) | |
| pages = data.get("query", {}).get("pages", []) | |
| if not pages or pages[0].get("missing"): | |
| raise ValueError(f'Wikipedia could not find an article named "{title}".') | |
| return pages[0].get("title", title) | |
| def get_article_data(title: str, link_limit: int = 120) -> tuple[str, str, list[str]]: | |
| data = wikipedia_request( | |
| { | |
| "action": "query", | |
| "prop": "extracts|links", | |
| "titles": title, | |
| "redirects": 1, | |
| "exintro": 1, | |
| "explaintext": 1, | |
| "plnamespace": 0, | |
| "pllimit": link_limit, | |
| } | |
| ) | |
| pages = data.get("query", {}).get("pages", []) | |
| if not pages or pages[0].get("missing"): | |
| raise ValueError(f'Wikipedia could not load "{title}".') | |
| page = pages[0] | |
| resolved_title = page.get("title", title) | |
| summary = page.get("extract") or "No introductory summary was available." | |
| links = [] | |
| blocked_prefixes = ( | |
| "List of", | |
| "Outline of", | |
| "Index of", | |
| "Glossary of", | |
| "Timeline of", | |
| ) | |
| for item in page.get("links", []): | |
| link_title = item.get("title", "").strip() | |
| if not link_title: | |
| continue | |
| if link_title.startswith(blocked_prefixes): | |
| continue | |
| if link_title.casefold() == resolved_title.casefold(): | |
| continue | |
| links.append(link_title) | |
| return resolved_title, summary[:900], sorted(set(links)) | |
| def get_link_titles(title: str, limit: int = 100) -> set[str]: | |
| try: | |
| _, _, links = get_article_data(title, limit) | |
| return {item.casefold() for item in links} | |
| except Exception: | |
| return set() | |
| def title_words(title: str) -> set[str]: | |
| ignored = { | |
| "a", | |
| "an", | |
| "and", | |
| "the", | |
| "of", | |
| "in", | |
| "on", | |
| "to", | |
| "for", | |
| "with", | |
| "by", | |
| } | |
| cleaned = "".join( | |
| character.lower() if character.isalnum() else " " | |
| for character in title | |
| ) | |
| return { | |
| word | |
| for word in cleaned.split() | |
| if len(word) > 2 and word not in ignored | |
| } | |
| def connection_score( | |
| article: str, | |
| target: str, | |
| article_links: set[str] | None = None, | |
| ) -> float: | |
| if article.casefold() == target.casefold(): | |
| return 100.0 | |
| article_words = title_words(article) | |
| target_words = title_words(target) | |
| word_overlap = len(article_words & target_words) | |
| score = word_overlap * 16.0 | |
| article_lower = article.casefold() | |
| target_lower = target.casefold() | |
| if article_lower in target_lower or target_lower in article_lower: | |
| score += 25.0 | |
| if article_links and target_lower in article_links: | |
| score += 55.0 | |
| return min(score, 95.0) | |
| def select_destinations( | |
| links: list[str], | |
| target: str, | |
| amount: int = 12, | |
| ) -> list[str]: | |
| if not links: | |
| return [] | |
| target_lower = target.casefold() | |
| target_terms = title_words(target) | |
| def rank(link: str) -> tuple[int, int, int]: | |
| link_terms = title_words(link) | |
| exact_target = int(link.casefold() == target_lower) | |
| shared_terms = len(link_terms & target_terms) | |
| readable_length = -abs(len(link) - 18) | |
| return exact_target, shared_terms, readable_length | |
| ranked = sorted(links, key=rank, reverse=True) | |
| priority = ranked[:5] | |
| remaining = [link for link in links if link not in priority] | |
| seed_text = f"{target}|{'|'.join(links[:20])}" | |
| seed = int(hashlib.sha256(seed_text.encode()).hexdigest()[:12], 16) | |
| local_random = random.Random(seed) | |
| local_random.shuffle(remaining) | |
| selected = priority + remaining[: max(0, amount - len(priority))] | |
| if target in links and target not in selected: | |
| selected[-1] = target | |
| return selected[:amount] | |
| def daily_challenge() -> tuple[str, str]: | |
| today_number = date.today().toordinal() | |
| return CHALLENGES[today_number % len(CHALLENGES)] | |
| def random_challenge() -> tuple[str, str]: | |
| return random.choice(CHALLENGES) | |
| def make_header( | |
| current: str, | |
| target: str, | |
| clicks: int, | |
| temperature: str, | |
| ) -> str: | |
| return f""" | |
| <div class="mission-grid"> | |
| <div class="mission-card current-card"> | |
| <span class="eyebrow">CURRENT LOCATION</span> | |
| <h2>{escape(current)}</h2> | |
| </div> | |
| <div class="mission-arrow"> | |
| <span>➜</span> | |
| </div> | |
| <div class="mission-card target-card"> | |
| <span class="eyebrow">TARGET DESTINATION</span> | |
| <h2>{escape(target)}</h2> | |
| </div> | |
| </div> | |
| <div class="game-stats"> | |
| <div class="stat-pill"> | |
| <span class="stat-label">MOVES</span> | |
| <strong>{clicks}</strong> | |
| </div> | |
| <div class="stat-pill heat-pill"> | |
| <span class="stat-label">SIGNAL</span> | |
| <strong>{escape(temperature)}</strong> | |
| </div> | |
| </div> | |
| """ | |
| def make_article_card(title: str, summary: str) -> str: | |
| article_url = ( | |
| "https://en.wikipedia.org/wiki/" | |
| + quote(title.replace(" ", "_"), safe="") | |
| ) | |
| return f""" | |
| <section class="article-panel"> | |
| <div class="article-heading"> | |
| <span class="location-pin">◉</span> | |
| <div> | |
| <span class="eyebrow">YOU HAVE ARRIVED AT</span> | |
| <h1>{escape(title)}</h1> | |
| </div> | |
| </div> | |
| <p>{escape(summary)}</p> | |
| <a | |
| class="wiki-link" | |
| href="{article_url}" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| > | |
| Read the full Wikipedia article ↗ | |
| </a> | |
| </section> | |
| """ | |
| def make_path(path: list[str]) -> str: | |
| if not path: | |
| return """ | |
| <div class="empty-path"> | |
| Your journey will appear here. | |
| </div> | |
| """ | |
| nodes = [] | |
| for index, title in enumerate(path): | |
| node_class = "path-node" | |
| if index == 0: | |
| node_class += " start-node" | |
| elif index == len(path) - 1: | |
| node_class += " active-node" | |
| nodes.append( | |
| f""" | |
| <div class="{node_class}"> | |
| <span>{index}</span> | |
| <strong>{escape(title)}</strong> | |
| </div> | |
| """ | |
| ) | |
| if index < len(path) - 1: | |
| nodes.append('<div class="path-line"></div>') | |
| return f""" | |
| <div class="path-wrapper"> | |
| {''.join(nodes)} | |
| </div> | |
| """ | |
| def temperature_message( | |
| previous_score: float | None, | |
| new_score: float, | |
| won: bool, | |
| ) -> str: | |
| if won: | |
| return "TARGET REACHED" | |
| if previous_score is None: | |
| return "SEARCHING" | |
| difference = new_score - previous_score | |
| if new_score >= 65: | |
| return "BURNING HOT" | |
| if difference >= 18: | |
| return "MUCH WARMER" | |
| if difference >= 4: | |
| return "GETTING WARMER" | |
| if difference <= -18: | |
| return "ICE COLD" | |
| if difference <= -4: | |
| return "GETTING COLDER" | |
| return "UNCHANGED" | |
| def victory_screen(path: list[str], target: str) -> str: | |
| clicks = max(len(path) - 1, 0) | |
| return f""" | |
| <section class="victory-panel"> | |
| <div class="victory-icon">◆</div> | |
| <span class="eyebrow">MISSION COMPLETE</span> | |
| <h1>You found {escape(target)}!</h1> | |
| <p> | |
| You crossed Wikipedia in | |
| <strong>{clicks} move{"s" if clicks != 1 else ""}</strong>. | |
| </p> | |
| <div class="victory-path"> | |
| {escape(" → ".join(path))} | |
| </div> | |
| </section> | |
| """ | |
| def error_outputs(message: str): | |
| return ( | |
| f""" | |
| <section class="error-panel"> | |
| <h2>Unable to start this mission</h2> | |
| <p>{escape(message)}</p> | |
| </section> | |
| """, | |
| "", | |
| gr.update(choices=[], value=None, interactive=False), | |
| "", | |
| [], | |
| "", | |
| "", | |
| 0.0, | |
| ) | |
| def load_location( | |
| article: str, | |
| target: str, | |
| path: list[str], | |
| previous_score: float | None, | |
| ): | |
| resolved_title, summary, links = get_article_data(article) | |
| target = resolve_article(target) | |
| if path: | |
| path[-1] = resolved_title | |
| else: | |
| path = [resolved_title] | |
| link_set = {link.casefold() for link in links} | |
| score = connection_score(resolved_title, target, link_set) | |
| won = resolved_title.casefold() == target.casefold() | |
| temperature = temperature_message(previous_score, score, won) | |
| header = make_header( | |
| resolved_title, | |
| target, | |
| max(len(path) - 1, 0), | |
| temperature, | |
| ) | |
| if won: | |
| content = victory_screen(path, target) | |
| destinations = gr.update( | |
| choices=[], | |
| value=None, | |
| interactive=False, | |
| label="Mission complete", | |
| ) | |
| else: | |
| content = make_article_card(resolved_title, summary) | |
| choices = select_destinations(links, target) | |
| destinations = gr.update( | |
| choices=choices, | |
| value=None, | |
| interactive=True, | |
| label="Choose your next destination", | |
| ) | |
| return ( | |
| header, | |
| content, | |
| destinations, | |
| make_path(path), | |
| path, | |
| resolved_title, | |
| target, | |
| score, | |
| ) | |
| def start_game(start: str, target: str): | |
| start = normalize_title(start) | |
| target = normalize_title(target) | |
| if not start or not target: | |
| return error_outputs("Enter both a starting article and a target.") | |
| try: | |
| resolved_start = resolve_article(start) | |
| resolved_target = resolve_article(target) | |
| return load_location( | |
| resolved_start, | |
| resolved_target, | |
| [resolved_start], | |
| None, | |
| ) | |
| except (requests.RequestException, ValueError) as error: | |
| return error_outputs(str(error)) | |
| def travel( | |
| destination: str, | |
| target: str, | |
| path: list[str], | |
| previous_score: float, | |
| ): | |
| if not destination: | |
| return ( | |
| gr.update(), | |
| gr.update(), | |
| gr.update(), | |
| gr.update(), | |
| path, | |
| path[-1] if path else "", | |
| target, | |
| previous_score, | |
| ) | |
| new_path = list(path) | |
| new_path.append(destination) | |
| try: | |
| return load_location( | |
| destination, | |
| target, | |
| new_path, | |
| previous_score, | |
| ) | |
| except (requests.RequestException, ValueError) as error: | |
| return error_outputs(str(error)) | |
| def undo_move( | |
| target: str, | |
| path: list[str], | |
| ): | |
| if len(path) <= 1: | |
| current = path[0] if path else "" | |
| if not current: | |
| return error_outputs("Start a mission first.") | |
| return load_location( | |
| current, | |
| target, | |
| path, | |
| None, | |
| ) | |
| new_path = path[:-1] | |
| previous_article = new_path[-1] | |
| return load_location( | |
| previous_article, | |
| target, | |
| new_path, | |
| None, | |
| ) | |
| def load_daily(): | |
| start, target = daily_challenge() | |
| return start_game(start, target) | |
| def load_random(): | |
| start, target = random_challenge() | |
| return start_game(start, target) | |
| CSS = """ | |
| :root { | |
| --background: #070b14; | |
| --panel: #111827; | |
| --panel-light: #182235; | |
| --border: rgba(255, 255, 255, 0.09); | |
| --text: #f7f9fc; | |
| --muted: #9aa8bd; | |
| --accent: #7c5cff; | |
| --accent-bright: #9c88ff; | |
| --cyan: #4dd7ff; | |
| --success: #48e3a5; | |
| } | |
| .gradio-container { | |
| max-width: 1180px !important; | |
| margin: 0 auto !important; | |
| padding: 20px !important; | |
| background: | |
| radial-gradient(circle at 50% -20%, rgba(124, 92, 255, 0.24), transparent 40%), | |
| var(--background) !important; | |
| } | |
| body, | |
| .gradio-container { | |
| font-family: Inter, ui-sans-serif, system-ui, sans-serif !important; | |
| } | |
| footer { | |
| display: none !important; | |
| } | |
| .hero { | |
| text-align: center; | |
| padding: 38px 10px 24px; | |
| } | |
| .hero-badge { | |
| display: inline-block; | |
| color: var(--cyan); | |
| border: 1px solid rgba(77, 215, 255, 0.32); | |
| background: rgba(77, 215, 255, 0.08); | |
| border-radius: 999px; | |
| padding: 7px 13px; | |
| font-size: 0.74rem; | |
| font-weight: 800; | |
| letter-spacing: 0.16em; | |
| } | |
| .hero h1 { | |
| margin: 16px 0 6px; | |
| color: var(--text); | |
| font-size: clamp(2.8rem, 7vw, 5.1rem); | |
| line-height: 0.95; | |
| letter-spacing: -0.06em; | |
| } | |
| .hero h1 span { | |
| color: var(--accent-bright); | |
| } | |
| .hero p { | |
| color: var(--muted); | |
| font-size: 1.05rem; | |
| margin: 15px auto 0; | |
| max-width: 630px; | |
| } | |
| .setup-panel, | |
| .control-panel { | |
| background: rgba(17, 24, 39, 0.9); | |
| border: 1px solid var(--border); | |
| border-radius: 22px; | |
| padding: 20px; | |
| box-shadow: 0 20px 55px rgba(0, 0, 0, 0.28); | |
| } | |
| .setup-panel label span, | |
| .destination-grid label span { | |
| color: var(--text) !important; | |
| font-weight: 800 !important; | |
| } | |
| .mission-grid { | |
| display: grid; | |
| grid-template-columns: 1fr auto 1fr; | |
| gap: 14px; | |
| align-items: stretch; | |
| margin: 22px 0 14px; | |
| } | |
| .mission-card { | |
| min-height: 125px; | |
| padding: 22px; | |
| border-radius: 20px; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| .current-card { | |
| border-color: rgba(77, 215, 255, 0.35); | |
| } | |
| .target-card { | |
| border-color: rgba(156, 136, 255, 0.4); | |
| } | |
| .mission-card h2 { | |
| color: var(--text); | |
| margin: 6px 0 0; | |
| font-size: 1.55rem; | |
| } | |
| .mission-arrow { | |
| display: flex; | |
| align-items: center; | |
| color: var(--accent-bright); | |
| font-size: 2rem; | |
| } | |
| .eyebrow { | |
| color: var(--muted); | |
| font-size: 0.7rem; | |
| font-weight: 900; | |
| letter-spacing: 0.16em; | |
| } | |
| .game-stats { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 14px; | |
| } | |
| .stat-pill { | |
| flex: 1; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 15px; | |
| padding: 13px 17px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .stat-pill strong { | |
| color: var(--text); | |
| } | |
| .stat-label { | |
| color: var(--muted); | |
| font-size: 0.7rem; | |
| font-weight: 900; | |
| letter-spacing: 0.12em; | |
| } | |
| .heat-pill strong { | |
| color: var(--cyan); | |
| } | |
| .article-panel, | |
| .victory-panel, | |
| .error-panel { | |
| background: | |
| linear-gradient(135deg, rgba(124, 92, 255, 0.09), transparent 45%), | |
| var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 22px; | |
| padding: clamp(22px, 5vw, 38px); | |
| margin: 14px 0; | |
| color: var(--text); | |
| } | |
| .article-heading { | |
| display: flex; | |
| gap: 14px; | |
| align-items: center; | |
| } | |
| .location-pin { | |
| color: var(--cyan); | |
| font-size: 1.7rem; | |
| } | |
| .article-panel h1, | |
| .victory-panel h1 { | |
| color: var(--text); | |
| margin: 4px 0 0; | |
| font-size: clamp(1.8rem, 5vw, 2.7rem); | |
| } | |
| .article-panel p { | |
| color: #c7d0df; | |
| line-height: 1.75; | |
| font-size: 1rem; | |
| } | |
| .wiki-link { | |
| display: inline-block; | |
| color: var(--cyan) !important; | |
| font-weight: 750; | |
| text-decoration: none !important; | |
| margin-top: 8px; | |
| } | |
| .destination-grid { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 22px; | |
| padding: 18px !important; | |
| margin-top: 14px; | |
| } | |
| .destination-grid .wrap { | |
| display: grid !important; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)) !important; | |
| gap: 10px !important; | |
| } | |
| .destination-grid label { | |
| min-height: 62px !important; | |
| border: 1px solid var(--border) !important; | |
| border-radius: 14px !important; | |
| background: var(--panel-light) !important; | |
| color: var(--text) !important; | |
| padding: 14px !important; | |
| transition: | |
| transform 160ms ease, | |
| border-color 160ms ease, | |
| background 160ms ease !important; | |
| } | |
| .destination-grid label:hover { | |
| transform: translateY(-2px); | |
| border-color: rgba(124, 92, 255, 0.8) !important; | |
| background: rgba(124, 92, 255, 0.16) !important; | |
| } | |
| .primary-button { | |
| min-height: 52px !important; | |
| border-radius: 14px !important; | |
| font-weight: 850 !important; | |
| } | |
| .path-section { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 22px; | |
| padding: 22px; | |
| margin-top: 14px; | |
| } | |
| .path-title { | |
| color: var(--text); | |
| font-size: 0.78rem; | |
| letter-spacing: 0.13em; | |
| font-weight: 900; | |
| margin-bottom: 18px; | |
| } | |
| .path-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| overflow-x: auto; | |
| padding-bottom: 8px; | |
| } | |
| .path-node { | |
| min-width: max-content; | |
| background: var(--panel-light); | |
| border: 1px solid var(--border); | |
| border-radius: 999px; | |
| color: var(--text); | |
| padding: 8px 13px 8px 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .path-node span { | |
| width: 27px; | |
| height: 27px; | |
| display: grid; | |
| place-items: center; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.08); | |
| color: var(--muted); | |
| font-size: 0.75rem; | |
| } | |
| .active-node { | |
| border-color: rgba(77, 215, 255, 0.55); | |
| } | |
| .active-node span { | |
| background: var(--cyan); | |
| color: #07101a; | |
| } | |
| .start-node span { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .path-line { | |
| width: 24px; | |
| min-width: 24px; | |
| height: 2px; | |
| background: rgba(255, 255, 255, 0.14); | |
| } | |
| .victory-panel { | |
| text-align: center; | |
| border-color: rgba(72, 227, 165, 0.45); | |
| background: | |
| radial-gradient(circle at 50% 0%, rgba(72, 227, 165, 0.19), transparent 48%), | |
| var(--panel); | |
| } | |
| .victory-icon { | |
| color: var(--success); | |
| font-size: 3rem; | |
| margin-bottom: 10px; | |
| } | |
| .victory-panel p { | |
| color: var(--muted); | |
| font-size: 1.05rem; | |
| } | |
| .victory-path { | |
| color: var(--success); | |
| background: rgba(72, 227, 165, 0.08); | |
| border: 1px solid rgba(72, 227, 165, 0.2); | |
| border-radius: 14px; | |
| padding: 15px; | |
| margin-top: 18px; | |
| overflow-wrap: anywhere; | |
| } | |
| .empty-path { | |
| color: var(--muted); | |
| } | |
| .error-panel { | |
| border-color: rgba(255, 100, 120, 0.38); | |
| } | |
| @media (max-width: 760px) { | |
| .gradio-container { | |
| padding: 12px !important; | |
| } | |
| .mission-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .mission-arrow { | |
| justify-content: center; | |
| transform: rotate(90deg); | |
| height: 25px; | |
| } | |
| .destination-grid .wrap { | |
| grid-template-columns: 1fr !important; | |
| } | |
| .game-stats { | |
| flex-direction: column; | |
| } | |
| } | |
| """ | |
| with gr.Blocks(title="WikiQuest") as demo: | |
| path_state = gr.State([]) | |
| current_state = gr.State("") | |
| target_state = gr.State("") | |
| score_state = gr.State(0.0) | |
| gr.HTML( | |
| """ | |
| <header class="hero"> | |
| <span class="hero-badge">THE INTERNET RABBIT HOLE</span> | |
| <h1>WIKI<span>QUEST</span></h1> | |
| <p> | |
| Travel between two completely different ideas using only | |
| the links hidden inside Wikipedia. | |
| </p> | |
| </header> | |
| """ | |
| ) | |
| with gr.Group(elem_classes=["setup-panel"]): | |
| with gr.Row(): | |
| start_input = gr.Textbox( | |
| label="Starting article", | |
| value="Motorcycle", | |
| placeholder="Example: Motorcycle", | |
| ) | |
| target_input = gr.Textbox( | |
| label="Target article", | |
| value="Black hole", | |
| placeholder="Example: Black hole", | |
| ) | |
| with gr.Row(): | |
| start_button = gr.Button( | |
| "Start Custom Mission", | |
| variant="primary", | |
| elem_classes=["primary-button"], | |
| ) | |
| daily_button = gr.Button( | |
| "Play Daily Challenge", | |
| elem_classes=["primary-button"], | |
| ) | |
| random_button = gr.Button( | |
| "Generate Random Mission", | |
| elem_classes=["primary-button"], | |
| ) | |
| mission_header = gr.HTML() | |
| article_display = gr.HTML( | |
| """ | |
| <section class="article-panel"> | |
| <span class="eyebrow">READY FOR DEPARTURE</span> | |
| <h1>Choose a mission above</h1> | |
| <p> | |
| Every destination shown during the game is a real link | |
| from your current Wikipedia article. | |
| </p> | |
| </section> | |
| """ | |
| ) | |
| destination_picker = gr.Radio( | |
| label="Choose your next destination", | |
| choices=[], | |
| interactive=True, | |
| elem_classes=["destination-grid"], | |
| ) | |
| with gr.Row(): | |
| travel_button = gr.Button( | |
| "Travel to Selected Article", | |
| variant="primary", | |
| elem_classes=["primary-button"], | |
| ) | |
| undo_button = gr.Button( | |
| "Undo Last Move", | |
| elem_classes=["primary-button"], | |
| ) | |
| gr.HTML('<section class="path-section"><div class="path-title">YOUR JOURNEY</div>') | |
| path_display = gr.HTML( | |
| '<div class="empty-path">Your journey will appear here.</div>' | |
| ) | |
| gr.HTML("</section>") | |
| game_outputs = [ | |
| mission_header, | |
| article_display, | |
| destination_picker, | |
| path_display, | |
| path_state, | |
| current_state, | |
| target_state, | |
| score_state, | |
| ] | |
| start_button.click( | |
| fn=start_game, | |
| inputs=[start_input, target_input], | |
| outputs=game_outputs, | |
| ) | |
| daily_button.click( | |
| fn=load_daily, | |
| outputs=game_outputs, | |
| ) | |
| random_button.click( | |
| fn=load_random, | |
| outputs=game_outputs, | |
| ) | |
| travel_button.click( | |
| fn=travel, | |
| inputs=[ | |
| destination_picker, | |
| target_state, | |
| path_state, | |
| score_state, | |
| ], | |
| outputs=game_outputs, | |
| ) | |
| destination_picker.change( | |
| fn=travel, | |
| inputs=[ | |
| destination_picker, | |
| target_state, | |
| path_state, | |
| score_state, | |
| ], | |
| outputs=game_outputs, | |
| ) | |
| undo_button.click( | |
| fn=undo_move, | |
| inputs=[ | |
| target_state, | |
| path_state, | |
| ], | |
| outputs=game_outputs, | |
| ) | |
| demo.launch( | |
| theme=gr.themes.Base(), | |
| css=CSS, | |
| ) |