kenlolhku commited on
Commit
4870df5
·
verified ·
1 Parent(s): 834e2e3

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -112
app.py CHANGED
@@ -1,7 +1,4 @@
1
- """Gradio Egg Timer app with a single action button.
2
-
3
- Designed to avoid flaky multi-button visibility updates in Gradio.
4
- """
5
 
6
  from __future__ import annotations
7
 
@@ -16,10 +13,10 @@ DEFAULT_EGG_COUNT = 1
16
  PLACEHOLDER_TIME = "--:--"
17
 
18
  ACTION_LABELS = {
19
- "idle": "開始煲水先",
20
- "boil": "水滾起",
21
  "simmer": "已經轉細火",
22
- "add_eggs": "已放,開始計時",
23
  "countdown": "計時中…",
24
  "done": "已完成",
25
  }
@@ -28,7 +25,7 @@ ACTION_LABELS = {
28
  def _timer_html(display_time: str) -> str:
29
  return (
30
  '<div style="text-align:center; margin: 8px 0 16px;">'
31
- '<div style="font-size:16px; color:#6b7280; margin-bottom:6px;">烚強倒數</div>'
32
  f'<div style="font-size:clamp(56px, 12vw, 110px); line-height:1; font-weight:700; letter-spacing:2px;">{display_time}</div>'
33
  "</div>"
34
  )
@@ -37,19 +34,19 @@ def _timer_html(display_time: str) -> str:
37
  def _build_status(stage: str, egg_count: int, doneness: str | None, total_seconds: int | None) -> str:
38
  if stage == "idle":
39
  if not doneness:
40
- return "### 歡迎你!\n請決定想烚到幾熟,我會一步一步烚,弱智都識。"
41
  return (
42
  "### 準備好可以開始\n"
43
  f"目前設定:**{egg_count} 隻蛋 / {doneness}**\n"
44
  f"建議時間:**{format_mm_ss(total_seconds or 0)}**"
45
  )
46
  if stage == "boil":
47
- return "### 第 1 步:煲滾水\n等到水大滾先進入下一步,唔使快,最緊要急。"
48
  if stage == "simmer":
49
- return "### 第 2 步:轉細火\n水變到微滾狀態,等水穩定啲再放蛋。"
50
  if stage == "add_eggs":
51
  return (
52
- "### 第 3 步:輕輕放D入水,咪撚整爛\n"
53
  f"而家會用 **{egg_count} 隻蛋 / {doneness}** 去計時。\n"
54
  f"建議時間:**{format_mm_ss(total_seconds or 0)}**"
55
  )
@@ -61,81 +58,95 @@ def _build_status(stage: str, egg_count: int, doneness: str | None, total_second
61
  return "### 完成!\n雞蛋煮好啦,可以即刻過冰水 30-60 秒,口感會更靚。"
62
 
63
 
64
- def _expected_action_label(stage: str, doneness: str | None) -> str:
65
- if stage == "idle" and not doneness:
66
- return "請先選熟度"
67
- return ACTION_LABELS.get(stage, "未知")
68
-
69
-
70
  def _controls_locked(stage: str) -> bool:
71
  return stage in {"countdown", "done"}
72
 
73
 
74
  def _action_button_update(stage: str, doneness: str | None) -> Any:
75
- if stage == "countdown":
76
- return gr.update(value=ACTION_LABELS[stage], visible=True, interactive=False)
77
- if stage == "done":
78
  return gr.update(value=ACTION_LABELS[stage], visible=True, interactive=False)
79
  if stage == "idle" and not doneness:
80
  return gr.update(value="請先選熟度", visible=True, interactive=False)
81
  return gr.update(value=ACTION_LABELS.get(stage, "下一步"), visible=True, interactive=True)
82
 
83
 
84
- def _build_debug_text(
85
- *,
86
- stage: str,
87
- source: str,
88
- egg_count: int | None,
89
- doneness: str | None,
90
- running: bool,
91
- total_seconds: int,
92
- remaining_seconds: int,
93
- ) -> str:
94
- egg_text = "-" if egg_count is None else str(egg_count)
95
- doneness_text = "-" if doneness is None else doneness
96
- return (
97
- f"source={source} | "
98
- f"stage={stage} | "
99
- f"eggs={egg_text} | "
100
- f"doneness={doneness_text} | "
101
- f"running={running} | "
102
- f"total={total_seconds} | "
103
- f"remaining={remaining_seconds} | "
104
- f"expected_action={_expected_action_label(stage, doneness)} | "
105
- f"controls_locked={_controls_locked(stage)}"
106
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
 
109
  def _render_screen(
110
  *,
111
  stage: str,
112
- source: str,
113
  egg_count: int,
114
  doneness: str | None,
115
  total_seconds: int,
116
  remaining_seconds: int,
117
  running: bool,
118
  status_override: str | None = None,
 
119
  ):
 
120
  display_seconds = remaining_seconds if stage in {"countdown", "done"} else total_seconds
121
  display_text = PLACEHOLDER_TIME if display_seconds <= 0 else format_mm_ss(display_seconds)
122
  status = status_override or _build_status(stage, egg_count, doneness, total_seconds)
123
- debug_text = _build_debug_text(
124
- stage=stage,
125
- source=source,
126
- egg_count=egg_count,
127
- doneness=doneness,
128
- running=running,
129
- total_seconds=total_seconds,
130
- remaining_seconds=remaining_seconds,
131
- )
132
  return (
133
  status,
134
- debug_text,
135
  _timer_html(display_text),
136
  _action_button_update(stage, doneness),
137
  gr.update(interactive=not _controls_locked(stage)),
138
  gr.update(interactive=not _controls_locked(stage)),
 
139
  )
140
 
141
 
@@ -143,14 +154,14 @@ def preview_settings(egg_count: int, doneness: str | None, stage: str, running:
143
  total_seconds = calculate_cook_seconds(egg_count, doneness) if doneness else 0
144
 
145
  if stage == "idle":
146
- status, debug_text, timer_html, btn_action, egg_update, doneness_update = _render_screen(
147
  stage="idle",
148
- source="preview_idle",
149
  egg_count=egg_count,
150
  doneness=doneness,
151
  total_seconds=total_seconds,
152
  remaining_seconds=total_seconds,
153
  running=False,
 
154
  )
155
  return (
156
  total_seconds,
@@ -158,29 +169,21 @@ def preview_settings(egg_count: int, doneness: str | None, stage: str, running:
158
  timer_html,
159
  status,
160
  btn_action,
161
- debug_text,
162
  egg_update,
163
  doneness_update,
 
164
  )
165
 
166
- debug_text = _build_debug_text(
167
- stage=stage,
168
- source="preview_non_idle",
169
- egg_count=egg_count,
170
- doneness=doneness,
171
- running=running,
172
- total_seconds=total_seconds,
173
- remaining_seconds=remaining_seconds,
174
- )
175
  return (
176
  total_seconds,
177
  gr.update(),
178
  gr.update(),
179
  gr.update(),
180
  gr.update(),
181
- debug_text,
182
  gr.update(interactive=not _controls_locked(stage)),
183
  gr.update(interactive=not _controls_locked(stage)),
 
184
  )
185
 
186
 
@@ -196,13 +199,13 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
196
  gr.update(active=False),
197
  *_render_screen(
198
  stage="idle",
199
- source="action_idle_blocked",
200
  egg_count=egg_count,
201
  doneness=doneness,
202
  total_seconds=0,
203
  remaining_seconds=0,
204
  running=False,
205
  status_override="### 未可以開始\n請先揀熟度。",
 
206
  ),
207
  )
208
  new_total = calculate_cook_seconds(egg_count, doneness)
@@ -215,12 +218,12 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
215
  gr.update(active=False),
216
  *_render_screen(
217
  stage="boil",
218
- source="action_to_boil",
219
  egg_count=egg_count,
220
  doneness=doneness,
221
  total_seconds=new_total,
222
  remaining_seconds=new_total,
223
  running=False,
 
224
  ),
225
  )
226
 
@@ -234,12 +237,12 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
234
  gr.update(active=False),
235
  *_render_screen(
236
  stage="simmer",
237
- source="action_to_simmer",
238
  egg_count=egg_count,
239
  doneness=doneness,
240
  total_seconds=total_seconds,
241
  remaining_seconds=total_seconds,
242
  running=False,
 
243
  ),
244
  )
245
 
@@ -253,12 +256,12 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
253
  gr.update(active=False),
254
  *_render_screen(
255
  stage="add_eggs",
256
- source="action_to_add_eggs",
257
  egg_count=egg_count,
258
  doneness=doneness,
259
  total_seconds=total_seconds,
260
  remaining_seconds=total_seconds,
261
  running=False,
 
262
  ),
263
  )
264
 
@@ -273,12 +276,12 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
273
  gr.update(active=True),
274
  *_render_screen(
275
  stage="countdown",
276
- source="action_begin_countdown",
277
  egg_count=egg_count,
278
  doneness=doneness,
279
  total_seconds=total_seconds,
280
  remaining_seconds=total_seconds,
281
  running=True,
 
282
  ),
283
  )
