import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.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'; 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; @override void initState() { super.initState(); controller = Get.find(); messageActionController = Get.find(); WidgetsBinding.instance.addObserver(this); } @override void 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), onPressed: () => Get.back(), ), titleSpacing: 0, title: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: const Icon( Icons.smart_toy_outlined, color: Colors.white, size: 22, ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '小言'.tr, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), ), Row( children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( '在线'.tr, style: TextStyle( fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500, ), ), ], ), ], ), ], ), actions: [ // TTS开关 Obx(() => IconButton( icon: Icon( controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, color: controller.isTtsEnabled.value ? primaryColor : isDarkMode ? Colors.grey[400] : Colors.grey, ), onPressed: () => controller.toggleTts(), )), // 更多选项 Container( width: 34, height: 34, margin: const EdgeInsets.only(right: 8), 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, spreadRadius: 0.5, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, color: isDarkMode ? Colors.white : Colors.black87, size: 20), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ PopupMenuItem( child: Row( children: [ Icon(Icons.delete_outline, color: primaryColor, size: 20), const SizedBox(width: 8), Text('清除聊天记录'.tr, style: TextStyle(fontSize: 14)), ], ), onTap: () => Future.delayed( const Duration(milliseconds: 100), () => controller.clearChat(), ), ), ], ), ), ], ), body: Stack( children: [ // 主要聊天界面 Column( children: [ // 聊天内容 Expanded( child: Obx( () => Align( alignment: Alignment.topCenter, child: ListView.builder( controller: controller.scrollController, shrinkWrap: true, reverse: true, padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), itemCount: controller.messages.length, itemBuilder: (context, index) { final message = controller .messages[controller.messages.length - 1 - index]; return Padding( padding: const EdgeInsets.only(bottom: 8), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { messageActionController.onLongPressMessage( context, isDarkMode, details, message); }, child: MessageBubble( isUser: message.isUser, message: message.text, timestamp: message.timestamp, isRecognizing: message.isRecognizing, hasImage: message.hasImage, imagePath: message.imagePath, meta: message.meta, ), )); }, ), ), ), ), // 输入区域 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: backgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, -1), ), ], border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.15), width: 1, ), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 图片预览区域 - 仅在图片输入模式下显示 Obx(() { if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) { return Column( mainAxisSize: MainAxisSize.min, children: [ // 图片预览和关闭按钮 Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(controller.selectedImagePath.value!), height: 150, width: double.infinity, fit: BoxFit.cover, ), ), Positioned( top: 8, right: 8, child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 16, ), ), ), ), ], ), // 预设提示选项 const SizedBox(height: 12), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPromptChip( context: context, label: '总结图片内容'.tr, onTap: () => controller.summarizeImage(), ), const SizedBox(width: 8), _buildPromptChip( context: context, label: '抽取图像文字'.tr, onTap: () => controller.extractTextFromImage(), ), const SizedBox(width: 8), _buildPromptChip( context: context, label: '翻译图像文字'.tr, onTap: () => controller.translateImageText(), ), ], ), ), const SizedBox(height: 12), ], ); } else { return const SizedBox.shrink(); } }), // 输入控件区域 - 包含输入行和展开的选项 Column( children: [ // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 children: [ // 键盘/语音切换按钮 Obx(() { final isTextMode = controller.isTextInputMode.value; return GestureDetector( onTap: () { controller.toggleInputMode(); }, child: Container( width: 44, height: 44, 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, ), ), ); }), const SizedBox(width: 8), // 文本输入框或语音波形图 Expanded( child: Obx(() { final isTextMode = controller.isTextInputMode.value; if (isTextMode) { // 文本输入模式 - 限制最大高度并只向上扩展 return Container( constraints: const BoxConstraints( minHeight: 44, // 最小高度 maxHeight: 120, // 最大高度限制,防止挤压按钮 ), padding: const EdgeInsets.symmetric( horizontal: 16), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), border: Border.all( color: isDarkMode ? Colors.grey[ 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] : Colors.grey.withOpacity(0.1), width: 1.0, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 children: [ Expanded( child: Obx(() { // 图片模式下使用更高的输入框 final isImageActive = controller .isImageInputActive.value; return TextField( 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, color: isDarkMode ? Colors.white : Colors.grey[800], ), decoration: InputDecoration( hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', hintStyle: TextStyle( color: Colors.grey[500], fontSize: 15, ), filled: false, border: InputBorder.none, focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, width: 1.0, ), ), contentPadding: EdgeInsets.symmetric( vertical: isImageActive ? 12 : 8), // 调整垂直内边距 ), onSubmitted: (_) => controller.sendMessage(), ); }), ), ], ), ); } else { // 语音输入模式 - 按住说话模式 return GestureDetector( onLongPressStart: (_) { // 开始按住说话 controller.startPushToTalk(); }, onLongPressEnd: (_) { // 结束按住说话 controller.endPushToTalk(); }, onLongPressCancel: () { // 取消按住说话 controller.endPushToTalk(); }, child: Obx(() { final isPushToTalkActive = controller.isPushToTalkActive.value; final isListening = controller.isListening.value; return Container( height: 50, padding: const EdgeInsets.symmetric( horizontal: 16), decoration: BoxDecoration( color: isPushToTalkActive ? (isDarkMode ? Colors.blue[900] : Colors.blue[100]) : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), borderRadius: BorderRadius.circular(22), border: Border.all( color: isPushToTalkActive ? Colors.blue : (isDarkMode ? Colors.grey[800]! : Colors.grey.withOpacity(0.1)), width: isPushToTalkActive ? 2.0 : 1.0, ), ), child: Row( children: [ // 语音波形图 - 只在按住时显示 if (isPushToTalkActive) ...[ const AgentSoundBarsWidget(), const Spacer(), ], // 状态文字和图标 Expanded( child: Row( mainAxisAlignment: isPushToTalkActive ? MainAxisAlignment.end : MainAxisAlignment.center, children: [ Icon( isPushToTalkActive ? Icons.mic : Icons.mic_none, size: 16, color: isPushToTalkActive ? Colors.blue : Colors.grey[500], ), const SizedBox(width: 6), Text( isPushToTalkActive ? '正在录音...' : '按住说话', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: isPushToTalkActive ? Colors.blue : Colors.grey[600], ), ), ], ), ), ], ), ); }), ); } }), ), const SizedBox(width: 8), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 Obx(() { final hasText = controller.hasTextInput.value; final hasImage = controller.isImageInputActive.value; final shouldShowSend = hasText || hasImage; final shouldShowExpand = controller.shouldShowExpandButton.value; if (shouldShowSend) { // 显示发送按钮(可能还有展开按钮) return Column( mainAxisSize: MainAxisSize.min, children: [ // 展开按钮(在发送按钮上方) if (shouldShowExpand && hasText) Padding( padding: const EdgeInsets.only(bottom: 40), child: GestureDetector( onTap: () { // 打开底部弹出编辑模式 controller.openFullScreenEdit(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const FullScreenEditor(), ).then((_) { // 弹出页关闭时的回调 controller.closeFullScreenEdit(); }); }, child: Container( width: 32, height: 32, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[700] : Colors.grey[300], borderRadius: BorderRadius.circular(16), ), child: Icon( Icons.expand_less, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 20, ), ), ), ), // 发送按钮 GestureDetector( onTap: () => controller.sendMessage(), child: Container( width: 44, height: 44, decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.circular(22), ), child: const Icon( Icons.send, color: Colors.white, size: 22, ), ), ), ], ); } else { // 显示'+'选择按钮 return Obx(() { final isExpanded = controller.isPlusOptionsExpanded.value; return GestureDetector( onTap: () { controller.togglePlusOptions(); }, child: Container( width: 44, height: 44, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), child: Icon( isExpanded ? Icons.close : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 22, ), ), ); }); } }), ], ), ), // 展开的'+'选择选项 Obx(() { final isExpanded = controller.isPlusOptionsExpanded.value; return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 child: isExpanded ? Container( margin: EdgeInsets.only(top: 8.h), padding: EdgeInsets.symmetric(horizontal:16.w), // 添加左右边距 child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布 children: [ // 相机按钮 _buildPlusOptionButton( context: context, icon: Icons.camera_alt, label: '拍照', onTap: () async { //controller.collapsePlusOptions(); await controller.takePhoto(); }, isDarkMode: isDarkMode, ), // 相册按钮 _buildPlusOptionButton( context: context, icon: Icons.photo_library, label: '相册', 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: '打电话', 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 theme = Theme.of(context); final primaryColor = theme.primaryColor; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(16), border: Border.all( color: primaryColor.withOpacity(0.2), width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 13, 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.r), // 调整为10.r border: Border.all( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, width: 1, ), ), 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: isDarkMode ? Colors.black : Colors.white, child: SafeArea( child: Column( children: [ // 顶部标题栏 _buildPhoneCallHeader(isDarkMode), // 中间音频可视化区域 Expanded( child: _buildPhoneCallVisualization(isDarkMode), ), // 底部控制按钮 _buildPhoneCallControls(isDarkMode), ], ), ), ); } // 构建打电话模式标题栏 Widget _buildPhoneCallHeader(bool isDarkMode) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // 返回按钮 GestureDetector( onTap: () => controller.endPhoneCall(), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(20), ), child: Icon( Icons.arrow_back, color: isDarkMode ? Colors.white : Colors.black, size: 20, ), ), ), const SizedBox(width: 16), // 标题 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'AI语音通话', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, ), ), Obx(() { final status = controller.isListening.value ? '正在通话中...' : controller.isSpeaking.value ? 'AI正在回复...' : '等待开始...'; return Text( status, style: TextStyle( fontSize: 14, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), ); }), ], ), ), ], ), ); } // 构建打电话模式音频可视化 Widget _buildPhoneCallVisualization(bool isDarkMode) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 音频可视化圆圈 Obx(() { final isActive = controller.isListening.value || controller.isSpeaking.value; return AnimatedContainer( duration: const Duration(milliseconds: 300), width: isActive ? 200 : 160, height: isActive ? 200 : 160, decoration: BoxDecoration( shape: BoxShape.circle, color: isDarkMode ? Colors.blue[900]?.withOpacity(0.3) : Colors.blue[100], border: Border.all( color: Colors.blue, width: isActive ? 3 : 2, ), ), child: Icon( Icons.phone, size: isActive ? 80 : 60, color: Colors.blue, ), ); }), const SizedBox(height: 32), // 状态文字 Obx(() { String statusText = '点击开始通话'; if (controller.isListening.value) { statusText = '正在听您说话...'; } else if (controller.isSpeaking.value) { statusText = 'AI正在回复...'; } else if (controller.isProcessing.value) { statusText = '正在处理...'; } return Text( statusText, style: TextStyle( fontSize: 16, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.w500, ), ); }), ], ), ); } // 构建打电话模式控制按钮 Widget _buildPhoneCallControls(bool isDarkMode) { return Container( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 静音按钮 Obx(() => _buildPhoneControlButton( icon: controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, onTap: () => controller.toggleTts(), isDarkMode: isDarkMode, isActive: controller.isTtsEnabled.value, )), // 主要通话按钮 Obx(() { final isActive = controller.isListening.value; return GestureDetector( onTap: () { if (isActive) { controller.stopVoiceInput(); } else { controller.startVoiceInput(); } }, child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? Colors.red : Colors.green, boxShadow: [ BoxShadow( color: (isActive ? Colors.red : Colors.green).withOpacity(0.3), blurRadius: 12, spreadRadius: 2, ), ], ), child: Icon( isActive ? Icons.call_end : Icons.call, color: Colors.white, size: 36, ), ), ); }), // 结束通话按钮 _buildPhoneControlButton( icon: Icons.call_end, onTap: () => controller.endPhoneCall(), isDarkMode: isDarkMode, isActive: false, backgroundColor: Colors.red, ), ], ), ); } // 构建打电话控制按钮 Widget _buildPhoneControlButton({ required IconData icon, required VoidCallback onTap, required bool isDarkMode, required bool isActive, Color? backgroundColor, }) { return GestureDetector( onTap: onTap, child: Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), border: isActive ? Border.all(color: Colors.blue, width: 2) : null, ), child: Icon( icon, color: backgroundColor != null ? Colors.white : (isDarkMode ? Colors.white : Colors.black), size: 24, ), ), ); } }