Takosaga commited on
Commit
ca5d5e7
Β·
1 Parent(s): b3e31cb

feat: add APKG export button and event handlers to UI

Browse files
Files changed (1) hide show
  1. frontend/ui/widgets.py +75 -26
frontend/ui/widgets.py CHANGED
@@ -80,10 +80,11 @@ def _enable_phase2() -> tuple:
80
 
81
  Both Audio and Images toggles default to ON after Phase 1. Voice dropdown becomes interactive β€” it becomes visible when audio toggle is turned ON (via audio_toggle.change).
82
  Explicitly sets value=True to prevent Gradio from resetting checkbox state on re-render.
83
- Export button remains VISIBLE but DISABLED until Phase 2 completes (when _current_cards is populated).
84
 
85
  Returns:
86
- Tuple of (images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, export_btn, phase_css) updates.
 
87
  """
88
  import gradio as gr
89
  return (
@@ -91,7 +92,10 @@ def _enable_phase2() -> tuple:
91
  gr.Checkbox(interactive=True, value=True), # audio_toggle
92
  gr.Button(interactive=True), # generate_cards_btn
93
  gr.Dropdown(interactive=True), # voice_dropdown
94
- gr.Button(visible=True, interactive=False), # export_btn (disabled until Phase 2)
 
 
 
95
  "", # phase_css
96
  )
97
 
@@ -105,11 +109,11 @@ def _reset_to_idle() -> tuple:
105
  Re-applies disabled CSS to phase-2 controls.
106
  Keeps voice dropdown visible but disabled (it becomes interactive when audio is toggled ON after Phase 1).
107
  Explicitly sets value=False to prevent Gradio from resetting checkbox state on re-render.
108
- Keeps export button visible but disabled until Phase 2 completes.
109
 
110
  Returns:
111
  Tuple of (generate_text_btn, images_toggle, audio_toggle, generate_cards_btn,
112
- voice_dropdown, phase_css, export_btn, export_file) updates.
113
  """
114
  import gradio as gr
115
  return (
@@ -119,8 +123,10 @@ def _reset_to_idle() -> tuple:
119
  gr.Button(visible=True, interactive=False, variant="secondary"), # generate_cards_btn
120
  gr.Dropdown(visible=True, interactive=False), # voice_dropdown
121
  """<style id="phase-css">#toggle-images, #toggle-audio { opacity: 0.45; pointer-events: none; cursor: not-allowed; } #language-dropdown, #voice-dropdown { opacity: 0.45; pointer-events: none; cursor: not-allowed; } #generate-cards-btn { opacity: 0.45; pointer-events: none; cursor: not-allowed; }</style>""", # phase_css
122
- gr.Button(visible=True, interactive=False), # export_btn (always visible, disabled until Phase 2)
 
123
  gr.File(value=None, visible=False), # export_file
 
124
  )
125
 
126
 
@@ -149,15 +155,16 @@ def _restore_generate_cards_button() -> tuple:
149
  """After a parameter change, restore the Generate Cards button so user can regenerate media.
150
 
151
  Called as a chained .then() handler after primary event handlers.
152
- Unhides the button and makes it interactive. Export button stays disabled.
153
 
154
  Returns:
155
- Tuple of (generate_cards_btn, export_btn) Gradio updates.
156
  """
157
  import gradio as gr
158
  return (
159
  gr.Button(visible=True, interactive=True), # generate_cards_btn
160
- gr.Button(visible=True, interactive=False), # export_btn (disabled until Phase 2)
 
161
  )
162
 
163
 
@@ -169,13 +176,14 @@ def _restore_generate_cards_button_only() -> tuple:
169
  Does NOT restore the Generate Text button (only appears on scenario/CEFR/batch reset).
170
 
171
  Returns:
