Skip to content

Commit 422b9fa

Browse files
RaulSimpetruclaude
andcommitted
Add visual workflow guidance with button highlighting
- Add WidgetHighlighter utility for flashing glow effects on widgets/tabs - Track session recording paths in RecordProtocol - Highlight Training protocol after 2+ recordings - Guide user through Training workflow with button highlights - Auto-select newly created dataset - Clear session recordings after dataset creation - Auto-load trained model in Online protocol - Highlight Select Recordings when switching to Training Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent af1ce7d commit 422b9fa

5 files changed

Lines changed: 427 additions & 21 deletions

File tree

‎myogestic/gui/protocols/online.py‎

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from PySide6.QtCore import QObject, Signal
1111
from PySide6.QtWidgets import QFileDialog
1212

13+
from myogestic.gui.widgets.highlighter import WidgetHighlighter
1314
from myogestic.gui.widgets.logger import LoggerLevel
1415
from myogestic.gui.widgets.templates.output_system import OutputSystemTemplate
1516
from myogestic.gui.widgets.templates.visual_interface import VisualInterface
@@ -71,6 +72,7 @@ def __init__(self, main_window: MyoGestic) -> None:
7172
super().__init__(main_window)
7273

7374
self._main_window = main_window
75+
self._highlighter = WidgetHighlighter(self)
7476

7577
self._selected_visual_interface: VisualInterface | None = None
7678
self._active_visual_interfaces: dict[str, VisualInterface] = {}
@@ -111,6 +113,9 @@ def __init__(self, main_window: MyoGestic) -> None:
111113

112114
self._output_systems__dict: dict[str, OutputSystemTemplate] = {}
113115

116+
# Pending model path for auto-loading after training
117+
self._pending_model_path: str | None = None
118+
114119
def _update_real_time_filter(self) -> None:
115120
self._model_interface.set_real_time_filter(self.real_time_filter_combo_box.currentText())
116121

@@ -132,6 +137,9 @@ def _update_device_configuration(self, is_configured: bool) -> None:
132137

133138
self.online_load_model_push_button.setEnabled(True)
134139

140+
# Guide user to load a model
141+
self._highlighter.highlight(self.online_load_model_push_button)
142+
135143
def online_emg_update(self, data: np.ndarray) -> None:
136144
try:
137145
(
@@ -234,9 +242,11 @@ def _toggle_recording(self):
234242

235243
self.recording_start_time = time.time()
236244
self.online_record_toggle_push_button.setText("Stop Recording")
245+
# Green when recording
246+
self.online_record_toggle_push_button.setStyleSheet("background-color: #5cb85c; color: white;")
237247
else:
238248
self.online_prediction_toggle_push_button.setEnabled(True)
239-
249+
240250
# Disconnect from the primary VI
241251
if self._selected_visual_interface is not None:
242252
self._selected_visual_interface.incoming_message_signal.disconnect(
@@ -245,6 +255,8 @@ def _toggle_recording(self):
245255
self._selected_visual_interface.setup_interface_ui.disconnect_custom_signals()
246256

247257
self.online_record_toggle_push_button.setText("Start Recording")
258+
# Red when not recording
259+
self.online_record_toggle_push_button.setStyleSheet("background-color: #d9534f; color: white;")
248260

249261
self._save_data()
250262

@@ -330,6 +342,9 @@ def _load_model(self) -> None:
330342
LoggerLevel.INFO,
331343
)
332344

345+
# Highlight the Start Prediction button to guide user
346+
self._highlighter.highlight(self.online_prediction_toggle_push_button)
347+
333348
if len(self.active_monitoring_widgets) != 0:
334349
self._send_model_information()
335350

@@ -359,6 +374,82 @@ def _load_model(self) -> None:
359374
k: v(self._main_window, self._model_interface.model.is_classifier) for k, v in current_output_map.items()
360375
}
361376

