@patternfly/chatbot 6.6.0-prerelease.4 → 6.6.0-prerelease.5
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 +2 -0
- package/dist/cjs/Message/Message.js +2 -2
- package/dist/cjs/Message/Message.test.js +37 -0
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +3 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +20 -2
- package/dist/cjs/ResponseActions/ResponseActions.test.js +106 -0
- package/dist/esm/Message/Message.d.ts +2 -0
- package/dist/esm/Message/Message.js +2 -2
- package/dist/esm/Message/Message.test.js +37 -0
- package/dist/esm/ResponseActions/ResponseActions.d.ts +3 -0
- package/dist/esm/ResponseActions/ResponseActions.js +21 -3
- package/dist/esm/ResponseActions/ResponseActions.test.js +107 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithIconSwapping.tsx +22 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +11 -0
- package/src/Message/Message.test.tsx +64 -0
- package/src/Message/Message.tsx +9 -1
- package/src/ResponseActions/ResponseActions.test.tsx +200 -0
- package/src/ResponseActions/ResponseActions.tsx +31 -3
|
@@ -168,6 +168,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
|
|
|
168
168
|
hasNoImagesInUserMessages?: boolean;
|
|
169
169
|
/** Sets background colors to be appropriate on primary chatbot background */
|
|
170
170
|
isPrimary?: boolean;
|
|
171
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked. */
|
|
172
|
+
useFilledIconsOnClick?: boolean;
|
|
171
173
|
}
|
|
172
174
|
export declare const MessageBase: FunctionComponent<MessageProps>;
|
|
173
175
|
declare const Message: import("react").ForwardRefExoticComponent<Omit<MessageProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -39,7 +39,7 @@ const ToolCall_1 = __importDefault(require("../ToolCall"));
|
|
|
39
39
|
const MarkdownContent_1 = __importDefault(require("../MarkdownContent"));
|
|
40
40
|
const react_styles_1 = require("@patternfly/react-styles");
|
|
41
41
|
const MessageBase = (_a) => {
|
|
42
|
-
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary"]);
|
|
42
|
+
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
|
|
43
43
|
const [messageText, setMessageText] = (0, react_1.useState)(content);
|
|
44
44
|
(0, react_1.useEffect)(() => {
|
|
45
45
|
setMessageText(content);
|
|
@@ -67,7 +67,7 @@ const MessageBase = (_a) => {
|
|
|
67
67
|
}
|
|
68
68
|
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()] }));
|
|
69
69
|
};
|
|
70
|
-
return ((0, jsx_runtime_1.jsxs)("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: (0, react_styles_1.css)(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && ((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: [isMetadataVisible && ((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.jsx)("div", { className: "pf-chatbot__message-response", children: children ? ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { 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)(jsx_runtime_1.Fragment, { children: Array.isArray(actions) ? ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__response-actions-groups", children: actions.map((actionGroup, index) => ((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection }, index))) })) : ((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) => {
|
|
70
|
+
return ((0, jsx_runtime_1.jsxs)("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: (0, react_styles_1.css)(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && ((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: [isMetadataVisible && ((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.jsx)("div", { className: "pf-chatbot__message-response", children: children ? ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { 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)(jsx_runtime_1.Fragment, { children: Array.isArray(actions) ? ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__response-actions-groups", children: actions.map((actionGroup, index) => ((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick }, index))) })) : ((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions, persistActionSelection: persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick })) })), 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) => {
|
|
71
71
|
var _a;
|
|
72
72
|
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));
|
|
73
73
|
}) })), !isLoading && endContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: endContent })] })) })] })] })));
|
|
@@ -22,6 +22,22 @@ const monitor_sampleapp_quickstart_1 = require("./QuickStarts/monitor-sampleapp-
|
|
|
22
22
|
const monitor_sampleapp_quickstart_with_image_1 = require("./QuickStarts/monitor-sampleapp-quickstart-with-image");
|
|
23
23
|
const rehype_external_links_1 = __importDefault(require("../__mocks__/rehype-external-links"));
|
|
24
24
|
const react_core_1 = require("@patternfly/react-core");
|
|
25
|
+
// Mock the icon components
|
|
26
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
27
|
+
OutlinedThumbsUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedThumbsUpIcon" }),
|
|
28
|
+
ThumbsUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ThumbsUpIcon" }),
|
|
29
|
+
OutlinedThumbsDownIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedThumbsDownIcon" }),
|
|
30
|
+
ThumbsDownIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ThumbsDownIcon" }),
|
|
31
|
+
OutlinedCopyIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedCopyIcon" }),
|
|
32
|
+
DownloadIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "DownloadIcon" }),
|
|
33
|
+
ExternalLinkAltIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ExternalLinkAltIcon" }),
|
|
34
|
+
VolumeUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "VolumeUpIcon" }),
|
|
35
|
+
PencilAltIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "PencilAltIcon" }),
|
|
36
|
+
CheckIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "CheckIcon" }),
|
|
37
|
+
CloseIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "CloseIcon" }),
|
|
38
|
+
ExternalLinkSquareAltIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ExternalLinkSquareAltIcon" }),
|
|
39
|
+
TimesIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "TimesIcon" })
|
|
40
|
+
}));
|
|
25
41
|
const ALL_ACTIONS = [
|
|
26
42
|
{ label: /Good response/i },
|
|
27
43
|
{ label: /Bad response/i },
|
|
@@ -1004,4 +1020,25 @@ describe('Message', () => {
|
|
|
1004
1020
|
(0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { alignment: "end", avatar: "./img", role: "user", name: "User", content: "" }));
|
|
1005
1021
|
expect(react_2.screen.getByRole('region')).toHaveClass('pf-m-end');
|
|
1006
1022
|
});
|
|
1023
|
+
// We're just testing the positive action here to ensure logic passes through as needed, the other actions are
|
|
1024
|
+
// tested in ResponseActions.test.tsx along with other aspects of this functionality
|
|
1025
|
+
it('should not swap icons when useFilledIconsOnClick is omitted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1026
|
+
const user = user_event_1.default.setup();
|
|
1027
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1028
|
+
positive: { onClick: jest.fn() }
|
|
1029
|
+
} }));
|
|
1030
|
+
expect(react_2.screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1031
|
+
yield user.click(react_2.screen.getByRole('button', { name: /Good response/i }));
|
|
1032
|
+
expect(react_2.screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1033
|
+
expect(react_2.screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
1034
|
+
}));
|
|
1035
|
+
it('should swap icons when useFilledIconsOnClick is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1036
|
+
const user = user_event_1.default.setup();
|
|
1037
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1038
|
+
positive: { onClick: jest.fn() }
|
|
1039
|
+
}, useFilledIconsOnClick: true }));
|
|
1040
|
+
yield user.click(react_2.screen.getByRole('button', { name: /Good response/i }));
|
|
1041
|
+
expect(react_2.screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
1042
|
+
expect(react_2.screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
1043
|
+
}));
|
|
1007
1044
|
});
|
|
@@ -47,6 +47,9 @@ export interface ResponseActionProps {
|
|
|
47
47
|
/** When true, the selected action will persist even when clicking outside the component.
|
|
48
48
|
* When false (default), clicking outside or clicking another action will deselect the current selection. */
|
|
49
49
|
persistActionSelection?: boolean;
|
|
50
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked.
|
|
51
|
+
* Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
|
|
52
|
+
useFilledIconsOnClick?: boolean;
|
|
50
53
|
}
|
|
51
54
|
export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
|
|
52
55
|
export default ResponseActions;
|
|
@@ -20,7 +20,7 @@ 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, persistActionSelection = false }) => {
|
|
23
|
+
const ResponseActions = ({ actions, persistActionSelection = false, useFilledIconsOnClick = 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);
|
|
@@ -69,6 +69,7 @@ const ResponseActions = ({ actions, persistActionSelection = false }) => {
|
|
|
69
69
|
};
|
|
70
70
|
}, [clickStatePersisted, persistActionSelection]);
|
|
71
71
|
const handleClick = (e, id, onClick) => {
|
|
72
|
+
e.stopPropagation();
|
|
72
73
|
if (persistActionSelection) {
|
|
73
74
|
if (activeButton === id) {
|
|
74
75
|
// Toggle off if clicking the same button
|
|
@@ -86,7 +87,24 @@ const ResponseActions = ({ actions, persistActionSelection = false }) => {
|
|
|
86
87
|
}
|
|
87
88
|
onClick && onClick(e);
|
|
88
89
|
};
|
|
89
|
-
|
|
90
|
+
const iconMap = {
|
|
91
|
+
positive: {
|
|
92
|
+
filled: (0, jsx_runtime_1.jsx)(react_icons_1.ThumbsUpIcon, {}),
|
|
93
|
+
outlined: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsUpIcon, {})
|
|
94
|
+
},
|
|
95
|
+
negative: {
|
|
96
|
+
filled: (0, jsx_runtime_1.jsx)(react_icons_1.ThumbsDownIcon, {}),
|
|
97
|
+
outlined: (0, jsx_runtime_1.jsx)(react_icons_1.OutlinedThumbsDownIcon, {})
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const getIcon = (actionName) => {
|
|
101
|
+
const isClicked = activeButton === actionName;
|
|
102
|
+
if (isClicked && useFilledIconsOnClick) {
|
|
103
|
+
return iconMap[actionName].filled;
|
|
104
|
+
}
|
|
105
|
+
return iconMap[actionName].outlined;
|
|
106
|
+
};
|
|
107
|
+
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: getIcon('positive'), 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: getIcon('negative'), 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) => {
|
|
90
108
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
91
109
|
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'] })));
|
|
92
110
|
})] }));
|
|
@@ -19,6 +19,20 @@ const ResponseActions_1 = __importDefault(require("./ResponseActions"));
|
|
|
19
19
|
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
|
+
// Mock the icon components
|
|
23
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
24
|
+
OutlinedThumbsUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedThumbsUpIcon" }),
|
|
25
|
+
ThumbsUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ThumbsUpIcon" }),
|
|
26
|
+
OutlinedThumbsDownIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedThumbsDownIcon" }),
|
|
27
|
+
ThumbsDownIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ThumbsDownIcon" }),
|
|
28
|
+
OutlinedCopyIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "OutlinedCopyIcon" }),
|
|
29
|
+
DownloadIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "DownloadIcon" }),
|
|
30
|
+
InfoCircleIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "InfoCircleIcon" }),
|
|
31
|
+
RedoIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "RedoIcon" }),
|
|
32
|
+
ExternalLinkAltIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "ExternalLinkAltIcon" }),
|
|
33
|
+
VolumeUpIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "VolumeUpIcon" }),
|
|
34
|
+
PencilAltIcon: () => (0, jsx_runtime_1.jsx)("div", { children: "PencilAltIcon" })
|
|
35
|
+
}));
|
|
22
36
|
const ALL_ACTIONS = [
|
|
23
37
|
{ type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
|
|
24
38
|
{ type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
|
|
@@ -315,4 +329,96 @@ describe('ResponseActions', () => {
|
|
|
315
329
|
yield user_event_1.default.click(customBtn);
|
|
316
330
|
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
|
317
331
|
}));
|
|
332
|
+
describe('icon swapping with useFilledIconsOnClick', () => {
|
|
333
|
+
it('should render outline icons by default', () => {
|
|
334
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
335
|
+
positive: { onClick: jest.fn() },
|
|
336
|
+
negative: { onClick: jest.fn() }
|
|
337
|
+
} }));
|
|
338
|
+
expect(react_1.screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
339
|
+
expect(react_1.screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
340
|
+
expect(react_1.screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
341
|
+
expect(react_1.screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
describe('positive actions', () => {
|
|
344
|
+
it('should not swap positive icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
345
|
+
const user = user_event_1.default.setup();
|
|
346
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
347
|
+
positive: { onClick: jest.fn() }
|
|
348
|
+
}, useFilledIconsOnClick: false }));
|
|
349
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Good response' }));
|
|
350
|
+
expect(react_1.screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
351
|
+
expect(react_1.screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
352
|
+
}));
|
|
353
|
+
it('should swap positive icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
354
|
+
const user = user_event_1.default.setup();
|
|
355
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
356
|
+
positive: { onClick: jest.fn() }
|
|
357
|
+
}, useFilledIconsOnClick: true }));
|
|
358
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Good response' }));
|
|
359
|
+
expect(react_1.screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
360
|
+
expect(react_1.screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
361
|
+
}));
|
|
362
|
+
it('should revert positive icon to outline icon when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
363
|
+
const user = user_event_1.default.setup();
|
|
364
|
+
(0, react_1.render)((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
365
|
+
positive: { onClick: jest.fn() }
|
|
366
|
+
}, useFilledIconsOnClick: true }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
367
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Good response' }));
|
|
368
|
+
expect(react_1.screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
369
|
+
yield user.click(react_1.screen.getByTestId('outside'));
|
|
370
|
+
expect(react_1.screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
371
|
+
}));
|
|
372
|
+
it('should not revert positive icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
373
|
+
const user = user_event_1.default.setup();
|
|
374
|
+
(0, react_1.render)((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
375
|
+
positive: { onClick: jest.fn() }
|
|
376
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
377
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Good response' }));
|
|
378
|
+
expect(react_1.screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
379
|
+
yield user.click(react_1.screen.getByTestId('outside'));
|
|
380
|
+
expect(react_1.screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
381
|
+
}));
|
|
382
|
+
describe('negative actions', () => {
|
|
383
|
+
it('should not swap negative icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
384
|
+
const user = user_event_1.default.setup();
|
|
385
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
386
|
+
negative: { onClick: jest.fn() }
|
|
387
|
+
}, useFilledIconsOnClick: false }));
|
|
388
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Bad response' }));
|
|
389
|
+
expect(react_1.screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
390
|
+
expect(react_1.screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
391
|
+
}));
|
|
392
|
+
it('should swap negative icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
393
|
+
const user = user_event_1.default.setup();
|
|
394
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
395
|
+
negative: { onClick: jest.fn() }
|
|
396
|
+
}, useFilledIconsOnClick: true }));
|
|
397
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Bad response' }));
|
|
398
|
+
expect(react_1.screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
399
|
+
expect(react_1.screen.queryByText('OutlinedThumbsDownIcon')).not.toBeInTheDocument();
|
|
400
|
+
}));
|
|
401
|
+
it('should revert negative icon to outline when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
402
|
+
const user = user_event_1.default.setup();
|
|
403
|
+
(0, react_1.render)((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
404
|
+
negative: { onClick: jest.fn() }
|
|
405
|
+
}, useFilledIconsOnClick: true }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
406
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Bad response' }));
|
|
407
|
+
expect(react_1.screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
408
|
+
yield user.click(react_1.screen.getByTestId('outside'));
|
|
409
|
+
expect(react_1.screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
410
|
+
}));
|
|
411
|
+
it('should not revert negative icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
412
|
+
const user = user_event_1.default.setup();
|
|
413
|
+
(0, react_1.render)((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
|
|
414
|
+
negative: { onClick: jest.fn() }
|
|
415
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
416
|
+
yield user.click(react_1.screen.getByRole('button', { name: 'Bad response' }));
|
|
417
|
+
expect(react_1.screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
418
|
+
yield user.click(react_1.screen.getByTestId('outside'));
|
|
419
|
+
expect(react_1.screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
420
|
+
}));
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
318
424
|
});
|
|
@@ -168,6 +168,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
|
|
|
168
168
|
hasNoImagesInUserMessages?: boolean;
|
|
169
169
|
/** Sets background colors to be appropriate on primary chatbot background */
|
|
170
170
|
isPrimary?: boolean;
|
|
171
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked. */
|
|
172
|
+
useFilledIconsOnClick?: boolean;
|
|
171
173
|
}
|
|
172
174
|
export declare const MessageBase: FunctionComponent<MessageProps>;
|
|
173
175
|
declare const Message: import("react").ForwardRefExoticComponent<Omit<MessageProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -33,7 +33,7 @@ import ToolCall from '../ToolCall';
|
|
|
33
33
|
import MarkdownContent from '../MarkdownContent';
|
|
34
34
|
import { css } from '@patternfly/react-styles';
|
|
35
35
|
export const MessageBase = (_a) => {
|
|
36
|
-
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary"]);
|
|
36
|
+
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
|
|
37
37
|
const [messageText, setMessageText] = useState(content);
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
setMessageText(content);
|
|
@@ -61,7 +61,7 @@ export const MessageBase = (_a) => {
|
|
|
61
61
|
}
|
|
62
62
|
return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? _jsx(ErrorMessage, Object.assign({}, error)) : handleMarkdown()] }));
|
|
63
63
|
};
|
|
64
|
-
return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className: "pf-chatbot__response-actions-groups", children: actions.map((actionGroup, index) => (_jsx(ResponseActions, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection }, index))) })) : (_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) => {
|
|
64
|
+
return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className: "pf-chatbot__response-actions-groups", children: actions.map((actionGroup, index) => (_jsx(ResponseActions, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick }, index))) })) : (_jsx(ResponseActions, { actions: actions, persistActionSelection: persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick })) })), userFeedbackForm && (_jsx(UserFeedback, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact }))), userFeedbackComplete && (_jsx(UserFeedbackComplete, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && (_jsx("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
|
|
65
65
|
var _a;
|
|
66
66
|
return (_jsx("div", { className: "pf-chatbot__message-attachment", children: _jsx(FileDetailsLabel, { fileName: attachment.name, fileId: attachment.id, onClose: attachment.onClose, onClick: attachment.onClick, isLoading: attachment.isLoading, closeButtonAriaLabel: attachment.closeButtonAriaLabel, languageTestId: attachment.languageTestId, spinnerTestId: attachment.spinnerTestId, variant: isPrimary ? 'outline' : undefined }) }, (_a = attachment.id) !== null && _a !== void 0 ? _a : attachment.name));
|
|
67
67
|
}) })), !isLoading && endContent && _jsx(_Fragment, { children: endContent })] })) })] })] })));
|
|
@@ -17,6 +17,22 @@ import { monitorSampleAppQuickStart } from './QuickStarts/monitor-sampleapp-quic
|
|
|
17
17
|
import { monitorSampleAppQuickStartWithImage } from './QuickStarts/monitor-sampleapp-quickstart-with-image';
|
|
18
18
|
import rehypeExternalLinks from '../__mocks__/rehype-external-links';
|
|
19
19
|
import { AlertActionLink, Button, CodeBlockAction } from '@patternfly/react-core';
|
|
20
|
+
// Mock the icon components
|
|
21
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
22
|
+
OutlinedThumbsUpIcon: () => _jsx("div", { children: "OutlinedThumbsUpIcon" }),
|
|
23
|
+
ThumbsUpIcon: () => _jsx("div", { children: "ThumbsUpIcon" }),
|
|
24
|
+
OutlinedThumbsDownIcon: () => _jsx("div", { children: "OutlinedThumbsDownIcon" }),
|
|
25
|
+
ThumbsDownIcon: () => _jsx("div", { children: "ThumbsDownIcon" }),
|
|
26
|
+
OutlinedCopyIcon: () => _jsx("div", { children: "OutlinedCopyIcon" }),
|
|
27
|
+
DownloadIcon: () => _jsx("div", { children: "DownloadIcon" }),
|
|
28
|
+
ExternalLinkAltIcon: () => _jsx("div", { children: "ExternalLinkAltIcon" }),
|
|
29
|
+
VolumeUpIcon: () => _jsx("div", { children: "VolumeUpIcon" }),
|
|
30
|
+
PencilAltIcon: () => _jsx("div", { children: "PencilAltIcon" }),
|
|
31
|
+
CheckIcon: () => _jsx("div", { children: "CheckIcon" }),
|
|
32
|
+
CloseIcon: () => _jsx("div", { children: "CloseIcon" }),
|
|
33
|
+
ExternalLinkSquareAltIcon: () => _jsx("div", { children: "ExternalLinkSquareAltIcon" }),
|
|
34
|
+
TimesIcon: () => _jsx("div", { children: "TimesIcon" })
|
|
35
|
+
}));
|
|
20
36
|
const ALL_ACTIONS = [
|
|
21
37
|
{ label: /Good response/i },
|
|
22
38
|
{ label: /Bad response/i },
|
|
@@ -999,4 +1015,25 @@ describe('Message', () => {
|
|
|
999
1015
|
render(_jsx(Message, { alignment: "end", avatar: "./img", role: "user", name: "User", content: "" }));
|
|
1000
1016
|
expect(screen.getByRole('region')).toHaveClass('pf-m-end');
|
|
1001
1017
|
});
|
|
1018
|
+
// We're just testing the positive action here to ensure logic passes through as needed, the other actions are
|
|
1019
|
+
// tested in ResponseActions.test.tsx along with other aspects of this functionality
|
|
1020
|
+
it('should not swap icons when useFilledIconsOnClick is omitted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1021
|
+
const user = userEvent.setup();
|
|
1022
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1023
|
+
positive: { onClick: jest.fn() }
|
|
1024
|
+
} }));
|
|
1025
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1026
|
+
yield user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1027
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1028
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
1029
|
+
}));
|
|
1030
|
+
it('should swap icons when useFilledIconsOnClick is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1031
|
+
const user = userEvent.setup();
|
|
1032
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1033
|
+
positive: { onClick: jest.fn() }
|
|
1034
|
+
}, useFilledIconsOnClick: true }));
|
|
1035
|
+
yield user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1036
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
1037
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
1038
|
+
}));
|
|
1002
1039
|
});
|
|
@@ -47,6 +47,9 @@ export interface ResponseActionProps {
|
|
|
47
47
|
/** When true, the selected action will persist even when clicking outside the component.
|
|
48
48
|
* When false (default), clicking outside or clicking another action will deselect the current selection. */
|
|
49
49
|
persistActionSelection?: boolean;
|
|
50
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked.
|
|
51
|
+
* Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
|
|
52
|
+
useFilledIconsOnClick?: boolean;
|
|
50
53
|
}
|
|
51
54
|
export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
|
|
52
55
|
export default ResponseActions;
|
|
@@ -12,9 +12,9 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import { createElement as _createElement } from "react";
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { useEffect, useRef, useState } from 'react';
|
|
15
|
-
import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, OutlinedThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
|
|
15
|
+
import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, ThumbsUpIcon, OutlinedThumbsDownIcon, ThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
|
|
16
16
|
import ResponseActionButton from './ResponseActionButton';
|
|
17
|
-
export const ResponseActions = ({ actions, persistActionSelection = false }) => {
|
|
17
|
+
export const ResponseActions = ({ actions, persistActionSelection = false, useFilledIconsOnClick = 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);
|
|
@@ -63,6 +63,7 @@ export const ResponseActions = ({ actions, persistActionSelection = false }) =>
|
|
|
63
63
|
};
|
|
64
64
|
}, [clickStatePersisted, persistActionSelection]);
|
|
65
65
|
const handleClick = (e, id, onClick) => {
|
|
66
|
+
e.stopPropagation();
|
|
66
67
|
if (persistActionSelection) {
|
|
67
68
|
if (activeButton === id) {
|
|
68
69
|
// Toggle off if clicking the same button
|
|
@@ -80,7 +81,24 @@ export const ResponseActions = ({ actions, persistActionSelection = false }) =>
|
|
|
80
81
|
}
|
|
81
82
|
onClick && onClick(e);
|
|
82
83
|
};
|
|
83
|
-
|
|
84
|
+
const iconMap = {
|
|
85
|
+
positive: {
|
|
86
|
+
filled: _jsx(ThumbsUpIcon, {}),
|
|
87
|
+
outlined: _jsx(OutlinedThumbsUpIcon, {})
|
|
88
|
+
},
|
|
89
|
+
negative: {
|
|
90
|
+
filled: _jsx(ThumbsDownIcon, {}),
|
|
91
|
+
outlined: _jsx(OutlinedThumbsDownIcon, {})
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const getIcon = (actionName) => {
|
|
95
|
+
const isClicked = activeButton === actionName;
|
|
96
|
+
if (isClicked && useFilledIconsOnClick) {
|
|
97
|
+
return iconMap[actionName].filled;
|
|
98
|
+
}
|
|
99
|
+
return iconMap[actionName].outlined;
|
|
100
|
+
};
|
|
101
|
+
return (_jsxs("div", { ref: responseActions, className: "pf-chatbot__response-actions", children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_a = positive.ariaLabel) !== null && _a !== void 0 ? _a : 'Good response', clickedAriaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_c = positive.tooltipContent) !== null && _c !== void 0 ? _c : 'Good response', clickedTooltipContent: (_d = positive.clickedTooltipContent) !== null && _d !== void 0 ? _d : 'Good response recorded', tooltipProps: positive.tooltipProps, icon: getIcon('positive'), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_e = negative.ariaLabel) !== null && _e !== void 0 ? _e : 'Bad response', clickedAriaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_g = negative.tooltipContent) !== null && _g !== void 0 ? _g : 'Bad response', clickedTooltipContent: (_h = negative.clickedTooltipContent) !== null && _h !== void 0 ? _h : 'Bad response recorded', tooltipProps: negative.tooltipProps, icon: getIcon('negative'), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_j = copy.ariaLabel) !== null && _j !== void 0 ? _j : 'Copy', clickedAriaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_l = copy.tooltipContent) !== null && _l !== void 0 ? _l : 'Copy', clickedTooltipContent: (_m = copy.clickedTooltipContent) !== null && _m !== void 0 ? _m : 'Copied', tooltipProps: copy.tooltipProps, icon: _jsx(OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && (_jsx(ResponseActionButton, Object.assign({}, edit, { ariaLabel: (_o = edit.ariaLabel) !== null && _o !== void 0 ? _o : 'Edit', clickedAriaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_q = edit.tooltipContent) !== null && _q !== void 0 ? _q : 'Edit ', clickedTooltipContent: (_r = edit.clickedTooltipContent) !== null && _r !== void 0 ? _r : 'Editing', tooltipProps: edit.tooltipProps, icon: _jsx(PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && (_jsx(ResponseActionButton, Object.assign({}, share, { ariaLabel: (_s = share.ariaLabel) !== null && _s !== void 0 ? _s : 'Share', clickedAriaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_u = share.tooltipContent) !== null && _u !== void 0 ? _u : 'Share', clickedTooltipContent: (_v = share.clickedTooltipContent) !== null && _v !== void 0 ? _v : 'Shared', tooltipProps: share.tooltipProps, icon: _jsx(ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && (_jsx(ResponseActionButton, Object.assign({}, download, { ariaLabel: (_w = download.ariaLabel) !== null && _w !== void 0 ? _w : 'Download', clickedAriaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_y = download.tooltipContent) !== null && _y !== void 0 ? _y : 'Download', clickedTooltipContent: (_z = download.clickedTooltipContent) !== null && _z !== void 0 ? _z : 'Downloaded', tooltipProps: download.tooltipProps, icon: _jsx(DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && (_jsx(ResponseActionButton, Object.assign({}, listen, { ariaLabel: (_0 = listen.ariaLabel) !== null && _0 !== void 0 ? _0 : 'Listen', clickedAriaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_2 = listen.tooltipContent) !== null && _2 !== void 0 ? _2 : 'Listen', clickedTooltipContent: (_3 = listen.clickedTooltipContent) !== null && _3 !== void 0 ? _3 : 'Listening', tooltipProps: listen.tooltipProps, icon: _jsx(VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
|
|
84
102
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
85
103
|
return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: (_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps, clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
|
|
86
104
|
})] }));
|
|
@@ -7,13 +7,27 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { render, screen } from '@testing-library/react';
|
|
12
12
|
import '@testing-library/jest-dom';
|
|
13
13
|
import ResponseActions from './ResponseActions';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
|
16
16
|
import Message from '../Message';
|
|
17
|
+
// Mock the icon components
|
|
18
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
19
|
+
OutlinedThumbsUpIcon: () => _jsx("div", { children: "OutlinedThumbsUpIcon" }),
|
|
20
|
+
ThumbsUpIcon: () => _jsx("div", { children: "ThumbsUpIcon" }),
|
|
21
|
+
OutlinedThumbsDownIcon: () => _jsx("div", { children: "OutlinedThumbsDownIcon" }),
|
|
22
|
+
ThumbsDownIcon: () => _jsx("div", { children: "ThumbsDownIcon" }),
|
|
23
|
+
OutlinedCopyIcon: () => _jsx("div", { children: "OutlinedCopyIcon" }),
|
|
24
|
+
DownloadIcon: () => _jsx("div", { children: "DownloadIcon" }),
|
|
25
|
+
InfoCircleIcon: () => _jsx("div", { children: "InfoCircleIcon" }),
|
|
26
|
+
RedoIcon: () => _jsx("div", { children: "RedoIcon" }),
|
|
27
|
+
ExternalLinkAltIcon: () => _jsx("div", { children: "ExternalLinkAltIcon" }),
|
|
28
|
+
VolumeUpIcon: () => _jsx("div", { children: "VolumeUpIcon" }),
|
|
29
|
+
PencilAltIcon: () => _jsx("div", { children: "PencilAltIcon" })
|
|
30
|
+
}));
|
|
17
31
|
const ALL_ACTIONS = [
|
|
18
32
|
{ type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
|
|
19
33
|
{ type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
|
|
@@ -310,4 +324,96 @@ describe('ResponseActions', () => {
|
|
|
310
324
|
yield userEvent.click(customBtn);
|
|
311
325
|
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
|
312
326
|
}));
|
|
327
|
+
describe('icon swapping with useFilledIconsOnClick', () => {
|
|
328
|
+
it('should render outline icons by default', () => {
|
|
329
|
+
render(_jsx(ResponseActions, { actions: {
|
|
330
|
+
positive: { onClick: jest.fn() },
|
|
331
|
+
negative: { onClick: jest.fn() }
|
|
332
|
+
} }));
|
|
333
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
334
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
335
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
336
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
describe('positive actions', () => {
|
|
339
|
+
it('should not swap positive icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
340
|
+
const user = userEvent.setup();
|
|
341
|
+
render(_jsx(ResponseActions, { actions: {
|
|
342
|
+
positive: { onClick: jest.fn() }
|
|
343
|
+
}, useFilledIconsOnClick: false }));
|
|
344
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
345
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
346
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
347
|
+
}));
|
|
348
|
+
it('should swap positive icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
349
|
+
const user = userEvent.setup();
|
|
350
|
+
render(_jsx(ResponseActions, { actions: {
|
|
351
|
+
positive: { onClick: jest.fn() }
|
|
352
|
+
}, useFilledIconsOnClick: true }));
|
|
353
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
354
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
355
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
356
|
+
}));
|
|
357
|
+
it('should revert positive icon to outline icon when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
358
|
+
const user = userEvent.setup();
|
|
359
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
360
|
+
positive: { onClick: jest.fn() }
|
|
361
|
+
}, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
362
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
363
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
364
|
+
yield user.click(screen.getByTestId('outside'));
|
|
365
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
366
|
+
}));
|
|
367
|
+
it('should not revert positive icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
368
|
+
const user = userEvent.setup();
|
|
369
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
370
|
+
positive: { onClick: jest.fn() }
|
|
371
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
372
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
373
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
374
|
+
yield user.click(screen.getByTestId('outside'));
|
|
375
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
376
|
+
}));
|
|
377
|
+
describe('negative actions', () => {
|
|
378
|
+
it('should not swap negative icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
379
|
+
const user = userEvent.setup();
|
|
380
|
+
render(_jsx(ResponseActions, { actions: {
|
|
381
|
+
negative: { onClick: jest.fn() }
|
|
382
|
+
}, useFilledIconsOnClick: false }));
|
|
383
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
384
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
385
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
386
|
+
}));
|
|
387
|
+
it('should swap negative icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
388
|
+
const user = userEvent.setup();
|
|
389
|
+
render(_jsx(ResponseActions, { actions: {
|
|
390
|
+
negative: { onClick: jest.fn() }
|
|
391
|
+
}, useFilledIconsOnClick: true }));
|
|
392
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
393
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
394
|
+
expect(screen.queryByText('OutlinedThumbsDownIcon')).not.toBeInTheDocument();
|
|
395
|
+
}));
|
|
396
|
+
it('should revert negative icon to outline when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
397
|
+
const user = userEvent.setup();
|
|
398
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
399
|
+
negative: { onClick: jest.fn() }
|
|
400
|
+
}, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
401
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
402
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
403
|
+
yield user.click(screen.getByTestId('outside'));
|
|
404
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
405
|
+
}));
|
|
406
|
+
it('should not revert negative icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
407
|
+
const user = userEvent.setup();
|
|
408
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
409
|
+
negative: { onClick: jest.fn() }
|
|
410
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
411
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
412
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
413
|
+
yield user.click(screen.getByTestId('outside'));
|
|
414
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
415
|
+
}));
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
313
419
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/chatbot",
|
|
3
|
-
"version": "6.6.0-prerelease.
|
|
3
|
+
"version": "6.6.0-prerelease.5",
|
|
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/MessageWithIconSwapping.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 IconSwappingExample: FunctionComponent = () => (
|
|
7
|
+
<Message
|
|
8
|
+
name="Bot"
|
|
9
|
+
role="bot"
|
|
10
|
+
avatar={patternflyAvatar}
|
|
11
|
+
content="Click the response actions to see the outlined icons swapped with the filled variants!"
|
|
12
|
+
actions={{
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
positive: { onClick: () => console.log('Good response') },
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
negative: { onClick: () => console.log('Bad response') },
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
copy: { onClick: () => console.log('Copied') }
|
|
19
|
+
}}
|
|
20
|
+
useFilledIconsOnClick
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
@@ -138,6 +138,17 @@ When `persistActionSelection` is `true`:
|
|
|
138
138
|
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
### Message actions that fill
|
|
142
|
+
|
|
143
|
+
To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
|
|
144
|
+
|
|
145
|
+
This is especially useful for actions that are intended to persist (such as the "positive" and "negative" responses), so that a user's selection is more clear and emphasized.
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
```js file="./MessageWithIconSwapping.tsx"
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
|
|
141
152
|
### Multiple messsage action groups
|
|
142
153
|
|
|
143
154
|
To maintain finer control over message action selection behavior, you can create groups of actions by passing an array of objects to the `actions` prop. This allows you to separate actions into conceptually or functionally different groups and implement different behavior for each group as needed. For example, you could separate feedback actions (thumbs up/down) form utility actions (copy and download), and have different selection behaviors for each group.
|
|
@@ -9,6 +9,23 @@ import rehypeExternalLinks from '../__mocks__/rehype-external-links';
|
|
|
9
9
|
import { AlertActionLink, Button, CodeBlockAction } from '@patternfly/react-core';
|
|
10
10
|
import { DeepThinkingProps } from '../DeepThinking';
|
|
11
11
|
|
|
12
|
+
// Mock the icon components
|
|
13
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
14
|
+
OutlinedThumbsUpIcon: () => <div>OutlinedThumbsUpIcon</div>,
|
|
15
|
+
ThumbsUpIcon: () => <div>ThumbsUpIcon</div>,
|
|
16
|
+
OutlinedThumbsDownIcon: () => <div>OutlinedThumbsDownIcon</div>,
|
|
17
|
+
ThumbsDownIcon: () => <div>ThumbsDownIcon</div>,
|
|
18
|
+
OutlinedCopyIcon: () => <div>OutlinedCopyIcon</div>,
|
|
19
|
+
DownloadIcon: () => <div>DownloadIcon</div>,
|
|
20
|
+
ExternalLinkAltIcon: () => <div>ExternalLinkAltIcon</div>,
|
|
21
|
+
VolumeUpIcon: () => <div>VolumeUpIcon</div>,
|
|
22
|
+
PencilAltIcon: () => <div>PencilAltIcon</div>,
|
|
23
|
+
CheckIcon: () => <div>CheckIcon</div>,
|
|
24
|
+
CloseIcon: () => <div>CloseIcon</div>,
|
|
25
|
+
ExternalLinkSquareAltIcon: () => <div>ExternalLinkSquareAltIcon</div>,
|
|
26
|
+
TimesIcon: () => <div>TimesIcon</div>
|
|
27
|
+
}));
|
|
28
|
+
|
|
12
29
|
const ALL_ACTIONS = [
|
|
13
30
|
{ label: /Good response/i },
|
|
14
31
|
{ label: /Bad response/i },
|
|
@@ -1351,4 +1368,51 @@ describe('Message', () => {
|
|
|
1351
1368
|
render(<Message alignment="end" avatar="./img" role="user" name="User" content="" />);
|
|
1352
1369
|
expect(screen.getByRole('region')).toHaveClass('pf-m-end');
|
|
1353
1370
|
});
|
|
1371
|
+
|
|
1372
|
+
// We're just testing the positive action here to ensure logic passes through as needed, the other actions are
|
|
1373
|
+
// tested in ResponseActions.test.tsx along with other aspects of this functionality
|
|
1374
|
+
it('should not swap icons when useFilledIconsOnClick is omitted', async () => {
|
|
1375
|
+
const user = userEvent.setup();
|
|
1376
|
+
|
|
1377
|
+
render(
|
|
1378
|
+
<Message
|
|
1379
|
+
avatar="./img"
|
|
1380
|
+
role="bot"
|
|
1381
|
+
name="Bot"
|
|
1382
|
+
content="Hi"
|
|
1383
|
+
actions={{
|
|
1384
|
+
positive: { onClick: jest.fn() }
|
|
1385
|
+
}}
|
|
1386
|
+
/>
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1390
|
+
|
|
1391
|
+
await user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1392
|
+
|
|
1393
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1394
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('should swap icons when useFilledIconsOnClick is true', async () => {
|
|
1398
|
+
const user = userEvent.setup();
|
|
1399
|
+
|
|
1400
|
+
render(
|
|
1401
|
+
<Message
|
|
1402
|
+
avatar="./img"
|
|
1403
|
+
role="bot"
|
|
1404
|
+
name="Bot"
|
|
1405
|
+
content="Hi"
|
|
1406
|
+
actions={{
|
|
1407
|
+
positive: { onClick: jest.fn() }
|
|
1408
|
+
}}
|
|
1409
|
+
useFilledIconsOnClick
|
|
1410
|
+
/>
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
await user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1414
|
+
|
|
1415
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
1416
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
1417
|
+
});
|
|
1354
1418
|
});
|
package/src/Message/Message.tsx
CHANGED
|
@@ -197,6 +197,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
|
|
|
197
197
|
hasNoImagesInUserMessages?: boolean;
|
|
198
198
|
/** Sets background colors to be appropriate on primary chatbot background */
|
|
199
199
|
isPrimary?: boolean;
|
|
200
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked. */
|
|
201
|
+
useFilledIconsOnClick?: boolean;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
@@ -249,6 +251,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
|
249
251
|
toolCall,
|
|
250
252
|
hasNoImagesInUserMessages = true,
|
|
251
253
|
isPrimary,
|
|
254
|
+
useFilledIconsOnClick,
|
|
252
255
|
...props
|
|
253
256
|
}: MessageProps) => {
|
|
254
257
|
const [messageText, setMessageText] = useState(content);
|
|
@@ -385,11 +388,16 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
|
385
388
|
key={index}
|
|
386
389
|
actions={actionGroup.actions || actionGroup}
|
|
387
390
|
persistActionSelection={persistActionSelection || actionGroup.persistActionSelection}
|
|
391
|
+
useFilledIconsOnClick={useFilledIconsOnClick}
|
|
388
392
|
/>
|
|
389
393
|
))}
|
|
390
394
|
</div>
|
|
391
395
|
) : (
|
|
392
|
-
<ResponseActions
|
|
396
|
+
<ResponseActions
|
|
397
|
+
actions={actions}
|
|
398
|
+
persistActionSelection={persistActionSelection}
|
|
399
|
+
useFilledIconsOnClick={useFilledIconsOnClick}
|
|
400
|
+
/>
|
|
393
401
|
)}
|
|
394
402
|
</>
|
|
395
403
|
)}
|
|
@@ -5,6 +5,21 @@ import userEvent from '@testing-library/user-event';
|
|
|
5
5
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
|
6
6
|
import Message from '../Message';
|
|
7
7
|
|
|
8
|
+
// Mock the icon components
|
|
9
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
10
|
+
OutlinedThumbsUpIcon: () => <div>OutlinedThumbsUpIcon</div>,
|
|
11
|
+
ThumbsUpIcon: () => <div>ThumbsUpIcon</div>,
|
|
12
|
+
OutlinedThumbsDownIcon: () => <div>OutlinedThumbsDownIcon</div>,
|
|
13
|
+
ThumbsDownIcon: () => <div>ThumbsDownIcon</div>,
|
|
14
|
+
OutlinedCopyIcon: () => <div>OutlinedCopyIcon</div>,
|
|
15
|
+
DownloadIcon: () => <div>DownloadIcon</div>,
|
|
16
|
+
InfoCircleIcon: () => <div>InfoCircleIcon</div>,
|
|
17
|
+
RedoIcon: () => <div>RedoIcon</div>,
|
|
18
|
+
ExternalLinkAltIcon: () => <div>ExternalLinkAltIcon</div>,
|
|
19
|
+
VolumeUpIcon: () => <div>VolumeUpIcon</div>,
|
|
20
|
+
PencilAltIcon: () => <div>PencilAltIcon</div>
|
|
21
|
+
}));
|
|
22
|
+
|
|
8
23
|
const ALL_ACTIONS = [
|
|
9
24
|
{ type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
|
|
10
25
|
{ type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
|
|
@@ -421,4 +436,189 @@ describe('ResponseActions', () => {
|
|
|
421
436
|
await userEvent.click(customBtn);
|
|
422
437
|
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
|
423
438
|
});
|
|
439
|
+
|
|
440
|
+
describe('icon swapping with useFilledIconsOnClick', () => {
|
|
441
|
+
it('should render outline icons by default', () => {
|
|
442
|
+
render(
|
|
443
|
+
<ResponseActions
|
|
444
|
+
actions={{
|
|
445
|
+
positive: { onClick: jest.fn() },
|
|
446
|
+
negative: { onClick: jest.fn() }
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
452
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
453
|
+
|
|
454
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
455
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('positive actions', () => {
|
|
459
|
+
it('should not swap positive icon when clicked and useFilledIconsOnClick is false', async () => {
|
|
460
|
+
const user = userEvent.setup();
|
|
461
|
+
|
|
462
|
+
render(
|
|
463
|
+
<ResponseActions
|
|
464
|
+
actions={{
|
|
465
|
+
positive: { onClick: jest.fn() }
|
|
466
|
+
}}
|
|
467
|
+
useFilledIconsOnClick={false}
|
|
468
|
+
/>
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
await user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
472
|
+
|
|
473
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
474
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should swap positive icon from outline to filled when clicked with useFilledIconsOnClick', async () => {
|
|
478
|
+
const user = userEvent.setup();
|
|
479
|
+
|
|
480
|
+
render(
|
|
481
|
+
<ResponseActions
|
|
482
|
+
actions={{
|
|
483
|
+
positive: { onClick: jest.fn() }
|
|
484
|
+
}}
|
|
485
|
+
useFilledIconsOnClick
|
|
486
|
+
/>
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
await user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
490
|
+
|
|
491
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
492
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should revert positive icon to outline icon when clicking outside', async () => {
|
|
496
|
+
const user = userEvent.setup();
|
|
497
|
+
|
|
498
|
+
render(
|
|
499
|
+
<div>
|
|
500
|
+
<ResponseActions
|
|
501
|
+
actions={{
|
|
502
|
+
positive: { onClick: jest.fn() }
|
|
503
|
+
}}
|
|
504
|
+
useFilledIconsOnClick
|
|
505
|
+
/>
|
|
506
|
+
<div data-testid="outside">Outside</div>
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
511
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
512
|
+
|
|
513
|
+
await user.click(screen.getByTestId('outside'));
|
|
514
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should not revert positive icon to outline icon when clicking outside if persistActionSelection is true', async () => {
|
|
518
|
+
const user = userEvent.setup();
|
|
519
|
+
|
|
520
|
+
render(
|
|
521
|
+
<div>
|
|
522
|
+
<ResponseActions
|
|
523
|
+
actions={{
|
|
524
|
+
positive: { onClick: jest.fn() }
|
|
525
|
+
}}
|
|
526
|
+
persistActionSelection
|
|
527
|
+
useFilledIconsOnClick
|
|
528
|
+
/>
|
|
529
|
+
<div data-testid="outside">Outside</div>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
await user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
534
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
535
|
+
|
|
536
|
+
await user.click(screen.getByTestId('outside'));
|
|
537
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('negative actions', () => {
|
|
541
|
+
it('should not swap negative icon when clicked and useFilledIconsOnClick is false', async () => {
|
|
542
|
+
const user = userEvent.setup();
|
|
543
|
+
|
|
544
|
+
render(
|
|
545
|
+
<ResponseActions
|
|
546
|
+
actions={{
|
|
547
|
+
negative: { onClick: jest.fn() }
|
|
548
|
+
}}
|
|
549
|
+
useFilledIconsOnClick={false}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
await user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
554
|
+
|
|
555
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
556
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should swap negative icon from outline to filled when clicked with useFilledIconsOnClick', async () => {
|
|
560
|
+
const user = userEvent.setup();
|
|
561
|
+
|
|
562
|
+
render(
|
|
563
|
+
<ResponseActions
|
|
564
|
+
actions={{
|
|
565
|
+
negative: { onClick: jest.fn() }
|
|
566
|
+
}}
|
|
567
|
+
useFilledIconsOnClick
|
|
568
|
+
/>
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
await user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
572
|
+
|
|
573
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
574
|
+
expect(screen.queryByText('OutlinedThumbsDownIcon')).not.toBeInTheDocument();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should revert negative icon to outline when clicking outside', async () => {
|
|
578
|
+
const user = userEvent.setup();
|
|
579
|
+
|
|
580
|
+
render(
|
|
581
|
+
<div>
|
|
582
|
+
<ResponseActions
|
|
583
|
+
actions={{
|
|
584
|
+
negative: { onClick: jest.fn() }
|
|
585
|
+
}}
|
|
586
|
+
useFilledIconsOnClick
|
|
587
|
+
/>
|
|
588
|
+
<div data-testid="outside">Outside</div>
|
|
589
|
+
</div>
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
await user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
593
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
594
|
+
|
|
595
|
+
await user.click(screen.getByTestId('outside'));
|
|
596
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should not revert negative icon to outline icon when clicking outside if persistActionSelection is true', async () => {
|
|
600
|
+
const user = userEvent.setup();
|
|
601
|
+
|
|
602
|
+
render(
|
|
603
|
+
<div>
|
|
604
|
+
<ResponseActions
|
|
605
|
+
actions={{
|
|
606
|
+
negative: { onClick: jest.fn() }
|
|
607
|
+
}}
|
|
608
|
+
persistActionSelection
|
|
609
|
+
useFilledIconsOnClick
|
|
610
|
+
/>
|
|
611
|
+
<div data-testid="outside">Outside</div>
|
|
612
|
+
</div>
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
await user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
616
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
617
|
+
|
|
618
|
+
await user.click(screen.getByTestId('outside'));
|
|
619
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
});
|
|
424
624
|
});
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
ExternalLinkAltIcon,
|
|
5
5
|
VolumeUpIcon,
|
|
6
6
|
OutlinedThumbsUpIcon,
|
|
7
|
+
ThumbsUpIcon,
|
|
7
8
|
OutlinedThumbsDownIcon,
|
|
9
|
+
ThumbsDownIcon,
|
|
8
10
|
OutlinedCopyIcon,
|
|
9
11
|
DownloadIcon,
|
|
10
12
|
PencilAltIcon
|
|
@@ -62,11 +64,15 @@ export interface ResponseActionProps {
|
|
|
62
64
|
/** When true, the selected action will persist even when clicking outside the component.
|
|
63
65
|
* When false (default), clicking outside or clicking another action will deselect the current selection. */
|
|
64
66
|
persistActionSelection?: boolean;
|
|
67
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked.
|
|
68
|
+
* Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
|
|
69
|
+
useFilledIconsOnClick?: boolean;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
|
|
68
73
|
actions,
|
|
69
|
-
persistActionSelection = false
|
|
74
|
+
persistActionSelection = false,
|
|
75
|
+
useFilledIconsOnClick = false
|
|
70
76
|
}) => {
|
|
71
77
|
const [activeButton, setActiveButton] = useState<string>();
|
|
72
78
|
const [clickStatePersisted, setClickStatePersisted] = useState<boolean>(false);
|
|
@@ -129,6 +135,7 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
|
|
|
129
135
|
id: string,
|
|
130
136
|
onClick?: (event: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
|
|
131
137
|
) => {
|
|
138
|
+
e.stopPropagation();
|
|
132
139
|
if (persistActionSelection) {
|
|
133
140
|
if (activeButton === id) {
|
|
134
141
|
// Toggle off if clicking the same button
|
|
@@ -145,6 +152,27 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
|
|
|
145
152
|
onClick && onClick(e);
|
|
146
153
|
};
|
|
147
154
|
|
|
155
|
+
const iconMap = {
|
|
156
|
+
positive: {
|
|
157
|
+
filled: <ThumbsUpIcon />,
|
|
158
|
+
outlined: <OutlinedThumbsUpIcon />
|
|
159
|
+
},
|
|
160
|
+
negative: {
|
|
161
|
+
filled: <ThumbsDownIcon />,
|
|
162
|
+
outlined: <OutlinedThumbsDownIcon />
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getIcon = (actionName: string) => {
|
|
167
|
+
const isClicked = activeButton === actionName;
|
|
168
|
+
|
|
169
|
+
if (isClicked && useFilledIconsOnClick) {
|
|
170
|
+
return iconMap[actionName].filled;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return iconMap[actionName].outlined;
|
|
174
|
+
};
|
|
175
|
+
|
|
148
176
|
return (
|
|
149
177
|
<div ref={responseActions} className="pf-chatbot__response-actions">
|
|
150
178
|
{positive && (
|
|
@@ -158,7 +186,7 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
|
|
|
158
186
|
tooltipContent={positive.tooltipContent ?? 'Good response'}
|
|
159
187
|
clickedTooltipContent={positive.clickedTooltipContent ?? 'Good response recorded'}
|
|
160
188
|
tooltipProps={positive.tooltipProps}
|
|
161
|
-
icon={
|
|
189
|
+
icon={getIcon('positive')}
|
|
162
190
|
isClicked={activeButton === 'positive'}
|
|
163
191
|
ref={positive.ref}
|
|
164
192
|
aria-expanded={positive['aria-expanded']}
|
|
@@ -176,7 +204,7 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
|
|
|
176
204
|
tooltipContent={negative.tooltipContent ?? 'Bad response'}
|
|
177
205
|
clickedTooltipContent={negative.clickedTooltipContent ?? 'Bad response recorded'}
|
|
178
206
|
tooltipProps={negative.tooltipProps}
|
|
179
|
-
icon={
|
|
207
|
+
icon={getIcon('negative')}
|
|
180
208
|
isClicked={activeButton === 'negative'}
|
|
181
209
|
ref={negative.ref}
|
|
182
210
|
aria-expanded={negative['aria-expanded']}
|