@patternfly/chatbot 2.2.0-prerelease.41 → 2.2.0-prerelease.43
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 +4 -0
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
- package/dist/cjs/MessageBar/MicrophoneButton.js +1 -1
- package/dist/cjs/MessageBox/MessageBox.js +5 -5
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +4 -0
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
- package/dist/esm/MessageBar/MicrophoneButton.js +1 -1
- package/dist/esm/MessageBox/MessageBox.js +5 -5
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +35 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerResizable.tsx +13 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx +60 -57
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +53 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +14 -0
- package/src/MessageBar/MicrophoneButton.tsx +1 -1
- package/src/MessageBox/MessageBox.tsx +5 -5
@@ -76,6 +76,10 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
|
|
76
76
|
loadingState?: SkeletonProps;
|
77
77
|
/** Content to show in error state. Error state will appear once content is passed in. */
|
78
78
|
errorState?: HistoryEmptyStateProps;
|
79
|
+
/** Content to show in empty state. Empty state will appear once content is passed in. */
|
80
|
+
emptyState?: HistoryEmptyStateProps;
|
81
|
+
/** Content to show in no results state. No results state will appear once content is passed in. */
|
82
|
+
noResultsState?: HistoryEmptyStateProps;
|
79
83
|
}
|
80
84
|
export declare const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps>;
|
81
85
|
export default ChatbotConversationHistoryNav;
|
@@ -27,7 +27,7 @@ const ChatbotConversationHistoryDropdown_1 = __importDefault(require("./ChatbotC
|
|
27
27
|
const LoadingState_1 = __importDefault(require("./LoadingState"));
|
28
28
|
const EmptyState_1 = __importDefault(require("./EmptyState"));
|
29
29
|
const ChatbotConversationHistoryNav = (_a) => {
|
30
|
-
var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, newChatButtonText = 'New chat', drawerContent, onNewChat, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Filter menu items', handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', menuProps, drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, isLoading, loadingState, errorState } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "newChatButtonText", "drawerContent", "onNewChat", "searchInputPlaceholder", "searchInputAriaLabel", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "menuProps", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "isLoading", "loadingState", "errorState"]);
|
30
|
+
var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, newChatButtonText = 'New chat', drawerContent, onNewChat, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Filter menu items', handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', menuProps, drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, isLoading, loadingState, errorState, emptyState, noResultsState } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "newChatButtonText", "drawerContent", "onNewChat", "searchInputPlaceholder", "searchInputAriaLabel", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "menuProps", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "isLoading", "loadingState", "errorState", "emptyState", "noResultsState"]);
|
31
31
|
const drawerRef = react_1.default.useRef(null);
|
32
32
|
const onExpand = () => {
|
33
33
|
drawerRef.current && drawerRef.current.focus();
|
@@ -58,6 +58,12 @@ const ChatbotConversationHistoryNav = (_a) => {
|
|
58
58
|
if (errorState) {
|
59
59
|
return react_1.default.createElement(EmptyState_1.default, Object.assign({}, errorState));
|
60
60
|
}
|
61
|
+
if (emptyState) {
|
62
|
+
return react_1.default.createElement(EmptyState_1.default, Object.assign({}, emptyState));
|
63
|
+
}
|
64
|
+
if (noResultsState) {
|
65
|
+
return react_1.default.createElement(EmptyState_1.default, Object.assign({}, noResultsState));
|
66
|
+
}
|
61
67
|
return (react_1.default.createElement(react_core_1.Menu, Object.assign({ isPlain: true, onSelect: onSelectActiveItem, activeItemId: activeItemId }, menuProps),
|
62
68
|
react_1.default.createElement(react_core_1.MenuContent, null, buildMenu())));
|
63
69
|
};
|
@@ -18,6 +18,7 @@ const react_2 = require("@testing-library/react");
|
|
18
18
|
const Chatbot_1 = require("../Chatbot/Chatbot");
|
19
19
|
const ChatbotConversationHistoryNav_1 = __importDefault(require("./ChatbotConversationHistoryNav"));
|
20
20
|
const react_core_1 = require("@patternfly/react-core");
|
21
|
+
const react_icons_1 = require("@patternfly/react-icons");
|
21
22
|
const ERROR = {
|
22
23
|
bodyText: (react_1.default.createElement(react_1.default.Fragment, null,
|
23
24
|
"To try again, check your connection and reload this page. If the issue persists,",
|
@@ -31,6 +32,16 @@ const ERROR = {
|
|
31
32
|
status: react_core_1.EmptyStateStatus.danger,
|
32
33
|
onClick: () => alert('Clicked Reload')
|
33
34
|
};
|
35
|
+
const NO_RESULTS = {
|
36
|
+
bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.',
|
37
|
+
titleText: 'No results found',
|
38
|
+
icon: react_icons_1.SearchIcon
|
39
|
+
};
|
40
|
+
const EMPTY_STATE = {
|
41
|
+
bodyText: 'Access timely assistance by starting a conversation with an AI model.',
|
42
|
+
titleText: 'Start a new chat',
|
43
|
+
icon: react_icons_1.OutlinedCommentsIcon
|
44
|
+
};
|
34
45
|
const ERROR_WITHOUT_BUTTON = {
|
35
46
|
bodyText: (react_1.default.createElement(react_1.default.Fragment, null,
|
36
47
|
"To try again, check your connection and reload this page. If the issue persists,",
|
@@ -162,4 +173,16 @@ describe('ChatbotConversationHistoryNav', () => {
|
|
162
173
|
(0, react_2.render)(react_1.default.createElement(ChatbotConversationHistoryNav_1.default, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: Chatbot_1.ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, isLoading: true, errorState: ERROR }));
|
163
174
|
expect(react_2.screen.getByRole('dialog', { name: /Loading/i })).toBeTruthy();
|
164
175
|
});
|
176
|
+
it('should accept emptyState', () => {
|
177
|
+
(0, react_2.render)(react_1.default.createElement(ChatbotConversationHistoryNav_1.default, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: Chatbot_1.ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, emptyState: EMPTY_STATE }));
|
178
|
+
expect(react_2.screen.getByRole('dialog', {
|
179
|
+
name: /Start a new chat Access timely assistance by starting a conversation with an AI model./i
|
180
|
+
})).toBeTruthy();
|
181
|
+
});
|
182
|
+
it('should accept no results state', () => {
|
183
|
+
(0, react_2.render)(react_1.default.createElement(ChatbotConversationHistoryNav_1.default, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: Chatbot_1.ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, noResultsState: NO_RESULTS }));
|
184
|
+
expect(react_2.screen.getByRole('dialog', {
|
185
|
+
name: /No results found Adjust your search query and try again. Check your spelling or try a more general term./i
|
186
|
+
})).toBeTruthy();
|
187
|
+
});
|
165
188
|
});
|
@@ -29,26 +29,26 @@ const MessageBoxBase = ({ announcement, ariaLabel = 'Scrollable message log', ch
|
|
29
29
|
setAtTop(scrollTop === 0);
|
30
30
|
setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
|
31
31
|
}
|
32
|
-
}, []);
|
32
|
+
}, [messageBoxRef]);
|
33
33
|
const checkOverflow = react_1.default.useCallback(() => {
|
34
34
|
const element = messageBoxRef.current;
|
35
35
|
if (element) {
|
36
36
|
const { scrollHeight, clientHeight } = element;
|
37
37
|
setIsOverflowing(scrollHeight >= clientHeight);
|
38
38
|
}
|
39
|
-
}, []);
|
39
|
+
}, [messageBoxRef]);
|
40
40
|
const scrollToTop = react_1.default.useCallback(() => {
|
41
41
|
const element = messageBoxRef.current;
|
42
42
|
if (element) {
|
43
43
|
element.scrollTo({ top: 0, behavior: 'smooth' });
|
44
44
|
}
|
45
|
-
}, []);
|
45
|
+
}, [messageBoxRef]);
|
46
46
|
const scrollToBottom = react_1.default.useCallback(() => {
|
47
47
|
const element = messageBoxRef.current;
|
48
48
|
if (element) {
|
49
49
|
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
50
50
|
}
|
51
|
-
}, []);
|
51
|
+
}, [messageBoxRef]);
|
52
52
|
// Detect scroll position
|
53
53
|
react_1.default.useEffect(() => {
|
54
54
|
const element = messageBoxRef.current;
|
@@ -62,7 +62,7 @@ const MessageBoxBase = ({ announcement, ariaLabel = 'Scrollable message log', ch
|
|
62
62
|
element.removeEventListener('scroll', handleScroll);
|
63
63
|
};
|
64
64
|
}
|
65
|
-
}, [checkOverflow, handleScroll]);
|
65
|
+
}, [checkOverflow, handleScroll, messageBoxRef]);
|
66
66
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
67
67
|
react_1.default.createElement(JumpButton_1.default, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop }),
|
68
68
|
react_1.default.createElement("div", { role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className !== null && className !== void 0 ? className : ''}`, ref: innerRef !== null && innerRef !== void 0 ? innerRef : messageBoxRef },
|
@@ -76,6 +76,10 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
|
|
76
76
|
loadingState?: SkeletonProps;
|
77
77
|
/** Content to show in error state. Error state will appear once content is passed in. */
|
78
78
|
errorState?: HistoryEmptyStateProps;
|
79
|
+
/** Content to show in empty state. Empty state will appear once content is passed in. */
|
80
|
+
emptyState?: HistoryEmptyStateProps;
|
81
|
+
/** Content to show in no results state. No results state will appear once content is passed in. */
|
82
|
+
noResultsState?: HistoryEmptyStateProps;
|
79
83
|
}
|
80
84
|
export declare const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps>;
|
81
85
|
export default ChatbotConversationHistoryNav;
|
@@ -21,7 +21,7 @@ import ConversationHistoryDropdown from './ChatbotConversationHistoryDropdown';
|
|
21
21
|
import LoadingState from './LoadingState';
|
22
22
|
import HistoryEmptyState from './EmptyState';
|
23
23
|
export const ChatbotConversationHistoryNav = (_a) => {
|
24
|
-
var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, newChatButtonText = 'New chat', drawerContent, onNewChat, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Filter menu items', handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', menuProps, drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, isLoading, loadingState, errorState } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "newChatButtonText", "drawerContent", "onNewChat", "searchInputPlaceholder", "searchInputAriaLabel", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "menuProps", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "isLoading", "loadingState", "errorState"]);
|
24
|
+
var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, newChatButtonText = 'New chat', drawerContent, onNewChat, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Filter menu items', handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', menuProps, drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, isLoading, loadingState, errorState, emptyState, noResultsState } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "newChatButtonText", "drawerContent", "onNewChat", "searchInputPlaceholder", "searchInputAriaLabel", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "menuProps", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "isLoading", "loadingState", "errorState", "emptyState", "noResultsState"]);
|
25
25
|
const drawerRef = React.useRef(null);
|
26
26
|
const onExpand = () => {
|
27
27
|
drawerRef.current && drawerRef.current.focus();
|
@@ -52,6 +52,12 @@ export const ChatbotConversationHistoryNav = (_a) => {
|
|
52
52
|
if (errorState) {
|
53
53
|
return React.createElement(HistoryEmptyState, Object.assign({}, errorState));
|
54
54
|
}
|
55
|
+
if (emptyState) {
|
56
|
+
return React.createElement(HistoryEmptyState, Object.assign({}, emptyState));
|
57
|
+
}
|
58
|
+
if (noResultsState) {
|
59
|
+
return React.createElement(HistoryEmptyState, Object.assign({}, noResultsState));
|
60
|
+
}
|
55
61
|
return (React.createElement(Menu, Object.assign({ isPlain: true, onSelect: onSelectActiveItem, activeItemId: activeItemId }, menuProps),
|
56
62
|
React.createElement(MenuContent, null, buildMenu())));
|
57
63
|
};
|
@@ -13,6 +13,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
13
13
|
import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
|
14
14
|
import ChatbotConversationHistoryNav from './ChatbotConversationHistoryNav';
|
15
15
|
import { EmptyStateStatus, Spinner } from '@patternfly/react-core';
|
16
|
+
import { OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons';
|
16
17
|
const ERROR = {
|
17
18
|
bodyText: (React.createElement(React.Fragment, null,
|
18
19
|
"To try again, check your connection and reload this page. If the issue persists,",
|
@@ -26,6 +27,16 @@ const ERROR = {
|
|
26
27
|
status: EmptyStateStatus.danger,
|
27
28
|
onClick: () => alert('Clicked Reload')
|
28
29
|
};
|
30
|
+
const NO_RESULTS = {
|
31
|
+
bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.',
|
32
|
+
titleText: 'No results found',
|
33
|
+
icon: SearchIcon
|
34
|
+
};
|
35
|
+
const EMPTY_STATE = {
|
36
|
+
bodyText: 'Access timely assistance by starting a conversation with an AI model.',
|
37
|
+
titleText: 'Start a new chat',
|
38
|
+
icon: OutlinedCommentsIcon
|
39
|
+
};
|
29
40
|
const ERROR_WITHOUT_BUTTON = {
|
30
41
|
bodyText: (React.createElement(React.Fragment, null,
|
31
42
|
"To try again, check your connection and reload this page. If the issue persists,",
|
@@ -157,4 +168,16 @@ describe('ChatbotConversationHistoryNav', () => {
|
|
157
168
|
render(React.createElement(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, isLoading: true, errorState: ERROR }));
|
158
169
|
expect(screen.getByRole('dialog', { name: /Loading/i })).toBeTruthy();
|
159
170
|
});
|
171
|
+
it('should accept emptyState', () => {
|
172
|
+
render(React.createElement(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, emptyState: EMPTY_STATE }));
|
173
|
+
expect(screen.getByRole('dialog', {
|
174
|
+
name: /Start a new chat Access timely assistance by starting a conversation with an AI model./i
|
175
|
+
})).toBeTruthy();
|
176
|
+
});
|
177
|
+
it('should accept no results state', () => {
|
178
|
+
render(React.createElement(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), reverseButtonOrder: false, handleTextInputChange: jest.fn(), conversations: initialConversations, noResultsState: NO_RESULTS }));
|
179
|
+
expect(screen.getByRole('dialog', {
|
180
|
+
name: /No results found Adjust your search query and try again. Check your spelling or try a more general term./i
|
181
|
+
})).toBeTruthy();
|
182
|
+
});
|
160
183
|
});
|
@@ -23,26 +23,26 @@ const MessageBoxBase = ({ announcement, ariaLabel = 'Scrollable message log', ch
|
|
23
23
|
setAtTop(scrollTop === 0);
|
24
24
|
setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
|
25
25
|
}
|
26
|
-
}, []);
|
26
|
+
}, [messageBoxRef]);
|
27
27
|
const checkOverflow = React.useCallback(() => {
|
28
28
|
const element = messageBoxRef.current;
|
29
29
|
if (element) {
|
30
30
|
const { scrollHeight, clientHeight } = element;
|
31
31
|
setIsOverflowing(scrollHeight >= clientHeight);
|
32
32
|
}
|
33
|
-
}, []);
|
33
|
+
}, [messageBoxRef]);
|
34
34
|
const scrollToTop = React.useCallback(() => {
|
35
35
|
const element = messageBoxRef.current;
|
36
36
|
if (element) {
|
37
37
|
element.scrollTo({ top: 0, behavior: 'smooth' });
|
38
38
|
}
|
39
|
-
}, []);
|
39
|
+
}, [messageBoxRef]);
|
40
40
|
const scrollToBottom = React.useCallback(() => {
|
41
41
|
const element = messageBoxRef.current;
|
42
42
|
if (element) {
|
43
43
|
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
44
44
|
}
|
45
|
-
}, []);
|
45
|
+
}, [messageBoxRef]);
|
46
46
|
// Detect scroll position
|
47
47
|
React.useEffect(() => {
|
48
48
|
const element = messageBoxRef.current;
|
@@ -56,7 +56,7 @@ const MessageBoxBase = ({ announcement, ariaLabel = 'Scrollable message log', ch
|
|
56
56
|
element.removeEventListener('scroll', handleScroll);
|
57
57
|
};
|
58
58
|
}
|
59
|
-
}, [checkOverflow, handleScroll]);
|
59
|
+
}, [checkOverflow, handleScroll, messageBoxRef]);
|
60
60
|
return (React.createElement(React.Fragment, null,
|
61
61
|
React.createElement(JumpButton, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop }),
|
62
62
|
React.createElement("div", { role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className !== null && className !== void 0 ? className : ''}`, ref: innerRef !== null && innerRef !== void 0 ? innerRef : messageBoxRef },
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@patternfly/chatbot",
|
3
|
-
"version": "2.2.0-prerelease.
|
3
|
+
"version": "2.2.0-prerelease.43",
|
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",
|
@@ -4,6 +4,7 @@ import ChatbotConversationHistoryNav, {
|
|
4
4
|
Conversation
|
5
5
|
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
|
6
6
|
import { Checkbox, EmptyStateStatus, Spinner } from '@patternfly/react-core';
|
7
|
+
import { OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons';
|
7
8
|
|
8
9
|
const initialConversations: { [key: string]: Conversation[] } = {
|
9
10
|
Today: [{ id: '1', text: 'Red Hat products and services' }],
|
@@ -46,6 +47,18 @@ const ERROR = {
|
|
46
47
|
onClick: () => alert('Clicked Reload')
|
47
48
|
};
|
48
49
|
|
50
|
+
const NO_RESULTS = {
|
51
|
+
bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.',
|
52
|
+
titleText: 'No results found',
|
53
|
+
icon: SearchIcon
|
54
|
+
};
|
55
|
+
|
56
|
+
const EMPTY_STATE = {
|
57
|
+
bodyText: 'Access timely assistance by starting a conversation with an AI model.',
|
58
|
+
titleText: 'Start a new chat',
|
59
|
+
icon: OutlinedCommentsIcon
|
60
|
+
};
|
61
|
+
|
49
62
|
export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
|
50
63
|
const [isOpen, setIsOpen] = React.useState(true);
|
51
64
|
const [isButtonOrderReversed, setIsButtonOrderReversed] = React.useState(false);
|
@@ -54,10 +67,12 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
|
|
54
67
|
);
|
55
68
|
const [isLoading, setIsLoading] = React.useState(false);
|
56
69
|
const [hasError, setHasError] = React.useState(false);
|
70
|
+
const [isEmpty, setIsEmpty] = React.useState(false);
|
71
|
+
const [hasNoResults, setHasNoResults] = React.useState(false);
|
57
72
|
const displayMode = ChatbotDisplayMode.embedded;
|
58
73
|
|
59
74
|
const findMatchingItems = (targetValue: string) => {
|
60
|
-
|
75
|
+
const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
|
61
76
|
const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
|
62
77
|
if (filteredItems.length > 0) {
|
63
78
|
acc[key] = filteredItems;
|
@@ -67,7 +82,9 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
|
|
67
82
|
|
68
83
|
// append message if no items are found
|
69
84
|
if (Object.keys(filteredConversations).length === 0) {
|
70
|
-
|
85
|
+
setHasNoResults(true);
|
86
|
+
} else {
|
87
|
+
setHasNoResults(false);
|
71
88
|
}
|
72
89
|
return filteredConversations;
|
73
90
|
};
|
@@ -105,6 +122,20 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
|
|
105
122
|
id="drawer-has-error"
|
106
123
|
name="drawer-has-error"
|
107
124
|
></Checkbox>
|
125
|
+
<Checkbox
|
126
|
+
label="Show empty state"
|
127
|
+
isChecked={isEmpty}
|
128
|
+
onChange={() => setIsEmpty(!isEmpty)}
|
129
|
+
id="drawer-is-empty"
|
130
|
+
name="drawer-is-empty"
|
131
|
+
></Checkbox>
|
132
|
+
<Checkbox
|
133
|
+
label="Show no results state"
|
134
|
+
isChecked={hasNoResults}
|
135
|
+
onChange={() => setHasNoResults(!hasNoResults)}
|
136
|
+
id="drawer-has-no-results"
|
137
|
+
name="drawer-has-no-results"
|
138
|
+
></Checkbox>
|
108
139
|
<ChatbotConversationHistoryNav
|
109
140
|
displayMode={displayMode}
|
110
141
|
onDrawerToggle={() => setIsOpen(!isOpen)}
|
@@ -129,6 +160,8 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
|
|
129
160
|
drawerContent={<div>Drawer content</div>}
|
130
161
|
isLoading={isLoading}
|
131
162
|
errorState={hasError ? ERROR : undefined}
|
163
|
+
emptyState={isEmpty ? EMPTY_STATE : undefined}
|
164
|
+
noResultsState={hasNoResults ? NO_RESULTS : undefined}
|
132
165
|
/>
|
133
166
|
</>
|
134
167
|
);
|
package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerResizable.tsx
CHANGED
@@ -4,6 +4,7 @@ import ChatbotConversationHistoryNav, {
|
|
4
4
|
Conversation
|
5
5
|
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
|
6
6
|
import { Checkbox } from '@patternfly/react-core';
|
7
|
+
import { SearchIcon } from '@patternfly/react-icons';
|
7
8
|
|
8
9
|
const initialConversations: { [key: string]: Conversation[] } = {
|
9
10
|
Today: [{ id: '1', text: 'Red Hat products and services' }],
|
@@ -31,15 +32,22 @@ const initialConversations: { [key: string]: Conversation[] } = {
|
|
31
32
|
]
|
32
33
|
};
|
33
34
|
|
35
|
+
const NO_RESULTS = {
|
36
|
+
bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.',
|
37
|
+
titleText: 'No results found',
|
38
|
+
icon: SearchIcon
|
39
|
+
};
|
40
|
+
|
34
41
|
export const ChatbotHeaderDrawerResizableDemo: React.FunctionComponent = () => {
|
35
42
|
const [isOpen, setIsOpen] = React.useState(true);
|
36
43
|
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
|
37
44
|
initialConversations
|
38
45
|
);
|
46
|
+
const [showNoResults, setShowNoResults] = React.useState(false);
|
39
47
|
const displayMode = ChatbotDisplayMode.embedded;
|
40
48
|
|
41
49
|
const findMatchingItems = (targetValue: string) => {
|
42
|
-
|
50
|
+
const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
|
43
51
|
const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
|
44
52
|
if (filteredItems.length > 0) {
|
45
53
|
acc[key] = filteredItems;
|
@@ -49,7 +57,9 @@ export const ChatbotHeaderDrawerResizableDemo: React.FunctionComponent = () => {
|
|
49
57
|
|
50
58
|
// append message if no items are found
|
51
59
|
if (Object.keys(filteredConversations).length === 0) {
|
52
|
-
|
60
|
+
setShowNoResults(true);
|
61
|
+
} else {
|
62
|
+
setShowNoResults(false);
|
53
63
|
}
|
54
64
|
return filteredConversations;
|
55
65
|
};
|
@@ -88,6 +98,7 @@ export const ChatbotHeaderDrawerResizableDemo: React.FunctionComponent = () => {
|
|
88
98
|
}}
|
89
99
|
drawerContent={<div>Drawer content</div>}
|
90
100
|
drawerPanelContentProps={{ isResizable: true, minSize: '200px' }}
|
101
|
+
emptyState={showNoResults ? NO_RESULTS : undefined}
|
91
102
|
/>
|
92
103
|
</>
|
93
104
|
);
|
@@ -83,7 +83,7 @@ import PFHorizontalLogoReverse from './PF-HorizontalLogo-Reverse.svg';
|
|
83
83
|
import userAvatar from '../Messages/user_avatar.svg';
|
84
84
|
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
|
85
85
|
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';
|
86
|
-
import { CloseIcon } from '@patternfly/react-icons';
|
86
|
+
import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons';
|
87
87
|
|
88
88
|
## Structure
|
89
89
|
|
package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx
CHANGED
@@ -36,71 +36,74 @@ export const CompareChild = ({ name, input, hasNewInput, setIsSendButtonDisabled
|
|
36
36
|
return id.toString();
|
37
37
|
};
|
38
38
|
|
39
|
-
const handleSend = (
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
name: 'You',
|
48
|
-
role: 'user',
|
49
|
-
content: input,
|
50
|
-
timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`
|
51
|
-
});
|
52
|
-
newMessages.push({
|
53
|
-
avatar: patternflyAvatar,
|
54
|
-
id: generateId(),
|
55
|
-
name,
|
56
|
-
role: 'bot',
|
57
|
-
timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`,
|
58
|
-
isLoading: true
|
59
|
-
});
|
60
|
-
setMessages(newMessages);
|
61
|
-
// make announcement to assistive devices that new messages have been added
|
62
|
-
setAnnouncement(`Message from You: ${input}. Message from ${name} is loading.`);
|
63
|
-
|
64
|
-
// this is for demo purposes only; in a real situation, there would be an API response we would wait for
|
65
|
-
setTimeout(() => {
|
66
|
-
const loadedMessages: MessageProps[] = [];
|
67
|
-
// we can't use structuredClone since messages contains functions, but we can't mutate
|
68
|
-
// items that are going into state or the UI won't update correctly
|
69
|
-
newMessages.forEach((message) => loadedMessages.push(message));
|
70
|
-
loadedMessages.pop();
|
71
|
-
loadedMessages.push({
|
39
|
+
const handleSend = React.useCallback(
|
40
|
+
(input: string) => {
|
41
|
+
const date = new Date();
|
42
|
+
const newMessages: MessageProps[] = [];
|
43
|
+
messages.forEach((message) => newMessages.push(message));
|
44
|
+
newMessages.push({
|
45
|
+
avatar: userAvatar,
|
46
|
+
avatarProps: { isBordered: true },
|
72
47
|
id: generateId(),
|
73
|
-
|
74
|
-
|
75
|
-
|
48
|
+
name: 'You',
|
49
|
+
role: 'user',
|
50
|
+
content: input,
|
51
|
+
timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`
|
52
|
+
});
|
53
|
+
newMessages.push({
|
76
54
|
avatar: patternflyAvatar,
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
negative: { onClick: () => console.log('Bad response') },
|
83
|
-
// eslint-disable-next-line no-console
|
84
|
-
copy: { onClick: () => console.log('Copy') },
|
85
|
-
// eslint-disable-next-line no-console
|
86
|
-
share: { onClick: () => console.log('Share') },
|
87
|
-
// eslint-disable-next-line no-console
|
88
|
-
listen: { onClick: () => console.log('Listen') }
|
89
|
-
},
|
90
|
-
timestamp: date.toLocaleString()
|
55
|
+
id: generateId(),
|
56
|
+
name,
|
57
|
+
role: 'bot',
|
58
|
+
timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`,
|
59
|
+
isLoading: true
|
91
60
|
});
|
92
|
-
setMessages(
|
93
|
-
// make announcement to assistive devices that new
|
94
|
-
setAnnouncement(`Message from ${
|
95
|
-
|
96
|
-
|
97
|
-
|
61
|
+
setMessages(newMessages);
|
62
|
+
// make announcement to assistive devices that new messages have been added
|
63
|
+
setAnnouncement(`Message from You: ${input}. Message from ${name} is loading.`);
|
64
|
+
|
65
|
+
// this is for demo purposes only; in a real situation, there would be an API response we would wait for
|
66
|
+
setTimeout(() => {
|
67
|
+
const loadedMessages: MessageProps[] = [];
|
68
|
+
// we can't use structuredClone since messages contains functions, but we can't mutate
|
69
|
+
// items that are going into state or the UI won't update correctly
|
70
|
+
newMessages.forEach((message) => loadedMessages.push(message));
|
71
|
+
loadedMessages.pop();
|
72
|
+
loadedMessages.push({
|
73
|
+
id: generateId(),
|
74
|
+
role: 'bot',
|
75
|
+
content: `API response from ${name} goes here`,
|
76
|
+
name,
|
77
|
+
avatar: patternflyAvatar,
|
78
|
+
isLoading: false,
|
79
|
+
actions: {
|
80
|
+
// eslint-disable-next-line no-console
|
81
|
+
positive: { onClick: () => console.log('Good response') },
|
82
|
+
// eslint-disable-next-line no-console
|
83
|
+
negative: { onClick: () => console.log('Bad response') },
|
84
|
+
// eslint-disable-next-line no-console
|
85
|
+
copy: { onClick: () => console.log('Copy') },
|
86
|
+
// eslint-disable-next-line no-console
|
87
|
+
share: { onClick: () => console.log('Share') },
|
88
|
+
// eslint-disable-next-line no-console
|
89
|
+
listen: { onClick: () => console.log('Listen') }
|
90
|
+
},
|
91
|
+
timestamp: date.toLocaleString()
|
92
|
+
});
|
93
|
+
setMessages(loadedMessages);
|
94
|
+
// make announcement to assistive devices that new message has loaded
|
95
|
+
setAnnouncement(`Message from ${name}: API response goes here`);
|
96
|
+
setIsSendButtonDisabled(false);
|
97
|
+
}, 5000);
|
98
|
+
},
|
99
|
+
[messages, name, setIsSendButtonDisabled]
|
100
|
+
);
|
98
101
|
|
99
102
|
React.useEffect(() => {
|
100
103
|
if (input) {
|
101
104
|
handleSend(input);
|
102
105
|
}
|
103
|
-
}, [hasNewInput]);
|
106
|
+
}, [hasNewInput, input]);
|
104
107
|
|
105
108
|
// Auto-scrolls to the latest message
|
106
109
|
React.useEffect(() => {
|
@@ -5,6 +5,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
5
|
import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
|
6
6
|
import ChatbotConversationHistoryNav, { Conversation } from './ChatbotConversationHistoryNav';
|
7
7
|
import { EmptyStateStatus, Spinner } from '@patternfly/react-core';
|
8
|
+
import { OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons';
|
8
9
|
|
9
10
|
const ERROR = {
|
10
11
|
bodyText: (
|
@@ -21,6 +22,18 @@ const ERROR = {
|
|
21
22
|
onClick: () => alert('Clicked Reload')
|
22
23
|
};
|
23
24
|
|
25
|
+
const NO_RESULTS = {
|
26
|
+
bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.',
|
27
|
+
titleText: 'No results found',
|
28
|
+
icon: SearchIcon
|
29
|
+
};
|
30
|
+
|
31
|
+
const EMPTY_STATE = {
|
32
|
+
bodyText: 'Access timely assistance by starting a conversation with an AI model.',
|
33
|
+
titleText: 'Start a new chat',
|
34
|
+
icon: OutlinedCommentsIcon
|
35
|
+
};
|
36
|
+
|
24
37
|
const ERROR_WITHOUT_BUTTON = {
|
25
38
|
bodyText: (
|
26
39
|
<>
|
@@ -362,4 +375,44 @@ describe('ChatbotConversationHistoryNav', () => {
|
|
362
375
|
);
|
363
376
|
expect(screen.getByRole('dialog', { name: /Loading/i })).toBeTruthy();
|
364
377
|
});
|
378
|
+
|
379
|
+
it('should accept emptyState', () => {
|
380
|
+
render(
|
381
|
+
<ChatbotConversationHistoryNav
|
382
|
+
onDrawerToggle={onDrawerToggle}
|
383
|
+
isDrawerOpen={true}
|
384
|
+
displayMode={ChatbotDisplayMode.fullscreen}
|
385
|
+
setIsDrawerOpen={jest.fn()}
|
386
|
+
reverseButtonOrder={false}
|
387
|
+
handleTextInputChange={jest.fn()}
|
388
|
+
conversations={initialConversations}
|
389
|
+
emptyState={EMPTY_STATE}
|
390
|
+
/>
|
391
|
+
);
|
392
|
+
expect(
|
393
|
+
screen.getByRole('dialog', {
|
394
|
+
name: /Start a new chat Access timely assistance by starting a conversation with an AI model./i
|
395
|
+
})
|
396
|
+
).toBeTruthy();
|
397
|
+
});
|
398
|
+
|
399
|
+
it('should accept no results state', () => {
|
400
|
+
render(
|
401
|
+
<ChatbotConversationHistoryNav
|
402
|
+
onDrawerToggle={onDrawerToggle}
|
403
|
+
isDrawerOpen={true}
|
404
|
+
displayMode={ChatbotDisplayMode.fullscreen}
|
405
|
+
setIsDrawerOpen={jest.fn()}
|
406
|
+
reverseButtonOrder={false}
|
407
|
+
handleTextInputChange={jest.fn()}
|
408
|
+
conversations={initialConversations}
|
409
|
+
noResultsState={NO_RESULTS}
|
410
|
+
/>
|
411
|
+
);
|
412
|
+
expect(
|
413
|
+
screen.getByRole('dialog', {
|
414
|
+
name: /No results found Adjust your search query and try again. Check your spelling or try a more general term./i
|
415
|
+
})
|
416
|
+
).toBeTruthy();
|
417
|
+
});
|
365
418
|
});
|
@@ -112,6 +112,10 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
|
|
112
112
|
loadingState?: SkeletonProps;
|
113
113
|
/** Content to show in error state. Error state will appear once content is passed in. */
|
114
114
|
errorState?: HistoryEmptyStateProps;
|
115
|
+
/** Content to show in empty state. Empty state will appear once content is passed in. */
|
116
|
+
emptyState?: HistoryEmptyStateProps;
|
117
|
+
/** Content to show in no results state. No results state will appear once content is passed in. */
|
118
|
+
noResultsState?: HistoryEmptyStateProps;
|
115
119
|
}
|
116
120
|
|
117
121
|
export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps> = ({
|
@@ -141,6 +145,8 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
|
|
141
145
|
isLoading,
|
142
146
|
loadingState,
|
143
147
|
errorState,
|
148
|
+
emptyState,
|
149
|
+
noResultsState,
|
144
150
|
...props
|
145
151
|
}: ChatbotConversationHistoryNavProps) => {
|
146
152
|
const drawerRef = React.useRef<HTMLDivElement>(null);
|
@@ -210,6 +216,14 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
|
|
210
216
|
if (errorState) {
|
211
217
|
return <HistoryEmptyState {...errorState} />;
|
212
218
|
}
|
219
|
+
|
220
|
+
if (emptyState) {
|
221
|
+
return <HistoryEmptyState {...emptyState} />;
|
222
|
+
}
|
223
|
+
|
224
|
+
if (noResultsState) {
|
225
|
+
return <HistoryEmptyState {...noResultsState} />;
|
226
|
+
}
|
213
227
|
return (
|
214
228
|
<Menu isPlain onSelect={onSelectActiveItem} activeItemId={activeItemId} {...menuProps}>
|
215
229
|
<MenuContent>{buildMenu()}</MenuContent>
|
@@ -81,7 +81,7 @@ export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> =
|
|
81
81
|
|
82
82
|
setSpeechRecognition(recognition);
|
83
83
|
}
|
84
|
-
}, [onSpeechRecognition]);
|
84
|
+
}, [onSpeechRecognition, language, onIsListeningChange]);
|
85
85
|
|
86
86
|
if (!speechRecognition) {
|
87
87
|
return null;
|
@@ -46,7 +46,7 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
|
|
46
46
|
setAtTop(scrollTop === 0);
|
47
47
|
setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
|
48
48
|
}
|
49
|
-
}, []);
|
49
|
+
}, [messageBoxRef]);
|
50
50
|
|
51
51
|
const checkOverflow = React.useCallback(() => {
|
52
52
|
const element = messageBoxRef.current;
|
@@ -54,21 +54,21 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
|
|
54
54
|
const { scrollHeight, clientHeight } = element;
|
55
55
|
setIsOverflowing(scrollHeight >= clientHeight);
|
56
56
|
}
|
57
|
-
}, []);
|
57
|
+
}, [messageBoxRef]);
|
58
58
|
|
59
59
|
const scrollToTop = React.useCallback(() => {
|
60
60
|
const element = messageBoxRef.current;
|
61
61
|
if (element) {
|
62
62
|
element.scrollTo({ top: 0, behavior: 'smooth' });
|
63
63
|
}
|
64
|
-
}, []);
|
64
|
+
}, [messageBoxRef]);
|
65
65
|
|
66
66
|
const scrollToBottom = React.useCallback(() => {
|
67
67
|
const element = messageBoxRef.current;
|
68
68
|
if (element) {
|
69
69
|
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
70
70
|
}
|
71
|
-
}, []);
|
71
|
+
}, [messageBoxRef]);
|
72
72
|
|
73
73
|
// Detect scroll position
|
74
74
|
React.useEffect(() => {
|
@@ -85,7 +85,7 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
|
|
85
85
|
element.removeEventListener('scroll', handleScroll);
|
86
86
|
};
|
87
87
|
}
|
88
|
-
}, [checkOverflow, handleScroll]);
|
88
|
+
}, [checkOverflow, handleScroll, messageBoxRef]);
|
89
89
|
|
90
90
|
return (
|
91
91
|
<>
|