@patternfly/chatbot 6.5.0-prerelease.12 → 6.5.0-prerelease.14

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.
@@ -64,6 +64,9 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
64
64
  actions?: {
65
65
  [key: string]: ActionProps;
66
66
  };
67
+ /** When true, the selected action will persist even when clicking outside the component.
68
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
69
+ persistActionSelection?: boolean;
67
70
  /** Sources for message */
68
71
  sources?: SourcesCardProps;
69
72
  /** Label for the English word "AI," used to tag messages with role "bot" */
@@ -58,7 +58,7 @@ const DeepThinking_1 = __importDefault(require("../DeepThinking"));
58
58
  const SuperscriptMessage_1 = __importDefault(require("./SuperscriptMessage/SuperscriptMessage"));
59
59
  const ToolCall_1 = __importDefault(require("../ToolCall"));
60
60
  const MessageBase = (_a) => {
61
- var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, 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 } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "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"]);
61
+ var { role, 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 } = _a, props = __rest(_a, ["role", "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"]);
62
62
  const [messageText, setMessageText] = (0, react_1.useState)(content);
63
63
  (0, react_1.useEffect)(() => {
64
64
  setMessageText(content);
@@ -219,7 +219,7 @@ const MessageBase = (_a) => {
219
219
  }
220
220
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [beforeMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: beforeMainContent }), error ? (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, Object.assign({}, error)) : handleMarkdown()] }));
221
221
  };
222
- return ((0, jsx_runtime_1.jsxs)("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && ((0, jsx_runtime_1.jsx)(react_core_1.Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-contents", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-meta", children: [name && ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__message-name", children: (0, jsx_runtime_1.jsx)(react_core_1.Truncate, { content: name }) })), role === 'bot' && ((0, jsx_runtime_1.jsx)(react_core_1.Label, { variant: "outline", isCompact: true, children: botWord })), (0, jsx_runtime_1.jsx)(react_core_1.Timestamp, { date: date, children: timestamp })] }), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-response", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: afterMainContent }), toolResponse && (0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, toolResponse)), deepThinking && (0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, deepThinking)), toolCall && (0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, toolCall)), !isLoading && sources && (0, jsx_runtime_1.jsx)(SourcesCard_1.default, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && ((0, jsx_runtime_1.jsx)(QuickStartTile_1.default, { 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 && (0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions }), userFeedbackForm && (0, jsx_runtime_1.jsx)(UserFeedback_1.default, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact })), userFeedbackComplete && ((0, jsx_runtime_1.jsx)(UserFeedbackComplete_1.default, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && ((0, jsx_runtime_1.jsx)(QuickResponse_1.default, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
222
+ return ((0, jsx_runtime_1.jsxs)("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && ((0, jsx_runtime_1.jsx)(react_core_1.Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-contents", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-meta", children: [name && ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__message-name", children: (0, jsx_runtime_1.jsx)(react_core_1.Truncate, { content: name }) })), role === 'bot' && ((0, jsx_runtime_1.jsx)(react_core_1.Label, { variant: "outline", isCompact: true, children: botWord })), (0, jsx_runtime_1.jsx)(react_core_1.Timestamp, { date: date, children: timestamp })] }), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-response", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: afterMainContent }), toolResponse && (0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, toolResponse)), deepThinking && (0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, deepThinking)), toolCall && (0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, toolCall)), !isLoading && sources && (0, jsx_runtime_1.jsx)(SourcesCard_1.default, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && ((0, jsx_runtime_1.jsx)(QuickStartTile_1.default, { 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 && ((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions, persistActionSelection: persistActionSelection })), userFeedbackForm && (0, jsx_runtime_1.jsx)(UserFeedback_1.default, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact })), userFeedbackComplete && ((0, jsx_runtime_1.jsx)(UserFeedbackComplete_1.default, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && ((0, jsx_runtime_1.jsx)(QuickResponse_1.default, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
223
223
  var _a;
224
224
  return ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-attachment", children: (0, jsx_runtime_1.jsx)(FileDetailsLabel_1.default, { 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));
225
225
  }) })), !isLoading && endContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: endContent })] })] })] })));
@@ -40,6 +40,9 @@ export interface ResponseActionProps {
40
40
  listen?: ActionProps;
41
41
  edit?: ActionProps;
42
42
  };
