@patternfly/chatbot 2.1.0-prerelease.17 → 2.1.0-prerelease.18
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/ChatbotToggle/ChatbotToggle.d.ts +7 -1
- package/dist/cjs/ChatbotToggle/ChatbotToggle.js +4 -4
- package/dist/cjs/ChatbotToggle/ChatbotToggle.test.d.ts +1 -0
- package/dist/cjs/ChatbotToggle/ChatbotToggle.test.js +60 -0
- package/dist/cjs/Message/Message.d.ts +5 -1
- package/dist/cjs/Message/Message.js +8 -2
- package/dist/cjs/Message/Message.test.js +100 -4
- package/dist/cjs/ResponseActions/ResponseActions.test.js +38 -0
- package/dist/css/main.css +19 -12
- package/dist/css/main.css.map +1 -1
- package/dist/esm/ChatbotToggle/ChatbotToggle.d.ts +7 -1
- package/dist/esm/ChatbotToggle/ChatbotToggle.js +4 -4
- package/dist/esm/ChatbotToggle/ChatbotToggle.test.d.ts +1 -0
- package/dist/esm/ChatbotToggle/ChatbotToggle.test.js +55 -0
- package/dist/esm/Message/Message.d.ts +5 -1
- package/dist/esm/Message/Message.js +8 -2
- package/dist/esm/Message/Message.test.js +100 -4
- package/dist/esm/ResponseActions/ResponseActions.test.js +38 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +8 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithAttachment.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +8 -2
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/PF-social-color-square.svg +19 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/PF-social-dark-square.svg +19 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +8 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/user_avatar.svg +18 -0
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotToggleBasic.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotWelcomeInteraction.tsx +3 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/CustomClosedIcon.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/SkipToContent.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/SquareChatbotToggle.tsx +14 -0
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +9 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +6 -4
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +3 -2
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +4 -3
- package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedChatbot.tsx +5 -3
- package/src/ChatbotToggle/ChatbotToggle.scss +11 -5
- package/src/ChatbotToggle/ChatbotToggle.test.tsx +47 -0
- package/src/ChatbotToggle/ChatbotToggle.tsx +15 -6
- package/src/Message/Message.scss +15 -9
- package/src/Message/Message.test.tsx +141 -4
- package/src/Message/Message.tsx +28 -3
- package/src/MessageBox/MessageBox.scss +2 -2
- package/src/ResponseActions/ResponseActions.test.tsx +44 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/user_avatar.jpg +0 -0
@@ -6,13 +6,11 @@
|
|
6
6
|
inset-block-end: var(--pf-t--global--spacer--md);
|
7
7
|
inset-inline-end: var(--pf-t--global--spacer--md);
|
8
8
|
background-color: var(--pf-t--global--background--color--inverse--default);
|
9
|
-
border-radius: var(--pf-t--global--border--radius--pill);
|
10
9
|
--pf-v6-c-button__icon--Color: var(--pf-t--chatbot-toggle--color);
|
11
10
|
padding: var(--pf-t--global--spacer--md);
|
12
|
-
width: 3rem;
|
13
|
-
height: 3rem;
|
14
11
|
|
15
|
-
&:hover,
|
12
|
+
&:hover,
|
13
|
+
&:focus {
|
16
14
|
background-color: var(--pf-t--chatbot-toggle--background--hover);
|
17
15
|
}
|
18
16
|
|
@@ -21,6 +19,14 @@
|
|
21
19
|
}
|
22
20
|
|
23
21
|
// Icon
|
24
|
-
svg {
|
22
|
+
svg {
|
23
|
+
width: var(--pf-t--global--spacer--lg);
|
24
|
+
height: var(--pf-t--global--spacer--lg);
|
25
|
+
}
|
25
26
|
}
|
26
27
|
|
28
|
+
.pf-chatbot__button--round {
|
29
|
+
border-radius: var(--pf-t--global--border--radius--pill);
|
30
|
+
width: 3rem;
|
31
|
+
height: 3rem;
|
32
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
5
|
+
import ChatbotToggle from './ChatbotToggle';
|
6
|
+
|
7
|
+
describe('ChatbotToggle', () => {
|
8
|
+
it('should render tooltipLabel correctly', async () => {
|
9
|
+
render(<ChatbotToggle tooltipLabel="Tooltip" />);
|
10
|
+
await userEvent.click(screen.getByRole('button', { name: /Tooltip toggle/i }));
|
11
|
+
expect(screen.getByRole('tooltip', { name: /Tooltip/i })).toBeTruthy();
|
12
|
+
});
|
13
|
+
it('should render toggleButtonLabel correctly', async () => {
|
14
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" toggleButtonLabel="Button" />);
|
15
|
+
expect(screen.getByRole('button', { name: /Button/i })).toBeTruthy();
|
16
|
+
});
|
17
|
+
it('should call onToggleChatbot when clicked', async () => {
|
18
|
+
const spy = jest.fn();
|
19
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" onToggleChatbot={spy} />);
|
20
|
+
await userEvent.click(screen.getByRole('button'));
|
21
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
22
|
+
});
|
23
|
+
it('should handle isChatbotVisible correctly when true', () => {
|
24
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" isChatbotVisible openIconTestId="Open" />);
|
25
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
|
26
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button--active');
|
27
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
|
28
|
+
expect(screen.getByTestId('Open')).toBeTruthy();
|
29
|
+
});
|
30
|
+
it('should handle isChatbotVisible correctly when false', () => {
|
31
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" isChatbotVisible={false} openIconTestId="Open" />);
|
32
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
|
33
|
+
expect(screen.getByRole('button')).not.toHaveClass('pf-chatbot__button--active');
|
34
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
|
35
|
+
expect(screen.queryByTestId('Open')).toBeFalsy();
|
36
|
+
});
|
37
|
+
it('should handle isRound correctly', () => {
|
38
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" isRound />);
|
39
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
|
40
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button--round');
|
41
|
+
});
|
42
|
+
it('should handle className correctly', () => {
|
43
|
+
render(<ChatbotToggle tooltipLabel="Chatbot" className="test" />);
|
44
|
+
expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
|
45
|
+
expect(screen.getByRole('button')).toHaveClass('test');
|
46
|
+
});
|
47
|
+
});
|
@@ -7,7 +7,7 @@ import AngleDownIcon from '@patternfly/react-icons/dist/esm/icons/angle-down-ico
|
|
7
7
|
|
8
8
|
export interface ChatbotToggleProps extends ButtonProps {
|
9
9
|
/** Contents of the tooltip applied to the toggle button */
|
10
|
-
|
10
|
+
tooltipLabel: React.ReactNode;
|
11
11
|
/** Props spread to the PF Tooltip component */
|
12
12
|
tooltipProps?: Omit<TooltipProps, 'content'>;
|
13
13
|
/** Flag indicating visibility of the chatbot appended to the toggle */
|
@@ -20,6 +20,12 @@ export interface ChatbotToggleProps extends ButtonProps {
|
|
20
20
|
closedToggleIcon?: () => JSX.Element;
|
21
21
|
/** Ref applied to toggle */
|
22
22
|
innerRef?: React.Ref<HTMLButtonElement>;
|
23
|
+
/** Whether toggle is a circle */
|
24
|
+
isRound?: boolean;
|
25
|
+
/** Class name applied to toggle */
|
26
|
+
className?: string;
|
27
|
+
/** Test id applied to default open icon */
|
28
|
+
openIconTestId?: string;
|
23
29
|
}
|
24
30
|
|
25
31
|
const ChatIcon = () => (
|
@@ -42,25 +48,28 @@ const ChatIcon = () => (
|
|
42
48
|
);
|
43
49
|
|
44
50
|
const ChatbotToggleBase: React.FunctionComponent<ChatbotToggleProps> = ({
|
45
|
-
|
51
|
+
tooltipLabel,
|
46
52
|
isChatbotVisible,
|
47
53
|
onToggleChatbot,
|
48
54
|
tooltipProps,
|
49
55
|
toggleButtonLabel,
|
50
56
|
closedToggleIcon: ClosedToggleIcon,
|
51
57
|
innerRef,
|
58
|
+
isRound = true,
|
59
|
+
className,
|
60
|
+
openIconTestId,
|
52
61
|
...props
|
53
62
|
}: ChatbotToggleProps) => {
|
54
63
|
// Configure icon
|
55
64
|
const closedIcon = ClosedToggleIcon ? <ClosedToggleIcon /> : <ChatIcon />;
|
56
|
-
const icon = isChatbotVisible ? <AngleDownIcon /> : closedIcon;
|
65
|
+
const icon = isChatbotVisible ? <AngleDownIcon data-testid={openIconTestId} /> : closedIcon;
|
57
66
|
|
58
67
|
return (
|
59
|
-
<Tooltip content={
|
68
|
+
<Tooltip content={tooltipLabel} {...tooltipProps}>
|
60
69
|
<Button
|
61
|
-
className={`pf-chatbot__button ${isChatbotVisible ? 'pf-chatbot__button--active' : ''}`}
|
70
|
+
className={`pf-chatbot__button ${isChatbotVisible ? 'pf-chatbot__button--active' : ''} ${isRound ? 'pf-chatbot__button--round' : ''} ${className ? className : ''}`}
|
62
71
|
variant="plain"
|
63
|
-
aria-label={toggleButtonLabel || `${
|
72
|
+
aria-label={toggleButtonLabel || `${tooltipLabel} toggle`}
|
64
73
|
onClick={onToggleChatbot}
|
65
74
|
aria-expanded={isChatbotVisible}
|
66
75
|
icon={<Icon isInline>{icon}</Icon>}
|
package/src/Message/Message.scss
CHANGED
@@ -15,23 +15,29 @@
|
|
15
15
|
gap: var(--pf-t--global--spacer--lg);
|
16
16
|
padding-bottom: var(--pf-t--global--spacer--2xl);
|
17
17
|
|
18
|
-
// Name
|
19
|
-
// --------------------------------------------------------------------------
|
20
|
-
.pf-v6-c-truncate {
|
21
|
-
--pf-v6-c-truncate--MinWidth: 0ch;
|
22
|
-
--pf-v6-c-truncate__start--MinWidth: 0ch;
|
23
|
-
}
|
24
18
|
// Avatar
|
25
19
|
// --------------------------------------------------------------------------
|
26
|
-
.pf-v6-c-avatar {
|
27
|
-
--pf-v6-c-avatar--
|
28
|
-
--pf-v6-c-avatar--Height: 3rem;
|
20
|
+
&-avatar.pf-v6-c-avatar {
|
21
|
+
--pf-v6-c-avatar--BorderRadius: 0;
|
29
22
|
position: sticky;
|
30
23
|
top: var(--pf-t--global--spacer--md);
|
31
24
|
object-fit: cover;
|
32
25
|
pointer-events: none; // prevent dragging - interferes with FileDropZone
|
33
26
|
}
|
34
27
|
|
28
|
+
&-avatar.pf-chatbot__message-avatar--round.pf-v6-c-avatar {
|
29
|
+
--pf-v6-c-avatar--Width: 3rem;
|
30
|
+
--pf-v6-c-avatar--Height: 3rem;
|
31
|
+
--pf-v6-c-avatar--BorderRadius: var(--pf-t--global--border--radius--pill);
|
32
|
+
}
|
33
|
+
|
34
|
+
// Name
|
35
|
+
// --------------------------------------------------------------------------
|
36
|
+
.pf-v6-c-truncate {
|
37
|
+
--pf-v6-c-truncate--MinWidth: 0ch;
|
38
|
+
--pf-v6-c-truncate__start--MinWidth: 0ch;
|
39
|
+
}
|
40
|
+
|
35
41
|
// Contents
|
36
42
|
// --------------------------------------------------------------------------
|
37
43
|
&-contents {
|
@@ -68,7 +68,14 @@ describe('Message', () => {
|
|
68
68
|
expect(screen.getByText('User')).toBeTruthy();
|
69
69
|
expect(screen.getByText('Hi')).toBeTruthy();
|
70
70
|
const date = new Date();
|
71
|
-
|
71
|
+
const formattedDate = date.toLocaleDateString();
|
72
|
+
expect(
|
73
|
+
screen.getByText((content, element) => {
|
74
|
+
const hasText = content.includes(formattedDate);
|
75
|
+
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
|
76
|
+
return hasText && isVisible;
|
77
|
+
})
|
78
|
+
).toBeInTheDocument();
|
72
79
|
expect(screen.queryByText('Loading message')).toBeFalsy();
|
73
80
|
expect(screen.getByRole('img')).toHaveAttribute('src', './img');
|
74
81
|
});
|
@@ -78,7 +85,14 @@ describe('Message', () => {
|
|
78
85
|
expect(screen.getByText('AI')).toBeTruthy();
|
79
86
|
expect(screen.getByText('Hi')).toBeTruthy();
|
80
87
|
const date = new Date();
|
81
|
-
|
88
|
+
const formattedDate = date.toLocaleDateString();
|
89
|
+
expect(
|
90
|
+
screen.getByText((content, element) => {
|
91
|
+
const hasText = content.includes(formattedDate);
|
92
|
+
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
|
93
|
+
return hasText && isVisible;
|
94
|
+
})
|
95
|
+
).toBeInTheDocument();
|
82
96
|
});
|
83
97
|
it('should render avatar correctly', () => {
|
84
98
|
render(<Message avatar="./testImg" role="bot" name="Bot" content="Hi" />);
|
@@ -98,7 +112,14 @@ describe('Message', () => {
|
|
98
112
|
expect(screen.getByText('Hi')).toBeTruthy();
|
99
113
|
expect(screen.getByText('2 hours ago')).toBeTruthy();
|
100
114
|
const date = new Date();
|
101
|
-
|
115
|
+
const formattedDate = date.toLocaleDateString();
|
116
|
+
expect(
|
117
|
+
screen.queryByText((content, element) => {
|
118
|
+
const hasText = content.includes(formattedDate);
|
119
|
+
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
|
120
|
+
return hasText && isVisible;
|
121
|
+
})
|
122
|
+
).not.toBeInTheDocument();
|
102
123
|
});
|
103
124
|
it('should render attachments', () => {
|
104
125
|
render(<Message avatar="./img" role="user" content="Hi" attachments={[{ name: 'testAttachment' }]} />);
|
@@ -132,7 +153,14 @@ describe('Message', () => {
|
|
132
153
|
expect(screen.getByText('AI')).toBeTruthy();
|
133
154
|
expect(screen.queryByText('Hi')).toBeFalsy();
|
134
155
|
const date = new Date();
|
135
|
-
|
156
|
+
const formattedDate = date.toLocaleDateString();
|
157
|
+
expect(
|
158
|
+
screen.getByText((content, element) => {
|
159
|
+
const hasText = content.includes(formattedDate);
|
160
|
+
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
|
161
|
+
return hasText && isVisible;
|
162
|
+
})
|
163
|
+
).toBeInTheDocument();
|
136
164
|
expect(screen.getByText('Loading message')).toBeTruthy();
|
137
165
|
});
|
138
166
|
it('should be able to show sources', async () => {
|
@@ -177,6 +205,83 @@ describe('Message', () => {
|
|
177
205
|
expect(screen.getByText('Loading message')).toBeTruthy();
|
178
206
|
expect(screen.queryByText('Getting started with Red Hat OpenShift')).toBeFalsy();
|
179
207
|
});
|
208
|
+
it('should be able to show quick response', async () => {
|
209
|
+
const spy = jest.fn();
|
210
|
+
render(
|
211
|
+
<Message
|
212
|
+
avatar="./img"
|
213
|
+
role="bot"
|
214
|
+
name="Bot"
|
215
|
+
content="Hi"
|
216
|
+
quickResponses={[
|
217
|
+
{
|
218
|
+
id: '1',
|
219
|
+
content: 'Yes',
|
220
|
+
onClick: spy,
|
221
|
+
className: 'test'
|
222
|
+
}
|
223
|
+
]}
|
224
|
+
/>
|
225
|
+
);
|
226
|
+
const quickResponse = screen.getByRole('button', { name: /Yes/i });
|
227
|
+
expect(quickResponse).toBeTruthy();
|
228
|
+
await userEvent.click(quickResponse);
|
229
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
230
|
+
});
|
231
|
+
it('should be able to show more than 1 quick response', async () => {
|
232
|
+
const spy = jest.fn();
|
233
|
+
render(
|
234
|
+
<Message
|
235
|
+
avatar="./img"
|
236
|
+
role="bot"
|
237
|
+
name="Bot"
|
238
|
+
content="Hi"
|
239
|
+
quickResponses={[
|
240
|
+
{
|
241
|
+
id: '1',
|
242
|
+
content: 'Yes',
|
243
|
+
onClick: spy
|
244
|
+
},
|
245
|
+
{
|
246
|
+
id: '2',
|
247
|
+
content: 'No',
|
248
|
+
onClick: spy
|
249
|
+
}
|
250
|
+
]}
|
251
|
+
/>
|
252
|
+
);
|
253
|
+
expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
|
254
|
+
expect(screen.getByRole('button', { name: /No/i })).toBeTruthy();
|
255
|
+
});
|
256
|
+
it('should be able to spread quickResponseContainerProps', async () => {
|
257
|
+
const spy = jest.fn();
|
258
|
+
render(
|
259
|
+
<Message
|
260
|
+
avatar="./img"
|
261
|
+
role="bot"
|
262
|
+
name="Bot"
|
263
|
+
content="Hi"
|
264
|
+
quickResponses={[
|
265
|
+
{
|
266
|
+
id: '1',
|
267
|
+
content: 'Yes',
|
268
|
+
onClick: spy
|
269
|
+
},
|
270
|
+
{
|
271
|
+
id: '2',
|
272
|
+
content: 'No',
|
273
|
+
onClick: spy
|
274
|
+
}
|
275
|
+
]}
|
276
|
+
// this is a LabelGroup prop that changes the default number shown
|
277
|
+
// to be different than what we use in ChatBot
|
278
|
+
quickResponseContainerProps={{ numLabels: 1 }}
|
279
|
+
/>
|
280
|
+
);
|
281
|
+
expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
|
282
|
+
expect(screen.queryByRole('button', { name: /No/i })).toBeFalsy();
|
283
|
+
expect(screen.getByRole('button', { name: /1 more/i }));
|
284
|
+
});
|
180
285
|
it('should be able to show actions', async () => {
|
181
286
|
render(
|
182
287
|
<Message
|
@@ -278,4 +383,36 @@ describe('Message', () => {
|
|
278
383
|
);
|
279
384
|
expect(screen.getByRole('button', { name: 'test' })).toBeTruthy();
|
280
385
|
});
|
386
|
+
it('should handle hasRoundAvatar correctly when it is true', () => {
|
387
|
+
render(<Message avatar="./img" role="user" name="User" content="Hi" hasRoundAvatar />);
|
388
|
+
expect(screen.getByRole('img')).toBeTruthy();
|
389
|
+
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
|
390
|
+
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar--round');
|
391
|
+
});
|
392
|
+
it('should handle hasRoundAvatar correctly when it is false', () => {
|
393
|
+
render(<Message avatar="./img" role="user" name="User" content="Hi" hasRoundAvatar={false} />);
|
394
|
+
expect(screen.getByRole('img')).toBeTruthy();
|
395
|
+
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
|
396
|
+
expect(screen.getByRole('img')).not.toHaveClass('pf-chatbot__message-avatar--round');
|
397
|
+
});
|
398
|
+
it('should handle avatarProps correctly by spreading it onto the Message Avatar', () => {
|
399
|
+
render(<Message avatar="./img" role="user" name="User" content="Hi" avatarProps={{ className: 'test' }} />);
|
400
|
+
expect(screen.getByRole('img')).toBeTruthy();
|
401
|
+
expect(screen.getByRole('img')).toHaveClass('test');
|
402
|
+
});
|
403
|
+
it('should handle avatarProps and hasRoundAvatar correctly', () => {
|
404
|
+
render(
|
405
|
+
<Message
|
406
|
+
avatar="./img"
|
407
|
+
role="user"
|
408
|
+
name="User"
|
409
|
+
content="Hi"
|
410
|
+
avatarProps={{ className: 'test' }}
|
411
|
+
hasRoundAvatar={false}
|
412
|
+
/>
|
413
|
+
);
|
414
|
+
expect(screen.getByRole('img')).toBeTruthy();
|
415
|
+
expect(screen.getByRole('img')).toHaveClass('test');
|
416
|
+
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
|
417
|
+
});
|
281
418
|
});
|
package/src/Message/Message.tsx
CHANGED
@@ -6,7 +6,16 @@ import React from 'react';
|
|
6
6
|
|
7
7
|
import Markdown from 'react-markdown';
|
8
8
|
import remarkGfm from 'remark-gfm';
|
9
|
-
import {
|
9
|
+
import {
|
10
|
+
Avatar,
|
11
|
+
AvatarProps,
|
12
|
+
Label,
|
13
|
+
LabelGroup,
|
14
|
+
LabelGroupProps,
|
15
|
+
LabelProps,
|
16
|
+
Timestamp,
|
17
|
+
Truncate
|
18
|
+
} from '@patternfly/react-core';
|
10
19
|
import MessageLoading from './MessageLoading';
|
11
20
|
import CodeBlockMessage from './CodeBlockMessage/CodeBlockMessage';
|
12
21
|
import TextMessage from './TextMessage/TextMessage';
|
@@ -76,6 +85,10 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
|
|
76
85
|
quickResponses?: QuickResponse[];
|
77
86
|
/** Props for quick responses container */
|
78
87
|
quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
|
88
|
+
/** Whether avatar is round */
|
89
|
+
hasRoundAvatar?: boolean;
|
90
|
+
/** Any additional props applied to the avatar, for additional customization */
|
91
|
+
avatarProps?: Omit<AvatarProps, 'alt'>;
|
79
92
|
}
|
80
93
|
|
81
94
|
export const Message: React.FunctionComponent<MessageProps> = ({
|
@@ -93,12 +106,19 @@ export const Message: React.FunctionComponent<MessageProps> = ({
|
|
93
106
|
quickResponses,
|
94
107
|
quickResponseContainerProps = { numLabels: 5 },
|
95
108
|
attachments,
|
109
|
+
hasRoundAvatar = true,
|
110
|
+
avatarProps,
|
96
111
|
...props
|
97
112
|
}: MessageProps) => {
|
113
|
+
let avatarClassName;
|
114
|
+
if (avatarProps && 'className' in avatarProps) {
|
115
|
+
const { className, ...rest } = avatarProps;
|
116
|
+
avatarClassName = className;
|
117
|
+
avatarProps = { ...rest };
|
118
|
+
}
|
98
119
|
// Keep timestamps consistent between Timestamp component and aria-label
|
99
120
|
const date = new Date();
|
100
121
|
const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
101
|
-
|
102
122
|
return (
|
103
123
|
<section
|
104
124
|
aria-label={`Message from ${role} - ${dateString}`}
|
@@ -106,7 +126,12 @@ export const Message: React.FunctionComponent<MessageProps> = ({
|
|
106
126
|
{...props}
|
107
127
|
>
|
108
128
|
{/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */}
|
109
|
-
<Avatar
|
129
|
+
<Avatar
|
130
|
+
className={`pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`}
|
131
|
+
src={avatar}
|
132
|
+
alt=""
|
133
|
+
{...avatarProps}
|
134
|
+
/>
|
110
135
|
<div className="pf-chatbot__message-contents">
|
111
136
|
<div className="pf-chatbot__message-meta">
|
112
137
|
{name && (
|
@@ -9,8 +9,8 @@
|
|
9
9
|
padding: var(--pf-t--global--spacer--lg);
|
10
10
|
}
|
11
11
|
|
12
|
-
.pf-chatbot__messagebox--bottom {
|
13
|
-
|
12
|
+
.pf-chatbot__messagebox--bottom > :first-child {
|
13
|
+
margin-top: auto !important;
|
14
14
|
}
|
15
15
|
|
16
16
|
// hide from view but not assistive technologies
|
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
4
4
|
import ResponseActions from './ResponseActions';
|
5
5
|
import userEvent from '@testing-library/user-event';
|
6
|
+
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
6
7
|
|
7
8
|
const ALL_ACTIONS = [
|
8
9
|
{ type: 'positive', label: 'Good response' },
|
@@ -12,6 +13,29 @@ const ALL_ACTIONS = [
|
|
12
13
|
{ type: 'listen', label: 'Listen' }
|
13
14
|
];
|
14
15
|
|
16
|
+
const CUSTOM_ACTIONS = [
|
17
|
+
{
|
18
|
+
regenerate: {
|
19
|
+
ariaLabel: 'Regenerate',
|
20
|
+
onClick: jest.fn(),
|
21
|
+
tooltipContent: 'Regenerate',
|
22
|
+
icon: <RedoIcon />
|
23
|
+
},
|
24
|
+
download: {
|
25
|
+
ariaLabel: 'Download',
|
26
|
+
onClick: jest.fn(),
|
27
|
+
tooltipContent: 'Download',
|
28
|
+
icon: <DownloadIcon />
|
29
|
+
},
|
30
|
+
info: {
|
31
|
+
ariaLabel: 'Info',
|
32
|
+
onClick: jest.fn(),
|
33
|
+
tooltipContent: 'Info',
|
34
|
+
icon: <InfoCircleIcon />
|
35
|
+
}
|
36
|
+
}
|
37
|
+
];
|
38
|
+
|
15
39
|
describe('ResponseActions', () => {
|
16
40
|
it('should render buttons correctly', () => {
|
17
41
|
ALL_ACTIONS.forEach(({ type, label }) => {
|
@@ -56,4 +80,24 @@ describe('ResponseActions', () => {
|
|
56
80
|
expect(screen.getByRole('button', { name: label })).toHaveClass('test');
|
57
81
|
});
|
58
82
|
});
|
83
|
+
|
84
|
+
it('should be able to add custom actions', () => {
|
85
|
+
CUSTOM_ACTIONS.forEach((action) => {
|
86
|
+
const key = Object.keys(action)[0];
|
87
|
+
render(
|
88
|
+
<ResponseActions
|
89
|
+
actions={{
|
90
|
+
[key]: {
|
91
|
+
tooltipContent: action[key].tooltipContent,
|
92
|
+
onClick: action[key].onClick,
|
93
|
+
// doing this just because it's easier to test without a regex for the button name
|
94
|
+
ariaLabel: action[key].ariaLabel.toLowerCase(),
|
95
|
+
icon: action[key].icon
|
96
|
+
}
|
97
|
+
}}
|
98
|
+
/>
|
99
|
+
);
|
100
|
+
expect(screen.getByRole('button', { name: key })).toBeTruthy();
|
101
|
+
});
|
102
|
+
});
|
59
103
|
});
|