# EuropaLex Frontend UI Components # Custom styled Gradio widget wrappers + full UI layout builder import logging def create_toggle(label: str, value: bool = True, elem_id: str = "") -> "gr.Checkbox": """Create a styled toggle checkbox for media options. Args: label: Display label with emoji (e.g., '🖼️ Images'). value: Default checked state. elem_id: Optional Gradio element ID. Returns: Configured gr.Checkbox instance. """ import gradio as gr return gr.Checkbox( label=label, value=value, elem_id=elem_id if elem_id else "toggle-" + label.lower().replace(" ", "-").replace("🖼️", "img").replace("🔊", "audio"), ) def create_voice_dropdown( default_voice: str = "female, young adult", ) -> "gr.Dropdown": """Create a voice selection dropdown for TTS audio generation. Six presets mapped to OmniVoice instruct strings (gender × age). Ordered by gender first, then age from oldest to youngest. Visible by default; disabled via CSS until Audio toggle is ON. Args: default_voice: Default OmniVoice instruct string. Returns: Configured gr.Dropdown with 6 voice presets. """ import gradio as gr choices = [ "Female — Middle-Aged", "Female — Young Adult", "Female — Teenager", "Male — Middle-Aged", "Male — Young Adult", "Male — Teenager", ] return gr.Dropdown( label="Voice", choices=choices, value=default_voice, elem_id="voice-dropdown", allow_custom_value=True, visible=True, ) # ─── Voice Mapping ──────────────────────────────────────────────── # Mapping from voice dropdown display labels to OmniVoice instruct strings _VOICE_MAP: dict[str, str] = { "Female — Middle-Aged": "female, middle-aged", "Female — Young Adult": "female, young adult", "Female — Teenager": "female, teenager", "Male — Middle-Aged": "male, middle-aged", "Male — Young Adult": "male, young adult", "Male — Teenager": "male, teenager", } # ─── UI State Helpers ───────────────────────────────────────────── def _enable_phase2() -> tuple: """After text generation, enable toggles, dropdowns and Generate Cards button by removing disabled CSS. 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). Explicitly sets value=True to prevent Gradio from resetting checkbox state on re-render. Export buttons remain VISIBLE but DISABLED until Phase 2 completes (when _current_cards is populated). Returns: Tuple of (images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, export_csv_btn, export_apkg_btn, export_file, export_apkg_file, phase_css) updates. """ import gradio as gr return ( gr.Checkbox(interactive=True, value=True), # images_toggle gr.Checkbox(interactive=True, value=True), # audio_toggle gr.Button(interactive=True), # generate_cards_btn gr.Dropdown(interactive=True), # voice_dropdown gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2) gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2) gr.File(value=None, visible=False), # export_file gr.File(value=None, visible=False), # export_apkg_file "", # phase_css ) def _reset_to_idle() -> tuple: """Reset UI to idle state when user changes parameters. Only resets toggle/button interactivity — keeps cards visible so the user can regenerate without losing their work. Also restores both buttons visibility (hidden by Phase 2). Re-applies disabled CSS to phase-2 controls. Keeps voice dropdown visible but disabled (it becomes interactive when audio is toggled ON after Phase 1). Explicitly sets value=False to prevent Gradio from resetting checkbox state on re-render. Keeps export buttons visible but disabled until Phase 2 completes. Returns: Tuple of (generate_text_btn, images_toggle, audio_toggle, generate_cards_btn, voice_dropdown, phase_css, export_csv_btn, export_apkg_btn, export_file, export_apkg_file) updates. """ import gradio as gr return ( gr.Button(visible=True, interactive=True), # generate_text_btn gr.Checkbox(interactive=False, value=False), # images_toggle gr.Checkbox(interactive=False, value=False), # audio_toggle gr.Button(visible=True, interactive=False, variant="secondary"), # generate_cards_btn gr.Dropdown(visible=True, interactive=False), # voice_dropdown """""", # phase_css gr.Button(visible=True, interactive=False), # export_csv_btn (always visible, disabled until Phase 2) gr.Button(visible=True, interactive=False), # export_apkg_btn (always visible, disabled until Phase 2) gr.File(value=None, visible=False), # export_file gr.File(value=None, visible=False), # export_apkg_file ) def _enable_language_dropdown_on_audio(is_checked: bool) -> tuple: """Update CSS and voice dropdown interactivity when audio toggle changes. Voice dropdown is always visible — toggling audio ON makes it interactive, toggling OFF disables it (but keeps it visible). Args: is_checked: Whether the audio toggle is currently checked. Returns: Tuple of (voice_dropdown_update, phase_css_html) updates. """ import gradio as gr if is_checked: # Audio ON: remove disabled CSS, make voice dropdown interactive return gr.Dropdown(interactive=True), "" else: # Audio OFF: apply disabled CSS to voice dropdown only (not generate button) return gr.Dropdown(interactive=False), """""" def _restore_generate_cards_button() -> tuple: """After a parameter change, restore the Generate Cards button so user can regenerate media. Called as a chained .then() handler after primary event handlers. Unhides the button and makes it interactive. Export buttons stay disabled. Returns: Tuple of (generate_cards_btn, export_csv_btn, export_apkg_btn) Gradio updates. """ import gradio as gr return ( gr.Button(visible=True, interactive=True), # generate_cards_btn gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2) gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2) ) def _restore_generate_cards_button_only() -> tuple: """Restore only the Generate Cards button visibility without disabling toggles. Used for language changes after Phase 2 has completed. Unlike _reset_to_idle(), this does NOT re-apply disabled CSS to toggles — they remain fully interactive. Does NOT restore the Generate Text button (only appears on scenario/CEFR/batch reset). Returns: Tuple of (generate_text_btn, generate_cards_btn, export_csv_btn, export_apkg_btn) Gradio updates. """ import gradio as gr return ( gr.Button(visible=False), # generate_text_btn (stays hidden) gr.Button(visible=True, interactive=True), # generate_cards_btn (restore) gr.Button(visible=True, interactive=False), # export_csv_btn (disabled until Phase 2) gr.Button(visible=True, interactive=False), # export_apkg_btn (disabled until Phase 2) ) # ─── UI Layout Builder ─────────────────────────────────────────── def build_ui() -> "gr.Blocks": """Construct the entire Gradio Blocks layout and return it. This function replaces the inline `with gr.Blocks() as demo:` block in app.py. It creates all widgets, assembles the layout, wires event handlers, and returns the configured `demo` object ready for `.launch()`. IMPORTANT: Imports from app.py happen INSIDE this function (not at module level) to avoid circular imports. app.py does NOT import from widgets.py at module level (only inside __main__), so the import chain is safe: import widgets → build_ui() body runs → imports from app → app has no widget deps. _phase1_texts is accessed via the _app_module reference — shared mutable state between Phase 1 (generate_text_async sets it) and Phase 2 (_handle_media_generation_v2 reads it). We use module-level access (not `from app import _phase1_texts`) because rebinding `_phase1_texts = [...]` in app.py would not update a `from-import` binding. Returns: Configured gr.Blocks instance with all events wired. """ import gradio as gr # Import business logic handlers INSIDE build_ui to avoid circular import from app import generate_text_async, generate_media_async import app as _app_module # access _phase1_texts via module ref (rebinding in app.py won't break the reference) from frontend.ui.cards import generate_cards_html, generate_progress_html # ─── CSS Block ──────────────────────────────────────────────── with gr.Blocks() as demo: gr.HTML("""
""") with gr.Row(): gr.Column(scale=1) with gr.Column(scale=3, elem_id="app-card"): gr.HTML('AI-powered flashcard generator — translate text into European languages, generate audio & images, and export as CSV
') with gr.Row(): scenario_input = gr.Textbox( label="Scenario or Topic", placeholder="e.g., ordering coffee, family members, weather", lines=1, elem_id="scenario-input", ) cefr_dropdown = gr.Dropdown( label="CEFR Level", choices=["A1", "A2", "B1", "B2"], value="A2", elem_id="cefr-dropdown", ) batch_slider = gr.Slider( minimum=1, maximum=10, value=3, step=1, label="Number of Cards", elem_id="batch-slider", ) # Phase 1 button: Generate Text generate_text_btn = gr.Button("Generate Text", elem_id="generate-btn") # Card display area (below Generate Text) card_output = gr.HTML(label="Generated Cards") # Phase 2 controls: language, toggles + button (below cards) with gr.Row(): language_dropdown = gr.Dropdown( label="Target Language", choices=[ "Bulgarian", "Croatian", "Czech", "Danish", "Dutch", "Estonian", "Finnish", "French", "German", "Greek", "Hungarian", "Irish", "Italian", "Latvian", "Lithuanian", "Maltese", "Polish", "Portuguese", "Romanian", "Slovak", "Slovenian", "Spanish", "Swedish", ], value="Latvian", elem_id="language-dropdown", ) with gr.Row(): audio_toggle = create_toggle("🔊 Audio", value=True, elem_id="toggle-audio") images_toggle = create_toggle("🖼️ Images", value=True, elem_id="toggle-images") voice_dropdown = create_voice_dropdown() # visible but disabled via CSS until Phase 2 + audio ON generate_cards_btn = gr.Button("Generate Cards", elem_id="generate-cards-btn", variant="secondary") # Dynamic CSS block — toggled to disable phase-2 controls until text generation completes phase_css = gr.HTML("""""") progress_html = gr.HTML(label="Progress") # Export area: CSV button + File download, APKG button + File download with gr.Row(): export_csv_btn = gr.Button( "📥 Export CSV + Media", variant="primary", visible=True, interactive=False, elem_id="export-btn", ) export_apkg_btn = gr.Button( "📥 Export Anki Cards", variant="primary", visible=True, interactive=False, elem_id="export-apkg-btn", ) export_file = gr.File( label="Download CSV", file_types=[".zip"], visible=False ) export_apkg_file = gr.File( label="Download Anki Cards", file_types=[".apkg"], visible=False ) gr.Column(scale=1) # ─── Event Wiring ────────────────────────────────────────────── def _handle_text_generation(scenario: str, cefr_level: str, batch_size: int): """Wrapper for generate_text_async that handles empty scenario.""" if not scenario.strip(): yield generate_progress_html(0, "⚠️ Please enter a scenario or topic."), '