43
+ /** When true, the selected action will persist even when clicking outside the component.
44
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
45
+ persistActionSelection?: boolean;
43
46
  }
44
47
  export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
45
48
  export default ResponseActions;
@@ -20,10 +20,11 @@ const jsx_runtime_1 = require("react/jsx-runtime");
20
20
  const react_2 = require("react");
21
21
  const react_icons_1 = require("@patternfly/react-icons");
22
22
  const ResponseActionButton_1 = __importDefault(require("./ResponseActionButton"));
23
- const ResponseActions = ({ actions }) => {
23
+ const ResponseActions = ({ actions, persistActionSelection = false }) => {
24
24
  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;
25
25
  const [activeButton, setActiveButton] = (0, react_2.useState)();
26
26
  const [clickStatePersisted, setClickStatePersisted] = (0, react_2.useState)(false);
27
+ const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
27
28
  (0, react_2.useEffect)(() => {
28
29
  // Define the order of precedence for checking initial `isClicked`
29
30
  const actionPrecedence = ['positive', 'negative', 'copy', 'edit', 'share', 'download', 'listen'];
@@ -45,11 +46,18 @@ const ResponseActions = ({ actions }) => {
45
46
  // Click state is explicitly controlled by consumer.
46
47
  setClickStatePersisted(true);
47
48
  }
49
+ // If persistActionSelection is true, all selections are persisted
50
+ if (persistActionSelection) {
51
+ setClickStatePersisted(true);
52
+ }
48
53
  setActiveButton(initialActive);
49
- }, [actions]);
50
- const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
54
+ }, [actions, persistActionSelection]);
51
55
  const responseActions = (0, react_2.useRef)(null);
52
56
  (0, react_2.useEffect)(() => {
57
+ // Only add click outside listener if not persisting selection
58
+ if (persistActionSelection) {
59
+ return;
60
+ }
53
61
  const handleClickOutside = (e) => {
54
62
  if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
55
63
  setActiveButton(undefined);
@@ -59,13 +67,26 @@ const ResponseActions = ({ actions }) => {
59
67
  return () => {
60
68
  window.removeEventListener('click', handleClickOutside);
61
69
  };
62
- }, [clickStatePersisted]);
70
+ }, [clickStatePersisted, persistActionSelection]);
63
71
  const handleClick = (e, id, onClick) => {
64
- setClickStatePersisted(false);
65
- setActiveButton(id);
72
+ if (persistActionSelection) {
73
+ if (activeButton === id) {
74
+ // Toggle off if clicking the same button
75
+ setActiveButton(undefined);
76
+ }
77
+ else {
78
+ // Set new active button
79
+ setActiveButton(id);
80
+ }
81
+ setClickStatePersisted(true);
82
+ }
83
+ else {
84
+ setClickStatePersisted(false);
85
+ setActiveButton(id);
86
+ }
66
87
  onClick && onClick(e);
67
88
  };
68
- return ((0, jsx_runtime_1.jsxs)("div", { ref: responseActions, className: "pf-chatbot__response-actions", children: [positive && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, Object.assign({}, positive, { ariaLabel: (_a = positive.ariaLabel) !== null && _a !== void 0 ? _a : 'Good response', clickedAriaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : '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 : 'Response recorded', tooltipProps: positive.tooltipProps, icon: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsUpIcon, {}), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, Object.assign({}, negative, { ariaLabel: (_e = negative.ariaLabel) !== null && _e !== void 0 ? _e : 'Bad response', clickedAriaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : '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 : 'Response recorded', tooltipProps: negative.tooltipProps, icon: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsDownIcon, {}), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
89
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: responseActions, className: "pf-chatbot__response-actions", children: [positive && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsUpIcon, {}), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsDownIcon, {}), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && ((0, jsx_runtime_1.jsx)(ResponseActionButton_1.default, 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: (0, jsx_runtime_1.jsx)(react_icons_1.VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
69
90
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
70
91
  return ((0, react_1.createElement)(ResponseActionButton_1.default, 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'] })));
71
92
  })] }));
@@ -20,8 +20,8 @@ const user_event_1 = __importDefault(require("@testing-library/user-event"));
20
20
  const react_icons_1 = require("@patternfly/react-icons");
21
21
  const Message_1 = __importDefault(require("../Message"));
22
22
  const ALL_ACTIONS = [
23
- { type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
24
- { type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
23
+ { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
24
+ { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
25
25
  { type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
26
26
  { type: 'edit', label: 'Edit', clickedLabel: 'Editing' },
27
27
  { type: 'share', label: 'Share', clickedLabel: 'Shared' },
@@ -88,13 +88,13 @@ describe('ResponseActions', () => {
88
88
  expect(button).toBeTruthy();
89
89
  });
90
90
  yield user_event_1.default.click(goodBtn);
91
- expect(react_1.screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
91
+ expect(react_1.screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
92
92
  let unclickedButtons = buttons.filter((button) => button !== goodBtn);
93
93
  unclickedButtons.forEach((button) => {
94
94
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
95
95
  });
96
96
  yield user_event_1.default.click(badBtn);
97
- expect(react_1.screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
97
+ expect(react_1.screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
98
98
  unclickedButtons = buttons.filter((button) => button !== badBtn);
99
99
  unclickedButtons.forEach((button) => {
100
100
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -111,10 +111,10 @@ describe('ResponseActions', () => {
111
111
  expect(goodBtn).toBeTruthy();
112
112
  expect(badBtn).toBeTruthy();
113
113
  yield user_event_1.default.click(goodBtn);
114
- expect(react_1.screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
114
+ expect(react_1.screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
115
115
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
116
116
  yield user_event_1.default.click(badBtn);
117
- expect(react_1.screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
117
+ expect(react_1.screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
118
118
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
119
119
  yield user_event_1.default.click(react_1.screen.getByText("I updated your account with those settings. You're ready to set up your first dashboard!"));
120
120
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -187,28 +187,28 @@ describe('ResponseActions', () => {
187
187
  });
188
188
  });
189
189
  it('should be able to call onClick correctly', () => __awaiter(void 0, void 0, void 0, function* () {
190
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label }) {
190
+ for (const { type, label } of ALL_ACTIONS) {
191
191
  const spy = jest.fn();
192
192
  (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: { [type]: { onClick: spy } } }));
193
193
  yield user_event_1.default.click(react_1.screen.getByRole('button', { name: label }));
194
194
  expect(spy).toHaveBeenCalledTimes(1);
195
- }));
195
+ }
196
196
  }));
197
197
  it('should swap clicked and non-clicked aria labels on click', () => __awaiter(void 0, void 0, void 0, function* () {
198
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
198
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
199
199
  (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: { [type]: { onClick: jest.fn() } } }));
200
200
  expect(react_1.screen.getByRole('button', { name: label })).toBeTruthy();
201
201
  yield user_event_1.default.click(react_1.screen.getByRole('button', { name: label }));
202
202
  expect(react_1.screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
203
- }));
203
+ }
204
204
  }));
205
205
  it('should swap clicked and non-clicked tooltips on click', () => __awaiter(void 0, void 0, void 0, function* () {
206
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
206
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
207
207
  (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: { [type]: { onClick: jest.fn() } } }));
208
208
  expect(react_1.screen.getByRole('button', { name: label })).toBeTruthy();
209
209
  yield user_event_1.default.click(react_1.screen.getByRole('button', { name: label }));
210
210
  expect(react_1.screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
211
- }));
211
+ }
212
212
  }));
213
213
  it('should be able to change aria labels', () => {
214
214
  const actions = [
@@ -260,4 +260,59 @@ describe('ResponseActions', () => {
260
260
  expect(react_1.screen.getByTestId(action[key])).toBeTruthy();
261
261
  });
262
262
  });
263
+ // we are testing for the reverse case already above
264
+ it('should not deselect when clicking outside when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
265
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(Message_1.default, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
266
+ positive: {},
267
+ negative: {}
268
+ }, persistActionSelection: true }));
269
+ const goodBtn = react_1.screen.getByRole('button', { name: 'Good response' });
270
+ yield user_event_1.default.click(goodBtn);
271
+ expect(react_1.screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
272
+ yield user_event_1.default.click(react_1.screen.getByText('Test content'));
273
+ expect(react_1.screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
274
+ }));
275
+ it('should switch selection to another button when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
276
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(Message_1.default, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
277
+ positive: {},
278
+ negative: {}
279
+ }, persistActionSelection: true }));
280
+ const goodBtn = react_1.screen.getByRole('button', { name: 'Good response' });
281
+ const badBtn = react_1.screen.getByRole('button', { name: 'Bad response' });
282
+ yield user_event_1.default.click(goodBtn);
283
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
284
+ yield user_event_1.default.click(badBtn);
285
+ expect(badBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
286
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
287
+ }));
288
+ it('should toggle off when clicking the same button when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
289
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(Message_1.default, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
290
+ positive: {},
291
+ negative: {}
292
+ }, persistActionSelection: true }));
293
+ const goodBtn = react_1.screen.getByRole('button', { name: 'Good response' });
294
+ yield user_event_1.default.click(goodBtn);
295
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
296
+ yield user_event_1.default.click(goodBtn);
297
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
298
+ }));
299
+ it('should work with custom actions when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
300
+ const actions = {
301
+ positive: { 'data-testid': 'positive', onClick: jest.fn() },
302
+ negative: { 'data-testid': 'negative', onClick: jest.fn() },
303
+ custom: {
304
+ 'data-testid': 'custom',
305
+ onClick: jest.fn(),
306
+ ariaLabel: 'Custom',
307
+ tooltipContent: 'Custom action',
308
+ icon: (0, jsx_runtime_1.jsx)(react_icons_1.DownloadIcon, {})
309
+ }
310
+ };
311
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions, persistActionSelection: true }));
312
+ const customBtn = react_1.screen.getByTestId('custom');
313
+ yield user_event_1.default.click(customBtn);
314
+ expect(customBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
315
+ yield user_event_1.default.click(customBtn);
316
+ expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
317
+ }));
263
318
  });
package/dist/css/main.css CHANGED
@@ -946,7 +946,7 @@
946
946
  }
947
947
 
948
948
  .pf-chatbot__deep-thinking {
949
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
949
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
950
950
  overflow: unset;
951
951
  }
952
952
 
@@ -2378,8 +2378,8 @@ li[id*=user-content-fn-]:has(> span > span > .pf-chatbot__message-text + .pf-cha
2378
2378
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle);
2379
2379
  }
2380
2380
  .pf-chatbot__response-actions .pf-chatbot__button--response-action.pf-v6-c-button.pf-m-plain.pf-m-small:focus {
2381
- --pf-v6-c-button--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked);
2382
2381
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular);
2382
+ --pf-v6-c-button--BackgroundColor: var(--pf-v6-c-button--hover--BackgroundColor);
2383
2383
  }
2384
2384
 
2385
2385
  .pf-v6-c-button.pf-chatbot__button--response-action-clicked.pf-v6-c-button.pf-m-plain.pf-m-small {
@@ -2627,7 +2627,7 @@ li[id*=user-content-fn-]:has(> span > span > .pf-chatbot__message-text + .pf-cha
2627
2627
  }
2628
2628
 
2629
2629
  .pf-chatbot__tool-response {
2630
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2630
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
2631
2631
  overflow: unset;
2632
2632
  }
2633
2633
 
@@ -2653,18 +2653,18 @@ li[id*=user-content-fn-]:has(> span > span > .pf-chatbot__message-text + .pf-cha
2653
2653
  }
2654
2654
 
2655
2655
  .pf-chatbot__tool-response-card {
2656
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2656
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
2657
2657
  --pf-v6-c-card--first-child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
2658
2658
  --pf-v6-c-card__title--not--last-child--PaddingBlockEnd: var(--pf-t--global--spacer--sm);
2659
2659
  --pf-v6-c-card--c-divider--child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
2660
2660
  }
2661
2661
  .pf-chatbot__tool-response-card .pf-v6-c-divider {
2662
- --pf-v6-c-divider--Color: var(--pf-t--global--border--color--control--read-only);
2662
+ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default);
2663
2663
  }
2664
2664
 
2665
2665
  .pf-chatbot__tool-call {
2666
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2667
2666
  --pf-v6-c-card--BorderRadius: var(--pf-t--global--border--radius--small);
2667
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
2668
2668
  overflow: unset;
2669
2669
  row-gap: var(--pf-t--global--spacer--sm);
2670
2670
  }
@@ -64,6 +64,9 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
64
64
  actions?: {
65
65
  [key: string]: ActionProps;
66
66
  };
67
+ /** When true, the selected action will persist even when clicking outside the component.
68
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
69
+ persistActionSelection?: boolean;
67
70
  /** Sources for message */