284
 
@@ -291,12 +294,12 @@ def handle_action(stage: str, egg_count: int, doneness: str | None, total_second
291
  gr.update(active=False),
292
  *_render_screen(
293
  stage=stage,
294
- source="action_noop",
295
  egg_count=egg_count,
296
  doneness=doneness,
297
  total_seconds=total_seconds,
298
  remaining_seconds=total_seconds,
299
  running=False,
 
300
  ),
301
  )
302
 
@@ -320,63 +323,53 @@ def tick(
320
  gr.update(),
321
  gr.update(),
322
  gr.update(),
323
- gr.update(),
324
  gr.update(active=False),
325
  )
326
 
327
  new_remaining = max(0, int(end_timestamp - time.time()))
328
 
329
  if new_remaining > 0:
330
- debug_text = _build_debug_text(
331
- stage=stage,
332
- source="tick_countdown",
333
- egg_count=egg_count,
334
- doneness=doneness,
335
- running=True,
336
- total_seconds=total_seconds,
337
- remaining_seconds=new_remaining,
338
- )
339
  return (
340
  stage,
341
  new_remaining,
342
  True,
343
  _timer_html(format_mm_ss(new_remaining)),
344
- debug_text,
345
- gr.update(),
346
  gr.update(),
347
  gr.update(),
348
  gr.update(),
 
349
  gr.update(active=True),
350
  )
