Matthias Nott
2026-03-24 5b875009924f990baece93fa5e25134906f86c6d
lib/widgets/message_bubble.dart
....@@ -4,6 +4,7 @@
44
55 import 'package:flutter/material.dart';
66 import 'package:flutter/services.dart';
7
+import 'package:flutter_markdown/flutter_markdown.dart';
78 import 'package:intl/intl.dart';
89
910 import '../models/message.dart';
....@@ -105,14 +106,17 @@
105106 Widget _buildContent(BuildContext context) {
106107 switch (message.type) {
107108 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);
116120
117121 case MessageType.voice:
118122 return _buildVoiceContent(context);
....@@ -120,6 +124,57 @@
120124 case MessageType.image:
121125 return _buildImageContent(context);
122126 }
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
+ );
123178 }
124179
125180 Widget _buildVoiceContent(BuildContext context) {
....@@ -194,14 +249,17 @@
194249 // Transcript
195250 if (message.content.isNotEmpty) ...[
196251 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),
205263 ],
206264 ],
207265 );