@patternfly/chatbot 6.6.0-prerelease.5 → 6.6.0-prerelease.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +1 -1
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +1 -1
- package/dist/cjs/ChatbotConversationHistoryNav/LoadingState.js +1 -1
- package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
- package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
- package/dist/cjs/Message/Message.d.ts +4 -0
- package/dist/cjs/Message/Message.js +2 -2
- package/dist/cjs/Message/Message.test.js +72 -0
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +6 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +12 -5
- package/dist/cjs/ResponseActions/ResponseActions.test.js +28 -0
- package/dist/css/main.css +27 -10
- package/dist/css/main.css.map +1 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +1 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +1 -1
- package/dist/esm/ChatbotConversationHistoryNav/LoadingState.js +1 -1
- package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
- package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
- package/dist/esm/Message/Message.d.ts +4 -0
- package/dist/esm/Message/Message.js +2 -2
- package/dist/esm/Message/Message.test.js +72 -0
- package/dist/esm/ResponseActions/ResponseActions.d.ts +6 -0
- package/dist/esm/ResponseActions/ResponseActions.js +12 -5
- package/dist/esm/ResponseActions/ResponseActions.test.js +28 -0
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/chatbot.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/design-guidelines.md +10 -12
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx +39 -18
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +9 -8
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderBasic.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +16 -16
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +3 -3
- package/src/Chatbot/Chatbot.scss +8 -1
- package/src/ChatbotContent/ChatbotContent.scss +5 -1
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +1 -1
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +1 -1
- package/src/ChatbotConversationHistoryNav/LoadingState.tsx +1 -5
- package/src/ChatbotFooter/ChatbotFooter.scss +8 -1
- package/src/ChatbotHeader/ChatbotHeader.scss +5 -2
- package/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx +1 -1
- package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +2 -2
- package/src/Message/Message.scss +12 -0
- package/src/Message/Message.test.tsx +136 -0
- package/src/Message/Message.tsx +12 -1
- package/src/MessageBar/MessageBar.scss +8 -5
- package/src/ResponseActions/ResponseActions.test.tsx +59 -0
- package/src/ResponseActions/ResponseActions.tsx +35 -10
|
@@ -33,7 +33,7 @@ import ToolCall from '../ToolCall';
|
|
|
33
33
|
import MarkdownContent from '../MarkdownContent';
|
|
34
34
|
import { css } from '@patternfly/react-styles';
|
|
35
35
|
export const MessageBase = (_a) => {
|
|
36
|
-
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
|
|
36
|
+
var { children, role, alignment = 'start', isMetadataVisible = true, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, showActionsOnInteraction = false, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, useFilledIconsOnClick } = _a, props = __rest(_a, ["children", "role", "alignment", "isMetadataVisible", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "persistActionSelection", "showActionsOnInteraction", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages", "isPrimary", "useFilledIconsOnClick"]);
|
|
37
37
|
const [messageText, setMessageText] = useState(content);
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
setMessageText(content);
|
|
@@ -61,7 +61,7 @@ export const MessageBase = (_a) => {
|
|
|
61
61
|
}
|
|
62
62
|
return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? _jsx(ErrorMessage, Object.assign({}, error)) : handleMarkdown()] }));
|
|
63
63
|
};
|
|
64
|
-
return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className:
|
|
64
|
+
return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: css(`pf-chatbot__message pf-chatbot__message--${role}`, alignment === 'end' && 'pf-m-end'), "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [avatar && (_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps))), _jsxs("div", { className: "pf-chatbot__message-contents", children: [isMetadataVisible && (_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] })), _jsx("div", { className: "pf-chatbot__message-response", children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), toolResponse && _jsx(ToolResponse, Object.assign({}, toolResponse)), deepThinking && _jsx(DeepThinking, Object.assign({}, deepThinking)), toolCall && _jsx(ToolCall, Object.assign({}, toolCall)), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (_jsx(_Fragment, { children: Array.isArray(actions) ? (_jsx("div", { className: css('pf-chatbot__response-actions-groups', showActionsOnInteraction && 'pf-m-visible-interaction'), children: actions.map((actionGroup, index) => (_jsx(ResponseActions, { actions: actionGroup.actions || actionGroup, persistActionSelection: persistActionSelection || actionGroup.persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick }, index))) })) : (_jsx(ResponseActions, { actions: actions, persistActionSelection: persistActionSelection, useFilledIconsOnClick: useFilledIconsOnClick, showActionsOnInteraction: showActionsOnInteraction })) })), userFeedbackForm && (_jsx(UserFeedback, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact }))), userFeedbackComplete && (_jsx(UserFeedbackComplete, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && (_jsx("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
|
|
65
65
|
var _a;
|
|
66
66
|
return (_jsx("div", { className: "pf-chatbot__message-attachment", children: _jsx(FileDetailsLabel, { fileName: attachment.name, fileId: attachment.id, onClose: attachment.onClose, onClick: attachment.onClick, isLoading: attachment.isLoading, closeButtonAriaLabel: attachment.closeButtonAriaLabel, languageTestId: attachment.languageTestId, spinnerTestId: attachment.spinnerTestId, variant: isPrimary ? 'outline' : undefined }) }, (_a = attachment.id) !== null && _a !== void 0 ? _a : attachment.name));
|
|
67
67
|
}) })), !isLoading && endContent && _jsx(_Fragment, { children: endContent })] })) })] })] })));
|
|
@@ -1036,4 +1036,76 @@ describe('Message', () => {
|
|
|
1036
1036
|
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
1037
1037
|
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
1038
1038
|
}));
|
|
1039
|
+
it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
|
|
1040
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: {
|
|
1041
|
+
positive: { onClick: jest.fn() }
|
|
1042
|
+
} }));
|
|
1043
|
+
const responseContainer = screen
|
|
1044
|
+
.getByRole('button', { name: 'Good response' })
|
|
1045
|
+
.closest('.pf-chatbot__response-actions');
|
|
1046
|
+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
|
|
1047
|
+
});
|
|
1048
|
+
it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
|
|
1049
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: {
|
|
1050
|
+
positive: { onClick: jest.fn() }
|
|
1051
|
+
} }));
|
|
1052
|
+
const responseContainer = screen
|
|
1053
|
+
.getByRole('button', { name: 'Good response' })
|
|
1054
|
+
.closest('.pf-chatbot__response-actions');
|
|
1055
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1056
|
+
});
|
|
1057
|
+
it('should not apply pf-m-visible-interaction class to response actions by default', () => {
|
|
1058
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1059
|
+
positive: { onClick: jest.fn() }
|
|
1060
|
+
} }));
|
|
1061
|
+
const responseContainer = screen
|
|
1062
|
+
.getByRole('button', { name: 'Good response' })
|
|
1063
|
+
.closest('.pf-chatbot__response-actions');
|
|
1064
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1065
|
+
});
|
|
1066
|
+
it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
|
|
1067
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: [
|
|
1068
|
+
{
|
|
1069
|
+
positive: { onClick: jest.fn() },
|
|
1070
|
+
negative: { onClick: jest.fn() }
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
copy: { onClick: jest.fn() }
|
|
1074
|
+
}
|
|
1075
|
+
] }));
|
|
1076
|
+
const responseContainer = screen
|
|
1077
|
+
.getByRole('button', { name: 'Good response' })
|
|
1078
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1079
|
+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
|
|
1080
|
+
});
|
|
1081
|
+
it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
|
|
1082
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: [
|
|
1083
|
+
{
|
|
1084
|
+
positive: { onClick: jest.fn() },
|
|
1085
|
+
negative: { onClick: jest.fn() }
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
copy: { onClick: jest.fn() }
|
|
1089
|
+
}
|
|
1090
|
+
] }));
|
|
1091
|
+
const responseContainer = screen
|
|
1092
|
+
.getByRole('button', { name: 'Good response' })
|
|
1093
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1094
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1095
|
+
});
|
|
1096
|
+
it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
|
|
1097
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: [
|
|
1098
|
+
{
|
|
1099
|
+
positive: { onClick: jest.fn() },
|
|
1100
|
+
negative: { onClick: jest.fn() }
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
copy: { onClick: jest.fn() }
|
|
1104
|
+
}
|
|
1105
|
+
] }));
|
|
1106
|
+
const responseContainer = screen
|
|
1107
|
+
.getByRole('button', { name: 'Good response' })
|
|
1108
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1109
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1110
|
+
});
|
|
1039
1111
|
});
|
|
@@ -34,6 +34,8 @@ type ExtendedActionProps = ActionProps & {
|
|
|
34
34
|
* Use this component when passing children to Message to customize its structure.
|
|
35
35
|
*/
|
|
36
36
|
export interface ResponseActionProps {
|
|
37
|
+
/** Additional classes for the response actions container. */
|
|
38
|
+
className?: string;
|
|
37
39
|
/** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */
|
|
38
40
|
actions: Record<string, ExtendedActionProps | undefined> & {
|
|
39
41
|
positive?: ActionProps;
|
|
@@ -50,6 +52,10 @@ export interface ResponseActionProps {
|
|
|
50
52
|
/** When true, automatically swaps to filled icon variants when predefined actions are clicked.
|
|
51
53
|
* Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
|
|
52
54
|
useFilledIconsOnClick?: boolean;
|
|
55
|
+
/** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
|
|
56
|
+
* that setting this to true will append tooltips inline instead of the document.body.
|
|
57
|
+
*/
|
|
58
|
+
showActionsOnInteraction?: boolean;
|
|
53
59
|
}
|
|
54
60
|
export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
|
|
55
61
|
export default ResponseActions;
|
|
@@ -14,8 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
import { useEffect, useRef, useState } from 'react';
|
|
15
15
|
import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, ThumbsUpIcon, OutlinedThumbsDownIcon, ThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
|
|
16
16
|
import ResponseActionButton from './ResponseActionButton';
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import { css } from '@patternfly/react-styles';
|
|
18
|
+
export const ResponseActions = (_a) => {
|
|
19
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
|
|
20
|
+
var { className, actions, persistActionSelection = false, useFilledIconsOnClick = false, showActionsOnInteraction = false } = _a, props = __rest(_a, ["className", "actions", "persistActionSelection", "useFilledIconsOnClick", "showActionsOnInteraction"]);
|
|
19
21
|
const [activeButton, setActiveButton] = useState();
|
|
20
22
|
const [clickStatePersisted, setClickStatePersisted] = useState(false);
|
|
21
23
|
const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
|
|
@@ -98,9 +100,14 @@ export const ResponseActions = ({ actions, persistActionSelection = false, useFi
|
|
|
98
100
|
}
|
|
99
101
|
return iconMap[actionName].outlined;
|
|
100
102
|
};
|
|
101
|
-
|
|
103
|
+
// We want to append the tooltip inline so that hovering the tooltip keeps the actions container visible
|
|
104
|
+
// when showActionsOnInteraction is true. Otherwise hovering the tooltip causes the actions container
|
|
105
|
+
// to disappear but the tooltip will remain visible.
|
|
106
|
+
const getTooltipContainer = () => responseActions.current || document.body;
|
|
107
|
+
const getTooltipProps = (tooltipProps) => (Object.assign(Object.assign({}, (showActionsOnInteraction && { appendTo: getTooltipContainer })), tooltipProps));
|
|
108
|
+
return (_jsxs("div", Object.assign({ ref: responseActions, className: css('pf-chatbot__response-actions', showActionsOnInteraction && 'pf-m-visible-interaction', className) }, props, { children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response', clickedAriaLabel: (_c = positive.ariaLabel) !== null && _c !== void 0 ? _c : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_d = positive.tooltipContent) !== null && _d !== void 0 ? _d : 'Good response', clickedTooltipContent: (_e = positive.clickedTooltipContent) !== null && _e !== void 0 ? _e : 'Good response recorded', tooltipProps: getTooltipProps(positive.tooltipProps), icon: getIcon('positive'), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response', clickedAriaLabel: (_g = negative.ariaLabel) !== null && _g !== void 0 ? _g : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_h = negative.tooltipContent) !== null && _h !== void 0 ? _h : 'Bad response', clickedTooltipContent: (_j = negative.clickedTooltipContent) !== null && _j !== void 0 ? _j : 'Bad response recorded', tooltipProps: getTooltipProps(negative.tooltipProps), icon: getIcon('negative'), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copy', clickedAriaLabel: (_l = copy.ariaLabel) !== null && _l !== void 0 ? _l : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_m = copy.tooltipContent) !== null && _m !== void 0 ? _m : 'Copy', clickedTooltipContent: (_o = copy.clickedTooltipContent) !== null && _o !== void 0 ? _o : 'Copied', tooltipProps: getTooltipProps(copy.tooltipProps), icon: _jsx(OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && (_jsx(ResponseActionButton, Object.assign({}, edit, { ariaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Edit', clickedAriaLabel: (_q = edit.ariaLabel) !== null && _q !== void 0 ? _q : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_r = edit.tooltipContent) !== null && _r !== void 0 ? _r : 'Edit ', clickedTooltipContent: (_s = edit.clickedTooltipContent) !== null && _s !== void 0 ? _s : 'Editing', tooltipProps: getTooltipProps(edit.tooltipProps), icon: _jsx(PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && (_jsx(ResponseActionButton, Object.assign({}, share, { ariaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Share', clickedAriaLabel: (_u = share.ariaLabel) !== null && _u !== void 0 ? _u : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_v = share.tooltipContent) !== null && _v !== void 0 ? _v : 'Share', clickedTooltipContent: (_w = share.clickedTooltipContent) !== null && _w !== void 0 ? _w : 'Shared', tooltipProps: getTooltipProps(share.tooltipProps), icon: _jsx(ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && (_jsx(ResponseActionButton, Object.assign({}, download, { ariaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Download', clickedAriaLabel: (_y = download.ariaLabel) !== null && _y !== void 0 ? _y : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_z = download.tooltipContent) !== null && _z !== void 0 ? _z : 'Download', clickedTooltipContent: (_0 = download.clickedTooltipContent) !== null && _0 !== void 0 ? _0 : 'Downloaded', tooltipProps: getTooltipProps(download.tooltipProps), icon: _jsx(DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && (_jsx(ResponseActionButton, Object.assign({}, listen, { ariaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listen', clickedAriaLabel: (_2 = listen.ariaLabel) !== null && _2 !== void 0 ? _2 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_3 = listen.tooltipContent) !== null && _3 !== void 0 ? _3 : 'Listen', clickedTooltipContent: (_4 = listen.clickedTooltipContent) !== null && _4 !== void 0 ? _4 : 'Listening', tooltipProps: getTooltipProps(listen.tooltipProps), icon: _jsx(VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
|
|
102
109
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
103
|
-
return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: (_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps, clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
|
|
104
|
-
})] }));
|
|
110
|
+
return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: getTooltipProps((_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps), clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
|
|
111
|
+
})] })));
|
|
105
112
|
};
|
|
106
113
|
export default ResponseActions;
|
|
@@ -324,6 +324,34 @@ describe('ResponseActions', () => {
|
|
|
324
324
|
yield userEvent.click(customBtn);
|
|
325
325
|
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
|
326
326
|
}));
|
|
327
|
+
it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
|
|
328
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
329
|
+
positive: { onClick: jest.fn() },
|
|
330
|
+
negative: { onClick: jest.fn() }
|
|
331
|
+
}, showActionsOnInteraction: true }));
|
|
332
|
+
expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
|
|
333
|
+
});
|
|
334
|
+
it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
|
|
335
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
336
|
+
positive: { onClick: jest.fn() },
|
|
337
|
+
negative: { onClick: jest.fn() }
|
|
338
|
+
}, showActionsOnInteraction: false }));
|
|
339
|
+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
|
|
340
|
+
});
|
|
341
|
+
it('should not apply pf-m-visible-interaction class by default', () => {
|
|
342
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
343
|
+
positive: { onClick: jest.fn() },
|
|
344
|
+
negative: { onClick: jest.fn() }
|
|
345
|
+
} }));
|
|
346
|
+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
|
|
347
|
+
});
|
|
348
|
+
it('should render with custom className', () => {
|
|
349
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
350
|
+
positive: { onClick: jest.fn() },
|
|
351
|
+
negative: { onClick: jest.fn() }
|
|
352
|
+
}, className: "custom-class" }));
|
|
353
|
+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
|
|
354
|
+
});
|
|
327
355
|
describe('icon swapping with useFilledIconsOnClick', () => {
|
|
328
356
|
it('should render outline icons by default', () => {
|
|
329
357
|
render(_jsx(ResponseActions, { actions: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/chatbot",
|
|
3
|
-
"version": "6.6.0-prerelease.
|
|
3
|
+
"version": "6.6.0-prerelease.7",
|
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -40,7 +40,7 @@ Explore our documentation, which covers both the components you'll need to build
|
|
|
40
40
|
- [Toggle](/extensions/chatbot/ui#toggle)
|
|
41
41
|
- [Header](/extensions/chatbot/ui#header)
|
|
42
42
|
- [Footer](/extensions/chatbot/ui#footer)
|
|
43
|
-
- [
|
|
43
|
+
- [Chat history drawer](/extensions/chatbot/ui#chat-history)
|
|
44
44
|
- [Modals](/extensions/chatbot/ui#modals)
|
|
45
45
|
- Messages: Components that customize features related to the conversation
|
|
46
46
|
- [Bot and user messages](/extensions/chatbot/messages)
|
|
@@ -15,8 +15,8 @@ import "./images.css"
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
1. **Container:** The window that contains the entire ChatBot experience and all of its components.
|
|
18
|
-
1. **Header:** A persistent region at the top of the ChatBot window that contains
|
|
19
|
-
1. **Chat history
|
|
18
|
+
1. **Header:** A persistent region at the top of the ChatBot window that contains chat history, branding, and actions.
|
|
19
|
+
1. **Chat history drawer:** A menu used to access previous chats.
|
|
20
20
|
1. **Options menu:** A menu that contains settings that are relevant to your product. This typically includes display options (more details in the [ChatBot variations section](#variations)) and other general settings (more details in the [ChatBot settings and preferences section](#chatbot-settings-and-preferences)).
|
|
21
21
|
1. **Messages:** Elements of the conversation between a ChatBot and user. More details can be found in the [message guidelines](#messages).
|
|
22
22
|
1. **Attachments:** Details about files that a user has uploaded to the ChatBot.
|
|
@@ -288,7 +288,7 @@ When a ChatBot is launched via an AI-supported action, the action should be sent
|
|
|
288
288
|
|
|
289
289
|
Each time a user begins a new chat, display a [welcome message](#welcome-message), with prompts that provide initial suggestions and indicate the actions that the ChatBot can take.
|
|
290
290
|
|
|
291
|
-
The default approach for users to create a new chat is by clicking the "New chat" button (which contains a "pen to square" icon) placed at the top of the [chat history
|
|
291
|
+
The default approach for users to create a new chat is by clicking the "New chat" button (which contains a "pen to square" icon) placed at the top of the [chat history drawer](#using-the-chat-history-drawer).
|
|
292
292
|
|
|
293
293
|
<div class="ws-docs-content-img">
|
|
294
294
|

|
|
@@ -310,14 +310,12 @@ This can be done using the [quick response](/extensions/chatbot/messages#message
|
|
|
310
310
|

|
|
311
311
|
</div>
|
|
312
312
|
|
|
313
|
-
### Using the chat history
|
|
313
|
+
### Using the chat history drawer
|
|
314
314
|
|
|
315
|
-
The
|
|
316
|
-
|
|
317
|
-
By clicking into the history menu, users can search through previous conversations and perform additional actions, such as sharing a conversation with others.
|
|
315
|
+
The chat history drawer can be opened via the hamburger menu in the ChatBot header. In this drawer, users can search through previous conversations and perform additional actions, such as sharing a conversation with others.
|
|
318
316
|
|
|
319
317
|
<div class="ws-docs-content-img">
|
|
320
|
-

|
|
321
319
|
</div>
|
|
322
320
|
|
|
323
321
|
When the chat history is still loading, display skeleton items:
|
|
@@ -383,16 +381,16 @@ If a message attachment fails, an error message should share the reason for fail
|
|
|
383
381
|
|
|
384
382
|
You can enable users to download chat transcripts, for their personal records or to share with others. When users choose to download a transcript, you can choose how you want to configure the behavior in your ChatBot.
|
|
385
383
|
|
|
386
|
-
For guidance, refer to our
|
|
384
|
+
For guidance, refer to our [chat transcripts demo](/extensions/chatbot/overview/demo#chat-transcripts), which opens a Markdown file for a conversation within a new tab.
|
|
387
385
|
|
|
388
386
|
Choose the download action location that best works for your ChatBot:
|
|
389
387
|
|
|
390
388
|
#### Download via chat history drawer
|
|
391
389
|
|
|
392
|
-
If your ChatBot uses
|
|
390
|
+
If your ChatBot uses chat history, you can provide a download option in the [actions menu linked to a previous conversation](/extensions/chatbot/ui#drawer-with-conversation-actions).
|
|
393
391
|
|
|
394
392
|
<div class="ws-docs-content-img">
|
|
395
|
-

|
|
396
394
|
</div>
|
|
397
395
|
|
|
398
396
|
#### Download message response action
|
|
@@ -405,7 +403,7 @@ To allow users to download individual bot messages, the message actions can incl
|
|
|
405
403
|
|
|
406
404
|
#### Download control in header
|
|
407
405
|
|
|
408
|
-
If you don't use
|
|
406
|
+
If you don't use chat history, you can place an option to download the transcript for the active chat within the header options menu.
|
|
409
407
|
|
|
410
408
|
<div class="ws-docs-content-img">
|
|
411
409
|

|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx
CHANGED
|
@@ -4,22 +4,43 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
|
|
|
4
4
|
import patternflyAvatar from './patternfly_avatar.jpg';
|
|
5
5
|
|
|
6
6
|
export const ResponseActionExample: FunctionComponent = () => (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
<>
|
|
8
|
+
<Message
|
|
9
|
+
name="Bot"
|
|
10
|
+
role="bot"
|
|
11
|
+
avatar={patternflyAvatar}
|
|
12
|
+
content="I updated your account with those settings. You're ready to set up your first dashboard!"
|
|
13
|
+
actions={{
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
positive: { onClick: () => console.log('Good response') },
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
negative: { onClick: () => console.log('Bad response') },
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
copy: { onClick: () => console.log('Copy') },
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
download: { onClick: () => console.log('Download') },
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
listen: { onClick: () => console.log('Listen') }
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
<Message
|
|
27
|
+
name="Bot"
|
|
28
|
+
role="bot"
|
|
29
|
+
showActionsOnInteraction
|
|
30
|
+
avatar={patternflyAvatar}
|
|
31
|
+
content="This message has response actions visually hidden until you hover over the message via mouse, or an action would receive focus via keyboard."
|
|
32
|
+
actions={{
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
positive: { onClick: () => console.log('Good response') },
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
negative: { onClick: () => console.log('Bad response') },
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
copy: { onClick: () => console.log('Copy') },
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
download: { onClick: () => console.log('Download') },
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
listen: { onClick: () => console.log('Listen') }
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</>
|
|
25
46
|
);
|
|
@@ -95,14 +95,16 @@ For example, you can use the default divider to display a "timestamp" for more s
|
|
|
95
95
|
|
|
96
96
|
### Message actions
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
To let users interact with a bot's responses, you can add support for message actions. While you can customize message actions to your needs, default options include the following:
|
|
99
99
|
|
|
100
|
-
-
|
|
101
|
-
- Copy
|
|
102
|
-
-
|
|
103
|
-
-
|
|
100
|
+
- Positive and negative feedback: Allows users to rate a message as "good" or "bad."
|
|
101
|
+
- Copy: Allows users to copy the message content to their clipboard.
|
|
102
|
+
- Download: Allows users to download the message content.
|
|
103
|
+
- Listen: Reads the message content out loud using text-to-speech.
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
You can display message actions by default, or use the `showActionsOnInteraction` prop to reveal actions on hover or keyboard focus.
|
|
106
|
+
|
|
107
|
+
**Note**: The underlying logic for these actions is not built-in and must be implemented within the consuming application.
|
|
106
108
|
|
|
107
109
|
```js file="./MessageWithResponseActions.tsx"
|
|
108
110
|
|
|
@@ -140,11 +142,10 @@ When `persistActionSelection` is `true`:
|
|
|
140
142
|
|
|
141
143
|
### Message actions that fill
|
|
142
144
|
|
|
143
|
-
To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
|
|
145
|
+
To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
|
|
144
146
|
|
|
145
147
|
This is especially useful for actions that are intended to persist (such as the "positive" and "negative" responses), so that a user's selection is more clear and emphasized.
|
|
146
148
|
|
|
147
|
-
|
|
148
149
|
```js file="./MessageWithIconSwapping.tsx"
|
|
149
150
|
|
|
150
151
|
```
|
|
@@ -189,9 +189,9 @@ The ChatBot header is persistent, and contains the title for the ChatBot window,
|
|
|
189
189
|
|
|
190
190
|
The `<ChatbotHeader>` has 2 sections:
|
|
191
191
|
|
|
192
|
-
- `<ChatbotHeaderMain>` contains the title and an optional menu toggle or new chat button:
|
|
192
|
+
- `<ChatbotHeaderMain>` contains the title and an optional hamburger menu toggle or new chat button:
|
|
193
193
|
- `<ChatbotHeaderTitle>` handles the layout and display of a title or image at different responsive sizes.
|
|
194
|
-
- `<ChatbotHeaderMenu>` (optional) is placed on the left side of the header and used to toggle a chat history
|
|
194
|
+
- `<ChatbotHeaderMenu>` (optional) is placed on the left side of the header and used to toggle a chat history drawer.
|
|
195
195
|
- `<ChatbotHeaderNewChatButton>` (optional) is placed on the left side of the header and used to initiate a new chat.
|
|
196
196
|
- `<ChatbotHeaderActions>` contains any additional controls:
|
|
197
197
|
- The `<ChatbotHeaderSelectorDropdown>` component is a standard PatternFly dropdown that matches the ChatBot styles.
|
|
@@ -227,8 +227,8 @@ There are a variety of options and customizations you can make to the header, to
|
|
|
227
227
|
|
|
228
228
|
In this example, select the respective checkbox to toggle these features:
|
|
229
229
|
|
|
230
|
-
- **
|
|
231
|
-
- **New chat button:** Used to start a new chat session. The header button can be used in addition to or in place of a new chat button within the [
|
|
230
|
+
- **Chat history drawer:** Users can select the hamburger menu toggle to open the [chat history drawer](#chat-history).
|
|
231
|
+
- **New chat button:** Used to start a new chat session. The header button can be used in addition to or in place of a new chat button within the [chat history drawer](/extensions/chatbot/ui/#drawer-with-search-and-new-chat-button).
|
|
232
232
|
- **Left-aligned logo**
|
|
233
233
|
- **Centered logo**
|
|
234
234
|
- **Selector dropdown:** Users can choose from preselected options in a dropdown menu. For example, they can toggle between AI models.
|
|
@@ -339,11 +339,11 @@ This example shows a simplified method of handling the "thinking" animation: aft
|
|
|
339
339
|
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
-
##
|
|
342
|
+
## Chat history
|
|
343
343
|
|
|
344
|
-
###
|
|
344
|
+
### Chat history drawer
|
|
345
345
|
|
|
346
|
-
|
|
346
|
+
A user's chat history is contained in an interactive drawer, where they can interact with previous conversations or start a new conversation.
|
|
347
347
|
|
|
348
348
|
The `<ChatbotConversationHistoryNav>` component is a wrapper placed within `<Chatbot>`, which contains all other ChatBot components in `drawerContent`. There is a focus trap so users can only tab within the drawer while it is open.
|
|
349
349
|
|
|
@@ -367,14 +367,14 @@ The code structure will look like this:
|
|
|
367
367
|
</Chatbot>
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
The
|
|
370
|
+
The chat history drawer looks different depending on the `displayMode` of the parent `<Chatbot>` (as shown in the [main ChatBot demo](/extensions/chatbot/overview/demo#basic-chatbot).):
|
|
371
371
|
|
|
372
|
-
- `Default` and `docked` display modes display the
|
|
373
|
-
- `Fullscreen` and `embedded` display modes display the
|
|
372
|
+
- `Default` and `docked` display modes display the chat history on top of the rest of the ChatBot content, with a PatternFly backdrop between the drawer panel and drawer content.
|
|
373
|
+
- `Fullscreen` and `embedded` display modes display the chat history in line with the drawer content.
|
|
374
374
|
|
|
375
375
|
### Drawer with search and "new chat" button
|
|
376
376
|
|
|
377
|
-
In the
|
|
377
|
+
In the chat history drawer, users can search previous ChatBot conversations via an input field. To customize the placeholder text, use `searchInputPlaceholder`. Provide an aria label via `searchInputAriaLabel`.
|
|
378
378
|
|
|
379
379
|
They can also start new conversations via a "New chat" button. To customize the button label, use `newChatButtonText`.
|
|
380
380
|
|
|
@@ -386,7 +386,7 @@ Both the search input field and "New chat" buttons are optional. The `reverseBut
|
|
|
386
386
|
|
|
387
387
|
### Drawer with search actions
|
|
388
388
|
|
|
389
|
-
You can customize the search experience within the
|
|
389
|
+
You can customize the search experience within the chat history drawer via the `searchActionStart` and `searchActionEnd` props, which provide additional search controls before and after the input field. These props are useful for adding filtering, sorting, or other search-related functionality.
|
|
390
390
|
|
|
391
391
|
You can also add a visual divider between the drawer head and the title by setting `hasDrawerHeadDivider` to `true`.
|
|
392
392
|
|
|
@@ -404,15 +404,15 @@ Actions can be added to conversations with `menuItems`. Optionally, you can also
|
|
|
404
404
|
|
|
405
405
|
### Pinning conversations
|
|
406
406
|
|
|
407
|
-
To help users track important conversations, add a "pin" option to the conversation action menus. This action moves a conversation to a dedicated "pinned" section at the top of the history drawer for quick access. Pinned items should contain an "unpin" option, so that users can remove pinned conversations as needed.
|
|
407
|
+
To help users track important conversations, add a "pin" option to the conversation action menus. This action moves a conversation to a dedicated "pinned" section at the top of the chat history drawer for quick access. Pinned items should contain an "unpin" option, so that users can remove pinned conversations as needed.
|
|
408
408
|
|
|
409
409
|
```js file="./ChatbotHeaderDrawerWithPin.tsx"
|
|
410
410
|
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
-
### Renaming conversations in history drawer
|
|
413
|
+
### Renaming conversations in chat history drawer
|
|
414
414
|
|
|
415
|
-
You can allow users to rename a conversation in the history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
|
|
415
|
+
You can allow users to rename a conversation in the chat history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
|
|
416
416
|
|
|
417
417
|
- When the modal opens, focus is placed at the end of the text input.
|
|
418
418
|
- When the modal closes, focus goes back to the action toggle that was previously opened.
|
|
@@ -433,7 +433,7 @@ If you're showing a conversation that is already active, you can set the `active
|
|
|
433
433
|
|
|
434
434
|
### Resizable drawer
|
|
435
435
|
|
|
436
|
-
By default, the
|
|
436
|
+
By default, the chat history drawer has a fixed width (384px) and a focus trap. To provide users with more flexibility as they navigate their chat history, or to better support embedded ChatBots on tablet-sized devices or smaller browser windows, you can instead make the drawer resizable. By default, even resizable drawers will still open to their full width on mobile devices.
|
|
437
437
|
|
|
438
438
|
In this example, the drawer can be resized up to the max size of the parent and resized down to 200px wide. To customize this behavior further (including width, style, and focus behavior) use PatternFly [`<Drawer>` props](/components/drawer#props), [`<DrawerPanelContent>` props](/components/drawer/#drawerpanelcontent), or any other drawer subcomponents.
|
|
439
439
|
|
|
@@ -95,7 +95,7 @@ This demo displays a basic ChatBot, which includes:
|
|
|
95
95
|
- Sending a message to the ChatBot.
|
|
96
96
|
- Receiving a response from a backend AI tool with a loading message state.
|
|
97
97
|
|
|
98
|
-
6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#
|
|
98
|
+
6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#chat-history) toggled open and closed by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
|
|
99
99
|
|
|
100
100
|
7. A "Skip to chatbot" button that allows you to skip to the chatbot content via the [PatternFly skip to content component](/extensions/chatbot/ui#skip-to-content). To display this button you must tab into the main window.
|
|
101
101
|
|
|
@@ -126,7 +126,7 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed
|
|
|
126
126
|
- [Speech to text.](/extensions/chatbot/ui#message-bar-with-speech-recognition-and-file-attachment)
|
|
127
127
|
- Sending a message to the ChatBot.
|
|
128
128
|
- Receiving a response from a backend AI tool with a loading message state.
|
|
129
|
-
6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#
|
|
129
|
+
6. A [`<ChatbotConversationHistoryNav>`](/extensions/chatbot/ui#chat-history) that can be toggled by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.
|
|
130
130
|
|
|
131
131
|
```js file="./EmbeddedChatbot.tsx" isFullscreen
|
|
132
132
|
|
|
@@ -196,7 +196,7 @@ This demo illustrates how you could add downloadable transcripts to your ChatBot
|
|
|
196
196
|
|
|
197
197
|
A message transcript includes details from a single chat message. To download a sample message transcript in this demo, click the "Download" action under a bot message.
|
|
198
198
|
|
|
199
|
-
A conversation transcript includes details from the entirety of a ChatBot conversation. To download a sample conversation transcript in this demo, open the chat history
|
|
199
|
+
A conversation transcript includes details from the entirety of a ChatBot conversation. To download a sample conversation transcript in this demo, open the chat history drawer and click "Download" in the options menu for the conversation.
|
|
200
200
|
|
|
201
201
|
In this example, file download is implemented with [file-saver](https://www.npmjs.com/package/file-saver).
|
|
202
202
|
|
package/src/Chatbot/Chatbot.scss
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
flex-direction: column;
|
|
10
10
|
width: 30rem;
|
|
11
11
|
height: 70vh;
|
|
12
|
-
background-color: var(
|
|
12
|
+
background-color: var(
|
|
13
|
+
--pf-t--global--background--color--floating--secondary--default,
|
|
14
|
+
--pf-t--global--background--color--secondary--default
|
|
15
|
+
);
|
|
13
16
|
border-radius: var(--pf-t--global--border--radius--medium);
|
|
14
17
|
box-shadow: var(--pf-t--global--box-shadow--lg);
|
|
15
18
|
font-size: var(--pf-t--global--font--size--md);
|
|
@@ -72,6 +75,8 @@
|
|
|
72
75
|
// Chatbot Display Mode - Fullscreen
|
|
73
76
|
// ============================================================================
|
|
74
77
|
.pf-chatbot--fullscreen {
|
|
78
|
+
background-color: var(--pf-t--global--background--color--secondary--default);
|
|
79
|
+
|
|
75
80
|
// for high contrast support
|
|
76
81
|
border: unset;
|
|
77
82
|
inset-block-end: 0;
|
|
@@ -87,6 +92,8 @@
|
|
|
87
92
|
// Chatbot Display Mode - Embedded
|
|
88
93
|
// ============================================================================
|
|
89
94
|
.pf-chatbot--embedded {
|
|
95
|
+
background-color: var(--pf-t--global--background--color--secondary--default);
|
|
96
|
+
|
|
90
97
|
// for high contrast support
|
|
91
98
|
border: unset;
|
|
92
99
|
position: static;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
.pf-chatbot__content {
|
|
5
5
|
position: relative;
|
|
6
|
-
background-color: var(
|
|
6
|
+
background-color: var(
|
|
7
|
+
--pf-t--global--background--color--floating--secondary--default,
|
|
8
|
+
--pf-t--global--background--color--secondary--default
|
|
9
|
+
);
|
|
7
10
|
overflow-y: auto;
|
|
8
11
|
overflow: hidden; // needed in Red Hat Developer Hub workspace
|
|
9
12
|
flex: 1; // needed in Composer AI
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
.pf-chatbot--fullscreen,
|
|
27
30
|
.pf-chatbot--embedded {
|
|
28
31
|
.pf-chatbot__content {
|
|
32
|
+
background-color: var(--pf-t--global--background--color--secondary--default);
|
|
29
33
|
display: flex;
|
|
30
34
|
justify-content: center;
|
|
31
35
|
}
|