The-Agency/lib/ui/widgets/chat/message_bubble.dart

217 lines
5.9 KiB
Dart

import "package:flutter/src/material/theme_data.dart";
import "package:flutter_markdown/flutter_markdown.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../../../src/permissions/permission_types.dart";
import "../../../src/session/session_types.dart";
import "advisor_message.dart";
import "../common/button.dart";
import "dart:typed_data";
import "attachment_preview.dart";
import "../../models/attachment.dart";
class MessageBubble extends StatelessWidget {
const MessageBubble({
required this.message,
this.isPendingPermission = false,
this.onPermissionDecision,
});
final Message message;
final bool isPendingPermission;
final void Function(PermissionDecision)? onPermissionDecision;
@override
Widget build(BuildContext context) {
final isUser = message.role == "user";
final isTool = message.role == "tool";
final isAssistant = message.role == "assistant";
final theme = Theme.of(context);
if (isUser) {
final atts = message.attachments;
return Align(
alignment: Alignment.centerRight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (atts != null && atts.isNotEmpty) ...[
SingleChildScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
child: Row(
children: [
for (final att in atts)
Padding(
padding: EdgeInsets.only(left: 8),
child: AttachmentItem(
attachment: Attachment(
name: att.name,
mimeType: att.mimeType,
data: Uint8List.fromList(att.data),
),
onRemove: () {},
),
),
],
),
),
Gap(6),
],
OutlinedContainer(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
backgroundColor: theme.colorScheme.border,
child: MarkdownBody(
data: message.content,
selectable: true,
shrinkWrap: true,
styleSheet: _toolMarkdownStyleSheet(context),
),
),
],
),
);
} else if (isAssistant) {
return MarkdownBody(
data: message.content,
selectable: true,
shrinkWrap: true,
styleSheet: _toolMarkdownStyleSheet(context),
);
} else if (isTool) {
final lines = message.content.split("\n");
final title = lines.first.trim();
final isAdvisor = title.startsWith("Advisor");
if (isAdvisor) {
final body = lines.skip(1).join("\n").trim();
return AdvisorMessage(title: title, body: body);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
OutlinedContainer(
padding: const EdgeInsets.all(10),
backgroundColor: theme.colorScheme.primary,
child: Icon(LucideIcons.wrench).iconSmall,
),
Gap(8),
Expanded(
child: Text(
title,
style: theme.typography.p.copyWith(fontSize: 13),
),
),
],
),
if (isPendingPermission) ...[
Gap(8),
Row(
children: [
AgcSecondaryButton(
onPressed: () => onPermissionDecision?.call(PermissionDecision.allowOnce),
child: Text("Yes").small,
),
Gap(8),
AgcGhostButton(
onPressed: () => onPermissionDecision?.call(PermissionDecision.allowAlways),
child: Text("Yes, for this session").small,
),
Gap(8),
AgcGhostButton(
onPressed: () => onPermissionDecision?.call(PermissionDecision.reject),
child: Text("No").small,
),
],
),
],
],
);
}
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.role,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
if (isAssistant || isTool)
MarkdownBody(
data: isTool
? _buildToolMarkdown(message.content)
: message.content,
selectable: true,
shrinkWrap: true,
styleSheet: isTool
? _toolMarkdownStyleSheet(context)
: null,
)
else
Text(message.content),
],
),
),
),
);
}
String _buildToolMarkdown(String content) {
final lines = content.split("\n");
if (lines.isEmpty) {
return "```text\n\n```";
}
final title = lines.first.trim();
final body = lines.skip(1).join("\n").trimRight();
if (body.isEmpty) {
return title;
}
return "$title\n\n```text\n$body\n```";
}
MarkdownStyleSheet _toolMarkdownStyleSheet(BuildContext context) {
final theme = Theme.of(context);
return MarkdownStyleSheet(
p: theme.typography.p.copyWith(
fontSize: 13
),
);
}
}