@patternfly/chatbot 6.6.0-prerelease.5 → 6.6.0-prerelease.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +1 -1
  2. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +1 -1
  3. package/dist/cjs/ChatbotConversationHistoryNav/LoadingState.js +1 -1
  4. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
  5. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
  6. package/dist/cjs/Message/Message.d.ts +4 -0
  7. package/dist/cjs/Message/Message.js +2 -2
  8. package/dist/cjs/Message/Message.test.js +72 -0
  9. package/dist/cjs/ResponseActions/ResponseActions.d.ts +6 -0
  10. package/dist/cjs/ResponseActions/ResponseActions.js +12 -5
  11. package/dist/cjs/ResponseActions/ResponseActions.test.js +28 -0
  12. package/dist/css/main.css +27 -10
  13. package/dist/css/main.css.map +1 -1
  14. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +1 -1
  15. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +1 -1
  16. package/dist/esm/ChatbotConversationHistoryNav/LoadingState.js +1 -1
  17. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
  18. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
  19. package/dist/esm/Message/Message.d.ts +4 -0
  20. package/dist/esm/Message/Message.js +2 -2
  21. package/dist/esm/Message/Message.test.js +72 -0
  22. package/dist/esm/ResponseActions/ResponseActions.d.ts +6 -0
  23. package/dist/esm/ResponseActions/ResponseActions.js +12 -5
  24. package/dist/esm/ResponseActions/ResponseActions.test.js +28 -0
  25. package/package.json +1 -1
  26. package/patternfly-docs/content/extensions/chatbot/chatbot.md +1 -1
  27. package/patternfly-docs/content/extensions/chatbot/design-guidelines.md +10 -12
  28. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx +39 -18
  29. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +9 -8
  30. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderBasic.tsx +1 -1
  31. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +16 -16
  32. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +3 -3
  33. package/src/Chatbot/Chatbot.scss +8 -1
  34. package/src/ChatbotContent/ChatbotContent.scss +5 -1
  35. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +1 -1
  36. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +1 -1
  37. package/src/ChatbotConversationHistoryNav/LoadingState.tsx +1 -5
  38. package/src/ChatbotFooter/ChatbotFooter.scss +8 -1
  39. package/src/ChatbotHeader/ChatbotHeader.scss +5 -2
  40. package/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx +1 -1
  41. package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +2 -2
  42. package/src/Message/Message.scss +12 -0
  43. package/src/Message/Message.test.tsx +136 -0
  44. package/src/Message/Message.tsx +12 -1
  45. package/src/MessageBar/MessageBar.scss +8 -5
  46. package/src/ResponseActions/ResponseActions.test.tsx +59 -0
  47. package/src/ResponseActions/ResponseActions.tsx +35 -10
@@ -33,7 +33,7 @@ import ToolCall from '../ToolCall';
33
33
  import MarkdownContent from '../MarkdownContent';
34
34
  import { css } from '@patternfly/react-styles';
35
35
  export const MessageBase = (_a) => {
36
- var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
36
+ var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, showActionsOnInteraction = false, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "showActionsOnInteraction", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
37
37
  const [messageText, setMessageText] = useState(content);
38
38
  useEffect(() => {
39
39
  setMessageText(content);
@@ -61,7 +61,7 @@ export const MessageBase = (_a) => {
61
61
  }
62
62
  return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? _jsx(ErrorMessage, Object.assign({}, error)) : handleMarkdown()] }));
63
63
  };
64
- return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className: "pf-chatbot__response-actions-groups", children: actions.map((actionGroup, index) => (_jsx(ResponseActions, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick }, index))) })) : (_jsx(ResponseActions, { actions: actions, persistActionSelection: persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick })) })), userFeedbackForm && (_jsx(UserFeedback, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact }))), userFeedbackComplete && (_jsx(UserFeedbackComplete, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && (_jsx("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
64
+ return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className: css('pf-chatbot__response-actions-groups', showActionsOnInteraction && 'pf-m-visible-interaction'), children: actions.map((actionGroup, index) => (_jsx(ResponseActions, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick }, index))) })) : (_jsx(ResponseActions, { actions: actions, persistActionSelection: persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick, showActionsOnInteraction: showActionsOnInteraction })) })), userFeedbackForm && (_jsx(UserFeedback, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact }))), userFeedbackComplete && (_jsx(UserFeedbackComplete, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && (_jsx("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
65
65
  var _a;
66
66
  return (_jsx("div", { className: "pf-chatbot__message-attachment", children: _jsx(FileDetailsLabel, { fileName: attachment.name, fileId: attachment.id, onClose: attachment.onClose, onClick: attachment.onClick, isLoading: attachment.isLoading, closeButtonAriaLabel: attachment.closeButtonAriaLabel, languageTestId: attachment.languageTestId, spinnerTestId: attachment.spinnerTestId, variant: isPrimary ? 'outline' : undefined }) }, (_a = attachment.id) !== null && _a !== void 0 ? _a : attachment.name));
67
67
  }) })), !isLoading && endContent && _jsx(_Fragment, { children: endContent })] })) })] })] })));