377+
def _auto_load_model(self, model_path: str) -> None:
378+
"""Load a model programmatically without file dialog.
379+
380+
This is used after training completes to automatically load the newly
381+
trained model into the Online protocol.
382+
383+
Args:
384+
model_path: Full path to the model .pkl file
385+
"""
386+
if not model_path:
387+
return
388+
389+
# Stop any flashing on Load Model button since we're auto-loading
390+
self._highlighter._stop_widget_flash(self.online_load_model_push_button)
391+
392+
# Ensure model interface exists (device must be configured)
393+
if self._model_interface is None:
394+
self._main_window.logger.print(
395+
"Cannot auto-load model: device not configured",
396+
LoggerLevel.WARNING,
397+
)
398+
return
399+
400+
try:
401+
self._model_information__dict = self._model_interface.load_model(model_path)
402+
except Exception as e:
403+
self._main_window.logger.print(
404+
f"Error in auto-loading model: {e}", LoggerLevel.ERROR
405+
)
406+
return
407+
408+
label = model_path.split("/")[-1].split("_")[-1].split(".")[0]
409+
410+
self.online_model_label.setText(f"{label} loaded!")
411+
412+
self.online_commands_group_box.setEnabled(True)
413+
self.online_record_toggle_push_button.setEnabled(False)
414+
415+
self._main_window.logger.print(
416+
f"Model auto-loaded. Label: {label}",
417+
LoggerLevel.INFO,
418+
)
419+
420+
# Highlight the Start Prediction button to guide user
421+
self._highlighter.highlight(self.online_prediction_toggle_push_button)
422+
423+
if len(self.active_monitoring_widgets) != 0:
424+
self._send_model_information()
425+
426+
# Get active VIs directly from main window to ensure we have current state
427+
active_vi_names = set(self._main_window.active_visual_interfaces.keys())
428+
trained_vi_name = self._model_information__dict.get("visual_interface")
429+
430+
if trained_vi_name and active_vi_names and trained_vi_name not in active_vi_names:
431+
self._main_window.logger.print(
432+
f"Warning: Model was trained with visual interface '{trained_vi_name}' "
433+
f"but it is not among the active interfaces: {active_vi_names}.",
434+
LoggerLevel.WARNING,
435+
)
436+
437+
# Only instantiate output systems for VIs that are currently active
438+
current_output_map = {
439+
k: v for k, v in CONFIG_REGISTRY.output_systems_map.items()
440+
if k in active_vi_names
441+
}
442+
443+
self._main_window.logger.print(
444+
f"Creating output systems for: {list(current_output_map.keys())}",
445+
LoggerLevel.INFO,
446+
)
447+
448+
self._output_systems__dict = {
449+
k: v(self._main_window, self._model_interface.model.is_classifier)
450+
for k, v in current_output_map.items()
451+
}
452+
362453
def close_event(self, event) -> None:
363454
for output_system in self._output_systems__dict.values():
364455
output_system.close_event(event)
@@ -402,6 +493,8 @@ def _setup_protocol_ui(self) -> None:
402493
"Record predictions and ground truth for analysis"
403494
)
404495
self.online_record_toggle_push_button.clicked.connect(self._toggle_recording)
496+
# Red by default (not recording)
497+
self.online_record_toggle_push_button.setStyleSheet("background-color: #d9534f; color: white;")
405498

