马良AI写作初始化仓库
This commit is contained in:
330
AINoval/lib/screens/chat/widgets/chat_message_bubble.dart
Normal file
330
AINoval/lib/screens/chat/widgets/chat_message_bubble.dart
Normal file
@@ -0,0 +1,330 @@
|
||||
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, // 加粗错误文本
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user