@@ -1036,4 +1036,76 @@ describe('Message', () => {
1036
1036
  expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
1037
1037
  expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
1038
1038
  }));
1039
+ it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
1040
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: {
1041
+ positive: { onClick: jest.fn() }
1042
+ } }));
1043
+ const responseContainer = screen
1044
+ .getByRole('button', { name: 'Good response' })
1045
+ .closest('.pf-chatbot__response-actions');
1046
+ expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1047
+ });
1048
+ it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
1049
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: {
1050
+ positive: { onClick: jest.fn() }
1051
+ } }));
1052
+ const responseContainer = screen
1053
+ .getByRole('button', { name: 'Good response' })
1054
+ .closest('.pf-chatbot__response-actions');
1055
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1056
+ });
1057
+ it('should not apply pf-m-visible-interaction class to response actions by default', () => {
1058
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
1059
+ positive: { onClick: jest.fn() }
1060
+ } }));
1061
+ const responseContainer = screen
1062
+ .getByRole('button', { name: 'Good response' })
1063
+ .closest('.pf-chatbot__response-actions');
1064
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1065
+ });
1066
+ it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
1067
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: [
1068
+ {
1069
+ positive: { onClick: jest.fn() },
1070
+ negative: { onClick: jest.fn() }
1071
+ },
1072
+ {
1073
+ copy: { onClick: jest.fn() }
1074
+ }
1075
+ ] }));
1076
+ const responseContainer = screen
1077
+ .getByRole('button', { name: 'Good response' })
1078
+ .closest('.pf-chatbot__response-actions-groups');
1079
+ expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1080
+ });
1081
+ it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
1082
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: [
1083
+ {
1084
+ positive: { onClick: jest.fn() },
1085
+ negative: { onClick: jest.fn() }
1086
+ },
1087
+ {
1088
+ copy: { onClick: jest.fn() }
1089
+ }
1090
+ ] }));
1091
+ const responseContainer = screen
1092
+ .getByRole('button', { name: 'Good response' })
1093
+ .closest('.pf-chatbot__response-actions-groups');
1094
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1095
+ });
1096
+ it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
1097
+ render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: [
1098
+ {
1099
+ positive: { onClick: jest.fn() },
1100
+ negative: { onClick: jest.fn() }
1101
+ },
1102
+ {
1103
+ copy: { onClick: jest.fn() }
1104
+ }
1105
+ ] }));
1106
+ const responseContainer = screen
1107
+ .getByRole('button', { name: 'Good response' })
1108
+ .closest('.pf-chatbot__response-actions-groups');
1109
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1110
+ });
1039
1111
  });
