it0/it0_app/lib/features/chat/presentation/widgets/message_bubble.dart

123 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../../core/theme/app_colors.dart';
import '../../domain/entities/chat_message.dart';
/// Renders a single chat message bubble with appropriate styling
/// for user and assistant messages. Assistant messages render Markdown.
class MessageBubble extends StatelessWidget {
final ChatMessage message;
const MessageBubble({super.key, required this.message});
bool get _isUser => message.role == MessageRole.user;
@override
Widget build(BuildContext context) {
return Align(
alignment: _isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.78,
),
decoration: BoxDecoration(
color: _isUser ? AppColors.primary : AppColors.surface,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(_isUser ? 16 : 4),
bottomRight: Radius.circular(_isUser ? 4 : 16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Role label for assistant messages
if (!_isUser && message.type == MessageType.thinking)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'思考中...',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 11,
fontStyle: FontStyle.italic,
),
),
),
// Message content — Markdown for assistant, plain text for user
if (_isUser)
SelectableText(
message.content,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
),
)
else
MarkdownBody(
data: message.content,
selectable: true,
styleSheet: _markdownStyleSheet(context),
),
// Timestamp
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_formatTime(message.timestamp),
style: TextStyle(
color: _isUser
? Colors.white.withOpacity(0.6)
: AppColors.textMuted,
fontSize: 10,
),
),
),
],
),
),
);
}
MarkdownStyleSheet _markdownStyleSheet(BuildContext context) {
return MarkdownStyleSheet(
p: const TextStyle(color: AppColors.textPrimary, fontSize: 15),
h1: const TextStyle(color: AppColors.textPrimary, fontSize: 22, fontWeight: FontWeight.bold),
h2: const TextStyle(color: AppColors.textPrimary, fontSize: 19, fontWeight: FontWeight.bold),
h3: const TextStyle(color: AppColors.textPrimary, fontSize: 17, fontWeight: FontWeight.w600),
strong: const TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.bold),
em: const TextStyle(color: AppColors.textSecondary, fontStyle: FontStyle.italic),
code: TextStyle(
color: AppColors.secondary,
backgroundColor: AppColors.background.withOpacity(0.5),
fontSize: 13,
fontFamily: 'monospace',
),
codeblockDecoration: BoxDecoration(
color: AppColors.background.withOpacity(0.6),
borderRadius: BorderRadius.circular(8),
),
codeblockPadding: const EdgeInsets.all(10),
blockquoteDecoration: BoxDecoration(
border: Border(left: BorderSide(color: AppColors.primary, width: 3)),
),
blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
tableBorder: TableBorder.all(color: AppColors.surfaceLight, width: 0.5),
tableHead: const TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.bold, fontSize: 13),
tableBody: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
tableCellsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
listBullet: const TextStyle(color: AppColors.textSecondary, fontSize: 15),
);
}
String _formatTime(DateTime time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
}