217 lines
5.9 KiB
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
|
|
),
|
|
);
|
|
}
|
|
}
|