import 'dart:convert'; import 'dart:ffi'; 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: [ 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) Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, children: [ // 时间戳(左对齐或右对齐) Text( timeFormat.format(timestamp), style: TextStyle( fontSize: 10, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[500], fontWeight: FontWeight.w400, ), ), // AI标识(仅在AI消息时显示) if (!isUser) Padding( padding: const EdgeInsets.only(left: 8), child: Text( "由AI生成".tr, style: TextStyle( fontSize: 8, color: Colors.blue[600], fontWeight: FontWeight.bold, fontStyle: FontStyle.italic, ), ), ), ], ), ], ), ), ], ), ); } /// 获取气泡颜色 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 _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('music_play')) { final card_music = meta!["music_play"]['card_music'] as Map?; if (card_music != null) { final musicId = card_music['id']; final musicName = card_music['name'] as String? ?? '未知歌曲'; final musicUrl = card_music['url'] as String? ?? ''; final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; final musicImage = card_music['image'] as String? ?? ''; final ts = card_music['ts'] as int? ?? 0; print("music_play card_music: $card_music"); //音乐卡片 return buildMusicItem(context, musicId, musicName, musicUrl, musicSinger, musicImage, ts); } } else if (meta!.keys.contains('music_playlist')) { final card_musiclist = meta!["music_playlist"]['card_musiclist'] as Map?; if (card_musiclist != null) { final rawList = card_musiclist['musics'] as List? ?? []; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 return buildMusicContent(context, playlist); } } else if (meta!.keys.contains('maps_weather')) { final city = meta!['maps_weather']['city'] as String? ?? '未知城市'; final rawList = meta!['maps_weather']['forecasts'] as List? ?? []; final daylist = rawList.map((e) => Map.from(e)).toList(); final now = daylist.isNotEmpty ? daylist[0] : {}; //天气 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('maps_route_navigation')) { final card_navigation = meta!['maps_route_navigation'] ['card_navigation'] as Map?; if (card_navigation != null) { final start = card_navigation['start'] as String? ?? ''; final end = card_navigation['end'] as String? ?? ''; final destination = card_navigation['destination'] as String? ?? ''; final rawList = card_navigation['items'] as List? ?? []; final list = rawList.map((e) => Map.from(e)).toList(); final rawNavigation = 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, ), ], ), ); } }