import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; import '../controllers/report_controller.dart'; import 'message_bubble.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/theme/app_colors.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; class AgentView extends StatelessWidget { const AgentView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final controller = Get.find(); 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: 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: !message.isUser ? GestureDetector( onLongPressStart: (LongPressStartDetails details) { _onLongPressMessageContent( 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, ), ) : 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(); } }), // 输入控件行 Row( 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( //height: 44, 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( children: [ Expanded( child: Obx(() { // 图片模式下使用更高的输入框 final isImageActive = controller.isImageInputActive.value; return TextField( controller: controller.textController, maxLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1, style: TextStyle( fontSize: 15, color: isDarkMode ? Colors.white : Colors.grey[800], ), decoration: InputDecoration( hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', hintStyle: TextStyle( color: Colors.grey[500], fontSize: 15, ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: isImageActive ? 12 : 0), ), onSubmitted: (_) => controller.sendTextMessage(), ); }), ), GestureDetector( onTap: () => controller.sendTextMessage(), child: Container( padding: const EdgeInsets.all(6), child: Icon( Icons.send, color: primaryColor, size: 22, ), ), ), ], ), ); } else { // 语音输入模式 - 始终显示波形图 return GestureDetector( onTap: () { if (controller.isListening.value) { controller.stopVoiceInput(); } else { controller.startVoiceInput(); } }, child: Container( height: 50, 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.withOpacity(0.1), width: 1.0, ), ), child: Row( children: [ // 语音波形图 - 始终显示 Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ ...List.generate(10, (index) { return _buildSoundBar(index); }), const Spacer(), // "正在聆听"文字 Obx(() { final isListening = controller.isListening.value; return Row( children: [ Icon( Icons.mic, size: 14, color: isListening ? primaryColor : Colors.grey[500], ), const SizedBox(width: 4), Text( '正在聆听', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: isListening ? primaryColor : Colors.grey[500], ), ), ], ); }), ], ), ), ], ), ), ); } }), ), const SizedBox(width: 8), // 图片选择按钮 Container( width: 44, height: 44, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), child: PopupMenuButton( icon: Icon( Icons.add_photo_alternate_outlined, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 22, ), padding: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), onSelected: (value) async { if (value == 'camera') { await controller.takePhoto(); } else if (value == 'gallery') { await controller.pickImage(); } }, itemBuilder: (context) => [ PopupMenuItem( value: 'camera', child: Row( children: [ Icon(Icons.camera_alt, color: primaryColor, size: 20), const SizedBox(width: 8), const Text('拍照', style: TextStyle(fontSize: 14)), ], ), ), PopupMenuItem( value: 'gallery', child: Row( children: [ Icon(Icons.photo_library, color: primaryColor, size: 20), const SizedBox(width: 8), const Text('从相册选择', style: TextStyle(fontSize: 14)), ], ), ), ], ), ), ], ), ], ), ), ], ), ); } void _onLongPressMessageContent(BuildContext context, bool isDarkMode, LongPressStartDetails details, Message message) { Haptics.vibrate(HapticsType.success); RenderBox? renderBox = Overlay.of(context).context.findRenderObject() as RenderBox; final RelativeRect position = RelativeRect.fromRect( Rect.fromLTRB( details.globalPosition.dx, details.globalPosition.dy, details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 ), Offset.zero & renderBox.size, ); showMenu( context: context, position: position, color: isDarkMode ? AppColors.cardBackground : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), items: [ _popupMenuEntry( Icon( Icons.copy, size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), '复制', isDarkMode ? Colors.white : Colors.black87, () { Clipboard.setData( ClipboardData( text: message.text, ), ).then((value) => Get.snackbar( 'success'.tr, // 成功 '复制成功', backgroundColor: Colors.blue.withOpacity(0.8), colorText: Colors.white, )); }, ), // 自定义分隔线样式 PopupMenuItem( height: 1, // 控制间距 enabled: false, child: Divider( height: 1, thickness: 0.8, indent: 0, endIndent: 0, color: isDarkMode ? Colors.grey[700] : Colors.grey[300], ), ), _popupMenuEntry( Icon( Icons.report, size: 20.w, color: isDarkMode ? Colors.red[700] : Colors.red, ), '举报'.tr, // 举报 isDarkMode ? Colors.red[700]! : Colors.red, () { // 实现举报功能 final reportController = Get.find(); reportController.initReport(message); _showReportForm(context, isDarkMode); // showModalBottomSheet( // context: context, // isScrollControlled: true, // 关键:允许弹窗随键盘移动 // backgroundColor: // isDarkMode ? AppColors.cardBackground : Colors.white, // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), // ), // builder: (BuildContext context) { // return Padding( // // 添加底部内边距以容纳键盘 // padding: EdgeInsets.only( // bottom: MediaQuery.of(context).viewInsets.bottom, // ), // child: Container( // padding: EdgeInsets.all(16.w), // child: SingleChildScrollView( // // 添加滚动视图 // child: Column( // mainAxisSize: MainAxisSize.min, // crossAxisAlignment: CrossAxisAlignment.stretch, // children: [ // Stack( // alignment: Alignment.center, // children: [ // Center( // child: Text( // '举报', // style: TextStyle( // fontSize: 18.sp, // fontWeight: FontWeight.bold, // color: isDarkMode // ? Colors.white // : Colors.black, // ), // ), // ), // SizedBox( // height: 20.w, // ), // Positioned( // right: 0, // child: IconButton( // icon: Icon( // Icons.close, // color: isDarkMode // ? Colors.grey[400] // : Colors.black54, // ), // onPressed: () => Navigator.pop(context), // ), // ), // ], // ), // Container( // padding: EdgeInsets.symmetric(horizontal: 12.w), // margin: EdgeInsets.only(bottom: 10.w), // child: Divider( // height: 1, // color: Colors.grey[300], // ), // ), // Wrap( // spacing: 8.w, // runSpacing: 8.h, // children: [ // _buildReportChip('有害/不安全', isDarkMode), // _buildReportChip('色情低俗', isDarkMode), // _buildReportChip('误导攻击', isDarkMode), // _buildReportChip('违法犯罪', isDarkMode), // _buildReportChip('隐私相关', isDarkMode), // _buildReportChip('侵犯我的著作权、名誉权等', isDarkMode), // _buildReportChip('其他', isDarkMode), // ], // ), // SizedBox(height: 16.h), // Container( // padding: EdgeInsets.all(12.w), // decoration: BoxDecoration( // color: isDarkMode // ? Colors.grey[800] // : Colors.grey[200], // borderRadius: BorderRadius.circular(8.r), // ), // child: TextField( // maxLines: 3, // style: TextStyle( // color: isDarkMode ? Colors.white : Colors.black, // ), // decoration: InputDecoration( // hintText: '我们想知道你举报的原因,你可以描述你遇到的问题。', // hintStyle: TextStyle( // color: isDarkMode // ? Colors.white70 // : Colors.black54, // fontSize: 14.sp, // ), // border: InputBorder.none, // ), // ), // ), // SizedBox(height: 14.h), // ElevatedButton( // style: ElevatedButton.styleFrom( // backgroundColor: Colors.blue, // foregroundColor: Colors.white, // padding: EdgeInsets.symmetric(vertical: 12.h), // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(8.r), // ), // ), // child: Text('提交'), // onPressed: () { // Navigator.pop(context); // Get.snackbar( // '提示', // '举报已提交,我们会尽快处理', // backgroundColor: Colors.green.withOpacity(0.8), // colorText: Colors.white, // ); // }, // ), // SizedBox(height: 8.h), // ], // ), // ), // ), // ); // }, // ); }, ), ], ); } // 更新_showReportForm方法 void _showReportForm(BuildContext context, bool isDarkMode) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), ), builder: (BuildContext context) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: _ReportForm(isDarkMode: isDarkMode), ); }, ); } // // 添加这个辅助方法来创建举报选项 // Widget _buildReportChip(String label, bool isDarkMode) { // return ChoiceChip( // label: Text( // label, // style: TextStyle( // color: isDarkMode ? Colors.white : Colors.black87, // fontSize: 14.sp, // ), // ), // selected: false, // backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(16.r), // ), // onSelected: (bool selected) { // // 可以在这里处理选择逻辑 // }, // ); // } PopupMenuEntry _popupMenuEntry( Icon icon, String title, Color color, Function() onTap, ) { return PopupMenuItem( onTap: onTap, child: SizedBox( width: 140.w, child: Row( children: [ icon, 8.horizontalSpace, Text( title, style: TextStyle( color: color, fontSize: 14.sp, ), ), ], ), ), ); } // 构建提示选项芯片 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 _buildSoundBar(int index) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: Obx(() => _SoundBar( delay: index * 0.1, isActive: Get.find().isListening.value, )), ); } } class _ReportForm extends StatelessWidget { final bool isDarkMode; final ReportController reportController = Get.find(); _ReportForm({required this.isDarkMode}); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(16.w), child: SingleChildScrollView( child: GetBuilder( builder: (_) => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Stack( alignment: Alignment.center, children: [ Center( child: Text( '举报', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black, ), ), ), Positioned( right: 0, child: IconButton( icon: Icon( Icons.close, color: isDarkMode ? Colors.grey[400] : Colors.black54, ), onPressed: () => Navigator.pop(context), ), ), ], ), Container( padding: EdgeInsets.symmetric(horizontal: 12.w), margin: EdgeInsets.only(bottom: 10.w), child: Divider( height: 1, color: Colors.grey[300], ), ), GetBuilder( builder: (controller) { return Wrap( spacing: 8.w, runSpacing: 8.h, children: [ _buildReportChip('有害/不安全', isDarkMode, controller), _buildReportChip('色情低俗', isDarkMode, controller), _buildReportChip('误导攻击', isDarkMode, controller), _buildReportChip('违法犯罪', isDarkMode, controller), _buildReportChip('隐私相关', isDarkMode, controller), _buildReportChip('侵犯我的著作权、名誉权等', isDarkMode, controller), _buildReportChip('其他', isDarkMode, controller), ], ); }, ), SizedBox(height: 16.h), Container( padding: EdgeInsets.all(12.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(8.r), ), child: TextField( maxLines: 3, onChanged: reportController.updateDescription, style: TextStyle( color: isDarkMode ? Colors.white : Colors.black, ), decoration: InputDecoration( hintText: '我们想知道你举报的原因,你可以描述你遇到的问题。', hintStyle: TextStyle( color: isDarkMode ? Colors.white70 : Colors.black54, fontSize: 14.sp, ), border: InputBorder.none, ), ), ), SizedBox(height: 14.h), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 12.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), ), child: Text('提交'), onPressed: () => reportController.submitReport(), ), SizedBox(height: 8.h), ], ), ), ), ); } Widget _buildReportChip( String label, bool isDarkMode, ReportController controller) { final isSelected = reportController.selectedReasons.contains(label); return GestureDetector( onTap: () => reportController.toggleReason(label), child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected ? Colors.red : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), borderRadius: BorderRadius.circular(16.r), border: Border.all( color: isSelected ? Colors.red[700]! : (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), width: isSelected ? 1.0 : 0.5, ), ), child: Text( label, style: TextStyle( color: isSelected ? Colors.white : (isDarkMode ? Colors.white : Colors.black87), fontSize: 14.sp, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ); } } // 声音柱动画组件 class _SoundBar extends StatefulWidget { final double delay; final bool isActive; const _SoundBar({ required this.delay, required this.isActive, }); @override _SoundBarState createState() => _SoundBarState(); } class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); _animation = Tween(begin: 3, end: 18).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOut, ), )..addListener(() { setState(() {}); }); _startOrStopAnimation(); } @override void didUpdateWidget(_SoundBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive != oldWidget.isActive) { _startOrStopAnimation(); } } void _startOrStopAnimation() { if (widget.isActive) { Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { if (mounted && widget.isActive) { _controller.repeat(reverse: true); } }); } else { _controller.reset(); // 设置为小高度的静态状态 _controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final primaryColor = Theme.of(context).primaryColor; return Container( width: 2.5, height: widget.isActive ? _animation.value : 3, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( color: widget.isActive ? primaryColor.withOpacity(0.8) : Colors.grey.withOpacity(0.4), borderRadius: BorderRadius.circular(1.5), ), ); } }