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'; /// Live Talk realtime voice screen for Copilot. /// Unlike Push-to-Talk (press & hold), this is a continuous conversation mode: /// tap once to start, the app keeps listening/responding automatically until /// the user taps again to stop. class CopilotLiveTalkPage extends StatefulWidget { const CopilotLiveTalkPage({Key? key}) : super(key: key); @override State createState() => _CopilotLiveTalkPageState(); } class _CopilotLiveTalkPageState 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?.stopLiveTalk().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, bool isLiveActive) { switch (status) { case PttStatus.idle: return isLiveActive ? "Getting ready…" : "Tap to start Live Talk"; case PttStatus.connecting: return "Connecting…"; case PttStatus.recording: return "Listening… (pause to send)"; case PttStatus.thinking: return "Thinking…"; case PttStatus.playingAudio: return "Speaking…"; case PttStatus.error: return "Something went wrong"; } } void _onMainButtonTap(CopilotProvider provider) { final status = provider.pttStatus; final isBusy = status == PttStatus.connecting || status == PttStatus.thinking; if (isBusy) return; if (provider.liveSessionActive) { provider.stopLiveTalk().catchError((_) {}); } else { provider.startLiveTalk(context.settingProvider.language!).catchError((_) {}); } } @override Widget build(BuildContext context) { return Scaffold( appBar: const DefaultAppBar(title: "Live 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( "Tap the mic to start a live conversation", 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 isActive = provider.liveSessionActive; final isRecording = status == PttStatus.recording; final isBusy = status == PttStatus.connecting || status == PttStatus.thinking; return Column( mainAxisSize: MainAxisSize.min, children: [ Text( _statusLabel(status, isActive), style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)), ), 16.height, GestureDetector( onTap: isBusy ? null : () => _onMainButtonTap(provider), child: AnimatedContainer( duration: const Duration(milliseconds: 150), height: isRecording || isActive ? 96 : 80, width: isRecording || isActive ? 96 : 80, decoration: BoxDecoration( shape: BoxShape.circle, color: isRecording ? Colors.red : isActive ? AppColor.primary10.withValues(alpha: 0.8) : AppColor.primary10, ), child: isBusy ? const Padding( padding: EdgeInsets.all(24), child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5), ) : Icon( isActive ? Icons.stop_rounded : Icons.graphic_eq_rounded, color: Colors.white, size: isRecording || isActive ? 44 : 36, ), ), ), 8.height, if (isActive) Text( "Tap to stop", style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)), ), 24.height, ], ).paddingOnly(top: 8, bottom: 8); }, ), ), ], ), ); } }