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); }, ), ), ], ), ); } }