From 485320fdf447e13f02b6f18e5342cf5d0e6b44b9 Mon Sep 17 00:00:00 2001 From: Maxfield Kouzel Date: Tue, 2 Jun 2026 16:46:45 -0400 Subject: [PATCH] feat: add renderMarkdown toggle to MessageBubble Adds a toggle for whether user message text should be rendered as markdown or as plain text. Previously always rendered as Markdown. Rendering as plain text is useful when rendering logs of old conversations. J=APV-939 TEST=auto,manual Added unit tests. Modified ChatPanel locally to use renderMarkdown=false, spun up test-site, and confirmed a Markdown link was rendered as plain text. --- docs/chat-ui-react.md | 2 +- docs/chat-ui-react.messagebubble.md | 4 +- docs/chat-ui-react.messagebubbleprops.md | 1 + ...react.messagebubbleprops.rendermarkdown.md | 13 +++++ etc/chat-ui-react.api.md | 3 +- src/components/MessageBubble.tsx | 24 ++++++--- tests/components/MessageBubble.test.tsx | 50 +++++++++++++++++++ 7 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 docs/chat-ui-react.messagebubbleprops.rendermarkdown.md diff --git a/docs/chat-ui-react.md b/docs/chat-ui-react.md index 32cc808..eb19aca 100644 --- a/docs/chat-ui-react.md +++ b/docs/chat-ui-react.md @@ -12,7 +12,7 @@ | [ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, })](./chat-ui-react.chatinput.md) | A component that allows user to input message and send to Chat API. | | [ChatPanel(props)](./chat-ui-react.chatpanel.md) | A component that renders a full panel for chat bot interactions. This includes the message bubbles for the conversation, input box with send button, and header (if provided). | | [ChatPopUp(props)](./chat-ui-react.chatpopup.md) | A component that renders a popup button that displays and hides a panel for chat bot interactions. | -| [MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, })](./chat-ui-react.messagebubble.md) | A component that displays the provided message. | +| [MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, renderMarkdown, })](./chat-ui-react.messagebubble.md) | A component that displays the provided message. | | [useComposedCssClasses(builtInClasses, customClasses)](./chat-ui-react.usecomposedcssclasses.md) | useComposedCssClasses merges a component's built-in tailwind classes with custom tailwind classes. | | [useReportAnalyticsEvent()](./chat-ui-react.usereportanalyticsevent.md) | Returns a function to send requests to Yext Analytics API. | diff --git a/docs/chat-ui-react.messagebubble.md b/docs/chat-ui-react.messagebubble.md index da23d33..b4646d4 100644 --- a/docs/chat-ui-react.messagebubble.md +++ b/docs/chat-ui-react.messagebubble.md @@ -9,14 +9,14 @@ A component that displays the provided message. **Signature:** ```typescript -export declare function MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, }: MessageBubbleProps): React.JSX.Element; +export declare function MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, renderMarkdown, }: MessageBubbleProps): React.JSX.Element; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, } | [MessageBubbleProps](./chat-ui-react.messagebubbleprops.md) | | +| { message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, renderMarkdown, } | [MessageBubbleProps](./chat-ui-react.messagebubbleprops.md) | | **Returns:** diff --git a/docs/chat-ui-react.messagebubbleprops.md b/docs/chat-ui-react.messagebubbleprops.md index b5d93c1..e130a3f 100644 --- a/docs/chat-ui-react.messagebubbleprops.md +++ b/docs/chat-ui-react.messagebubbleprops.md @@ -21,6 +21,7 @@ export interface MessageBubbleProps | [linkTarget?](./chat-ui-react.messagebubbleprops.linktarget.md) | | string | _(Optional)_ Link target open behavior on click. | | [message](./chat-ui-react.messagebubbleprops.message.md) | | Message | The message to display. | | [onLinkClick?](./chat-ui-react.messagebubbleprops.onlinkclick.md) | | (href?: string) => void | _(Optional)_ A callback which is called when user clicks a link. | +| [renderMarkdown?](./chat-ui-react.messagebubbleprops.rendermarkdown.md) | | boolean | _(Optional)_ Whether to render the message text as markdown. Defaults to true. | | [showFeedbackButtons?](./chat-ui-react.messagebubbleprops.showfeedbackbuttons.md) | | boolean | _(Optional)_ Whether to show the feedback buttons on the message bubble. Defaults to true. | | [showTimestamp?](./chat-ui-react.messagebubbleprops.showtimestamp.md) | | boolean | _(Optional)_ Whether to show the timestamp of the message with the message bubble. Defaults to true. | diff --git a/docs/chat-ui-react.messagebubbleprops.rendermarkdown.md b/docs/chat-ui-react.messagebubbleprops.rendermarkdown.md new file mode 100644 index 0000000..95c41eb --- /dev/null +++ b/docs/chat-ui-react.messagebubbleprops.rendermarkdown.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [MessageBubbleProps](./chat-ui-react.messagebubbleprops.md) > [renderMarkdown](./chat-ui-react.messagebubbleprops.rendermarkdown.md) + +## MessageBubbleProps.renderMarkdown property + +Whether to render the message text as markdown. Defaults to true. + +**Signature:** + +```typescript +renderMarkdown?: boolean; +``` diff --git a/etc/chat-ui-react.api.md b/etc/chat-ui-react.api.md index 99612bc..fe9b106 100644 --- a/etc/chat-ui-react.api.md +++ b/etc/chat-ui-react.api.md @@ -178,7 +178,7 @@ export interface InitialMessagePopUpCssClasses { } // @public -export function MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, }: MessageBubbleProps): React_2.JSX.Element; +export function MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, linkTarget, onLinkClick, renderMarkdown, }: MessageBubbleProps): React_2.JSX.Element; // @public export interface MessageBubbleCssClasses { @@ -219,6 +219,7 @@ export interface MessageBubbleProps { linkTarget?: string; message: Message; onLinkClick?: (href?: string) => void; + renderMarkdown?: boolean; showFeedbackButtons?: boolean; showTimestamp?: boolean; } diff --git a/src/components/MessageBubble.tsx b/src/components/MessageBubble.tsx index 87e6597..638f853 100644 --- a/src/components/MessageBubble.tsx +++ b/src/components/MessageBubble.tsx @@ -79,6 +79,11 @@ export interface MessageBubbleProps { linkTarget?: string; /** A callback which is called when user clicks a link. */ onLinkClick?: (href?: string) => void; + /** + * Whether to render the message text as markdown. + * Defaults to true. + */ + renderMarkdown?: boolean; } /** @@ -96,6 +101,7 @@ export function MessageBubble({ formatTimestamp = defaultFormatTimestamp, linkTarget, onLinkClick, + renderMarkdown = true, }: MessageBubbleProps) { const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses); const bubbleCssClasses = twMerge( @@ -140,13 +146,17 @@ export function MessageBubble({ responseId={message.responseId} /> )} - + {renderMarkdown ? ( + + ) : ( +
{message.text}
+ )} {/* fallback on empty space here to perserve the height for timestamp div */} {showTimestamp && ( diff --git a/tests/components/MessageBubble.test.tsx b/tests/components/MessageBubble.test.tsx index d2f6370..3326f57 100644 --- a/tests/components/MessageBubble.test.tsx +++ b/tests/components/MessageBubble.test.tsx @@ -79,3 +79,53 @@ it("applies link target setting (parent)", async () => { expect(screen.getByText("msg link")).toHaveAttribute("target", "_parent"); }); + +it("renders markdown by default", () => { + const linkMessage: Message = { + text: "Test [msg link](msglink)", + timestamp: "2023-06-01T15:26:55.362Z", + source: MessageSource.USER, + }; + + render( + + + + ); + + expect(screen.getByText("msg link")).toHaveAttribute("href", "msglink"); +}); + +it("renders raw message text when renderMarkdown is false", () => { + const linkMessage: Message = { + text: "Test [msg link](msglink)", + timestamp: "2023-06-01T15:26:55.362Z", + source: MessageSource.USER, + }; + + render( + + + + ); + + expect(screen.getByText(linkMessage.text)).toBeInTheDocument(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); +}); + +it("escapes html when renderMarkdown is false", () => { + const htmlMessage: Message = { + text: "Test", + timestamp: "2023-06-01T15:26:55.362Z", + source: MessageSource.USER, + }; + + render( + + + + ); + + expect(screen.getByText(htmlMessage.text)).toBeInTheDocument(); + expect(screen.queryByText("Test")).not.toBeInTheDocument(); +});