406499
self.online_prediction_toggle_push_button = self._main_window.ui.onlinePredictionTogglePushButton
407500
self.online_prediction_toggle_push_button.setToolTip(

‎myogestic/gui/protocols/protocol.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ def _protocol_toggled(self, index: int, checked: bool) -> None:
6060
self._protocol_mode__stacked_widget.setCurrentIndex(index)
6161
self._current_protocol = self.available_protocols[index]
6262

63+
# Auto-load pending model when switching to Online protocol
64+
if index == 2: # Online protocol
65+
online_protocol = self.available_protocols[2]
66+
if (
67+
hasattr(online_protocol, "_pending_model_path")
68+
and online_protocol._pending_model_path
69+
):
70+
online_protocol._auto_load_model(online_protocol._pending_model_path)
71+
online_protocol._pending_model_path = None
72+
73+
# Highlight Select Recordings button when switching to Training with session recordings
74+
if index == 1: # Training protocol
75+
record_protocol = self.available_protocols[0]
76+
training_protocol = self.available_protocols[1]
77+
if (
78+
hasattr(record_protocol, "_session_recording_paths")
79+
and record_protocol._session_recording_paths
80+
):
81+
training_protocol._highlighter.highlight(
82+
training_protocol.training_create_datasets_select_recordings_push_button
83+
)
84+
6385
def _pass_on_selected_visual_interface(self) -> None:
6486
for protocol in self.available_protocols:
6587
protocol._selected_visual_interface = (

‎myogestic/gui/protocols/record.py‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
QWidget,
2222
)
2323

24+
from myogestic.gui.widgets.highlighter import WidgetHighlighter
2425
from myogestic.gui.widgets.logger import LoggerLevel
2526
from myogestic.gui.widgets.templates.visual_interface import VisualInterface
2627
from myogestic.utils.constants import RECORDING_DIR_PATH
@@ -82,6 +83,10 @@ class RecordProtocol(QObject):
8283
def __init__(self, main_window: MyoGestic) -> None:
8384
super().__init__(main_window)
8485
self._main_window = main_window
86+
self._highlighter = WidgetHighlighter(self)
87+
88+
# Track recordings made this session for preloading in Training
89+
self._session_recording_paths: list[str] = []
8590

8691
self._sampling_frequency: Optional[int] = None
8792
self._selected_visual_interface: Optional[VisualInterface] = None
@@ -371,14 +376,23 @@ def _accept_recording(self) -> None:
371376
label = self._review_label_edit.text() or "default"
372377
biosignal_data, biosignal_timings = self.retrieve_recorded_data()
373378

379+
saved_path = None
374380
if self._active_visual_interfaces:
375-
self._save_combined_recording(biosignal_data, biosignal_timings, label)
381+
saved_path = self._save_combined_recording(biosignal_data, biosignal_timings, label)
376382
elif self._default_recording_interface is not None:
377-
self._save_default_recording(biosignal_data, biosignal_timings, label)
383+
saved_path = self._save_default_recording(biosignal_data, biosignal_timings, label)
384+
385+
# Track recording path for preloading in Training
386+
if saved_path:
387+
self._session_recording_paths.append(saved_path)
378388

379389
self._reset_all_recording_ui()
380390
self._main_window.logger.print(f"Recording with label '{label}' accepted!")
381391

392+
# Guide user to Training protocol after at least 2 recordings
393+
if len(self._session_recording_paths) >= 2:
394+
self._highlighter.highlight(self._main_window.ui.protocolTrainingRadioButton)
395+
382396
def _reject_recording(self) -> None:
383397
"""Discard recording and reset everything."""
384398
self._reset_all_recording_ui()
@@ -452,10 +466,12 @@ def _save_combined_recording(
452466
)
453467

454468
RECORDING_DIR_PATH.mkdir(parents=True, exist_ok=True)
455-
with (RECORDING_DIR_PATH / file_name).open("wb") as f:
469+
full_path = RECORDING_DIR_PATH / file_name
470+
with full_path.open("wb") as f:
456471
pickle.dump(save_dict, f)
457472

458473
self._main_window.logger.print(f"Recording saved as: {file_name}")
474+
return str(full_path)
459475

460476
def _save_default_recording(
461477
self,
@@ -494,10 +510,12 @@ def _save_default_recording(
494510
)
495511

496512
RECORDING_DIR_PATH.mkdir(parents=True, exist_ok=True)
497-
with (RECORDING_DIR_PATH / file_name).open("wb") as f:
513+
full_path = RECORDING_DIR_PATH / file_name
514+
with full_path.open("wb") as f:
498515
pickle.dump(save_dict, f)
499516

500517
self._main_window.logger.print(f"Recording saved as: {file_name}")
518+
return str(full_path)
501519

502520
def _reset_all_recording_ui(self) -> None:
503521
"""Reset shared controls and all per-VI UIs."""

0 commit comments

Comments
 (0)