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? attachments; final DateTime? timestamp; final VoidCallback? onRetry; final VoidCallback? onEdit; @override State createState() => _UserBubbleState(); } class _UserBubbleState extends State { 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), ), ), ); } }