331 lines
13 KiB
Dart
331 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||
import '../../../models/chat_models.dart';
|
||
import 'chat_message_actions_bar.dart';
|
||
|
||
// 🚀 移除了TypewriterText组件,简化消息显示逻辑
|
||
|
||
class ChatMessageBubble extends StatelessWidget {
|
||
const ChatMessageBubble({
|
||
Key? key,
|
||
required this.message,
|
||
required this.onActionSelected,
|
||
}) : super(key: key);
|
||
final ChatMessage message;
|
||
final Function(MessageAction) onActionSelected;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 假设 message.role 可以区分用户和 AI (如果用 sender,则替换为 message.sender)
|
||
final bool isUserMessage = message.role ==
|
||
MessageRole.user; // 或者 message.sender == MessageSender.user
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 6.0), // 稍微减少垂直间距
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
isUserMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||
crossAxisAlignment: CrossAxisAlignment.start, // 保持顶部对齐
|
||
children: [
|
||
// AI 头像占位符 (如果需要显示)
|
||
if (!isUserMessage) _buildAvatar(context, false),
|
||
if (!isUserMessage) const SizedBox(width: 8),
|
||
|
||
// 消息气泡容器 - 使用LayoutBuilder
|
||
Flexible(
|
||
child: LayoutBuilder(builder: (context, constraints) {
|
||
// 基于LayoutBuilder中的约束计算最大宽度,保证气泡不会太宽
|
||
final maxWidth = constraints.maxWidth * 0.95;
|
||
|
||
return Container(
|
||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||
child: Column(
|
||
// 用户消息时间戳靠右,AI 消息时间戳靠左
|
||
crossAxisAlignment: isUserMessage
|
||
? CrossAxisAlignment.end
|
||
: CrossAxisAlignment.start,
|
||
children: [
|
||
// 气泡主体
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 10.0, horizontal: 14.0), // 调整内边距
|
||
decoration: BoxDecoration(
|
||
color: isUserMessage
|
||
? Theme.of(context).colorScheme.primary // 用户消息用主色
|
||
: Theme.of(context)
|
||
.colorScheme
|
||
.surfaceContainer, // AI消息用 surfaceContainer
|
||
// 实现"尾巴"效果的圆角
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: const Radius.circular(16.0),
|
||
topRight: const Radius.circular(16.0),
|
||
bottomLeft: Radius.circular(
|
||
isUserMessage ? 16.0 : 4.0), // 用户左下圆角,AI左下小圆角/直角
|
||
bottomRight: Radius.circular(
|
||
isUserMessage ? 4.0 : 16.0), // 用户右下小圆角/直角,AI右下圆角
|
||
),
|
||
// 可以为 AI 消息添加细微边框
|
||
border: !isUserMessage
|
||
? Border.all(
|
||
color: Theme.of(context)
|
||
.colorScheme
|
||
.outlineVariant
|
||
.withOpacity(0.3),
|
||
width: 0.5,
|
||
)
|
||
: null,
|
||
),
|
||
child: isUserMessage
|
||
? _buildUserMessageContent(context)
|
||
: _buildAIMessageContent(context),
|
||
),
|
||
// 时间戳
|
||
Padding(
|
||
padding: const EdgeInsets.only(
|
||
top: 4.0, left: 6.0, right: 6.0),
|
||
child: Text(
|
||
message.formattedTime,
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context)
|
||
.colorScheme
|
||
.onSurfaceVariant
|
||
.withOpacity(0.7),
|
||
),
|
||
),
|
||
),
|
||
|
||
// 通用操作栏(复制等)
|
||
ChatMessageActionsBar(
|
||
textToCopy: message.content,
|
||
alignEnd: isUserMessage,
|
||
compact: true,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
|
||
// 用户头像占位符 (如果需要显示)
|
||
if (isUserMessage) const SizedBox(width: 8),
|
||
if (isUserMessage) _buildAvatar(context, true),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 头像构建方法 (可选)
|
||
Widget _buildAvatar(BuildContext context, bool isUser) {
|
||
// 现在使用 Icon 代替 CircleAvatar
|
||
return Icon(
|
||
isUser ? Icons.person_outline : Icons.smart_toy_outlined,
|
||
color: isUser
|
||
? Theme.of(context).colorScheme.primary
|
||
: Theme.of(context).colorScheme.secondary,
|
||
size: 28, // 调整大小
|
||
);
|
||
/* return CircleAvatar(
|
||
radius: 16, // 调整大小
|
||
backgroundColor: isUser
|
||
? Theme.of(context).colorScheme.primaryContainer
|
||
: Theme.of(context).colorScheme.secondaryContainer,
|
||
child: Icon(
|
||
isUser ? Icons.person_outline : Icons.smart_toy_outlined, // 使用 outline 图标
|
||
size: 18, // 图标大小
|
||
color: isUser
|
||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||
: Theme.of(context).colorScheme.onSecondaryContainer,
|
||
),
|
||
); */
|
||
}
|
||
|
||
// 构建用户消息内容
|
||
Widget _buildUserMessageContent(BuildContext context) {
|
||
return SelectableText(
|
||
message.content,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onPrimary, // 用户消息文本颜色
|
||
fontSize: 14, // 调整字体大小
|
||
height: 1.4, // 调整行高
|
||
),
|
||
);
|
||
}
|
||
|
||
// 构建AI消息内容 (Markdown) - 修改为支持打字机效果
|
||
Widget _buildAIMessageContent(BuildContext context) {
|
||
final textTheme = Theme.of(context).textTheme;
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (message.status == MessageStatus.error)
|
||
_buildErrorMessage(context)
|
||
else if (message.status == MessageStatus.streaming || message.status == MessageStatus.pending)
|
||
// 🚀 对于正在生成的消息,显示简单的等待状态
|
||
_buildWaitingContent(context)
|
||
else
|
||
// 🚀 对于已完成的消息,直接使用可选择的 Markdown
|
||
MarkdownBody(
|
||
data: message.content.isEmpty ? '思考中...' : message.content,
|
||
selectable: true,
|
||
styleSheet: MarkdownStyleSheet(
|
||
p: textTheme.bodyMedium?.copyWith(
|
||
color: colorScheme.onSurfaceVariant, // AI 消息主要文本颜色
|
||
fontSize: 14, // 字体大小
|
||
height: 1.4, // 行高
|
||
),
|
||
h1: textTheme.titleLarge?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
h2: textTheme.titleMedium?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
h3: textTheme.titleSmall?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
code: textTheme.bodyMedium?.copyWith(
|
||
fontFamily: 'monospace',
|
||
backgroundColor: colorScheme.surfaceContainerHighest
|
||
.withOpacity(0.5), // 代码背景色
|
||
color: colorScheme.onSurfaceVariant, // 代码文字颜色
|
||
fontSize: 13,
|
||
),
|
||
codeblockDecoration: BoxDecoration(
|
||
color: colorScheme.surfaceContainerHighest
|
||
.withOpacity(0.5), // 代码块背景色
|
||
borderRadius: BorderRadius.circular(4),
|
||
border: Border.all(
|
||
color:
|
||
colorScheme.outlineVariant.withOpacity(0.3)), // 代码块边框
|
||
),
|
||
blockquoteDecoration: BoxDecoration(
|
||
// 引用块样式
|
||
border: Border(
|
||
left: BorderSide(color: colorScheme.primary, width: 4)),
|
||
color: colorScheme.primaryContainer.withOpacity(0.1),
|
||
borderRadius: const BorderRadius.only(
|
||
topRight: Radius.circular(4),
|
||
bottomRight: Radius.circular(4)),
|
||
),
|
||
blockquotePadding: const EdgeInsets.all(12), // 引用块内边距
|
||
listBulletPadding: const EdgeInsets.only(right: 4), // 列表标记边距
|
||
listIndent: 16, // 列表缩进
|
||
),
|
||
),
|
||
|
||
// ActionChip 样式调整
|
||
if (message.actions != null && message.actions!.isNotEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 10.0), // Chip 与上方内容的间距
|
||
child: Wrap(
|
||
spacing: 8,
|
||
runSpacing: 6,
|
||
children: message.actions!.map((action) {
|
||
return ActionChip(
|
||
label: Text(action.label),
|
||
onPressed: () => onActionSelected(action),
|
||
backgroundColor: colorScheme.secondaryContainer
|
||
.withOpacity(0.5), // Chip 背景色
|
||
labelStyle: textTheme.bodySmall?.copyWith(
|
||
// Chip 文字样式
|
||
color: colorScheme.onSecondaryContainer,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8, vertical: 2), // Chip 内边距
|
||
side: BorderSide.none, // 移除边框
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16)), // 圆角
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// 🚀 新增:构建等待状态内容,直接显示消息内容
|
||
Widget _buildWaitingContent(BuildContext context) {
|
||
final textTheme = Theme.of(context).textTheme;
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
|
||
// 🚀 如果有消息内容,直接显示为可选择的Markdown,否则显示等待提示
|
||
if (message.content.isNotEmpty) {
|
||
return MarkdownBody(
|
||
data: message.content,
|
||
selectable: true,
|
||
styleSheet: MarkdownStyleSheet(
|
||
p: textTheme.bodyMedium?.copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
fontSize: 14,
|
||
height: 1.4,
|
||
),
|
||
h1: textTheme.titleLarge?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
h2: textTheme.titleMedium?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
h3: textTheme.titleSmall?.copyWith(
|
||
color: colorScheme.onSurface, fontWeight: FontWeight.w600),
|
||
code: textTheme.bodyMedium?.copyWith(
|
||
fontFamily: 'monospace',
|
||
backgroundColor: colorScheme.surfaceContainerHighest
|
||
.withOpacity(0.5),
|
||
color: colorScheme.onSurfaceVariant,
|
||
fontSize: 13,
|
||
),
|
||
codeblockDecoration: BoxDecoration(
|
||
color: colorScheme.surfaceContainerHighest
|
||
.withOpacity(0.5),
|
||
borderRadius: BorderRadius.circular(4),
|
||
border: Border.all(
|
||
color: colorScheme.outlineVariant.withOpacity(0.3)),
|
||
),
|
||
blockquoteDecoration: BoxDecoration(
|
||
border: Border(
|
||
left: BorderSide(color: colorScheme.primary, width: 4)),
|
||
color: colorScheme.primaryContainer.withOpacity(0.1),
|
||
borderRadius: const BorderRadius.only(
|
||
topRight: Radius.circular(4),
|
||
bottomRight: Radius.circular(4)),
|
||
),
|
||
blockquotePadding: const EdgeInsets.all(12),
|
||
listBulletPadding: const EdgeInsets.only(right: 4),
|
||
listIndent: 16,
|
||
),
|
||
);
|
||
} else {
|
||
// 🚀 只有在没有内容时才显示简单的等待提示
|
||
return SelectableText(
|
||
'AI正在思考...',
|
||
style: textTheme.bodyMedium?.copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
fontSize: 14,
|
||
height: 1.4,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 构建错误消息 (样式微调)
|
||
Widget _buildErrorMessage(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Icon(
|
||
Icons.error_outline,
|
||
color: Theme.of(context).colorScheme.error,
|
||
size: 18, // 调整图标大小
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: SelectableText(
|
||
message.content.isEmpty ? '发生错误' : message.content, // 默认错误消息
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
color: Theme.of(context).colorScheme.error,
|
||
fontWeight: FontWeight.w500, // 加粗错误文本
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|