68
71
  sources?: SourcesCardProps;
69
72
  /** Label for the English word "AI," used to tag messages with role "bot" */
@@ -52,7 +52,7 @@ import DeepThinking from '../DeepThinking';
52
52
  import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage';
53
53
  import ToolCall from '../ToolCall';
54
54
  export const MessageBase = (_a) => {
55
- var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, 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 } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "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"]);
55
+ var { role, 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 } = _a, props = __rest(_a, ["role", "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"]);
56
56
  const [messageText, setMessageText] = useState(content);
57
57
  useEffect(() => {
58
58
  setMessageText(content);
@@ -213,7 +213,7 @@ export const MessageBase = (_a) => {
213
213
  }
214
214
  return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? _jsx(ErrorMessage, Object.assign({}, error)) : handleMarkdown()] }));
215
215
  };
216
- return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "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: [_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 })] }), _jsxs("div", { className: "pf-chatbot__message-response", 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(ResponseActions, { actions: actions }), 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) => {
216
+ return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "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: [_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 })] }), _jsxs("div", { className: "pf-chatbot__message-response", 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(ResponseActions, { actions: actions, persistActionSelection: persistActionSelection })), 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) => {
217
217
  var _a;
218
218
  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));
219
219
  }) })), !isLoading && endContent && _jsx(_Fragment, { children: endContent })] })] })] })));
