@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.
- package/dist/cjs/Message/Message.d.ts +3 -0
- package/dist/cjs/Message/Message.js +2 -2
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +3 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +28 -7
- package/dist/cjs/ResponseActions/ResponseActions.test.js +67 -12
- package/dist/css/main.css +6 -6
- package/dist/esm/Message/Message.d.ts +3 -0
- package/dist/esm/Message/Message.js +2 -2
- package/dist/esm/ResponseActions/ResponseActions.d.ts +3 -0
- package/dist/esm/ResponseActions/ResponseActions.js +28 -7
- package/dist/esm/ResponseActions/ResponseActions.test.js +67 -12
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithPersistedActions.tsx +22 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +14 -0
- package/src/DeepThinking/DeepThinking.scss +1 -1
- package/src/Message/Message.tsx +7 -1
- package/src/ResponseActions/ResponseActions.scss +1 -1
- package/src/ResponseActions/ResponseActions.test.tsx +111 -12
- package/src/ResponseActions/ResponseActions.tsx +38 -10
- package/src/ToolCall/ToolCall.scss +1 -1
- package/src/ToolResponse/ToolResponse.scss +3 -3
|
@@ -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
|
-
|
|
65
|
-
|
|
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 : '
|
|
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: '
|
|
24
|
-
{ type: 'negative', label: 'Bad response', clickedLabel: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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--
|
|
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--
|
|
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--
|
|
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--
|
|
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
|
-
|
|
59
|
-
|
|
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 : '
|
|
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: '
|
|
19
|
-
{ type: 'negative', label: 'Bad response', clickedLabel: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithPersistedActions.tsx
ADDED
|
@@ -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:
|
package/src/Message/Message.tsx
CHANGED
|
@@ -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 &&
|
|
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: '
|
|
10
|
-
{ type: 'negative', label: 'Bad response', clickedLabel: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> = ({
|
|
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
|
-
|
|
110
|
-
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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--
|
|
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--
|
|
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--
|
|
34
|
+
--pf-v6-c-divider--Color: var(--pf-t--global--border--color--default);
|
|
35
35
|
}
|
|
36
36
|
}
|