From 5b875009924f990baece93fa5e25134906f86c6d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Tue, 24 Mar 2026 10:02:20 +0100
Subject: [PATCH] feat: markdown rendering in assistant message bubbles
---
lib/widgets/message_bubble.dart | 90 +++++++++++++++++++++++++++++++++++++--------
pubspec.lock | 16 ++++++++
pubspec.yaml | 1
3 files changed, 91 insertions(+), 16 deletions(-)
diff --git a/lib/widgets/message_bubble.dart b/lib/widgets/message_bubble.dart
index c5df417..870ff55 100644
--- a/lib/widgets/message_bubble.dart
+++ b/lib/widgets/message_bubble.dart
@@ -4,6 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:intl/intl.dart';
import '../models/message.dart';
@@ -105,14 +106,17 @@
Widget _buildContent(BuildContext context) {
switch (message.type) {
case MessageType.text:
- return SelectableText(
- message.content,
- style: TextStyle(
- fontSize: 15,
- color: _isUser ? Colors.white : null,
- height: 1.4,
- ),
- );
+ if (_isUser) {
+ return SelectableText(
+ message.content,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Colors.white,
+ height: 1.4,
+ ),
+ );
+ }
+ return _buildMarkdown(context);
case MessageType.voice:
return _buildVoiceContent(context);
@@ -120,6 +124,57 @@
case MessageType.image:
return _buildImageContent(context);
}
+ }
+
+ Widget _buildMarkdown(BuildContext context) {
+ final isDark = Theme.of(context).brightness == Brightness.dark;
+ final textColor = isDark ? Colors.white : Colors.black87;
+ final codeBackground = isDark
+ ? Colors.white.withAlpha(20)
+ : Colors.black.withAlpha(15);
+
+ return MarkdownBody(
+ data: message.content,
+ selectable: true,
+ softLineBreak: true,
+ styleSheet: MarkdownStyleSheet(
+ p: TextStyle(fontSize: 15, height: 1.4, color: textColor),
+ h1: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor),
+ h2: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor),
+ h3: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor),
+ strong: TextStyle(fontWeight: FontWeight.bold, color: textColor),
+ em: TextStyle(fontStyle: FontStyle.italic, color: textColor),
+ code: TextStyle(
+ fontSize: 13,
+ fontFamily: 'monospace',
+ color: textColor,
+ backgroundColor: codeBackground,
+ ),
+ codeblockDecoration: BoxDecoration(
+ color: codeBackground,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ codeblockPadding: const EdgeInsets.all(10),
+ listBullet: TextStyle(fontSize: 15, color: textColor),
+ blockquoteDecoration: BoxDecoration(
+ border: Border(
+ left: BorderSide(color: AppColors.accent, width: 3),
+ ),
+ ),
+ blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
+ ),
+ onTapLink: (text, href, title) {
+ if (href != null) {
+ Clipboard.setData(ClipboardData(text: href));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Link copied: $href'),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ },
+ );
}
Widget _buildVoiceContent(BuildContext context) {
@@ -194,14 +249,17 @@
// Transcript
if (message.content.isNotEmpty) ...[
const SizedBox(height: 8),
- SelectableText(
- message.content,
- style: TextStyle(
- fontSize: 14,
- color: _isUser ? Colors.white.withAlpha(220) : null,
- height: 1.3,
- ),
- ),
+ if (_isUser)
+ SelectableText(
+ message.content,
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.white.withAlpha(220),
+ height: 1.3,
+ ),
+ )
+ else
+ _buildMarkdown(context),
],
],
);
diff --git a/pubspec.lock b/pubspec.lock
index c246b00..ec7686d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -254,6 +254,14 @@
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_markdown:
+ dependency: "direct main"
+ description:
+ name: flutter_markdown
+ sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.7+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -488,6 +496,14 @@
url: "https://pub.dev"
source: hosted
version: "1.3.0"
+ markdown:
+ dependency: transitive
+ description:
+ name: markdown
+ sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.3.1"
matcher:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a892e29..f4da55f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -30,6 +30,7 @@
uuid: ^4.5.1
collection: ^1.19.1
file_picker: ^10.3.10
+ flutter_markdown: ^0.7.7+1
dev_dependencies:
flutter_test:
--
Gitblit v1.3.1