@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.
@@ -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
  });
@@ -64,7 +64,7 @@ const MicrophoneButton = (_a) => {
64
64
  };
65
65
  setSpeechRecognition(recognition);
66
66
  }
67
- }, [onSpeechRecognition]);
67
+ }, [onSpeechRecognition, language, onIsListeningChange]);
68
68
  if (!speechRecognition) {
69
69
  return null;
70
70
  }
@@ -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
  });
@@ -58,7 +58,7 @@ export const MicrophoneButton = (_a) => {
58
58
  };
59
59
  setSpeechRecognition(recognition);
60
60
  }
61
- }, [onSpeechRecognition]);
61
+ }, [onSpeechRecognition, language, onIsListeningChange]);
62
62
  if (!speechRecognition) {
63
63
  return null;
64
64
  }
@@ -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.41",
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
- let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
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
- filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
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
  );
@@ -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
- let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
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
- filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
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
 
@@ -36,71 +36,74 @@ export const CompareChild = ({ name, input, hasNewInput, setIsSendButtonDisabled
36
36
  return id.toString();
37
37
  };
38
38
 
39
- const handleSend = (input: string) => {
40
- const date = new Date();
41
- const newMessages: MessageProps[] = [];
42
- messages.forEach((message) => newMessages.push(message));
43
- newMessages.push({
44
- avatar: userAvatar,
45
- avatarProps: { isBordered: true },
46
- id: generateId(),
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
- role: 'bot',
74
- content: `API response from ${name} goes here`,
75
- name,
48
+ name: 'You',
49
+ role: 'user',
50
+ content: input,
51
+ timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`
52
+ });
53
+ newMessages.push({
76
54
  avatar: patternflyAvatar,
77
- isLoading: false,
78
- actions: {
79
- // eslint-disable-next-line no-console
80
- positive: { onClick: () => console.log('Good response') },
81
- // eslint-disable-next-line no-console
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(loadedMessages);
93
- // make announcement to assistive devices that new message has loaded
94
- setAnnouncement(`Message from ${name}: API response goes here`);
95
- setIsSendButtonDisabled(false);
96
- }, 5000);
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
  <>