351
 
352
  done_message = (
353
  "### 搞掂!\n"
354
- f"你嘅 **{doneness}** 蛋已經完成。\n"
355
  "建議即刻放入冰水 30-60 秒,會更易剝殼同保持口感。"
356
  )
357
- status, debug_text, timer_html, btn_action, egg_update, doneness_update = _render_screen(
358
  stage="done",
359
- source="tick_done",
360
  egg_count=egg_count,
361
  doneness=doneness,
362
  total_seconds=total_seconds,
363
  remaining_seconds=0,
364
  running=False,
365
  status_override=done_message,
 
366
  )
367
  return (
368
  "done",
369
  0,
370
  False,
371
  timer_html,
372
- debug_text,
373
  status,
374
  btn_action,
375
  egg_update,
376
  doneness_update,
 
377
  gr.update(active=False),
378
  )
379
 
 
380
  def reset_ui():
381
  stage = "idle"
382
  total_seconds = 0
@@ -390,21 +383,21 @@ def reset_ui():
390
  gr.update(active=False),
391
  *_render_screen(
392
  stage=stage,
393
- source="reset_ui",
394
  egg_count=DEFAULT_EGG_COUNT,
395
  doneness=None,
396
  total_seconds=total_seconds,
397
  remaining_seconds=remaining_seconds,
398
  running=False,
 
399
  ),
400
  gr.update(value=DEFAULT_EGG_COUNT, interactive=True),
401
  gr.update(value=None, interactive=True),
