From 754bd0c93991c994873a96add7074986c662ca3f Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 21 Oct 2026 12:36:58 +0300 Subject: [PATCH] connection loading added --- .../copilot/controller/copilot_provider.dart | 22 ++++++++++++- .../screens/copilot_push_to_talk_page.dart | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/modules/copilot/controller/copilot_provider.dart b/lib/modules/copilot/controller/copilot_provider.dart index 8582f899..b74ed940 100644 --- a/lib/modules/copilot/controller/copilot_provider.dart +++ b/lib/modules/copilot/controller/copilot_provider.dart @@ -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 history = []; bool historyLoading = false; @@ -33,6 +36,7 @@ class CopilotProvider extends ChangeNotifier { // in and out of the Push-to-Talk screen quickly. Future _pttOpQueue = Future.value(); PttStatus pttStatus = PttStatus.idle; + PttHubState pttHubState = PttHubState.disconnected; String? pttErrorMessage; List pttMessages = []; String? pttSessionId; @@ -367,6 +371,7 @@ class CopilotProvider extends ChangeNotifier { Future _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 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) { diff --git a/lib/modules/copilot/screens/copilot_push_to_talk_page.dart b/lib/modules/copilot/screens/copilot_push_to_talk_page.dart index ec02189b..7919dca0 100644 --- a/lib/modules/copilot/screens/copilot_push_to_talk_page.dart +++ b/lib/modules/copilot/screens/copilot_push_to_talk_page.dart @@ -90,6 +90,35 @@ class _CopilotPushToTalkPageState extends State with Widg } } + /// Shown in place of the mic while the socket is connecting / disconnected. + Widget _buildConnectionPlaceholder(BuildContext context, CopilotProvider provider) { + // Treat "disconnected without an error" as connecting (e.g. the first frame + // before the connection attempt kicks off). + final isConnecting = provider.pttHubState == PttHubState.connecting || provider.pttErrorMessage == null; + return SizedBox( + height: 148, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (isConnecting) ...[ + const SizedBox(height: 28, width: 28, child: CircularProgressIndicator(strokeWidth: 2.5)), + 12.height, + Text( + "Connecting…", + style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)), + ), + ] else + TextButton.icon( + onPressed: () => provider.ensurePushToTalkConnected().catchError((_) {}), + icon: const Icon(Icons.refresh_rounded), + label: const Text("Retry connection"), + ), + ], + ), + ).paddingOnly(top: 8, bottom: 8); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -149,6 +178,10 @@ class _CopilotPushToTalkPageState extends State with Widg SafeArea( child: Consumer( builder: (context, provider, _) { + // Hide the mic until the socket connection is established. + if (provider.pttHubState != PttHubState.connected) { + return _buildConnectionPlaceholder(context, provider); + } final status = provider.pttStatus; final isRecording = status == PttStatus.recording; final isBusy = status == PttStatus.connecting || status == PttStatus.thinking || status == PttStatus.playingAudio;