import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/views/widgets/sound/sound_player.dart'; import 'package:url_launcher/url_launcher.dart'; import '../model/copilot_message_model.dart'; class CopilotMessageBubble extends StatelessWidget { final CopilotMessage message; final bool showHeader; const CopilotMessageBubble({Key? key, required this.message, this.showHeader = true}) : super(key: key); @override Widget build(BuildContext context) { final isUser = message.role == CopilotRole.user; return isUser ? _senderCard(context) : _recipientCard(context); } static const Widget _avatar = SizedBox( height: 26, width: 26, child: DecoratedBox(decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey)), ); String get _timeText => (message.timestamp?.toIso8601String() ?? DateTime.now().toIso8601String()).chatMsgTime; Widget _senderCard(BuildContext context) { final senderHeader = Row( mainAxisSize: MainAxisSize.min, children: [ Text( "Me", overflow: TextOverflow.ellipsis, maxLines: 1, style: AppTextStyles.bodyText.copyWith(color: AppColor.neutral50, fontWeight: FontWeight.w600), ), 8.width, _avatar, ], ); return Align( alignment: Alignment.centerRight, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (showHeader) ...[senderHeader, 4.height] else 8.height, Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( fit: FlexFit.loose, child: Container( padding: const EdgeInsets.all(8), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75), decoration: BoxDecoration( color: AppColor.white50, borderRadius: BorderRadius.circular(6), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (message.localAudioPath != null) ASoundPlayer(audio: message.localAudioPath!) else Text( message.content, style: AppTextStyles.bodyText2.copyWith(color: AppColor.textColor(context)), ), Text( _timeText, style: AppTextStyles.bodyText2.copyWith(color: AppColor.textColor(context)), ), ], ), ), ), (26 + 8).width, ], ), ], ), ); } Widget _recipientCard(BuildContext context) { final showLoadingDots = message.isStreaming && message.content.isEmpty; final recipientHeader = Row( mainAxisSize: MainAxisSize.min, children: [ _avatar, 8.width, Text( "Copilot", overflow: TextOverflow.ellipsis, maxLines: 1, style: AppTextStyles.bodyText.copyWith(color: AppColor.neutral50, fontWeight: FontWeight.w600), ), ], ); return Align( alignment: Alignment.centerLeft, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showHeader) ...[recipientHeader, 4.height] else 8.height, Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(left: 26 + 8, right: 26 + 8), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75), decoration: BoxDecoration( color: AppColor.background(context), borderRadius: BorderRadius.circular(6), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (showLoadingDots) const Align( alignment: Alignment.centerLeft, child: SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2, color: AppColor.primary10), ), ) else _buildMessageContent(context), // if (message.content.isNotEmpty && message.images.isNotEmpty) 8.height, // if (message.content.isNotEmpty) // Text( // message.content, // style: AppTextStyles.bodyText2.copyWith( // color: message.isError ? Colors.red : AppColor.white10, // ), // ), if (message.images.isNotEmpty) ...[ 8.height, SizedBox( height: 90, width: double.infinity, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: message.images.length, separatorBuilder: (_, __) => 8.width, itemBuilder: (context, index) { final img = message.images[index]; return Image.memory( base64Decode(img.base64), width: 100, height: 100, fit: BoxFit.contain, ); }, ), ), ], Align( alignment: Alignment.centerRight, widthFactor: 1, child: Text( _timeText, style: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), ), ), ], ), ), ], ), ); } Widget _buildMessageContent(BuildContext context) { var content = message.content; content = content.replaceAllMapped( RegExp(r'(\*\*[^*\n]+:\*\*)'), (match) => '${match.group(1)} \n', ); content = content.replaceAllMapped( RegExp(r'(Link:)\s*', caseSensitive: false), (match) => '${match.group(1)} \n', ); return MarkdownBody( data: content, selectable: true, onTapLink: (text, href, title) async { if (href == null || href.isEmpty) return; final uri = Uri.tryParse(href); if (uri == null) return; await launchUrl( uri, mode: LaunchMode.externalApplication, ); }, styleSheet: MarkdownStyleSheet( p: AppTextStyles.bodyText2.copyWith( color: message.isError ? Colors.red : AppColor.textStyleColor(context), ), strong: AppTextStyles.bodyText2.copyWith( color: message.isError ? Colors.red : AppColor.textStyleColor(context), fontWeight: FontWeight.bold, ), listBullet: AppTextStyles.bodyText2.copyWith( color: message.isError ? Colors.red : AppColor.textStyleColor(context), ), a: AppTextStyles.bodyText2.copyWith( color: AppColor.primary10, decoration: TextDecoration.underline, ), ), ); } }