Async pre-generation, fix loader, fix collection display
Browse filesKey changes:
- Pre-generate reward image immediately on "Let's go!" so it's ready when user earns it
- locked placeholder appears in collection the moment generation starts
- Image unlocks (reveal animation) when user earns the reward
- Fallback on-demand generation if pre-image not ready in time
- Fix loader: use value-swapping ("" vs LOADER_HTML) instead of visibility toggle β more reliable on HF Spaces
- Fix collection: drop canvas CORS approach, use PythonβJS base64 injection via gr.HTML polling
- Collection stores base64 JPEGs in localStorage, supports tap-to-zoom modal
- Event chain: go_btn β start_game β pregenerate; check_btn β check_answer β generate_on_demand β pregenerate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@@ -36,10 +36,30 @@ Full loop shortcut: `bash scripts/dev.sh` (runs steps 1β4, then run test.sh ma
|
|
| 36 |
|
| 37 |
**Game state dict keys:**
|
| 38 |
`name`, `level`, `score`, `streak`, `correct_since_reward`, `level_correct`,
|
| 39 |
-
`question`, `answer`, `selected_animals`, `selected_places`,
|
|
|
|
| 40 |
|
| 41 |
**Reward trigger:** every `REWARD_EVERY = 3` correct answers.
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
## Gradio rules
|
| 45 |
- Use `gr.update(visible=True/False)` for visibility β never return bare booleans
|
|
|
|
| 36 |
|
| 37 |
**Game state dict keys:**
|
| 38 |
`name`, `level`, `score`, `streak`, `correct_since_reward`, `level_correct`,
|
| 39 |
+
`question`, `answer`, `selected_animals`, `selected_places`,
|
| 40 |
+
`reward_count`, `pregenerate_next`, `generate_now`, `pending_reward_id`
|
| 41 |
|
| 42 |
**Reward trigger:** every `REWARD_EVERY = 3` correct answers.
|
| 43 |
+
|
| 44 |
+
**Pre-generation flow (async image UX):**
|
| 45 |
+
1. "Let's go!" β `start_game()` β adds locked placeholder for reward #1 in collection β
|
| 46 |
+
`.then(pregenerate_image)` starts background generation immediately
|
| 47 |
+
2. `pregenerate_image` stores PIL image in `hidden_image` (gr.Image) and base64 data URL in
|
| 48 |
+
`hidden_data_url` (gr.Textbox), sets `state["pending_reward_id"]`
|
| 49 |
+
3. When user earns a reward: `check_answer` receives `hidden_image` + `hidden_data_url` as inputs.
|
| 50 |
+
- If pre-image matches reward_id β show immediately + unlock in collection + kick off next pregenerate
|
| 51 |
+
- If not ready β show LOADER_HTML + set `generate_now=True`
|
| 52 |
+
4. `.then(generate_on_demand)` β generates on-demand if `generate_now`, unlocks collection
|
| 53 |
+
5. `.then(pregenerate_image)` β generates next reward in background
|
| 54 |
+
|
| 55 |
+
**Collection system:**
|
| 56 |
+
- `collection_trigger = gr.HTML("")` receives JSON-encoded action payloads
|
| 57 |
+
- JS polls `#numzoo-coll-data` element every 200ms for `{ts, actions:[{action,id,src}]}`
|
| 58 |
+
- Actions: `add-locked` (locked placeholder), `unlock` (reveal image), `failed` (remove)
|
| 59 |
+
- Images stored in localStorage as base64 JPEGs (key: `numzoo_collection_v2`)
|
| 60 |
+
|
| 61 |
+
**Loader fix:** Use value-swapping (`""` = hidden, `LOADER_HTML` = shown) instead of `visible` flag.
|
| 62 |
+
This is more reliable in Gradio 6 than toggling visibility on gr.HTML components.
|
| 63 |
|
| 64 |
## Gradio rules
|
| 65 |
- Use `gr.update(visible=True/False)` for visibility β never return bare booleans
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
π¦ NumZoo β Math practice with cute AI-generated animal rewards!
|
| 3 |
"""
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
import random
|
|
|
|
| 6 |
import gradio as gr
|
|
|
|
| 7 |
from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
|
| 8 |
from image_generator import generate_reward_image
|
| 9 |
|
|
@@ -28,6 +33,21 @@ def _status_text(state: dict) -> str:
|
|
| 28 |
def _safe_question(state: dict) -> str:
|
| 29 |
return f"## {state.get('question', '')} = ?"
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# ---------------------------------------------------------------------------
|
| 32 |
# Step 1 β Name entry
|
| 33 |
# ---------------------------------------------------------------------------
|
|
@@ -56,45 +76,89 @@ def start_game(animal_sel: list, place_sel: list, state: dict):
|
|
| 56 |
try:
|
| 57 |
if not animal_sel or not place_sel:
|
| 58 |
return (state, gr.update(visible=True), gr.update(visible=False),
|
| 59 |
-
"β οΈ Pick at least one animal and one place!", "", "")
|
| 60 |
|
| 61 |
state.update({
|
| 62 |
-
"selected_animals":
|
| 63 |
-
"selected_places":
|
| 64 |
-
"level":
|
| 65 |
-
"score":
|
| 66 |
-
"streak":
|
| 67 |
-
"correct_since_reward":
|
| 68 |
-
"level_correct":
|
|
|
|
|
|
|
|
|
|
| 69 |
})
|
| 70 |
|
| 71 |
question, answer = generate_question(state["level"])
|
| 72 |
state["question"] = question
|
| 73 |
state["answer"] = answer
|
| 74 |
|
|
|
|
| 75 |
return (state, gr.update(visible=False), gr.update(visible=True),
|
| 76 |
-
"", _status_text(state), _safe_question(state)
|
|
|
|
| 77 |
|
| 78 |
except Exception as e:
|
| 79 |
print(f"start_game error: {e}")
|
| 80 |
return (state, gr.update(visible=True), gr.update(visible=False),
|
| 81 |
-
"β οΈ Something went wrong, try again.", "", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# ---------------------------------------------------------------------------
|
| 84 |
-
# Step 3 β Answer checking
|
| 85 |
# ---------------------------------------------------------------------------
|
| 86 |
|
| 87 |
-
def check_answer(user_input: str, state: dict):
|
| 88 |
try:
|
| 89 |
if not state or not state.get("question"):
|
| 90 |
return (state, _status_text(state), _safe_question(state), "",
|
| 91 |
-
"", gr.update(visible=False), gr.update(visible=False),
|
|
|
|
| 92 |
|
| 93 |
try:
|
| 94 |
user_answer = int(user_input.strip())
|
| 95 |
except (ValueError, AttributeError):
|
| 96 |
return (state, _status_text(state), _safe_question(state), "",
|
| 97 |
-
"β οΈ Numbers only!", gr.update(visible=False), gr.update(visible=False),
|
|
|
|
| 98 |
|
| 99 |
correct = (user_answer == state["answer"])
|
| 100 |
|
|
@@ -104,7 +168,6 @@ def check_answer(user_input: str, state: dict):
|
|
| 104 |
state["correct_since_reward"] += 1
|
| 105 |
state["level_correct"] += 1
|
| 106 |
|
| 107 |
-
# Level-up?
|
| 108 |
threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
|
| 109 |
level_msg = ""
|
| 110 |
if state["level_correct"] >= threshold and state["level"] < 4:
|
|
@@ -115,19 +178,47 @@ def check_answer(user_input: str, state: dict):
|
|
| 115 |
streak_fire = "π₯" * min(state["streak"], 5)
|
| 116 |
feedback = f"β
{level_msg}" if level_msg else f"β
Great! {streak_fire}"
|
| 117 |
|
| 118 |
-
# Reward due? β show loading panel, trigger generate step
|
| 119 |
if state["correct_since_reward"] >= REWARD_EVERY:
|
| 120 |
state["correct_since_reward"] = 0
|
| 121 |
-
state["
|
|
|
|
|
|
|
| 122 |
question, answer = generate_question(state["level"])
|
| 123 |
state["question"] = question
|
| 124 |
state["answer"] = answer
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
else:
|
| 133 |
state["streak"] = 0
|
|
@@ -140,55 +231,58 @@ def check_answer(user_input: str, state: dict):
|
|
| 140 |
|
| 141 |
return (state, _status_text(state), _safe_question(state), "",
|
| 142 |
feedback,
|
| 143 |
-
gr.update(visible=False),
|
| 144 |
-
gr.update(
|
| 145 |
-
None,
|
| 146 |
-
"")
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
-
|
|
|
|
| 150 |
state["generate_now"] = False
|
| 151 |
question, answer = generate_question(state.get("level", 1))
|
| 152 |
state["question"] = question
|
| 153 |
state["answer"] = answer
|
| 154 |
return (state, _status_text(state), _safe_question(state), "",
|
| 155 |
"β οΈ Something went wrong!",
|
| 156 |
-
gr.update(visible=False), gr.update(visible=False),
|
|
|
|
| 157 |
|
| 158 |
# ---------------------------------------------------------------------------
|
| 159 |
-
#
|
| 160 |
# ---------------------------------------------------------------------------
|
| 161 |
|
| 162 |
-
def
|
| 163 |
-
"""
|
| 164 |
if not state.get("generate_now"):
|
| 165 |
-
return
|
| 166 |
|
| 167 |
-
state["generate_now"]
|
|
|
|
|
|
|
| 168 |
|
| 169 |
try:
|
| 170 |
animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)])
|
| 171 |
places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
|
| 172 |
streak = state.get("streak", 0)
|
| 173 |
|
| 174 |
-
print(f"[
|
| 175 |
result, prompt = generate_reward_image(streak, animals, places)
|
| 176 |
-
print(f"[
|
| 177 |
|
| 178 |
-
if result is None:
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
return gr.update(visible=False), gr.update(visible=True, value=result), ""
|
| 186 |
|
| 187 |
except Exception as e:
|
| 188 |
import traceback
|
| 189 |
-
print(f"[
|
| 190 |
-
|
| 191 |
-
return gr.update(visible=False), gr.update(visible=False), f"β οΈ Error: {e}"
|
| 192 |
|
| 193 |
# ---------------------------------------------------------------------------
|
| 194 |
# Restart
|
|
@@ -196,7 +290,7 @@ def generate_image(state: dict):
|
|
| 196 |
|
| 197 |
def restart(state: dict):
|
| 198 |
return ({}, gr.update(visible=True), gr.update(visible=False),
|
| 199 |
-
gr.update(visible=False), gr.update(), "")
|
| 200 |
|
| 201 |
# ---------------------------------------------------------------------------
|
| 202 |
# Gradio UI
|
|
@@ -212,7 +306,6 @@ CSS = """
|
|
| 212 |
#feedback-box { text-align: center; font-size: 1.3em; min-height: 2em; }
|
| 213 |
#status-box { text-align: center; padding: 0.4em; border-radius: 8px; }
|
| 214 |
#reward-img { border-radius: 16px; }
|
| 215 |
-
#loader { text-align: center; font-size: 1.4em; padding: 2em; }
|
| 216 |
.answer-input input { font-size: 2em !important; text-align: center !important; }
|
| 217 |
/* Emoji picker grid β animals: 6 cols Γ 2 rows, places: 5 cols Γ 2 rows */
|
| 218 |
.emoji-group .wrap {
|
|
@@ -224,38 +317,141 @@ CSS = """
|
|
| 224 |
#animal-picker .wrap { grid-template-columns: repeat(6, 52px) !important; }
|
| 225 |
#place-picker .wrap { grid-template-columns: repeat(5, 52px) !important; }
|
| 226 |
.emoji-group label {
|
| 227 |
-
width: 52px !important;
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
font-size: 1.8em !important;
|
| 233 |
-
cursor: pointer !important;
|
| 234 |
-
border-radius: 12px !important;
|
| 235 |
-
border: 2px solid transparent !important;
|
| 236 |
-
transition: all 0.15s !important;
|
| 237 |
-
user-select: none !important;
|
| 238 |
}
|
| 239 |
.emoji-group label:has(input:checked) {
|
| 240 |
-
background: #e9d5ff !important;
|
| 241 |
-
border-color: #7c3aed !important;
|
| 242 |
}
|
| 243 |
.emoji-group input[type="checkbox"] { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
"""
|
| 245 |
|
| 246 |
LOADER_HTML = """
|
| 247 |
-
<div
|
| 248 |
-
<div style="font-size:3em; animation:
|
| 249 |
<div style="margin-top:0.5em; color:#888; font-size:1.1em;">Generating your rewardβ¦</div>
|
| 250 |
</div>
|
| 251 |
-
<style>
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
"""
|
| 255 |
|
| 256 |
with gr.Blocks(title="π¦ NumZoo") as demo:
|
| 257 |
|
| 258 |
-
state
|
|
|
|
|
|
|
| 259 |
|
| 260 |
gr.Markdown("# π¦ NumZoo", elem_id="title")
|
| 261 |
gr.Markdown("*Do maths. Win cute animals!*", elem_id="subtitle")
|
|
@@ -263,22 +459,19 @@ with gr.Blocks(title="π¦ NumZoo") as demo:
|
|
| 263 |
gr.HTML("""
|
| 264 |
<script>
|
| 265 |
document.addEventListener('DOMContentLoaded', function() {
|
| 266 |
-
|
| 267 |
if (saved) {
|
| 268 |
-
setTimeout(()
|
| 269 |
-
|
| 270 |
if (input) {
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
| 274 |
}
|
| 275 |
}, 800);
|
| 276 |
}
|
| 277 |
});
|
| 278 |
document.addEventListener('input', function(e) {
|
| 279 |
-
if (e.target.placeholder === 'Your nameβ¦')
|
| 280 |
-
localStorage.setItem('numzoo_name', e.target.value);
|
| 281 |
-
}
|
| 282 |
});
|
| 283 |
</script>
|
| 284 |
""")
|
|
@@ -313,96 +506,14 @@ with gr.Blocks(title="π¦ NumZoo") as demo:
|
|
| 313 |
|
| 314 |
with gr.Group(visible=False) as reward_panel:
|
| 315 |
gr.Markdown("### π Your reward!")
|
| 316 |
-
loader_html = gr.HTML(
|
| 317 |
reward_image = gr.Image(label="", show_label=False,
|
| 318 |
-
elem_id="reward-img", height=400)
|
| 319 |
-
reward_error = gr.Markdown(""
|
| 320 |
|
| 321 |
# ββ Collection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 322 |
-
gr.HTML(""
|
| 323 |
-
|
| 324 |
-
<div id="numzoo-collection-header"
|
| 325 |
-
style="display:none; font-weight:bold; font-size:1.1em; margin:0 0 8px 4px;">πΌοΈ Collection</div>
|
| 326 |
-
<div id="numzoo-collection"
|
| 327 |
-
style="display:flex; flex-wrap:wrap; gap:8px; justify-content:center;"></div>
|
| 328 |
-
</div>
|
| 329 |
-
|
| 330 |
-
<!-- Zoom modal -->
|
| 331 |
-
<div id="numzoo-modal"
|
| 332 |
-
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.85);
|
| 333 |
-
z-index:9999; align-items:center; justify-content:center; cursor:pointer;">
|
| 334 |
-
<img id="numzoo-modal-img"
|
| 335 |
-
style="max-width:92vw; max-height:92vh; border-radius:20px; box-shadow:0 8px 40px #0008;">
|
| 336 |
-
</div>
|
| 337 |
-
|
| 338 |
-
<script>
|
| 339 |
-
(function() {
|
| 340 |
-
const STORAGE_KEY = 'numzoo_collection';
|
| 341 |
-
const MAX_IMAGES = 30;
|
| 342 |
-
|
| 343 |
-
function getCollection() {
|
| 344 |
-
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
|
| 345 |
-
catch { return []; }
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
function renderCollection() {
|
| 349 |
-
const col = getCollection();
|
| 350 |
-
const wrap = document.getElementById('numzoo-collection');
|
| 351 |
-
const header = document.getElementById('numzoo-collection-header');
|
| 352 |
-
if (!wrap) return;
|
| 353 |
-
wrap.innerHTML = '';
|
| 354 |
-
if (col.length === 0) { header.style.display = 'none'; return; }
|
| 355 |
-
header.style.display = 'block';
|
| 356 |
-
[...col].reverse().forEach(src => {
|
| 357 |
-
const img = document.createElement('img');
|
| 358 |
-
img.src = src;
|
| 359 |
-
Object.assign(img.style, {
|
| 360 |
-
width: '80px', height: '80px', objectFit: 'cover',
|
| 361 |
-
borderRadius: '12px', cursor: 'pointer',
|
| 362 |
-
border: '2px solid transparent', transition: 'border-color .15s'
|
| 363 |
-
});
|
| 364 |
-
img.onmouseenter = () => img.style.borderColor = '#7c3aed';
|
| 365 |
-
img.onmouseleave = () => img.style.borderColor = 'transparent';
|
| 366 |
-
img.onclick = () => {
|
| 367 |
-
document.getElementById('numzoo-modal-img').src = src;
|
| 368 |
-
document.getElementById('numzoo-modal').style.display = 'flex';
|
| 369 |
-
};
|
| 370 |
-
wrap.appendChild(img);
|
| 371 |
-
});
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
document.getElementById('numzoo-modal').onclick = function() {
|
| 375 |
-
this.style.display = 'none';
|
| 376 |
-
};
|
| 377 |
-
|
| 378 |
-
// Poll for new reward image and save to localStorage via canvas
|
| 379 |
-
let lastSaved = '';
|
| 380 |
-
setInterval(() => {
|
| 381 |
-
const img = document.querySelector('#reward-img img');
|
| 382 |
-
if (!img || !img.complete || !img.src || img.src === lastSaved) return;
|
| 383 |
-
if (img.src.includes('.svg') || img.naturalWidth === 0) return;
|
| 384 |
-
lastSaved = img.src;
|
| 385 |
-
try {
|
| 386 |
-
const canvas = document.createElement('canvas');
|
| 387 |
-
canvas.width = img.naturalWidth;
|
| 388 |
-
canvas.height = img.naturalHeight;
|
| 389 |
-
canvas.getContext('2d').drawImage(img, 0, 0);
|
| 390 |
-
const dataUrl = canvas.toDataURL('image/jpeg', 0.75);
|
| 391 |
-
const col = getCollection();
|
| 392 |
-
if (!col.includes(dataUrl)) {
|
| 393 |
-
col.push(dataUrl);
|
| 394 |
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(col.slice(-MAX_IMAGES)));
|
| 395 |
-
renderCollection();
|
| 396 |
-
}
|
| 397 |
-
} catch(e) { console.warn('NumZoo collection save error:', e); }
|
| 398 |
-
}, 800);
|
| 399 |
-
|
| 400 |
-
// Render on page load
|
| 401 |
-
document.addEventListener('DOMContentLoaded', renderCollection);
|
| 402 |
-
setTimeout(renderCollection, 1200); // fallback after Gradio hydrates
|
| 403 |
-
})();
|
| 404 |
-
</script>
|
| 405 |
-
""")
|
| 406 |
|
| 407 |
restart_btn = gr.Button("π Restart", variant="secondary", size="sm")
|
| 408 |
|
|
@@ -415,25 +526,37 @@ with gr.Blocks(title="π¦ NumZoo") as demo:
|
|
| 415 |
[state, welcome_panel, emoji_panel, game_panel,
|
| 416 |
animal_picker, place_picker])
|
| 417 |
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
gen_outputs = [loader_html, reward_image, reward_error]
|
| 426 |
-
|
| 427 |
-
check_btn.click(check_answer, [answer_input, state], check_outputs).then(
|
| 428 |
-
generate_image, inputs=[state], outputs=gen_outputs
|
| 429 |
-
)
|
| 430 |
-
answer_input.submit(check_answer, [answer_input, state], check_outputs).then(
|
| 431 |
-
generate_image, inputs=[state], outputs=gen_outputs
|
| 432 |
)
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
restart_btn.click(restart, [state],
|
| 435 |
[state, welcome_panel, emoji_panel, game_panel,
|
| 436 |
-
player_name, picker_error])
|
| 437 |
|
| 438 |
|
| 439 |
if __name__ == "__main__":
|
|
|
|
| 2 |
π¦ NumZoo β Math practice with cute AI-generated animal rewards!
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import base64
|
| 6 |
+
import io
|
| 7 |
+
import json
|
| 8 |
import random
|
| 9 |
+
|
| 10 |
import gradio as gr
|
| 11 |
+
|
| 12 |
from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
|
| 13 |
from image_generator import generate_reward_image
|
| 14 |
|
|
|
|
| 33 |
def _safe_question(state: dict) -> str:
|
| 34 |
return f"## {state.get('question', '')} = ?"
|
| 35 |
|
| 36 |
+
def _img_to_data_url(pil_image) -> str:
|
| 37 |
+
"""Convert PIL image to JPEG data URL for localStorage."""
|
| 38 |
+
buf = io.BytesIO()
|
| 39 |
+
pil_image.save(buf, format="JPEG", quality=80)
|
| 40 |
+
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 41 |
+
|
| 42 |
+
def _coll(*actions) -> str:
|
| 43 |
+
"""Encode collection actions into the collection_trigger HTML component.
|
| 44 |
+
Each action is a (action_name, reward_id, src) tuple."""
|
| 45 |
+
payload = json.dumps({
|
| 46 |
+
"ts": random.random(),
|
| 47 |
+
"actions": [{"action": a, "id": str(i), "src": s} for a, i, s in actions],
|
| 48 |
+
})
|
| 49 |
+
return f"<div id='numzoo-coll-data' data-payload='{payload}'></div>"
|
| 50 |
+
|
| 51 |
# ---------------------------------------------------------------------------
|
| 52 |
# Step 1 β Name entry
|
| 53 |
# ---------------------------------------------------------------------------
|
|
|
|
| 76 |
try:
|
| 77 |
if not animal_sel or not place_sel:
|
| 78 |
return (state, gr.update(visible=True), gr.update(visible=False),
|
| 79 |
+
"β οΈ Pick at least one animal and one place!", "", "", "")
|
| 80 |
|
| 81 |
state.update({
|
| 82 |
+
"selected_animals": animal_sel[:3],
|
| 83 |
+
"selected_places": place_sel[:3],
|
| 84 |
+
"level": 1,
|
| 85 |
+
"score": 0,
|
| 86 |
+
"streak": 0,
|
| 87 |
+
"correct_since_reward": 0,
|
| 88 |
+
"level_correct": 0,
|
| 89 |
+
"reward_count": 0,
|
| 90 |
+
"pregenerate_next": True, # trigger background generation immediately
|
| 91 |
+
"generate_now": False,
|
| 92 |
})
|
| 93 |
|
| 94 |
question, answer = generate_question(state["level"])
|
| 95 |
state["question"] = question
|
| 96 |
state["answer"] = answer
|
| 97 |
|
| 98 |
+
# Add locked placeholder for reward #1 immediately
|
| 99 |
return (state, gr.update(visible=False), gr.update(visible=True),
|
| 100 |
+
"", _status_text(state), _safe_question(state),
|
| 101 |
+
_coll(("add-locked", 1, "")))
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
print(f"start_game error: {e}")
|
| 105 |
return (state, gr.update(visible=True), gr.update(visible=False),
|
| 106 |
+
"β οΈ Something went wrong, try again.", "", "", "")
|
| 107 |
+
|
| 108 |
+
# ---------------------------------------------------------------------------
|
| 109 |
+
# Background pre-generation β starts right after "Let's go!" and again after
|
| 110 |
+
# each reward, so the next image is ready before the user earns it.
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
|
| 113 |
+
def pregenerate_image(state: dict):
|
| 114 |
+
"""Generate next reward image in background. No-op if pregenerate_next is False."""
|
| 115 |
+
if not state.get("pregenerate_next"):
|
| 116 |
+
return state, gr.update(), gr.update(), ""
|
| 117 |
+
|
| 118 |
+
state["pregenerate_next"] = False
|
| 119 |
+
next_id = state.get("reward_count", 0) + 1
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)])
|
| 123 |
+
places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
|
| 124 |
+
streak = state.get("streak", 0)
|
| 125 |
+
|
| 126 |
+
print(f"[pregenerate] generating reward #{next_id}")
|
| 127 |
+
result, prompt = generate_reward_image(streak, animals, places)
|
| 128 |
+
print(f"[pregenerate] reward #{next_id} done | prompt={prompt!r}")
|
| 129 |
+
|
| 130 |
+
if result is not None:
|
| 131 |
+
data_url = _img_to_data_url(result)
|
| 132 |
+
state["pending_reward_id"] = next_id
|
| 133 |
+
# Store in hidden components β collection stays LOCKED until user earns reward
|
| 134 |
+
return state, result, data_url, ""
|
| 135 |
+
|
| 136 |
+
state.pop("pending_reward_id", None)
|
| 137 |
+
return state, None, "", ""
|
| 138 |
+
|
| 139 |
+
except Exception as e:
|
| 140 |
+
import traceback
|
| 141 |
+
print(f"[pregenerate] β {e}\n{traceback.format_exc()}")
|
| 142 |
+
state.pop("pending_reward_id", None)
|
| 143 |
+
return state, None, "", ""
|
| 144 |
|
| 145 |
# ---------------------------------------------------------------------------
|
| 146 |
+
# Step 3 β Answer checking
|
| 147 |
# ---------------------------------------------------------------------------
|
| 148 |
|
| 149 |
+
def check_answer(user_input: str, state: dict, pre_image, pre_data_url: str):
|
| 150 |
try:
|
| 151 |
if not state or not state.get("question"):
|
| 152 |
return (state, _status_text(state), _safe_question(state), "",
|
| 153 |
+
"", gr.update(visible=False), "", gr.update(visible=False), "",
|
| 154 |
+
gr.update(), gr.update(), "")
|
| 155 |
|
| 156 |
try:
|
| 157 |
user_answer = int(user_input.strip())
|
| 158 |
except (ValueError, AttributeError):
|
| 159 |
return (state, _status_text(state), _safe_question(state), "",
|
| 160 |
+
"β οΈ Numbers only!", gr.update(visible=False), "", gr.update(visible=False), "",
|
| 161 |
+
gr.update(), gr.update(), "")
|
| 162 |
|
| 163 |
correct = (user_answer == state["answer"])
|
| 164 |
|
|
|
|
| 168 |
state["correct_since_reward"] += 1
|
| 169 |
state["level_correct"] += 1
|
| 170 |
|
|
|
|
| 171 |
threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
|
| 172 |
level_msg = ""
|
| 173 |
if state["level_correct"] >= threshold and state["level"] < 4:
|
|
|
|
| 178 |
streak_fire = "π₯" * min(state["streak"], 5)
|
| 179 |
feedback = f"β
{level_msg}" if level_msg else f"β
Great! {streak_fire}"
|
| 180 |
|
|
|
|
| 181 |
if state["correct_since_reward"] >= REWARD_EVERY:
|
| 182 |
state["correct_since_reward"] = 0
|
| 183 |
+
state["reward_count"] = state.get("reward_count", 0) + 1
|
| 184 |
+
reward_id = state["reward_count"]
|
| 185 |
+
|
| 186 |
question, answer = generate_question(state["level"])
|
| 187 |
state["question"] = question
|
| 188 |
state["answer"] = answer
|
| 189 |
+
|
| 190 |
+
# Check if a pre-generated image is ready for this exact reward
|
| 191 |
+
pending_id = state.get("pending_reward_id")
|
| 192 |
+
has_pre = pre_image is not None and bool(pre_data_url) and pending_id == reward_id
|
| 193 |
+
|
| 194 |
+
if has_pre:
|
| 195 |
+
# β
Pre-generated image ready β show immediately, unlock in collection
|
| 196 |
+
state.pop("pending_reward_id", None)
|
| 197 |
+
state["pregenerate_next"] = True # kick off next background gen
|
| 198 |
+
next_id = reward_id + 1
|
| 199 |
+
return (state, _status_text(state), _safe_question(state), "",
|
| 200 |
+
feedback,
|
| 201 |
+
gr.update(visible=True), # reward_panel
|
| 202 |
+
"", # loader cleared
|
| 203 |
+
gr.update(visible=True, value=pre_image), # image shown
|
| 204 |
+
"",
|
| 205 |
+
None, # clear hidden_image
|
| 206 |
+
"", # clear hidden_data_url
|
| 207 |
+
_coll(("unlock", reward_id, pre_data_url),
|
| 208 |
+
("add-locked", next_id, "")))
|
| 209 |
+
else:
|
| 210 |
+
# β³ On-demand generation (image not ready yet)
|
| 211 |
+
state["generate_now"] = True
|
| 212 |
+
state["pregenerate_next"] = False
|
| 213 |
+
return (state, _status_text(state), _safe_question(state), "",
|
| 214 |
+
feedback,
|
| 215 |
+
gr.update(visible=True), # reward_panel
|
| 216 |
+
LOADER_HTML, # show loader
|
| 217 |
+
gr.update(visible=False), # image hidden
|
| 218 |
+
"",
|
| 219 |
+
gr.update(), # keep hidden_image
|
| 220 |
+
gr.update(), # keep hidden_data_url
|
| 221 |
+
"") # collection stays locked
|
| 222 |
|
| 223 |
else:
|
| 224 |
state["streak"] = 0
|
|
|
|
| 231 |
|
| 232 |
return (state, _status_text(state), _safe_question(state), "",
|
| 233 |
feedback,
|
| 234 |
+
gr.update(visible=False), "", gr.update(visible=False), "",
|
| 235 |
+
gr.update(), gr.update(), "")
|
|
|
|
|
|
|
| 236 |
|
| 237 |
except Exception as e:
|
| 238 |
+
import traceback
|
| 239 |
+
print(f"check_answer error: {e}\n{traceback.format_exc()}")
|
| 240 |
state["generate_now"] = False
|
| 241 |
question, answer = generate_question(state.get("level", 1))
|
| 242 |
state["question"] = question
|
| 243 |
state["answer"] = answer
|
| 244 |
return (state, _status_text(state), _safe_question(state), "",
|
| 245 |
"β οΈ Something went wrong!",
|
| 246 |
+
gr.update(visible=False), "", gr.update(visible=False), "",
|
| 247 |
+
gr.update(), gr.update(), "")
|
| 248 |
|
| 249 |
# ---------------------------------------------------------------------------
|
| 250 |
+
# On-demand generation β fallback when pre-image wasn't ready
|
| 251 |
# ---------------------------------------------------------------------------
|
| 252 |
|
| 253 |
+
def generate_on_demand(state: dict):
|
| 254 |
+
"""Generate image on demand. No-op if generate_now is False."""
|
| 255 |
if not state.get("generate_now"):
|
| 256 |
+
return state, "", gr.update(), "", ""
|
| 257 |
|
| 258 |
+
state["generate_now"] = False
|
| 259 |
+
state["pregenerate_next"] = True # kick off next background gen after this
|
| 260 |
+
reward_id = state.get("reward_count", 1)
|
| 261 |
|
| 262 |
try:
|
| 263 |
animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)])
|
| 264 |
places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
|
| 265 |
streak = state.get("streak", 0)
|
| 266 |
|
| 267 |
+
print(f"[on_demand] generating reward #{reward_id}")
|
| 268 |
result, prompt = generate_reward_image(streak, animals, places)
|
| 269 |
+
print(f"[on_demand] done | reward #{reward_id}")
|
| 270 |
|
| 271 |
+
if result is not None:
|
| 272 |
+
data_url = _img_to_data_url(result)
|
| 273 |
+
next_id = reward_id + 1
|
| 274 |
+
return (state, "",
|
| 275 |
+
gr.update(visible=True, value=result),
|
| 276 |
+
"",
|
| 277 |
+
_coll(("unlock", reward_id, data_url),
|
| 278 |
+
("add-locked", next_id, "")))
|
| 279 |
|
| 280 |
+
return state, "", gr.update(visible=False), "β οΈ Could not generate image β try again later!", ""
|
|
|
|
| 281 |
|
| 282 |
except Exception as e:
|
| 283 |
import traceback
|
| 284 |
+
print(f"[on_demand] β {e}\n{traceback.format_exc()}")
|
| 285 |
+
return state, "", gr.update(visible=False), f"β οΈ Error: {e}", ""
|
|
|
|
| 286 |
|
| 287 |
# ---------------------------------------------------------------------------
|
| 288 |
# Restart
|
|
|
|
| 290 |
|
| 291 |
def restart(state: dict):
|
| 292 |
return ({}, gr.update(visible=True), gr.update(visible=False),
|
| 293 |
+
gr.update(visible=False), gr.update(), "", None, "")
|
| 294 |
|
| 295 |
# ---------------------------------------------------------------------------
|
| 296 |
# Gradio UI
|
|
|
|
| 306 |
#feedback-box { text-align: center; font-size: 1.3em; min-height: 2em; }
|
| 307 |
#status-box { text-align: center; padding: 0.4em; border-radius: 8px; }
|
| 308 |
#reward-img { border-radius: 16px; }
|
|
|
|
| 309 |
.answer-input input { font-size: 2em !important; text-align: center !important; }
|
| 310 |
/* Emoji picker grid β animals: 6 cols Γ 2 rows, places: 5 cols Γ 2 rows */
|
| 311 |
.emoji-group .wrap {
|
|
|
|
| 317 |
#animal-picker .wrap { grid-template-columns: repeat(6, 52px) !important; }
|
| 318 |
#place-picker .wrap { grid-template-columns: repeat(5, 52px) !important; }
|
| 319 |
.emoji-group label {
|
| 320 |
+
width: 52px !important; height: 52px !important;
|
| 321 |
+
display: flex !important; align-items: center !important; justify-content: center !important;
|
| 322 |
+
font-size: 1.8em !important; cursor: pointer !important;
|
| 323 |
+
border-radius: 12px !important; border: 2px solid transparent !important;
|
| 324 |
+
transition: all 0.15s !important; user-select: none !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
}
|
| 326 |
.emoji-group label:has(input:checked) {
|
| 327 |
+
background: #e9d5ff !important; border-color: #7c3aed !important;
|
|
|
|
| 328 |
}
|
| 329 |
.emoji-group input[type="checkbox"] { display: none !important; }
|
| 330 |
+
/* Unlock reveal animation */
|
| 331 |
+
@keyframes numzoo-unblur {
|
| 332 |
+
from { filter: blur(16px) brightness(0.4); transform: scale(0.95); }
|
| 333 |
+
to { filter: blur(0) brightness(1); transform: scale(1); }
|
| 334 |
+
}
|
| 335 |
+
.numzoo-unlock { animation: numzoo-unblur 0.7s ease-out forwards; }
|
| 336 |
+
/* Collection locked pulse */
|
| 337 |
+
@keyframes numzoo-pulse { 0%,100% { opacity:.4; } 50% { opacity:.9; } }
|
| 338 |
"""
|
| 339 |
|
| 340 |
LOADER_HTML = """
|
| 341 |
+
<div style="text-align:center; padding:2em;">
|
| 342 |
+
<div style="font-size:3em; animation:spin 1s linear infinite; display:inline-block;">π¨</div>
|
| 343 |
<div style="margin-top:0.5em; color:#888; font-size:1.1em;">Generating your rewardβ¦</div>
|
| 344 |
</div>
|
| 345 |
+
<style>@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style>
|
| 346 |
+
"""
|
| 347 |
+
|
| 348 |
+
COLLECTION_HTML = """
|
| 349 |
+
<div id="numzoo-collection-wrap" style="margin-top:16px;">
|
| 350 |
+
<div id="numzoo-collection-header"
|
| 351 |
+
style="display:none; font-weight:bold; font-size:1.05em; margin:0 0 8px 4px;">πΌοΈ Collection</div>
|
| 352 |
+
<div id="numzoo-collection"
|
| 353 |
+
style="display:flex; flex-wrap:wrap; gap:8px; justify-content:center;"></div>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
+
<!-- Zoom modal -->
|
| 357 |
+
<div id="numzoo-modal"
|
| 358 |
+
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88);
|
| 359 |
+
z-index:9999; align-items:center; justify-content:center; cursor:pointer;">
|
| 360 |
+
<img id="numzoo-modal-img"
|
| 361 |
+
style="max-width:92vw; max-height:92vh; border-radius:20px; box-shadow:0 8px 40px #0006;">
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<script>
|
| 365 |
+
(function() {
|
| 366 |
+
var KEY = 'numzoo_collection_v2', MAX = 30;
|
| 367 |
+
|
| 368 |
+
function getCol() { try { return JSON.parse(localStorage.getItem(KEY)||'[]'); } catch(e) { return []; } }
|
| 369 |
+
function saveCol(c) { localStorage.setItem(KEY, JSON.stringify(c.slice(-MAX))); }
|
| 370 |
+
|
| 371 |
+
window.numzooAddLocked = function(id) {
|
| 372 |
+
id = String(id);
|
| 373 |
+
var col = getCol();
|
| 374 |
+
if (!col.find(function(x){return x.id===id;})) {
|
| 375 |
+
col.push({id:id, status:'locked', src:null});
|
| 376 |
+
saveCol(col);
|
| 377 |
+
}
|
| 378 |
+
renderCollection();
|
| 379 |
+
};
|
| 380 |
+
|
| 381 |
+
window.numzooUnlock = function(id, src) {
|
| 382 |
+
id = String(id);
|
| 383 |
+
var col = getCol(), item = col.find(function(x){return x.id===id;});
|
| 384 |
+
if (item) { item.status='unlocked'; item.src=src; }
|
| 385 |
+
else { col.push({id:id, status:'unlocked', src:src}); }
|
| 386 |
+
saveCol(col);
|
| 387 |
+
renderCollection();
|
| 388 |
+
};
|
| 389 |
+
|
| 390 |
+
window.numzooMarkFailed = function(id) {
|
| 391 |
+
id = String(id);
|
| 392 |
+
saveCol(getCol().filter(function(x){return x.id!==id;}));
|
| 393 |
+
renderCollection();
|
| 394 |
+
};
|
| 395 |
+
|
| 396 |
+
function renderCollection() {
|
| 397 |
+
var col = getCol(), wrap = document.getElementById('numzoo-collection'),
|
| 398 |
+
hdr = document.getElementById('numzoo-collection-header');
|
| 399 |
+
if (!wrap) return;
|
| 400 |
+
wrap.innerHTML = '';
|
| 401 |
+
if (!col.length) { hdr.style.display='none'; return; }
|
| 402 |
+
hdr.style.display = 'block';
|
| 403 |
+
col.slice().reverse().forEach(function(item) {
|
| 404 |
+
var el = document.createElement('div');
|
| 405 |
+
el.style.cssText = 'position:relative;width:80px;height:80px;border-radius:12px;overflow:hidden;flex-shrink:0;';
|
| 406 |
+
if (item.status === 'locked') {
|
| 407 |
+
el.style.background = '#1e1e2e';
|
| 408 |
+
el.style.border = '2px dashed #555';
|
| 409 |
+
el.innerHTML =
|
| 410 |
+
'<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:2em;z-index:1;position:relative;">π</div>' +
|
| 411 |
+
'<div style="position:absolute;inset:0;background:linear-gradient(135deg,#2d2d44,#111128);animation:numzoo-pulse 2s ease-in-out infinite;"></div>';
|
| 412 |
+
} else if (item.src) {
|
| 413 |
+
var img = document.createElement('img');
|
| 414 |
+
img.src = item.src;
|
| 415 |
+
img.style.cssText = 'width:100%;height:100%;object-fit:cover;cursor:pointer;display:block;';
|
| 416 |
+
img.onclick = function() {
|
| 417 |
+
document.getElementById('numzoo-modal-img').src = item.src;
|
| 418 |
+
document.getElementById('numzoo-modal').style.display = 'flex';
|
| 419 |
+
};
|
| 420 |
+
el.appendChild(img);
|
| 421 |
+
}
|
| 422 |
+
wrap.appendChild(el);
|
| 423 |
+
});
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
document.getElementById('numzoo-modal').onclick = function() { this.style.display='none'; };
|
| 427 |
+
|
| 428 |
+
// Poll the collection_trigger component for action payloads
|
| 429 |
+
var lastTs = -1;
|
| 430 |
+
setInterval(function() {
|
| 431 |
+
var el = document.getElementById('numzoo-coll-data');
|
| 432 |
+
if (!el) return;
|
| 433 |
+
try {
|
| 434 |
+
var data = JSON.parse(el.dataset.payload||'{}');
|
| 435 |
+
if (!data.ts || data.ts === lastTs) return;
|
| 436 |
+
lastTs = data.ts;
|
| 437 |
+
(data.actions||[]).forEach(function(act) {
|
| 438 |
+
if (act.action === 'add-locked') window.numzooAddLocked(act.id);
|
| 439 |
+
else if (act.action === 'unlock') window.numzooUnlock(act.id, act.src);
|
| 440 |
+
else if (act.action === 'failed') window.numzooMarkFailed(act.id);
|
| 441 |
+
});
|
| 442 |
+
} catch(e) {}
|
| 443 |
+
}, 200);
|
| 444 |
+
|
| 445 |
+
setTimeout(renderCollection, 800);
|
| 446 |
+
})();
|
| 447 |
+
</script>
|
| 448 |
"""
|
| 449 |
|
| 450 |
with gr.Blocks(title="π¦ NumZoo") as demo:
|
| 451 |
|
| 452 |
+
state = gr.State({})
|
| 453 |
+
hidden_image = gr.Image(visible=False, label="", type="pil")
|
| 454 |
+
hidden_data_url = gr.Textbox(visible=False, value="")
|
| 455 |
|
| 456 |
gr.Markdown("# π¦ NumZoo", elem_id="title")
|
| 457 |
gr.Markdown("*Do maths. Win cute animals!*", elem_id="subtitle")
|
|
|
|
| 459 |
gr.HTML("""
|
| 460 |
<script>
|
| 461 |
document.addEventListener('DOMContentLoaded', function() {
|
| 462 |
+
var saved = localStorage.getItem('numzoo_name');
|
| 463 |
if (saved) {
|
| 464 |
+
setTimeout(function() {
|
| 465 |
+
var input = document.querySelector('input[placeholder="Your nameβ¦"]');
|
| 466 |
if (input) {
|
| 467 |
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input, saved);
|
| 468 |
+
input.dispatchEvent(new Event('input', {bubbles:true}));
|
|
|
|
| 469 |
}
|
| 470 |
}, 800);
|
| 471 |
}
|
| 472 |
});
|
| 473 |
document.addEventListener('input', function(e) {
|
| 474 |
+
if (e.target.placeholder === 'Your nameβ¦') localStorage.setItem('numzoo_name', e.target.value);
|
|
|
|
|
|
|
| 475 |
});
|
| 476 |
</script>
|
| 477 |
""")
|
|
|
|
| 506 |
|
| 507 |
with gr.Group(visible=False) as reward_panel:
|
| 508 |
gr.Markdown("### π Your reward!")
|
| 509 |
+
loader_html = gr.HTML("") # empty = hidden; content = shown
|
| 510 |
reward_image = gr.Image(label="", show_label=False,
|
| 511 |
+
elem_id="reward-img", height=400, visible=False)
|
| 512 |
+
reward_error = gr.Markdown("")
|
| 513 |
|
| 514 |
# ββ Collection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 515 |
+
collection_trigger = gr.HTML("") # receives JSON-encoded collection action payloads
|
| 516 |
+
gr.HTML(COLLECTION_HTML)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
restart_btn = gr.Button("π Restart", variant="secondary", size="sm")
|
| 519 |
|
|
|
|
| 526 |
[state, welcome_panel, emoji_panel, game_panel,
|
| 527 |
animal_picker, place_picker])
|
| 528 |
|
| 529 |
+
# "Let's go!" β start game β immediately pre-generate first reward image
|
| 530 |
+
go_btn.click(
|
| 531 |
+
start_game, [animal_picker, place_picker, state],
|
| 532 |
+
[state, emoji_panel, game_panel, picker_error, status_md, question_md, collection_trigger]
|
| 533 |
+
).then(
|
| 534 |
+
pregenerate_image, [state],
|
| 535 |
+
[state, hidden_image, hidden_data_url, collection_trigger]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
)
|
| 537 |
|
| 538 |
+
check_outputs = [
|
| 539 |
+
state, status_md, question_md, answer_input,
|
| 540 |
+
feedback_md, reward_panel, loader_html, reward_image, reward_error,
|
| 541 |
+
hidden_image, hidden_data_url, collection_trigger,
|
| 542 |
+
]
|
| 543 |
+
ondemand_outputs = [state, loader_html, reward_image, reward_error, collection_trigger]
|
| 544 |
+
pre_outputs = [state, hidden_image, hidden_data_url, collection_trigger]
|
| 545 |
+
|
| 546 |
+
for trigger in [check_btn.click, answer_input.submit]:
|
| 547 |
+
trigger(
|
| 548 |
+
check_answer,
|
| 549 |
+
[answer_input, state, hidden_image, hidden_data_url],
|
| 550 |
+
check_outputs,
|
| 551 |
+
).then(
|
| 552 |
+
generate_on_demand, [state], ondemand_outputs
|
| 553 |
+
).then(
|
| 554 |
+
pregenerate_image, [state], pre_outputs
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
restart_btn.click(restart, [state],
|
| 558 |
[state, welcome_panel, emoji_panel, game_panel,
|
| 559 |
+
player_name, picker_error, hidden_image, hidden_data_url])
|
| 560 |
|
| 561 |
|
| 562 |
if __name__ == "__main__":
|