Skip to content

Conversation

@kodjima33
Copy link
Collaborator

Summary

When starting speech-to-text recording on the chat page and switching to a different page, recording would stop. This PR fixes that behavior.

Changes

  • Add VoiceRecorderProvider for centralized recording state management
  • Recording continues when navigating between pages
  • Refactor voice_recorder_widget to use provider instead of local state
  • Works on both mobile and macOS

Technical Details

The previous implementation used local widget state which was disposed when navigating away. The new implementation uses a global provider that persists across the widget tree, allowing recording to continue regardless of page navigation.

- Add VoiceRecorderProvider for centralized recording state management
- Recording continues when navigating between pages
- Refactor voice_recorder_widget to use provider instead of local state
- Works on both mobile and macOS
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the voice recording functionality by extracting its state management into a new VoiceRecorderProvider (a ChangeNotifier). The VoiceRecorderWidget and ChatPage are updated to consume this provider, removing local state related to voice recording, such as _showVoiceRecorder, and delegating recording, processing, and transcription logic to the provider. The review comments identify a critical architectural issue where the VoiceRecorderProvider stores UI callbacks (onTranscriptReady, onClose), which is a fragile pattern that can lead to memory leaks and crashes. The reviewer suggests removing these callbacks from the provider, advocating for the UI to react solely to provider state changes. Specifically, the processRecording method in the provider should update its state to transcribeSuccess for user review, only calling close() if the transcript is empty, and the VoiceRecorderWidget should explicitly call provider.close() when the user confirms sending a transcript.

Comment on lines +30 to +52
// Callbacks for UI integration
Function(String transcript)? _onTranscriptReady;
VoidCallback? _onClose;

VoiceRecorderState get state => _state;
String get transcript => _transcript;
bool get isProcessing => _isProcessing;
List<double> get audioLevels => List.unmodifiable(_audioLevels);
bool get isRecording => _state == VoiceRecorderState.recording;
bool get isActive => _state != VoiceRecorderState.idle;

void setCallbacks({
Function(String transcript)? onTranscriptReady,
VoidCallback? onClose,
}) {
_onTranscriptReady = onTranscriptReady;
_onClose = onClose;
}

void clearCallbacks() {
_onTranscriptReady = null;
_onClose = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Storing UI callbacks like _onTranscriptReady and _onClose in the provider is a fragile pattern that can lead to memory leaks and crashes. If the UI widget that sets these callbacks is disposed, the captured BuildContext and other state become stale, causing errors when the provider later invokes them.

A more robust approach is to remove these callbacks from the provider. The UI should react to state changes from the provider. I'll leave related comments on other files to complete this refactoring.

Suggested change
// Callbacks for UI integration
Function(String transcript)? _onTranscriptReady;
VoidCallback? _onClose;
VoiceRecorderState get state => _state;
String get transcript => _transcript;
bool get isProcessing => _isProcessing;
List<double> get audioLevels => List.unmodifiable(_audioLevels);
bool get isRecording => _state == VoiceRecorderState.recording;
bool get isActive => _state != VoiceRecorderState.idle;
void setCallbacks({
Function(String transcript)? onTranscriptReady,
VoidCallback? onClose,
}) {
_onTranscriptReady = onTranscriptReady;
_onClose = onClose;
}
void clearCallbacks() {
_onTranscriptReady = null;
_onClose = null;
}
VoiceRecorderState get state => _state;
String get transcript => _transcript;
bool get isProcessing => _isProcessing;
List<double> get audioLevels => List.unmodifiable(_audioLevels);
bool get isRecording => _state == VoiceRecorderState.recording;
bool get isActive => _state != VoiceRecorderState.idle;
References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

Comment on lines +173 to +191
try {
final transcript = await transcribeVoiceMessage(audioFile);
_transcript = transcript;
_state = VoiceRecorderState.transcribeSuccess;
_isProcessing = false;
notifyListeners();

if (transcript.isNotEmpty) {
_onTranscriptReady?.call(transcript);
// Auto-close after successful transcription
close();
}
} catch (e) {
debugPrint('Error processing recording: $e');
_state = VoiceRecorderState.transcribeFailed;
_isProcessing = false;
notifyListeners();
AppSnackbar.showSnackbarError('Failed to transcribe audio');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This logic bypasses the transcribeSuccess UI state, which is meant to allow the user to review the transcript. It also uses a callback that can cause lifecycle issues.

To fix this, processRecording should just update the state to transcribeSuccess. The UI will then display the transcript for review. If the transcription is empty, it's best to close the recorder UI.

    try {
      final transcript = await transcribeVoiceMessage(audioFile);
      if (transcript.isEmpty) {
        close();
        return;
      }
      _transcript = transcript;
      _state = VoiceRecorderState.transcribeSuccess;
      _isProcessing = false;
      notifyListeners();
    } catch (e) {
      debugPrint('Error processing recording: $e');
      _state = VoiceRecorderState.transcribeFailed;
      _isProcessing = false;
      notifyListeners();
      AppSnackbar.showSnackbarError('Failed to transcribe audio');
    }
References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

}

notifyListeners();
_onClose?.call();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

As part of removing the callback pattern, the call to _onClose should be removed.

References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

Comment on lines 557 to 559
onClose: () {
setState(() {
_showVoiceRecorder = false;
});
// Provider handles the state change
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The onClose callback is being removed as part of a refactoring to improve state management. The VoiceRecorderWidget will now handle closing itself by calling the provider directly.

References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

Comment on lines +37 to +40
provider.setCallbacks(
onTranscriptReady: widget.onTranscriptReady,
onClose: widget.onClose,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Following the removal of the callback pattern from the provider, setting callbacks here is no longer necessary.

References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

),
IconButton(
icon: const Icon(Icons.send, color: Colors.white),
onPressed: () => widget.onTranscriptReady(provider.transcript),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

After the user confirms the transcript by tapping 'send', the voice recorder UI should be closed. You should call provider.close() here to reset the state.

Suggested change
onPressed: () => widget.onTranscriptReady(provider.transcript),
onPressed: () {
widget.onTranscriptReady(provider.transcript);
provider.close();
},
References
  1. When separating UI and provider logic, provider methods (e.g., clearFilters, removeFilter) should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions (e.g., applyFilters for server-side updates) after the state has been modified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants