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:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user