connection loading added

main_production_copilot
Sikander Saleem 5 hours ago
parent 575c70dd16
commit 754bd0c939

@ -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) {

@ -90,6 +90,35 @@ class _CopilotPushToTalkPageState extends State<CopilotPushToTalkPage> 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<CopilotPushToTalkPage> with Widg
SafeArea(
child: Consumer<CopilotProvider>(
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;

Loading…
Cancel
Save