172
- Tuple of (generate_text_btn, generate_cards_btn, export_btn) Gradio updates.
173
  """
174
  import gradio as gr
175
  return (
176
  gr.Button(visible=False), # generate_text_btn (stays hidden)
177
  gr.Button(visible=True, interactive=True), # generate_cards_btn (restore)
178
- gr.Button(visible=True, interactive=False), # export_btn (disabled until Phase 2)
 
179
  )
180
 
181
 
@@ -269,17 +277,27 @@ def build_ui() -> "gr.Blocks":
269
 
270
  progress_html = gr.HTML(label="Progress")
271
 
272
- # Export area: regular button to trigger export + File component for download
273
  with gr.Row():
274
- export_btn = gr.Button(
275
  "πŸ“₯ Export CSV + Media",
276
  variant="primary",
277
  visible=True,
278
  interactive=False,
279
  elem_id="export-btn",
280
  )
 
 
 
 
 
 
 
281
  export_file = gr.File(
282
- label="Download", file_types=[".zip"], visible=False
 
 
 
283
  )
284
 
285
  gr.Column(scale=1)
@@ -325,7 +343,7 @@ def build_ui() -> "gr.Blocks":
325
  return (
326
  gr.Button(visible=False), # generate_text_btn
327
  gr.Button(visible=False), # generate_cards_btn
328
- gr.Button(visible=True, interactive=True), # export_btn (enable)
329
  gr.File(value=None, visible=False), # export_file (cleared)
330
  )
331
 
@@ -336,7 +354,7 @@ def build_ui() -> "gr.Blocks":
336
  ).then(
337
  fn=_enable_phase2,
338
  inputs=[],
339
- outputs=[images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, export_btn, phase_css],
340
  )
341
 
342
  # When audio toggle changes: show/hide voice dropdown and manage disabled CSS
@@ -356,7 +374,7 @@ def build_ui() -> "gr.Blocks":
356
  ).then(
357
  fn=_restore_generate_cards_button,
358
  inputs=[],
359
- outputs=[generate_cards_btn, export_btn],
360
  )
361
 
362
  generate_cards_btn.click(
@@ -366,32 +384,32 @@ def build_ui() -> "gr.Blocks":
366
  ).then(
367
  fn=_on_media_generation_complete,
368
  inputs=[],
369
- outputs=[generate_text_btn, generate_cards_btn, export_btn, export_file],
370
  )
371
 
372
  # Reset toggles and both buttons when user changes any input parameter
373
- scenario_input.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_btn, export_file])
374
- cefr_dropdown.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_btn, export_file])
375
- batch_slider.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_btn, export_file])
376
  # Language change β€” only restore Generate Cards button (toggles stay interactive)
377
  language_dropdown.change(
378
  fn=_restore_generate_cards_button_only,
379
  inputs=[],
380
- outputs=[generate_text_btn, generate_cards_btn, export_btn],
381
  )
382
 
383
  # Image toggle change β€” restore Generate Cards button so user can regenerate with/without images
384
  images_toggle.change(
385
  fn=lambda: (gr.Button(visible=True, interactive=True), gr.Button(visible=True, interactive=False)),
386
  inputs=[],
387
- outputs=[generate_cards_btn, export_btn],
388
  )
389
 
390
  # Voice dropdown change β€” restore Generate Cards button so user can regenerate with different voice
391
  voice_dropdown.change(
392
- fn=lambda: (gr.Button(visible=True, interactive=True), gr.Button(visible=True, interactive=False)),
393
  inputs=[],
394
- outputs=[generate_cards_btn, export_btn],
395
  )
396
 
397
  # ─── Export Event Wiring ──────────────────────────────────────
@@ -420,10 +438,41 @@ def build_ui() -> "gr.Blocks":
420
  return generate_progress_html(0, f"\u26a0\ufe0f Export failed: {e}"), None, gr.File(visible=False)
421
 
422
  # Export button click β€” generates zip and shows it in gr.File for download
423
- export_btn.click(
424
  fn=_handle_export_csv_event,
425
  inputs=[scenario_input, cefr_dropdown, language_dropdown],
426
  outputs=[progress_html, export_file, export_file],
427
  )
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  return demo
 
80
 
81
  Both Audio and Images toggles default to ON after Phase 1. Voice dropdown becomes interactive β€” it becomes visible when audio toggle is turned ON (via audio_toggle.change).
82
  Explicitly sets value=True to prevent Gradio from resetting checkbox state on re-render.
83
+ Export buttons remain VISIBLE but DISABLED until Phase 2 completes (when _current_cards is populated).
84
 
85
  Returns:
86
+ Tuple of (images_toggle, audio_toggle, generate_cards_btn, voice_dropdown,
87
+ export_csv_btn, export_apkg_btn, export_file, export_apkg_file, phase_css) updates.
88
  """
89
  import gradio as gr