@@ -40,6 +40,9 @@ export interface ResponseActionProps {
40
40
  listen?: ActionProps;
41
41
  edit?: ActionProps;
42
42
  };
43
+ /** When true, the selected action will persist even when clicking outside the component.
44
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
45
+ persistActionSelection?: boolean;
43
46
  }
44
47
  export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
45
48
  export default ResponseActions;
@@ -14,10 +14,11 @@ 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, OutlinedThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
16
16
  import ResponseActionButton from './ResponseActionButton';
17
- export const ResponseActions = ({ actions }) => {
17
+ export const ResponseActions = ({ actions, persistActionSelection = false }) => {
18
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;
19
19
  const [activeButton, setActiveButton] = useState();
20
20
  const [clickStatePersisted, setClickStatePersisted] = useState(false);
21
+ const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
21
22
  useEffect(() => {
22
23
  // Define the order of precedence for checking initial `isClicked`
23
24
  const actionPrecedence = ['positive', 'negative', 'copy', 'edit', 'share', 'download', 'listen'];
@@ -39,11 +40,18 @@ export const ResponseActions = ({ actions }) => {
39
40
  // Click state is explicitly controlled by consumer.
40
41
  setClickStatePersisted(true);
41
42
  }
43
+ // If persistActionSelection is true, all selections are persisted
44
+ if (persistActionSelection) {
45
+ setClickStatePersisted(true);
46
+ }
42
47
  setActiveButton(initialActive);
43
- }, [actions]);
44
- const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
48
+ }, [actions, persistActionSelection]);
45
49
  const responseActions = useRef(null);
46
50
  useEffect(() => {
51
+ // Only add click outside listener if not persisting selection
52
+ if (persistActionSelection) {
53
+ return;
54
+ }
47
55
  const handleClickOutside = (e) => {
48
56
  if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
49
57
  setActiveButton(undefined);
@@ -53,13 +61,26 @@ export const ResponseActions = ({ actions }) => {
53
61
  return () => {
54
62
  window.removeEventListener('click', handleClickOutside);
55
63
  };
56
- }, [clickStatePersisted]);
64
+ }, [clickStatePersisted, persistActionSelection]);
57
65
  const handleClick = (e, id, onClick) => {
58
- setClickStatePersisted(false);
59
- setActiveButton(id);
66
+ if (persistActionSelection) {
67
+ if (activeButton === id) {
68
+ // Toggle off if clicking the same button
69
+ setActiveButton(undefined);
70
+ }
71
+ else {
72
+ // Set new active button
73
+ setActiveButton(id);
74
+ }
75
+ setClickStatePersisted(true);
76
+ }
77
+ else {
78
+ setClickStatePersisted(false);
79
+ setActiveButton(id);
80
+ }
60
81
  onClick && onClick(e);
61
82
  };
62
- 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 : '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 : 'Response recorded', tooltipProps: positive.tooltipProps, icon: _jsx(OutlinedThumbsUpIcon, {}), 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 : '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 : 'Response recorded', tooltipProps: negative.tooltipProps, icon: _jsx(OutlinedThumbsDownIcon, {}), 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) => {
83
+ 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: _jsx(OutlinedThumbsUpIcon, {}), 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: _jsx(OutlinedThumbsDownIcon, {}), 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) => {
63
84
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
64
85
  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'] })));
65
86
  })] }));
@@ -15,8 +15,8 @@ import userEvent from '@testing-library/user-event';
15
15
  import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
16
16
  import Message from '../Message';
17
17
  const ALL_ACTIONS = [
18
- { type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
19
- { type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
18
+ { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
19
+ { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
20
20
  { type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
21
21
  { type: 'edit', label: 'Edit', clickedLabel: 'Editing' },
22
22
  { type: 'share', label: 'Share', clickedLabel: 'Shared' },
@@ -83,13 +83,13 @@ describe('ResponseActions', () => {
83
83
  expect(button).toBeTruthy();
84
84
  });
85
85
  yield userEvent.click(goodBtn);
86
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
86
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
87
87
  let unclickedButtons = buttons.filter((button) => button !== goodBtn);
88
88
  unclickedButtons.forEach((button) => {
89
89
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
90
90
  });
91
91
  yield userEvent.click(badBtn);
92
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
92
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
93
93
  unclickedButtons = buttons.filter((button) => button !== badBtn);
94
94
  unclickedButtons.forEach((button) => {
95
95
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -106,10 +106,10 @@ describe('ResponseActions', () => {
106
106
  expect(goodBtn).toBeTruthy();
107
107
  expect(badBtn).toBeTruthy();
108
108
  yield userEvent.click(goodBtn);
109
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
109
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
110
110
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
111
111
  yield userEvent.click(badBtn);
112
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
112
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
113
113
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
114
114
  yield userEvent.click(screen.getByText("I updated your account with those settings. You're ready to set up your first dashboard!"));
115
115
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -182,28 +182,28 @@ describe('ResponseActions', () => {
182
182
  });
183
183
  });
184
184
  it('should be able to call onClick correctly', () => __awaiter(void 0, void 0, void 0, function* () {
185
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label }) {
185
+ for (const { type, label } of ALL_ACTIONS) {
186
186
  const spy = jest.fn();
187
187
  render(_jsx(ResponseActions, { actions: { [type]: { onClick: spy } } }));
188
188
  yield userEvent.click(screen.getByRole('button', { name: label }));
189
189
  expect(spy).toHaveBeenCalledTimes(1);
190
- }));
190
+ }
191
191
  }));
192
192
  it('should swap clicked and non-clicked aria labels on click', () => __awaiter(void 0, void 0, void 0, function* () {
193
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
193
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
194
194
  render(_jsx(ResponseActions, { actions: { [type]: { onClick: jest.fn() } } }));
195
195
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
196
196
  yield userEvent.click(screen.getByRole('button', { name: label }));
197
197
  expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
198
- }));
198
+ }
199
199
  }));
200
200
  it('should swap clicked and non-clicked tooltips on click', () => __awaiter(void 0, void 0, void 0, function* () {
201
- ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
201
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
202
202
  render(_jsx(ResponseActions, { actions: { [type]: { onClick: jest.fn() } } }));
203
203
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
204
204
  yield userEvent.click(screen.getByRole('button', { name: label }));
205
205
  expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
206
- }));
206
+ }
207
207
  }));
208
208
  it('should be able to change aria labels', () => {
209
209
  const actions = [
@@ -255,4 +255,59 @@ describe('ResponseActions', () => {
255
255
  expect(screen.getByTestId(action[key])).toBeTruthy();
256
256
  });
257
257
  });
258
+ // we are testing for the reverse case already above
259
+ it('should not deselect when clicking outside when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
260
+ render(_jsx(Message, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
261
+ positive: {},
262
+ negative: {}
263
+ }, persistActionSelection: true }));
264
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
265
+ yield userEvent.click(goodBtn);
266
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
267
+ yield userEvent.click(screen.getByText('Test content'));
268
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
269
+ }));
270
+ it('should switch selection to another button when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
271
+ render(_jsx(Message, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
272
+ positive: {},
273
+ negative: {}
274
+ }, persistActionSelection: true }));
275
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
276
+ const badBtn = screen.getByRole('button', { name: 'Bad response' });
277
+ yield userEvent.click(goodBtn);
278
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
279
+ yield userEvent.click(badBtn);
280
+ expect(badBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
281
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
282
+ }));
283
+ it('should toggle off when clicking the same button when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
284
+ render(_jsx(Message, { name: "Bot", role: "bot", avatar: "", content: "Test content", actions: {
285
+ positive: {},
286
+ negative: {}
287
+ }, persistActionSelection: true }));
288
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
289
+ yield userEvent.click(goodBtn);
290
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
291
+ yield userEvent.click(goodBtn);
292
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
293
+ }));
294
+ it('should work with custom actions when persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
295
+ const actions = {
296
+ positive: { 'data-testid': 'positive', onClick: jest.fn() },
297
+ negative: { 'data-testid': 'negative', onClick: jest.fn() },
298
+ custom: {
299
+ 'data-testid': 'custom',
300
+ onClick: jest.fn(),
301
+ ariaLabel: 'Custom',
302
+ tooltipContent: 'Custom action',
303
+ icon: _jsx(DownloadIcon, {})
304
+ }
305
+ };
306
+ render(_jsx(ResponseActions, { actions: actions, persistActionSelection: true }));
307
+ const customBtn = screen.getByTestId('custom');
308
+ yield userEvent.click(customBtn);
309
+ expect(customBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
310
+ yield userEvent.click(customBtn);
311
+ expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
312
+ }));
258
313
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.5.0-prerelease.12",
3
+ "version": "6.5.0-prerelease.14",
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",
@@ -0,0 +1,22 @@
1
+ import { FunctionComponent } from 'react';
2
+
3
+ import Message from '@patternfly/chatbot/dist/dynamic/Message';
4
+ import patternflyAvatar from './patternfly_avatar.jpg';
5
+
6
+ export const MessageWithPersistedActions: 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! Click a button and then click outside the message - notice the selection persists."
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
+ listen: { onClick: () => console.log('Listen') }
19
+ }}
20
+ persistActionSelection
21
+ />
22
+ );
@@ -108,6 +108,20 @@ Once the component has rendered, user interactions will take precedence over the
108
108
 
109
109
  ```
110
110
 
111
+ ### Message actions persistent selections
112
+
113
+ By default, message actions will automatically deselect when you click outside the component or on a different action button. To persist the selection instead, set `persistActionSelection` to `true`.
114
+
115
+ When `persistActionSelection` is `true`:
116
+
117
+ - The selected action will remain selected even when you click outside the component.
118
+ - Clicking a different button will still switch the selection to that button.
119
+ - Clicking the same action button again will toggle the selection off, though you will have to move your focus elsewhere to see the visual state change.
120
+
121
+ ```js file="./MessageWithPersistedActions.tsx"
122
+
123
+ ```
124
+
111
125
  ### Custom message actions
112
126
 
113
127
  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 `<Message>` component. This object can contain the following customizations:
@@ -1,5 +1,5 @@
1
1
  .pf-chatbot__deep-thinking {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
3
3
  overflow: unset;
4
4
  }
5
5
 
@@ -108,6 +108,9 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
108
108
  actions?: {
109
109
  [key: string]: ActionProps;
110
110
  };
111
+ /** When true, the selected action will persist even when clicking outside the component.
112
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
113
+ persistActionSelection?: boolean;
111
114
  /** Sources for message */
