From 74babcc4517face82c5b10149c935a53b37cdfe4 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 5 Dec 2025 10:22:17 -0500 Subject: [PATCH 1/6] feat(Messages): allowed more composable structures --- .../chatbot/examples/Messages/Messages.md | 4 + packages/module/src/Message/Message.tsx | 139 ++++++++++-------- .../MessageAndActions.test.tsx | 23 +++ .../MessageAndActions/MessageAndActions.tsx | 25 ++++ .../MessageAttachment.test.tsx | 23 +++ .../MessageAttachment/MessageAttachment.tsx | 24 +++ .../MessageAttachmentsContainer.test.tsx | 23 +++ .../MessageAttachmentsContainer.tsx | 28 ++++ .../ResponseActionsGroups.test.tsx | 23 +++ .../ResponseActionsGroups.tsx | 28 ++++ packages/module/src/Message/index.ts | 13 ++ 11 files changed, 289 insertions(+), 64 deletions(-) create mode 100644 packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx create mode 100644 packages/module/src/Message/MessageAndActions/MessageAndActions.tsx create mode 100644 packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx create mode 100644 packages/module/src/Message/MessageAttachment/MessageAttachment.tsx create mode 100644 packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx create mode 100644 packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx create mode 100644 packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx create mode 100644 packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx 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 a42378d1e..f59d6aae5 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 @@ -19,6 +19,10 @@ propComponents: 'FileDropZone', 'PreviewAttachment', 'Message', + 'MessageAndActionsProps', + 'MessageAttachmentsContainerProps', + 'MessageAttachmentProps', + 'ResponseActionsGroupsProps', 'MessageExtraContent', 'PreviewAttachment', 'ActionProps', diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 8b298c325..90777d81a 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -67,6 +67,8 @@ export interface MessageExtraContent { } export interface MessageProps extends Omit, 'role'> { + /** Children to render instead of the default message structure, allowing more fine-tuned message control. When provided, this will override the default rendering of content, toolResponse, deepThinking, toolCall, sources, quickStarts, actions, etc. */ + children?: ReactNode; /** Unique id for message */ id?: string; /** Role of the user sending the message */ @@ -193,6 +195,7 @@ export interface MessageProps extends Omit, 'role'> { } export const MessageBase: FunctionComponent = ({ + children, role, content, extraContent, @@ -341,74 +344,82 @@ export const MessageBase: FunctionComponent = ({ {timestamp}
-
- {renderMessage()} - {afterMainContent && <>{afterMainContent}} - {toolResponse && } - {deepThinking && } - {toolCall && } - {!isLoading && sources && } - {quickStarts && quickStarts.quickStart && ( - - )} - {!isLoading && !isEditable && actions && ( - <> - {Array.isArray(actions) ? ( -
- {actions.map((actionGroup, index) => ( - - ))} -
- ) : ( - + {children ? ( + <>{children} + ) : ( + <> +
+ {renderMessage()} + {afterMainContent && <>{afterMainContent}} + {toolResponse && } + {deepThinking && } + {toolCall && } + {!isLoading && sources && } + {quickStarts && quickStarts.quickStart && ( + + )} + {!isLoading && !isEditable && actions && ( + <> + {Array.isArray(actions) ? ( +
+ {actions.map((actionGroup, index) => ( + + ))} +
+ ) : ( + + )} + + )} + {userFeedbackForm && ( + )} - - )} - {userFeedbackForm && } - {userFeedbackComplete && ( - - )} - {!isLoading && quickResponses && ( - - )} -
- {attachments && ( -
- {attachments.map((attachment) => ( -
- + )} + {!isLoading && quickResponses && ( + + )} +
+ {attachments && ( +
+ {attachments.map((attachment) => ( +
+ +
+ ))}
- ))} -
+ )} + {!isLoading && endContent && <>{endContent}} + )} - {!isLoading && endContent && <>{endContent}}
diff --git a/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx b/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx new file mode 100644 index 000000000..21032240e --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAndActions from './MessageAndActions'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-and-actions class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-and-actions', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx new file mode 100644 index 000000000..dc9870c55 --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx @@ -0,0 +1,25 @@ +// ============================================================================ +// Message And Actions - Container for message content and actions +// ============================================================================ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container that wraps the primary message content and inline actions, such as ToolCall, ToolResponse, DeepThinking, ResponseActions, etc. + * Attachments should not be rendered inside this container. + * Use this component when passing children to Message to customize its structure. + */ +export interface MessageAndActionsProps extends HTMLProps { + /** Content to render inside the message and actions container. */ + children: ReactNode; + /** Additional classes applied to the message and actions container. */ + className?: string; +} + +export const MessageAndActions: FunctionComponent = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export default MessageAndActions; diff --git a/packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx b/packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx new file mode 100644 index 000000000..b9835fff5 --- /dev/null +++ b/packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAttachment from './MessageAttachment'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-attachment class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachment', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAttachment/MessageAttachment.tsx b/packages/module/src/Message/MessageAttachment/MessageAttachment.tsx new file mode 100644 index 000000000..978dedabd --- /dev/null +++ b/packages/module/src/Message/MessageAttachment/MessageAttachment.tsx @@ -0,0 +1,24 @@ +// ============================================================================ +// Message Attachment - Container for a single message attachment +// ============================================================================ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container for a single message attachment item, typically the FileDetailsLabel component. You must wrap any attachment components in this container. + * Use this component within MessageAttachmentsContainer when passing children to Message to customize its structure. + */ +export interface MessageAttachmentProps extends HTMLProps { + /** Content to render inside a single attachment container */ + children: ReactNode; + /** Additional classes applied to the attachment container. */ + className?: string; +} + +export const MessageAttachment: FunctionComponent = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export default MessageAttachment; diff --git a/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx b/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx new file mode 100644 index 000000000..88b3776e9 --- /dev/null +++ b/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAttachmentsContainer from './MessageAttachmentsContainer'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-attachments-container class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachments-container', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx b/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx new file mode 100644 index 000000000..10362b7b5 --- /dev/null +++ b/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx @@ -0,0 +1,28 @@ +// ============================================================================ +// Message Attachments Container - Container for message attachments +// ============================================================================ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container to wrap MessageAttachment components. You must wrap any MessageAttachment components in this container. + * Use this component when passing children to Message to customize its structure. + */ +export interface MessageAttachmentsContainerProps extends HTMLProps { + /** Content to render inside the attachments container */ + children: ReactNode; + /** Additional classes applied to the attachments container. */ + className?: string; +} + +export const MessageAttachmentsContainer: FunctionComponent = ({ + children, + className, + ...props +}) => ( +
+ {children} +
+); + +export default MessageAttachmentsContainer; diff --git a/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx b/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx new file mode 100644 index 000000000..be14f6c6e --- /dev/null +++ b/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import ResponseActionsGroups from './ResponseActionsGroups'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__response-actions-groups class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__response-actions-groups', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx b/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx new file mode 100644 index 000000000..602654782 --- /dev/null +++ b/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx @@ -0,0 +1,28 @@ +// ============================================================================ +// Response Actions Groups - Container for multiple action groups +// ============================================================================ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container for grouping multiple related ResponseActions components, typically used for having different persistence states amongst groups. + * Use this component when passing children to Message to customize its structure. + */ +export interface ResponseActionsGroupsProps extends HTMLProps { + /** Content to render inside the response actions groups container */ + children: ReactNode; + /** Additional classes applied to the response actions groups container. */ + className?: string; +} + +export const ResponseActionsGroups: FunctionComponent = ({ + children, + className, + ...props +}) => ( +
+ {children} +
+); + +export default ResponseActionsGroups; diff --git a/packages/module/src/Message/index.ts b/packages/module/src/Message/index.ts index d62ce192d..6a0852508 100644 --- a/packages/module/src/Message/index.ts +++ b/packages/module/src/Message/index.ts @@ -2,3 +2,16 @@ export { default } from './Message'; export { rehypeCodeBlockToggle } from './Plugins/rehypeCodeBlockToggle'; export * from './Message'; + +// Sub-component containers for flexible message composition +export { default as MessageAndActions } from './MessageAndActions/MessageAndActions'; +export * from './MessageAndActions/MessageAndActions'; + +export { default as MessageAttachmentsContainer } from './MessageAttachmentsContainer/MessageAttachmentsContainer'; +export * from './MessageAttachmentsContainer/MessageAttachmentsContainer'; + +export { default as MessageAttachment } from './MessageAttachment/MessageAttachment'; +export * from './MessageAttachment/MessageAttachment'; + +export { default as ResponseActionsGroups } from './ResponseActionsGroups/ResponseActionsGroups'; +export * from './ResponseActionsGroups/ResponseActionsGroups'; From f3b0af0b98383f62619bf400d4bee266b924d4ba Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Mon, 8 Dec 2025 09:31:45 -0500 Subject: [PATCH 2/6] Renamed MessageAttachments, added tests for Loading, moved Action groups --- .../chatbot/examples/Messages/Messages.md | 29 +++++++++++++------ .../src/MarkdownContent/MarkdownContent.tsx | 9 ++++-- .../MessageAndActions/MessageAndActions.tsx | 3 -- .../MessageAttachmentItem.test.tsx} | 10 +++---- .../MessageAttachmentItem.tsx} | 13 +++++---- .../MessageAttachmentsContainer.test.tsx | 0 .../MessageAttachmentsContainer.tsx | 3 -- .../src/Message/MessageLoading.test.tsx | 23 +++++++++++++++ .../module/src/Message/MessageLoading.tsx | 19 ++++++++++-- packages/module/src/Message/index.ts | 14 +-------- .../src/ResponseActions/ResponseActions.tsx | 6 ++++ .../ResponseActionsGroups.test.tsx | 0 .../ResponseActionsGroups.tsx | 0 packages/module/src/ResponseActions/index.ts | 1 + 14 files changed, 87 insertions(+), 43 deletions(-) rename packages/module/src/Message/{MessageAttachment/MessageAttachment.test.tsx => MessageAttachments/MessageAttachmentItem.test.tsx} (62%) rename packages/module/src/Message/{MessageAttachment/MessageAttachment.tsx => MessageAttachments/MessageAttachmentItem.tsx} (60%) rename packages/module/src/Message/{MessageAttachmentsContainer => MessageAttachments}/MessageAttachmentsContainer.test.tsx (100%) rename packages/module/src/Message/{MessageAttachmentsContainer => MessageAttachments}/MessageAttachmentsContainer.tsx (79%) create mode 100644 packages/module/src/Message/MessageLoading.test.tsx rename packages/module/src/{Message/ResponseActionsGroups => ResponseActions}/ResponseActionsGroups.test.tsx (100%) rename packages/module/src/{Message/ResponseActionsGroups => ResponseActions}/ResponseActionsGroups.tsx (100%) 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 f59d6aae5..dd2258c65 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 @@ -12,24 +12,35 @@ source: react # These are found through the sourceProps function provided in patternfly-docs.source.js propComponents: [ + 'ActionProps', 'AttachMenu', 'AttachmentEdit', 'FileDetailsProps', 'FileDetailsLabelProps', 'FileDropZone', - 'PreviewAttachment', 'Message', - 'MessageAndActionsProps', - 'MessageAttachmentsContainerProps', - 'MessageAttachmentProps', - 'ResponseActionsGroupsProps', + 'MessageAndActions', + 'ResponseActionsGroups', + 'ResponseActions', + 'MessageAttachmentsContainer', + 'MessageAttachmentItem', 'MessageExtraContent', + 'MessageInput', + 'MessageLoading', + 'ErrorMessage', 'PreviewAttachment', - 'ActionProps', - 'SourcesCardProps', + 'QuickResponse', + 'QuickStartTile', + 'QuickStart', + 'QuickStartAction', + 'DeepThinking', + 'ToolCall', + 'ToolResponse', + 'SourcesCard', + 'UserFeedback', + 'UserFeedbackComplete', 'UserFeedbackProps', - 'UserFeedbackCompleteProps', - 'QuickResponseProps' + 'UserFeedbackCompleteProps' ] sortValue: 3 --- diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index f852ea003..5b158b881 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -30,10 +30,15 @@ import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage import { ButtonProps } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; +/** + * MarkdownContent renders content either as plain text or with content with markdown support. + * + * Use this component when passing children to Message to customize its structure. + */ export interface MarkdownContentProps { - /** The markdown content to render */ + /** The content to render. Supports markdown formatting by default, or plain text when isMarkdownDisabled is true. */ content?: string; - /** Disables markdown parsing, allowing only text input */ + /** Disables markdown parsing, allowing only plain text input */ isMarkdownDisabled?: boolean; /** Props for code blocks */ codeBlockProps?: CodeBlockMessageProps; diff --git a/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx index dc9870c55..5547f7078 100644 --- a/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx +++ b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx @@ -1,6 +1,3 @@ -// ============================================================================ -// Message And Actions - Container for message content and actions -// ============================================================================ import { FunctionComponent, HTMLProps, ReactNode } from 'react'; import { css } from '@patternfly/react-styles'; diff --git a/packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx similarity index 62% rename from packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx rename to packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx index b9835fff5..49e58d726 100644 --- a/packages/module/src/Message/MessageAttachment/MessageAttachment.test.tsx +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx @@ -1,23 +1,23 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; -import MessageAttachment from './MessageAttachment'; +import MessageAttachmentItem from './MessageAttachmentItem'; test('Renders with children', () => { - render(Test content); + render(Test content); expect(screen.getByText('Test content')).toBeInTheDocument(); }); test('Renders with pf-chatbot__message-attachment class by default', () => { - render(Test content); + render(Test content); expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachment', { exact: true }); }); test('Renders with custom className', () => { - render(Test content); + render(Test content); expect(screen.getByText('Test content')).toHaveClass('custom-class'); }); test('Spreads additional props', () => { - render(Test content); + render(Test content); expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); }); diff --git a/packages/module/src/Message/MessageAttachment/MessageAttachment.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx similarity index 60% rename from packages/module/src/Message/MessageAttachment/MessageAttachment.tsx rename to packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx index 978dedabd..3d4a8abd9 100644 --- a/packages/module/src/Message/MessageAttachment/MessageAttachment.tsx +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx @@ -1,6 +1,3 @@ -// ============================================================================ -// Message Attachment - Container for a single message attachment -// ============================================================================ import { FunctionComponent, HTMLProps, ReactNode } from 'react'; import { css } from '@patternfly/react-styles'; @@ -8,17 +5,21 @@ import { css } from '@patternfly/react-styles'; * The container for a single message attachment item, typically the FileDetailsLabel component. You must wrap any attachment components in this container. * Use this component within MessageAttachmentsContainer when passing children to Message to customize its structure. */ -export interface MessageAttachmentProps extends HTMLProps { +export interface MessageAttachmentItemProps extends HTMLProps { /** Content to render inside a single attachment container */ children: ReactNode; /** Additional classes applied to the attachment container. */ className?: string; } -export const MessageAttachment: FunctionComponent = ({ children, className, ...props }) => ( +export const MessageAttachmentItem: FunctionComponent = ({ + children, + className, + ...props +}) => (
{children}
); -export default MessageAttachment; +export default MessageAttachmentItem; diff --git a/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.test.tsx similarity index 100% rename from packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.test.tsx rename to packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.test.tsx diff --git a/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx similarity index 79% rename from packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx rename to packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx index 10362b7b5..ffe31ca65 100644 --- a/packages/module/src/Message/MessageAttachmentsContainer/MessageAttachmentsContainer.tsx +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx @@ -1,6 +1,3 @@ -// ============================================================================ -// Message Attachments Container - Container for message attachments -// ============================================================================ import { FunctionComponent, HTMLProps, ReactNode } from 'react'; import { css } from '@patternfly/react-styles'; diff --git a/packages/module/src/Message/MessageLoading.test.tsx b/packages/module/src/Message/MessageLoading.test.tsx new file mode 100644 index 000000000..c610bd56a --- /dev/null +++ b/packages/module/src/Message/MessageLoading.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageLoading from './MessageLoading'; + +test('Renders with pf-chatbot__message-loading class by default', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveClass('pf-chatbot__message-loading', { exact: true }); +}); + +test('Renders with pf-m-primary class when isPrimary is true', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveClass('pf-chatbot__message-loading pf-m-primary'); +}); + +test('Renders loading word when loadingWord is passed', () => { + render(); + expect(screen.getByText('Loading message')).toBeInTheDocument(); +}); + +test('Spreads additional props', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveAttribute('id', 'custom-id'); +}); diff --git a/packages/module/src/Message/MessageLoading.tsx b/packages/module/src/Message/MessageLoading.tsx index 96f8015fa..3f74bca86 100644 --- a/packages/module/src/Message/MessageLoading.tsx +++ b/packages/module/src/Message/MessageLoading.tsx @@ -2,8 +2,23 @@ // Chatbot Main - Message - Processing // ============================================================================ -const MessageLoading = ({ loadingWord, isPrimary }) => ( -
+import { FunctionComponent } from 'react'; +import type { HTMLProps } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * MessageLoading displays a loading animation for messages. + * Use this component when passing children to Message to show a loading state. + */ +export interface MessageLoadingProps extends HTMLProps { + /** Text announced to screen readers during loading. */ + loadingWord?: string; + /** Flag indicating whether primary styling is applied */ + isPrimary?: boolean; +} + +const MessageLoading: FunctionComponent = ({ loadingWord, isPrimary, ...props }) => ( +
{loadingWord} diff --git a/packages/module/src/Message/index.ts b/packages/module/src/Message/index.ts index 6a0852508..6d9b470a3 100644 --- a/packages/module/src/Message/index.ts +++ b/packages/module/src/Message/index.ts @@ -1,17 +1,5 @@ export { default } from './Message'; + export { rehypeCodeBlockToggle } from './Plugins/rehypeCodeBlockToggle'; export * from './Message'; - -// Sub-component containers for flexible message composition -export { default as MessageAndActions } from './MessageAndActions/MessageAndActions'; -export * from './MessageAndActions/MessageAndActions'; - -export { default as MessageAttachmentsContainer } from './MessageAttachmentsContainer/MessageAttachmentsContainer'; -export * from './MessageAttachmentsContainer/MessageAttachmentsContainer'; - -export { default as MessageAttachment } from './MessageAttachment/MessageAttachment'; -export * from './MessageAttachment/MessageAttachment'; - -export { default as ResponseActionsGroups } from './ResponseActionsGroups/ResponseActionsGroups'; -export * from './ResponseActionsGroups/ResponseActionsGroups'; diff --git a/packages/module/src/ResponseActions/ResponseActions.tsx b/packages/module/src/ResponseActions/ResponseActions.tsx index 7598f9227..c1cfbcff8 100644 --- a/packages/module/src/ResponseActions/ResponseActions.tsx +++ b/packages/module/src/ResponseActions/ResponseActions.tsx @@ -42,6 +42,12 @@ export interface ActionProps extends Omit { type ExtendedActionProps = ActionProps & { [key: string]: any; }; + +/** + * The various actions that can be attached to a bot message for users to interact with. + * Use this component when passing children to Message to customize its structure. + */ + export interface ResponseActionProps { /** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */ actions: Record & { diff --git a/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx b/packages/module/src/ResponseActions/ResponseActionsGroups.test.tsx similarity index 100% rename from packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.test.tsx rename to packages/module/src/ResponseActions/ResponseActionsGroups.test.tsx diff --git a/packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx b/packages/module/src/ResponseActions/ResponseActionsGroups.tsx similarity index 100% rename from packages/module/src/Message/ResponseActionsGroups/ResponseActionsGroups.tsx rename to packages/module/src/ResponseActions/ResponseActionsGroups.tsx diff --git a/packages/module/src/ResponseActions/index.ts b/packages/module/src/ResponseActions/index.ts index c429555cb..08a5e9496 100644 --- a/packages/module/src/ResponseActions/index.ts +++ b/packages/module/src/ResponseActions/index.ts @@ -1,3 +1,4 @@ export { default } from './ResponseActions'; export * from './ResponseActions'; +export * from './ResponseActionsGroups'; From c16601fc760fb5b8c4b00dea4981393bb261425a Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 9 Dec 2025 08:15:40 -0500 Subject: [PATCH 3/6] Updated exports, added new example --- .../Messages/MessageWithCustomStructure.tsx | 84 +++++++++++++++++++ .../chatbot/examples/Messages/Messages.md | 72 +++++++++++----- .../src/Message/ErrorMessage/ErrorMessage.tsx | 19 ++++- .../src/Message/MessageAndActions/index.ts | 1 + .../src/Message/MessageAttachments/index.ts | 2 + packages/module/src/Message/MessageInput.tsx | 2 +- .../module/src/Message/MessageLoading.tsx | 2 +- .../module/src/Message/QuickResponse/index.ts | 1 + .../Message/QuickStarts/QuickStartTile.tsx | 2 +- .../module/src/Message/QuickStarts/index.ts | 2 + .../src/Message/UserFeedback/UserFeedback.tsx | 2 +- .../UserFeedback/UserFeedbackComplete.tsx | 5 +- .../module/src/Message/UserFeedback/index.ts | 2 + packages/module/src/Message/index.ts | 9 +- 14 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx create mode 100644 packages/module/src/Message/MessageAndActions/index.ts create mode 100644 packages/module/src/Message/MessageAttachments/index.ts create mode 100644 packages/module/src/Message/QuickResponse/index.ts create mode 100644 packages/module/src/Message/QuickStarts/index.ts create mode 100644 packages/module/src/Message/UserFeedback/index.ts diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx new file mode 100644 index 000000000..d177232ee --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx @@ -0,0 +1,84 @@ +import { FunctionComponent } from 'react'; +import Message, { + ErrorMessage, + MessageAndActions, + MessageAttachmentItem, + MessageAttachmentsContainer, + MessageLoading +} from '@patternfly/chatbot/dist/dynamic/Message'; +import MarkdownContent from '@patternfly/chatbot/dist/dynamic/MarkdownContent'; +import ToolCall from '@patternfly/chatbot/dist/dynamic/ToolCall'; +import ToolResponse from '@patternfly/chatbot/dist/dynamic/ToolResponse'; +import FileDetailsLabel from '@patternfly/chatbot/dist/dynamic/FileDetailsLabel'; +import ResponseActions, { ResponseActionsGroups } from '@patternfly/chatbot/dist/dynamic/ResponseActions'; +import patternflyAvatar from './patternfly_avatar.jpg'; +import userAvatar from './user_avatar.svg'; + +export const MessageWithCustomStructure: FunctionComponent = () => ( + <> + + + + + + + + + + + + + console.log('Good response'), ariaLabel: 'Good response' }, + negative: { onClick: () => console.log('Bad response'), ariaLabel: 'Bad response' } + }} + persistActionSelection={true} + /> + console.log('Copied!'), ariaLabel: 'Copy' }, + download: { onClick: () => console.log('Downloaded!'), ariaLabel: 'Download' } + }} + persistActionSelection={false} + /> + console.log('Listening'), ariaLabel: 'Listen' } + }} + persistActionSelection={true} + /> + + + + + + + + + + + + + + + + + + + + +); 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 dd2258c65..4ccdc0781 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 @@ -12,41 +12,42 @@ source: react # These are found through the sourceProps function provided in patternfly-docs.source.js propComponents: [ - 'ActionProps', 'AttachMenu', 'AttachmentEdit', - 'FileDetailsProps', - 'FileDetailsLabelProps', 'FileDropZone', 'Message', - 'MessageAndActions', - 'ResponseActionsGroups', - 'ResponseActions', - 'MessageAttachmentsContainer', - 'MessageAttachmentItem', - 'MessageExtraContent', - 'MessageInput', - 'MessageLoading', 'ErrorMessage', - 'PreviewAttachment', - 'QuickResponse', - 'QuickStartTile', - 'QuickStart', - 'QuickStartAction', + 'MessageLoadingProps', + 'MessageInputProps', + 'MessageAndActionsProps', + 'MarkdownContent', + 'QuickResponseProps', + 'QuickStartTileProps', + 'UserFeedback', + 'UserFeedbackComplete', 'DeepThinking', 'ToolCall', 'ToolResponse', 'SourcesCard', - 'UserFeedback', - 'UserFeedbackComplete', - 'UserFeedbackProps', - 'UserFeedbackCompleteProps' + 'ResponseActionsGroupsProps', + 'ResponseActionProps', + 'ActionProps', + 'MessageAttachmentsContainerProps', + 'MessageAttachmentItemProps', + 'FileDetailsProps', + 'FileDetailsLabelProps', + 'MessageExtraContent', + 'PreviewAttachment' ] sortValue: 3 --- -import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import Message, { ErrorMessage, MessageAndActions, MessageLoading, MessageAttachmentItem, MessageAttachmentsContainer } from '@patternfly/chatbot/dist/dynamic/Message'; +import MarkdownContent from '@patternfly/chatbot/dist/dynamic/MarkdownContent'; import MessageDivider from '@patternfly/chatbot/dist/dynamic/MessageDivider'; +import ToolCall from '@patternfly/chatbot/dist/dynamic/ToolCall'; +import ResponseActions, { ResponseActionsGroups } from '@patternfly/chatbot/dist/dynamic/ResponseActions'; +import ToolResponse from '@patternfly/chatbot/dist/dynamic/ToolResponse'; import { rehypeCodeBlockToggle } from '@patternfly/chatbot/dist/esm/Message/Plugins/rehypeCodeBlockToggle'; import SourcesCard from '@patternfly/chatbot/dist/dynamic/SourcesCard'; import { ArrowCircleDownIcon, ArrowRightIcon, CheckCircleIcon, CopyIcon, CubeIcon, CubesIcon, DownloadIcon, InfoCircleIcon, OutlinedQuestionCircleIcon, RedoIcon, RobotIcon, WrenchIcon } from '@patternfly/react-icons'; @@ -286,6 +287,35 @@ You can add custom content to specific parts of a `` via the `extraCont ``` +### Custom message structure + +For more advanced use cases, you can build completely custom message structures by passing children directly to ``. This approach is useful when you need to customize the order or structure of message elements beyond what the standard props allow. + +When creating custom message structures, you must follow an intended composable structure: + +1. **Message content and actions** must be wrapped in ``. This includes, but is not limited to: + + - `` - For rendering markdown or plain text content + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` and `` + - `` + - `` and `` + +2. **File attachments** must be placed outside ``, wrapped in attachment containers: + - `` - Container for all attachments + - `` - Individual attachment wrapper (contains `` or other attachment components) + +```ts file="./MessageWithCustomStructure.tsx" + +``` + ## File attachments ### Messages with attachments diff --git a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx index 000f2a7bc..8ec94da67 100644 --- a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx +++ b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx @@ -4,8 +4,23 @@ import { Alert, AlertProps } from '@patternfly/react-core'; -const ErrorMessage = ({ title, actionLinks, children, ...props }: AlertProps) => ( - +/** + * ErrorMessage displays an inline danger alert for error states in messages. + * Use this component when passing children to Message to display error information. + */ +export interface ErrorMessageProps extends Partial { + /** Title of the error alert */ + title?: React.ReactNode; + /** Action links to display in the alert footer */ + actionLinks?: React.ReactNode; + /** Content to display in the error alert body */ + children?: React.ReactNode; + /** Additional classes for the error alert */ + className?: string; +} + +export const ErrorMessage = ({ title, actionLinks, children, className, ...props }: ErrorMessageProps) => ( + {children} ); diff --git a/packages/module/src/Message/MessageAndActions/index.ts b/packages/module/src/Message/MessageAndActions/index.ts new file mode 100644 index 000000000..4b01abd71 --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/index.ts @@ -0,0 +1 @@ +export * from './MessageAndActions'; diff --git a/packages/module/src/Message/MessageAttachments/index.ts b/packages/module/src/Message/MessageAttachments/index.ts new file mode 100644 index 000000000..95b4fbae1 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/index.ts @@ -0,0 +1,2 @@ +export * from './MessageAttachmentItem'; +export * from './MessageAttachmentsContainer'; diff --git a/packages/module/src/Message/MessageInput.tsx b/packages/module/src/Message/MessageInput.tsx index 02f42c6b0..9cad0eb11 100644 --- a/packages/module/src/Message/MessageInput.tsx +++ b/packages/module/src/Message/MessageInput.tsx @@ -22,7 +22,7 @@ export interface MessageInputProps extends FormProps { content?: string; } -const MessageInput: FunctionComponent = ({ +export const MessageInput: FunctionComponent = ({ editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', diff --git a/packages/module/src/Message/MessageLoading.tsx b/packages/module/src/Message/MessageLoading.tsx index 3f74bca86..61faa0c18 100644 --- a/packages/module/src/Message/MessageLoading.tsx +++ b/packages/module/src/Message/MessageLoading.tsx @@ -17,7 +17,7 @@ export interface MessageLoadingProps extends HTMLProps { isPrimary?: boolean; } -const MessageLoading: FunctionComponent = ({ loadingWord, isPrimary, ...props }) => ( +export const MessageLoading: FunctionComponent = ({ loadingWord, isPrimary, ...props }) => (
{loadingWord} diff --git a/packages/module/src/Message/QuickResponse/index.ts b/packages/module/src/Message/QuickResponse/index.ts new file mode 100644 index 000000000..edcff972b --- /dev/null +++ b/packages/module/src/Message/QuickResponse/index.ts @@ -0,0 +1 @@ +export * from './QuickResponse'; diff --git a/packages/module/src/Message/QuickStarts/QuickStartTile.tsx b/packages/module/src/Message/QuickStarts/QuickStartTile.tsx index ae8528f5d..c154674e7 100644 --- a/packages/module/src/Message/QuickStarts/QuickStartTile.tsx +++ b/packages/module/src/Message/QuickStarts/QuickStartTile.tsx @@ -53,7 +53,7 @@ export interface QuickStartTileProps { isCompact?: boolean; } -const QuickStartTile: FC = ({ +export const QuickStartTile: FC = ({ className, quickStart, onClick, diff --git a/packages/module/src/Message/QuickStarts/index.ts b/packages/module/src/Message/QuickStarts/index.ts new file mode 100644 index 000000000..a7688a023 --- /dev/null +++ b/packages/module/src/Message/QuickStarts/index.ts @@ -0,0 +1,2 @@ +export * from './QuickStartTile'; +export * from './types'; diff --git a/packages/module/src/Message/UserFeedback/UserFeedback.tsx b/packages/module/src/Message/UserFeedback/UserFeedback.tsx index f71854b99..39a524161 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedback.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedback.tsx @@ -78,7 +78,7 @@ export interface UserFeedbackProps extends Omit, OUIAProp privacyStatement?: string; } -const UserFeedback: FunctionComponent = ({ +export const UserFeedback: FunctionComponent = ({ className, timestamp, title = 'Why did you choose this rating?', diff --git a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx index 4fc50abc1..6b13e3751 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx @@ -2,10 +2,7 @@ // Chatbot Main - Messages - Feedback Complete Card // ============================================================================ import type { MouseEvent as ReactMouseEvent, FunctionComponent } from 'react'; - import { useState, useRef, useEffect } from 'react'; - -// Import PatternFly components import { Card, CardBody, CardHeader, CardProps, CardTitle, OUIAProps, useOUIAProps } from '@patternfly/react-core'; import CloseButton from './CloseButton'; @@ -48,7 +45,7 @@ export interface UserFeedbackCompleteProps extends Omit, OUIAP timestamp?: string; } -const UserFeedbackComplete: FunctionComponent = ({ +export const UserFeedbackComplete: FunctionComponent = ({ className, title = 'Feedback submitted', body = "We've received your response. Thank you for sharing your feedback!", diff --git a/packages/module/src/Message/UserFeedback/index.ts b/packages/module/src/Message/UserFeedback/index.ts new file mode 100644 index 000000000..26df8cba9 --- /dev/null +++ b/packages/module/src/Message/UserFeedback/index.ts @@ -0,0 +1,2 @@ +export * from './UserFeedback'; +export * from './UserFeedbackComplete'; diff --git a/packages/module/src/Message/index.ts b/packages/module/src/Message/index.ts index 6d9b470a3..606605d8a 100644 --- a/packages/module/src/Message/index.ts +++ b/packages/module/src/Message/index.ts @@ -1,5 +1,12 @@ export { default } from './Message'; - export { rehypeCodeBlockToggle } from './Plugins/rehypeCodeBlockToggle'; +export * from './ErrorMessage/ErrorMessage'; +export * from './MessageAndActions'; +export * from './MessageAttachments'; export * from './Message'; +export * from './MessageLoading'; +export * from './MessageInput'; +export * from './QuickResponse'; +export * from './QuickStarts'; +export * from './UserFeedback'; From 277063ac74626b0ee56c224dd19899db0673e1c4 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 9 Dec 2025 08:51:38 -0500 Subject: [PATCH 4/6] Added some tests, imported css helper for QuickResponse --- .../ErrorMessage/ErrorMessage.test.tsx | 38 +++++ .../src/Message/ErrorMessage/ErrorMessage.tsx | 8 +- .../QuickResponse/QuickResponse.test.tsx | 131 ++++++++++++++++++ .../Message/QuickResponse/QuickResponse.tsx | 5 +- 4 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx create mode 100644 packages/module/src/Message/QuickResponse/QuickResponse.test.tsx diff --git a/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx b/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx new file mode 100644 index 000000000..5b97acb2d --- /dev/null +++ b/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ErrorMessage from './ErrorMessage'; + +test('Renders with title', () => { + render(); + + expect(screen.getByText('Error occurred')).toBeVisible(); +}); + +test('Renders with children', () => { + render(This is the error message body); + + expect(screen.getByText('This is the error message body')).toBeVisible(); +}); + +test('Renders with action links', () => { + const actionLinks = ( + + Retry action link + + ); + render(); + + expect(screen.getByText('Retry action link')).toBeVisible(); +}); + +test('Renders with custom className', () => { + render(); + + expect(screen.getByText('Error occurred').parentElement).toHaveClass('custom-error-class'); +}); + +test('Renders with spread props', () => { + render(); + + expect(screen.getByText('Error occurred').parentElement).toHaveAttribute('id', 'test-error-id'); +}); diff --git a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx index 8ec94da67..229bb75e3 100644 --- a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx +++ b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx @@ -9,14 +9,14 @@ import { Alert, AlertProps } from '@patternfly/react-core'; * Use this component when passing children to Message to display error information. */ export interface ErrorMessageProps extends Partial { - /** Title of the error alert */ - title?: React.ReactNode; - /** Action links to display in the alert footer */ - actionLinks?: React.ReactNode; /** Content to display in the error alert body */ children?: React.ReactNode; /** Additional classes for the error alert */ className?: string; + /** Title of the error alert */ + title?: React.ReactNode; + /** Action links to display in the alert footer */ + actionLinks?: React.ReactNode; } export const ErrorMessage = ({ title, actionLinks, children, className, ...props }: ErrorMessageProps) => ( diff --git a/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx b/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx new file mode 100644 index 000000000..67bbf2076 --- /dev/null +++ b/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import QuickResponse from './QuickResponse'; + +test('Renders with quick responses', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' }, + { id: '3', content: 'Response 3' } + ]; + render(); + + expect(screen.getByText('Response 1')).toBeVisible(); + expect(screen.getByText('Response 2')).toBeVisible(); + expect(screen.getByText('Response 3')).toBeVisible(); +}); + +test('Renders with compact styling', () => { + const quickResponses = [{ id: '1', content: 'Compact response' }]; + render(); + + expect(screen.getByText('Compact response').closest('.pf-v6-c-label')).toHaveClass('pf-m-compact'); +}); + +test('Renders with custom className on response', () => { + const quickResponses = [{ id: '1', content: 'Custom class response', className: 'custom-response-class' }]; + render(); + + expect(screen.getByText('Custom class response').closest('.pf-v6-c-label')).toHaveClass('custom-response-class'); +}); + +test('Renders with custom container className', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render( + + ); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label-group')).toHaveClass('custom-container-class'); +}); + +test('Spreads additional custom container props', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label-group__list')).toHaveAttribute( + 'id', + 'custom-container-id' + ); +}); + +test('Renders with pf-chatbot__message-quick-response--selected class after click', async () => { + const user = userEvent.setup(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 1')); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label')).toHaveClass( + 'pf-chatbot__message-quick-response--selected' + ); +}); + +test('Does not calls onClick handler when not passed', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const quickResponses = [{ id: '1', content: 'Clickable response' }]; + render(); + + await user.click(screen.getByText('Clickable response')); + + expect(handleClick).not.toHaveBeenCalled(); +}); + +test('Calls onClick handler when passed', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const quickResponses = [{ id: '1', content: 'Clickable response', onClick: handleClick }]; + render(); + + await user.click(screen.getByText('Clickable response')); + + expect(handleClick).toHaveBeenCalled(); +}); + +test('Does not call onSelect when not passed', async () => { + const user = userEvent.setup(); + const handleSelect = jest.fn(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 2')); + + expect(handleSelect).not.toHaveBeenCalled(); +}); + +test('Calls onSelect when passed', async () => { + const user = userEvent.setup(); + const handleSelect = jest.fn(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 2')); + + expect(handleSelect).toHaveBeenCalledWith('2'); +}); + +test('Spreads additional response props', () => { + const quickResponses = [{ id: '1', content: 'Response with props', isCompact: true, 'aria-label': 'Test label' }]; + render(); + + expect(screen.getByText('Response with props').closest('.pf-v6-c-label')).toHaveAttribute('aria-label', 'Test label'); +}); diff --git a/packages/module/src/Message/QuickResponse/QuickResponse.tsx b/packages/module/src/Message/QuickResponse/QuickResponse.tsx index 36e62cdbd..033cdd9fa 100644 --- a/packages/module/src/Message/QuickResponse/QuickResponse.tsx +++ b/packages/module/src/Message/QuickResponse/QuickResponse.tsx @@ -2,6 +2,7 @@ import type { FunctionComponent } from 'react'; import { useState } from 'react'; import { Label, LabelGroup, LabelGroupProps, LabelProps } from '@patternfly/react-core'; import { CheckIcon } from '@patternfly/react-icons'; +import { css } from '@patternfly/react-styles'; export interface QuickResponse extends Omit { content: string; @@ -35,7 +36,7 @@ export const QuickResponse: FunctionComponent = ({ }; return ( {quickResponses.map(({ id, onClick, content, className, ...props }: QuickResponse) => ( @@ -45,7 +46,7 @@ export const QuickResponse: FunctionComponent = ({ color="blue" key={id} onClick={() => handleQuickResponseClick(id, onClick)} - className={`${id === selectedQuickResponse ? 'pf-chatbot__message-quick-response--selected' : ''} ${className ? className : ''}`} + className={css(id === selectedQuickResponse && 'pf-chatbot__message-quick-response--selected', className)} isCompact={isCompact} {...props} > From d545a1a27b117d7aac575f40d7d9577d37a8cd82 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 9 Dec 2025 08:56:22 -0500 Subject: [PATCH 5/6] Removed consoles --- .../Messages/MessageWithCustomStructure.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx index d177232ee..60b8470d5 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx @@ -14,6 +14,26 @@ import ResponseActions, { ResponseActionsGroups } from '@patternfly/chatbot/dist import patternflyAvatar from './patternfly_avatar.jpg'; import userAvatar from './user_avatar.svg'; +const handlePositiveResponse = () => { + // Handle positive response +}; + +const handleNegativeResponse = () => { + // Handle negative response +}; + +const handleCopy = () => { + // Handle copy action +}; + +const handleDownload = () => { + // Handle download action +}; + +const handleListen = () => { + // Handle listen action +}; + export const MessageWithCustomStructure: FunctionComponent = () => ( <> @@ -43,21 +63,21 @@ export const MessageWithCustomStructure: FunctionComponent = () => ( console.log('Good response'), ariaLabel: 'Good response' }, - negative: { onClick: () => console.log('Bad response'), ariaLabel: 'Bad response' } + positive: { onClick: handlePositiveResponse, ariaLabel: 'Good response' }, + negative: { onClick: handleNegativeResponse, ariaLabel: 'Bad response' } }} persistActionSelection={true} /> console.log('Copied!'), ariaLabel: 'Copy' }, - download: { onClick: () => console.log('Downloaded!'), ariaLabel: 'Download' } + copy: { onClick: handleCopy, ariaLabel: 'Copy' }, + download: { onClick: handleDownload, ariaLabel: 'Download' } }} persistActionSelection={false} /> console.log('Listening'), ariaLabel: 'Listen' } + listen: { onClick: handleListen, ariaLabel: 'Listen' } }} persistActionSelection={true} /> From ccb4163314b9feebdd11dda333eb2ed920984405 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 10 Dec 2025 14:38:29 -0500 Subject: [PATCH 6/6] Erin feedback --- .../examples/Messages/MessageWithCustomStructure.tsx | 4 +--- .../extensions/chatbot/examples/Messages/Messages.md | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx index 60b8470d5..e49187b9d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx @@ -56,10 +56,8 @@ export const MessageWithCustomStructure: FunctionComponent = () => ( /> + - ` via the `extraCont For more advanced use cases, you can build completely custom message structures by passing children directly to ``. This approach is useful when you need to customize the order or structure of message elements beyond what the standard props allow. -When creating custom message structures, you must follow an intended composable structure: +When creating custom message structures, you must follow an intended composable structure. -1. **Message content and actions** must be wrapped in ``. This includes, but is not limited to: +1. **Message content and actions:** Wrap in ``. This includes, but is not limited to: - - `` - For rendering markdown or plain text content + - ``: For rendering markdown or plain text content - `` - `` - `` @@ -308,9 +308,9 @@ When creating custom message structures, you must follow an intended composable - `` - `` and `` -2. **File attachments** must be placed outside ``, wrapped in attachment containers: - - `` - Container for all attachments - - `` - Individual attachment wrapper (contains `` or other attachment components) +2. **File attachments:** Placed outside `` and wrapped in attachment containers: + - ``: Container for all attachments + - ``: Individual attachment wrapper (contains `` or other attachment components) ```ts file="./MessageWithCustomStructure.tsx"