From c5533b7efa563a254fb34aa165f682b0b8dc81f4 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:09:24 +0300 Subject: [PATCH] push to talk in progress --- .../screens/copilot_push_to_talk_page.dart | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 lib/modules/copilot/screens/copilot_push_to_talk_page.dart diff --git a/lib/modules/copilot/screens/copilot_push_to_talk_page.dart b/lib/modules/copilot/screens/copilot_push_to_talk_page.dart new file mode 100644 index 00000000..33cabfd8 --- /dev/null +++ b/lib/modules/copilot/screens/copilot_push_to_talk_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import '../controller/copilot_provider.dart'; +import '../widgets/copilot_message_bubble.dart'; + +/// Push-to-Talk realtime voice screen for Copilot. +class CopilotPushToTalkPage extends StatefulWidget { + const CopilotPushToTalkPage({Key? key}) : super(key: key); + + @override + State createState() => _CopilotPushToTalkPageState(); +} + +class _CopilotPushToTalkPageState extends State { + final ScrollController _scrollController = ScrollController(); + CopilotProvider? _provider; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _provider = context.read(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = context.read(); + provider.startNewPttConversation(); + provider.connectPushToTalkHub().catchError((_) {}); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + _provider?.cancelPushToTalk().catchError((_) {}); + _provider?.disconnectPushToTalkHub().catchError((_) {}); + super.dispose(); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + }); + } + + String _statusLabel(PttStatus status) { + switch (status) { + case PttStatus.idle: + return "Hold the mic and speak"; + case PttStatus.connecting: + return "Connecting…"; + case PttStatus.recording: + return "Listening…"; + case PttStatus.thinking: + return "Thinking…"; + case PttStatus.playingAudio: + return "Speaking…"; + case PttStatus.error: + return "Something went wrong"; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const DefaultAppBar(title: "Push to Talk"), + body: Column( + children: [ + Expanded( + child: Consumer( + builder: (context, provider, _) { + if (provider.pttMessages.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 24, + mainAxisSize: MainAxisSize.min, + children: [ + "atoms_logo".toSvgAsset(width: 54), + Text( + "Press and hold the mic to talk", + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: AppTextStyles.heading6.copyWith(color: AppColor.lightTextColor(context), fontWeight: FontWeight.w500), + ), + ], + ).center; + } + + _scrollToBottom(); + final messages = provider.pttMessages; + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: messages.length, + itemBuilder: (context, index) { + final currentMessage = messages[index]; + final bool showHeader = index == 0 || currentMessage.role != messages[index - 1].role; + return CopilotMessageBubble( + message: currentMessage, + showHeader: showHeader, + userAvatar: Provider.of(context, listen: false).user?.profilePhotoName, + ); + }, + ); + }, + ), + ), + Consumer( + builder: (context, provider, _) { + if (provider.pttErrorMessage == null) return const SizedBox(); + return Container( + width: double.infinity, + color: Colors.red.withValues(alpha: 0.1), + padding: const EdgeInsets.all(8), + child: Text(provider.pttErrorMessage!, style: const TextStyle(color: Colors.red)), + ); + }, + ), + SafeArea( + child: Consumer( + builder: (context, provider, _) { + final status = provider.pttStatus; + final isRecording = status == PttStatus.recording; + final isBusy = status == PttStatus.connecting || status == PttStatus.thinking || status == PttStatus.playingAudio; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _statusLabel(status), + style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)), + ), + 16.height, + GestureDetector( + onLongPressStart: isBusy + ? null + : (_) { + context.read().beginPushToTalk(context.settingProvider.language!).catchError((_) {}); + }, + onLongPressEnd: (_) { + context.read().endPushToTalk().catchError((_) {}); + }, + onLongPressCancel: () { + context.read().endPushToTalk().catchError((_) {}); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + height: isRecording ? 96 : 80, + width: isRecording ? 96 : 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isRecording ? Colors.red : AppColor.primary10, + ), + child: isBusy + ? const Padding( + padding: EdgeInsets.all(24), + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5), + ) + : Icon(Icons.mic_rounded, color: Colors.white, size: isRecording ? 44 : 36), + ), + ), + 24.height, + ], + ).paddingOnly(top: 8, bottom: 8); + }, + ), + ), + ], + ), + ); + } +} +