112
115
  sources?: SourcesCardProps;
113
116
  /** Label for the English word "AI," used to tag messages with role "bot" */
@@ -202,6 +205,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
202
205
  timestamp,
203
206
  isLoading,
204
207
  actions,
208
+ persistActionSelection,
205
209
  sources,
206
210
  botWord = 'AI',
207
211
  loadingWord = 'Loading message',
@@ -501,7 +505,9 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
501
505
  isCompact={isCompact}
502
506
  />
503
507
  )}
504
- {!isLoading && !isEditable && actions && <ResponseActions actions={actions} />}
508
+ {!isLoading && !isEditable && actions && (
509
+ <ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
510
+ )}
505
511
  {userFeedbackForm && <UserFeedback {...userFeedbackForm} timestamp={dateString} isCompact={isCompact} />}
506
512
  {userFeedbackComplete && (
507
513
  <UserFeedbackComplete {...userFeedbackComplete} timestamp={dateString} isCompact={isCompact} />
@@ -16,8 +16,8 @@
16
16
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle);
17
17
  }
18
18
  &:focus {
19
- --pf-v6-c-button--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked);
20
19
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular);
20
+ --pf-v6-c-button--BackgroundColor: var(--pf-v6-c-button--hover--BackgroundColor);
21
21
  }
