diff --git a/packages/flutter_chat_core/lib/src/utils/typedefs.dart b/packages/flutter_chat_core/lib/src/utils/typedefs.dart index aa8b4c6c6..9d44140af 100644 --- a/packages/flutter_chat_core/lib/src/utils/typedefs.dart +++ b/packages/flutter_chat_core/lib/src/utils/typedefs.dart @@ -36,3 +36,13 @@ typedef ChatItem = int? messageGroupingTimeoutInSeconds, bool? isRemoved, }); + +/// Builder signature for rendering streamed Markdown content. +/// Used by widgets like `FlyerChatTextStreamMessage` to customize Markdown rendering. +typedef GptMarkdownBuilder = + Widget Function( + BuildContext context, + String text, + TextStyle? paragraphStyle, + void Function(String url, String title)? onLinkTap, + ); diff --git a/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart b/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart index 9b3d417e1..cecc8a067 100644 --- a/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart +++ b/packages/flyer_chat_text_message/lib/src/flyer_chat_text_message.dart @@ -73,6 +73,10 @@ class FlyerChatTextMessage extends StatelessWidget { /// The callback function to handle link clicks. final void Function(String url, String title)? onLinkTap; + /// Optional builder to customize how Markdown is rendered. + /// If provided, it will be used instead of the default [GptMarkdown] widget. + final GptMarkdownBuilder? gptMarkdownBuilder; + /// The position of the link preview widget relative to the text. /// If set to [LinkPreviewPosition.none], the link preview widget will not be displayed. /// A [LinkPreviewBuilder] must be provided for the preview to be displayed. @@ -102,6 +106,7 @@ class FlyerChatTextMessage extends StatelessWidget { this.timeAndStatusPosition = TimeAndStatusPosition.end, this.timeAndStatusPositionInlineInsets = const EdgeInsets.only(bottom: 2), this.onLinkTap, + this.gptMarkdownBuilder, this.linkPreviewPosition = LinkPreviewPosition.bottom, this.topWidget, }); @@ -137,20 +142,31 @@ class FlyerChatTextMessage extends StatelessWidget { ) : null; - final textContent = GptMarkdownTheme( - gptThemeData: GptMarkdownTheme.of(context).copyWith( - linkColor: isSentByMe ? sentLinksColor : receivedLinksColor, - linkHoverColor: isSentByMe ? sentLinksColor : receivedLinksColor, - ), - child: GptMarkdown( - message.text, - style: - _isOnlyEmoji - ? paragraphStyle?.copyWith(fontSize: onlyEmojiFontSize) - : paragraphStyle, - onLinkTap: onLinkTap, - ), - ); + final effectiveParagraphStyle = + _isOnlyEmoji + ? paragraphStyle?.copyWith(fontSize: onlyEmojiFontSize) + : paragraphStyle; + + final textContent = + gptMarkdownBuilder != null + ? gptMarkdownBuilder!( + context, + message.text, + effectiveParagraphStyle, + onLinkTap, + ) + : GptMarkdownTheme( + gptThemeData: GptMarkdownTheme.of(context).copyWith( + linkColor: isSentByMe ? sentLinksColor : receivedLinksColor, + linkHoverColor: + isSentByMe ? sentLinksColor : receivedLinksColor, + ), + child: GptMarkdown( + message.text, + style: effectiveParagraphStyle, + onLinkTap: onLinkTap, + ), + ); final linkPreviewWidget = linkPreviewPosition != LinkPreviewPosition.none diff --git a/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart b/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart index deab4d099..26fc53510 100644 --- a/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart +++ b/packages/flyer_chat_text_stream_message/lib/src/flyer_chat_text_stream_message.dart @@ -92,6 +92,10 @@ class FlyerChatTextStreamMessage extends StatefulWidget { /// The callback function to handle link clicks. final void Function(String url, String title)? onLinkTap; + /// Optional builder to customize how Markdown is rendered. + /// If provided, it will be used instead of the default [GptMarkdown] widget. + final GptMarkdownBuilder? gptMarkdownBuilder; + /// The text to display while in the loading state. Defaults to "Thinking". final String loadingText; @@ -128,6 +132,7 @@ class FlyerChatTextStreamMessage extends StatefulWidget { this.chunkAnimationDuration = const Duration(milliseconds: 350), this.mode = TextStreamMessageMode.animatedOpacity, this.onLinkTap, + this.gptMarkdownBuilder, this.loadingText = 'Thinking', this.shimmerBaseColor, this.shimmerHighlightColor, @@ -371,6 +376,15 @@ class _FlyerChatTextStreamMessageState extends State if (widget.streamState is StreamStateCompleted) { final state = widget.streamState as StreamStateCompleted; + final builder = widget.gptMarkdownBuilder; + if (builder != null) { + return builder( + context, + state.finalText, + paragraphStyle, + widget.onLinkTap, + ); + } return GptMarkdown( state.finalText, style: paragraphStyle, @@ -392,6 +406,15 @@ class _FlyerChatTextStreamMessageState extends State if (widget.mode == TextStreamMessageMode.instantMarkdown) { final combinedText = _segments.map((s) => s.text).join(''); + final builder = widget.gptMarkdownBuilder; + if (builder != null) { + return builder( + context, + combinedText, + paragraphStyle, + widget.onLinkTap, + ); + } return GptMarkdown(combinedText, style: paragraphStyle); } else { return RichText(