import 'dart:convert'; import 'package:common_utils/common_utils.dart'; import 'package:deep_voice/modules/agent/views/navigation_card.dart'; import 'weather_card.dart'; import 'music_card.dart'; import 'travel_card.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:get_storage/get_storage.dart'; import 'dart:io'; import 'package:flutter_markdown/flutter_markdown.dart'; import '../controllers/agent_controller.dart'; /// 消息气泡组件 class MessageBubble extends GetView { final bool isUser; final String message; final DateTime timestamp; final bool isRecognizing; final bool hasImage; final String? imagePath; final Map? meta; const MessageBubble({ Key? key, required this.isUser, required this.message, required this.timestamp, this.isRecognizing = false, this.hasImage = false, this.imagePath, this.meta, }) : super(key: key); @override Widget build(BuildContext context) { final timeFormat = DateFormat('HH:mm'); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ //if (!isUser) _buildAvatar(isUser, context), //const SizedBox(width: 8), Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( padding: hasImage ? (message == '[图片]' ? EdgeInsets.zero : isUser ? const EdgeInsets.fromLTRB( 12, 12, 16, 12) // 用户消息右侧多一点内边距 : const EdgeInsets.fromLTRB( 16, 12, 12, 12)) // AI消息左侧多一点内边距 : isUser ? const EdgeInsets.fromLTRB( 12, 12, 16, 12) // 用户消息右侧多一点内边距 : const EdgeInsets.fromLTRB( 16, 12, 12, 12), // AI消息左侧多一点内边距 decoration: BoxDecoration( color: _getBubbleColor(context), borderRadius: BorderRadius.circular(18).copyWith( topLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), topRight: !isUser ? const Radius.circular(18) : const Radius.circular(4), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: _buildMessageContent(context), ), const SizedBox(height: 4), // 识别中的消息不显示时间 if (!isRecognizing) Text( timeFormat.format(timestamp), style: TextStyle( fontSize: 10, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[500], fontWeight: FontWeight.w400, ), ), ], ), ), //const SizedBox(width: 8), //if (isUser) _buildAvatar(isUser, context), ], ), ); } /// 获取气泡颜色 Color _getBubbleColor(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; if (isUser) { return isRecognizing ? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色 : Color(0xFF3B82F6); // 使用更明亮的蓝色 } else { return isDarkMode ? Color(0xFF2A2A2A) : Color(0xFFF1F5F9); // 根据主题调整AI消息背景色 } } /// 构建头像 // Widget _buildAvatar(bool isUser, BuildContext context) { // if (isUser) { // final storage = GetStorage(); // final localAvatarPath = storage.read('local_avatar') ?? ''; // return Container( // width: 36, // height: 36, // decoration: BoxDecoration( // color: localAvatarPath.isEmpty ? Colors.grey[300] : null, // borderRadius: BorderRadius.circular(18), // boxShadow: [ // BoxShadow( // color: Colors.black.withOpacity(0.05), // blurRadius: 2, // offset: const Offset(0, 1), // ), // ], // ), // child: localAvatarPath.isNotEmpty // ? ClipRRect( // borderRadius: BorderRadius.circular(18), // child: Image.file( // File(localAvatarPath), // fit: BoxFit.cover, // ), // ) // : Icon( // Icons.person, // color: Colors.grey[700], // size: 20, // ), // ); // } else { // // AI头像保持不变 // return Container( // width: 36, // height: 36, // decoration: BoxDecoration( // color: Theme.of(context).primaryColor, // borderRadius: BorderRadius.circular(18), // boxShadow: [ // BoxShadow( // color: Colors.black.withOpacity(0.05), // blurRadius: 2, // offset: const Offset(0, 1), // ), // ], // ), // child: Icon( // Icons.smart_toy_outlined, // color: Colors.white, // size: 20, // ), // ); // } // } /// 构建消息内容 Widget _buildMessageContent(BuildContext context) { // 如果是识别中的消息,添加特殊样式 if (isRecognizing) { return Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( message, style: const TextStyle( color: Colors.white, fontStyle: FontStyle.italic, fontSize: 14, height: 1.4, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), _buildTypingIndicator(), ], ); } // 如果是图片消息,显示图片 if (hasImage && imagePath != null) { // 如果消息是默认的[图片]标记,只显示图片 if (message == '[图片]') { return Container( padding: const EdgeInsets.all(8), child: GestureDetector( onTap: () { // 点击图片时打开图片查看器 Get.find() .openImageViewer(context, imagePath ?? ''); }, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(imagePath!), width: 200, height: 200, fit: BoxFit.cover, ), ), ), ); } // 如果消息是自定义文本,显示文本+图片 return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message, style: TextStyle( color: isUser ? Colors.white : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 8), GestureDetector( onTap: () { // 点击图片时打开图片查看器 Get.find() .openImageViewer(context, imagePath ?? ''); }, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(imagePath!), width: 200, height: 200, fit: BoxFit.cover, ), ), ), ], ); } // 判断消息内容,显示特殊卡片 if (!isUser && meta != null) { // 判断消息类型,显示不同的卡片 if (meta!.keys.contains('card_music')) { final musicId = meta!['card_music']['id'].toString(); final musicName = meta!['card_music']['name'] as String; final musicUrl = meta!['card_music']['url'] as String; final musicSinger = meta!['card_music']['sgener'] as String; final musicImage = meta!['card_music']['image'] as String; //音乐卡片 return buildMusicItem( context, musicId, musicName, musicUrl, musicSinger, musicImage); } else if (meta!.keys.contains('card_musiclist')) { final rawList = meta!['card_musiclist']['musics'] as List; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 return buildMusicContent(context, playlist); } else if (meta!.keys.contains('card_weather')) { final city = meta!['card_weather']['city'] as String; final rawList = meta!['card_weather']['dayweather'] as List; final daylist = rawList.map((e) => Map.from(e)).toList(); final now = meta!['card_weather']['day'] as Map; //天气 return buildWeatherContent( context, city, now, daylist, ); } else if (meta!.keys.contains('card_travel')) { final rawList = meta!['card_travel']['items'] as List; final travellist = rawList.map((e) => Map.from(e)).toList(); // 旅游 return buildTravelContent(context, travellist); } else if (meta!.keys.contains('card_navigation')) { final start = meta!['card_navigation']['start'] as String; final end = meta!['card_navigation']['end'] as String; final destination = meta!['card_navigation']['destination'] as String; final rawList = meta!['card_navigation']['items'] as List; final list = rawList.map((e) => Map.from(e)).toList(); final rawNavigation = meta!['card_navigation']['navigation']; final Map? navigation = rawNavigation is Map ? rawNavigation : null; print("导航实时数据 ${rawNavigation}"); // 导航 return navigationCard(context, start: start, end: end, destination: destination, modes: list, navigation: navigation); } // if (message.contains('播放列表') || message.contains('音乐')) { // return _buildMusicContent(context); // } else if (message.contains('景点') || message.contains('旅游')) { // return _buildTravelContent(context); // } else if (message.contains('天气') || // message.contains('气温') || // message.contains('预报')) { // return _buildWeatherContent(context); // } } // 使用Markdown渲染AI消息,用户消息仍然使用普通文本 if (!isUser) { return MarkdownBody( data: message, styleSheet: MarkdownStyleSheet( p: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.9) : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), h1: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black, fontSize: 18, fontWeight: FontWeight.bold, ), h2: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black, fontSize: 16, fontWeight: FontWeight.bold, ), h3: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black, fontSize: 15, fontWeight: FontWeight.bold, ), code: TextStyle( backgroundColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey[800] : Colors.grey[200], fontFamily: 'monospace', fontSize: 13, ), codeblockDecoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(4), ), blockquote: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[700], fontSize: 14, fontStyle: FontStyle.italic, ), blockquoteDecoration: BoxDecoration( border: Border( left: BorderSide( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[600]! : Colors.grey[400]!, width: 4, ), ), ), listBullet: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.9) : Colors.black87, fontSize: 14, ), ), //selectable: true, ); } // 用户消息仍然使用普通文本 return Text( message, style: TextStyle( color: isUser ? Colors.white : (Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.9) : Colors.black87), fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ); } /// 构建输入中的指示器 Widget _buildTypingIndicator() { return SizedBox( width: 40, height: 20, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildDot(100), _buildDot(300), _buildDot(500), ], ), ); } /// 构建动画点 Widget _buildDot(int milliseconds) { return _AnimatedDot(delayMilliseconds: milliseconds); } } /// 动画点组件 class _AnimatedDot extends StatefulWidget { final int delayMilliseconds; const _AnimatedDot({required this.delayMilliseconds}); @override _AnimatedDotState createState() => _AnimatedDotState(); } class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), ); _animation = Tween(begin: 0, end: 6).animate( CurvedAnimation( parent: _controller, curve: Interval(0.0, 0.5, curve: Curves.easeOut), reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn), ), )..addListener(() { setState(() {}); }); Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () { if (mounted) { _controller.repeat(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( width: 6, height: 6, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(3), boxShadow: [ BoxShadow( color: Colors.white.withOpacity(0.5), blurRadius: _animation.value, spreadRadius: _animation.value / 2, ), ], ), ); } }