@@ -34,6 +34,8 @@ type ExtendedActionProps = ActionProps & {
34
34
  * Use this component when passing children to Message to customize its structure.
35
35
  */
36
36
  export interface ResponseActionProps {
37
+ /** Additional classes for the response actions container. */
38
+ className?: string;
37
39
  /** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */
38
40
  actions: Record<string, ExtendedActionProps | undefined> & {
39
41
  positive?: ActionProps;
@@ -50,6 +52,10 @@ export interface ResponseActionProps {
50
52
  /** When true, automatically swaps to filled icon variants when predefined actions are clicked.
51
53
  * Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
52
54
  useFilledIconsOnClick?: boolean;
55
+ /** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
56
+ * that setting this to true will append tooltips inline instead of the document.body.
57
+ */
58
+ showActionsOnInteraction?: boolean;
53
59
  }
54
60
  export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
55
61
  export default ResponseActions;
@@ -14,8 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import { useEffect, useRef, useState } from 'react';
15
15
  import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, ThumbsUpIcon, OutlinedThumbsDownIcon, ThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
16
16
  import ResponseActionButton from './ResponseActionButton';
17
- export const ResponseActions = ({ actions, persistActionSelection = false, useFilledIconsOnClick = false }) => {
18
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
17
+ import { css } from '@patternfly/react-styles';
18
+ export const ResponseActions = (_a) => {
19
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
20
+ var { className, actions, persistActionSelection = false, useFilledIconsOnClick = false, showActionsOnInteraction = false } = _a, props = __rest(_a, ["className", "actions", "persistActionSelection", "useFilledIconsOnClick", "showActionsOnInteraction"]);
19
21
  const [activeButton, setActiveButton] = useState();
20
22
  const [clickStatePersisted, setClickStatePersisted] = useState(false);
21
23
  const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
@@ -98,9 +100,14 @@ export const ResponseActions = ({ actions, persistActionSelection = false, useFi
98
100
  }
99
101
  return iconMap[actionName].outlined;
100
102
  };
101
- return (_jsxs("div", { ref: responseActions, className: "pf-chatbot__response-actions", children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_a = positive.ariaLabel) !== null && _a !== void 0 ? _a : 'Good response', clickedAriaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_c = positive.tooltipContent) !== null && _c !== void 0 ? _c : 'Good response', clickedTooltipContent: (_d = positive.clickedTooltipContent) !== null && _d !== void 0 ? _d : 'Good response recorded', tooltipProps: positive.tooltipProps, icon: getIcon('positive'), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_e = negative.ariaLabel) !== null && _e !== void 0 ? _e : 'Bad response', clickedAriaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_g = negative.tooltipContent) !== null && _g !== void 0 ? _g : 'Bad response', clickedTooltipContent: (_h = negative.clickedTooltipContent) !== null && _h !== void 0 ? _h : 'Bad response recorded', tooltipProps: negative.tooltipProps, icon: getIcon('negative'), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_j = copy.ariaLabel) !== null && _j !== void 0 ? _j : 'Copy', clickedAriaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_l = copy.tooltipContent) !== null && _l !== void 0 ? _l : 'Copy', clickedTooltipContent: (_m = copy.clickedTooltipContent) !== null && _m !== void 0 ? _m : 'Copied', tooltipProps: copy.tooltipProps, icon: _jsx(OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && (_jsx(ResponseActionButton, Object.assign({}, edit, { ariaLabel: (_o = edit.ariaLabel) !== null && _o !== void 0 ? _o : 'Edit', clickedAriaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_q = edit.tooltipContent) !== null && _q !== void 0 ? _q : 'Edit ', clickedTooltipContent: (_r = edit.clickedTooltipContent) !== null && _r !== void 0 ? _r : 'Editing', tooltipProps: edit.tooltipProps, icon: _jsx(PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && (_jsx(ResponseActionButton, Object.assign({}, share, { ariaLabel: (_s = share.ariaLabel) !== null && _s !== void 0 ? _s : 'Share', clickedAriaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_u = share.tooltipContent) !== null && _u !== void 0 ? _u : 'Share', clickedTooltipContent: (_v = share.clickedTooltipContent) !== null && _v !== void 0 ? _v : 'Shared', tooltipProps: share.tooltipProps, icon: _jsx(ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && (_jsx(ResponseActionButton, Object.assign({}, download, { ariaLabel: (_w = download.ariaLabel) !== null && _w !== void 0 ? _w : 'Download', clickedAriaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_y = download.tooltipContent) !== null && _y !== void 0 ? _y : 'Download', clickedTooltipContent: (_z = download.clickedTooltipContent) !== null && _z !== void 0 ? _z : 'Downloaded', tooltipProps: download.tooltipProps, icon: _jsx(DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && (_jsx(ResponseActionButton, Object.assign({}, listen, { ariaLabel: (_0 = listen.ariaLabel) !== null && _0 !== void 0 ? _0 : 'Listen', clickedAriaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_2 = listen.tooltipContent) !== null && _2 !== void 0 ? _2 : 'Listen', clickedTooltipContent: (_3 = listen.clickedTooltipContent) !== null && _3 !== void 0 ? _3 : 'Listening', tooltipProps: listen.tooltipProps, icon: _jsx(VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
103
+ // We want to append the tooltip inline so that hovering the tooltip keeps the actions container visible
104
+ // when showActionsOnInteraction is true. Otherwise hovering the tooltip causes the actions container
105
+ // to disappear but the tooltip will remain visible.
106
+ const getTooltipContainer = () => responseActions.current || document.body;
107
+ const getTooltipProps = (tooltipProps) => (Object.assign(Object.assign({}, (showActionsOnInteraction && { appendTo: getTooltipContainer })), tooltipProps));
108
+ return (_jsxs("div", Object.assign({ ref: responseActions, className: css('pf-chatbot__response-actions', showActionsOnInteraction && 'pf-m-visible-interaction', className) }, props, { children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response', clickedAriaLabel: (_c = positive.ariaLabel) !== null && _c !== void 0 ? _c : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_d = positive.tooltipContent) !== null && _d !== void 0 ? _d : 'Good response', clickedTooltipContent: (_e = positive.clickedTooltipContent) !== null && _e !== void 0 ? _e : 'Good response recorded', tooltipProps: getTooltipProps(positive.tooltipProps), icon: getIcon('positive'), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response', clickedAriaLabel: (_g = negative.ariaLabel) !== null && _g !== void 0 ? _g : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_h = negative.tooltipContent) !== null && _h !== void 0 ? _h : 'Bad response', clickedTooltipContent: (_j = negative.clickedTooltipContent) !== null && _j !== void 0 ? _j : 'Bad response recorded', tooltipProps: getTooltipProps(negative.tooltipProps), icon: getIcon('negative'), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copy', clickedAriaLabel: (_l = copy.ariaLabel) !== null && _l !== void 0 ? _l : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_m = copy.tooltipContent) !== null && _m !== void 0 ? _m : 'Copy', clickedTooltipContent: (_o = copy.clickedTooltipContent) !== null && _o !== void 0 ? _o : 'Copied', tooltipProps: getTooltipProps(copy.tooltipProps), icon: _jsx(OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && (_jsx(ResponseActionButton, Object.assign({}, edit, { ariaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Edit', clickedAriaLabel: (_q = edit.ariaLabel) !== null && _q !== void 0 ? _q : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_r = edit.tooltipContent) !== null && _r !== void 0 ? _r : 'Edit ', clickedTooltipContent: (_s = edit.clickedTooltipContent) !== null && _s !== void 0 ? _s : 'Editing', tooltipProps: getTooltipProps(edit.tooltipProps), icon: _jsx(PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && (_jsx(ResponseActionButton, Object.assign({}, share, { ariaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Share', clickedAriaLabel: (_u = share.ariaLabel) !== null && _u !== void 0 ? _u : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_v = share.tooltipContent) !== null && _v !== void 0 ? _v : 'Share', clickedTooltipContent: (_w = share.clickedTooltipContent) !== null && _w !== void 0 ? _w : 'Shared', tooltipProps: getTooltipProps(share.tooltipProps), icon: _jsx(ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && (_jsx(ResponseActionButton, Object.assign({}, download, { ariaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Download', clickedAriaLabel: (_y = download.ariaLabel) !== null && _y !== void 0 ? _y : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_z = download.tooltipContent) !== null && _z !== void 0 ? _z : 'Download', clickedTooltipContent: (_0 = download.clickedTooltipContent) !== null && _0 !== void 0 ? _0 : 'Downloaded', tooltipProps: getTooltipProps(download.tooltipProps), icon: _jsx(DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && (_jsx(ResponseActionButton, Object.assign({}, listen, { ariaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listen', clickedAriaLabel: (_2 = listen.ariaLabel) !== null && _2 !== void 0 ? _2 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_3 = listen.tooltipContent) !== null && _3 !== void 0 ? _3 : 'Listen', clickedTooltipContent: (_4 = listen.clickedTooltipContent) !== null && _4 !== void 0 ? _4 : 'Listening', tooltipProps: getTooltipProps(listen.tooltipProps), icon: _jsx(VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
102
109
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
103
- return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: (_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps, clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
104
- })] }));
110
+ return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: getTooltipProps((_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps), clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
111
+ })] })));
105
112
  };
106
113
  export default ResponseActions;
@@ -324,6 +324,34 @@ describe('ResponseActions', () => {
324
324
  yield userEvent.click(customBtn);
325
325
  expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
326
326
  }));
327
+ it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
328
+ render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
329
+ positive: { onClick: jest.fn() },
330
+ negative: { onClick: jest.fn() }
331
+ }, showActionsOnInteraction: true }));
332
+ expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
333
+ });
334
+ it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
335
+ render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
336
+ positive: { onClick: jest.fn() },
337
+ negative: { onClick: jest.fn() }
338
+ }, showActionsOnInteraction: false }));
339
+ expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
340
+ });
341
+ it('should not apply pf-m-visible-interaction class by default', () => {
342
+ render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
343
+ positive: { onClick: jest.fn() },
344
+ negative: { onClick: jest.fn() }
345
+ } }));
346
+ expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
347
+ });
348
+ it('should render with custom className', () => {
349
+ render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
350
+ positive: { onClick: jest.fn() },
351
+ negative: { onClick: jest.fn() }
352
+ }, className: "custom-class" }));
353
+ expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
354
+ });
327
355
  describe('icon swapping with useFilledIconsOnClick', () => {
328
356
  it('should render outline icons by default', () => {
329
357
  render(_jsx(ResponseActions, { actions: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.6.0-prerelease.5",
3
+ "version": "6.6.0-prerelease.7",
4
4
  "description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -40,7 +40,7 @@ Explore our documentation, which covers both the components you'll need to build
40
40
  - [Toggle](/extensions/chatbot/ui#toggle)
41
41
  - [Header](/extensions/chatbot/ui#header)
42
42
  - [Footer](/extensions/chatbot/ui#footer)
43
- - [Navigation](/extensions/chatbot/ui#navigation)
43
+ - [Chat history drawer](/extensions/chatbot/ui#chat-history)
44
44
  - [Modals](/extensions/chatbot/ui#modals)
45
45
  - Messages: Components that customize features related to the conversation
46
46
  - [Bot and user messages](/extensions/chatbot/messages)
@@ -15,8 +15,8 @@ import "./images.css"
15
15
  </div>
16
16
 
17
17
  1. **Container:** The window that contains the entire ChatBot experience and all of its components.
18
- 1. **Header:** A persistent region at the top of the ChatBot window that contains navigation, branding, and actions.
19
- 1. **Chat history menu:** A menu that contains a history of previous chats.
18
+ 1. **Header:** A persistent region at the top of the ChatBot window that contains chat history, branding, and actions.
19
+ 1. **Chat history drawer:** A menu used to access previous chats.
20
20
  1. **Options menu:** A menu that contains settings that are relevant to your product. This typically includes display options (more details in the [ChatBot variations section](#variations)) and other general settings (more details in the [ChatBot settings and preferences section](#chatbot-settings-and-preferences)).
21
21
  1. **Messages:** Elements of the conversation between a ChatBot and user. More details can be found in the [message guidelines](#messages).
22
22
  1. **Attachments:** Details about files that a user has uploaded to the ChatBot.
@@ -288,7 +288,7 @@ When a ChatBot is launched via an AI-supported action, the action should be sent
288
288
 
289
289
  Each time a user begins a new chat, display a [welcome message](#welcome-message), with prompts that provide initial suggestions and indicate the actions that the ChatBot can take.
290
290
 
291
- The default approach for users to create a new chat is by clicking the "New chat" button (which contains a "pen to square" icon) placed at the top of the [chat history menu](#using-the-chat-history-menu).
291
+ The default approach for users to create a new chat is by clicking the "New chat" button (which contains a "pen to square" icon) placed at the top of the [chat history drawer](#using-the-chat-history-drawer).
292
292
 
293
293
  <div class="ws-docs-content-img">
294
294
  ![A blue "New chat" button at the top right of a drawer labeled "Chat history".](./img/new-chat-in-nav.svg)
@@ -310,14 +310,12 @@ This can be done using the [quick response](/extensions/chatbot/messages#message
310
310
  ![Confirmation options from a bot in response to a user's request.](./img/quick-response-confirmation.svg)
311
311
  </div>
312
312
 
313
- ### Using the chat history menu
313
+ ### Using the chat history drawer
314
314
 
315
- The ChatBot history menu contains a log of a user's previous chats. Clicking the menu icon opens a side drawer in the ChatBot window.
316
-
317
- By clicking into the history menu, users can search through previous conversations and perform additional actions, such as sharing a conversation with others.
315
+ The chat history drawer can be opened via the hamburger menu in the ChatBot header. In this drawer, users can search through previous conversations and perform additional actions, such as sharing a conversation with others.
318
316
 
319
317
  <div class="ws-docs-content-img">
320
- ![Conversation history with an options menu opened on a previous conversation.](./img/conversation-history.svg)
318
+ ![Chat history with an options menu opened on a previous conversation.](./img/conversation-history.svg)
321
319
  </div>
322
320
 
323
321
  When the chat history is still loading, display skeleton items:
@@ -383,16 +381,16 @@ If a message attachment fails, an error message should share the reason for fail
383
381
 
384
382
  You can enable users to download chat transcripts, for their personal records or to share with others. When users choose to download a transcript, you can choose how you want to configure the behavior in your ChatBot.
385
383
 
386
- For guidance, refer to our download transcripts demo, which opens a Markdown file for a conversation within a new tab.
384
+ For guidance, refer to our [chat transcripts demo](/extensions/chatbot/overview/demo#chat-transcripts), which opens a Markdown file for a conversation within a new tab.
387
385
 
388
386
  Choose the download action location that best works for your ChatBot:
389
387
 
390
388
  #### Download via chat history drawer
391
389
 
392
- If your ChatBot uses a chat history drawer, you can provide a download option in the [actions menu linked to a previous conversation](/extensions/chatbot/ui#drawer-with-conversation-actions).
390
+ If your ChatBot uses chat history, you can provide a download option in the [actions menu linked to a previous conversation](/extensions/chatbot/ui#drawer-with-conversation-actions).
393
391
 
394
392
  <div class="ws-docs-content-img">
395
- ![Expanded menu for previous chat in the history window, which shows a download option.](./img/download-chat-history.svg)
393
+ ![Expanded menu for previous chat in the chat history drawer, which shows a download option.](./img/download-chat-history.svg)
396
394
  </div>
397
395
 
398
396
  #### Download message response action
@@ -405,7 +403,7 @@ To allow users to download individual bot messages, the message actions can incl
405
403
 
406
404
  #### Download control in header
407
405
 
408
- If you don't use a chat history drawer, you can place an option to download the transcript for the active chat within the header options menu.
406
+ If you don't use chat history, you can place an option to download the transcript for the active chat within the header options menu.
409
407
 
410
408
  <div class="ws-docs-content-img">
411
409
  ![Download transcript action within the ChatBot header options menu.](./img/download-header.svg)
@@ -4,22 +4,43 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
4
4
  import patternflyAvatar from './patternfly_avatar.jpg';
5
5
 
6
6
  export const ResponseActionExample: FunctionComponent = () => (
7
- <Message
8
- name="Bot"
9
- role="bot"
10
- avatar={patternflyAvatar}
11
- content="I updated your account with those settings. You're ready to set up your first dashboard!"
12
- actions={{
13
- // eslint-disable-next-line no-console
14
- positive: { onClick: () => console.log('Good response') },
15
- // eslint-disable-next-line no-console
16
- negative: { onClick: () => console.log('Bad response') },
17
- // eslint-disable-next-line no-console
18
- copy: { onClick: () => console.log('Copy') },
19
- // eslint-disable-next-line no-console
20
- download: { onClick: () => console.log('Download') },
21
- // eslint-disable-next-line no-console
22
- listen: { onClick: () => console.log('Listen') }
23
- }}
24
- />
7
+ <>
8
+ <Message
9
+ name="Bot"
10
+ role="bot"
11
+ avatar={patternflyAvatar}
12
+ content="I updated your account with those settings. You're ready to set up your first dashboard!"
13
+ actions={{
14
+ // eslint-disable-next-line no-console
15
+ positive: { onClick: () => console.log('Good response') },
16
+ // eslint-disable-next-line no-console
17
+ negative: { onClick: () => console.log('Bad response') },
18
+ // eslint-disable-next-line no-console
19
+ copy: { onClick: () => console.log('Copy') },
20
+ // eslint-disable-next-line no-console
21
+ download: { onClick: () => console.log('Download') },
22
+ // eslint-disable-next-line no-console
23
+ listen: { onClick: () => console.log('Listen') }
24
+ }}
25
+ />
26
+ <Message
27
+ name="Bot"
28
+ role="bot"
29
+ showActionsOnInteraction
30
+ avatar={patternflyAvatar}
31
+ content="This message has response actions visually hidden until you hover over the message via mouse, or an action would receive focus via keyboard."
32
+ actions={{
33
+ // eslint-disable-next-line no-console
34
+ positive: { onClick: () => console.log('Good response') },
35
+ // eslint-disable-next-line no-console
36
+ negative: { onClick: () => console.log('Bad response') },
37
+ // eslint-disable-next-line no-console
38
+ copy: { onClick: () => console.log('Copy') },
39
+ // eslint-disable-next-line no-console
40
+ download: { onClick: () => console.log('Download') },
41
+ // eslint-disable-next-line no-console
42
+ listen: { onClick: () => console.log('Listen') }
43
+ }}
44
+ />
45
+ </>
25
46
  );
@@ -95,14 +95,16 @@ For example, you can use the default divider to display a "timestamp" for more s
95
95
 
96
96
  ### Message actions
97
97
 
98
- You can add actions to a message, to allow users to interact with the message content. These actions can include:
98
+ To let users interact with a bot's responses, you can add support for message actions. While you can customize message actions to your needs, default options include the following:
99
99
 
100
- - Feedback responses that allow users to rate a message as "good" or "bad".
101
- - Copy and share controls that allow users to share the message content with others.
102
- - An edit action to allow users to edit a message they previously sent. This should only be applied to user messages - see the [user messages example](#user-messages) for details on how to implement this action.
103
- - A listen action, that will read the message content out loud.
100
+ - Positive and negative feedback: Allows users to rate a message as "good" or "bad."
101
+ - Copy: Allows users to copy the message content to their clipboard.
102
+ - Download: Allows users to download the message content.
103
+ - Listen: Reads the message content out loud using text-to-speech.
104
104
 
105
- **Note:** The logic for the actions is not built into the component and must be implemented by the consuming application.
105
+ You can display message actions by default, or use the `showActionsOnInteraction` prop to reveal actions on hover or keyboard focus.
106
+
107
+ **Note**: The underlying logic for these actions is not built-in and must be implemented within the consuming application.
106
108
 
107
109
  ```js file="./MessageWithResponseActions.tsx"
108
110
 
@@ -140,11 +142,10 @@ When `persistActionSelection` is `true`:
140
142
 
141
143
  ### Message actions that fill
142
144
 
143
- To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
145
+ To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
144
146
 
145
147
  This is especially useful for actions that are intended to persist (such as the "positive" and "negative" responses), so that a user's selection is more clear and emphasized.
146
148
 
147
-
148
149
  ```js file="./MessageWithIconSwapping.tsx"
149
150
 
150
151
  ```
@@ -80,7 +80,7 @@ export const BasicDemo: FunctionComponent = () => {
80
80
  showAll && setShowAll(!showAll);
81
81
  }}
82
82
  name="basic-inline-radio"
83
- label="With menu"
83
+ label="With chat history drawer"
84
84
  id="menu"
85
85
  />
86
86
  <Checkbox
@@ -189,9 +189,9 @@ The ChatBot header is persistent, and contains the title for the ChatBot window,
189
189
 
190
190
  The `<ChatbotHeader>` has 2 sections:
191
191
 
192
- - `<ChatbotHeaderMain>` contains the title and an optional menu toggle or new chat button:
192
+ - `<ChatbotHeaderMain>` contains the title and an optional hamburger menu toggle or new chat button:
193
193
  - `<ChatbotHeaderTitle>` handles the layout and display of a title or image at different responsive sizes.
194
- - `<ChatbotHeaderMenu>` (optional) is placed on the left side of the header and used to toggle a chat history menu.
194
+ - `<ChatbotHeaderMenu>` (optional) is placed on the left side of the header and used to toggle a chat history drawer.
195
195
  - `<ChatbotHeaderNewChatButton>` (optional) is placed on the left side of the header and used to initiate a new chat.
196
196
  - `<ChatbotHeaderActions>` contains any additional controls:
197
197
  - The `<ChatbotHeaderSelectorDropdown>` component is a standard PatternFly dropdown that matches the ChatBot styles.
@@ -227,8 +227,8 @@ There are a variety of options and customizations you can make to the header, to
227
227
 
228
228
  In this example, select the respective checkbox to toggle these features:
229
229
 
230
- - **Menu:** Users can select the menu toggle to open a menu of additional options or actions.
231
- - **New chat button:** Used to start a new chat session. The header button can be used in addition to or in place of a new chat button within the [conversation history drawer](/extensions/chatbot/ui/#drawer-with-search-and-new-chat-button).
230
+ - **Chat history drawer:** Users can select the hamburger menu toggle to open the [chat history drawer](#chat-history).
231
+ - **New chat button:** Used to start a new chat session. The header button can be used in addition to or in place of a new chat button within the [chat history drawer](/extensions/chatbot/ui/#drawer-with-search-and-new-chat-button).
232
232
  - **Left-aligned logo**
233
233
  - **Centered logo**
234
234
  - **Selector dropdown:** Users can choose from preselected options in a dropdown menu. For example, they can toggle between AI models.
@@ -339,11 +339,11 @@ This example shows a simplified method of handling the "thinking" animation: aft
339
339
 
340
340
  ```
341
341
 
342
- ## Navigation
342
+ ## Chat history
343
343
 
344
- ### Side nav in a drawer
344
+ ### Chat history drawer
345
345
 
346
- The ChatBot conversation history is contained in an interactive drawer, where users can interact with previous conversations or start a new conversation.
346
+ A user's chat history is contained in an interactive drawer, where they can interact with previous conversations or start a new conversation.
347
347
 
348
348
  The `<ChatbotConversationHistoryNav>` component is a wrapper placed within `<Chatbot>`, which contains all other ChatBot components in `drawerContent`. There is a focus trap so users can only tab within the drawer while it is open.
349
349
 
@@ -367,14 +367,14 @@ The code structure will look like this:
367
367
  </Chatbot>
368
368
  ```
369
369
 
370
- The conversation history drawer looks different depending on the `displayMode` of the parent `<Chatbot>`. (As shown in the [main ChatBot demo](/extensions/chatbot/overview/demo#basic-chatbot).):
370
+ The chat history drawer looks different depending on the `displayMode` of the parent `<Chatbot>` (as shown in the [main ChatBot demo](/extensions/chatbot/overview/demo#basic-chatbot).):
371
371
 
372
- - `Default` and `docked` display modes display the conversation history on top of the rest of the ChatBot content, with a PatternFly backdrop between the drawer panel and drawer content.
373
- - `Fullscreen` and `embedded` display modes display the conversation history in line with the drawer content.
372
+ - `Default` and `docked` display modes display the chat history on top of the rest of the ChatBot content, with a PatternFly backdrop between the drawer panel and drawer content.
373
+ - `Fullscreen` and `embedded` display modes display the chat history in line with the drawer content.
374
374
 
375
375
  ### Drawer with search and "new chat" button
376
376
 
377
- In the conversation history drawer, users can search previous ChatBot conversations via an input field. To customize the placeholder text, use `searchInputPlaceholder`. Provide an aria label via `searchInputAriaLabel`.
377
+ In the chat history drawer, users can search previous ChatBot conversations via an input field. To customize the placeholder text, use `searchInputPlaceholder`. Provide an aria label via `searchInputAriaLabel`.
378
378
 
379
379
  They can also start new conversations via a "New chat" button. To customize the button label, use `newChatButtonText`.
380
380
 
@@ -386,7 +386,7 @@ Both the search input field and "New chat" buttons are optional. The `reverseBut
386
386
 
387
387
  ### Drawer with search actions
388
388
 
389
- You can customize the search experience within the conversation history drawer via the `searchActionStart` and `searchActionEnd` props, which provide additional search controls before and after the input field. These props are useful for adding filtering, sorting, or other search-related functionality.
389
+ You can customize the search experience within the chat history drawer via the `searchActionStart` and `searchActionEnd` props, which provide additional search controls before and after the input field. These props are useful for adding filtering, sorting, or other search-related functionality.
390
390
 
391
391
  You can also add a visual divider between the drawer head and the title by setting `hasDrawerHeadDivider` to `true`.
392
392
 
@@ -404,15 +404,15 @@ Actions can be added to conversations with `menuItems`. Optionally, you can also
404
404
 
405
405
  ### Pinning conversations
406
406
 
407
- To help users track important conversations, add a "pin" option to the conversation action menus. This action moves a conversation to a dedicated "pinned" section at the top of the history drawer for quick access. Pinned items should contain an "unpin" option, so that users can remove pinned conversations as needed.
407
+ To help users track important conversations, add a "pin" option to the conversation action menus. This action moves a conversation to a dedicated "pinned" section at the top of the chat history drawer for quick access. Pinned items should contain an "unpin" option, so that users can remove pinned conversations as needed.
408
408
 
409
409
  ```js file="./ChatbotHeaderDrawerWithPin.tsx"
410
410
 
411
411
  ```
412
412
 
413
- ### Renaming conversations in history drawer
413
+ ### Renaming conversations in chat history drawer
414
414
 
415
- You can allow users to rename a conversation in the history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
415
+ You can allow users to rename a conversation in the chat history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
416
416
 
417
417
  - When the modal opens, focus is placed at the end of the text input.
418
418
  - When the modal closes, focus goes back to the action toggle that was previously opened.
@@ -433,7 +433,7 @@ If you're showing a conversation that is already active, you can set the `active
433
433
 
434
434
  ### Resizable drawer
435
435
 
436
- By default, the conversation history drawer has a fixed width (384px) and a focus trap. To provide users with more flexibility as they navigate their conversation history, or to better support embedded ChatBots on tablet-sized devices or smaller browser windows, you can instead make the drawer resizable. By default, even resizable drawers will still open to their full width on mobile devices.
436
+ By default, the chat history drawer has a fixed width (384px) and a focus trap. To provide users with more flexibility as they navigate their chat history, or to better support embedded ChatBots on tablet-sized devices or smaller browser windows, you can instead make the drawer resizable. By default, even resizable drawers will still open to their full width on mobile devices.
437
437
 
438
438
  In this example, the drawer can be resized up to the max size of the parent and resized down to 200px wide. To customize this behavior further (including width, style, and focus behavior) use PatternFly [`<Drawer>` props](/components/drawer#props), [`<DrawerPanelContent>` props](/components/drawer/#drawerpanelcontent), or any other drawer subcomponents.
439
439
 
@@ -95,7 +95,7 @@ This demo displays a basic ChatBot, which includes:
95
95
  - Sending a message to the ChatBot.
96
96
  - Receiving a response from a backend AI tool with a loading message state.
97
97
 
98
- 6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#navigation) toggled open and closed by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
98
+ 6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#chat-history) toggled open and closed by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
99
99
 
100
100
  7. A "Skip to chatbot" button that allows you to skip to the chatbot content via the [PatternFly skip to content component](/extensions/chatbot/ui#skip-to-content). To display this button you must tab into the main window.
101
101
 
@@ -126,7 +126,7 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed
126
126
  - [Speech to text.](/extensions/chatbot/ui#message-bar-with-speech-recognition-and-file-attachment)
127
127
  - Sending a message to the ChatBot.
128
128
  - Receiving a response from a backend AI tool with a loading message state.
129
- 6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#navigation) that can be toggled by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
129
+ 6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#chat-history) that can be toggled by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
130
130
 
131
131
  ```js file="./EmbeddedChatbot.tsx" isFullscreen
132
132
 
@@ -196,7 +196,7 @@ This demo illustrates how you could add downloadable transcripts to your ChatBot
196
196
 
197
197
  A message transcript includes details from a single chat message. To download a sample message transcript in this demo, click the "Download" action under a bot message.
198
198
 
199
- A conversation transcript includes details from the entirety of a ChatBot conversation. To download a sample conversation transcript in this demo, open the chat history menu and click "Download" in the options menu for the conversation.
199
+ A conversation transcript includes details from the entirety of a ChatBot conversation. To download a sample conversation transcript in this demo, open the chat history drawer and click "Download" in the options menu for the conversation.
200
200
 
201
201
  In this example, file download is implemented with [file-saver](https://www.npmjs.com/package/file-saver).
202
202
 
@@ -9,7 +9,10 @@
9
9
  flex-direction: column;
10
10
  width: 30rem;
11
11
  height: 70vh;
12
- background-color: var(--pf-t--global--background--color--secondary--default);
12
+ background-color: var(
13
+ --pf-t--global--background--color--floating--secondary--default,
14
+ --pf-t--global--background--color--secondary--default
15
+ );
13
16
  border-radius: var(--pf-t--global--border--radius--medium);
14
17
  box-shadow: var(--pf-t--global--box-shadow--lg);
15
18
  font-size: var(--pf-t--global--font--size--md);
@@ -72,6 +75,8 @@
72
75
  // Chatbot Display Mode - Fullscreen
73
76
  // ============================================================================
74
77
  .pf-chatbot--fullscreen {
78
+ background-color: var(--pf-t--global--background--color--secondary--default);
79
+
75
80
  // for high contrast support
76
81
  border: unset;
77
82
  inset-block-end: 0;
@@ -87,6 +92,8 @@
87
92
  // Chatbot Display Mode - Embedded
88
93
  // ============================================================================
89
94
  .pf-chatbot--embedded {
95
+ background-color: var(--pf-t--global--background--color--secondary--default);
96
+
90
97
  // for high contrast support
91
98
  border: unset;
92
99
  position: static;
@@ -3,7 +3,10 @@
3
3
  // ============================================================================
4
4
  .pf-chatbot__content {
5
5
  position: relative;
6
- background-color: var(--pf-t--global--background--color--secondary--default);
6
+ background-color: var(
7
+ --pf-t--global--background--color--floating--secondary--default,
8
+ --pf-t--global--background--color--secondary--default
9
+ );
7
10
  overflow-y: auto;
8
11
  overflow: hidden; // needed in Red Hat Developer Hub workspace
9
12
  flex: 1; // needed in Composer AI
@@ -26,6 +29,7 @@
26
29
  .pf-chatbot--fullscreen,
27
30
  .pf-chatbot--embedded {
28
31
  .pf-chatbot__content {
32
+ background-color: var(--pf-t--global--background--color--secondary--default);
29
33
  display: flex;
30
34
  justify-content: center;
31
35
  }