feat: markdown rendering in assistant message bubbles
| .. | .. |
|---|
| 4 | 4 | |
|---|
| 5 | 5 | import 'package:flutter/material.dart'; |
|---|
| 6 | 6 | import 'package:flutter/services.dart'; |
|---|
| 7 | +import 'package:flutter_markdown/flutter_markdown.dart'; |
|---|
| 7 | 8 | import 'package:intl/intl.dart'; |
|---|
| 8 | 9 | |
|---|
| 9 | 10 | import '../models/message.dart'; |
|---|
| .. | .. |
|---|
| 105 | 106 | Widget _buildContent(BuildContext context) { |
|---|
| 106 | 107 | switch (message.type) { |
|---|
| 107 | 108 | case MessageType.text: |
|---|
| 108 | | - return SelectableText( |
|---|
| 109 | | - message.content, |
|---|
| 110 | | - style: TextStyle( |
|---|
| 111 | | - fontSize: 15, |
|---|
| 112 | | - color: _isUser ? Colors.white : null, |
|---|
| 113 | | - height: 1.4, |
|---|
| 114 | | - ), |
|---|
| 115 | | - ); |
|---|
| 109 | + if (_isUser) { |
|---|
| 110 | + return SelectableText( |
|---|
| 111 | + message.content, |
|---|
| 112 | + style: const TextStyle( |
|---|
| 113 | + fontSize: 15, |
|---|
| 114 | + color: Colors.white, |
|---|
| 115 | + height: 1.4, |
|---|
| 116 | + ), |
|---|
| 117 | + ); |
|---|
| 118 | + } |
|---|
| 119 | + return _buildMarkdown(context); |
|---|
| 116 | 120 | |
|---|
| 117 | 121 | case MessageType.voice: |
|---|
| 118 | 122 | return _buildVoiceContent(context); |
|---|
| .. | .. |
|---|
| 120 | 124 | case MessageType.image: |
|---|
| 121 | 125 | return _buildImageContent(context); |
|---|
| 122 | 126 | } |
|---|
| 127 | + } |
|---|
| 128 | + |
|---|
| 129 | + Widget _buildMarkdown(BuildContext context) { |
|---|
| 130 | + final isDark = Theme.of(context).brightness == Brightness.dark; |
|---|
| 131 | + final textColor = isDark ? Colors.white : Colors.black87; |
|---|
| 132 | + final codeBackground = isDark |
|---|
| 133 | + ? Colors.white.withAlpha(20) |
|---|
| 134 | + : Colors.black.withAlpha(15); |
|---|
| 135 | + |
|---|
| 136 | + return MarkdownBody( |
|---|
| 137 | + data: message.content, |
|---|
| 138 | + selectable: true, |
|---|
| 139 | + softLineBreak: true, |
|---|
| 140 | + styleSheet: MarkdownStyleSheet( |
|---|
| 141 | + p: TextStyle(fontSize: 15, height: 1.4, color: textColor), |
|---|
| 142 | + h1: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 143 | + h2: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 144 | + h3: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 145 | + strong: TextStyle(fontWeight: FontWeight.bold, color: textColor), |
|---|
| 146 | + em: TextStyle(fontStyle: FontStyle.italic, color: textColor), |
|---|
| 147 | + code: TextStyle( |
|---|
| 148 | + fontSize: 13, |
|---|
| 149 | + fontFamily: 'monospace', |
|---|
| 150 | + color: textColor, |
|---|
| 151 | + backgroundColor: codeBackground, |
|---|
| 152 | + ), |
|---|
| 153 | + codeblockDecoration: BoxDecoration( |
|---|
| 154 | + color: codeBackground, |
|---|
| 155 | + borderRadius: BorderRadius.circular(8), |
|---|
| 156 | + ), |
|---|
| 157 | + codeblockPadding: const EdgeInsets.all(10), |
|---|
| 158 | + listBullet: TextStyle(fontSize: 15, color: textColor), |
|---|
| 159 | + blockquoteDecoration: BoxDecoration( |
|---|
| 160 | + border: Border( |
|---|
| 161 | + left: BorderSide(color: AppColors.accent, width: 3), |
|---|
| 162 | + ), |
|---|
| 163 | + ), |
|---|
| 164 | + blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), |
|---|
| 165 | + ), |
|---|
| 166 | + onTapLink: (text, href, title) { |
|---|
| 167 | + if (href != null) { |
|---|
| 168 | + Clipboard.setData(ClipboardData(text: href)); |
|---|
| 169 | + ScaffoldMessenger.of(context).showSnackBar( |
|---|
| 170 | + SnackBar( |
|---|
| 171 | + content: Text('Link copied: $href'), |
|---|
| 172 | + duration: const Duration(seconds: 2), |
|---|
| 173 | + ), |
|---|
| 174 | + ); |
|---|
| 175 | + } |
|---|
| 176 | + }, |
|---|
| 177 | + ); |
|---|
| 123 | 178 | } |
|---|
| 124 | 179 | |
|---|
| 125 | 180 | Widget _buildVoiceContent(BuildContext context) { |
|---|
| .. | .. |
|---|
| 194 | 249 | // Transcript |
|---|
| 195 | 250 | if (message.content.isNotEmpty) ...[ |
|---|
| 196 | 251 | const SizedBox(height: 8), |
|---|
| 197 | | - SelectableText( |
|---|
| 198 | | - message.content, |
|---|
| 199 | | - style: TextStyle( |
|---|
| 200 | | - fontSize: 14, |
|---|
| 201 | | - color: _isUser ? Colors.white.withAlpha(220) : null, |
|---|
| 202 | | - height: 1.3, |
|---|
| 203 | | - ), |
|---|
| 204 | | - ), |
|---|
| 252 | + if (_isUser) |
|---|
| 253 | + SelectableText( |
|---|
| 254 | + message.content, |
|---|
| 255 | + style: TextStyle( |
|---|
| 256 | + fontSize: 14, |
|---|
| 257 | + color: Colors.white.withAlpha(220), |
|---|
| 258 | + height: 1.3, |
|---|
| 259 | + ), |
|---|
| 260 | + ) |
|---|
| 261 | + else |
|---|
| 262 | + _buildMarkdown(context), |
|---|
| 205 | 263 | ], |
|---|
| 206 | 264 | ], |
|---|
| 207 | 265 | ); |
|---|
| .. | .. |
|---|
| 254 | 254 | url: "https://pub.dev" |
|---|
| 255 | 255 | source: hosted |
|---|
| 256 | 256 | version: "6.0.0" |
|---|
| 257 | + flutter_markdown: |
|---|
| 258 | + dependency: "direct main" |
|---|
| 259 | + description: |
|---|
| 260 | + name: flutter_markdown |
|---|
| 261 | + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" |
|---|
| 262 | + url: "https://pub.dev" |
|---|
| 263 | + source: hosted |
|---|
| 264 | + version: "0.7.7+1" |
|---|
| 257 | 265 | flutter_plugin_android_lifecycle: |
|---|
| 258 | 266 | dependency: transitive |
|---|
| 259 | 267 | description: |
|---|
| .. | .. |
|---|
| 488 | 496 | url: "https://pub.dev" |
|---|
| 489 | 497 | source: hosted |
|---|
| 490 | 498 | version: "1.3.0" |
|---|
| 499 | + markdown: |
|---|
| 500 | + dependency: transitive |
|---|
| 501 | + description: |
|---|
| 502 | + name: markdown |
|---|
| 503 | + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 |
|---|
| 504 | + url: "https://pub.dev" |
|---|
| 505 | + source: hosted |
|---|
| 506 | + version: "7.3.1" |
|---|
| 491 | 507 | matcher: |
|---|
| 492 | 508 | dependency: transitive |
|---|
| 493 | 509 | description: |
|---|
| .. | .. |
|---|
| 30 | 30 | uuid: ^4.5.1 |
|---|
| 31 | 31 | collection: ^1.19.1 |
|---|
| 32 | 32 | file_picker: ^10.3.10 |
|---|
| 33 | + flutter_markdown: ^0.7.7+1 |
|---|
| 33 | 34 | |
|---|
| 34 | 35 | dev_dependencies: |
|---|
| 35 | 36 | flutter_test: |
|---|