| """ |
| 🦁 NumZoo — Math practice with cute AI-generated animal rewards! |
| """ |
|
|
| import base64 |
| import io |
| import os |
| import random |
|
|
| import gradio as gr |
|
|
| from math_engine import generate_question, LEVEL_NAMES, level_up_message |
| from image_generator import generate_reward_image |
|
|
| |
| |
| |
|
|
| ANSWERS_PER_LEVEL = 5 |
|
|
| |
| |
| ANIMAL_EMOJIS = [ |
| "🐶", "🐱", "🐰", "🐭", "🦊", "🐺", |
| "🐻", "🐼", "🐨", "🦁", "🐯", "🐷", |
| "🐮", "🐴", "🐑", "🐸", "🦦", "🐧", |
| "🐙", "🦋", "🐝", "🐞", "🦄", "🐉", |
| ] |
| PLACE_EMOJIS = [ |
| "🌊", "🏔️", "🌸", "🌈", "🌙", "⭐", "🌴", "🏡", "🌺", "🍄", |
| "🏜️", "🏰", "🎢", "⛺", "🪩", "🎪", "⛵", "🚀", |
| ] |
|
|
| MATH_LEVEL_NAMES = {1: "Additions", 2: "Subtractions", 3: "Multiplications", 4: "Mix", 5: "Mix +"} |
|
|
| |
| |
| |
|
|
| def _math_level(state: dict) -> int: |
| return min(state.get("level", 1), 5) |
|
|
| def _status_text(state: dict) -> str: |
| if not state: |
| return "" |
| diff = MATH_LEVEL_NAMES.get(_math_level(state), "") |
| correct = state.get("correct_this_level", 0) |
| level = state.get("level", 1) |
| return (f"👤 {state['name']} | 🎮 Level: {level} | " |
| f"📚 {diff} | ✅ {correct} / {ANSWERS_PER_LEVEL}") |
|
|
| def _safe_question(state: dict) -> str: |
| return f"## {state.get('question', '')} = ?" |
|
|
| def _img_to_data_url(pil_image) -> str: |
| buf = io.BytesIO() |
| pil_image.save(buf, format="JPEG", quality=80) |
| return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode() |
|
|
| def _picker_level_label(level: int) -> str: |
| diff = MATH_LEVEL_NAMES.get(min(level, 5), "Mix +") |
| return f"## 🌟 Level {level} — {diff}" |
|
|
| |
| |
| |
|
|
| def _add_locked(items: list, level_id: int) -> list: |
| """Append a locked placeholder for level_id if not already in the list.""" |
| id_str = str(level_id) |
| if any(x["id"] == id_str for x in items): |
| return items |
| return items + [{"id": id_str, "status": "locked", "src": None}] |
|
|
| def _unlock_item(items: list, level_id: int, src: str) -> list: |
| """Replace the locked entry for level_id with an unlocked image.""" |
| id_str = str(level_id) |
| updated, found = [], False |
| for item in items: |
| if item["id"] == id_str: |
| updated.append({"id": id_str, "status": "unlocked", "src": src}) |
| found = True |
| else: |
| updated.append(item) |
| if not found: |
| updated.append({"id": id_str, "status": "unlocked", "src": src}) |
| return updated |
|
|
| def _render_collection(items: list) -> str: |
| """Render the collection as HTML, newest first. Empty string when no items.""" |
| if not items: |
| return "" |
| cards = [] |
| for item in reversed(items): |
| if item["status"] == "locked": |
| cards.append( |
| '<div class="nz-card nz-card-locked">' |
| '<div class="nz-card-pulse"></div>' |
| '<div class="nz-card-icon">🔒</div>' |
| '</div>' |
| ) |
| elif item.get("src"): |
| src = item["src"] |
| cards.append( |
| f'<div class="nz-card">' |
| f'<img src="{src}" ' |
| f'onclick="var m=document.getElementById(\'nz-modal\'),' |
| f'i=document.getElementById(\'nz-modal-img\');' |
| f'i.src=this.src;m.style.display=\'flex\';">' |
| f'</div>' |
| ) |
| if not cards: |
| return "" |
| return ( |
| '<div id="nz-coll-wrap">' |
| '<div class="nz-coll-title">🖼️ My Collection</div>' |
| '<div id="nz-coll-grid">' |
| + "".join(cards) |
| + '</div></div>' |
| ) |
|
|
| |
| |
| |
|
|
| def enter_name(player_name: str, state: dict): |
| try: |
| name = player_name.strip() or "Player" |
| state = {"name": name, "level": 1, "score": 0, "streak": 0} |
| return ( |
| state, |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| random.sample(ANIMAL_EMOJIS, 1), |
| random.sample(PLACE_EMOJIS, 1), |
| ) |
| except Exception as e: |
| print(f"enter_name error: {e}") |
| return (state, gr.update(visible=True), gr.update(visible=False), |
| gr.update(visible=False), gr.update(visible=False), [], []) |
|
|
| |
| |
| |
|
|
| def start_level(animal_sel: list, place_sel: list, state: dict, coll_items: list): |
| try: |
| if not animal_sel or not place_sel: |
| return (state, gr.update(visible=True), gr.update(visible=False), |
| "⚠️ Pick at least one animal and one place!", |
| "", gr.update(), gr.update(), gr.update(), gr.update(), |
| gr.update(), gr.update()) |
|
|
| level = state.get("level", 1) |
| state.update({ |
| "selected_animals": animal_sel[:5], |
| "selected_places": place_sel[:3], |
| "correct_this_level": 0, |
| "generate_now": False, |
| }) |
|
|
| math_lv = _math_level(state) |
| question, answer = generate_question(math_lv) |
| state["question"] = question |
| state["answer"] = answer |
|
|
| new_coll = _add_locked(coll_items, level) |
| return (state, |
| gr.update(visible=False), |
| gr.update(visible=True), |
| "", |
| _status_text(state), |
| gr.update(value=_safe_question(state), visible=True), |
| gr.update(value="", visible=True), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| new_coll, |
| _render_collection(new_coll)) |
|
|
| except Exception as e: |
| print(f"start_level error: {e}") |
| return (state, gr.update(visible=True), gr.update(visible=False), |
| "⚠️ Something went wrong, try again.", |
| "", gr.update(), gr.update(), gr.update(), gr.update(), |
| gr.update(), gr.update()) |
|
|
| |
| |
| |
| |
|
|
| def pregenerate_image(state: dict): |
| level = state.get("level", 1) |
| try: |
| animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)]) |
| places = state.get("selected_places", [random.choice(PLACE_EMOJIS)]) |
|
|
| print(f"[pregenerate] level={level} | animals={animals} | places={places}") |
| result, prompt = generate_reward_image(animals, places, reward_id=level) |
| print(f"[pregenerate] level={level} done | prompt={prompt!r}") |
|
|
| if result is not None: |
| data_url = _img_to_data_url(result) |
| state["pending_reward_id"] = level |
| return state, result, data_url |
|
|
| state.pop("pending_reward_id", None) |
| return state, None, "" |
|
|
| except Exception as e: |
| import traceback |
| print(f"[pregenerate] ❌ {e}\n{traceback.format_exc()}") |
| state.pop("pending_reward_id", None) |
| return state, None, "" |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| def check_answer(user_input: str, state: dict, pre_image, pre_data_url: str, coll_items: list): |
|
|
| def _no_change(): |
| return (state, _status_text(state), gr.update(), gr.update(), "", |
| gr.update(visible=False), "", gr.update(visible=False), "", |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) |
|
|
| try: |
| if not state or not state.get("question"): |
| return _no_change() |
|
|
| try: |
| user_answer = int(user_input.strip()) |
| except (ValueError, AttributeError): |
| return (state, _status_text(state), gr.update(), gr.update(), |
| "⚠️ Numbers only!", |
| gr.update(visible=False), "", gr.update(visible=False), "", |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) |
|
|
| correct = (user_answer == state["answer"]) |
|
|
| if correct: |
| state["score"] += 1 |
| state["streak"] += 1 |
| state["correct_this_level"] = state.get("correct_this_level", 0) + 1 |
|
|
| level = state.get("level", 1) |
| correct_count = state["correct_this_level"] |
| remaining = ANSWERS_PER_LEVEL - correct_count |
| streak_fire = "🔥" * min(state["streak"], 5) |
|
|
| if correct_count >= ANSWERS_PER_LEVEL: |
| |
| state["generate_now"] = False |
| pending_id = state.get("pending_reward_id") |
| has_pre = (pre_image is not None |
| and bool(pre_data_url) |
| and pending_id == level) |
|
|
| if has_pre: |
| state.pop("pending_reward_id", None) |
| new_coll = _unlock_item(coll_items, level, pre_data_url) |
| return (state, _status_text(state), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| f"🎉 Level {level} complete! {streak_fire}", |
| gr.update(visible=True), |
| "", |
| gr.update(visible=True, value=pre_image), |
| "", |
| None, "", |
| new_coll, _render_collection(new_coll), |
| gr.update(visible=False)) |
| else: |
| state["generate_now"] = True |
| return (state, _status_text(state), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| f"🎉 Level {level} complete! {streak_fire}", |
| gr.update(visible=True), |
| LOADER_HTML, |
| gr.update(visible=False), |
| "", |
| gr.update(), gr.update(), |
| gr.update(), gr.update(), |
| gr.update(visible=False)) |
| else: |
| |
| math_lv = _math_level(state) |
| question, answer = generate_question(math_lv) |
| state["question"] = question |
| state["answer"] = answer |
| return (state, _status_text(state), |
| gr.update(value=_safe_question(state)), gr.update(value=""), |
| f"✅ {remaining} to go! {streak_fire}", |
| gr.update(visible=False), "", gr.update(visible=False), "", |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) |
|
|
| else: |
| correct_answer = state["answer"] |
| current_q = state["question"] |
| state["streak"] = 0 |
| math_lv = _math_level(state) |
| question, answer = generate_question(math_lv) |
| state["question"] = question |
| state["answer"] = answer |
| return (state, _status_text(state), |
| gr.update(value=_safe_question(state)), gr.update(value=""), |
| f"❌ {current_q} = **{correct_answer}**", |
| gr.update(visible=False), "", gr.update(visible=False), "", |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) |
|
|
| except Exception as e: |
| import traceback |
| print(f"check_answer error: {e}\n{traceback.format_exc()}") |
| state["generate_now"] = False |
| math_lv = _math_level(state) |
| question, answer = generate_question(math_lv) |
| state["question"] = question |
| state["answer"] = answer |
| return (state, _status_text(state), |
| gr.update(value=_safe_question(state)), gr.update(value=""), |
| "⚠️ Something went wrong!", |
| gr.update(visible=False), "", gr.update(visible=False), "", |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) |
|
|
| |
| |
| |
|
|
| def generate_on_demand(state: dict, coll_items: list): |
| """Generate reward image on demand. No-op if generate_now is False.""" |
| if not state.get("generate_now"): |
| return state, "", gr.update(), "", gr.update(), gr.update() |
|
|
| state["generate_now"] = False |
| level = state.get("level", 1) |
|
|
| try: |
| animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)]) |
| places = state.get("selected_places", [random.choice(PLACE_EMOJIS)]) |
|
|
| print(f"[on_demand] level={level}") |
| result, prompt = generate_reward_image(animals, places, reward_id=level) |
| print(f"[on_demand] level={level} done") |
|
|
| if result is not None: |
| data_url = _img_to_data_url(result) |
| new_coll = _unlock_item(coll_items, level, data_url) |
| return (state, "", |
| gr.update(visible=True, value=result), |
| "", |
| new_coll, |
| _render_collection(new_coll)) |
|
|
| return state, "", gr.update(visible=False), "⚠️ Could not generate image — try again later!", gr.update(), gr.update() |
|
|
| except Exception as e: |
| import traceback |
| print(f"[on_demand] ❌ {e}\n{traceback.format_exc()}") |
| return state, "", gr.update(visible=False), f"⚠️ Error: {e}", gr.update(), gr.update() |
|
|
| |
| |
| |
|
|
| def next_level(state: dict): |
| try: |
| new_level = state.get("level", 1) + 1 |
| state["level"] = new_level |
| state["correct_this_level"] = 0 |
| state.pop("pending_reward_id", None) |
|
|
| label = _picker_level_label(new_level) |
| return (state, |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(value=label, visible=True), |
| random.sample(ANIMAL_EMOJIS, 3), |
| random.sample(PLACE_EMOJIS, 3), |
| None, |
| "") |
| except Exception as e: |
| print(f"next_level error: {e}") |
| return (state, gr.update(), gr.update(), gr.update(), |
| gr.update(), gr.update(), None, "") |
|
|
| |
| |
| |
|
|
| def restart(state: dict): |
| return ({}, gr.update(visible=True), gr.update(visible=False), |
| gr.update(visible=False), gr.update(), "", None, "", [], "") |
|
|
| |
| |
| |
|
|
| def _load_banner() -> str: |
| path = os.path.join(os.path.dirname(os.path.abspath(__file__)), |
| "samples", "panda-frog-fox-wave.jpeg") |
| try: |
| with open(path, "rb") as f: |
| b64 = base64.b64encode(f.read()).decode() |
| return f"data:image/jpeg;base64,{b64}" |
| except Exception: |
| return "" |
|
|
| _BANNER_URL = _load_banner() |
|
|
| BANNER_HTML = f""" |
| <div style="position:relative; width:100%; height:190px; border-radius:20px; overflow:hidden; |
| margin-bottom:12px; box-shadow:0 4px 20px rgba(0,0,0,0.18);"> |
| <img src="{_BANNER_URL}" |
| style="width:100%; height:100%; object-fit:cover; object-position:center 85%; |
| filter:brightness(0.88) saturate(0.8);"> |
| <div style="position:absolute; inset:0; |
| background:linear-gradient(to bottom, rgba(10,5,30,0) 15%, rgba(10,5,30,0.62) 100%);"></div> |
| <div style="position:absolute; bottom:0; left:0; right:0; text-align:center; padding:0 16px 18px;"> |
| <div id="title" |
| style="font-size:2.6em; font-weight:900; color:#fff; letter-spacing:0.04em; line-height:1.1; |
| text-shadow:0 2px 18px rgba(110,50,230,0.7), 0 1px 4px rgba(0,0,0,0.55);"> |
| NumZoo |
| </div> |
| <div style="font-size:0.95em; color:rgba(255,255,255,0.88); font-style:italic; |
| letter-spacing:0.02em; margin-top:3px; |
| text-shadow:0 1px 6px rgba(0,0,0,0.45);"> |
| Do maths. Win cute animals! |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| |
| |
| |
|
|
| CSS = """ |
| .gradio-container { max-width: 560px !important; margin: 0 auto !important; } |
| /* All top-level panels fill the container identically */ |
| .gradio-group { width: 100% !important; box-sizing: border-box !important; } |
| .gradio-group > .form { padding: 20px 24px !important; box-sizing: border-box !important; width: 100% !important; } |
| .prose h3 { margin-top: 14px !important; margin-bottom: 8px !important; padding-left: 4px !important; } |
| #question-box { text-align: center; font-size: 2.6em; font-weight: bold; padding: 0.5em; } |
| #feedback-box { text-align: center; font-size: 1.3em; min-height: 2em; } |
| #status-box { text-align: center; padding: 0.4em; border-radius: 8px; } |
| #reward-img { border-radius: 16px; } |
| #picker-level { text-align: center; padding: 0.3em 0 0.6em; } |
| .answer-input input { font-size: 2em !important; text-align: center !important; } |
| /* Emoji picker grid */ |
| .emoji-group .wrap { |
| display: grid !important; gap: 8px !important; |
| justify-content: center !important; justify-items: center !important; |
| } |
| #animal-picker .wrap { grid-template-columns: repeat(6, 52px) !important; } |
| #place-picker .wrap { grid-template-columns: repeat(6, 52px) !important; } |
| .emoji-group label { |
| width: 52px !important; height: 52px !important; |
| display: flex !important; align-items: center !important; justify-content: center !important; |
| font-size: 1.8em !important; cursor: pointer !important; |
| border-radius: 12px !important; border: 2px solid transparent !important; |
| transition: all 0.15s !important; user-select: none !important; |
| } |
| .emoji-group label:has(input:checked) { |
| background: #e9d5ff !important; border-color: #7c3aed !important; |
| } |
| .emoji-group input[type="checkbox"] { display: none !important; } |
| /* Collection */ |
| @keyframes numzoo-pulse { 0%,100% { opacity:.4; } 50% { opacity:.9; } } |
| #nz-coll-wrap { margin-top: 20px; } |
| .nz-coll-title { |
| font-size: 1.1em; font-weight: bold; text-align: center; |
| padding: 8px 20px; margin: 0 0 12px; |
| background: rgba(124, 58, 237, 0.12) !important; |
| border: 1.5px solid rgba(124, 58, 237, 0.35) !important; |
| border-radius: 14px; color: #a78bfa !important; letter-spacing: 0.02em; |
| } |
| #nz-coll-grid { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; } |
| .nz-card { width: 80px; height: 80px; border-radius: 12px; overflow: hidden; |
| flex-shrink: 0; position: relative; } |
| .nz-card-locked { background: #1e1e2e; border: 2px dashed #555; } |
| .nz-card-pulse { position: absolute; inset: 0; |
| background: linear-gradient(135deg,#2d2d44,#111128); |
| animation: numzoo-pulse 2s ease-in-out infinite; } |
| .nz-card-icon { width: 100%; height: 100%; display: flex; align-items: center; |
| justify-content: center; font-size: 2em; z-index: 1; position: relative; } |
| .nz-card img { width: 100%; height: 100%; object-fit: cover; cursor: pointer; display: block; } |
| """ |
|
|
| LOADER_HTML = """ |
| <div style="text-align:center; padding:2em;"> |
| <div style="font-size:3em; animation:spin 1s linear infinite; display:inline-block;">🎨</div> |
| <div style="margin-top:0.5em; color:#888; font-size:1.1em;">Generating your reward…</div> |
| </div> |
| <style>@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style> |
| """ |
|
|
| ZOOM_MODAL_HTML = """ |
| <div id="nz-modal" onclick="this.style.display='none'" |
| style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); |
| z-index:9999; align-items:center; justify-content:center; cursor:pointer;"> |
| <img id="nz-modal-img" |
| style="max-width:92vw; max-height:92vh; border-radius:20px; box-shadow:0 8px 40px #0006;"> |
| </div> |
| """ |
|
|
| with gr.Blocks(title="🦁 NumZoo") as demo: |
|
|
| state = gr.State({}) |
| coll_items = gr.State([]) |
| hidden_image = gr.Image(visible=False, label="", type="pil") |
| hidden_data_url = gr.Textbox(visible=False, value="") |
|
|
| gr.HTML(BANNER_HTML) |
|
|
| gr.HTML(""" |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| var saved = localStorage.getItem('numzoo_name'); |
| if (saved) { |
| setTimeout(function() { |
| var input = document.querySelector('input[placeholder="Your name…"]'); |
| if (input) { |
| Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input, saved); |
| input.dispatchEvent(new Event('input', {bubbles:true})); |
| } |
| }, 800); |
| } |
| }); |
| document.addEventListener('input', function(e) { |
| if (e.target.placeholder === 'Your name…') localStorage.setItem('numzoo_name', e.target.value); |
| }); |
| // Stamp inputmode=numeric on the answer box so mobile shows a number keyboard. |
| // Runs on every DOM mutation because Gradio can re-render the input. |
| (function() { |
| function patch() { |
| var inp = document.querySelector('.answer-input input'); |
| if (inp && inp.getAttribute('inputmode') !== 'numeric') { |
| inp.setAttribute('inputmode', 'numeric'); |
| inp.setAttribute('pattern', '[0-9]*'); |
| } |
| } |
| patch(); |
| new MutationObserver(patch).observe(document.body, { childList: true, subtree: true }); |
| })(); |
| </script> |
| """) |
|
|
| |
| with gr.Group(visible=True) as welcome_panel: |
| gr.Markdown("### Who are you?") |
| player_name = gr.Textbox(placeholder="Your name…", label=" ", show_label=False, max_lines=1) |
| next_btn = gr.Button("Next ➡️", variant="primary", size="lg") |
|
|
| |
| with gr.Group(visible=False) as emoji_panel: |
| picker_level_md = gr.Markdown("", visible=False, elem_id="picker-level") |
| gr.Markdown("### Pick your animals 🐾") |
| animal_picker = gr.CheckboxGroup(choices=ANIMAL_EMOJIS, label="", |
| show_label=False, elem_classes=["emoji-group"], |
| elem_id="animal-picker") |
| gr.Markdown("### Pick your places 🌍") |
| place_picker = gr.CheckboxGroup(choices=PLACE_EMOJIS, label="", |
| show_label=False, elem_classes=["emoji-group"], |
| elem_id="place-picker") |
| picker_error = gr.Markdown("") |
| go_btn = gr.Button("Let's go! 🚀", variant="primary", size="lg") |
|
|
| |
| with gr.Group(visible=False) as game_panel: |
| status_md = gr.Markdown("", elem_id="status-box", container=True) |
| question_md = gr.Markdown("", elem_id="question-box") |
| answer_input = gr.Textbox(placeholder="?", label="Your answer", |
| max_lines=1, elem_classes=["answer-input"]) |
| check_btn = gr.Button("✔️ Check!", variant="primary") |
| feedback_md = gr.Markdown("", elem_id="feedback-box") |
|
|
| with gr.Group(visible=False) as reward_panel: |
| gr.Markdown("### 🎁 Your reward!") |
| loader_html = gr.HTML("") |
| reward_image = gr.Image(label="", show_label=False, |
| elem_id="reward-img", height=400, visible=False) |
| reward_error = gr.Markdown("") |
| next_level_btn = gr.Button("Next Level ➡️", variant="primary", size="lg") |
|
|
| |
| collection_html = gr.HTML("", elem_id="nz-collection-html") |
| gr.HTML(ZOOM_MODAL_HTML) |
|
|
| restart_btn = gr.Button("🔄 Restart", variant="secondary", size="sm") |
|
|
| |
|
|
| enter_name_outputs = [state, welcome_panel, emoji_panel, game_panel, |
| picker_level_md, animal_picker, place_picker] |
| next_btn.click(enter_name, [player_name, state], enter_name_outputs) |
| player_name.submit(enter_name, [player_name, state], enter_name_outputs) |
|
|
| |
| start_level_outputs = [state, emoji_panel, game_panel, picker_error, |
| status_md, question_md, answer_input, check_btn, |
| reward_panel, coll_items, collection_html] |
| go_btn.click( |
| start_level, [animal_picker, place_picker, state, coll_items], |
| start_level_outputs, show_progress="hidden" |
| ).then( |
| pregenerate_image, [state], [state, hidden_image, hidden_data_url], |
| show_progress="hidden" |
| ) |
|
|
| check_outputs = [ |
| state, status_md, question_md, answer_input, |
| feedback_md, reward_panel, loader_html, reward_image, reward_error, |
| hidden_image, hidden_data_url, coll_items, collection_html, check_btn, |
| ] |
| ondemand_outputs = [state, loader_html, reward_image, reward_error, |
| coll_items, collection_html] |
|
|
| for trigger in [check_btn.click, answer_input.submit]: |
| trigger( |
| check_answer, |
| [answer_input, state, hidden_image, hidden_data_url, coll_items], |
| check_outputs, |
| show_progress="hidden", |
| ).then( |
| generate_on_demand, [state, coll_items], ondemand_outputs, |
| show_progress="hidden", |
| ) |
|
|
| next_level_outputs = [state, game_panel, emoji_panel, picker_level_md, |
| animal_picker, place_picker, hidden_image, hidden_data_url] |
| next_level_btn.click(next_level, [state], next_level_outputs) |
|
|
| restart_btn.click( |
| restart, [state], |
| [state, welcome_panel, emoji_panel, game_panel, |
| player_name, picker_error, hidden_image, hidden_data_url, coll_items, collection_html] |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch( |
| css=CSS, |
| theme=gr.themes.Soft(primary_hue="purple", secondary_hue="pink"), |
| ) |
|
|