import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import '../../../core/widgets/intro_overlay_builder.dart'; import '../../../data/models/user_Info.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; import '../widgets/full_screen_editor.dart'; import '../widgets/sound_bar_widget.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:intl/intl.dart'; class AgentView extends StatefulWidget { const AgentView({Key? key}) : super(key: key); @override State createState() => _AgentViewState(); } class _AgentViewState extends State with WidgetsBindingObserver { late AgentController controller; late MessageActionController messageActionController; double _keyboardHeight = 0; // 👇 新增:用于控制 TextField 焦点 final FocusNode _textFocusNode = FocusNode(); @override void initState() { super.initState(); controller = Get.find(); messageActionController = Get.find(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { _textFocusNode.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); final bottomInset = View.of(context).viewInsets.bottom; final newKeyboardHeight = bottomInset / View.of(context).devicePixelRatio; if (_keyboardHeight != newKeyboardHeight) { _keyboardHeight = newKeyboardHeight; // 更新键盘可见状态 controller.updateKeyboardVisibility(_keyboardHeight > 0); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final primaryColor = theme.primaryColor; final backgroundColor = theme.scaffoldBackgroundColor; final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( appBar: AppBar( elevation: 1, backgroundColor: backgroundColor, leading: IconButton( icon: Icon(Icons.arrow_back_ios, color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp), onPressed: () => Get.back(), ), titleSpacing: 0, title: Row( children: [ Container( width: 40.w, height: 40.w, decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.circular(20.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 3.w, offset: const Offset(0, 1), ), ], ), child: Icon( Icons.support_agent, color: Colors.white, size: 22.sp, ), ), SizedBox(width: 12.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Lily'.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), ), Row( children: [ Container( width: 6.w, height: 6.w, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), SizedBox(width: 4.w), Text( 'online'.tr, style: TextStyle( fontSize: 12.sp, color: Colors.green, fontWeight: FontWeight.w500, ), ), // Obx(() { // final vipexptime = controller.vipExpiryTime.value; // DateTime? expiry; // if (vipexptime > 0) { // expiry = vipexptime > 1000000000000 // ? DateTime.fromMillisecondsSinceEpoch(vipexptime) // : DateTime.fromMillisecondsSinceEpoch( // vipexptime * 1000); // } // final text = expiry != null // ? '${'expiryLabel'.tr}${DateFormat('yyyy-MM-dd').format(expiry)}' // : 'notActivated'.tr; // return Text( // text, // style: TextStyle( // fontSize: 12.sp, // color: Colors.green, // fontWeight: FontWeight.w500, // ), // ); // }), ], ), ], ), ], ), actions: [ // TTS开关 Obx(() => IconButton( icon: Icon( controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, size: 24.sp, color: controller.isTtsEnabled.value ? primaryColor : isDarkMode ? Colors.grey[400] : Colors.grey, ), onPressed: () => controller.toggleTts(), )), // 更多选项 Container( width: 34.w, height: 34.w, margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], shape: BoxShape.circle, boxShadow: [ if (!isDarkMode) BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2.w, spreadRadius: 0.5.w, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.w), ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ // 显示工具开关选项 PopupMenuItem( child: Obx(() => Row( children: [ Icon( controller.isShowToolInterruptEnabled.value ? Icons.build_circle : Icons.build_circle_outlined, color: controller.isShowToolInterruptEnabled.value ? primaryColor : (isDarkMode ? Colors.grey[400] : Colors.grey), size: 20.sp, ), SizedBox(width: 8.w), Text( //'ttsInterruptToggle'.tr, "显示工具", style: TextStyle(fontSize: 14.sp), ), const Spacer(), Switch( value: controller.isShowToolInterruptEnabled.value, onChanged: (value) => controller.toggleShowAIToolInterrupt(), activeColor: primaryColor, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], )), onTap: () => controller.toggleTtsInterrupt(), ), // 播报打断开关选项 PopupMenuItem( child: Obx(() => Row( children: [ Icon( controller.isTtsInterruptEnabled.value ? Icons.voice_over_off : Icons.voice_over_off_outlined, color: controller.isTtsInterruptEnabled.value ? primaryColor : (isDarkMode ? Colors.grey[400] : Colors.grey), size: 20.sp, ), SizedBox(width: 8.w), Text( //'ttsInterruptToggle'.tr, "voice_interrupt".tr, // 语音打断 style: TextStyle(fontSize: 14.sp), ), const Spacer(), Switch( value: controller.isTtsInterruptEnabled.value, onChanged: (value) => controller.toggleTtsInterrupt(), activeColor: primaryColor, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], )), onTap: () => controller.toggleTtsInterrupt(), ), // 清除聊天记录选项 PopupMenuItem( child: Row( children: [ Icon(Icons.delete_outline, color: primaryColor, size: 20.sp), SizedBox(width: 8.w), Text('clearChatHistory'.tr, style: TextStyle(fontSize: 14.sp)), ], ), onTap: () => Future.delayed( const Duration(milliseconds: 100), () => controller.clearChat(), ), ), ], ), ), ], ), body: Stack( children: [ IntroStepBuilder( group: 'ai', order: 3, getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return Obx( () => IntroOverlayBuilder.homeBuilder( params, false, title: controller.newAIReplyCompleted.value ? 'aiReplyCompleted'.tr : 'aiThinking'.tr, onNext: controller.newAIReplyCompleted.value ? () {} : null, ), ); }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, ), ), // 主要聊天界面 Column( children: [ // 聊天内容 Expanded( child: GestureDetector( onTap: () { // 点击聊天区域时,收起键盘并切换工具显示状态 FocusScope.of(context).unfocus(); if (controller.isPlusOptionsExpanded.value) { controller.collapsePlusOptions(); } }, child: Obx(() { return Align( alignment: Alignment.topCenter, child: ListView.builder( controller: controller.scrollController, shrinkWrap: true, reverse: true, padding: EdgeInsets.symmetric( vertical: 16.h, horizontal: 16.w), itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0), itemBuilder: (context, index) { // 如果是最后一项且有更多消息,显示加载指示器 if (index == controller.messages.length && controller.hasMoreMessages.value) { return _buildLoadMoreIndicator(); } final message = controller .messages[controller.messages.length - 1 - index]; return index == 0 ? IntroStepBuilder( group: 'ai', order: 4, getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['endKey']}' .tr, onFinish: () async { if (controller .noviceGuideService.aiStep < 5) { await Future.delayed(const Duration( milliseconds: 300)); controller .noviceGuideService.aiStep++; Intro.of(Get.context!) .start(group: 'ai', reset: true); } }, onTap: () async { params.onFinish(); if (controller .noviceGuideService.aiStep < 5) { await Future.delayed(const Duration( milliseconds: 300)); controller .noviceGuideService.aiStep++; Intro.of(Get.context!) .start(group: 'ai', reset: true); } }, ); }, builder: (BuildContext context, GlobalKey key) => Padding( key: key, padding: EdgeInsets.only(bottom: 8.h), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { messageActionController .onLongPressMessage(context, isDarkMode, details, message); }, child: MessageBubble( sessionid: message.sessionid, isUser: message.isUser, message: message.text, timestamp: message.timestamp, isRecognizing: message.isRecognizing, hasImage: message.hasImage, imagePath: message.imagePath, meta: message.meta, functionCalls: message.functionCalls, isToolCallsExpanded: message.isToolCallsExpanded, ), ), ), ) : Padding( padding: EdgeInsets.only(bottom: 8.h), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { messageActionController .onLongPressMessage(context, isDarkMode, details, message); }, child: MessageBubble( sessionid: message.sessionid, isUser: message.isUser, message: message.text, timestamp: message.timestamp, isRecognizing: message.isRecognizing, hasImage: message.hasImage, imagePath: message.imagePath, meta: message.meta, functionCalls: message.functionCalls, isToolCallsExpanded: message.isToolCallsExpanded, ), ), ); }, ), ); }), ), ), // 输入区域 Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: backgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 8.w, offset: const Offset(0, -1), ), ], border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.15), width: 1.w, ), ), ), child: Column( children: [ // 图片预览区域 - 仅在图片输入模式下显示 Obx( () { if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) { return Column( children: [ // 图片预览和关闭按钮 Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12.w), child: Image.file( File(controller.selectedImagePath.value!), height: 150.h, width: double.infinity, fit: BoxFit.cover, ), ), Positioned( top: 8.h, right: 8.w, child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), child: Icon( Icons.close, color: Colors.white, size: 16.sp, ), ), ), ), ], ), // 预设提示选项 SizedBox(height: 12.h), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPromptChip( context: context, label: 'summarizeImageContent'.tr, //总结图片内容 onTap: () => controller.summarizeImage(), ), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'extractImageText'.tr, //抽取图像文字 onTap: () => controller.extractTextFromImage(), ), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'translateImageText'.tr, //翻译图像文字 onTap: () => controller.translateImageText(), ), ], ), ), SizedBox(height: 12.h), ], ); } else { // 文本输入模式下,且输入框为空 return Column( children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPromptChip( context: context, label: 'presetMusic'.tr, // 来点音乐 onTap: () => controller .preSetPromptAction('Music')), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'presetNavigation'.tr, // 导航 onTap: () => controller.preSetPromptAction( 'Navigation')), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'presetWeather'.tr, // 天气 onTap: () => controller .preSetPromptAction('Weather')), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'presetNews'.tr, // 新闻 onTap: () => controller .preSetPromptAction('News')), SizedBox(width: 8.w), _buildPromptChip( context: context, label: 'presetFood'.tr, // 美食 onTap: () => controller .preSetPromptAction('Food')), ], )), SizedBox(height: 12.h), ], ); } }, ), // 输入控件区域 - 包含输入行和展开的选项 Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 children: [ // 键盘/语音切换按钮 Obx(() { final isTextMode = controller.isTextInputMode.value; return GestureDetector( onTap: () { controller.toggleInputMode(); // 切换模式时,确保焦点正确 if (!isTextMode) { // 切换到语音模式时,请求焦点 Future.delayed(Duration.zero, () { _textFocusNode.requestFocus(); }); } }, child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric(vertical: 2.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), child: Icon( isTextMode ? Icons.mic_none : Icons.keyboard, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 22.sp, ), ), ); }), SizedBox(width: 8.w), // 文本输入框或语音波形图 Expanded( child: Obx(() { final isTextMode = controller.isTextInputMode.value; if (controller.isTextInputMode.value) { // 文本输入模式 - 限制最大高度并只向上扩展 return IntroStepBuilder( group: 'ai', order: 1, getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['startKey']}' .tr, buttonText: 'confirmInput'.tr, onNext: () { controller.textController.text = '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['inputKey']}' .tr; }, onTap: () { controller.textController.text = '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['inputKey']}' .tr; params.onNext!(); }, ); }, onWidgetLoad: () { controller.noviceGuideService .startAIGuide(); }, builder: (BuildContext context, GlobalKey key) => Container( key: key, constraints: BoxConstraints( minHeight: 48.w, ), padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 5.w, ), alignment: Alignment.center, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22.w), border: Border.all( color: isDarkMode ? Colors.grey[800]! : Colors.grey.withOpacity(0.1), width: 1.w, ), ), child: Obx(() { // 图片模式下使用更高的输入框 final isImageActive = controller.isImageInputActive.value; return TextField( focusNode: _textFocusNode, // 绑定焦点节点 controller: controller.textController, maxLines: isImageActive ? 3 : 5, // 设置合理的最大行数 minLines: isImageActive ? 2 : 1, keyboardType: TextInputType.multiline, // 启用多行键盘 textInputAction: TextInputAction.newline, // 支持回车换行 onTap: () { // 当点击输入框时,如果'+'选项已展开,则收缩 if (controller .isPlusOptionsExpanded.value) { controller.collapsePlusOptions(); } }, style: TextStyle( fontSize: 15.sp, color: isDarkMode ? Colors.white : Colors.grey[800], ), decoration: InputDecoration( hintText: isImageActive ? 'addQuestionAboutImage' .tr // 添加对图片的问题或描述... : 'inputMessage'.tr, // 输入消息... hintStyle: TextStyle( color: Colors.grey[500], fontSize: 15.sp, ), filled: false, isDense: true, border: InputBorder.none, focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, width: 1.w, ), ), contentPadding: EdgeInsets.zero, ), onSubmitted: (_) => controller.sendMessage(), ); }), ), ); } else { Timer? _holdTimer; // 增加延迟触发的计时器 // 语音输入模式 - 按住说话模式 return Listener( onPointerDown: (_) { controller.startPushToTalk(); }, onPointerUp: (_) { controller.endPushToTalk(); }, onPointerCancel: (_) { controller.endPushToTalk(); }, // onLongPressStart: (_) { // // 开始按住说话 // controller.startPushToTalk(); // }, // onLongPressEnd: (_) { // // 结束按住说话 // controller.endPushToTalk(); // }, // onLongPressCancel: () { // // 取消按住说话 // controller.endPushToTalk(); // }, child: Obx(() { final isPushToTalkActive = controller.isPushToTalkActive.value; final isListening = controller.isListening.value; return Container( height: 48.w, padding: EdgeInsets.symmetric( horizontal: 16.w), decoration: BoxDecoration( color: isPushToTalkActive ? (isDarkMode ? Colors.blue[900] : Colors.blue[100]) : (isDarkMode ? Colors.grey[800] : const Color.fromARGB( 255, 255, 255, 255)), borderRadius: BorderRadius.circular(22.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), spreadRadius: 1, blurRadius: 2, offset: const Offset(0, 1), ), ], // border: Border.all( // color: isPushToTalkActive // ? Colors.blue // : (isDarkMode // ? Colors.grey[800]! // : const Color.fromARGB( // 255, 255, 255, 255) // .withOpacity(0.1)), // width: // isPushToTalkActive ? 2.w : 1.w, // ), ), child: Row( children: [ // 语音波形图 - 只在按住时显示 if (isPushToTalkActive) ...[ const AgentSoundBarsWidget(), //const Spacer(), ], // 状态文字和图标 Expanded( child: Row( mainAxisAlignment: isPushToTalkActive ? MainAxisAlignment .center : MainAxisAlignment .center, children: [ Icon( isPushToTalkActive ? Icons.mic : Icons.mic_none, size: 16.sp, color: isPushToTalkActive ? Colors.blue : Colors.grey[500], ), const SizedBox(width: 6), Text( isPushToTalkActive ? 'recording' .tr // 正在录音... : 'holdToSpeak' .tr, // 按住说话 style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: isPushToTalkActive ? Colors.blue : Colors.grey[600], ), ), ], ), ), ], ), ); }), ); } }), ), SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 IntroStepBuilder( group: 'ai', order: 2, getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '${'guideClickSendPrefix'.tr}${'${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['sendKey']}'.tr}', alignment: false, onTap: () async { await controller .noviceGuideService.aiStepsList[ controller.noviceGuideService .aiStep]['method'](); params.onNext!(); controller.sendMessage(); }, ); }, builder: (BuildContext context, GlobalKey key) => Obx( key: key, () { final hasText = controller.hasTextInput.value; final hasImage = controller.isImageInputActive.value; final shouldShowSend = hasText || hasImage; final shouldShowExpand = controller.shouldShowExpandButton.value; if (shouldShowSend) { // 显示发送按钮(可能还有展开按钮) return Column( children: [ // 展开按钮(在发送按钮上方) if (shouldShowExpand && hasText) Padding( padding: EdgeInsets.only(bottom: 40.h), child: GestureDetector( onTap: () { // 打开底部弹出编辑模式 controller.openFullScreenEdit(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const FullScreenEditor(), ).then((_) { // 弹出页关闭时的回调 controller .closeFullScreenEdit(); }); }, child: Container( width: 32.w, height: 32.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[700] : Colors.grey[300], borderRadius: BorderRadius.circular( 16.w), ), child: Icon( Icons.expand_less, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 20.sp, ), ), ), ), // 发送按钮 GestureDetector( onTap: () => controller.sendMessage(), child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric( vertical: 2.w), decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.circular(22.w), ), child: Icon( Icons.send, color: Colors.white, size: 22.sp, ), ), ), ], ); } else { // 显示'+'选择按钮 return Obx(() { final isExpanded = controller .isPlusOptionsExpanded.value; return GestureDetector( onTap: () { controller.togglePlusOptions(); }, child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric( vertical: 2.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22.w), ), child: Icon( isExpanded ? Icons.close : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 22.sp, ), ), ); }); } }, ), ), ], ), // 展开的'+'选择选项 Obx(() { final isExpanded = controller.isPlusOptionsExpanded.value; return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸 child: isExpanded ? LayoutBuilder( // 添加LayoutBuilder获取实际约束 builder: (context, constraints) { return SingleChildScrollView( // 确保内容可滚动 scrollDirection: Axis.horizontal, // 水平滚动 child: Container( margin: EdgeInsets.only(top: 16.h), padding: EdgeInsets.symmetric( horizontal: 16.w), // 根据可用宽度调整容器宽度 width: max(constraints.maxWidth, 300.w), // 最小300宽 child: Wrap( // 使用Wrap替代Row,自动换行 alignment: WrapAlignment.center, spacing: 26.w, // 按钮水平间距 runSpacing: 8.h, // 按钮垂直间距 children: [ // 相机按钮 _buildPlusOptionButton( context: context, icon: Icons.camera_alt, label: 'takePhoto'.tr, // 拍照 onTap: () async { //controller.collapsePlusOptions(); await controller.takePhoto(); }, isDarkMode: isDarkMode, ), // 相册按钮 _buildPlusOptionButton( context: context, icon: Icons.photo_library, label: 'photoAlbum'.tr, // 相册 onTap: () async { //controller.collapsePlusOptions(); await controller.pickImage(); }, isDarkMode: isDarkMode, ), // 文件按钮(预留位置) // _buildPlusOptionButton( // context: context, // icon: Icons.folder, // label: '文件', // onTap: () { // //controller.collapsePlusOptions(); // // TODO: 实现文件选择功能 // }, // isDarkMode: isDarkMode, // ), // 通话按钮 _buildPlusOptionButton( context: context, icon: Icons.phone, label: 'makeCall'.tr, // 打电话 onTap: () { // 启动打电话模式 controller.startPhoneCall(); }, isDarkMode: isDarkMode, ), ], ), ), ); }, ) : const SizedBox.shrink(), ); }), ], ), ], ), ), ], ), // 打电话模式覆盖层 Obx(() { if (controller.isPhoneCallMode.value) { return _buildPhoneCallOverlay(context, isDarkMode); } return const SizedBox.shrink(); }), ], ), ); } // 构建提示选项芯片 Widget _buildPromptChip({ required BuildContext context, required String label, required VoidCallback onTap, }) { final primaryColor = Theme.of(context).primaryColor; return GestureDetector( onTap: onTap, child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), decoration: BoxDecoration( color: primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(16.w), border: Border.all( color: primaryColor.withOpacity(0.2), width: 1.w, ), ), child: Text( label, style: TextStyle( fontSize: 13.sp, color: primaryColor, fontWeight: FontWeight.w500, ), ), ), ); } // 构建"+"号展开选项按钮 Widget _buildPlusOptionButton({ required BuildContext context, required IconData icon, required String label, required VoidCallback onTap, required bool isDarkMode, }) { final primaryColor = Theme.of(context).primaryColor; return GestureDetector( onTap: onTap, child: SizedBox( // 调整为一行四个按钮的宽度,考虑屏幕宽度和间距 width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮 child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ // 正方形图标容器 - 增大尺寸 Container( width: 50.w, // 调整为44.w以适应布局 height: 50.w, // 调整为44.w以适应布局 decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[100], borderRadius: BorderRadius.circular(12.w), border: Border.all( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, width: 1.w, ), ), child: Icon( icon, color: primaryColor, size: 20.sp, // 调整为18.sp ), ), SizedBox(height: 4.h), // 调整为3.h // 文字标签 Text( label, style: TextStyle( fontSize: 10.sp, // 调整为9.sp color: isDarkMode ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ); } // 构建打电话模式覆盖层 Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { return Container( color: Colors.transparent, // 设置整个覆盖层为透明 child: Column( children: [ // 中间空白区域 - 完全透明 const Expanded( child: SizedBox(), // 空的占位符,完全透明 ), // 底部控制按钮 - 包含音频波纹 Container( color: isDarkMode ? Colors.black : Colors.white, child: _buildPhoneCallControls(isDarkMode), ), ], ), ); } // 构建打电话模式控制按钮 Widget _buildPhoneCallControls(bool isDarkMode) { return Container( padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ // 录音控制按钮(左侧) - 控制录音输入功能 Obx(() => _buildPhoneControlButton( icon: controller.isListening.value ? Icons.mic // 录音中显示麦克风图标 : Icons.mic_off, // 未录音显示麦克风关闭图标 onTap: () => controller.togglePhoneCallRecording(), isDarkMode: isDarkMode, isActive: controller.isListening.value, // backgroundColor: controller.isListening.value // ? Colors.red // 录音中显示红色背景 // : Theme.of(context).primaryColor, // 未录音显示主题色 )), // 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素 Container( width: 120.w, // 设置固定宽度 height: 64.h, // 与按钮高度一致 alignment: Alignment.center, // 确保波纹居中 child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数 ), // 关闭打电话模式按钮(右侧) - 退出打电话模式 _buildPhoneControlButton( icon: Icons.close, onTap: () => controller.endPhoneCall(), isDarkMode: isDarkMode, isActive: false, //backgroundColor: Colors.red, iconColor: Colors.redAccent, ), ], ), ); } // 构建打电话控制按钮 Widget _buildPhoneControlButton({ required IconData icon, required VoidCallback onTap, required bool isDarkMode, required bool isActive, Color? backgroundColor, Color? iconColor, }) { return GestureDetector( onTap: onTap, child: Container( width: 64.w, // 按钮尺寸 height: 64.h, decoration: BoxDecoration( shape: BoxShape.circle, color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[250]), border: Border.all(color: Colors.blue, width: 2.w), boxShadow: [ BoxShadow( color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3), blurRadius: 8.w, spreadRadius: 1.w, offset: Offset(0, 2.h), ), ], ), child: Icon( icon, color: iconColor ?? (isDarkMode ? Colors.white : Colors.black), size: 28.sp, // 图标尺寸 ), ), ); } // 构建加载更多指示器 Widget _buildLoadMoreIndicator() { return Obx( () { if (controller.isLoadingMore.value) { // 正在加载状态 return Container( padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 16.w, height: 16.h, child: CircularProgressIndicator( strokeWidth: 2.w, ), ), SizedBox(width: 8.w), Text( 'loadMoreMessages'.tr, style: TextStyle( fontSize: 14.sp, color: Colors.grey, ), ), ], ), ), ); } else if (controller.isLoadingMoreError.value) { // 加载失败状态 return Container( padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: GestureDetector( onTap: () => controller.loadMoreMessages(), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.refresh, size: 16.sp, color: Colors.grey, ), SizedBox(width: 8.w), Text( 'loadFailedClickRetry'.tr, style: TextStyle( fontSize: 14.sp, color: Colors.grey, ), ), ], ), ), ), ); } else { // 默认状态(有更多消息但未加载) return Container( padding: EdgeInsets.symmetric(vertical: 8.h), child: Center( child: Text( 'swipeUpForMore'.tr, style: TextStyle( fontSize: 12.sp, color: Colors.grey, ), ), ), ); } }, ); } }