feat: enhance markdown image handling with customizable builder

- Introduced an `imageBuilderOverride` parameter in the `ConduitMarkdown` class to allow customization of how markdown images are rendered.
- Updated the `StreamingMarkdownWidget` to accept the new `imageBuilderOverride` parameter, enabling enhanced image handling in streaming contexts.
- Implemented an `imageBuilderOverride` in the `_AssistantMessageWidgetState` to utilize `EnhancedImageAttachment`, providing caching, authentication headers, and fullscreen viewing for markdown images.
- Refactored the `_ImageBuilder` class to support the new image building logic, improving flexibility and maintainability of image rendering in markdown content.
This commit is contained in:
cogwheel0
2025-10-10 16:12:31 +05:30
parent e73c5ee93a
commit 570fa26011
3 changed files with 28 additions and 6 deletions

View File

@@ -771,6 +771,16 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
content: processedContent, content: processedContent,
isStreaming: widget.isStreaming, isStreaming: widget.isStreaming,
onTapLink: (url, _) => _launchUri(url), onTapLink: (url, _) => _launchUri(url),
imageBuilderOverride: (uri, title, alt) {
// Route markdown images through the enhanced image widget so they
// get caching, auth headers, fullscreen viewer, and sharing.
return EnhancedImageAttachment(
attachmentId: uri.toString(),
isMarkdownFormat: true,
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 400),
disableAnimation: widget.isStreaming,
);
},
); );
final responseBuilder = ref.watch(assistantResponseBuilderProvider); final responseBuilder = ref.watch(assistantResponseBuilderProvider);

View File

@@ -29,6 +29,7 @@ class ConduitMarkdown {
bool selectable = true, bool selectable = true,
bool shrinkWrap = false, bool shrinkWrap = false,
ScrollPhysics? physics, ScrollPhysics? physics,
Widget Function(Uri uri, String? title, String? alt)? imageBuilderOverride,
}) { }) {
return MarkdownBody( return MarkdownBody(
data: data, data: data,
@@ -36,6 +37,11 @@ class ConduitMarkdown {
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
styleSheet: _buildStyleSheet(context), styleSheet: _buildStyleSheet(context),
builders: _buildCustomBuilders(context, onTapLink), builders: _buildCustomBuilders(context, onTapLink),
// Allow callers to override how markdown images render (e.g., to use
// EnhancedImageAttachment in assistant views). Fallback to default.
imageBuilder: (uri, title, alt) => imageBuilderOverride != null
? imageBuilderOverride(uri, title, alt)
: _ImageBuilder(context).buildFromUri(uri),
extensionSet: md.ExtensionSet.gitHubFlavored, extensionSet: md.ExtensionSet.gitHubFlavored,
onTapLink: onTapLink != null onTapLink: onTapLink != null
? (text, href, title) => onTapLink(href ?? '', title) ? (text, href, title) => onTapLink(href ?? '', title)
@@ -51,6 +57,7 @@ class ConduitMarkdown {
required String data, required String data,
MarkdownLinkTapCallback? onTapLink, MarkdownLinkTapCallback? onTapLink,
bool selectable = true, bool selectable = true,
Widget Function(Uri uri, String? title, String? alt)? imageBuilderOverride,
}) { }) {
return build( return build(
context: context, context: context,
@@ -58,6 +65,7 @@ class ConduitMarkdown {
onTapLink: onTapLink, onTapLink: onTapLink,
selectable: selectable, selectable: selectable,
shrinkWrap: true, shrinkWrap: true,
imageBuilderOverride: imageBuilderOverride,
); );
} }
@@ -141,7 +149,6 @@ class ConduitMarkdown {
) { ) {
return { return {
'code': _CodeBlockBuilder(context), 'code': _CodeBlockBuilder(context),
'img': _ImageBuilder(context),
'mermaid': _MermaidBuilder(context), 'mermaid': _MermaidBuilder(context),
'latex': _LatexBuilder(context), 'latex': _LatexBuilder(context),
'details': _DetailsBuilder(context), 'details': _DetailsBuilder(context),
@@ -345,22 +352,23 @@ class _ImageBuilder extends MarkdownElementBuilder {
@override @override
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
final theme = context.conduitTheme;
final url = element.attributes['src'] ?? ''; final url = element.attributes['src'] ?? '';
final uri = Uri.tryParse(url); final uri = Uri.tryParse(url);
if (uri == null) { if (uri == null) {
return _buildImageError(context, theme); return _buildImageError(context, context.conduitTheme);
} }
return buildFromUri(uri);
}
/// Public helper used by the Markdown `imageBuilder` callback.
Widget buildFromUri(Uri uri) {
final theme = context.conduitTheme;
if (uri.scheme == 'data') { if (uri.scheme == 'data') {
return _buildBase64Image(uri.toString(), context, theme); return _buildBase64Image(uri.toString(), context, theme);
} }
if (uri.scheme.isEmpty || uri.scheme == 'http' || uri.scheme == 'https') { if (uri.scheme.isEmpty || uri.scheme == 'http' || uri.scheme == 'https') {
return _buildNetworkImage(uri.toString(), context, theme); return _buildNetworkImage(uri.toString(), context, theme);
} }
return _buildImageError(context, theme); return _buildImageError(context, theme);
} }

View File

@@ -13,11 +13,14 @@ class StreamingMarkdownWidget extends StatelessWidget {
required this.content, required this.content,
required this.isStreaming, required this.isStreaming,
this.onTapLink, this.onTapLink,
this.imageBuilderOverride,
}); });
final String content; final String content;
final bool isStreaming; final bool isStreaming;
final MarkdownLinkTapCallback? onTapLink; final MarkdownLinkTapCallback? onTapLink;
final Widget Function(Uri uri, String? title, String? alt)?
imageBuilderOverride;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -34,6 +37,7 @@ class StreamingMarkdownWidget extends StatelessWidget {
data: data, data: data,
onTapLink: onTapLink, onTapLink: onTapLink,
selectable: false, selectable: false,
imageBuilderOverride: imageBuilderOverride,
); );
} }