From 4dcfd53823b9d7db890e6965dc6eaa62da4c6374 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 13 Nov 2025 15:03:22 -0500 Subject: [PATCH 1/7] feat(ResponseActions): allowed grouped actions for multiple persistence --- .../MessageWithMultipleActionGroups.tsx | 67 ++++++ .../chatbot/examples/Messages/Messages.md | 18 ++ packages/module/src/Message/Message.test.tsx | 200 +++++++++++++++++- packages/module/src/Message/Message.tsx | 42 +++- 4 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx new file mode 100644 index 000000000..f42d16225 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx @@ -0,0 +1,67 @@ +import { FunctionComponent } from 'react'; + +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithMultipleActionGroups: FunctionComponent = () => ( + <> + console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') } + }, + persistActionSelection: true + }, + { + actions: { + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') } + }, + persistActionSelection: false + }, + { + actions: { + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + }, + persistActionSelection: true + } + ]} + /> + console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') } + }, + { + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') } + }, + { + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('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 a79efe236..8d0b0d4d4 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 @@ -122,6 +122,24 @@ When `persistActionSelection` is `true`: ``` +### Multiple action groups + +You can display multiple groups of response actions by passing an array of action group objects to the `actions` prop. This allows you to organize related actions together and apply different persistence settings to each group. + +When using multiple action groups: + +- Pass an array of objects to `actions`, where each object contains: + - `actions`: An object containing the actions for that group (same format as single action object) + - `shouldSelectionPersist` (optional): A boolean to control whether selections persist for this specific group +- Each group renders as a separate `` component wrapped in a PatternFly `` layout component. +- All action groups will render inline on the same row with consistent spacing. + +This is useful when you want to separate feedback actions (like thumbs up/down) from utility actions (like copy, download, regenerate) and have different selection behaviors for each group. + +```js file="./MessageWithMultipleActionGroups.tsx" + +``` + ### Custom message actions Beyond the standard message actions (good response, bad response, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `` component. This object can contain the following customizations: diff --git a/packages/module/src/Message/Message.test.tsx b/packages/module/src/Message/Message.test.tsx index 351242fdd..01b7703ad 100644 --- a/packages/module/src/Message/Message.test.tsx +++ b/packages/module/src/Message/Message.test.tsx @@ -437,7 +437,7 @@ describe('Message', () => { expect(screen.queryByRole('button', { name: /No/i })).toBeFalsy(); expect(screen.getByRole('button', { name: /1 more/i })); }); - it('should be able to show actions', async () => { + it('Renders response actions when a single actions object is passed', async () => { render( { /> ); ALL_ACTIONS.forEach(({ label }) => { - expect(screen.getByRole('button', { name: label })).toBeTruthy(); + expect(screen.getByRole('button', { name: label })).toBeVisible(); }); }); + it('Renders response actions when an array of actions objects is passed', async () => { + render( + console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') } + }, + { + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + edit: { onClick: () => console.log('Edit') }, + // eslint-disable-next-line no-console + share: { onClick: () => console.log('Share') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') } + }, + { + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + } + ]} + /> + ); + ALL_ACTIONS.forEach(({ label }) => { + expect(screen.getByRole('button', { name: label })).toBeVisible(); + }); + }); + it('Renders response actions when an array of objects containing actions objects is passed', async () => { + render( + console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') } + } + }, + { + actions: { + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + edit: { onClick: () => console.log('Edit') }, + // eslint-disable-next-line no-console + share: { onClick: () => console.log('Share') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') } + } + }, + { + actions: { + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + } + } + ]} + /> + ); + ALL_ACTIONS.forEach(({ label }) => { + expect(screen.getByRole('button', { name: label })).toBeVisible(); + }); + }); + + it('should handle persistActionSelection correctly when a single actions object is passed', async () => { + render( + + ); + const goodBtn = screen.getByRole('button', { name: /Good response/i }); + const badBtn = screen.getByRole('button', { name: /Bad response/i }); + + await userEvent.click(goodBtn); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + + await userEvent.click(screen.getByText('Test message')); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + + await userEvent.click(badBtn); + expect(screen.getByRole('button', { name: /Bad response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + }); + + it('should handle persistActionSelection correctly when an array of actions objects is passed', async () => { + render( + + ); + const goodBtn = screen.getByRole('button', { name: /Good response/i }); + const copyBtn = screen.getByRole('button', { name: /Copy/i }); + + await userEvent.click(goodBtn); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + + await userEvent.click(screen.getByText('Test message')); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + + await userEvent.click(copyBtn); + expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked'); + + await userEvent.click(screen.getByText('Test message')); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked'); + }); + + it('should handle persistActionSelection correctly when an array of objects containing actions objects is passed', async () => { + render( + + ); + const goodBtn = screen.getByRole('button', { name: /Good response/i }); + const copyBtn = screen.getByRole('button', { name: /Copy/i }); + + await userEvent.click(goodBtn); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + + await userEvent.click(copyBtn); + expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked'); + + await userEvent.click(screen.getByText('Test message')); + expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + expect(copyBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + }); + it('should not show actions if loading', async () => { render( { expect(screen.queryByRole('button', { name: label })).toBeFalsy(); }); }); + it('should render unordered lists correctly', () => { render(); expect(screen.getByText('Here is an unordered list:')).toBeTruthy(); diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 4cbc8fc12..46a0c6088 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -11,6 +11,7 @@ import { AvatarProps, ButtonProps, ContentVariants, + Flex, FormProps, Label, LabelGroupProps, @@ -104,12 +105,27 @@ export interface MessageProps extends Omit, 'role'> { isLoading?: boolean; /** Array of attachments attached to a message */ attachments?: MessageAttachment[]; - /** Props for message actions, such as feedback (positive or negative), copy button, edit message, share, and listen */ - actions?: { - [key: string]: ActionProps; - }; + /** Props for message actions, such as feedback (positive or negative), copy button, edit message, share, and listen. + * Can be a single actions object or an array of action group objects. When passing an array, you can pass an object of actions or + * an object that contains an actions property for finer control of selection persistence. + */ + actions?: + | { + [key: string]: ActionProps; + } + | { + [key: string]: ActionProps; + }[] + | { + actions: { + [key: string]: ActionProps; + }; + persistActionSelection?: boolean; + }[]; /** When true, the selected action will persist even when clicking outside the component. - * When false (default), clicking outside or clicking another action will deselect the current selection. */ + * When false (default), clicking outside or clicking another action will deselect the current selection. + * For finer control of multiple action groups, use persistActionSelection on each group. + */ persistActionSelection?: boolean; /** Sources for message */ sources?: SourcesCardProps; @@ -506,7 +522,21 @@ export const MessageBase: FunctionComponent = ({ /> )} {!isLoading && !isEditable && actions && ( - + <> + {Array.isArray(actions) ? ( + + {actions.map((actionGroup, index) => ( + + ))} + + ) : ( + + )} + )} {userFeedbackForm && } {userFeedbackComplete && ( From 73b48b132a6cd6446d3c1348b68dd56389f74e87 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 13 Nov 2025 15:17:48 -0500 Subject: [PATCH 2/7] Updated example verbiage --- .../chatbot/examples/Messages/Messages.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 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 8d0b0d4d4..41aa4890f 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 @@ -124,17 +124,14 @@ When `persistActionSelection` is `true`: ### Multiple action groups -You can display multiple groups of response actions by passing an array of action group objects to the `actions` prop. This allows you to organize related actions together and apply different persistence settings to each group. +You can display multiple groups of response actions by passing an array of objects to the `actions` prop. This allows you to separate groups of actions, such as feedback actions (thumbs up/down) and utility actions (copy, download, and regenerate), and have different selection behaviors for each group. -When using multiple action groups: +When passing an array of objects to `actions`, you have two options depending on the level of flexibility you may need: -- Pass an array of objects to `actions`, where each object contains: - - `actions`: An object containing the actions for that group (same format as single action object) - - `shouldSelectionPersist` (optional): A boolean to control whether selections persist for this specific group -- Each group renders as a separate `` component wrapped in a PatternFly `` layout component. -- All action groups will render inline on the same row with consistent spacing. - -This is useful when you want to separate feedback actions (like thumbs up/down) from utility actions (like copy, download, regenerate) and have different selection behaviors for each group. +- Pass an array of objects where each object contains: + - `actions`: An object containing the actions for that group (the same format as a single action object) + - `persistActionSelection` (optional): A boolean to control whether selections persists for this specific group +- Pass an array of actions objects (the same format as a single action object), and optionally the `persistActionSelection` property to apply to all groups. ```js file="./MessageWithMultipleActionGroups.tsx" From d0b3319d81d77374a7b9d970906e83d5bd398c3b Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 13 Nov 2025 15:21:01 -0500 Subject: [PATCH 3/7] Removed copy/download from second message in example --- .../examples/Messages/MessageWithMultipleActionGroups.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx index f42d16225..96ba82289 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx @@ -50,12 +50,6 @@ export const MessageWithMultipleActionGroups: FunctionComponent = () => ( // eslint-disable-next-line no-console negative: { onClick: () => console.log('Bad response') } }, - { - // eslint-disable-next-line no-console - copy: { onClick: () => console.log('Copy') }, - // eslint-disable-next-line no-console - download: { onClick: () => console.log('Download') } - }, { // eslint-disable-next-line no-console listen: { onClick: () => console.log('Listen') } From caa7d8870fe551478ccf4e97467110b2a10dde1b Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 14 Nov 2025 15:11:43 -0500 Subject: [PATCH 4/7] Verbiage updates from Erin --- .../MessageWithMultipleActionGroups.tsx | 4 ++-- .../chatbot/examples/Messages/Messages.md | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx index 96ba82289..99c32c56d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx @@ -9,7 +9,7 @@ export const MessageWithMultipleActionGroups: FunctionComponent = () => ( name="Bot" role="bot" avatar={patternflyAvatar} - content="This message contains multiple action groups, each with their own selection persistence. The first group contains feedback actions with persistent selections, the second group contains utility actions with non-persistent selections, and the third has a listen action with persistent selection." + content="This message contains multiple action groups, each with their own selection persistence. \n1. Feedback actions (thumbs up/down) with persistent selections \n2. Utility actions (copy, download) with non-persistent selections \n3. Listen action with persistent selection." actions={[ { actions: { @@ -42,7 +42,7 @@ export const MessageWithMultipleActionGroups: FunctionComponent = () => ( name="Bot" role="bot" avatar={patternflyAvatar} - content="This message contains multiple action groups with the same selection persistence applied to each group." + content="This message contains multiple action groups, both of which persist selections." actions={[ { // eslint-disable-next-line no-console 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 41aa4890f..101a8ff73 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 @@ -122,16 +122,19 @@ When `persistActionSelection` is `true`: ``` -### Multiple action groups +### Multiple messsage action groups -You can display multiple groups of response actions by passing an array of objects to the `actions` prop. This allows you to separate groups of actions, such as feedback actions (thumbs up/down) and utility actions (copy, download, and regenerate), and have different selection behaviors for each group. +To maintain finer control over message action selection behavior, you can create groups of actions by passing an array of objects to the `actions` prop. This allows you to separate actions into conceptually or functionally different groups and implement different behavior for each group as needed. For example, you could separate feedback actions (thumbs up/down) form utility actions (copy and download), and have different selection behaviors for each group. -When passing an array of objects to `actions`, you have two options depending on the level of flexibility you may need: +To provide flexibility for your use case, there are 2 approaches you can take to pass an array of objects to `actions`: -- Pass an array of objects where each object contains: - - `actions`: An object containing the actions for that group (the same format as a single action object) - - `persistActionSelection` (optional): A boolean to control whether selections persists for this specific group -- Pass an array of actions objects (the same format as a single action object), and optionally the `persistActionSelection` property to apply to all groups. +1. Pass an array of objects, where each object contains: + + - `actions`: An `action` object containing the actions for that group (the same format as a single `action` object) + + - `persistActionSelection` (optional): A boolean to control whether selections persists for this specific group + +2. Pass an array of `action` objects (the same format as a single `action` object) and (optionally) a value for the `persistActionSelection` property that will apply to all groups. ```js file="./MessageWithMultipleActionGroups.tsx" From 44f4092d871c4cf57bfc2ab6426de396eae779e4 Mon Sep 17 00:00:00 2001 From: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:28:07 -0500 Subject: [PATCH 5/7] Update packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx Co-authored-by: Erin Donehoo <105813956+edonehoo@users.noreply.github.com> --- .../examples/Messages/MessageWithMultipleActionGroups.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx index 99c32c56d..4f4d6269b 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMultipleActionGroups.tsx @@ -9,7 +9,7 @@ export const MessageWithMultipleActionGroups: FunctionComponent = () => ( name="Bot" role="bot" avatar={patternflyAvatar} - content="This message contains multiple action groups, each with their own selection persistence. \n1. Feedback actions (thumbs up/down) with persistent selections \n2. Utility actions (copy, download) with non-persistent selections \n3. Listen action with persistent selection." + content="This message contains multiple action groups, each with their own selection persistence: \n1. Feedback actions (thumbs up/down) with persistent selections \n2. Utility actions (copy, download) with non-persistent selections \n3. Listen action with persistent selection" actions={[ { actions: { From 19f0079d9d1f5c65e5b8b65950974b738ed0dddc Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 21 Nov 2025 16:04:59 -0500 Subject: [PATCH 6/7] Fixed visual discrepancy in Firefox --- packages/module/src/Message/Message.tsx | 4 ++-- .../module/src/ResponseActions/ResponseActions.scss | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 46a0c6088..56002c3b3 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -524,7 +524,7 @@ export const MessageBase: FunctionComponent = ({ {!isLoading && !isEditable && actions && ( <> {Array.isArray(actions) ? ( - +
{actions.map((actionGroup, index) => ( = ({ persistActionSelection={persistActionSelection || actionGroup.persistActionSelection} /> ))} - +
) : ( )} diff --git a/packages/module/src/ResponseActions/ResponseActions.scss b/packages/module/src/ResponseActions/ResponseActions.scss index cf8b740dd..baa6f63b2 100644 --- a/packages/module/src/ResponseActions/ResponseActions.scss +++ b/packages/module/src/ResponseActions/ResponseActions.scss @@ -22,6 +22,17 @@ } } +.pf-chatbot__response-actions-groups { + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + gap: var(--pf-t--global--spacer--xs); + + .pf-chatbot__response-actions { + display: flex; + } +} + .pf-v6-c-button.pf-chatbot__button--response-action-clicked.pf-v6-c-button.pf-m-plain.pf-m-small { --pf-v6-c-button--m-plain--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked); --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular); From 0b1a48f6b469cdf5cdfa8f6f29e64597335e8e6f Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Sat, 22 Nov 2025 07:38:53 -0500 Subject: [PATCH 7/7] Removed unused import --- packages/module/src/Message/Message.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 56002c3b3..c487c59b2 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -11,7 +11,6 @@ import { AvatarProps, ButtonProps, ContentVariants, - Flex, FormProps, Label, LabelGroupProps,