import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import '../models/message.dart'; import 'code_block.dart'; import 'tool_use_card.dart'; import 'thinking_indicator.dart'; class MessageBubble extends StatelessWidget { final Message message; const MessageBubble({super.key, required this.message}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: message.isUser ? _buildUserBubble() : _buildAssistantBubble(), ); } Widget _buildUserBubble() { return Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 48), // Spacing for alignment Flexible( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade700, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), bottomLeft: Radius.circular(16), bottomRight: Radius.circular(4), ), ), child: Text( message.content, style: const TextStyle( color: Colors.white, fontSize: 15, ), ), ), ), const SizedBox(width: 8), CircleAvatar( radius: 16, backgroundColor: Colors.orange.shade800, child: const Icon(Icons.person, size: 18, color: Colors.white), ), ], ); } Widget _buildAssistantBubble() { return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 16, backgroundColor: const Color(0xFF2D2D2D), child: Icon(Icons.auto_awesome, size: 18, color: Colors.orange.shade400), ), const SizedBox(width: 8), Flexible( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF2D2D2D), borderRadius: const BorderRadius.only( topLeft: Radius.circular(4), topRight: Radius.circular(16), bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), ), child: _buildContent(), ), ), const SizedBox(width: 48), // Spacing for alignment ], ); } Widget _buildContent() { if (message.isThinking && message.content.isEmpty) { return const ThinkingIndicator(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Tool uses first if (message.toolUses != null && message.toolUses!.isNotEmpty) ...message.toolUses!.map((tool) => ToolUseCard(toolUse: tool)), // Then the text content with Markdown if (message.content.isNotEmpty) MarkdownBody( data: message.content, selectable: true, styleSheet: MarkdownStyleSheet( p: const TextStyle( color: Colors.white, fontSize: 15, height: 1.5, ), h1: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), h2: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), h3: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), code: TextStyle( color: Colors.orange.shade300, backgroundColor: Colors.black26, fontFamily: 'JetBrainsMono', fontSize: 13, ), codeblockDecoration: BoxDecoration( color: const Color(0xFF282C34), borderRadius: BorderRadius.circular(8), ), blockquote: const TextStyle( color: Colors.white70, fontStyle: FontStyle.italic, ), blockquoteDecoration: BoxDecoration( border: Border( left: BorderSide(color: Colors.orange.shade400, width: 3), ), ), listBullet: const TextStyle(color: Colors.white70), a: TextStyle(color: Colors.orange.shade300), ), builders: { 'code': _CodeBlockBuilder(), }, ), // Streaming indicator if (message.isStreaming && !message.isThinking) Padding( padding: const EdgeInsets.only(top: 8), child: SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.orange.shade400, ), ), ), ], ); } } class _CodeBlockBuilder extends MarkdownElementBuilder { @override Widget? visitElementAfter(element, preferredStyle) { // Check if it's a fenced code block final content = element.textContent; String? language; // Try to detect language from class attribute if (element.attributes.containsKey('class')) { final classes = element.attributes['class']!; if (classes.startsWith('language-')) { language = classes.substring(9); } } // For inline code, use default style if (!content.contains('\n') && content.length < 50) { return null; // Use default styling } return CodeBlock( code: content, language: language, ); } }