refactor: Move AI & TTS settings into collapsible panel
- Added "AI & TTS" toggle button beneath Translation section - AI Processing and Text-to-Speech sections now in collapsible panel - Panel hidden by default for cleaner interface - Click button to expand/collapse the panel
+225 −206 · 1 file changed
-
src/whispering_ui/components/sidebar.py+225 −206
@@ -330,215 +330,234 @@ def create_sidebar(state: AppState, bridge: ProcessingBridge, output_container=N ui.separator().classes('my-1') - # === AI SECTION === - with ui.row().classes('items-center justify-between w-full'): - ui.label('AI Processing').classes('font-bold text-sm') + # === AI & TTS COLLAPSIBLE PANEL === + # Toggle button for AI & TTS settings + ai_tts_panel_visible = [False] # Track panel visibility + + def toggle_ai_tts_panel(): + ai_tts_panel_visible[0] = not ai_tts_panel_visible[0] + ai_tts_panel.set_visibility(ai_tts_panel_visible[0]) + ai_tts_toggle_btn.text = 'AI & TTS ▲' if ai_tts_panel_visible[0] else 'AI & TTS ▼' + + ai_tts_toggle_btn = ui.button( + 'AI & TTS ▼', + on_click=toggle_ai_tts_panel + ).classes('w-full').props('dense flat') + + # Collapsible panel container + ai_tts_panel = ui.column().classes('w-full gap-1 pl-2 border-l-2 border-gray-600') + ai_tts_panel.set_visibility(False) + + with ai_tts_panel: + # === AI SECTION === + with ui.row().classes('items-center justify-between w-full'): + ui.label('AI Processing').classes('font-bold text-sm') + if state.ai_available: + ui.button(icon='help_outline', on_click=lambda: show_help_dialog('ai')).props('flat dense round size=sm') + + # Enable AI checkbox + ai_cb = ui.checkbox('Enable AI', value=state.ai_enabled).props('dense') + ai_section = ui.column().classes('w-full gap-1') + ai_controls = [] + ai_process_btn = None + ai_trigger_select = None + ai_interval_select = None + ai_words_num = None + + def register_ai(control): + ai_controls.append(control) + return control + + # AI controls if state.ai_available: - ui.button(icon='help_outline', on_click=lambda: show_help_dialog('ai')).props('flat dense round size=sm') - - # Enable AI checkbox - ai_cb = ui.checkbox('Enable AI', value=state.ai_enabled).props('dense') - ai_section = ui.column().classes('w-full gap-1') - ai_controls = [] - ai_process_btn = None - ai_trigger_select = None - ai_interval_select = None - ai_words_num = None - - def register_ai(control): - ai_controls.append(control) - return control - - # AI controls - if state.ai_available: - with ai_section: - try: - from ai_config import load_ai_config - ai_config = load_ai_config() - if ai_config: - # Task selection - personas = ai_config.get_personas() - persona_names = [p['name'] for p in personas] - - with ui.row().classes('items-center w-full gap-1'): - ui.label('Task:').classes('text-xs w-12') - task_select = register_ai(ui.select( - options=persona_names, - value=persona_names[min(state.ai_persona_index, len(persona_names)-1)] - ).classes('flex-grow').props('dense')) - task_select.on_value_change(lambda e: setattr(state, 'ai_persona_index', - persona_names.index(e.value) if e.value in persona_names else 0)) - - # Translate checkboxes - compact - with ui.row().classes('items-center w-full gap-2'): - ai_trans_cb = register_ai(ui.checkbox('Translate', value=state.ai_translate).props('dense')) - ai_trans_cb.on_value_change(lambda e: setattr(state, 'ai_translate', e.value)) - - ai_trans_only_cb = register_ai(ui.checkbox('Only (1:1)', value=state.ai_translate_only).props('dense')) - ai_trans_only_cb.on_value_change(lambda e: setattr(state, 'ai_translate_only', e.value)) - - # Model selection - models = ai_config.get_models() - model_names = [m['name'] for m in models] - - with ui.row().classes('items-center w-full gap-1'): - ui.label('Model:').classes('text-xs w-12') - ai_model_combo = register_ai(ui.select( - options=model_names, - value=model_names[min(state.ai_model_index, len(model_names)-1)] - ).classes('flex-grow').props('dense')) - ai_model_combo.on_value_change(lambda e: setattr(state, 'ai_model_index', - model_names.index(e.value) if e.value in model_names else 0)) - - # Trigger controls - compact layout - ai_manual_cb = register_ai(ui.checkbox('Manual mode', value=state.ai_manual_mode).props('dense')) - - ai_process_btn = register_ai(ui.button('⚡ Process Now', on_click=lambda: bridge.manual_ai_trigger()).classes('w-full').props('dense')) - ai_process_btn.set_enabled(state.ai_manual_mode) - - # Trigger mode and settings - with ui.row().classes('items-center w-full gap-1'): - ui.label('Trigger:').classes('text-xs') - ai_trigger_select = register_ai(ui.select( - options=["Time", "Words"], - value=state.ai_trigger_mode.capitalize() - ).classes('w-16').props('dense')) - ai_trigger_select.set_enabled(not state.ai_manual_mode) - - # Interval control - interval_labels = ["5s", "10s", "15s", "20s", "25s", "30s", "45s", "1m", "1.5m", "2m"] - interval_values = [5, 10, 15, 20, 25, 30, 45, 60, 90, 120] - interval_map = dict(zip(interval_labels, interval_values)) - - current_label = "20s" - for lbl, val in interval_map.items(): - if val == state.ai_process_interval: - current_label = lbl - break - - ui.label('Int:').classes('text-xs') - ai_interval_select = register_ai(ui.select( - options=interval_labels, - value=current_label - ).classes('w-14').props('dense')) - - def on_interval_change(e): - state.ai_process_interval = interval_map.get(e.value, 20) - - ai_interval_select.on_value_change(on_interval_change) - ai_interval_select.set_enabled(not state.ai_manual_mode) - ai_interval_select.set_visibility(state.ai_trigger_mode == "time") - - ui.label('W:').classes('text-xs') - ai_words_num = register_ai(ui.number(value=state.ai_process_words, min=50, max=500, step=50).classes('w-16').props('dense')) - ai_words_num.on_value_change(lambda e: setattr(state, 'ai_process_words', int(e.value or 150))) - ai_words_num.set_enabled(not state.ai_manual_mode) - ai_words_num.set_visibility(state.ai_trigger_mode == "words") - - # Wire up event handlers - ai_manual_cb.on_value_change(lambda e: _on_manual_mode_changed( - state, e.value, ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num)) - - ai_trigger_select.on_value_change(lambda e: _on_trigger_changed( - state, e.value, ai_interval_select, ai_words_num)) - - except Exception as e: - print(f"Error loading AI config: {e}") - - def on_ai_toggle(e): - state.ai_enabled = e.value - active = e.value and state.ai_available - _set_section_visual_state(ai_section, active) - _set_controls_enabled(ai_controls, active) - if active and state.ai_available and all(ctrl is not None for ctrl in (ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num)): - _on_manual_mode_changed(state, state.ai_manual_mode, ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num) - _on_trigger_changed(state, state.ai_trigger_mode.capitalize(), ai_interval_select, ai_words_num) - - ai_cb.on_value_change(on_ai_toggle) - if not state.ai_available: - ai_cb.disable() - - _set_section_visual_state(ai_section, state.ai_enabled and state.ai_available) - _set_controls_enabled(ai_controls, state.ai_enabled and state.ai_available) + with ai_section: + try: + from ai_config import load_ai_config + ai_config = load_ai_config() + if ai_config: + # Task selection + personas = ai_config.get_personas() + persona_names = [p['name'] for p in personas] + + with ui.row().classes('items-center w-full gap-1'): + ui.label('Task:').classes('text-xs w-12') + task_select = register_ai(ui.select( + options=persona_names, + value=persona_names[min(state.ai_persona_index, len(persona_names)-1)] + ).classes('flex-grow').props('dense')) + task_select.on_value_change(lambda e: setattr(state, 'ai_persona_index', + persona_names.index(e.value) if e.value in persona_names else 0)) + + # Translate checkboxes - compact + with ui.row().classes('items-center w-full gap-2'): + ai_trans_cb = register_ai(ui.checkbox('Translate', value=state.ai_translate).props('dense')) + ai_trans_cb.on_value_change(lambda e: setattr(state, 'ai_translate', e.value)) + + ai_trans_only_cb = register_ai(ui.checkbox('Only (1:1)', value=state.ai_translate_only).props('dense')) + ai_trans_only_cb.on_value_change(lambda e: setattr(state, 'ai_translate_only', e.value)) + + # Model selection + models = ai_config.get_models() + model_names = [m['name'] for m in models] + + with ui.row().classes('items-center w-full gap-1'): + ui.label('Model:').classes('text-xs w-12') + ai_model_combo = register_ai(ui.select( + options=model_names, + value=model_names[min(state.ai_model_index, len(model_names)-1)] + ).classes('flex-grow').props('dense')) + ai_model_combo.on_value_change(lambda e: setattr(state, 'ai_model_index', + model_names.index(e.value) if e.value in model_names else 0)) + + # Trigger controls - compact layout + ai_manual_cb = register_ai(ui.checkbox('Manual mode', value=state.ai_manual_mode).props('dense')) + + ai_process_btn = register_ai(ui.button('⚡ Process Now', on_click=lambda: bridge.manual_ai_trigger()).classes('w-full').props('dense')) + ai_process_btn.set_enabled(state.ai_manual_mode) + + # Trigger mode and settings + with ui.row().classes('items-center w-full gap-1'): + ui.label('Trigger:').classes('text-xs') + ai_trigger_select = register_ai(ui.select( + options=["Time", "Words"], + value=state.ai_trigger_mode.capitalize() + ).classes('w-16').props('dense')) + ai_trigger_select.set_enabled(not state.ai_manual_mode) + + # Interval control + interval_labels = ["5s", "10s", "15s", "20s", "25s", "30s", "45s", "1m", "1.5m", "2m"] + interval_values = [5, 10, 15, 20, 25, 30, 45, 60, 90, 120] + interval_map = dict(zip(interval_labels, interval_values)) + + current_label = "20s" + for lbl, val in interval_map.items(): + if val == state.ai_process_interval: + current_label = lbl + break + + ui.label('Int:').classes('text-xs') + ai_interval_select = register_ai(ui.select( + options=interval_labels, + value=current_label + ).classes('w-14').props('dense')) + + def on_interval_change(e): + state.ai_process_interval = interval_map.get(e.value, 20) + + ai_interval_select.on_value_change(on_interval_change) + ai_interval_select.set_enabled(not state.ai_manual_mode) + ai_interval_select.set_visibility(state.ai_trigger_mode == "time") + + ui.label('W:').classes('text-xs') + ai_words_num = register_ai(ui.number(value=state.ai_process_words, min=50, max=500, step=50).classes('w-16').props('dense')) + ai_words_num.on_value_change(lambda e: setattr(state, 'ai_process_words', int(e.value or 150))) + ai_words_num.set_enabled(not state.ai_manual_mode) + ai_words_num.set_visibility(state.ai_trigger_mode == "words") + + # Wire up event handlers + ai_manual_cb.on_value_change(lambda e: _on_manual_mode_changed( + state, e.value, ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num)) + + ai_trigger_select.on_value_change(lambda e: _on_trigger_changed( + state, e.value, ai_interval_select, ai_words_num)) + + except Exception as e: + print(f"Error loading AI config: {e}") + + def on_ai_toggle(e): + state.ai_enabled = e.value + active = e.value and state.ai_available + _set_section_visual_state(ai_section, active) + _set_controls_enabled(ai_controls, active) + if active and state.ai_available and all(ctrl is not None for ctrl in (ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num)): + _on_manual_mode_changed(state, state.ai_manual_mode, ai_process_btn, ai_trigger_select, ai_interval_select, ai_words_num) + _on_trigger_changed(state, state.ai_trigger_mode.capitalize(), ai_interval_select, ai_words_num) + + ai_cb.on_value_change(on_ai_toggle) + if not state.ai_available: + ai_cb.disable() + + _set_section_visual_state(ai_section, state.ai_enabled and state.ai_available) + _set_controls_enabled(ai_controls, state.ai_enabled and state.ai_available) + + ui.separator().classes('my-1') + + # === TTS SECTION === + with ui.row().classes('items-center justify-between w-full'): + ui.label('Text-to-Speech').classes('font-bold text-sm') + if state.tts_available: + ui.button(icon='help_outline', on_click=lambda: show_help_dialog('tts')).props('flat dense round size=sm') + + # Enable TTS + tts_cb = ui.checkbox('Enable TTS', value=state.tts_enabled).props('dense') + tts_section = ui.column().classes('w-full gap-1') + tts_controls = [] + + def register_tts(control): + tts_controls.append(control) + return control - ui.separator().classes('my-1') - - # === TTS SECTION === - with ui.row().classes('items-center justify-between w-full'): - ui.label('Text-to-Speech').classes('font-bold text-sm') if state.tts_available: - ui.button(icon='help_outline', on_click=lambda: show_help_dialog('tts')).props('flat dense round size=sm') - - # Enable TTS - tts_cb = ui.checkbox('Enable TTS', value=state.tts_enabled).props('dense') - tts_section = ui.column().classes('w-full gap-1') - tts_controls = [] - - def register_tts(control): - tts_controls.append(control) - return control - - if state.tts_available: - with tts_section: - # Source selection - compact - with ui.row().classes('items-center w-full gap-1'): - ui.label('Src:').classes('text-xs w-10') - - tts_w_cb = register_tts(ui.checkbox('W', value=(state.tts_source == "whisper")).props('dense')) - tts_a_cb = register_tts(ui.checkbox('A', value=(state.tts_source == "ai")).props('dense')) - tts_t_cb = register_tts(ui.checkbox('T', value=(state.tts_source == "translation")).props('dense')) - - tts_w_cb.on_value_change(lambda e: _on_tts_source_changed(state, "whisper", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) - tts_a_cb.on_value_change(lambda e: _on_tts_source_changed(state, "ai", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) - tts_t_cb.on_value_change(lambda e: _on_tts_source_changed(state, "translation", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) - - # Voice selection with streamlined upload - with ui.row().classes('items-center w-full gap-1'): - ui.label('Voice:').classes('text-xs w-10') - tts_voice_label = ui.label(state.tts_voice_display_name).classes('flex-grow text-xs text-gray-400 truncate') - - upload = register_tts(ui.upload( - on_upload=lambda e: _on_voice_upload(e, state, bridge, tts_voice_label), - auto_upload=True, - max_file_size=50_000_000, - max_files=1 - ).props('accept=audio/*').classes('hidden')) - - register_tts(ui.button(icon='folder_open', on_click=lambda u=upload: u.run_method('pickFiles')).props('flat dense round size=sm')) - register_tts(ui.button(icon='clear', on_click=lambda: _clear_voice(state, bridge, tts_voice_label)).props('flat dense round size=sm')) - - # Output options - compact - with ui.row().classes('items-center w-full gap-1'): - ui.label('Out:').classes('text-xs w-10') - tts_save_cb = register_tts(ui.checkbox('Save', value=state.tts_save_file).props('dense')) - tts_save_cb.on_value_change(lambda e: setattr(state, 'tts_save_file', e.value)) - - tts_format_select = register_tts(ui.select(options=["wav", "ogg"], value=state.tts_format).classes('w-16').props('dense')) - tts_format_select.on_value_change(lambda e: setattr(state, 'tts_format', e.value)) - - # TTS status - compact - tts_status_label = ui.label('').classes('text-xs text-blue-400') - - def update_tts_status(): - if state.tts_status_message: - tts_status_label.text = state.tts_status_message[:50] - else: - tts_status_label.text = '' - - ui.timer(0.2, update_tts_status) - - def on_tts_toggle(e): - state.tts_enabled = e.value - active = e.value and state.tts_available - _set_section_visual_state(tts_section, active) - _set_controls_enabled(tts_controls, active) - - tts_cb.on_value_change(on_tts_toggle) - if not state.tts_available: - tts_cb.disable() - - _set_section_visual_state(tts_section, state.tts_enabled and state.tts_available) - _set_controls_enabled(tts_controls, state.tts_enabled and state.tts_available) + with tts_section: + # Source selection - compact + with ui.row().classes('items-center w-full gap-1'): + ui.label('Src:').classes('text-xs w-10') + + tts_w_cb = register_tts(ui.checkbox('W', value=(state.tts_source == "whisper")).props('dense')) + tts_a_cb = register_tts(ui.checkbox('A', value=(state.tts_source == "ai")).props('dense')) + tts_t_cb = register_tts(ui.checkbox('T', value=(state.tts_source == "translation")).props('dense')) + + tts_w_cb.on_value_change(lambda e: _on_tts_source_changed(state, "whisper", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) + tts_a_cb.on_value_change(lambda e: _on_tts_source_changed(state, "ai", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) + tts_t_cb.on_value_change(lambda e: _on_tts_source_changed(state, "translation", e.value, tts_w_cb, tts_a_cb, tts_t_cb)) + + # Voice selection with streamlined upload + with ui.row().classes('items-center w-full gap-1'): + ui.label('Voice:').classes('text-xs w-10') + tts_voice_label = ui.label(state.tts_voice_display_name).classes('flex-grow text-xs text-gray-400 truncate') + + upload = register_tts(ui.upload( + on_upload=lambda e: _on_voice_upload(e, state, bridge, tts_voice_label), + auto_upload=True, + max_file_size=50_000_000, + max_files=1 + ).props('accept=audio/*').classes('hidden')) + + register_tts(ui.button(icon='folder_open', on_click=lambda u=upload: u.run_method('pickFiles')).props('flat dense round size=sm')) + register_tts(ui.button(icon='clear', on_click=lambda: _clear_voice(state, bridge, tts_voice_label)).props('flat dense round size=sm')) + + # Output options - compact + with ui.row().classes('items-center w-full gap-1'): + ui.label('Out:').classes('text-xs w-10') + tts_save_cb = register_tts(ui.checkbox('Save', value=state.tts_save_file).props('dense')) + tts_save_cb.on_value_change(lambda e: setattr(state, 'tts_save_file', e.value)) + + tts_format_select = register_tts(ui.select(options=["wav", "ogg"], value=state.tts_format).classes('w-16').props('dense')) + tts_format_select.on_value_change(lambda e: setattr(state, 'tts_format', e.value)) + + # TTS status - compact + tts_status_label = ui.label('').classes('text-xs text-blue-400') + + def update_tts_status(): + if state.tts_status_message: + tts_status_label.text = state.tts_status_message[:50] + else: + tts_status_label.text = '' + + ui.timer(0.2, update_tts_status) + + def on_tts_toggle(e): + state.tts_enabled = e.value + active = e.value and state.tts_available + _set_section_visual_state(tts_section, active) + _set_controls_enabled(tts_controls, active) + + tts_cb.on_value_change(on_tts_toggle) + if not state.tts_available: + tts_cb.disable() + + _set_section_visual_state(tts_section, state.tts_enabled and state.tts_available) + _set_controls_enabled(tts_controls, state.tts_enabled and state.tts_available) # Update UI periodically - faster for audio level def update_ui():