From a2d6cb6f3c8c102981219ad3208ed87f6ede7c5d Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 4 Jan 2026 03:13:41 -0400 Subject: [PATCH] feat: Add dual-mode dictation (walkie-talkie + continuous) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Press and hold: walkie-talkie mode, records while held, transcribes on release - Double-click: continuous mode (existing behavior), click again to stop - Uses Dear PyGui item handlers for mouse down/up detection - Visual feedback shows current mode on button label 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/discussions/ui/gui.py | 102 ++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/src/discussions/ui/gui.py b/src/discussions/ui/gui.py index de7e41a..a1525e6 100644 --- a/src/discussions/ui/gui.py +++ b/src/discussions/ui/gui.py @@ -409,14 +409,77 @@ class DiscussionGUI: if dpg.does_item_exist(button_tag): dpg.set_item_label(button_tag, "Read") - def _dictate_clicked(self): - """Handle dictate button click - toggle recording.""" - if self._continuous_recorder is not None: - # Currently recording - stop - self._stop_dictation() - else: - # Start recording + def _on_dictate_activated(self, sender, app_data): + """Handle mouse down on dictate button - start timing for mode detection.""" + import time + + current_time = time.time() + + # Check for double-click (two clicks within 300ms) + if current_time - self._last_dictate_click_time < 0.3: + # Double-click detected + self._last_dictate_click_time = 0 # Reset to prevent triple-click issues + self._dictation_mode = "continuous" self._start_dictation() + return + + self._last_dictate_click_time = current_time + self._mouse_down_time = current_time + + # If we're in continuous mode, a click should stop it + if self._dictation_mode == "continuous": + self._stop_dictation() + self._dictation_mode = "idle" + return + + # Start push-to-talk mode preparation + # We'll confirm it in deactivated if held long enough + self._dictation_mode = "push_to_talk_pending" + + # Update button visual to show it's being pressed + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "● Hold...") + + def _on_dictate_deactivated(self, sender, app_data): + """Handle mouse up on dictate button - determine action based on hold duration.""" + import time + + current_time = time.time() + hold_duration = current_time - getattr(self, '_mouse_down_time', current_time) + + if self._dictation_mode == "push_to_talk_pending": + if hold_duration > 0.2: # Held for more than 200ms - it was push-to-talk + # Button was held down, treat as walkie-talkie release + self._dictation_mode = "idle" + if self._continuous_recorder is not None: + self._stop_dictation() + else: + # Was just a long press but recording didn't start yet + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "Dictate") + else: + # Short click - wait for potential double-click + self._dictation_mode = "idle" + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "Dictate") + elif self._dictation_mode == "push_to_talk": + # Release from push-to-talk mode - stop recording + self._dictation_mode = "idle" + self._stop_dictation() + # continuous mode is handled by click, not release + + def _on_dictate_active(self, sender, app_data): + """Handle button being held down - start push-to-talk recording after threshold.""" + import time + + current_time = time.time() + hold_duration = current_time - getattr(self, '_mouse_down_time', current_time) + + if self._dictation_mode == "push_to_talk_pending" and hold_duration > 0.2: + # Held long enough - start push-to-talk recording + self._dictation_mode = "push_to_talk" + if self._continuous_recorder is None: + self._start_dictation() def _start_dictation(self): """Start continuous recording with chunked transcription.""" @@ -431,9 +494,12 @@ class DiscussionGUI: self._continuous_recorder.start() - # Update button to show recording state + # Update button to show recording state based on mode if dpg.does_item_exist("dictate_btn"): - dpg.set_item_label("dictate_btn", "● Recording...") + if self._dictation_mode == "continuous": + dpg.set_item_label("dictate_btn", "● Continuous (click to stop)") + else: # push_to_talk + dpg.set_item_label("dictate_btn", "● Recording (release to stop)") def _stop_dictation(self): """Stop recording and transcribe any remaining audio.""" @@ -556,6 +622,12 @@ class DiscussionGUI: self._transcription_queue = [] # Queue of transcription results self._transcription_lock = threading.Lock() + # Dictation mode tracking for press-and-hold vs double-click + self._dictation_mode = "idle" # "idle", "push_to_talk", "push_to_talk_pending", "continuous" + self._last_dictate_click_time = 0.0 + self._mouse_down_time = 0.0 + self._push_to_talk_thread: Optional[threading.Thread] = None + # Initialize Dear PyGui dpg.create_context() dpg.create_viewport(title="Orchestrated Discussions", width=1400, height=900) @@ -1918,6 +1990,11 @@ class DiscussionGUI: window_tag = "comment_dialog" if dpg.does_item_exist(window_tag): dpg.delete_item(window_tag) + if dpg.does_item_exist("dictate_btn_handlers"): + dpg.delete_item("dictate_btn_handlers") + # Reset dictation state when dialog opens + self._dictation_mode = "idle" + self._last_dictate_click_time = 0.0 with dpg.window(label="Add Comment", tag=window_tag, modal=True, width=600, height=400, pos=[400, 200]): @@ -1928,9 +2005,14 @@ class DiscussionGUI: dpg.add_button( label="Dictate", tag="dictate_btn", - callback=lambda: self._dictate_clicked(), width=100 ) + # Add item handlers for press-and-hold vs double-click detection + with dpg.item_handler_registry(tag="dictate_btn_handlers"): + dpg.add_item_activated_handler(callback=self._on_dictate_activated) + dpg.add_item_deactivated_handler(callback=self._on_dictate_deactivated) + dpg.add_item_active_handler(callback=self._on_dictate_active) + dpg.bind_item_handler_registry("dictate_btn", "dictate_btn_handlers") dpg.add_button( label="Add Artifact", callback=lambda: self._launch_artifact_editor_for_comment(),