22
22
  }
23
23
  }
@@ -6,8 +6,8 @@ import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'
6
6
  import Message from '../Message';
7
7
 
8
8
  const ALL_ACTIONS = [
9
- { type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
10
- { type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
9
+ { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
10
+ { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
11
11
  { type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
12
12
  { type: 'edit', label: 'Edit', clickedLabel: 'Editing' },
13
13
  { type: 'share', label: 'Share', clickedLabel: 'Shared' },
@@ -81,7 +81,7 @@ describe('ResponseActions', () => {
81
81
  expect(button).toBeTruthy();
82
82
  });
83
83
  await userEvent.click(goodBtn);
84
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
84
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
85
85
  'pf-chatbot__button--response-action-clicked'
86
86
  );
87
87
  let unclickedButtons = buttons.filter((button) => button !== goodBtn);
@@ -89,7 +89,7 @@ describe('ResponseActions', () => {
89
89
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
90
90
  });
91
91
  await userEvent.click(badBtn);
92
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
92
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass(
93
93
  'pf-chatbot__button--response-action-clicked'
94
94
  );
95
95
  unclickedButtons = buttons.filter((button) => button !== badBtn);
@@ -117,13 +117,13 @@ describe('ResponseActions', () => {
117
117
  expect(badBtn).toBeTruthy();
118
118
 
119
119
  await userEvent.click(goodBtn);
120
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
120
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
121
121
  'pf-chatbot__button--response-action-clicked'
122
122
  );
123
123
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
124
124
 
125
125
  await userEvent.click(badBtn);
126
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
126
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass(
127
127
  'pf-chatbot__button--response-action-clicked'
128
128
  );
129
129
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -238,30 +238,30 @@ describe('ResponseActions', () => {
238
238
  });
239
239
 
240
240
  it('should be able to call onClick correctly', async () => {
241
- ALL_ACTIONS.forEach(async ({ type, label }) => {
241
+ for (const { type, label } of ALL_ACTIONS) {
242
242
  const spy = jest.fn();
243
243
  render(<ResponseActions actions={{ [type]: { onClick: spy } }} />);
244
244
  await userEvent.click(screen.getByRole('button', { name: label }));
245
245
  expect(spy).toHaveBeenCalledTimes(1);
246
- });
246
+ }
247
247
  });
248
248
 
249
249
  it('should swap clicked and non-clicked aria labels on click', async () => {
250
- ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
250
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
251
251
  render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
252
252
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
253
253
  await userEvent.click(screen.getByRole('button', { name: label }));
254
254
  expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
255
- });
255
+ }
256
256
  });
257
257
 
258
258
  it('should swap clicked and non-clicked tooltips on click', async () => {
259
- ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
259
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
260
260
  render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
261
261
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
262
262
  await userEvent.click(screen.getByRole('button', { name: label }));
263
263
  expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
264
- });
264
+ }
265
265
  });