90
  return (
 
92
  gr.Checkbox(interactive=True, value=True), # audio_toggle
93
  gr.Button(interactive=True), # generate_cards_btn
94
  gr.Dropdown(interactive=True), # voice_dropdown
95
+ gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2)
96
+ gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2)
97
+ gr.File(value=None, visible=False), # export_file
98
+ gr.File(value=None, visible=False), # export_apkg_file
99
  "", # phase_css
100
  )
101
 
 
109
  Re-applies disabled CSS to phase-2 controls.
110
  Keeps voice dropdown visible but disabled (it becomes interactive when audio is toggled ON after Phase 1).
111
  Explicitly sets value=False to prevent Gradio from resetting checkbox state on re-render.
112
+ Keeps export buttons visible but disabled until Phase 2 completes.
113
 
114
  Returns:
115
  Tuple of (generate_text_btn, images_toggle, audio_toggle, generate_cards_btn,
116
+ voice_dropdown, phase_css, export_csv_btn, export_apkg_btn, export_file, export_apkg_file) updates.
117
  """
118
  import gradio as gr
119
  return (
 
123
  gr.Button(visible=True, interactive=False, variant="secondary"), # generate_cards_btn
124
  gr.Dropdown(visible=True, interactive=False), # voice_dropdown
125
  """<style id="phase-css">#toggle-images, #toggle-audio { opacity: 0.45; pointer-events: none; cursor: not-allowed; } #language-dropdown, #voice-dropdown { opacity: 0.45; pointer-events: none; cursor: not-allowed; } #generate-cards-btn { opacity: 0.45; pointer-events: none; cursor: not-allowed; }</style>""", # phase_css
126
+ gr.Button(visible=True, interactive=False), # export_csv_btn (always visible, disabled until Phase 2)
127
+ gr.Button(visible=True, interactive=False), # export_apkg_btn (always visible, disabled until Phase 2)
128
  gr.File(value=None, visible=False), # export_file
129
+ gr.File(value=None, visible=False), # export_apkg_file
130
  )
131
 
132
 
 
155
  """After a parameter change, restore the Generate Cards button so user can regenerate media.
156
 
157
  Called as a chained .then() handler after primary event handlers.
158
+ Unhides the button and makes it interactive. Export buttons stay disabled.
159
 
160
  Returns:
161
+ Tuple of (generate_cards_btn, export_csv_btn, export_apkg_btn) Gradio updates.
162
  """
163
  import gradio as gr
164
  return (
165
  gr.Button(visible=True, interactive=True), # generate_cards_btn
166
+ gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2)
167
+ gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2)
168
  )
169
 
170
 
 
176
  Does NOT restore the Generate Text button (only appears on scenario/CEFR/batch reset).
177
 
178
  Returns:
179
+ Tuple of (generate_text_btn, generate_cards_btn, export_csv_btn, export_apkg_btn) Gradio updates.
180
  """
181
  import gradio as gr
182
  return (
183
  gr.Button(visible=False), # generate_text_btn (stays hidden)
184
  gr.Button(visible=True, interactive=True), # generate_cards_btn (restore)
185
+ gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2)
186
+ gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2)
187
  )
188
 
189
 
 
277
 
278
  progress_html = gr.HTML(label="Progress")
279
 
280
+ # Export area: CSV button + File download, APKG button + File download
281
  with gr.Row():
282
+ export_csv_btn = gr.Button(
283
  "πŸ“₯ Export CSV + Media",
284
  variant="primary",
285
  visible=True,
286
  interactive=False,
287
  elem_id="export-btn",
288
  )
289
+ export_apkg_btn = gr.Button(
290
+ "πŸ“₯ Export Anki Cards",
291
+ variant="primary",
292
+ visible=True,
293
+ interactive=False,
294
+ elem_id="export-apkg-btn",
295
+ )
296
  export_file = gr.File(
297
+ label="Download CSV", file_types=[".zip"], visible=False
298
+ )
299
+ export_apkg_file = gr.File(
300
+ label="Download Anki Cards", file_types=[".apkg"], visible=False
301
  )
302
 
303
  gr.Column(scale=1)
 
343
  return (
344
  gr.Button(visible=False), # generate_text_btn
345
  gr.Button(visible=False), # generate_cards_btn
346
+ gr.Button(visible=True, interactive=True), # export_csv_btn (enable)
347
  gr.File(value=None, visible=False), # export_file (cleared)
348
  )
349
 
 
354
  ).then(
355
  fn=_enable_phase2,
356
  inputs=[],
357
+ outputs=[images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, export_csv_btn, phase_css],
358
  )
359
 
360
  # When audio toggle changes: show/hide voice dropdown and manage disabled CSS
 
374
  ).then(
375
  fn=_restore_generate_cards_button,
376
  inputs=[],
377
+ outputs=[generate_cards_btn, export_csv_btn],
378
  )
379
 
380
  generate_cards_btn.click(
 
384
  ).then(
385
  fn=_on_media_generation_complete,
386
  inputs=[],
387
+ outputs=[generate_text_btn, generate_cards_btn, export_csv_btn, export_file],
388
  )
389
 
390
  # Reset toggles and both buttons when user changes any input parameter
391
+ scenario_input.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_csv_btn, export_file])
392
+ cefr_dropdown.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_csv_btn, export_file])
393
+ batch_slider.change(_reset_to_idle, inputs=[], outputs=[generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_csv_btn, export_file])
394
  # Language change β€” only restore Generate Cards button (toggles stay interactive)
395
  language_dropdown.change(
396
  fn=_restore_generate_cards_button_only,
397
  inputs=[],
398
+ outputs=[generate_text_btn, generate_cards_btn, export_csv_btn],
399
  )
400
 
401
  # Image toggle change β€” restore Generate Cards button so user can regenerate with/without images
402
  images_toggle.change(
403
  fn=lambda: (gr.Button(visible=True, interactive=True), gr.Button(visible=True, interactive=False)),
404
  inputs=[],
405
+ outputs=[generate_cards_btn, export_csv_btn],
406
  )
407
 
408
  # Voice dropdown change β€” restore Generate Cards button so user can regenerate with different voice
409
  voice_dropdown.change(
410
+ fn=lambda: (gr.Button(visible=True, interactive=True), gr.Button(visible=True, interactive=False), gr.Button(visible=True, interactive=False)),
411
  inputs=[],
412
+ outputs=[generate_cards_btn, export_csv_btn, export_apkg_btn],
413
  )
414
 
415
  # ─── Export Event Wiring ──────────────────────────────────────
 
438
  return generate_progress_html(0, f"\u26a0\ufe0f Export failed: {e}"), None, gr.File(visible=False)
439
 
440
  # Export button click β€” generates zip and shows it in gr.File for download
441
+ export_csv_btn.click(
442
  fn=_handle_export_csv_event,
443
  inputs=[scenario_input, cefr_dropdown, language_dropdown],
444
  outputs=[progress_html, export_file, export_file],
445
  )
446
 
447
+ # ─── APKG Export Event Wiring ───────────────────────────────
448
+
449
+ def _handle_export_apkg_event(scenario: str, cefr_level: str, target_language: str):
450
+ """Export current cards as Anki package (.apkg).
451
+
452
+ Sets the generated .apkg file path as the value of export_apkg_file component,
453
+ which Gradio renders as a downloadable file link.
454
+ """
455
+ from frontend.ui.cards import generate_progress_html
456
+
457
+ if not _app_module._current_cards:
458
+ return generate_progress_html(0, "\u26a0\ufe0f No cards to export."), None, gr.File(visible=False)
459
+
460
+ try:
461
+ apkg_path = _app_module._handle_export_apkg(scenario, cefr_level, target_language)
462
+ if apkg_path is None:
463
+ return generate_progress_html(0, "\u26a0\ufe0f Export failed."), None, gr.File(visible=False)
464
+ # Show the file for download β€” gr.File component renders it as a clickable link
465
+ return generate_progress_html(100, "Export complete! Click the file below to download."), apkg_path, gr.File(visible=True)
466
+ except Exception as e:
467
+ logger = logging.getLogger(__name__)
468
+ logger.error("APKG export failed: %s", e, exc_info=True)
469
+ return generate_progress_html(0, f"\u26a0\ufe0f Export failed: {e}"), None, gr.File(visible=False)
470
+
471
+ # APKG Export button click β€” generates .apkg and shows it in gr.File for download
472
+ export_apkg_btn.click(
473
+ fn=_handle_export_apkg_event,
474
+ inputs=[scenario_input, cefr_dropdown, language_dropdown],
475
+ outputs=[progress_html, export_apkg_file, export_apkg_file],
476
+ )
477
+
478
  return demo