feat(mobile): render assistant messages as markdown (#1405)

* feat(mobile): render assistant messages as markdown, keep user text plain

Add flutter_markdown dependency and conditionally render chat bubbles:
- User messages use plain Text to avoid formatting markdown characters
- Assistant messages use MarkdownBody with styled headings, bold, italic,
  lists and code blocks matching the existing color scheme
- Bump Dart SDK constraint to >=3.3.0 to satisfy flutter_markdown 0.7.2

* fix(mobile): address markdown rendering review comments

- Extract MarkdownStyleSheet into _markdownStyle() helper to avoid
  rebuilding TextStyles on every message render
- Replace deprecated imageBuilder with sizedImageBuilder; block http/https
  image URIs to prevent unsolicited remote fetches from AI-generated content
- Commit updated pubspec.lock with flutter_markdown 0.7.2 resolved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix tests

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
Tristan Katana
2026-04-09 00:52:40 +03:00
committed by GitHub
parent a30e9b75a7
commit c9f4e8d3d8
3 changed files with 63 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import '../models/chat.dart';
import '../providers/auth_provider.dart';
@@ -414,6 +415,22 @@ class _MessageBubble extends StatelessWidget {
required this.formatTime,
});
/// Builds the markdown stylesheet once per render context instead of inline,
/// avoiding redundant TextStyle allocations per message bubble.
MarkdownStyleSheet _markdownStyle(BuildContext context) {
final color = Theme.of(context).colorScheme.onSurfaceVariant;
return MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
p: TextStyle(color: color),
strong: TextStyle(color: color, fontWeight: FontWeight.bold),
em: TextStyle(color: color, fontStyle: FontStyle.italic),
listBullet: TextStyle(color: color),
h1: TextStyle(color: color, fontSize: 20, fontWeight: FontWeight.bold),
h2: TextStyle(color: color, fontSize: 18, fontWeight: FontWeight.bold),
h3: TextStyle(color: color, fontSize: 16, fontWeight: FontWeight.bold),
code: TextStyle(color: color, fontFamily: 'monospace'),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -455,14 +472,27 @@ class _MessageBubble extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.content,
style: TextStyle(
color: isUser
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant,
if (isUser)
Text(
message.content,
style: TextStyle(
color: colorScheme.onPrimary,
),
)
else
MarkdownBody(
data: message.content,
selectable: false,
softLineBreak: true,
styleSheet: _markdownStyle(context),
sizedImageBuilder: (config) {
// Block remote images to prevent unsolicited network requests.
if (config.uri.scheme == 'http' || config.uri.scheme == 'https') {
return const SizedBox.shrink();
}
return Image.asset(config.uri.toString());
},
),
),
if (message.toolCalls != null &&
message.toolCalls!.isNotEmpty)
Padding(

View File

@@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -198,6 +198,14 @@ packages:
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_secure_storage:
dependency: "direct main"
description:
@@ -344,22 +352,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
source: hosted
version: "7.3.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -657,10 +673,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.9"
typed_data:
dependency: transitive
description:

View File

@@ -4,7 +4,7 @@ publish_to: 'none'
version: 0.6.9+20260402
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.27.0'
dependencies:
@@ -24,6 +24,7 @@ dependencies:
url_launcher: ^6.2.5
flutter_svg: ^2.2.0
package_info_plus: ^8.0.0
flutter_markdown: ^0.7.2
dev_dependencies:
flutter_test: