The-Agency/lib/ui/widgets/chat/bubbles/user_bubble.dart

189 lines
5.3 KiB
Dart

import "dart:typed_data";
import "package:clawd_code/ui/widgets/common/button.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "package:flutter/services.dart";
import "../../../models/attachment.dart";
import "../attachment_preview.dart";
import "../../../../src/session/session_types.dart";
Color _msgColour(BuildContext context) {
final theme = Theme.of(context);
final dark = theme.brightness == Brightness.dark;
final h = HSLColor.fromColor(theme.colorScheme.border).hue;
return dark
? HSLColor.fromAHSL(1, h, 0.25, 0.25).toColor()
: HSLColor.fromAHSL(1, h, 0.30, 0.6).toColor();
}
class UserBubble extends StatefulWidget {
const UserBubble({
super.key,
required this.content,
this.attachments,
this.timestamp,
this.onRetry,
this.onEdit,
});
final String content;
final List<MessageAttachment>? attachments;
final DateTime? timestamp;
final VoidCallback? onRetry;
final VoidCallback? onEdit;
@override
State<UserBubble> createState() => _UserBubbleState();
}
class _UserBubbleState extends State<UserBubble> {
bool _hovered = false;
String _formatTime(DateTime dt) {
final local = dt.toLocal();
final now = DateTime.now();
final h = local.hour.toString().padLeft(2, "0");
final m = local.minute.toString().padLeft(2, "0");
final time = "$h:$m";
final isToday = local.year == now.year && local.month == now.month && local.day == now.day;
if (isToday) return "Today at $time";
// show date for anything older
final day = local.day.toString().padLeft(2, "0");
final month = local.month.toString().padLeft(2, "0");
return "${local.year == now.year ? "" : "${local.year}/"}$month/$day at $time";
}
@override
Widget build(BuildContext context) {
final atts = widget.attachments;
final muted = Theme.of(context).colorScheme.mutedForeground;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: 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: const EdgeInsets.only(left: 8),
child: AttachmentItem(
attachment: Attachment(
name: att.name,
mimeType: att.mimeType,
data: Uint8List.fromList(att.data),
),
onRemove: () {},
),
),
],
),
),
const Gap(6),
],
OutlinedContainer(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
borderColor: _msgColour(context).scaleAlpha(0.9),
backgroundColor: _msgColour(context),
boxShadow: [],
child: SelectableText(widget.content),
),
const Gap(6),
AnimatedOpacity(
opacity: _hovered ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.timestamp != null)
Text(
_formatTime(widget.timestamp!),
style: TextStyle(color: muted),
).xSmall,
const Gap(10),
_ActionBtn(
icon: LucideIcons.refreshCw,
tooltip: "Retry",
onTap: widget.onRetry,
color: muted,
),
_ActionBtn(
icon: LucideIcons.pencil,
tooltip: "Edit",
onTap: widget.onEdit,
color: muted,
),
_ActionBtn(
icon: LucideIcons.copy,
tooltip: "Copy",
color: muted,
onTap: () {
Clipboard.setData(ClipboardData(text: widget.content));
},
),
],
),
),
],
),
));
}
}
class _ActionBtn extends StatelessWidget {
const _ActionBtn({
required this.icon,
required this.tooltip,
required this.color,
this.onTap,
});
final IconData icon;
final String tooltip;
final Color color;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
if (true) {
return IconButton.ghost(
onPressed: () {
if (onTap != null) onTap!();
},
icon: Icon(
icon,
).iconSmall.iconMutedForeground,
);
}
return Tooltip(
tooltip: TooltipContainer(child: Text(tooltip)),
child: GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
child: Icon(icon, size: 14, color: color),
),
),
);
}
}