import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import '../../../core/widgets/intro_overlay_builder.dart'; import '../../../data/models/channel_app_model.dart'; import '../../../data/services/ble_manager.dart'; import '../../../data/services/version_update_service.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.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; void _showTtsSpeedDialog() { final primaryColor = Theme.of(context).primaryColor; final isDarkMode = Theme.of(context).brightness == Brightness.dark; final RxInt tempRate = controller.ttsRatePercent.value.obs; Get.dialog( AlertDialog( backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.w), ), title: Text( 'ttsSpeed'.tr, // 语速 style: TextStyle(fontSize: 16.sp), ), content: Obx(() { return Column( mainAxisSize: MainAxisSize.min, children: [ Align( alignment: Alignment.centerLeft, child: Text( 'ttsSpeedDefaultHint'.tr, // 默认 +25% style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), ), ), SizedBox(height: 12.h), Text( '+${tempRate.value}%', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, color: primaryColor, ), ), SizedBox(height: 8.h), Slider( value: tempRate.value.toDouble(), min: 0, max: 100, divisions: 20, label: '+${tempRate.value}%', activeColor: primaryColor, onChanged: (value) { tempRate.value = value.round(); }, ), ], ); }), actions: [ TextButton( onPressed: () => Get.back(), child: Text('cancel'.tr), ), TextButton( onPressed: () async { Get.back(); await controller.updateTtsRate(tempRate.value); }, child: Text('confirm'.tr), ), ], ), barrierDismissible: true, ); } @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; final isOfficialChannel = Get.isRegistered() && VersionUpdateService.to.cachedChannel == UserChannel.OFFICIAL; 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.withValues(alpha: 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.withValues(alpha: 0.1) : Colors.grey[100], shape: BoxShape.circle, boxShadow: [ if (!isDarkMode) BoxShadow( color: Colors.black.withValues(alpha: 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) => [ if (isOfficialChannel) 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( "显示工具", 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.speed, color: primaryColor, size: 20.sp), SizedBox(width: 8.w), Text('ttsSpeed'.tr, // 语速 style: TextStyle(fontSize: 14.sp)), ], ), onTap: () => Future.delayed( const Duration(milliseconds: 100), () => _showTtsSpeedDialog(), ), ), // 清除聊天记录选项 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: SafeArea( top: false, child: 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, ), ), // 主要聊天界面(占满整个 body,底部按钮浮在其上) 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.only( top: 16.h, // 预留浮动通话/挂断按钮的高度,避免最后一条消息被遮挡 bottom: 140.h, left: 16.w, right: 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, ), ), ); }, ), ); }), ), // 底部浮动通话按钮:悬浮在聊天界面之上 // 点击启动耳机录音 → JieliAssistantPort 通路 // (MODE_RECORD + STRATEGY_DEVICE_ALWAYS_RECORDING) Align( alignment: Alignment.bottomCenter, child: Obx(() { if (controller.isDeviceCallMode.value) { return const SizedBox.shrink(); } final isConnected = Get.find().isConnected; return Padding( padding: EdgeInsets.only(bottom: 16.h), child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () => controller.startDeviceCall(), child: Container( width: 68.w, height: 68.w, decoration: BoxDecoration( color: isConnected ? const Color(0xFF10B981) : Colors.grey, shape: BoxShape.circle, boxShadow: isConnected ? [ BoxShadow( color: const Color(0xFF10B981) .withValues(alpha: 0.35), blurRadius: 16.w, offset: Offset(0, 6.h), ), ] : null, ), child: Icon( Icons.call, color: Colors.white, size: 30.sp, ), ), ), SizedBox(height: 8.h), Text( '通话', style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], ), ), ], ), ); }), ), // 打电话模式覆盖层 Obx(() { if (controller.isPhoneCallMode.value) { return _buildPhoneCallOverlay(context, isDarkMode); } return const SizedBox.shrink(); }), // 设备通话模式覆盖层 Obx(() { if (controller.isDeviceCallMode.value) { return _buildDeviceCallOverlay(context, isDarkMode); } return const SizedBox.shrink(); }), ], ), ), ); } // 构建打电话模式覆盖层 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 _buildDeviceCallOverlay(BuildContext context, bool isDarkMode) { return Align( alignment: Alignment.bottomCenter, child: Padding( padding: EdgeInsets.only(bottom: 16.h), child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () => controller.endDeviceCall(), child: Container( width: 68.w, height: 68.w, decoration: BoxDecoration( color: const Color(0xFFEF4444), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: const Color(0xFFEF4444).withValues(alpha: 0.35), blurRadius: 16.w, offset: Offset(0, 6.h), ), ], ), child: Transform.rotate( angle: 2.356, child: Icon( Icons.call_end, color: Colors.white, size: 30.sp, ), ), ), ), SizedBox(height: 8.h), Text( '挂断', style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], ), ), ], ), ), ); } // 构建加载更多指示器 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, ), ), ), ); } }, ); } }