402
  )
403
 
404
 
405
- with gr.Blocks(title="蛋計時小幫手") as demo:
406
- gr.Markdown("# 蛋計時小幫手")
407
- gr.Markdown("陪你一步一步煮出理想熟度,慢慢嚟,最緊要快。")
408
 
409
  with gr.Row():
410
  egg_count = gr.Slider(minimum=1, maximum=12, step=1, value=DEFAULT_EGG_COUNT, label="雞蛋數量")
@@ -422,20 +415,7 @@ with gr.Blocks(title="烚蛋計時小幫手") as demo:
422
 
423
  status_md = gr.Markdown(_build_status("idle", DEFAULT_EGG_COUNT, None, None))
424
  timer_display = gr.HTML(_timer_html(PLACEHOLDER_TIME))
425
- debug_text = gr.Textbox(
426
- value=_build_debug_text(
427
- stage="idle",
428
- source="init",
429
- egg_count=DEFAULT_EGG_COUNT,
430
- doneness=None,
431
- running=False,
432
- total_seconds=0,
433
- remaining_seconds=0,
434
- ),
435
- label="Debug state",
436
- interactive=False,
437
- visible=True,
438
- )
439
 
440
  with gr.Row():
441
  btn_action = gr.Button("請先選熟度", variant="primary", visible=True, interactive=False)
@@ -447,7 +427,7 @@ with gr.Blocks(title="烚蛋計時小幫手") as demo:
447
  input_component.change(
448
  fn=preview_settings,
449
  inputs=[egg_count, doneness, stage_state, running_state, remaining_state],
450
- outputs=[total_state, remaining_state, timer_display, status_md, btn_action, debug_text, egg_count, doneness],
451
  )
452
 
453
  btn_action.click(
@@ -461,11 +441,11 @@ with gr.Blocks(title="烚蛋計時小幫手") as demo:
461
  end_ts_state,
462
  ticker,
463
  status_md,
464
- debug_text,
465
  timer_display,
466
  btn_action,
467
  egg_count,
468
  doneness,
 
469
  ],
470
  )
471
 
@@ -480,11 +460,11 @@ with gr.Blocks(title="烚蛋計時小幫手") as demo:
480
  end_ts_state,
481
  ticker,
482
  status_md,
483
- debug_text,
484
  timer_display,
485
  btn_action,
486
  egg_count,
487
  doneness,
 
488
  ],
489
  )
490
 
@@ -504,11 +484,11 @@ with gr.Blocks(title="烚蛋計時小幫手") as demo:
504
  remaining_state,
505
  running_state,
506
  timer_display,
507
- debug_text,
508
  status_md,
509
  btn_action,
510
  egg_count,
511
  doneness,
 
512
  ticker,
513
  ],
514
  )
 
1
+ """Gradio Egg Timer app with a single action button and confetti on completion."""
 
 
 
2
 
3
  from __future__ import annotations
4
 
 
13
  PLACEHOLDER_TIME = "--:--"
14
 
15
  ACTION_LABELS = {
16
+ "idle": "開始流程",
17
+ "boil": "水已經滾起",
18
  "simmer": "已經轉細火",
19
+ "add_eggs": "已放,開始計時",
20
  "countdown": "計時中…",
21
  "done": "已完成",
22
  }
 
25
  def _timer_html(display_time: str) -> str:
26
  return (
27
  '<div style="text-align:center; margin: 8px 0 16px;">'
28
+ '<div style="font-size:16px; color:#6b7280; margin-bottom:6px;">建議/剩餘時間</div>'
29
  f'<div style="font-size:clamp(56px, 12vw, 110px); line-height:1; font-weight:700; letter-spacing:2px;">{display_time}</div>'
30
  "</div>"
31
  )
 
34
  def _build_status(stage: str, egg_count: int, doneness: str | None, total_seconds: int | None) -> str:
35
  if stage == "idle":
36
  if not doneness:
37
+ return "### 歡迎你!\n請先選擇之後我會一步一步。"
38
  return (
39
  "### 準備好可以開始\n"
40
  f"目前設定:**{egg_count} 隻蛋 / {doneness}**\n"
41
  f"建議時間:**{format_mm_ss(total_seconds or 0)}**"
42
  )
43
  if stage == "boil":
44
+ return "### 第 1 步:煲滾水\n等到水大滾先進入下一步,唔使急,我等你。"
45
  if stage == "simmer":
46
+ return "### 第 2 步:轉細火\n轉做微滾狀態,等水穩定啲再放蛋。"
47
  if stage == "add_eggs":
48
  return (
49
+ "### 第 3 步:輕輕放蛋\n"
50
  f"而家會用 **{egg_count} 隻蛋 / {doneness}** 去計時。\n"
51
  f"建議時間:**{format_mm_ss(total_seconds or 0)}**"
52
  )
 
58
  return "### 完成!\n雞蛋煮好啦,可以即刻過冰水 30-60 秒,口感會更靚。"
59
 
60
 
 
 
 
 
 
 
61
  def _controls_locked(stage: str) -> bool:
62
  return stage in {"countdown", "done"}
63
 
64
 
65
  def _action_button_update(stage: str, doneness: str | None) -> Any:
66
+ if stage in {"countdown", "done"}:
 
 
67
  return gr.update(value=ACTION_LABELS[stage], visible=True, interactive=False)
68
  if stage == "idle" and not doneness:
69
  return gr.update(value="請先選熟度", visible=True, interactive=False)
70
  return gr.update(value=ACTION_LABELS.get(stage, "下一步"), visible=True, interactive=True)
71
 
72
 
73
+ def _confetti_html() -> str:
74
+ nonce = str(time.time_ns())
75
+ return f"""
76
+ <div id="confetti-anchor-{nonce}"></div>
77
+ <script>
78
+ (() => {{
79
+ const existing = document.getElementById('egg-confetti-overlay');
80
+ if (existing) existing.remove();
81
+
82
+ const overlay = document.createElement('div');
83
+ overlay.id = 'egg-confetti-overlay';
84
+ overlay.style.position = 'fixed';
85
+ overlay.style.inset = '0';
86
+ overlay.style.pointerEvents = 'none';
87
+ overlay.style.overflow = 'hidden';
88
+ overlay.style.zIndex = '9999';
89
+ document.body.appendChild(overlay);
90
+
91
+ const colors = ['#f59e0b', '#ef4444', '#10b981', '#3b82f6', '#a855f7', '#f97316'];
92
+ const count = 120;
93
+
94
+ for (let i = 0; i < count; i++) {{
95
+ const piece = document.createElement('div');
96
+ piece.style.position = 'absolute';
97
+ piece.style.left = `${{Math.random() * 100}}vw`;
98
+ piece.style.top = '-20px';
99
+ piece.style.width = `${{6 + Math.random() * 8}}px`;
100
+ piece.style.height = `${{10 + Math.random() * 14}}px`;
101
+ piece.style.background = colors[Math.floor(Math.random() * colors.length)];
102
+ piece.style.opacity = '0.95';
103
+ piece.style.borderRadius = '2px';
104
+ piece.style.transform = `rotate(${{Math.random() * 360}}deg)`;
105
+
106
+ const duration = 1600 + Math.random() * 1400;
107
+ const drift = -120 + Math.random() * 240;
108
+ const spin = -720 + Math.random() * 1440;
109
+
110
+ piece.animate([
111
+ {{ transform: `translate(0, 0) rotate(0deg)`, opacity: 1 }},
112
+ {{ transform: `translate(${{drift}}px, 105vh) rotate(${{spin}}deg)`, opacity: 1 }}
113
+ ], {{
114
+ duration,
115
+ easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
116
+ fill: 'forwards'
117
+ }});
118
+
119
+ overlay.appendChild(piece);
120
+ }}
121
+
122
+ setTimeout(() => overlay.remove(), 3200);
123
+ }})();
124
+ </script>
125
+ """
126
 
127
 