266
266
 
267
267
  it('should be able to change aria labels', () => {
@@ -322,4 +322,103 @@ describe('ResponseActions', () => {
322
322
  expect(screen.getByTestId(action[key])).toBeTruthy();
323
323
  });
324
324
  });
325
+
326
+ // we are testing for the reverse case already above
327
+ it('should not deselect when clicking outside when persistActionSelection is true', async () => {
328
+ render(
329
+ <Message
330
+ name="Bot"
331
+ role="bot"
332
+ avatar=""
333
+ content="Test content"
334
+ actions={{
335
+ positive: {},
336
+ negative: {}
337
+ }}
338
+ persistActionSelection
339
+ />
340
+ );
341
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
342
+
343
+ await userEvent.click(goodBtn);
344
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
345
+ 'pf-chatbot__button--response-action-clicked'
346
+ );
347
+
348
+ await userEvent.click(screen.getByText('Test content'));
349
+
350
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
351
+ 'pf-chatbot__button--response-action-clicked'
352
+ );
353
+ });
354
+
355
+ it('should switch selection to another button when persistActionSelection is true', async () => {
356
+ render(
357
+ <Message
358
+ name="Bot"
359
+ role="bot"
360
+ avatar=""
361
+ content="Test content"
362
+ actions={{
363
+ positive: {},
364
+ negative: {}
365
+ }}
366
+ persistActionSelection
367
+ />
368
+ );
369
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
370
+ const badBtn = screen.getByRole('button', { name: 'Bad response' });
371
+
372
+ await userEvent.click(goodBtn);
373
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
374
+
375
+ await userEvent.click(badBtn);
376
+ expect(badBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
377
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
378
+ });
379
+
380
+ it('should toggle off when clicking the same button when persistActionSelection is true', async () => {
381
+ render(
382
+ <Message
383
+ name="Bot"
384
+ role="bot"
385
+ avatar=""
386
+ content="Test content"
387
+ actions={{
388
+ positive: {},
389
+ negative: {}
390
+ }}
391
+ persistActionSelection
392
+ />
393
+ );
394
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
395
+
396
+ await userEvent.click(goodBtn);
397
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
398
+
399
+ await userEvent.click(goodBtn);
400
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
401
+ });
402
+
403
+ it('should work with custom actions when persistActionSelection is true', async () => {
404
+ const actions = {
405
+ positive: { 'data-testid': 'positive', onClick: jest.fn() },
406
+ negative: { 'data-testid': 'negative', onClick: jest.fn() },
407
+ custom: {
408
+ 'data-testid': 'custom',
409
+ onClick: jest.fn(),
410
+ ariaLabel: 'Custom',
411
+ tooltipContent: 'Custom action',
412
+ icon: <DownloadIcon />
413
+ }
414
+ };
415
+ render(<ResponseActions actions={actions} persistActionSelection />);
416
+
417
+ const customBtn = screen.getByTestId('custom');
418
+ await userEvent.click(customBtn);
419
+ expect(customBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
420
+
421
+ await userEvent.click(customBtn);
422
+ expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
423
+ });
325
424
  });
@@ -53,11 +53,20 @@ export interface ResponseActionProps {
53
53
  listen?: ActionProps;
54
54
  edit?: ActionProps;
55
55
  };
