From c063ba6aa937790e82fa16a83e2b1975e8546581 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 12 Nov 2025 09:15:27 -0500 Subject: [PATCH 01/15] feat(Calls/Response/Thinking): added support for Markdown --- .../module/src/DeepThinking/DeepThinking.tsx | 49 +++- .../src/MarkdownContent/MarkdownContent.tsx | 240 ++++++++++++++++++ packages/module/src/MarkdownContent/index.ts | 2 + packages/module/src/Message/Message.tsx | 202 ++------------- packages/module/src/ToolCall/ToolCall.tsx | 31 ++- .../module/src/ToolResponse/ToolResponse.tsx | 79 +++++- packages/module/src/index.ts | 3 + 7 files changed, 412 insertions(+), 194 deletions(-) create mode 100644 packages/module/src/MarkdownContent/MarkdownContent.tsx create mode 100644 packages/module/src/MarkdownContent/index.ts diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index 11c29549d..c8df44262 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -10,6 +10,8 @@ import { ExpandableSectionProps } from '@patternfly/react-core'; import { useState, type FunctionComponent } from 'react'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface DeepThinkingProps { /** Toggle content shown for expandable section */ @@ -26,6 +28,14 @@ export interface DeepThinkingProps { cardProps?: CardProps; /** Additional props passed to main card body */ cardBodyProps?: CardBodyProps; + /** Whether to enable markdown rendering for toggleContent. When true and toggleContent is a string, it will be parsed as markdown. */ + isToggleContentMarkdown?: boolean; + /** Whether to enable markdown rendering for subheading. When true, subheading will be parsed as markdown. */ + isSubheadingMarkdown?: boolean; + /** Whether to enable markdown rendering for body. When true and body is a string, it will be parsed as markdown. */ + isBodyMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; } export const DeepThinking: FunctionComponent = ({ @@ -35,7 +45,11 @@ export const DeepThinking: FunctionComponent = ({ subheading, toggleContent, isDefaultExpanded = true, - cardBodyProps + cardBodyProps, + isToggleContentMarkdown, + isSubheadingMarkdown, + isBodyMarkdown, + markdownContentProps }: DeepThinkingProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -43,11 +57,38 @@ export const DeepThinking: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderToggleContent = () => { + if (isToggleContentMarkdown && typeof toggleContent === 'string') { + return ; + } + return toggleContent; + }; + + const renderSubheading = () => { + if (!subheading) { + return null; + } + if (isSubheadingMarkdown) { + return ; + } + return subheading; + }; + + const renderBody = () => { + if (!body) { + return null; + } + if (isBodyMarkdown && typeof body === 'string') { + return ; + } + return body; + }; + return ( = ({
{subheading && (
- {subheading} + {renderSubheading()}
)} - {body &&
{body}
} + {body &&
{renderBody()}
}
diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx new file mode 100644 index 000000000..c6c846134 --- /dev/null +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -0,0 +1,240 @@ +// ============================================================================ +// Markdown Content - Shared component for rendering markdown +// With aid from Jean-Claude Van Code +// ============================================================================ +import { type FunctionComponent, ReactNode } from 'react'; +import Markdown, { Options } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { ContentVariants } from '@patternfly/react-core'; +import CodeBlockMessage, { CodeBlockMessageProps } from '../Message/CodeBlockMessage/CodeBlockMessage'; +import TextMessage from '../Message/TextMessage/TextMessage'; +import ListItemMessage from '../Message/ListMessage/ListItemMessage'; +import UnorderedListMessage from '../Message/ListMessage/UnorderedListMessage'; +import OrderedListMessage from '../Message/ListMessage/OrderedListMessage'; +import TableMessage from '../Message/TableMessage/TableMessage'; +import TrMessage from '../Message/TableMessage/TrMessage'; +import TdMessage from '../Message/TableMessage/TdMessage'; +import TbodyMessage from '../Message/TableMessage/TbodyMessage'; +import TheadMessage from '../Message/TableMessage/TheadMessage'; +import ThMessage from '../Message/TableMessage/ThMessage'; +import { TableProps } from '@patternfly/react-table'; +import ImageMessage from '../Message/ImageMessage/ImageMessage'; +import rehypeUnwrapImages from 'rehype-unwrap-images'; +import rehypeExternalLinks from 'rehype-external-links'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeHighlight from 'rehype-highlight'; +import 'highlight.js/styles/vs2015.css'; +import { PluggableList } from 'unified'; +import LinkMessage from '../Message/LinkMessage/LinkMessage'; +import { rehypeMoveImagesOutOfParagraphs } from '../Message/Plugins/rehypeMoveImagesOutOfParagraphs'; +import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage'; +import { ButtonProps } from '@patternfly/react-core'; + +export interface MarkdownContentProps { + /** The markdown content to render */ + content?: string; + /** Disables markdown parsing, allowing only text input */ + isMarkdownDisabled?: boolean; + /** Props for code blocks */ + codeBlockProps?: CodeBlockMessageProps; + /** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */ + tableProps?: Required> & TableProps; + /** Additional rehype plugins passed from the consumer */ + additionalRehypePlugins?: PluggableList; + /** Additional remark plugins passed from the consumer */ + additionalRemarkPlugins?: PluggableList; + /** Whether to open links in message in new tab. */ + openLinkInNewTab?: boolean; + /** Props for links */ + linkProps?: ButtonProps; + /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */ + reactMarkdownProps?: Options; + /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */ + remarkGfmProps?: Options; + /** Whether to strip out images in markdown */ + hasNoImages?: boolean; + /** Sets background colors to be appropriate on primary chatbot background */ + isPrimary?: boolean; + /** Custom component to render when markdown is disabled */ + textComponent?: ReactNode; +} + +export const MarkdownContent: FunctionComponent = ({ + content, + isMarkdownDisabled, + codeBlockProps, + tableProps, + openLinkInNewTab = true, + additionalRehypePlugins = [], + additionalRemarkPlugins = [], + linkProps, + reactMarkdownProps, + remarkGfmProps, + hasNoImages = false, + isPrimary, + textComponent +}: MarkdownContentProps) => { + let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; + if (openLinkInNewTab) { + rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]); + } + if (additionalRehypePlugins) { + rehypePlugins.push(...additionalRehypePlugins); + } + + const disallowedElements = hasNoImages ? ['img'] : []; + if (reactMarkdownProps && reactMarkdownProps.disallowedElements) { + disallowedElements.push(...reactMarkdownProps.disallowedElements); + } + + if (isMarkdownDisabled) { + if (textComponent) { + return <>{textComponent}; + } + return ( + + {content} + + ); + } + + return ( + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return
; + }, + p: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + code: ({ children, ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...codeProps } = props; + return ( + + {children} + + ); + }, + h1: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h2: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h3: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h4: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h5: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h6: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + blockquote: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + ul: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + ol: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + li: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + // table requires node attribute for calculating headers for mobile breakpoint + table: (props) => , + tbody: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + thead: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + tr: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + td: (props) => { + // Conflicts with Td type + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, width, ...rest } = props; + return ; + }, + th: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + img: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + a: (props) => { + // node is just the details of the document structure - not needed + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + // some a types conflict with ButtonProps, but it's ok because we are using an a tag + // there are too many to handle manually + + {props.children} + + ); + }, + // used for footnotes + sup: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + } + }} + remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} + rehypePlugins={rehypePlugins} + {...reactMarkdownProps} + remarkRehypeOptions={{ + // removes sr-only class from footnote labels applied by default + footnoteLabelProperties: { className: [''] }, + ...reactMarkdownProps?.remarkRehypeOptions + }} + disallowedElements={disallowedElements} + > + {content} + + ); +}; + +export default MarkdownContent; diff --git a/packages/module/src/MarkdownContent/index.ts b/packages/module/src/MarkdownContent/index.ts new file mode 100644 index 000000000..8269e6e03 --- /dev/null +++ b/packages/module/src/MarkdownContent/index.ts @@ -0,0 +1,2 @@ +export { default } from './MarkdownContent'; +export * from './MarkdownContent'; diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index c487c59b2..8b298c325 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -3,14 +3,12 @@ // ============================================================================ import { forwardRef, ReactNode, useEffect, useState } from 'react'; import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react'; -import Markdown, { Options } from 'react-markdown'; -import remarkGfm from 'remark-gfm'; +import { Options } from 'react-markdown'; import { AlertProps, Avatar, AvatarProps, ButtonProps, - ContentVariants, FormProps, Label, LabelGroupProps, @@ -18,42 +16,25 @@ import { Truncate } from '@patternfly/react-core'; import MessageLoading from './MessageLoading'; -import CodeBlockMessage, { CodeBlockMessageProps } from './CodeBlockMessage/CodeBlockMessage'; -import TextMessage from './TextMessage/TextMessage'; +import { CodeBlockMessageProps } from './CodeBlockMessage/CodeBlockMessage'; import FileDetailsLabel from '../FileDetailsLabel/FileDetailsLabel'; import ResponseActions, { ActionProps } from '../ResponseActions/ResponseActions'; import SourcesCard, { SourcesCardProps } from '../SourcesCard'; -import ListItemMessage from './ListMessage/ListItemMessage'; -import UnorderedListMessage from './ListMessage/UnorderedListMessage'; -import OrderedListMessage from './ListMessage/OrderedListMessage'; import QuickStartTile from './QuickStarts/QuickStartTile'; import { QuickStart, QuickstartAction } from './QuickStarts/types'; import QuickResponse from './QuickResponse/QuickResponse'; import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback'; import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete'; -import TableMessage from './TableMessage/TableMessage'; -import TrMessage from './TableMessage/TrMessage'; -import TdMessage from './TableMessage/TdMessage'; -import TbodyMessage from './TableMessage/TbodyMessage'; -import TheadMessage from './TableMessage/TheadMessage'; -import ThMessage from './TableMessage/ThMessage'; import { TableProps } from '@patternfly/react-table'; -import ImageMessage from './ImageMessage/ImageMessage'; -import rehypeUnwrapImages from 'rehype-unwrap-images'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSanitize from 'rehype-sanitize'; -import rehypeHighlight from 'rehype-highlight'; // see the full list of styles here: https://highlightjs.org/examples import 'highlight.js/styles/vs2015.css'; import { PluggableList } from 'unified'; -import LinkMessage from './LinkMessage/LinkMessage'; import ErrorMessage from './ErrorMessage/ErrorMessage'; import MessageInput from './MessageInput'; -import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs'; import ToolResponse, { ToolResponseProps } from '../ToolResponse'; import DeepThinking, { DeepThinkingProps } from '../DeepThinking'; -import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage'; import ToolCall, { ToolCallProps } from '../ToolCall'; +import MarkdownContent from '../MarkdownContent'; export interface MessageAttachment { /** Name of file attached to the message */ @@ -267,14 +248,8 @@ export const MessageBase: FunctionComponent = ({ }, [content]); const { beforeMainContent, afterMainContent, endContent } = extraContent || {}; - let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; - if (openLinkInNewTab) { - rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]); - } - if (additionalRehypePlugins) { - rehypePlugins.push(...additionalRehypePlugins); - } - let avatarClassName; + + let avatarClassName: string | undefined; if (avatarProps && 'className' in avatarProps) { const { className, ...rest } = avatarProps; avatarClassName = className; @@ -284,157 +259,22 @@ export const MessageBase: FunctionComponent = ({ const date = new Date(); const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; - const disallowedElements = role === 'user' && hasNoImagesInUserMessages ? ['img'] : []; - if (reactMarkdownProps && reactMarkdownProps.disallowedElements) { - disallowedElements.push(...reactMarkdownProps.disallowedElements); - } - - const handleMarkdown = () => { - if (isMarkdownDisabled) { - return ( - - {messageText} - - ); - } - return ( - { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return
; - }, - p: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - code: ({ children, ...props }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...codeProps } = props; - return ( - - {children} - - ); - }, - h1: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h2: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h3: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h4: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h5: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h6: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - blockquote: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - ul: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - ol: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - li: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - // table requires node attribute for calculating headers for mobile breakpoint - table: (props) => , - tbody: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - thead: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - tr: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - td: (props) => { - // Conflicts with Td type - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, width, ...rest } = props; - return ; - }, - th: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - img: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - a: (props) => { - // node is just the details of the document structure - not needed - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ( - // some a types conflict with ButtonProps, but it's ok because we are using an a tag - // there are too many to handle manually - - {props.children} - - ); - }, - // used for footnotes - sup: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - } - }} - remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} - rehypePlugins={rehypePlugins} - {...reactMarkdownProps} - remarkRehypeOptions={{ - // removes sr-only class from footnote labels applied by default - footnoteLabelProperties: { className: [''] }, - ...reactMarkdownProps?.remarkRehypeOptions - }} - disallowedElements={disallowedElements} - > - {messageText} - - ); - }; + const handleMarkdown = () => ( + + ); const renderMessage = () => { if (isLoading) { diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index 774984813..13da6e2ed 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -19,6 +19,8 @@ import { Spinner, SpinnerProps } from '@patternfly/react-core'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface ToolCallProps { /** Title text for the tool call. */ @@ -61,6 +63,12 @@ export interface ToolCallProps { cardFooterProps?: CardFooterProps; /** Additional props for the expandable section when expandableContent is passed. */ expandableSectionProps?: Omit; + /** Whether to enable markdown rendering for titleText. When true, titleText will be parsed as markdown. */ + isTitleMarkdown?: boolean; + /** Whether to enable markdown rendering for expandableContent. When true and expandableContent is a string, it will be parsed as markdown. */ + isExpandableContentMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; } export const ToolCall: FunctionComponent = ({ @@ -83,7 +91,10 @@ export const ToolCall: FunctionComponent = ({ cardBodyProps, cardFooterProps, expandableSectionProps, - spinnerProps + spinnerProps, + isTitleMarkdown, + isExpandableContentMarkdown, + markdownContentProps }: ToolCallProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -91,6 +102,13 @@ export const ToolCall: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderTitle = () => { + if (isTitleMarkdown) { + return ; + } + return titleText; + }; + const titleContent = ( {isLoading ? ( @@ -99,10 +117,17 @@ export const ToolCall: FunctionComponent = ({ {{loadingText}} ) : ( - {titleText} + {renderTitle()} )} ); + + const renderExpandableContent = () => { + if (isExpandableContentMarkdown && typeof expandableContent === 'string') { + return ; + } + return expandableContent; + }; const defaultActions = ( <> @@ -138,7 +163,7 @@ export const ToolCall: FunctionComponent = ({ isIndented {...expandableSectionProps} > - {expandableContent} + {renderExpandableContent()} ) : ( titleContent diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index 39bfdb269..84e5eb6fe 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -14,6 +14,8 @@ import { ExpandableSectionProps } from '@patternfly/react-core'; import { useState, type FunctionComponent } from 'react'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface ToolResponseProps { /** Toggle content shown for expandable section */ @@ -42,6 +44,18 @@ export interface ToolResponseProps { toolResponseCardDividerProps?: DividerProps; /** Additional props passed to tool response card title */ toolResponseCardTitleProps?: CardTitleProps; + /** Whether to enable markdown rendering for toggleContent. When true and toggleContent is a string, it will be parsed as markdown. */ + isToggleContentMarkdown?: boolean; + /** Whether to enable markdown rendering for subheading. When true, subheading will be parsed as markdown. */ + isSubheadingMarkdown?: boolean; + /** Whether to enable markdown rendering for body. When true and body is a string, it will be parsed as markdown. */ + isBodyMarkdown?: boolean; + /** Whether to enable markdown rendering for cardBody. When true and cardBody is a string, it will be parsed as markdown. */ + isCardBodyMarkdown?: boolean; + /** Whether to enable markdown rendering for cardTitle. When true and cardTitle is a string, it will be parsed as markdown. */ + isCardTitleMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; } export const ToolResponse: FunctionComponent = ({ @@ -57,7 +71,13 @@ export const ToolResponse: FunctionComponent = ({ toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, - toolResponseCardTitleProps + toolResponseCardTitleProps, + isToggleContentMarkdown, + isSubheadingMarkdown, + isBodyMarkdown, + isCardBodyMarkdown, + isCardTitleMarkdown, + markdownContentProps }: ToolResponseProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -65,11 +85,58 @@ export const ToolResponse: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderToggleContent = () => { + if (isToggleContentMarkdown && typeof toggleContent === 'string') { + return ; + } + return toggleContent; + }; + + const renderSubheading = () => { + if (!subheading) { + return null; + } + if (isSubheadingMarkdown) { + return ; + } + return subheading; + }; + + const renderBody = () => { + if (!body) { + return null; + } + if (isBodyMarkdown && typeof body === 'string') { + return ; + } + return body; + }; + + const renderCardTitle = () => { + if (!cardTitle) { + return null; + } + if (isCardTitleMarkdown && typeof cardTitle === 'string') { + return ; + } + return cardTitle; + }; + + const renderCardBody = () => { + if (!cardBody) { + return null; + } + if (isCardBodyMarkdown && typeof cardBody === 'string') { + return ; + } + return cardBody; + }; + return ( = ({
{subheading && (
- {subheading} + {renderSubheading()}
)} - {body &&
{body}
} + {body &&
{renderBody()}
} {(cardTitle || cardBody) && ( - {cardTitle && {cardTitle}} + {cardTitle && {renderCardTitle()}} {cardTitle && cardBody && } - {cardBody && {cardBody}} + {cardBody && {renderCardBody()}} )}
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 757c148bd..c692ab34d 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -63,6 +63,9 @@ export * from './ImagePreview'; export { default as LoadingMessage } from './LoadingMessage'; export * from './LoadingMessage'; +export { default as MarkdownContent } from './MarkdownContent'; +export * from './MarkdownContent'; + export { default as Message } from './Message'; export * from './Message'; From 91da7e41da15d3add4738d81b57e799d466e62a0 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 13 Nov 2025 13:12:41 -0500 Subject: [PATCH 02/15] Added logic to retain styles --- .../src/MarkdownContent/MarkdownContent.tsx | 56 ++++++++++++++----- .../CodeBlockMessage/CodeBlockMessage.scss | 4 ++ .../CodeBlockMessage/CodeBlockMessage.tsx | 8 ++- .../src/Message/LinkMessage/LinkMessage.scss | 5 ++ .../src/Message/LinkMessage/LinkMessage.tsx | 26 ++++++++- .../src/Message/ListMessage/ListMessage.scss | 8 +++ .../ListMessage/OrderedListMessage.tsx | 30 +++++++--- .../ListMessage/UnorderedListMessage.tsx | 22 ++++++-- .../Message/TableMessage/TableMessage.scss | 11 ++++ .../src/Message/TableMessage/TableMessage.tsx | 20 ++++++- .../src/Message/TextMessage/TextMessage.tsx | 49 +++++++++++++--- .../module/src/ToolResponse/ToolResponse.scss | 17 ++++++ .../module/src/ToolResponse/ToolResponse.tsx | 10 ++-- packages/module/src/main.scss | 1 + 14 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 packages/module/src/Message/LinkMessage/LinkMessage.scss diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index c6c846134..730a08fa8 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -29,6 +29,7 @@ import LinkMessage from '../Message/LinkMessage/LinkMessage'; import { rehypeMoveImagesOutOfParagraphs } from '../Message/Plugins/rehypeMoveImagesOutOfParagraphs'; import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage'; import { ButtonProps } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; export interface MarkdownContentProps { /** The markdown content to render */ @@ -57,6 +58,8 @@ export interface MarkdownContentProps { isPrimary?: boolean; /** Custom component to render when markdown is disabled */ textComponent?: ReactNode; + /** Flag indicating whether content should retain various styles of its context (typically font-size and text color). */ + shouldRetainStyles?: boolean; } export const MarkdownContent: FunctionComponent = ({ @@ -72,7 +75,8 @@ export const MarkdownContent: FunctionComponent = ({ remarkGfmProps, hasNoImages = false, isPrimary, - textComponent + textComponent, + shouldRetainStyles }: MarkdownContentProps) => { let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; if (openLinkInNewTab) { @@ -104,18 +108,36 @@ export const MarkdownContent: FunctionComponent = ({ section: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return
; + return ( +
+ ); }, p: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ( + + ); }, code: ({ children, ...props }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...codeProps } = props; return ( - + {children} ); @@ -123,47 +145,49 @@ export const MarkdownContent: FunctionComponent = ({ h1: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, h2: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, h3: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, h4: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, h5: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, h6: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, blockquote: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ( + + ); }, ul: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, ol: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; - return ; + return ; }, li: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -171,7 +195,9 @@ export const MarkdownContent: FunctionComponent = ({ return ; }, // table requires node attribute for calculating headers for mobile breakpoint - table: (props) => , + table: (props) => ( + + ), tbody: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; @@ -210,7 +236,7 @@ export const MarkdownContent: FunctionComponent = ({ return ( // some a types conflict with ButtonProps, but it's ok because we are using an a tag // there are too many to handle manually - + {props.children} ); diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss index 1c7a905eb..78c24423b 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss @@ -75,6 +75,10 @@ overflow: hidden !important; } } + + .pf-m-markdown > .pf-v6-c-code-block__code { + font-size: inherit; + } } .pf-chatbot__message-inline-code { diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx index 7eb295bd3..144d840de 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx @@ -19,6 +19,7 @@ import { import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon'; import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon'; +import { css } from '@patternfly/react-styles'; export interface CodeBlockMessageProps { /** Content rendered in code block */ @@ -41,6 +42,8 @@ export interface CodeBlockMessageProps { customActions?: React.ReactNode; /** Sets background colors to be appropriate on primary chatbot background */ isPrimary?: boolean; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } const DEFAULT_EXPANDED_TEXT = 'Show less'; @@ -57,6 +60,7 @@ const CodeBlockMessage = ({ collapsedText = DEFAULT_COLLAPSED_TEXT, customActions, isPrimary, + shouldRetainStyles, ...props }: CodeBlockMessageProps) => { const [copied, setCopied] = useState(false); @@ -138,9 +142,9 @@ const CodeBlockMessage = ({ ); return ( -
+
- + <> {isExpandable ? ( { +export interface LinkMessageProps { + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} + +const LinkMessage = ({ + children, + target, + href, + id, + shouldRetainStyles, + ...props +}: LinkMessageProps & ButtonProps & ExtraProps) => { if (target === '_blank') { return ( @@ -28,7 +42,15 @@ const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & Ext return ( // need to explicitly call this out or id doesn't seem to get passed - required for footnotes - ); diff --git a/packages/module/src/Message/ListMessage/ListMessage.scss b/packages/module/src/Message/ListMessage/ListMessage.scss index 0dfb7fcd5..f3dad3947 100644 --- a/packages/module/src/Message/ListMessage/ListMessage.scss +++ b/packages/module/src/Message/ListMessage/ListMessage.scss @@ -13,6 +13,14 @@ li { font-size: var(--pf-t--global--font--size--md); } + + &.pf-m-markdown { + .pf-v6-c-list, + ul, + li { + font-size: inherit; + } + } } .pf-chatbot__message--user { diff --git a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx index cc5fbff23..1b4547a3d 100644 --- a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx @@ -4,13 +4,29 @@ import { ExtraProps } from 'react-markdown'; import { List, ListComponent, OrderType } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; -const OrderedListMessage = ({ children, start }: JSX.IntrinsicElements['ol'] & ExtraProps) => ( -
- - {children} - -
-); +export interface OrderedListMessageProps { + /** The ordered list content */ + children?: React.ReactNode; + /** The number to start the ordered list at. */ + start?: number; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} + +const OrderedListMessage = ({ + children, + start, + shouldRetainStyles +}: OrderedListMessageProps & JSX.IntrinsicElements['ol'] & ExtraProps) => { + return ( +
+ + {children} + +
+ ); +}; export default OrderedListMessage; diff --git a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx index b30875cf2..1488eb5fe 100644 --- a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx @@ -4,11 +4,23 @@ import { ExtraProps } from 'react-markdown'; import { List } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; +export interface UnrderedListMessageProps { + /** The ordered list content */ + children?: React.ReactNode; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} -const UnorderedListMessage = ({ children }: JSX.IntrinsicElements['ul'] & ExtraProps) => ( -
- {children} -
-); +const UnorderedListMessage = ({ + children, + shouldRetainStyles +}: UnrderedListMessageProps & JSX.IntrinsicElements['ul'] & ExtraProps) => { + return ( +
+ {children} +
+ ); +}; export default UnorderedListMessage; diff --git a/packages/module/src/Message/TableMessage/TableMessage.scss b/packages/module/src/Message/TableMessage/TableMessage.scss index 16a4fba0f..b53a5e66f 100644 --- a/packages/module/src/Message/TableMessage/TableMessage.scss +++ b/packages/module/src/Message/TableMessage/TableMessage.scss @@ -25,4 +25,15 @@ .pf-v6-c-table__tr:last-of-type { border-block-end: 0; } + + &.pf-m-markdown { + table, + tbody, + td, + thead, + th, + tr { + font-size: inherit; + } + } } diff --git a/packages/module/src/Message/TableMessage/TableMessage.tsx b/packages/module/src/Message/TableMessage/TableMessage.tsx index befda02c4..251fe0d1e 100644 --- a/packages/module/src/Message/TableMessage/TableMessage.tsx +++ b/packages/module/src/Message/TableMessage/TableMessage.tsx @@ -5,6 +5,7 @@ import { Children, cloneElement } from 'react'; import { ExtraProps } from 'react-markdown'; import { Table, TableProps } from '@patternfly/react-table'; +import { css } from '@patternfly/react-styles'; interface Properties { line: number; @@ -20,10 +21,20 @@ export interface TableNode { } export interface TableMessageProps { + /** Content of the table */ + children?: React.ReactNode; + /** Flag indicating whether primary styles should be applied. */ isPrimary?: boolean; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } -const TableMessage = ({ children, isPrimary, ...props }: Omit & ExtraProps & TableMessageProps) => { +const TableMessage = ({ + children, + isPrimary, + shouldRetainStyles, + ...props +}: Omit & ExtraProps & TableMessageProps) => { const { className, ...rest } = props; // This allows us to parse the nested data we get back from the 3rd party Markdown parser @@ -76,7 +87,12 @@ const TableMessage = ({ children, isPrimary, ...props }: Omit {modifyChildren(children)} diff --git a/packages/module/src/Message/TextMessage/TextMessage.tsx b/packages/module/src/Message/TextMessage/TextMessage.tsx index 6b8a23fa5..683eae524 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.tsx +++ b/packages/module/src/Message/TextMessage/TextMessage.tsx @@ -4,22 +4,57 @@ import { ExtraProps } from 'react-markdown'; import { Content, ContentProps } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; export interface TextMessageProps { + /** The text message content */ + children?: React.ReactNode; + /** Flag indicating whether primary styling is applied. */ isPrimary?: boolean; + /** The wrapper component to use for the PatternFly Content component. Defaults to a div. */ + component?: + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'h5' + | 'h6' + | 'p' + | 'a' + | 'small' + | 'blockquote' + | 'pre' + | 'hr' + | 'ul' + | 'ol' + | 'dl' + | 'li' + | 'dt' + | 'dd'; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } const TextMessage = ({ component, children, isPrimary, + shouldRetainStyles, ...props -}: Omit & ExtraProps & TextMessageProps) => ( - - - {children} - - -); +}: Omit & ExtraProps & TextMessageProps) => { + const Wrapper = (shouldRetainStyles && component) || 'div'; + + return ( + + + {children} + + + ); +}; export default TextMessage; diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index 3d8a5b547..c5e186625 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -5,6 +5,11 @@ .pf-chatbot__tool-response-expandable-section { --pf-v6-c-expandable-section--Gap: var(--pf-t--global--spacer--xs); + + .pf-m-markdown { + font-size: inherit; + color: inherit; + } } .pf-chatbot__tool-response-section { @@ -17,11 +22,19 @@ font-size: var(--pf-t--global--font--size--body--sm); font-weight: var(--pf-t--global--font--weight--body--default); color: var(--pf-t--global--text--color--subtle); + + .pf-m-markdown { + font-size: var(--pf-t--global--font--size--body--sm); + } } .pf-chatbot__tool-response-body { color: var(--pf-t--global--text--color--subtle); margin-block-end: var(--pf-t--global--spacer--xs); + + .pf-m-markdown { + font-size: inherit; + } } .pf-chatbot__tool-response-card { @@ -33,4 +46,8 @@ .pf-v6-c-divider { --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default); } + + .pf-m-markdown { + font-size: inherit; + } } diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index 84e5eb6fe..d720ec5f6 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -87,7 +87,7 @@ export const ToolResponse: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ; } return toggleContent; }; @@ -97,7 +97,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isSubheadingMarkdown) { - return ; + return ; } return subheading; }; @@ -107,7 +107,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isBodyMarkdown && typeof body === 'string') { - return ; + return ; } return body; }; @@ -117,7 +117,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isCardTitleMarkdown && typeof cardTitle === 'string') { - return ; + return ; } return cardTitle; }; @@ -127,7 +127,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isCardBodyMarkdown && typeof cardBody === 'string') { - return ; + return ; } return cardBody; }; diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss index 42701d28d..e68020366 100644 --- a/packages/module/src/main.scss +++ b/packages/module/src/main.scss @@ -20,6 +20,7 @@ @import './Message/Message'; @import './Message/CodeBlockMessage/CodeBlockMessage'; @import './Message/ImageMessage/ImageMessage'; +@import './Message/LinkMessage/LinkMessage'; @import './Message/TextMessage/TextMessage'; @import './Message/ListMessage/ListMessage'; @import './Message/TableMessage/TableMessage'; From ff663cc7156877f3cf3c0b77b2409f82dc14a4cc Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 13 Nov 2025 13:18:46 -0500 Subject: [PATCH 03/15] Fixed lint errors --- .../ListMessage/OrderedListMessage.tsx | 16 ++++++-------- .../ListMessage/UnorderedListMessage.tsx | 12 +++++----- .../src/Message/TextMessage/TextMessage.tsx | 22 ++++++------------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx index 1b4547a3d..0bdd8e9f4 100644 --- a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx @@ -19,14 +19,12 @@ const OrderedListMessage = ({ children, start, shouldRetainStyles -}: OrderedListMessageProps & JSX.IntrinsicElements['ol'] & ExtraProps) => { - return ( -
- - {children} - -
- ); -}; +}: OrderedListMessageProps & JSX.IntrinsicElements['ol'] & ExtraProps) => ( +
+ + {children} + +
+); export default OrderedListMessage; diff --git a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx index 1488eb5fe..a51b2f17f 100644 --- a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx @@ -15,12 +15,10 @@ export interface UnrderedListMessageProps { const UnorderedListMessage = ({ children, shouldRetainStyles -}: UnrderedListMessageProps & JSX.IntrinsicElements['ul'] & ExtraProps) => { - return ( -
- {children} -
- ); -}; +}: UnrderedListMessageProps & JSX.IntrinsicElements['ul'] & ExtraProps) => ( +
+ {children} +
+); export default UnorderedListMessage; diff --git a/packages/module/src/Message/TextMessage/TextMessage.tsx b/packages/module/src/Message/TextMessage/TextMessage.tsx index 683eae524..ecbb39da3 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.tsx +++ b/packages/module/src/Message/TextMessage/TextMessage.tsx @@ -41,20 +41,12 @@ const TextMessage = ({ isPrimary, shouldRetainStyles, ...props -}: Omit & ExtraProps & TextMessageProps) => { - const Wrapper = (shouldRetainStyles && component) || 'div'; - - return ( - - - {children} - - - ); -}; +}: Omit & ExtraProps & TextMessageProps) => ( + + + {children} + + +); export default TextMessage; From b63aa366c53e66844f3ba7f2abe459105a2a2444 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 14 Nov 2025 09:22:25 -0500 Subject: [PATCH 04/15] Added example and updated styling --- .../MessageWithMarkdownToolResponse.tsx | 310 ++++++++++++++++++ .../chatbot/examples/Messages/Messages.md | 6 + .../src/MarkdownContent/MarkdownContent.tsx | 1 - .../CodeBlockMessage/CodeBlockMessage.scss | 2 +- .../CodeBlockMessage/CodeBlockMessage.tsx | 4 +- .../src/Message/TextMessage/TextMessage.scss | 11 + .../src/Message/TextMessage/TextMessage.tsx | 4 +- .../module/src/ToolResponse/ToolResponse.scss | 26 +- 8 files changed, 345 insertions(+), 19 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx new file mode 100644 index 000000000..bb32208ff --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx @@ -0,0 +1,310 @@ +import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; +import { CopyIcon, WrenchIcon } from '@patternfly/react-icons'; +import { + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, + ExpandableSectionVariant, + Flex, + FlexItem, + Label +} from '@patternfly/react-core'; +export const MessageWithToolResponseExample = () => { + const [isExpanded, setIsExpanded] = useState(false); + const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + const comprehensiveMarkdownBody = `Here's a comprehensive markdown example with various formatting options: + +# h1 Heading + +## h2 Heading + +### h3 Heading + +#### h4 Heading + +##### h5 Heading + +###### h6 Heading + +## Text Emphasis + +**Bold text, formatted with double asterisks** + +__Bold text, formatted with double underscores__ + +*Italic text, formatted with single asterisks* + +_Italic text, formatted with single underscores_ + +~~Strikethrough~~ + +## Inline Code + +Here is an inline code example - \`() => void\` + +## Code Blocks + +Here is some YAML code: + +~~~yaml +apiVersion: helm.openshift.io/v1beta1/ +kind: HelmChartRepository +metadata: + name: azure-sample-repo0oooo00ooo +spec: + connectionConfig: + url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs +~~~ + +Here is some JavaScript code: + +~~~js +const MessageLoading = () => ( +
+ + Loading message + +
+); + +export default MessageLoading; +~~~ + +## Block Quotes + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs (>) right next to each other... +> > > ...or with spaces between each sign. + +## Lists + +### Ordered List + +1. Item 1 +2. Item 2 +3. Item 3 + +### Unordered List + +* Item 1 +* Item 2 +* Item 3 + +### More Complex List + +You may be wondering whether you can display more complex lists with formatting. In response to your question, I will explain how to spread butter on toast. + +1. **Using a \`toaster\`:** + + - Place \`bread\` in a \`toaster\` + - Once \`bread\` is lightly browned, remove from \`toaster\` + +2. **Using a \`knife\`:** + + - Acquire 1 tablespoon of room temperature \`butter\`. Use \`knife\` to spread butter on \`toast\`. Bon appétit! + +## Links + +A paragraph with a URL: https://reactjs.org. + +## Tables + +To customize your table, you can use [PatternFly TableProps](/components/table#table) + +| Version | GA date | User role +|-|-|-| +| 2.5 | September 30, 2024 | Administrator | +| 2.5 | June 27, 2023 | Editor | +| 3.0 | April 1, 2025 | Administrator + +## Images + +![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif) + +## Footnotes + +This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] + +[^1]: This is a short footnote. To return the highlight to the original message, click the arrow. + +[^bignote]: This is a long footnote with multiple paragraphs and formatting. + + To break long footnotes into paragraphs, indent the text. + + Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. + + > You can even include blockquotes in footnotes! +`; + return ( + Thought for 3 seconds', + body: comprehensiveMarkdownBody, + isBodyMarkdown: true, + cardTitle: ( + + + + + + + + + toolName + + + + + Execution time: + 0.12 seconds + + + + + + + + + ), + cardBody: ( + <> + + + Parameters + + + Optional description text for parameters. + + + + + + + + + + + + + + + + + + + + + Response + + + Descriptive text about the tool response, including completion status, details on the data that was + processed, or anything else relevant to the use case. + + + + + + ) + }} + /> + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 101a8ff73..1369be3f0 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -221,6 +221,12 @@ If you are using [model context protocol (MCP)](https://www.redhat.com/en/blog/m ``` +### Messages with Markdown in tool response + +```ts file="./MessageWithMarkdownToolResponse.tsx" + +``` + ### Messages with deep thinking You can share details about the "thought process" behind an LLM's response, also known as deep thinking. To display a customizable, expandable card with these details, pass `deepThinking` to `` and provide a subheading (optional) and content body. diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index 730a08fa8..8a1da4df3 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -136,7 +136,6 @@ export const MarkdownContent: FunctionComponent = ({ {...codeBlockProps} isPrimary={isPrimary} shouldRetainStyles={shouldRetainStyles} - // className={css('pf-m-markdown', codeBlockProps?.className)} > {children} diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss index 78c24423b..d62bf567b 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss @@ -76,7 +76,7 @@ } } - .pf-m-markdown > .pf-v6-c-code-block__code { + &.pf-m-markdown .pf-v6-c-code-block__code { font-size: inherit; } } diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx index 144d840de..632e20c42 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx @@ -142,9 +142,9 @@ const CodeBlockMessage = ({ ); return ( -
+
- + <> {isExpandable ? ( [class^='pf-v6-c-content'] { + font-size: inherit; + } + + // &.pf-m-markdown + & { + // margin-top: 10px; + // } } // ============================================================================ diff --git a/packages/module/src/Message/TextMessage/TextMessage.tsx b/packages/module/src/Message/TextMessage/TextMessage.tsx index ecbb39da3..8ed20ef9b 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.tsx +++ b/packages/module/src/Message/TextMessage/TextMessage.tsx @@ -42,8 +42,8 @@ const TextMessage = ({ shouldRetainStyles, ...props }: Omit & ExtraProps & TextMessageProps) => ( - - + + {children} diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index c5e186625..b5d5d04ed 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -6,10 +6,10 @@ .pf-chatbot__tool-response-expandable-section { --pf-v6-c-expandable-section--Gap: var(--pf-t--global--spacer--xs); - .pf-m-markdown { - font-size: inherit; - color: inherit; - } + // .pf-m-markdown { + // font-size: inherit; + // color: inherit; + // } } .pf-chatbot__tool-response-section { @@ -23,18 +23,18 @@ font-weight: var(--pf-t--global--font--weight--body--default); color: var(--pf-t--global--text--color--subtle); - .pf-m-markdown { - font-size: var(--pf-t--global--font--size--body--sm); - } + // .pf-m-markdown { + // font-size: var(--pf-t--global--font--size--body--sm); + // } } .pf-chatbot__tool-response-body { color: var(--pf-t--global--text--color--subtle); margin-block-end: var(--pf-t--global--spacer--xs); - .pf-m-markdown { - font-size: inherit; - } + // .pf-m-markdown { + // font-size: inherit; + // } } .pf-chatbot__tool-response-card { @@ -47,7 +47,7 @@ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default); } - .pf-m-markdown { - font-size: inherit; - } + // .pf-m-markdown { + // font-size: inherit; + // } } From e7941115c1fa46e7620d2fb791fddb2ddc7379ac Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 14 Nov 2025 09:28:29 -0500 Subject: [PATCH 05/15] Fixed lint error --- .../examples/Messages/MessageWithMarkdownToolResponse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx index bb32208ff..4f45411df 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx @@ -14,7 +14,7 @@ import { FlexItem, Label } from '@patternfly/react-core'; -export const MessageWithToolResponseExample = () => { +export const MessageWithToolResponseExample: FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { setIsExpanded(isExpanded); From 7d1eca08fee5d9d037eeee55a474e90784f6f874 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 18 Nov 2025 14:29:33 -0500 Subject: [PATCH 06/15] Retained styling for ToolCall and DeepThinking --- packages/module/src/DeepThinking/DeepThinking.tsx | 6 +++--- packages/module/src/ToolCall/ToolCall.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index c8df44262..df91027bd 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -59,7 +59,7 @@ export const DeepThinking: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ; } return toggleContent; }; @@ -69,7 +69,7 @@ export const DeepThinking: FunctionComponent = ({ return null; } if (isSubheadingMarkdown) { - return ; + return ; } return subheading; }; @@ -79,7 +79,7 @@ export const DeepThinking: FunctionComponent = ({ return null; } if (isBodyMarkdown && typeof body === 'string') { - return ; + return ; } return body; }; diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index 13da6e2ed..e2012a61d 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -104,7 +104,7 @@ export const ToolCall: FunctionComponent = ({ const renderTitle = () => { if (isTitleMarkdown) { - return ; + return ; } return titleText; }; @@ -124,7 +124,7 @@ export const ToolCall: FunctionComponent = ({ const renderExpandableContent = () => { if (isExpandableContentMarkdown && typeof expandableContent === 'string') { - return ; + return ; } return expandableContent; }; From 3236a9e15e8028e9771066cec5a6ff289f0c8e16 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 18 Nov 2025 15:17:07 -0500 Subject: [PATCH 07/15] Added tests --- .../src/DeepThinking/DeepThinking.test.tsx | 48 +++++ .../MarkdownContent/MarkdownContent.test.tsx | 202 ++++++++++++++++++ .../module/src/ToolCall/ToolCall.test.tsx | 40 ++++ .../src/ToolResponse/ToolResponse.test.tsx | 75 +++++++ 4 files changed, 365 insertions(+) create mode 100644 packages/module/src/MarkdownContent/MarkdownContent.test.tsx diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx index 1621e4e02..7c0e0666b 100644 --- a/packages/module/src/DeepThinking/DeepThinking.test.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -119,4 +119,52 @@ describe('DeepThinking', () => { expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); expect(screen.getByText('Thinking content')).not.toBeVisible(); }); + + it('should render toggleContent as markdown when isToggleContentMarkdown is true', () => { + const toggleContent = '**Bold thinking**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold thinking')).toBeTruthy(); + }); + + it('should not render toggleContent as markdown when isToggleContentMarkdown is false', () => { + const toggleContent = '**Bold thinking**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeFalsy(); + expect(screen.getByText('**Bold thinking**')).toBeTruthy(); + }); + + it('should render subheading as markdown when isSubheadingMarkdown is true', () => { + const subheading = '**Bold subheading**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold subheading')).toBeTruthy(); + }); + + it('should not render subheading as markdown when isSubheadingMarkdown is false', () => { + const subheading = '**Bold subheading**'; + render(); + expect(screen.getByText('**Bold subheading**')).toBeTruthy(); + }); + + it('should render body as markdown when isBodyMarkdown is true', () => { + const body = '**Bold body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold body')).toBeTruthy(); + }); + + it('should not render body as markdown when isBodyMarkdown is false', () => { + const body = '**Bold body**'; + render(); + expect(screen.getByText('**Bold body**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const body = '**Bold body**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); diff --git a/packages/module/src/MarkdownContent/MarkdownContent.test.tsx b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx new file mode 100644 index 000000000..0ebd4ac6d --- /dev/null +++ b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx @@ -0,0 +1,202 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MarkdownContent from './MarkdownContent'; +import rehypeExternalLinks from '../__mocks__/rehype-external-links'; + +const BOLD_TEXT = '**Bold text**'; +const ITALIC_TEXT = '*Italic text*'; +const INLINE_CODE = 'Here is inline code: `const x = 5`'; +const CODE_BLOCK = `\`\`\`javascript +function hello() { + console.log('Hello, world!'); +} +\`\`\``; +const HEADING = '# Heading 1'; +const LINK = '[PatternFly](https://www.patternfly.org/)'; +const UNORDERED_LIST = ` +* Item 1 +* Item 2 +* Item 3 +`; +const ORDERED_LIST = ` +1. First item +2. Second item +3. Third item +`; +const TABLE = ` +| Column 1 | Column 2 | +|----------|----------| +| Cell 1 | Cell 2 | +| Cell 3 | Cell 4 | +`; +const BLOCKQUOTE = '> This is a blockquote'; +const IMAGE = '![Alt text](https://example.com/image.png)'; + +describe('MarkdownContent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render bold text correctly', () => { + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold text')).toBeTruthy(); + }); + + it('should render italic text correctly', () => { + const { container } = render(); + expect(container.querySelector('em')).toBeTruthy(); + expect(screen.getByText('Italic text')).toBeTruthy(); + }); + + it('should render inline code correctly', () => { + render(); + expect(screen.getByText(/const x = 5/)).toBeTruthy(); + }); + + it('should render code blocks correctly', () => { + render(); + expect(screen.getByText(/function hello/)).toBeTruthy(); + expect(screen.getByText(/console.log/)).toBeTruthy(); + }); + + it('should render headings correctly', () => { + render(); + expect(screen.getByRole('heading', { name: /Heading 1/i })).toBeTruthy(); + }); + + it('should render links correctly', () => { + render(); + expect(screen.getByRole('link', { name: /PatternFly/i })).toBeTruthy(); + }); + + it('should render unordered lists correctly', () => { + render(); + expect(screen.getByText('Item 1')).toBeTruthy(); + expect(screen.getByText('Item 2')).toBeTruthy(); + expect(screen.getByText('Item 3')).toBeTruthy(); + expect(screen.getAllByRole('listitem')).toHaveLength(3); + }); + + it('should render ordered lists correctly', () => { + render(); + expect(screen.getByText('First item')).toBeTruthy(); + expect(screen.getByText('Second item')).toBeTruthy(); + expect(screen.getByText('Third item')).toBeTruthy(); + expect(screen.getAllByRole('listitem')).toHaveLength(3); + }); + + it('should render tables correctly', () => { + render(); + expect(screen.getByRole('grid', { name: /Test table/i })).toBeTruthy(); + expect(screen.getByRole('columnheader', { name: /Column 1/i })).toBeTruthy(); + expect(screen.getByRole('columnheader', { name: /Column 2/i })).toBeTruthy(); + expect(screen.getByRole('cell', { name: /Cell 1/i })).toBeTruthy(); + expect(screen.getByRole('cell', { name: /Cell 2/i })).toBeTruthy(); + }); + + it('should render blockquotes correctly', () => { + render(); + expect(screen.getByText(/This is a blockquote/)).toBeTruthy(); + }); + + it('should render images when hasNoImages is false', () => { + render(); + expect(screen.getByRole('img', { name: /Alt text/i })).toBeTruthy(); + }); + + it('should not render images when hasNoImages is true', () => { + render(); + expect(screen.queryByRole('img', { name: /Alt text/i })).toBeFalsy(); + }); + + it('should disable markdown rendering when isMarkdownDisabled is true', () => { + render(); + expect(screen.getByText('**Bold text**')).toBeTruthy(); + }); + + it('should render text component when isMarkdownDisabled is true and textComponent is provided', () => { + const textComponent =
Custom text component
; + render(); + expect(screen.getByTestId('custom-text')).toBeTruthy(); + expect(screen.getByText('Custom text component')).toBeTruthy(); + }); + + it('should apply isPrimary prop to elements', () => { + const { container } = render(); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); + + it('should apply shouldRetainStyles prop to elements', () => { + const { container } = render(); + expect(container.querySelector('.pf-m-markdown')).toBeTruthy(); + }); + + it('should pass codeBlockProps to code blocks', () => { + render(); + expect(screen.getByRole('button', { name: /Custom code block/i })).toBeTruthy(); + }); + + it('should pass tableProps to tables', () => { + render(); + expect(screen.getByRole('grid', { name: /Custom table label/i })).toBeTruthy(); + }); + + it('should open links in new tab when openLinkInNewTab is true', () => { + render(); + expect(rehypeExternalLinks).toHaveBeenCalledTimes(1); + }); + + it('should not open links in new tab when openLinkInNewTab is false', () => { + render(); + expect(rehypeExternalLinks).not.toHaveBeenCalled(); + }); + + it('should pass linkProps to links', async () => { + const onClick = jest.fn(); + render(); + const link = screen.getByRole('link', { name: /PatternFly/i }); + link.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should handle reactMarkdownProps.disallowedElements', () => { + render(); + // Code block should not render when disallowed + expect(screen.queryByRole('button', { name: /Copy code/i })).toBeFalsy(); + }); + + it('should render plain text when no markdown is present', () => { + render(); + expect(screen.getByText('Plain text without markdown')).toBeTruthy(); + }); + + it('should handle empty content', () => { + const { container } = render(); + expect(container.textContent).toBe(''); + }); + + it('should handle undefined content', () => { + const { container } = render(); + expect(container.textContent).toBe(''); + }); + + it('should render multiple markdown elements together', () => { + const content = `# Heading + +**Bold text** and *italic text* + +\`\`\`javascript +const x = 5; +\`\`\` + +[Link](https://example.com)`; + + render(); + expect(screen.getByRole('heading', { name: /Heading/i })).toBeTruthy(); + expect(screen.getByText('Bold text')).toBeTruthy(); + expect(screen.getByText('italic text')).toBeTruthy(); + expect(screen.getByText(/const x = 5/)).toBeTruthy(); + expect(screen.getByRole('link', { name: /Link/i })).toBeTruthy(); + }); +}); diff --git a/packages/module/src/ToolCall/ToolCall.test.tsx b/packages/module/src/ToolCall/ToolCall.test.tsx index 91f4e7432..fa418d78f 100644 --- a/packages/module/src/ToolCall/ToolCall.test.tsx +++ b/packages/module/src/ToolCall/ToolCall.test.tsx @@ -232,4 +232,44 @@ describe('ToolCall', () => { expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); expect(screen.queryByText('Expandable Content')).not.toBeVisible(); }); + + it('should render titleText as markdown when isTitleMarkdown is true', () => { + const titleText = '**Bold title**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold title')).toBeTruthy(); + }); + + it('should not render titleText as markdown when isTitleMarkdown is false', () => { + const titleText = '**Bold title**'; + render(); + expect(screen.getByText('**Bold title**')).toBeTruthy(); + }); + + it('should render expandableContent as markdown when isExpandableContentMarkdown is true', async () => { + const user = userEvent.setup(); + const expandableContent = '**Bold expandable content**'; + const { container } = render( + + ); + await user.click(screen.getByRole('button', { name: defaultProps.titleText })); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold expandable content')).toBeTruthy(); + }); + + it('should not render expandableContent as markdown when isExpandableContentMarkdown is false', async () => { + const user = userEvent.setup(); + const expandableContent = '**Bold expandable content**'; + render(); + await user.click(screen.getByRole('button', { name: defaultProps.titleText })); + expect(screen.getByText('**Bold expandable content**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const titleText = '**Bold title**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); diff --git a/packages/module/src/ToolResponse/ToolResponse.test.tsx b/packages/module/src/ToolResponse/ToolResponse.test.tsx index 8d4e3135d..6737363a3 100644 --- a/packages/module/src/ToolResponse/ToolResponse.test.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.test.tsx @@ -149,4 +149,79 @@ describe('ToolResponse', () => { expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible(); expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible(); }); + + it('should render toggleContent as markdown when isToggleContentMarkdown is true', () => { + const toggleContent = '**Bold toggle**'; + const { container } = render( + + ); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold toggle')).toBeTruthy(); + }); + + it('should not render toggleContent as markdown when isToggleContentMarkdown is false', () => { + const toggleContent = '**Bold toggle**'; + render(); + expect(screen.getByText('**Bold toggle**')).toBeTruthy(); + }); + + it('should render subheading as markdown when isSubheadingMarkdown is true', () => { + const subheading = '**Bold subheading**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold subheading')).toBeTruthy(); + }); + + it('should not render subheading as markdown when isSubheadingMarkdown is false', () => { + const subheading = '**Bold subheading**'; + render(); + expect(screen.getByText('**Bold subheading**')).toBeTruthy(); + }); + + it('should render body as markdown when isBodyMarkdown is true', () => { + const body = '**Bold body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold body')).toBeTruthy(); + }); + + it('should not render body as markdown when isBodyMarkdown is false', () => { + const body = '**Bold body**'; + render(); + expect(screen.getByText('**Bold body**')).toBeTruthy(); + }); + + it('should render cardTitle as markdown when isCardTitleMarkdown is true', () => { + const cardTitle = '**Bold card title**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold card title')).toBeTruthy(); + }); + + it('should not render cardTitle as markdown when isCardTitleMarkdown is false', () => { + const cardTitle = '**Bold card title**'; + render(); + expect(screen.getByText('**Bold card title**')).toBeTruthy(); + }); + + it('should render cardBody as markdown when isCardBodyMarkdown is true', () => { + const cardBody = '**Bold card body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold card body')).toBeTruthy(); + }); + + it('should not render cardBody as markdown when isCardBodyMarkdown is false', () => { + const cardBody = '**Bold card body**'; + render(); + expect(screen.getByText('**Bold card body**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const body = '**Bold body**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); From 43463634f46d4297172abd8996c1834f9fb98b9e Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 20 Nov 2025 13:39:56 -0500 Subject: [PATCH 08/15] Removed comments, added prop for retain styles --- .../module/src/DeepThinking/DeepThinking.tsx | 11 +++++++---- .../CodeBlockMessage/CodeBlockMessage.tsx | 2 +- .../src/Message/TextMessage/TextMessage.scss | 4 ---- packages/module/src/ToolCall/ToolCall.tsx | 9 ++++++--- .../module/src/ToolResponse/ToolResponse.scss | 17 ----------------- .../module/src/ToolResponse/ToolResponse.tsx | 15 +++++++++------ 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index df91027bd..b77b2dcef 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -36,6 +36,8 @@ export interface DeepThinkingProps { isBodyMarkdown?: boolean; /** Props passed to MarkdownContent component when markdown is enabled */ markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const DeepThinking: FunctionComponent = ({ @@ -49,7 +51,8 @@ export const DeepThinking: FunctionComponent = ({ isToggleContentMarkdown, isSubheadingMarkdown, isBodyMarkdown, - markdownContentProps + markdownContentProps, + shouldRetainStyles = false }: DeepThinkingProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -59,7 +62,7 @@ export const DeepThinking: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ; } return toggleContent; }; @@ -69,7 +72,7 @@ export const DeepThinking: FunctionComponent = ({ return null; } if (isSubheadingMarkdown) { - return ; + return ; } return subheading; }; @@ -79,7 +82,7 @@ export const DeepThinking: FunctionComponent = ({ return null; } if (isBodyMarkdown && typeof body === 'string') { - return ; + return ; } return body; }; diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx index 632e20c42..1e051ff42 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx @@ -144,7 +144,7 @@ const CodeBlockMessage = ({ return (
- + <> {isExpandable ? ( [class^='pf-v6-c-content'] { font-size: inherit; } - - // &.pf-m-markdown + & { - // margin-top: 10px; - // } } // ============================================================================ diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index e2012a61d..d8d02ed39 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -69,6 +69,8 @@ export interface ToolCallProps { isExpandableContentMarkdown?: boolean; /** Props passed to MarkdownContent component when markdown is enabled */ markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const ToolCall: FunctionComponent = ({ @@ -94,7 +96,8 @@ export const ToolCall: FunctionComponent = ({ spinnerProps, isTitleMarkdown, isExpandableContentMarkdown, - markdownContentProps + markdownContentProps, + shouldRetainStyles = false }: ToolCallProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -104,7 +107,7 @@ export const ToolCall: FunctionComponent = ({ const renderTitle = () => { if (isTitleMarkdown) { - return ; + return ; } return titleText; }; @@ -124,7 +127,7 @@ export const ToolCall: FunctionComponent = ({ const renderExpandableContent = () => { if (isExpandableContentMarkdown && typeof expandableContent === 'string') { - return ; + return ; } return expandableContent; }; diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index b5d5d04ed..3d8a5b547 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -5,11 +5,6 @@ .pf-chatbot__tool-response-expandable-section { --pf-v6-c-expandable-section--Gap: var(--pf-t--global--spacer--xs); - - // .pf-m-markdown { - // font-size: inherit; - // color: inherit; - // } } .pf-chatbot__tool-response-section { @@ -22,19 +17,11 @@ font-size: var(--pf-t--global--font--size--body--sm); font-weight: var(--pf-t--global--font--weight--body--default); color: var(--pf-t--global--text--color--subtle); - - // .pf-m-markdown { - // font-size: var(--pf-t--global--font--size--body--sm); - // } } .pf-chatbot__tool-response-body { color: var(--pf-t--global--text--color--subtle); margin-block-end: var(--pf-t--global--spacer--xs); - - // .pf-m-markdown { - // font-size: inherit; - // } } .pf-chatbot__tool-response-card { @@ -46,8 +33,4 @@ .pf-v6-c-divider { --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default); } - - // .pf-m-markdown { - // font-size: inherit; - // } } diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index d720ec5f6..cc654af78 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -56,6 +56,8 @@ export interface ToolResponseProps { isCardTitleMarkdown?: boolean; /** Props passed to MarkdownContent component when markdown is enabled */ markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const ToolResponse: FunctionComponent = ({ @@ -77,7 +79,8 @@ export const ToolResponse: FunctionComponent = ({ isBodyMarkdown, isCardBodyMarkdown, isCardTitleMarkdown, - markdownContentProps + markdownContentProps, + shouldRetainStyles = false }: ToolResponseProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -87,7 +90,7 @@ export const ToolResponse: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ; } return toggleContent; }; @@ -97,7 +100,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isSubheadingMarkdown) { - return ; + return ; } return subheading; }; @@ -107,7 +110,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isBodyMarkdown && typeof body === 'string') { - return ; + return ; } return body; }; @@ -117,7 +120,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isCardTitleMarkdown && typeof cardTitle === 'string') { - return ; + return ; } return cardTitle; }; @@ -127,7 +130,7 @@ export const ToolResponse: FunctionComponent = ({ return null; } if (isCardBodyMarkdown && typeof cardBody === 'string') { - return ; + return ; } return cardBody; }; From 45e84300ac57130b02d30a7c559ffef2163124ad Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 20 Nov 2025 14:41:11 -0500 Subject: [PATCH 09/15] Updated tests --- package-lock.json | 73 ++++------- .../MessageWithMarkdownToolResponse.tsx | 1 + .../MarkdownContent/MarkdownContent.test.tsx | 11 +- .../src/Message/TextMessage/TextMessage.scss | 1 + .../module/src/ToolResponse/ToolResponse.scss | 4 + yarn.lock | 119 ++---------------- 6 files changed, 44 insertions(+), 165 deletions(-) diff --git a/package-lock.json b/package-lock.json index 280cd0a95..f95fd49a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -3133,7 +3132,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3781,7 +3779,6 @@ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", "license": "MIT", - "peer": true, "dependencies": { "@monaco-editor/loader": "^1.5.0" }, @@ -3835,7 +3832,6 @@ "version": "3.6.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -4220,8 +4216,7 @@ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.4.0.tgz", "integrity": "sha512-4drFhg74sEc/fftark5wZevODIog17qR4pwLCdB3j5iK3Uu5oMA2SdLhsEeEQggalfnFzve/Km87MdVR0ghhvQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@patternfly/patternfly-a11y": { "version": "5.1.0", @@ -4301,7 +4296,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz", "integrity": "sha512-zMgJmcFohp2FqgAoZHg7EXZS7gnaFESquk0qIavemYI0FsqspVlzV2/PUru7w+86+jXfqebRhgubPRsv1eJwEg==", "license": "MIT", - "peer": true, "dependencies": { "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", @@ -4336,7 +4330,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz", "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==", "license": "MIT", - "peer": true, "dependencies": { "@patternfly/react-core": "^6.4.0", "@patternfly/react-icons": "^6.4.0", @@ -4586,7 +4579,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.1", "@swc/types": "^0.1.5" @@ -5151,7 +5143,6 @@ "version": "29.5.12", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -5266,7 +5257,6 @@ "node_modules/@types/react": { "version": "18.2.61", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5444,7 +5434,6 @@ "version": "5.62.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5896,7 +5885,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6018,7 +6006,6 @@ "version": "6.12.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7227,7 +7214,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -10059,8 +10045,7 @@ "node_modules/devtools-protocol": { "version": "0.0.1367902", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -10144,7 +10129,8 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/dot-case": { "version": "3.0.4", @@ -10298,7 +10284,6 @@ "version": "0.1.13", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -10340,7 +10325,6 @@ "version": "2.4.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -10651,7 +10635,6 @@ "version": "8.57.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10706,7 +10689,6 @@ "version": "9.1.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10842,7 +10824,6 @@ "version": "2.29.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -11032,7 +11013,6 @@ "version": "15.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -11116,7 +11096,6 @@ "version": "6.1.1", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -14720,7 +14699,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -18805,6 +18783,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -20129,7 +20108,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -20249,7 +20227,6 @@ "version": "3.2.5", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20682,6 +20659,22 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/puppeteer/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "dev": true, @@ -20811,7 +20804,6 @@ "node_modules/react": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -20848,7 +20840,6 @@ "node_modules/react-dom": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -21679,7 +21670,6 @@ "version": "7.10.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.10.5", @@ -22453,7 +22443,6 @@ "version": "1.72.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -22550,7 +22539,6 @@ "version": "8.12.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -24375,7 +24363,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -24666,8 +24653,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -24874,7 +24860,6 @@ "version": "4.7.4", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -25667,7 +25652,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25700,7 +25684,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.21", "react-fast-compare": "^3.2.0", @@ -25717,7 +25700,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-brush-container": "37.3.6", @@ -25738,7 +25720,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25754,7 +25735,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -25772,7 +25752,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25788,7 +25767,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25805,7 +25783,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25822,7 +25799,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25870,7 +25846,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -25888,7 +25863,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25925,7 +25899,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "delaunay-find": "0.0.6", "lodash": "^4.17.19", @@ -26061,7 +26034,6 @@ "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -26179,7 +26151,6 @@ "version": "5.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.0.1", diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx index 4f45411df..0120d36ea 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx @@ -149,6 +149,7 @@ This is some text that has a short footnote[^1] and this is text with a longer f avatar={patternflyAvatar} content="This example demonstrates a tool response with a comprehensive markdown body showing all formatting options:" toolResponse={{ + shouldRetainStyles: true, isToggleContentMarkdown: true, toggleContent: '# Tool response: toolName', isSubheadingMarkdown: true, diff --git a/packages/module/src/MarkdownContent/MarkdownContent.test.tsx b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx index 0ebd4ac6d..c05292347 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.test.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx @@ -56,8 +56,10 @@ describe('MarkdownContent', () => { it('should render code blocks correctly', () => { render(); - expect(screen.getByText(/function hello/)).toBeTruthy(); - expect(screen.getByText(/console.log/)).toBeTruthy(); + + expect(screen.getByText(/function hello/)).toBeVisible(); + expect(screen.getByText(/console.log/)).toBeVisible(); + expect(screen.getByRole('button', { name: 'Copy code' })).toBeVisible(); }); it('should render headings correctly', () => { @@ -97,7 +99,10 @@ describe('MarkdownContent', () => { it('should render blockquotes correctly', () => { render(); - expect(screen.getByText(/This is a blockquote/)).toBeTruthy(); + + const quote = screen.getByText(/This is a blockquote/); + expect(quote).toBeVisible(); + expect(quote.closest('.pf-v6-c-content--blockquote')?.tagName).toBe('BLOCKQUOTE'); }); it('should render images when hasNoImages is false', () => { diff --git a/packages/module/src/Message/TextMessage/TextMessage.scss b/packages/module/src/Message/TextMessage/TextMessage.scss index bffd02a1f..59ed28641 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.scss +++ b/packages/module/src/Message/TextMessage/TextMessage.scss @@ -59,6 +59,7 @@ } &.pf-m-markdown > [class^='pf-v6-c-content'] { font-size: inherit; + color: inherit; } } diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index 3d8a5b547..75cee20a6 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -34,3 +34,7 @@ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default); } } + +.pf-chatbot__tool-response-expandable-section .pf-v6-c-expandable-section__toggle .pf-m-markdown { + padding: inherit; +} diff --git a/yarn.lock b/yarn.lock index bafd8ce08..6a2da9002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,13 +1005,6 @@ "@discoveryjs/json-ext@^0.5.0", "@discoveryjs/json-ext@0.5.7": version "0.5.7" -"@emnapi/runtime@^0.44.0": - version "0.44.0" - resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz" - integrity sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw== - dependencies: - tslib "^2.4.0" - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" dependencies: @@ -1064,112 +1057,11 @@ optionalDependencies: "@img/sharp-libvips-darwin-arm64" "1.0.0" -"@img/sharp-darwin-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz" - integrity sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.0" - "@img/sharp-libvips-darwin-arm64@1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz" integrity sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw== -"@img/sharp-libvips-darwin-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz" - integrity sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA== - -"@img/sharp-libvips-linux-arm@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz" - integrity sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw== - -"@img/sharp-libvips-linux-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz" - integrity sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA== - -"@img/sharp-libvips-linux-s390x@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz" - integrity sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw== - -"@img/sharp-libvips-linux-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz" - integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q== - -"@img/sharp-libvips-linuxmusl-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz" - integrity sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ== - -"@img/sharp-libvips-linuxmusl-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz" - integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg== - -"@img/sharp-linux-arm@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz" - integrity sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.0" - -"@img/sharp-linux-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz" - integrity sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.0" - -"@img/sharp-linux-s390x@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz" - integrity sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.0" - -"@img/sharp-linux-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz" - integrity sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.0" - -"@img/sharp-linuxmusl-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz" - integrity sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" - -"@img/sharp-linuxmusl-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz" - integrity sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.0" - -"@img/sharp-wasm32@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz" - integrity sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw== - dependencies: - "@emnapi/runtime" "^0.44.0" - -"@img/sharp-win32-ia32@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz" - integrity sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg== - -"@img/sharp-win32-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz" - integrity sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q== - "@isaacs/balanced-match@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" @@ -1582,7 +1474,7 @@ acorn-static-class-features "^1.0.0" astring "^1.7.5" -"@patternfly/chatbot@file:/Users/erindonehoo/Desktop/repos/chatbot/packages/module": +"@patternfly/chatbot@file:/Users/eolkowsk/GitHub/PatternFly/chatbot/packages/module": version "1.0.0" resolved "file:packages/module" dependencies: @@ -10419,7 +10311,7 @@ tslib@^1.8.1: tslib@^1.9.0: version "1.14.1" -tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.1, tslib@2: +tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.1, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.1, tslib@2: version "2.8.1" tsutils@^3.21.0: @@ -10514,12 +10406,17 @@ typedoc@0.23.0: minimatch "^5.1.0" shiki "^0.10.1" -typescript@*, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", "typescript@>=4.3 <6", typescript@>=4.9.5, "typescript@4.6.x || 4.7.x", typescript@4.7.4: +typescript@*, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", "typescript@>=4.3 <6", "typescript@4.6.x || 4.7.x", typescript@4.7.4: version "4.7.4" typescript@^5.3.3: version "5.6.3" +typescript@>=4.9.5: + version "5.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + uglify-js@^3.1.4: version "3.17.4" From 99fdced57bb0b93059bc6114b6148d1bb741de0a Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 20 Nov 2025 15:03:28 -0500 Subject: [PATCH 10/15] Added demo for overlay chatbot with toolresponse MD --- .../chatbot/examples/demos/Chatbot.md | 34 +- .../demos/ChatbotWithMarkdownToolResponse.tsx | 713 ++++++++++++++++++ .../module/src/ToolResponse/ToolResponse.scss | 6 + 3 files changed, 752 insertions(+), 1 deletion(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index 680932aae..5c9953042 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -52,6 +52,20 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon'; +import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon'; +import { WrenchIcon } from '@patternfly/react-icons/dist/esm/icons/wrench-icon'; +import { +Button, +DescriptionList, +DescriptionListDescription, +DescriptionListGroup, +DescriptionListTerm, +ExpandableSection, +ExpandableSectionVariant, +Flex, +FlexItem, +Label +} from '@patternfly/react-core'; import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg'; import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg'; import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; @@ -59,7 +73,7 @@ import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking"; -import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent } from 'react'; +import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent, MouseEvent as ReactMouseEvent } from 'react'; import saveAs from 'file-saver'; ### Basic ChatBot @@ -189,3 +203,21 @@ In this example, file download is implemented with [file-saver](https://www.npmj ```js file="./ChatbotTranscripts.tsx" isFullscreen ``` + +### ChatBot with markdown tool response + +This demo displays a ChatBot in overlay mode with a comprehensive markdown tool response example. This demo includes: + +1. The [``](/extensions/chatbot/ui#toggle) that controls the [`` container.](/extensions/chatbot/ui#container) +2. A [``](/extensions/chatbot/ui#header) with display mode switching capabilities. +3. A message with a [``](/extensions/chatbot/messages#tool-response) component that demonstrates: + - Markdown-formatted toggle content, subheading, and body + - Comprehensive markdown formatting examples (headings, code blocks, lists, tables, images, footnotes, etc.) + - Custom card title and card body with tool response details + - The `shouldRetainStyles` prop set to `true` to preserve markdown styles + +This example showcases how tool responses with rich markdown content render within an overlay ChatBot context. + +```js file="./ChatbotWithMarkdownToolResponse.tsx" isFullscreen + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx new file mode 100644 index 000000000..5c25ad636 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx @@ -0,0 +1,713 @@ +import { useEffect, useRef, useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react'; +import { + Bullseye, + Brand, + DropdownList, + DropdownItem, + DropdownGroup, + SkipToContent, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, + ExpandableSectionVariant, + Flex, + FlexItem, + Label +} from '@patternfly/react-core'; +import { CopyIcon, WrenchIcon } from '@patternfly/react-icons'; + +import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt'; +import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotConversationHistoryNav, { + Conversation +} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import ChatbotHeader, { + ChatbotHeaderMenu, + ChatbotHeaderMain, + ChatbotHeaderTitle, + ChatbotHeaderActions, + ChatbotHeaderSelectorDropdown, + ChatbotHeaderOptionsDropdown +} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; + +import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; +import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; +import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; + +import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg'; +import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg'; +import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; +import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; +import userAvatar from '../Messages/user_avatar.svg'; +import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; + +const footnoteProps = { + label: 'ChatBot uses AI. Check for mistakes.', + popover: { + title: 'Verify information', + description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`, + bannerImage: { + src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif', + alt: 'Example image for footnote popover' + }, + cta: { + label: 'Dismiss', + onClick: () => { + alert('Do something!'); + } + }, + link: { + label: 'View AI policy', + url: 'https://www.redhat.com/' + } + } +}; + +const comprehensiveMarkdownBody = `Here's a comprehensive markdown example with various formatting options: + +# h1 Heading + +## h2 Heading + +### h3 Heading + +#### h4 Heading + +##### h5 Heading + +###### h6 Heading + +## Text Emphasis + +**Bold text, formatted with double asterisks** + +__Bold text, formatted with double underscores__ + +*Italic text, formatted with single asterisks* + +_Italic text, formatted with single underscores_ + +~~Strikethrough~~ + +## Inline Code + +Here is an inline code example - \`() => void\` + +## Code Blocks + +Here is some YAML code: + +~~~yaml +apiVersion: helm.openshift.io/v1beta1/ +kind: HelmChartRepository +metadata: + name: azure-sample-repo0oooo00ooo +spec: + connectionConfig: + url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs +~~~ + +Here is some JavaScript code: + +~~~js +const MessageLoading = () => ( +
+ + Loading message + +
+); + +export default MessageLoading; +~~~ + +## Block Quotes + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs (>) right next to each other... +> > > ...or with spaces between each sign. + +## Lists + +### Ordered List + +1. Item 1 +2. Item 2 +3. Item 3 + +### Unordered List + +* Item 1 +* Item 2 +* Item 3 + +### More Complex List + +You may be wondering whether you can display more complex lists with formatting. In response to your question, I will explain how to spread butter on toast. + +1. **Using a \`toaster\`:** + + - Place \`bread\` in a \`toaster\` + - Once \`bread\` is lightly browned, remove from \`toaster\` + +2. **Using a \`knife\`:** + + - Acquire 1 tablespoon of room temperature \`butter\`. Use \`knife\` to spread butter on \`toast\`. Bon appétit! + +## Links + +A paragraph with a URL: https://reactjs.org. + +## Tables + +To customize your table, you can use [PatternFly TableProps](/components/table#table) + +| Version | GA date | User role +|-|-|-| +| 2.5 | September 30, 2024 | Administrator | +| 2.5 | June 27, 2023 | Editor | +| 3.0 | April 1, 2025 | Administrator + +## Images + +![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif) + +## Footnotes + +This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] + +[^1]: This is a short footnote. To return the highlight to the original message, click the arrow. + +[^bignote]: This is a long footnote with multiple paragraphs and formatting. + + To break long footnotes into paragraphs, indent the text. + + Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. + + > You can even include blockquotes in footnotes! +`; + +// It's important to set a date and timestamp prop since the Message components re-render. +// The timestamps re-render with them. +const date = new Date(); + +const ToolResponseContent = () => ( + + + + + + + + + toolName + + + + + Execution time: + 0.12 seconds + + + + + + + + +); + +const ToolResponseCard = () => { + const [isExpanded, setIsExpanded] = useState(false); + const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + + return ( + <> + + + Parameters + + + Optional description text for parameters. + + + + + + + + + + + + + + + + + + + + + Response + + + Descriptive text about the tool response, including completion status, details on the data that was + processed, or anything else relevant to the use case. + + + + + + ); +}; + +const initialMessages: MessageProps[] = [ + { + id: '1', + role: 'user', + content: 'Can you show me a comprehensive markdown example with tool response?', + name: 'User', + avatar: userAvatar, + timestamp: date.toLocaleString(), + avatarProps: { isBordered: true } + }, + { + id: '2', + role: 'bot', + content: + 'This example demonstrates a tool response with a comprehensive markdown body showing all formatting options:', + name: 'Bot', + avatar: patternflyAvatar, + timestamp: date.toLocaleString(), + toolResponse: { + shouldRetainStyles: true, + isToggleContentMarkdown: true, + toggleContent: '# Tool response: toolName', + isSubheadingMarkdown: true, + subheading: '> Thought for 3 seconds', + body: comprehensiveMarkdownBody, + isBodyMarkdown: true, + cardTitle: , + cardBody: + } + } +]; + +const welcomePrompts = [ + { + title: 'Show markdown example', + message: 'Can you show me a comprehensive markdown example with tool response?' + }, + { + title: 'Formatting options', + message: 'What formatting options are available in markdown?' + } +]; + +const initialConversations = { + Today: [{ id: '1', text: 'Can you show me a comprehensive markdown example with tool response?' }], + 'This month': [ + { + id: '2', + text: 'Markdown formatting guide' + }, + { id: '3', text: 'Tool response examples' } + ] +}; + +export const ChatbotWithMarkdownToolResponseDemo: FunctionComponent = () => { + const [chatbotVisible, setChatbotVisible] = useState(true); + const [displayMode, setDisplayMode] = useState(ChatbotDisplayMode.default); + const [messages, setMessages] = useState(initialMessages); + const [selectedModel, setSelectedModel] = useState('Granite 7B'); + const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [conversations, setConversations] = useState( + initialConversations + ); + const [announcement, setAnnouncement] = useState(); + const scrollToBottomRef = useRef(null); + const toggleRef = useRef(null); + const chatbotRef = useRef(null); + const historyRef = useRef(null); + + // Auto-scrolls to the latest message + useEffect(() => { + // don't scroll the first load - in this demo, we know we start with two messages + if (messages.length > 2) { + scrollToBottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages]); + + const onSelectModel = ( + _event: ReactMouseEvent | undefined, + value: string | number | undefined + ) => { + setSelectedModel(value as string); + }; + + const onSelectDisplayMode = ( + _event: ReactMouseEvent | undefined, + value: string | number | undefined + ) => { + setDisplayMode(value as ChatbotDisplayMode); + }; + + // you will likely want to come up with your own unique id function; this is for demo purposes only + const generateId = () => { + const id = Date.now() + Math.random(); + return id.toString(); + }; + + const handleSend = (message: string) => { + setIsSendButtonDisabled(true); + const newMessages: MessageProps[] = []; + // We can't use structuredClone since messages contains functions, but we can't mutate + // items that are going into state or the UI won't update correctly + messages.forEach((message) => newMessages.push(message)); + // It's important to set a timestamp prop since the Message components re-render. + // The timestamps re-render with them. + const date = new Date(); + newMessages.push({ + id: generateId(), + role: 'user', + content: message, + name: 'User', + avatar: userAvatar, + timestamp: date.toLocaleString(), + avatarProps: { isBordered: true } + }); + newMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response goes here', + name: 'Bot', + isLoading: true, + avatar: patternflyAvatar, + timestamp: date.toLocaleString() + }); + setMessages(newMessages); + // make announcement to assistive devices that new messages have been added + setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`); + + // this is for demo purposes only; in a real situation, there would be an API response we would wait for + setTimeout(() => { + const loadedMessages: MessageProps[] = []; + // We can't use structuredClone since messages contains functions, but we can't mutate + // items that are going into state or the UI won't update correctly + newMessages.forEach((message) => loadedMessages.push(message)); + loadedMessages.pop(); + loadedMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response goes here', + name: 'Bot', + isLoading: false, + avatar: patternflyAvatar, + timestamp: date.toLocaleString() + }); + setMessages(loadedMessages); + // make announcement to assistive devices that new message has loaded + setAnnouncement(`Message from Bot: API response goes here`); + setIsSendButtonDisabled(false); + }, 2000); + }; + + const findMatchingItems = (targetValue: string) => { + let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, {}); + + // append message if no items are found + if (Object.keys(filteredConversations).length === 0) { + filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }]; + } + return filteredConversations; + }; + + const horizontalLogo = ( + + + + + ); + + const iconLogo = ( + <> + + + + ); + + const handleSkipToContent = (e) => { + e.preventDefault(); + /* eslint-disable indent */ + switch (displayMode) { + case ChatbotDisplayMode.default: + if (!chatbotVisible && toggleRef.current) { + toggleRef.current.focus(); + } + if (chatbotVisible && chatbotRef.current) { + chatbotRef.current.focus(); + } + break; + + case ChatbotDisplayMode.docked: + if (chatbotRef.current) { + chatbotRef.current.focus(); + } + break; + default: + if (historyRef.current) { + historyRef.current.focus(); + } + break; + } + /* eslint-enable indent */ + }; + + return ( + <> + + Skip to chatbot + + + + { + setIsDrawerOpen(!isDrawerOpen); + setConversations(initialConversations); + }} + isDrawerOpen={isDrawerOpen} + setIsDrawerOpen={setIsDrawerOpen} + activeItemId="1" + // eslint-disable-next-line no-console + onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} + conversations={conversations} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + setMessages([]); + setConversations(initialConversations); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + // this is where you would perform search on the items in the drawer + // and update the state + const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); + setConversations(newConversations); + }} + drawerContent={ + <> + + + setIsDrawerOpen(!isDrawerOpen)} + /> + + + + + + + Granite 7B + + + Llama 3.0 + + + Mistral 3B + + + + + + + } + isSelected={displayMode === ChatbotDisplayMode.default} + > + Overlay + + } + isSelected={displayMode === ChatbotDisplayMode.docked} + > + Dock to window + + } + isSelected={displayMode === ChatbotDisplayMode.fullscreen} + > + Fullscreen + + + + + + + + {/* Update the announcement prop on MessageBox whenever a new message is sent + so that users of assistive devices receive sufficient context */} + + + {/* This code block enables scrolling to the top of the last message. + You can instead choose to move the div with scrollToBottomRef on it below + the map of messages, so that users are forced to scroll to the bottom. + If you are using streaming, you will want to take a different approach; + see: https://github.com/patternfly/chatbot/issues/201#issuecomment-2400725173 */} + {messages.map((message, index) => { + if (index === messages.length - 1) { + return ( + <> +
+ + + ); + } + return ; + })} +
+
+ + + + + + } + >
+
+ + ); +}; diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index 75cee20a6..ce10df3ab 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -38,3 +38,9 @@ .pf-chatbot__tool-response-expandable-section .pf-v6-c-expandable-section__toggle .pf-m-markdown { padding: inherit; } + +.pf-chatbot__tool-response { + .pf-chatbot__message-image { + max-width: 100%; + } +} From d25d27c319929814e948ed8b175a9200b75f2815 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 20 Nov 2025 15:11:59 -0500 Subject: [PATCH 11/15] Fixed lint errors --- packages/module/src/DeepThinking/DeepThinking.tsx | 4 +++- packages/module/src/ToolCall/ToolCall.tsx | 8 +++++++- packages/module/src/ToolResponse/ToolResponse.tsx | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index b77b2dcef..4bcf51227 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -62,7 +62,9 @@ export const DeepThinking: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ( + + ); } return toggleContent; }; diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index d8d02ed39..afdc29db0 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -127,7 +127,13 @@ export const ToolCall: FunctionComponent = ({ const renderExpandableContent = () => { if (isExpandableContentMarkdown && typeof expandableContent === 'string') { - return ; + return ( + + ); } return expandableContent; }; diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index cc654af78..6c3289872 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -90,7 +90,9 @@ export const ToolResponse: FunctionComponent = ({ const renderToggleContent = () => { if (isToggleContentMarkdown && typeof toggleContent === 'string') { - return ; + return ( + + ); } return toggleContent; }; From 402c76558173fcec6ca664173aedc25f2b53091d Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 15:38:39 -0500 Subject: [PATCH 12/15] Added new examples section for Markdown --- .../MessageWithMarkdownDeepThinking.tsx | 26 ++++ .../Messages/MessageWithMarkdownToolCall.tsx | 29 ++++ .../MessageWithMarkdownToolResponse.tsx | 143 ++---------------- .../chatbot/examples/Messages/Messages.md | 38 ++++- .../src/MarkdownContent/MarkdownContent.tsx | 2 +- 5 files changed, 104 insertions(+), 134 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx new file mode 100644 index 000000000..f9d9e37dc --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx @@ -0,0 +1,26 @@ +import { FunctionComponent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithMarkdownDeepThinkingExample: FunctionComponent = () => ( + Thought for 3 seconds', + isSubheadingMarkdown: true, + body: `I considered **multiple approaches** to answer your question: + +1. *Direct response* - Quick but less comprehensive +2. *Research-based* - Thorough but time-consuming +3. **Balanced approach** - Combines speed and accuracy + +I chose option 3 because it provides the best user experience.`, + isBodyMarkdown: true + }} + /> +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx new file mode 100644 index 000000000..1c1cd5051 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx @@ -0,0 +1,29 @@ +import { FunctionComponent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithMarkdownToolCallExample: FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx index 0120d36ea..915944bcf 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx @@ -19,142 +19,31 @@ export const MessageWithToolResponseExample: FunctionComponent = () => { const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { setIsExpanded(isExpanded); }; - const comprehensiveMarkdownBody = `Here's a comprehensive markdown example with various formatting options: - -# h1 Heading - -## h2 Heading - -### h3 Heading - -#### h4 Heading - -##### h5 Heading - -###### h6 Heading - -## Text Emphasis - -**Bold text, formatted with double asterisks** - -__Bold text, formatted with double underscores__ - -*Italic text, formatted with single asterisks* - -_Italic text, formatted with single underscores_ - -~~Strikethrough~~ - -## Inline Code - -Here is an inline code example - \`() => void\` - -## Code Blocks - -Here is some YAML code: - -~~~yaml -apiVersion: helm.openshift.io/v1beta1/ -kind: HelmChartRepository -metadata: - name: azure-sample-repo0oooo00ooo -spec: - connectionConfig: - url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs -~~~ - -Here is some JavaScript code: - -~~~js -const MessageLoading = () => ( -
- - Loading message - -
-); - -export default MessageLoading; -~~~ - -## Block Quotes - -> Blockquotes can also be nested... ->> ...by using additional greater-than signs (>) right next to each other... -> > > ...or with spaces between each sign. - -## Lists - -### Ordered List - -1. Item 1 -2. Item 2 -3. Item 3 - -### Unordered List - -* Item 1 -* Item 2 -* Item 3 - -### More Complex List - -You may be wondering whether you can display more complex lists with formatting. In response to your question, I will explain how to spread butter on toast. - -1. **Using a \`toaster\`:** - - - Place \`bread\` in a \`toaster\` - - Once \`bread\` is lightly browned, remove from \`toaster\` - -2. **Using a \`knife\`:** - - - Acquire 1 tablespoon of room temperature \`butter\`. Use \`knife\` to spread butter on \`toast\`. Bon appétit! - -## Links - -A paragraph with a URL: https://reactjs.org. - -## Tables - -To customize your table, you can use [PatternFly TableProps](/components/table#table) - -| Version | GA date | User role -|-|-|-| -| 2.5 | September 30, 2024 | Administrator | -| 2.5 | June 27, 2023 | Editor | -| 3.0 | April 1, 2025 | Administrator - -## Images - -![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif) - -## Footnotes - -This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] - -[^1]: This is a short footnote. To return the highlight to the original message, click the arrow. - -[^bignote]: This is a long footnote with multiple paragraphs and formatting. - - To break long footnotes into paragraphs, indent the text. - - Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. - - > You can even include blockquotes in footnotes! -`; + const toolResponseBody = `The tool processed **3 database queries** and returned the following results: + +1. User data - *42 records* +2. Transaction history - *128 records* +3. Analytics metrics - *15 data points* + +\`\`\`json +{ + "status": "success", + "execution_time": "0.12s" +} +\`\`\``; return ( Thought for 3 seconds', - body: comprehensiveMarkdownBody, + subheading: '> Completed in 0.12 seconds', + body: toolResponseBody, isBodyMarkdown: true, cardTitle: ( ` and provide a subheading (optional) and content body. @@ -355,3 +349,35 @@ An attachment dropzone allows users to upload files via drag and drop. ```js file="./FileDropZone.tsx" ``` + +## Examples with Markdown + +The ChatBot supports Markdown formatting in several message components, allowing you to display rich, formatted content. This is particularly useful when you need to include code snippets, lists, emphasis, or other formatted text. + +To enable Markdown rendering, use the appropriate Markdown flag prop (such as `isBodyMarkdown`, `isSubheadingMarkdown`, or `isExpandableContentMarkdown`) depending on the component and content you're formatting. + +**Important:** When using Markdown in these components, set `shouldRetainStyles: true` to retain the styling of the context the Markdown is used in. This ensures that Markdown content maintains the proper font sizes, colors, and other styling properties of its parent component. For example, Markdown passed into a toggle will retain the ChatBot toggle styling, while Markdown in a card body will maintain the appropriate card body styling. Without this prop, the Markdown may override the contextual styles and appear inconsistent with the rest of the ChatBot interface. + +### Tool calls with Markdown + +When displaying tool call information, you can use Markdown in the expandable content to provide formatted details about what the tool is processing. This is useful for showing structured data, code snippets, or formatted lists. + +```ts file="./MessageWithMarkdownToolCall.tsx" + +``` + +### Deep thinking with Markdown + +Deep thinking content can include Markdown formatting in both the subheading and body to better communicate the LLM's reasoning process. This allows you to emphasize key points, structure thought processes with lists, or include other formatting. + +```ts file="./MessageWithMarkdownDeepThinking.tsx" + +``` + +### Tool responses with Markdown + +Tool response cards support Markdown in multiple areas including the toggle content, subheading, and body. Use `shouldRetainStyles: true` along with the appropriate Markdown flag props to ensure proper formatting and spacing: + +```ts file="./MessageWithMarkdownToolResponse.tsx" + +``` diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index 8a1da4df3..76a930b8e 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -1,6 +1,6 @@ // ============================================================================ // Markdown Content - Shared component for rendering markdown -// With aid from Jean-Claude Van Code +// This was aided by Claude code // ============================================================================ import { type FunctionComponent, ReactNode } from 'react'; import Markdown, { Options } from 'react-markdown'; From 358e0cfabe2bd51ed48e7c3f1a35985aaf3066ee Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 16:33:59 -0500 Subject: [PATCH 13/15] Erin feedback --- .../extensions/chatbot/examples/Messages/Messages.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 5be6cdada..a42378d1e 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -352,11 +352,11 @@ An attachment dropzone allows users to upload files via drag and drop. ## Examples with Markdown -The ChatBot supports Markdown formatting in several message components, allowing you to display rich, formatted content. This is particularly useful when you need to include code snippets, lists, emphasis, or other formatted text. +The ChatBot supports Markdown formatting in several message components, allowing you to display rich, formatted content. This is particularly useful when you need to include code snippets, lists, emphasis, or other formatted text. The following examples demonstrate different ways you can use Markdown in a few of the ChatBot components, but this is not an exhaustive list of all Markdown customizations you can make. To enable Markdown rendering, use the appropriate Markdown flag prop (such as `isBodyMarkdown`, `isSubheadingMarkdown`, or `isExpandableContentMarkdown`) depending on the component and content you're formatting. -**Important:** When using Markdown in these components, set `shouldRetainStyles: true` to retain the styling of the context the Markdown is used in. This ensures that Markdown content maintains the proper font sizes, colors, and other styling properties of its parent component. For example, Markdown passed into a toggle will retain the ChatBot toggle styling, while Markdown in a card body will maintain the appropriate card body styling. Without this prop, the Markdown may override the contextual styles and appear inconsistent with the rest of the ChatBot interface. +**Important:** When using Markdown in these components, set `shouldRetainStyles: true` to retain the styling of the context the Markdown is used in. This ensures that Markdown content maintains the proper font sizes, colors, and other styling properties of its parent component. For example, Markdown passed into a toggle will retain the ChatBot toggle styling, while Markdown in a card body will maintain the appropriate card body styling. Without this prop, the Markdown may override the contextual styles and create inconsistencies with the rest of the ChatBot interface. ### Tool calls with Markdown @@ -376,7 +376,7 @@ Deep thinking content can include Markdown formatting in both the subheading and ### Tool responses with Markdown -Tool response cards support Markdown in multiple areas including the toggle content, subheading, and body. Use `shouldRetainStyles: true` along with the appropriate Markdown flag props to ensure proper formatting and spacing: +Tool response cards support Markdown in multiple areas, including the toggle content, subheading, and body. Use `shouldRetainStyles: true` along with the appropriate Markdown flag props to ensure proper formatting and spacing. ```ts file="./MessageWithMarkdownToolResponse.tsx" From 2aa781bc22b6884b3592fd3373fcdca567b311f3 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Mon, 1 Dec 2025 09:51:11 -0500 Subject: [PATCH 14/15] Removed full screen demo --- .../chatbot/examples/demos/Chatbot.md | 18 - .../demos/ChatbotWithMarkdownToolResponse.tsx | 713 ------------------ 2 files changed, 731 deletions(-) delete mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index 5c9953042..8814917f7 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -203,21 +203,3 @@ In this example, file download is implemented with [file-saver](https://www.npmj ```js file="./ChatbotTranscripts.tsx" isFullscreen ``` - -### ChatBot with markdown tool response - -This demo displays a ChatBot in overlay mode with a comprehensive markdown tool response example. This demo includes: - -1. The [``](/extensions/chatbot/ui#toggle) that controls the [`` container.](/extensions/chatbot/ui#container) -2. A [``](/extensions/chatbot/ui#header) with display mode switching capabilities. -3. A message with a [``](/extensions/chatbot/messages#tool-response) component that demonstrates: - - Markdown-formatted toggle content, subheading, and body - - Comprehensive markdown formatting examples (headings, code blocks, lists, tables, images, footnotes, etc.) - - Custom card title and card body with tool response details - - The `shouldRetainStyles` prop set to `true` to preserve markdown styles - -This example showcases how tool responses with rich markdown content render within an overlay ChatBot context. - -```js file="./ChatbotWithMarkdownToolResponse.tsx" isFullscreen - -``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx deleted file mode 100644 index 5c25ad636..000000000 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotWithMarkdownToolResponse.tsx +++ /dev/null @@ -1,713 +0,0 @@ -import { useEffect, useRef, useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react'; -import { - Bullseye, - Brand, - DropdownList, - DropdownItem, - DropdownGroup, - SkipToContent, - Button, - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - ExpandableSection, - ExpandableSectionVariant, - Flex, - FlexItem, - Label -} from '@patternfly/react-core'; -import { CopyIcon, WrenchIcon } from '@patternfly/react-icons'; - -import ChatbotToggle from '@patternfly/chatbot/dist/dynamic/ChatbotToggle'; -import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; -import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; -import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt'; -import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; -import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; -import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; -import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; -import ChatbotConversationHistoryNav, { - Conversation -} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; -import ChatbotHeader, { - ChatbotHeaderMenu, - ChatbotHeaderMain, - ChatbotHeaderTitle, - ChatbotHeaderActions, - ChatbotHeaderSelectorDropdown, - ChatbotHeaderOptionsDropdown -} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; - -import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; -import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; -import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; - -import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg'; -import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg'; -import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; -import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; -import userAvatar from '../Messages/user_avatar.svg'; -import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; - -const footnoteProps = { - label: 'ChatBot uses AI. Check for mistakes.', - popover: { - title: 'Verify information', - description: `While ChatBot strives for accuracy, AI is experimental and can make mistakes. We cannot guarantee that all information provided by ChatBot is up to date or without error. You should always verify responses using reliable sources, especially for crucial information and decision making.`, - bannerImage: { - src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif', - alt: 'Example image for footnote popover' - }, - cta: { - label: 'Dismiss', - onClick: () => { - alert('Do something!'); - } - }, - link: { - label: 'View AI policy', - url: 'https://www.redhat.com/' - } - } -}; - -const comprehensiveMarkdownBody = `Here's a comprehensive markdown example with various formatting options: - -# h1 Heading - -## h2 Heading - -### h3 Heading - -#### h4 Heading - -##### h5 Heading - -###### h6 Heading - -## Text Emphasis - -**Bold text, formatted with double asterisks** - -__Bold text, formatted with double underscores__ - -*Italic text, formatted with single asterisks* - -_Italic text, formatted with single underscores_ - -~~Strikethrough~~ - -## Inline Code - -Here is an inline code example - \`() => void\` - -## Code Blocks - -Here is some YAML code: - -~~~yaml -apiVersion: helm.openshift.io/v1beta1/ -kind: HelmChartRepository -metadata: - name: azure-sample-repo0oooo00ooo -spec: - connectionConfig: - url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs -~~~ - -Here is some JavaScript code: - -~~~js -const MessageLoading = () => ( -
- - Loading message - -
-); - -export default MessageLoading; -~~~ - -## Block Quotes - -> Blockquotes can also be nested... ->> ...by using additional greater-than signs (>) right next to each other... -> > > ...or with spaces between each sign. - -## Lists - -### Ordered List - -1. Item 1 -2. Item 2 -3. Item 3 - -### Unordered List - -* Item 1 -* Item 2 -* Item 3 - -### More Complex List - -You may be wondering whether you can display more complex lists with formatting. In response to your question, I will explain how to spread butter on toast. - -1. **Using a \`toaster\`:** - - - Place \`bread\` in a \`toaster\` - - Once \`bread\` is lightly browned, remove from \`toaster\` - -2. **Using a \`knife\`:** - - - Acquire 1 tablespoon of room temperature \`butter\`. Use \`knife\` to spread butter on \`toast\`. Bon appétit! - -## Links - -A paragraph with a URL: https://reactjs.org. - -## Tables - -To customize your table, you can use [PatternFly TableProps](/components/table#table) - -| Version | GA date | User role -|-|-|-| -| 2.5 | September 30, 2024 | Administrator | -| 2.5 | June 27, 2023 | Editor | -| 3.0 | April 1, 2025 | Administrator - -## Images - -![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif) - -## Footnotes - -This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] - -[^1]: This is a short footnote. To return the highlight to the original message, click the arrow. - -[^bignote]: This is a long footnote with multiple paragraphs and formatting. - - To break long footnotes into paragraphs, indent the text. - - Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. - - > You can even include blockquotes in footnotes! -`; - -// It's important to set a date and timestamp prop since the Message components re-render. -// The timestamps re-render with them. -const date = new Date(); - -const ToolResponseContent = () => ( - - - - - - - - - toolName - - - - - Execution time: - 0.12 seconds - - - - - - - - -); - -const ToolResponseCard = () => { - const [isExpanded, setIsExpanded] = useState(false); - const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { - setIsExpanded(isExpanded); - }; - - return ( - <> - - - Parameters - - - Optional description text for parameters. - - - - - - - - - - - - - - - - - - - - - Response - - - Descriptive text about the tool response, including completion status, details on the data that was - processed, or anything else relevant to the use case. - - - - - - ); -}; - -const initialMessages: MessageProps[] = [ - { - id: '1', - role: 'user', - content: 'Can you show me a comprehensive markdown example with tool response?', - name: 'User', - avatar: userAvatar, - timestamp: date.toLocaleString(), - avatarProps: { isBordered: true } - }, - { - id: '2', - role: 'bot', - content: - 'This example demonstrates a tool response with a comprehensive markdown body showing all formatting options:', - name: 'Bot', - avatar: patternflyAvatar, - timestamp: date.toLocaleString(), - toolResponse: { - shouldRetainStyles: true, - isToggleContentMarkdown: true, - toggleContent: '# Tool response: toolName', - isSubheadingMarkdown: true, - subheading: '> Thought for 3 seconds', - body: comprehensiveMarkdownBody, - isBodyMarkdown: true, - cardTitle: , - cardBody: - } - } -]; - -const welcomePrompts = [ - { - title: 'Show markdown example', - message: 'Can you show me a comprehensive markdown example with tool response?' - }, - { - title: 'Formatting options', - message: 'What formatting options are available in markdown?' - } -]; - -const initialConversations = { - Today: [{ id: '1', text: 'Can you show me a comprehensive markdown example with tool response?' }], - 'This month': [ - { - id: '2', - text: 'Markdown formatting guide' - }, - { id: '3', text: 'Tool response examples' } - ] -}; - -export const ChatbotWithMarkdownToolResponseDemo: FunctionComponent = () => { - const [chatbotVisible, setChatbotVisible] = useState(true); - const [displayMode, setDisplayMode] = useState(ChatbotDisplayMode.default); - const [messages, setMessages] = useState(initialMessages); - const [selectedModel, setSelectedModel] = useState('Granite 7B'); - const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [conversations, setConversations] = useState( - initialConversations - ); - const [announcement, setAnnouncement] = useState(); - const scrollToBottomRef = useRef(null); - const toggleRef = useRef(null); - const chatbotRef = useRef(null); - const historyRef = useRef(null); - - // Auto-scrolls to the latest message - useEffect(() => { - // don't scroll the first load - in this demo, we know we start with two messages - if (messages.length > 2) { - scrollToBottomRef.current?.scrollIntoView({ behavior: 'smooth' }); - } - }, [messages]); - - const onSelectModel = ( - _event: ReactMouseEvent | undefined, - value: string | number | undefined - ) => { - setSelectedModel(value as string); - }; - - const onSelectDisplayMode = ( - _event: ReactMouseEvent | undefined, - value: string | number | undefined - ) => { - setDisplayMode(value as ChatbotDisplayMode); - }; - - // you will likely want to come up with your own unique id function; this is for demo purposes only - const generateId = () => { - const id = Date.now() + Math.random(); - return id.toString(); - }; - - const handleSend = (message: string) => { - setIsSendButtonDisabled(true); - const newMessages: MessageProps[] = []; - // We can't use structuredClone since messages contains functions, but we can't mutate - // items that are going into state or the UI won't update correctly - messages.forEach((message) => newMessages.push(message)); - // It's important to set a timestamp prop since the Message components re-render. - // The timestamps re-render with them. - const date = new Date(); - newMessages.push({ - id: generateId(), - role: 'user', - content: message, - name: 'User', - avatar: userAvatar, - timestamp: date.toLocaleString(), - avatarProps: { isBordered: true } - }); - newMessages.push({ - id: generateId(), - role: 'bot', - content: 'API response goes here', - name: 'Bot', - isLoading: true, - avatar: patternflyAvatar, - timestamp: date.toLocaleString() - }); - setMessages(newMessages); - // make announcement to assistive devices that new messages have been added - setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`); - - // this is for demo purposes only; in a real situation, there would be an API response we would wait for - setTimeout(() => { - const loadedMessages: MessageProps[] = []; - // We can't use structuredClone since messages contains functions, but we can't mutate - // items that are going into state or the UI won't update correctly - newMessages.forEach((message) => loadedMessages.push(message)); - loadedMessages.pop(); - loadedMessages.push({ - id: generateId(), - role: 'bot', - content: 'API response goes here', - name: 'Bot', - isLoading: false, - avatar: patternflyAvatar, - timestamp: date.toLocaleString() - }); - setMessages(loadedMessages); - // make announcement to assistive devices that new message has loaded - setAnnouncement(`Message from Bot: API response goes here`); - setIsSendButtonDisabled(false); - }, 2000); - }; - - const findMatchingItems = (targetValue: string) => { - let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { - const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); - if (filteredItems.length > 0) { - acc[key] = filteredItems; - } - return acc; - }, {}); - - // append message if no items are found - if (Object.keys(filteredConversations).length === 0) { - filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }]; - } - return filteredConversations; - }; - - const horizontalLogo = ( - - - - - ); - - const iconLogo = ( - <> - - - - ); - - const handleSkipToContent = (e) => { - e.preventDefault(); - /* eslint-disable indent */ - switch (displayMode) { - case ChatbotDisplayMode.default: - if (!chatbotVisible && toggleRef.current) { - toggleRef.current.focus(); - } - if (chatbotVisible && chatbotRef.current) { - chatbotRef.current.focus(); - } - break; - - case ChatbotDisplayMode.docked: - if (chatbotRef.current) { - chatbotRef.current.focus(); - } - break; - default: - if (historyRef.current) { - historyRef.current.focus(); - } - break; - } - /* eslint-enable indent */ - }; - - return ( - <> - - Skip to chatbot - - - - { - setIsDrawerOpen(!isDrawerOpen); - setConversations(initialConversations); - }} - isDrawerOpen={isDrawerOpen} - setIsDrawerOpen={setIsDrawerOpen} - activeItemId="1" - // eslint-disable-next-line no-console - onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} - conversations={conversations} - onNewChat={() => { - setIsDrawerOpen(!isDrawerOpen); - setMessages([]); - setConversations(initialConversations); - }} - handleTextInputChange={(value: string) => { - if (value === '') { - setConversations(initialConversations); - } - // this is where you would perform search on the items in the drawer - // and update the state - const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); - setConversations(newConversations); - }} - drawerContent={ - <> - - - setIsDrawerOpen(!isDrawerOpen)} - /> - - - - - - - Granite 7B - - - Llama 3.0 - - - Mistral 3B - - - - - - - } - isSelected={displayMode === ChatbotDisplayMode.default} - > - Overlay - - } - isSelected={displayMode === ChatbotDisplayMode.docked} - > - Dock to window - - } - isSelected={displayMode === ChatbotDisplayMode.fullscreen} - > - Fullscreen - - - - - - - - {/* Update the announcement prop on MessageBox whenever a new message is sent - so that users of assistive devices receive sufficient context */} - - - {/* This code block enables scrolling to the top of the last message. - You can instead choose to move the div with scrollToBottomRef on it below - the map of messages, so that users are forced to scroll to the bottom. - If you are using streaming, you will want to take a different approach; - see: https://github.com/patternfly/chatbot/issues/201#issuecomment-2400725173 */} - {messages.map((message, index) => { - if (index === messages.length - 1) { - return ( - <> -
- - - ); - } - return ; - })} -
-
- - - - - - } - >
-
- - ); -}; From bc6af471bc197a43e7d6e75f2ec80551ba215da1 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 3 Dec 2025 17:10:29 -0500 Subject: [PATCH 15/15] Removed comment about claude code --- packages/module/src/MarkdownContent/MarkdownContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index 76a930b8e..f852ea003 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -1,6 +1,5 @@ // ============================================================================ // Markdown Content - Shared component for rendering markdown -// This was aided by Claude code // ============================================================================ import { type FunctionComponent, ReactNode } from 'react'; import Markdown, { Options } from 'react-markdown';