128
  def _render_screen(
129
  *,
130
  stage: str,
 
131
  egg_count: int,
132
  doneness: str | None,
133
  total_seconds: int,
134
  remaining_seconds: int,
135
  running: bool,
136
  status_override: str | None = None,
137
+ celebration_html: str = "",
138
  ):
139
+ del running # Kept in signature for easier call symmetry.
140
  display_seconds = remaining_seconds if stage in {"countdown", "done"} else total_seconds
141
  display_text = PLACEHOLDER_TIME if display_seconds <= 0 else format_mm_ss(display_seconds)
142
  status = status_override or _build_status(stage, egg_count, doneness, total_seconds)
 
 
 
 
 
 
 
 
 
143
  return (
144
  status,
 
145
  _timer_html(display_text),
146
  _action_button_update(stage, doneness),
147
  gr.update(interactive=not _controls_locked(stage)),
148
  gr.update(interactive=not _controls_locked(stage)),
149
+ celebration_html,
150
  )
151
 
152
 
 
154
  total_seconds = calculate_cook_seconds(egg_count, doneness) if doneness else 0
155
 
156
  if stage == "idle":
157
+ status, timer_html, btn_action, egg_update, doneness_update, celebration = _render_screen(
158
  stage="idle",
 
159
  egg_count=egg_count,
160
  doneness=doneness,
161
  total_seconds=total_seconds,
162
  remaining_seconds=total_seconds,
163
  running=False,
164
+ celebration_html="",
165
  )
166
  return (
167
  total_seconds,
 
169
  timer_html,
170
  status,
171
  btn_action,
 
172
  egg_update,
173
  doneness_update,
174
+ celebration,
175
  )
176
 
177
+ del running, remaining_seconds
 
 
 
 
 
 
 
 
178
  return (
179
  total_seconds,
180
  gr.update(),
181
  gr.update(),
182
  gr.update(),
183
  gr.update(),
 
184
  gr.update(interactive=not _controls_locked(stage)),
185
  gr.update(interactive=not _controls_locked(stage)),
186
+ "",
187
  )
188
 
189
 
 
199
  gr.update(active=False),
200
  *_render_screen(
201
  stage="idle",
 
202
  egg_count=egg_count,
203
  doneness=doneness,
204
  total_seconds=0,
205
  remaining_seconds=0,
206
  running=False,
207
  status_override="### 未可以開始\n請先揀熟度。",
208
+ celebration_html="",
209
  ),
210
  )
211
  new_total = calculate_cook_seconds(egg_count, doneness)
 
218
  gr.update(active=False),
219
  *_render_screen(
220
  stage="boil",
 
221
  egg_count=egg_count,
222
  doneness=doneness,
223
  total_seconds=new_total,
224
  remaining_seconds=new_total,
225
  running=False,
226
+ celebration_html="",
227
  ),
228
  )
229
 
 
237
  gr.update(active=False),
238
  *_render_screen(
239
  stage="simmer",
 
240
  egg_count=egg_count,
241
  doneness=doneness,
242
  total_seconds=total_seconds,
243
  remaining_seconds=total_seconds,
244
  running=False,
245
+ celebration_html="",
246
  ),
247
  )
248
 
 
256
  gr.update(active=False),
257
  *_render_screen(
258
  stage="add_eggs",
 
259
  egg_count=egg_count,
260
  doneness=doneness,
261
  total_seconds=total_seconds,
262
  remaining_seconds=total_seconds,
263
  running=False,
264
+ celebration_html="",
265
  ),
266
  )
267
 
 
276
  gr.update(active=True),
277
  *_render_screen(
278
  stage="countdown",
 
279
  egg_count=egg_count,
280
  doneness=doneness,
281
  total_seconds=total_seconds,
282
  remaining_seconds=total_seconds,
283
  running=True,
284
+ celebration_html="",
285
  ),
286
  )
287
 
 
294
  gr.update(active=False),
295
  *_render_screen(
296
  stage=stage,
 
297
  egg_count=egg_count,
298
  doneness=doneness,
299
  total_seconds=total_seconds,
300
  remaining_seconds=total_seconds,
301
  running=False,
302
+ celebration_html="",
303
  ),