56
+ /** When true, the selected action will persist even when clicking outside the component.
57
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
58
+ persistActionSelection?: boolean;
56
59
  }
57
60
 
58
- export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ actions }) => {
61
+ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
62
+ actions,
63
+ persistActionSelection = false
64
+ }) => {
59
65
  const [activeButton, setActiveButton] = useState<string>();
60
66
  const [clickStatePersisted, setClickStatePersisted] = useState<boolean>(false);
67
+
68
+ const { positive, negative, copy, edit, share, download, listen, ...additionalActions } = actions;
69
+
61
70
  useEffect(() => {
62
71
  // Define the order of precedence for checking initial `isClicked`
63
72
  const actionPrecedence = ['positive', 'negative', 'copy', 'edit', 'share', 'download', 'listen'];
@@ -82,13 +91,21 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
82
91
  // Click state is explicitly controlled by consumer.
83
92
  setClickStatePersisted(true);
84
93
  }
94
+ // If persistActionSelection is true, all selections are persisted
95
+ if (persistActionSelection) {
96
+ setClickStatePersisted(true);
97
+ }
85
98
  setActiveButton(initialActive);
86
- }, [actions]);
99
+ }, [actions, persistActionSelection]);
87
100
 
88
- const { positive, negative, copy, edit, share, download, listen, ...additionalActions } = actions;
89
101
  const responseActions = useRef<HTMLDivElement>(null);
90
102
 
91
103
  useEffect(() => {
104
+ // Only add click outside listener if not persisting selection
105
+ if (persistActionSelection) {
106
+ return;
107
+ }
108
+
92
109
  const handleClickOutside = (e) => {
93
110
  if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
94
111
  setActiveButton(undefined);
@@ -99,15 +116,26 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
99
116
  return () => {
100
117
  window.removeEventListener('click', handleClickOutside);
101
118
  };
102
- }, [clickStatePersisted]);
119
+ }, [clickStatePersisted, persistActionSelection]);
103
120
 
104
121
  const handleClick = (
105
122
  e: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent,
106
123
  id: string,
107
124
  onClick?: (event: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
108
125
  ) => {
109
- setClickStatePersisted(false);
110
- setActiveButton(id);
126
+ if (persistActionSelection) {
127
+ if (activeButton === id) {
128
+ // Toggle off if clicking the same button
129
+ setActiveButton(undefined);
130
+ } else {
131
+ // Set new active button
132
+ setActiveButton(id);
133
+ }
134
+ setClickStatePersisted(true);
135
+ } else {
136
+ setClickStatePersisted(false);
137
+ setActiveButton(id);
138
+ }
111
139
  onClick && onClick(e);
112
140
  };
113
141
 
@@ -117,12 +145,12 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
117
145
  <ResponseActionButton
118
146
  {...positive}
119
147
  ariaLabel={positive.ariaLabel ?? 'Good response'}
120
- clickedAriaLabel={positive.ariaLabel ?? 'Response recorded'}
148
+ clickedAriaLabel={positive.ariaLabel ?? 'Good response recorded'}
121
149
  onClick={(e) => handleClick(e, 'positive', positive.onClick)}
122
150
  className={positive.className}
123
151
  isDisabled={positive.isDisabled}
124
152
  tooltipContent={positive.tooltipContent ?? 'Good response'}
125
- clickedTooltipContent={positive.clickedTooltipContent ?? 'Response recorded'}
153
+ clickedTooltipContent={positive.clickedTooltipContent ?? 'Good response recorded'}
126
154
  tooltipProps={positive.tooltipProps}
127
155
  icon={<OutlinedThumbsUpIcon />}
128
156
  isClicked={activeButton === 'positive'}
@@ -135,12 +163,12 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
135
163
  <ResponseActionButton
136
164
  {...negative}
137
165
  ariaLabel={negative.ariaLabel ?? 'Bad response'}
138
- clickedAriaLabel={negative.ariaLabel ?? 'Response recorded'}
166
+ clickedAriaLabel={negative.ariaLabel ?? 'Bad response recorded'}
139
167
  onClick={(e) => handleClick(e, 'negative', negative.onClick)}
140
168
  className={negative.className}
141
169
  isDisabled={negative.isDisabled}
142
170
  tooltipContent={negative.tooltipContent ?? 'Bad response'}
143
- clickedTooltipContent={negative.clickedTooltipContent ?? 'Response recorded'}
171
+ clickedTooltipContent={negative.clickedTooltipContent ?? 'Bad response recorded'}
144
172
  tooltipProps={negative.tooltipProps}
145
173
  icon={<OutlinedThumbsDownIcon />}
146
174
  isClicked={activeButton === 'negative'}
@@ -1,6 +1,6 @@
1
1
  .pf-chatbot__tool-call {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
3
2
  --pf-v6-c-card--BorderRadius: var(--pf-t--global--border--radius--small);
3
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
4
4
 
5
5
  overflow: unset;
6
6
  row-gap: var(--pf-t--global--spacer--sm);
@@ -1,5 +1,5 @@
1
1
  .pf-chatbot__tool-response {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
3
3
  overflow: unset;
4
4
  }
5
5
 
@@ -25,12 +25,12 @@
25
25
  }
26
26
 
27
27
  .pf-chatbot__tool-response-card {
28
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
28
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
29
29
  --pf-v6-c-card--first-child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
30
30
  --pf-v6-c-card__title--not--last-child--PaddingBlockEnd: var(--pf-t--global--spacer--sm);
31
31
  --pf-v6-c-card--c-divider--child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
32
32
 
33
33
  .pf-v6-c-divider {
34
- --pf-v6-c-divider--Color: var(--pf-t--global--border--color--control--read-only);
34
+ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default);
35
35
  }
36
36
  }