|
|
|
|
@ -17,6 +17,9 @@ import '../utils/resilient_signalr_http_client.dart';
|
|
|
|
|
|
|
|
|
|
enum PttStatus { idle, connecting, recording, thinking, playingAudio, error }
|
|
|
|
|
|
|
|
|
|
/// Connection state of the push-to-talk SignalR hub (observable by the UI).
|
|
|
|
|
enum PttHubState { disconnected, connecting, connected }
|
|
|
|
|
|
|
|
|
|
class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
List<CopilotHistoryItem> history = [];
|
|
|
|
|
bool historyLoading = false;
|
|
|
|
|
@ -33,6 +36,7 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
// in and out of the Push-to-Talk screen quickly.
|
|
|
|
|
Future<void> _pttOpQueue = Future.value();
|
|
|
|
|
PttStatus pttStatus = PttStatus.idle;
|
|
|
|
|
PttHubState pttHubState = PttHubState.disconnected;
|
|
|
|
|
String? pttErrorMessage;
|
|
|
|
|
List<CopilotMessage> pttMessages = [];
|
|
|
|
|
String? pttSessionId;
|
|
|
|
|
@ -367,6 +371,7 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
Future<void> _doConnectPushToTalkHub() async {
|
|
|
|
|
// Always start from a clean slate so every entry is a fresh connection.
|
|
|
|
|
await _stopHubAndCleanup();
|
|
|
|
|
_setHubState(PttHubState.connecting);
|
|
|
|
|
|
|
|
|
|
final token = ApiManager.instance.user?.token ?? '';
|
|
|
|
|
final url = "${URLs.copilotHubUrl}?access_token=$token";
|
|
|
|
|
@ -391,11 +396,13 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
hub.onreconnecting(({Exception? error}) {
|
|
|
|
|
if (!identical(hub, _pttHub)) return;
|
|
|
|
|
_pttLog('hub reconnecting error=$error');
|
|
|
|
|
_setHubState(PttHubState.connecting);
|
|
|
|
|
_handleConnectionInterrupted("Connection lost. Reconnecting…");
|
|
|
|
|
});
|
|
|
|
|
hub.onreconnected(({String? connectionId}) {
|
|
|
|
|
if (!identical(hub, _pttHub)) return;
|
|
|
|
|
_pttLog('hub reconnected connectionId=$connectionId');
|
|
|
|
|
pttHubState = PttHubState.connected;
|
|
|
|
|
// Server-side recording state is bound to the old connection id, so the
|
|
|
|
|
// user must start a new turn.
|
|
|
|
|
if (pttStatus == PttStatus.error || pttStatus == PttStatus.connecting) {
|
|
|
|
|
@ -407,7 +414,8 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
hub.onclose(({Exception? error}) {
|
|
|
|
|
if (!identical(hub, _pttHub)) return; // intentional stop of an old hub
|
|
|
|
|
_pttLog('hub closed error=$error');
|
|
|
|
|
_handleConnectionInterrupted("Connection lost. Hold the mic to reconnect.");
|
|
|
|
|
pttHubState = PttHubState.disconnected;
|
|
|
|
|
_handleConnectionInterrupted("Connection lost. Tap retry to reconnect.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_pttHub!.on("RecordingStarted", _onRecordingStarted);
|
|
|
|
|
@ -424,16 +432,27 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
throw TimeoutException('Connection to push-to-talk service timed out');
|
|
|
|
|
});
|
|
|
|
|
_pttLog('connectPushToTalkHub: connected, state=${_pttHub!.state}');
|
|
|
|
|
pttHubState = PttHubState.connected;
|
|
|
|
|
if (pttStatus == PttStatus.error) pttStatus = PttStatus.idle;
|
|
|
|
|
pttErrorMessage = null;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
} catch (e, st) {
|
|
|
|
|
_pttLog('connectPushToTalkHub: failed error=$e\n$st');
|
|
|
|
|
pttErrorMessage = "Failed to connect to push-to-talk service";
|
|
|
|
|
pttStatus = PttStatus.error;
|
|
|
|
|
await _stopHubAndCleanup();
|
|
|
|
|
pttHubState = PttHubState.disconnected;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _setHubState(PttHubState state) {
|
|
|
|
|
if (pttHubState == state) return;
|
|
|
|
|
pttHubState = state;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Disconnects the push-to-talk connection cleanly. Called when leaving the
|
|
|
|
|
/// screen. Serialized via [_pttOpQueue] so it never races with a connect.
|
|
|
|
|
Future<void> disconnectPushToTalkHub() {
|
|
|
|
|
@ -456,6 +475,7 @@ class CopilotProvider extends ChangeNotifier {
|
|
|
|
|
// Detach first so callbacks (onclose) from this hub are ignored.
|
|
|
|
|
final hub = _pttHub;
|
|
|
|
|
_pttHub = null;
|
|
|
|
|
pttHubState = PttHubState.disconnected;
|
|
|
|
|
try {
|
|
|
|
|
await _abortMicStreaming();
|
|
|
|
|
if (hub != null) {
|
|
|
|
|
|