You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

500 lines
16 KiB

import 'dart:convert';
import 'package:common_utils/common_utils.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<AgentController> {
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<String>('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: 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),
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<String, dynamic>.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<String, dynamic>.from(e)).toList();
final now = meta!['card_weather']['day'] as Map<String, dynamic>;
//天气
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<String, dynamic>.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<String, dynamic>.from(e)).toList();
final rawNavigation = meta!['card_navigation']['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? 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<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = Tween<double>(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,
),
],
),
);
}
}