@patternfly/chatbot 2.2.0-prerelease.31 → 2.2.0-prerelease.33
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/Chatbot/Chatbot.d.ts +2 -1
- package/dist/cjs/Chatbot/Chatbot.js +1 -0
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
- package/dist/cjs/ChatbotHeader/ChatbotHeaderTitle.d.ts +3 -1
- package/dist/cjs/ChatbotHeader/ChatbotHeaderTitle.js +4 -2
- package/dist/cjs/ChatbotHeader/ChatbotHeaderTitle.test.js +15 -7
- package/dist/cjs/Message/UserFeedback/UserFeedback.d.ts +1 -1
- package/dist/cjs/Message/UserFeedback/UserFeedback.js +2 -3
- package/dist/cjs/Message/UserFeedback/UserFeedback.test.js +0 -13
- package/dist/css/main.css +33 -3
- package/dist/css/main.css.map +1 -1
- package/dist/esm/Chatbot/Chatbot.d.ts +2 -1
- package/dist/esm/Chatbot/Chatbot.js +1 -0
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
- package/dist/esm/ChatbotHeader/ChatbotHeaderTitle.d.ts +3 -1
- package/dist/esm/ChatbotHeader/ChatbotHeaderTitle.js +4 -2
- package/dist/esm/ChatbotHeader/ChatbotHeaderTitle.test.js +15 -7
- package/dist/esm/Message/UserFeedback/UserFeedback.d.ts +1 -1
- package/dist/esm/Message/UserFeedback/UserFeedback.js +2 -3
- package/dist/esm/Message/UserFeedback/UserFeedback.test.js +0 -13
- package/package.json +4 -2
- package/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +67 -62
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +11 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotInDrawer.tsx +453 -0
- package/src/Chatbot/Chatbot.scss +19 -0
- package/src/Chatbot/Chatbot.tsx +2 -1
- package/src/ChatbotContent/ChatbotContent.scss +1 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +2 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +1 -1
- package/src/ChatbotFooter/ChatbotFooter.scss +9 -0
- package/src/ChatbotHeader/ChatbotHeader.scss +2 -1
- package/src/ChatbotHeader/ChatbotHeaderTitle.test.tsx +23 -7
- package/src/ChatbotHeader/ChatbotHeaderTitle.tsx +7 -2
- package/src/Message/UserFeedback/UserFeedback.test.tsx +0 -21
- package/src/Message/UserFeedback/UserFeedback.tsx +1 -5
- package/src/MessageBar/MessageBar.scss +3 -3
- package/src/MessageBox/MessageBox.scss +1 -0
- package/src/SourcesCard/SourcesCard.scss +6 -0
@@ -0,0 +1,453 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import {
|
4
|
+
Brand,
|
5
|
+
DropdownList,
|
6
|
+
DropdownItem,
|
7
|
+
Page,
|
8
|
+
Masthead,
|
9
|
+
MastheadMain,
|
10
|
+
MastheadBrand,
|
11
|
+
MastheadLogo,
|
12
|
+
PageSidebarBody,
|
13
|
+
PageSidebar,
|
14
|
+
MastheadToggle,
|
15
|
+
PageToggleButton,
|
16
|
+
SkipToContent,
|
17
|
+
Drawer,
|
18
|
+
DrawerContent,
|
19
|
+
DrawerContentBody,
|
20
|
+
DrawerPanelContent
|
21
|
+
} from '@patternfly/react-core';
|
22
|
+
|
23
|
+
import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
|
24
|
+
import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent';
|
25
|
+
import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt';
|
26
|
+
import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
|
27
|
+
import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar';
|
28
|
+
import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox';
|
29
|
+
import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message';
|
30
|
+
import ChatbotConversationHistoryNav, {
|
31
|
+
Conversation
|
32
|
+
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
|
33
|
+
import ChatbotHeader, {
|
34
|
+
ChatbotHeaderMenu,
|
35
|
+
ChatbotHeaderMain,
|
36
|
+
ChatbotHeaderTitle,
|
37
|
+
ChatbotHeaderActions,
|
38
|
+
ChatbotHeaderSelectorDropdown
|
39
|
+
} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
|
40
|
+
import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
|
41
|
+
import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
|
42
|
+
import { BarsIcon } from '@patternfly/react-icons';
|
43
|
+
import userAvatar from '../Messages/user_avatar.svg';
|
44
|
+
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
|
45
|
+
|
46
|
+
const footnoteProps = {
|
47
|
+
label: 'ChatBot uses AI. Check for mistakes.',
|
48
|
+
popover: {
|
49
|
+
title: 'Verify accuracy',
|
50
|
+
description: `While ChatBot strives for accuracy, there's always a possibility of errors. It's a good practice to verify critical information from reliable sources, especially if it's crucial for decision-making or actions.`,
|
51
|
+
bannerImage: {
|
52
|
+
src: 'https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif',
|
53
|
+
alt: 'Example image for footnote popover'
|
54
|
+
},
|
55
|
+
cta: {
|
56
|
+
label: 'Got it',
|
57
|
+
onClick: () => {
|
58
|
+
alert('Do something!');
|
59
|
+
}
|
60
|
+
},
|
61
|
+
link: {
|
62
|
+
label: 'Learn more',
|
63
|
+
url: 'https://www.redhat.com/'
|
64
|
+
}
|
65
|
+
}
|
66
|
+
};
|
67
|
+
|
68
|
+
const markdown = `A paragraph with *emphasis* and **strong importance**.
|
69
|
+
|
70
|
+
> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
|
71
|
+
|
72
|
+
Here is an inline code - \`() => void\`
|
73
|
+
|
74
|
+
Here is some YAML code:
|
75
|
+
|
76
|
+
~~~yaml
|
77
|
+
apiVersion: helm.openshift.io/v1beta1/
|
78
|
+
kind: HelmChartRepository
|
79
|
+
metadata:
|
80
|
+
name: azure-sample-repo0oooo00ooo
|
81
|
+
spec:
|
82
|
+
connectionConfig:
|
83
|
+
url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
|
84
|
+
~~~
|
85
|
+
|
86
|
+
Here is some JavaScript code:
|
87
|
+
|
88
|
+
~~~js
|
89
|
+
import React from 'react';
|
90
|
+
|
91
|
+
const MessageLoading = () => (
|
92
|
+
<div className="pf-chatbot__message-loading">
|
93
|
+
<span className="pf-chatbot__message-loading-dots">
|
94
|
+
<span className="pf-v6-screen-reader">Loading message</span>
|
95
|
+
</span>
|
96
|
+
</div>
|
97
|
+
);
|
98
|
+
|
99
|
+
export default MessageLoading;
|
100
|
+
|
101
|
+
~~~
|
102
|
+
`;
|
103
|
+
|
104
|
+
// It's important to set a date and timestamp prop since the Message components re-render.
|
105
|
+
// The timestamps re-render with them.
|
106
|
+
const date = new Date();
|
107
|
+
|
108
|
+
const initialMessages: MessageProps[] = [
|
109
|
+
{
|
110
|
+
id: '1',
|
111
|
+
role: 'user',
|
112
|
+
content: 'Hello, can you give me an example of what you can do?',
|
113
|
+
name: 'User',
|
114
|
+
avatar: userAvatar,
|
115
|
+
timestamp: date.toLocaleString(),
|
116
|
+
avatarProps: { isBordered: true }
|
117
|
+
},
|
118
|
+
{
|
119
|
+
id: '2',
|
120
|
+
role: 'bot',
|
121
|
+
content: markdown,
|
122
|
+
name: 'Bot',
|
123
|
+
avatar: patternflyAvatar,
|
124
|
+
timestamp: date.toLocaleString(),
|
125
|
+
actions: {
|
126
|
+
// eslint-disable-next-line no-console
|
127
|
+
positive: { onClick: () => console.log('Good response') },
|
128
|
+
// eslint-disable-next-line no-console
|
129
|
+
negative: { onClick: () => console.log('Bad response') },
|
130
|
+
// eslint-disable-next-line no-console
|
131
|
+
copy: { onClick: () => console.log('Copy') },
|
132
|
+
// eslint-disable-next-line no-console
|
133
|
+
share: { onClick: () => console.log('Share') },
|
134
|
+
// eslint-disable-next-line no-console
|
135
|
+
listen: { onClick: () => console.log('Listen') }
|
136
|
+
}
|
137
|
+
}
|
138
|
+
];
|
139
|
+
|
140
|
+
const welcomePrompts = [
|
141
|
+
{
|
142
|
+
title: 'Topic 1',
|
143
|
+
message: 'Helpful prompt for Topic 1'
|
144
|
+
},
|
145
|
+
{
|
146
|
+
title: 'Topic 2',
|
147
|
+
message: 'Helpful prompt for Topic 2'
|
148
|
+
}
|
149
|
+
];
|
150
|
+
|
151
|
+
const initialConversations = {
|
152
|
+
Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }],
|
153
|
+
'This month': [
|
154
|
+
{
|
155
|
+
id: '2',
|
156
|
+
text: 'Enterprise Linux installation and setup'
|
157
|
+
},
|
158
|
+
{ id: '3', text: 'Troubleshoot system crash' }
|
159
|
+
],
|
160
|
+
March: [
|
161
|
+
{ id: '4', text: 'Ansible security and updates' },
|
162
|
+
{ id: '5', text: 'Red Hat certification' },
|
163
|
+
{ id: '6', text: 'Lightspeed user documentation' }
|
164
|
+
],
|
165
|
+
February: [
|
166
|
+
{ id: '7', text: 'Crashing pod assistance' },
|
167
|
+
{ id: '8', text: 'OpenShift AI pipelines' },
|
168
|
+
{ id: '9', text: 'Updating subscription plan' },
|
169
|
+
{ id: '10', text: 'Red Hat licensing options' }
|
170
|
+
],
|
171
|
+
January: [
|
172
|
+
{ id: '11', text: 'RHEL system performance' },
|
173
|
+
{ id: '12', text: 'Manage user accounts' }
|
174
|
+
]
|
175
|
+
};
|
176
|
+
|
177
|
+
export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
|
178
|
+
const [messages, setMessages] = React.useState<MessageProps[]>(initialMessages);
|
179
|
+
const [selectedModel, setSelectedModel] = React.useState('Granite 7B');
|
180
|
+
const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
|
181
|
+
const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
|
182
|
+
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
|
183
|
+
initialConversations
|
184
|
+
);
|
185
|
+
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
|
186
|
+
const [announcement, setAnnouncement] = React.useState<string>();
|
187
|
+
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
|
188
|
+
const historyRef = React.useRef<HTMLButtonElement>(null);
|
189
|
+
const drawerRef = React.useRef<HTMLDivElement>();
|
190
|
+
|
191
|
+
const displayMode = ChatbotDisplayMode.drawer;
|
192
|
+
const isExpanded = true;
|
193
|
+
|
194
|
+
const onExpand = () => {
|
195
|
+
drawerRef.current && drawerRef.current.focus();
|
196
|
+
};
|
197
|
+
|
198
|
+
// Auto-scrolls to the latest message
|
199
|
+
React.useEffect(() => {
|
200
|
+
// don't scroll the first load - in this demo, we know we start with two messages
|
201
|
+
if (messages.length > 2) {
|
202
|
+
scrollToBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
203
|
+
}
|
204
|
+
}, [messages]);
|
205
|
+
|
206
|
+
const onSelectModel = (
|
207
|
+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
208
|
+
value: string | number | undefined
|
209
|
+
) => {
|
210
|
+
setSelectedModel(value as string);
|
211
|
+
};
|
212
|
+
|
213
|
+
// you will likely want to come up with your own unique id function; this is for demo purposes only
|
214
|
+
const generateId = () => {
|
215
|
+
const id = Date.now() + Math.random();
|
216
|
+
return id.toString();
|
217
|
+
};
|
218
|
+
|
219
|
+
const handleSend = (message: string) => {
|
220
|
+
setIsSendButtonDisabled(true);
|
221
|
+
const newMessages: MessageProps[] = [];
|
222
|
+
// We can't use structuredClone since messages contains functions, but we can't mutate
|
223
|
+
// items that are going into state or the UI won't update correctly
|
224
|
+
messages.forEach((message) => newMessages.push(message));
|
225
|
+
// It's important to set a timestamp prop since the Message components re-render.
|
226
|
+
// The timestamps re-render with them.
|
227
|
+
const date = new Date();
|
228
|
+
newMessages.push({
|
229
|
+
id: generateId(),
|
230
|
+
role: 'user',
|
231
|
+
content: message,
|
232
|
+
name: 'User',
|
233
|
+
avatar: userAvatar,
|
234
|
+
timestamp: date.toLocaleString(),
|
235
|
+
avatarProps: { isBordered: true }
|
236
|
+
});
|
237
|
+
newMessages.push({
|
238
|
+
id: generateId(),
|
239
|
+
role: 'bot',
|
240
|
+
content: 'API response goes here',
|
241
|
+
name: 'Bot',
|
242
|
+
avatar: patternflyAvatar,
|
243
|
+
isLoading: true,
|
244
|
+
timestamp: date.toLocaleString()
|
245
|
+
});
|
246
|
+
setMessages(newMessages);
|
247
|
+
// make announcement to assistive devices that new messages have been added
|
248
|
+
setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);
|
249
|
+
|
250
|
+
// this is for demo purposes only; in a real situation, there would be an API response we would wait for
|
251
|
+
setTimeout(() => {
|
252
|
+
const loadedMessages: MessageProps[] = [];
|
253
|
+
// we can't use structuredClone since messages contains functions, but we can't mutate
|
254
|
+
// items that are going into state or the UI won't update correctly
|
255
|
+
newMessages.forEach((message) => loadedMessages.push(message));
|
256
|
+
loadedMessages.pop();
|
257
|
+
loadedMessages.push({
|
258
|
+
id: generateId(),
|
259
|
+
role: 'bot',
|
260
|
+
content: 'API response goes here',
|
261
|
+
name: 'Bot',
|
262
|
+
avatar: patternflyAvatar,
|
263
|
+
isLoading: false,
|
264
|
+
actions: {
|
265
|
+
// eslint-disable-next-line no-console
|
266
|
+
positive: { onClick: () => console.log('Good response') },
|
267
|
+
// eslint-disable-next-line no-console
|
268
|
+
negative: { onClick: () => console.log('Bad response') },
|
269
|
+
// eslint-disable-next-line no-console
|
270
|
+
copy: { onClick: () => console.log('Copy') },
|
271
|
+
// eslint-disable-next-line no-console
|
272
|
+
share: { onClick: () => console.log('Share') },
|
273
|
+
// eslint-disable-next-line no-console
|
274
|
+
listen: { onClick: () => console.log('Listen') }
|
275
|
+
},
|
276
|
+
timestamp: date.toLocaleString()
|
277
|
+
});
|
278
|
+
setMessages(loadedMessages);
|
279
|
+
// make announcement to assistive devices that new message has loaded
|
280
|
+
setAnnouncement(`Message from Bot: API response goes here`);
|
281
|
+
setIsSendButtonDisabled(false);
|
282
|
+
}, 5000);
|
283
|
+
};
|
284
|
+
|
285
|
+
const findMatchingItems = (targetValue: string) => {
|
286
|
+
let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
|
287
|
+
const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
|
288
|
+
if (filteredItems.length > 0) {
|
289
|
+
acc[key] = filteredItems;
|
290
|
+
}
|
291
|
+
return acc;
|
292
|
+
}, {});
|
293
|
+
|
294
|
+
// append message if no items are found
|
295
|
+
if (Object.keys(filteredConversations).length === 0) {
|
296
|
+
filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
|
297
|
+
}
|
298
|
+
return filteredConversations;
|
299
|
+
};
|
300
|
+
|
301
|
+
const iconLogo = (
|
302
|
+
<>
|
303
|
+
<Brand className="show-light" src={PFIconLogoColor} alt="PatternFly" />
|
304
|
+
<Brand className="show-dark" src={PFIconLogoReverse} alt="PatternFly" />
|
305
|
+
</>
|
306
|
+
);
|
307
|
+
const masthead = (
|
308
|
+
<Masthead>
|
309
|
+
<MastheadMain>
|
310
|
+
<MastheadToggle>
|
311
|
+
<PageToggleButton
|
312
|
+
variant="plain"
|
313
|
+
aria-label="Global navigation"
|
314
|
+
isSidebarOpen={isSidebarOpen}
|
315
|
+
onSidebarToggle={() => setIsSidebarOpen(!isSidebarOpen)}
|
316
|
+
id="fill-nav-toggle"
|
317
|
+
>
|
318
|
+
<BarsIcon />
|
319
|
+
</PageToggleButton>
|
320
|
+
</MastheadToggle>
|
321
|
+
<MastheadBrand>
|
322
|
+
<MastheadLogo href="https://patternfly.org" target="_blank">
|
323
|
+
Logo
|
324
|
+
</MastheadLogo>
|
325
|
+
</MastheadBrand>
|
326
|
+
</MastheadMain>
|
327
|
+
</Masthead>
|
328
|
+
);
|
329
|
+
|
330
|
+
const sidebar = (
|
331
|
+
<PageSidebar isSidebarOpen={isSidebarOpen} id="fill-sidebar">
|
332
|
+
<PageSidebarBody>Navigation</PageSidebarBody>
|
333
|
+
</PageSidebar>
|
334
|
+
);
|
335
|
+
|
336
|
+
const skipToChatbot = (event: React.MouseEvent) => {
|
337
|
+
event.preventDefault();
|
338
|
+
if (historyRef.current) {
|
339
|
+
historyRef.current.focus();
|
340
|
+
}
|
341
|
+
};
|
342
|
+
|
343
|
+
const skipToContent = (
|
344
|
+
/* You can also add a SkipToContent for your main content here */
|
345
|
+
<SkipToContent href="#" onClick={skipToChatbot}>
|
346
|
+
Skip to chatbot
|
347
|
+
</SkipToContent>
|
348
|
+
);
|
349
|
+
|
350
|
+
const chatbot = (
|
351
|
+
<Chatbot displayMode={displayMode}>
|
352
|
+
<ChatbotConversationHistoryNav
|
353
|
+
displayMode={displayMode}
|
354
|
+
onDrawerToggle={() => {
|
355
|
+
setIsDrawerOpen(!isDrawerOpen);
|
356
|
+
setConversations(initialConversations);
|
357
|
+
}}
|
358
|
+
isDrawerOpen={isDrawerOpen}
|
359
|
+
setIsDrawerOpen={setIsDrawerOpen}
|
360
|
+
activeItemId="1"
|
361
|
+
// eslint-disable-next-line no-console
|
362
|
+
onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)}
|
363
|
+
conversations={conversations}
|
364
|
+
onNewChat={() => {
|
365
|
+
setIsDrawerOpen(!isDrawerOpen);
|
366
|
+
setMessages([]);
|
367
|
+
setConversations(initialConversations);
|
368
|
+
}}
|
369
|
+
handleTextInputChange={(value: string) => {
|
370
|
+
if (value === '') {
|
371
|
+
setConversations(initialConversations);
|
372
|
+
}
|
373
|
+
// this is where you would perform search on the items in the drawer
|
374
|
+
// and update the state
|
375
|
+
const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
|
376
|
+
setConversations(newConversations);
|
377
|
+
}}
|
378
|
+
drawerContent={
|
379
|
+
<>
|
380
|
+
<ChatbotHeader>
|
381
|
+
<ChatbotHeaderMain>
|
382
|
+
<ChatbotHeaderMenu
|
383
|
+
ref={historyRef}
|
384
|
+
aria-expanded={isDrawerOpen}
|
385
|
+
onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)}
|
386
|
+
/>
|
387
|
+
<ChatbotHeaderTitle>{iconLogo}</ChatbotHeaderTitle>
|
388
|
+
</ChatbotHeaderMain>
|
389
|
+
<ChatbotHeaderActions>
|
390
|
+
<ChatbotHeaderSelectorDropdown value={selectedModel} onSelect={onSelectModel}>
|
391
|
+
<DropdownList>
|
392
|
+
<DropdownItem value="Granite 7B" key="granite">
|
393
|
+
Granite 7B
|
394
|
+
</DropdownItem>
|
395
|
+
<DropdownItem value="Llama 3.0" key="llama">
|
396
|
+
Llama 3.0
|
397
|
+
</DropdownItem>
|
398
|
+
<DropdownItem value="Mistral 3B" key="mistral">
|
399
|
+
Mistral 3B
|
400
|
+
</DropdownItem>
|
401
|
+
</DropdownList>
|
402
|
+
</ChatbotHeaderSelectorDropdown>
|
403
|
+
</ChatbotHeaderActions>
|
404
|
+
</ChatbotHeader>
|
405
|
+
<ChatbotContent>
|
406
|
+
{/* Update the announcement prop on MessageBox whenever a new message is sent
|
407
|
+
so that users of assistive devices receive sufficient context */}
|
408
|
+
<MessageBox announcement={announcement}>
|
409
|
+
<ChatbotWelcomePrompt
|
410
|
+
title="Hello, Chatbot User"
|
411
|
+
description="How may I help you today?"
|
412
|
+
prompts={welcomePrompts}
|
413
|
+
/>
|
414
|
+
{/* This code block enables scrolling to the top of the last message.
|
415
|
+
You can instead choose to move the div with scrollToBottomRef on it below
|
416
|
+
the map of messages, so that users are forced to scroll to the bottom.
|
417
|
+
If you are using streaming, you will want to take a different approach;
|
418
|
+
see: https://github.com/patternfly/chatbot/issues/201#issuecomment-2400725173 */}
|
419
|
+
{messages.map((message, index) => {
|
420
|
+
if (index === messages.length - 1) {
|
421
|
+
return (
|
422
|
+
<>
|
423
|
+
<div ref={scrollToBottomRef}></div>
|
424
|
+
<Message key={message.id} {...message} />
|
425
|
+
</>
|
426
|
+
);
|
427
|
+
}
|
428
|
+
return <Message key={message.id} {...message} />;
|
429
|
+
})}
|
430
|
+
</MessageBox>
|
431
|
+
</ChatbotContent>
|
432
|
+
<ChatbotFooter>
|
433
|
+
<MessageBar onSendMessage={handleSend} hasMicrophoneButton isSendButtonDisabled={isSendButtonDisabled} />
|
434
|
+
<ChatbotFootnote {...footnoteProps} />
|
435
|
+
</ChatbotFooter>
|
436
|
+
</>
|
437
|
+
}
|
438
|
+
></ChatbotConversationHistoryNav>
|
439
|
+
</Chatbot>
|
440
|
+
);
|
441
|
+
|
442
|
+
const panelContent = <DrawerPanelContent>{chatbot}</DrawerPanelContent>;
|
443
|
+
|
444
|
+
return (
|
445
|
+
<Drawer isExpanded={isExpanded} isInline onExpand={onExpand}>
|
446
|
+
<DrawerContent panelContent={panelContent}>
|
447
|
+
<DrawerContentBody>
|
448
|
+
<Page skipToContent={skipToContent} masthead={masthead} sidebar={sidebar} isContentFilled></Page>
|
449
|
+
</DrawerContentBody>
|
450
|
+
</DrawerContent>
|
451
|
+
</Drawer>
|
452
|
+
);
|
453
|
+
};
|
package/src/Chatbot/Chatbot.scss
CHANGED
@@ -106,3 +106,22 @@
|
|
106
106
|
.pf-chatbot-container--fullscreen {
|
107
107
|
border-radius: unset;
|
108
108
|
}
|
109
|
+
|
110
|
+
// ============================================================================
|
111
|
+
// Chatbot Display Mode - Drawer
|
112
|
+
// ============================================================================
|
113
|
+
.pf-chatbot--drawer {
|
114
|
+
inset-block-end: 0;
|
115
|
+
inset-inline-end: 0;
|
116
|
+
padding: 0;
|
117
|
+
width: 100%;
|
118
|
+
height: 100%;
|
119
|
+
border-radius: 0;
|
120
|
+
box-shadow: none;
|
121
|
+
border-left: var(--pf-t--global--border--width--divider--default) solid;
|
122
|
+
border-color: var(--pf-t--global--border--color--default);
|
123
|
+
|
124
|
+
.pf-chatbot-container {
|
125
|
+
border-radius: var(--pf-t--global--border--radius--sharp);
|
126
|
+
}
|
127
|
+
}
|
package/src/Chatbot/Chatbot.tsx
CHANGED
@@ -18,6 +18,7 @@
|
|
18
18
|
// Chatbot Display Mode - Fullscreen and Embedded
|
19
19
|
// ============================================================================
|
20
20
|
@media screen and (min-width: 64rem) {
|
21
|
+
.pf-chatbot--drawer,
|
21
22
|
.pf-chatbot--fullscreen,
|
22
23
|
.pf-chatbot--embedded {
|
23
24
|
.pf-chatbot__content {
|
@@ -155,6 +155,7 @@
|
|
155
155
|
// ============================================================================
|
156
156
|
// Chatbot Display Mode - Fullscreen
|
157
157
|
// ============================================================================
|
158
|
+
.pf-chatbot--drawer,
|
158
159
|
.pf-chatbot--fullscreen {
|
159
160
|
.pf-chatbot__history.pf-v6-c-drawer {
|
160
161
|
height: 100vh;
|
@@ -181,6 +182,7 @@
|
|
181
182
|
}
|
182
183
|
|
183
184
|
.pf-chatbot--docked,
|
185
|
+
.pf-chatbot--drawer,
|
184
186
|
.pf-chatbot--embedded,
|
185
187
|
.pf-chatbot--fullscreen {
|
186
188
|
.pf-chatbot__history {
|
@@ -285,7 +285,7 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
|
|
285
285
|
<DrawerContentBody {...drawerContentBodyProps}>
|
286
286
|
<>
|
287
287
|
<div
|
288
|
-
className={`${isDrawerOpen && (displayMode === ChatbotDisplayMode.default || displayMode === ChatbotDisplayMode.docked) ? 'pf-v6-c-backdrop pf-chatbot__drawer-backdrop' : undefined} `}
|
288
|
+
className={`${isDrawerOpen && (displayMode === ChatbotDisplayMode.default || displayMode === ChatbotDisplayMode.docked || displayMode === ChatbotDisplayMode.drawer) ? 'pf-v6-c-backdrop pf-chatbot__drawer-backdrop' : undefined} `}
|
289
289
|
></div>
|
290
290
|
{drawerContent}
|
291
291
|
</>
|
@@ -48,3 +48,12 @@
|
|
48
48
|
padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--lg);
|
49
49
|
}
|
50
50
|
}
|
51
|
+
|
52
|
+
// ============================================================================
|
53
|
+
// Chatbot Display Mode - Drawer
|
54
|
+
// ============================================================================
|
55
|
+
.pf-chatbot--drawer {
|
56
|
+
.pf-chatbot__footer-container {
|
57
|
+
padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--lg);
|
58
|
+
}
|
59
|
+
}
|
@@ -70,8 +70,9 @@
|
|
70
70
|
}
|
71
71
|
|
72
72
|
// ============================================================================
|
73
|
-
// Chatbot Display Mode - Docked
|
73
|
+
// Chatbot Display Mode - Docked and Drawer
|
74
74
|
// ============================================================================
|
75
|
+
.pf-chatbot--drawer,
|
75
76
|
.pf-chatbot--docked {
|
76
77
|
.pf-chatbot__header {
|
77
78
|
background-color: var(--pf-t--chatbot--background);
|
@@ -17,27 +17,27 @@ describe('ChatbotHeaderTitle', () => {
|
|
17
17
|
});
|
18
18
|
|
19
19
|
it('should render title for default display mode', () => {
|
20
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.default} showOnDefault=
|
20
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.default} showOnDefault="Default header title" />);
|
21
21
|
expect(screen.getByText('Default header title')).toBeTruthy();
|
22
22
|
});
|
23
23
|
|
24
24
|
it('should render title for docked display mode', () => {
|
25
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.docked} showOnDocked=
|
25
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.docked} showOnDocked="Docked header title" />);
|
26
26
|
expect(screen.getByText('Docked header title')).toBeTruthy();
|
27
27
|
});
|
28
28
|
|
29
29
|
it('should fallback to default title when docked display mode title is not configured', () => {
|
30
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.docked} showOnDefault=
|
30
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.docked} showOnDefault="Default header title" />);
|
31
31
|
expect(screen.getByText('Default header title')).toBeTruthy();
|
32
32
|
});
|
33
33
|
|
34
34
|
it('should render title for embedded display mode', () => {
|
35
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.embedded} showOnEmbedded=
|
35
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.embedded} showOnEmbedded="Embedded header title" />);
|
36
36
|
expect(screen.getByText('Embedded header title')).toBeTruthy();
|
37
37
|
});
|
38
38
|
|
39
39
|
it('should fallback to default title when embedded display mode title is not configured', () => {
|
40
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.embedded} showOnDefault=
|
40
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.embedded} showOnDefault="Default header title" />);
|
41
41
|
expect(screen.getByText('Default header title')).toBeTruthy();
|
42
42
|
});
|
43
43
|
|
@@ -45,7 +45,7 @@ describe('ChatbotHeaderTitle', () => {
|
|
45
45
|
render(
|
46
46
|
<ChatbotHeaderTitle
|
47
47
|
displayMode={ChatbotDisplayMode.fullscreen}
|
48
|
-
showOnFullScreen=
|
48
|
+
showOnFullScreen="Fullscreen header title"
|
49
49
|
className="custom-header-class"
|
50
50
|
/>
|
51
51
|
);
|
@@ -53,7 +53,23 @@ describe('ChatbotHeaderTitle', () => {
|
|
53
53
|
});
|
54
54
|
|
55
55
|
it('should fallback to default title when fullscreen display mode title is not configured', () => {
|
56
|
-
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.fullscreen} showOnDefault=
|
56
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.fullscreen} showOnDefault="Default header title" />);
|
57
|
+
expect(screen.getByText('Default header title')).toBeTruthy();
|
58
|
+
});
|
59
|
+
|
60
|
+
it('should render title for drawer display mode', () => {
|
61
|
+
render(
|
62
|
+
<ChatbotHeaderTitle
|
63
|
+
displayMode={ChatbotDisplayMode.drawer}
|
64
|
+
showOnDrawer="Drawer header title"
|
65
|
+
className="custom-header-class"
|
66
|
+
/>
|
67
|
+
);
|
68
|
+
expect(screen.getByText('Drawer header title')).toBeTruthy();
|
69
|
+
});
|
70
|
+
|
71
|
+
it('should fallback to default title when drawer display mode title is not configured', () => {
|
72
|
+
render(<ChatbotHeaderTitle displayMode={ChatbotDisplayMode.drawer} showOnDefault="Default header title" />);
|
57
73
|
expect(screen.getByText('Default header title')).toBeTruthy();
|
58
74
|
});
|
59
75
|
});
|
@@ -14,8 +14,10 @@ export interface ChatbotHeaderTitleProps {
|
|
14
14
|
showOnFullScreen?: React.ReactNode | string;
|
15
15
|
/** Content to display on docked screen */
|
16
16
|
showOnDocked?: React.ReactNode | string;
|
17
|
-
/** Content to display on
|
17
|
+
/** Content to display on embedded screen */
|
18
18
|
showOnEmbedded?: React.ReactNode | string;
|
19
|
+
/** Content to display in drawer mode */
|
20
|
+
showOnDrawer?: React.ReactNode | string;
|
19
21
|
/** Content to display by default; this will be shown if a case is not explicitly set */
|
20
22
|
showOnDefault?: React.ReactNode | string;
|
21
23
|
}
|
@@ -27,10 +29,11 @@ export const ChatbotHeaderTitle: React.FunctionComponent<ChatbotHeaderTitleProps
|
|
27
29
|
showOnFullScreen,
|
28
30
|
showOnDocked,
|
29
31
|
showOnEmbedded,
|
32
|
+
showOnDrawer,
|
30
33
|
showOnDefault
|
31
34
|
}: ChatbotHeaderTitleProps) => {
|
32
35
|
const renderChildren = () => {
|
33
|
-
if (displayMode
|
36
|
+
if (displayMode) {
|
34
37
|
/* eslint-disable indent */
|
35
38
|
switch (displayMode) {
|
36
39
|
case ChatbotDisplayMode.fullscreen:
|
@@ -39,6 +42,8 @@ export const ChatbotHeaderTitle: React.FunctionComponent<ChatbotHeaderTitleProps
|
|
39
42
|
return showOnDocked ?? showOnDefault;
|
40
43
|
case ChatbotDisplayMode.embedded:
|
41
44
|
return showOnEmbedded ?? showOnDefault;
|
45
|
+
case ChatbotDisplayMode.drawer:
|
46
|
+
return showOnDrawer ?? showOnDefault;
|
42
47
|
default:
|
43
48
|
return showOnDefault;
|
44
49
|
}
|
@@ -20,7 +20,6 @@ describe('UserFeedback', () => {
|
|
20
20
|
expect(screen.getByRole('button', { name: /Resolved my issue/i })).toBeTruthy();
|
21
21
|
expect(screen.getByRole('button', { name: /Submit/i })).toBeTruthy();
|
22
22
|
expect(screen.getByRole('button', { name: 'Close feedback for message received at 12/12/12' })).toBeTruthy();
|
23
|
-
expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
|
24
23
|
expect(screen.queryByRole('textbox', { name: /Provide optional additional feedback/i })).toBeFalsy();
|
25
24
|
});
|
26
25
|
it('should render different title correctly', () => {
|
@@ -125,26 +124,6 @@ describe('UserFeedback', () => {
|
|
125
124
|
);
|
126
125
|
expect(screen.getByRole('button', { name: /Ima button/i })).toBeTruthy();
|
127
126
|
});
|
128
|
-
it('should handle onClose correctly when cancel button is clicked', async () => {
|
129
|
-
const spy = jest.fn();
|
130
|
-
render(<UserFeedback onSubmit={jest.fn} quickResponses={MOCK_RESPONSES} onClose={spy} timestamp="12/12/12" />);
|
131
|
-
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
132
|
-
expect(cancelButton).toBeTruthy();
|
133
|
-
await userEvent.click(cancelButton);
|
134
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
135
|
-
});
|
136
|
-
it('should change cancel word correctly', () => {
|
137
|
-
render(
|
138
|
-
<UserFeedback
|
139
|
-
onSubmit={jest.fn}
|
140
|
-
quickResponses={MOCK_RESPONSES}
|
141
|
-
onClose={jest.fn}
|
142
|
-
cancelWord="Exit"
|
143
|
-
timestamp="12/12/12"
|
144
|
-
/>
|
145
|
-
);
|
146
|
-
expect(screen.getByRole('button', { name: 'Exit' })).toBeTruthy();
|
147
|
-
});
|
148
127
|
it('should handle className', async () => {
|
149
128
|
render(
|
150
129
|
<UserFeedback
|