304
  )
305
 
 
323
  gr.update(),
324
  gr.update(),
325
  gr.update(),
 
326
  gr.update(active=False),
327
  )
328
 
329
  new_remaining = max(0, int(end_timestamp - time.time()))
330
 
331
  if new_remaining > 0:
 
 
 
 
 
 
 
 
 
332
  return (
333
  stage,
334
  new_remaining,
335
  True,
336
  _timer_html(format_mm_ss(new_remaining)),
 
 
337
  gr.update(),
338
  gr.update(),
339
  gr.update(),
340
+ "",
341
  gr.update(active=True),
342
  )
343
 
344
  done_message = (
345
  "### 搞掂!\n"
346
+ f"你嘅 **{doneness}** 蛋已經完成。\n"
347
  "建議即刻放入冰水 30-60 秒,會更易剝殼同保持口感。"
348
  )
349
+ status, timer_html, btn_action, egg_update, doneness_update, celebration = _render_screen(
350
  stage="done",
 
351
  egg_count=egg_count,
352
  doneness=doneness,
353
  total_seconds=total_seconds,
354
  remaining_seconds=0,
355
  running=False,
356
  status_override=done_message,
357
+ celebration_html=_confetti_html(),
358
  )
359
  return (
360
  "done",
361
  0,
362
  False,
363
  timer_html,
 
364
  status,
365
  btn_action,
366
  egg_update,
367
  doneness_update,
368
+ celebration,
369
  gr.update(active=False),
370
  )
371
 
372
+
373
  def reset_ui():
374
  stage = "idle"
375
  total_seconds = 0
 
383
  gr.update(active=False),
384
  *_render_screen(
385
  stage=stage,
 
386
  egg_count=DEFAULT_EGG_COUNT,
387
  doneness=None,
388
  total_seconds=total_seconds,
389
  remaining_seconds=remaining_seconds,
390
  running=False,
391
+ celebration_html="",
392
  ),
393
  gr.update(value=DEFAULT_EGG_COUNT, interactive=True),
394
  gr.update(value=None, interactive=True),
395
  )
396
 
397
 
398
+ with gr.Blocks(title="蛋計時小幫手") as demo:
399
+ gr.Markdown("# 蛋計時小幫手")
400
+ gr.Markdown("陪你一步一步煮出理想熟度,慢慢嚟就得。")
401
 
402
  with gr.Row():
403
  egg_count = gr.Slider(minimum=1, maximum=12, step=1, value=DEFAULT_EGG_COUNT, label="雞蛋數量")
 
415
 
416
  status_md = gr.Markdown(_build_status("idle", DEFAULT_EGG_COUNT, None, None))
417
  timer_display = gr.HTML(_timer_html(PLACEHOLDER_TIME))
418
+ celebration_html = gr.HTML("")
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
  with gr.Row():
421
  btn_action = gr.Button("請先選熟度", variant="primary", visible=True, interactive=False)
 
427
  input_component.change(
428
  fn=preview_settings,
429
  inputs=[egg_count, doneness, stage_state, running_state, remaining_state],
430
+ outputs=[total_state, remaining_state, timer_display, status_md, btn_action, egg_count, doneness, celebration_html],
431
  )
432
 
433
  btn_action.click(
 
441
  end_ts_state,
442
  ticker,
443
  status_md,
 
444
  timer_display,
445
  btn_action,
446
  egg_count,
447
  doneness,
448
+ celebration_html,
449
  ],
450
  )
451
 
 
460
  end_ts_state,
461
  ticker,
462
  status_md,
 
463
  timer_display,
464
  btn_action,
465
  egg_count,
466
  doneness,
467
+ celebration_html,
468
  ],
469
  )
470
 
 
484
  remaining_state,
485
  running_state,
486
  timer_display,
 
487
  status_md,
488
  btn_action,
489
  egg_count,
490
  doneness,
491
+ celebration_html,
492
  ticker,
493
  ],
494
  )