@patternfly/chatbot 6.3.2 → 6.4.0-prerelease.10

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.
Files changed (133) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.d.ts +2 -0
  2. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +2 -2
  3. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.js +6 -6
  4. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +27 -4
  5. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +8 -14
  6. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +53 -2
  7. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
  8. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
  9. package/dist/cjs/ChatbotHeader/ChatbotHeaderNewChatButton.d.ts +18 -0
  10. package/dist/cjs/ChatbotHeader/ChatbotHeaderNewChatButton.js +25 -0
  11. package/dist/cjs/ChatbotHeader/ChatbotHeaderNewChatButton.test.d.ts +1 -0
  12. package/dist/cjs/ChatbotHeader/ChatbotHeaderNewChatButton.test.js +22 -0
  13. package/dist/cjs/ChatbotHeader/index.d.ts +1 -0
  14. package/dist/cjs/ChatbotHeader/index.js +1 -0
  15. package/dist/cjs/FileDropZone/FileDropZone.d.ts +1 -2
  16. package/dist/cjs/Message/Message.d.ts +9 -2
  17. package/dist/cjs/Message/Message.js +40 -34
  18. package/dist/cjs/Message/Message.test.js +37 -0
  19. package/dist/cjs/Message/MessageInput.d.ts +3 -1
  20. package/dist/cjs/Message/MessageInput.js +2 -2
  21. package/dist/cjs/MessageBar/AttachButton.d.ts +2 -2
  22. package/dist/cjs/MessageBar/MessageBar.d.ts +2 -2
  23. package/dist/cjs/MessageBox/JumpButton.d.ts +5 -0
  24. package/dist/cjs/MessageBox/JumpButton.js +1 -1
  25. package/dist/cjs/MessageBox/JumpButton.test.js +4 -4
  26. package/dist/cjs/MessageBox/MessageBox.d.ts +9 -0
  27. package/dist/cjs/MessageBox/MessageBox.js +2 -2
  28. package/dist/cjs/MessageBox/MessageBox.test.js +2 -2
  29. package/dist/cjs/MessageDivider/MessageDivider.d.ts +9 -0
  30. package/dist/cjs/MessageDivider/MessageDivider.js +23 -0
  31. package/dist/cjs/MessageDivider/MessageDivider.test.d.ts +1 -0
  32. package/dist/cjs/MessageDivider/MessageDivider.test.js +29 -0
  33. package/dist/cjs/MessageDivider/index.d.ts +2 -0
  34. package/dist/cjs/MessageDivider/index.js +23 -0
  35. package/dist/cjs/ResponseActions/ResponseActions.d.ts +1 -0
  36. package/dist/cjs/ResponseActions/ResponseActions.js +4 -4
  37. package/dist/cjs/ResponseActions/ResponseActions.test.js +6 -1
  38. package/dist/cjs/index.d.ts +2 -0
  39. package/dist/cjs/index.js +4 -1
  40. package/dist/css/main.css +103 -81
  41. package/dist/css/main.css.map +1 -1
  42. package/dist/dynamic/MessageDivider/package.json +1 -0
  43. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.d.ts +2 -0
  44. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +2 -2
  45. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.js +6 -6
  46. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +27 -4
  47. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +10 -16
  48. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +54 -3
  49. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +1 -1
  50. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.test.js +1 -1
  51. package/dist/esm/ChatbotHeader/ChatbotHeaderNewChatButton.d.ts +18 -0
  52. package/dist/esm/ChatbotHeader/ChatbotHeaderNewChatButton.js +22 -0
  53. package/dist/esm/ChatbotHeader/ChatbotHeaderNewChatButton.test.d.ts +1 -0
  54. package/dist/esm/ChatbotHeader/ChatbotHeaderNewChatButton.test.js +20 -0
  55. package/dist/esm/ChatbotHeader/index.d.ts +1 -0
  56. package/dist/esm/ChatbotHeader/index.js +1 -0
  57. package/dist/esm/FileDropZone/FileDropZone.d.ts +1 -2
  58. package/dist/esm/Message/Message.d.ts +9 -2
  59. package/dist/esm/Message/Message.js +40 -34
  60. package/dist/esm/Message/Message.test.js +37 -0
  61. package/dist/esm/Message/MessageInput.d.ts +3 -1
  62. package/dist/esm/Message/MessageInput.js +2 -2
  63. package/dist/esm/MessageBar/AttachButton.d.ts +2 -2
  64. package/dist/esm/MessageBar/MessageBar.d.ts +2 -2
  65. package/dist/esm/MessageBox/JumpButton.d.ts +5 -0
  66. package/dist/esm/MessageBox/JumpButton.js +1 -1
  67. package/dist/esm/MessageBox/JumpButton.test.js +4 -4
  68. package/dist/esm/MessageBox/MessageBox.d.ts +9 -0
  69. package/dist/esm/MessageBox/MessageBox.js +2 -2
  70. package/dist/esm/MessageBox/MessageBox.test.js +2 -2
  71. package/dist/esm/MessageDivider/MessageDivider.d.ts +9 -0
  72. package/dist/esm/MessageDivider/MessageDivider.js +21 -0
  73. package/dist/esm/MessageDivider/MessageDivider.test.d.ts +1 -0
  74. package/dist/esm/MessageDivider/MessageDivider.test.js +24 -0
  75. package/dist/esm/MessageDivider/index.d.ts +2 -0
  76. package/dist/esm/MessageDivider/index.js +2 -0
  77. package/dist/esm/ResponseActions/ResponseActions.d.ts +1 -0
  78. package/dist/esm/ResponseActions/ResponseActions.js +5 -5
  79. package/dist/esm/ResponseActions/ResponseActions.test.js +6 -1
  80. package/dist/esm/index.d.ts +2 -0
  81. package/dist/esm/index.js +2 -0
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +9 -4
  84. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDividers.tsx +24 -0
  85. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +15 -1
  86. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +39 -7
  87. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotConversationEditing.tsx +202 -0
  88. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderBasic.tsx +17 -3
  89. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +45 -5
  90. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithPin.tsx +206 -0
  91. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +30 -4
  92. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +33 -1
  93. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotDisplayMode.tsx +486 -0
  94. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotTranscripts.tsx +565 -0
  95. package/src/Chatbot/Chatbot.scss +1 -1
  96. package/src/ChatbotContent/ChatbotContent.scss +1 -1
  97. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx +6 -6
  98. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx +5 -2
  99. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +70 -32
  100. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +176 -3
  101. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +110 -60
  102. package/src/ChatbotFooter/ChatbotFooter.scss +1 -1
  103. package/src/ChatbotHeader/ChatbotHeader.scss +3 -3
  104. package/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx +1 -1
  105. package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +2 -2
  106. package/src/ChatbotHeader/ChatbotHeaderNewChatButton.test.tsx +25 -0
  107. package/src/ChatbotHeader/ChatbotHeaderNewChatButton.tsx +64 -0
  108. package/src/ChatbotHeader/index.ts +1 -0
  109. package/src/ChatbotModal/ChatbotModal.scss +1 -1
  110. package/src/ChatbotToggle/ChatbotToggle.scss +2 -2
  111. package/src/FileDetails/__snapshots__/FileDetails.test.tsx.snap +6 -9
  112. package/src/FileDetailsLabel/__snapshots__/FileDetailsLabel.test.tsx.snap +6 -9
  113. package/src/FileDropZone/FileDropZone.tsx +2 -2
  114. package/src/Message/Message.scss +9 -7
  115. package/src/Message/Message.test.tsx +54 -0
  116. package/src/Message/Message.tsx +70 -50
  117. package/src/Message/MessageInput.tsx +5 -1
  118. package/src/MessageBar/AttachButton.tsx +2 -2
  119. package/src/MessageBar/MessageBar.tsx +2 -2
  120. package/src/MessageBar/SendButton.scss +3 -3
  121. package/src/MessageBox/JumpButton.scss +1 -1
  122. package/src/MessageBox/JumpButton.test.tsx +4 -4
  123. package/src/MessageBox/JumpButton.tsx +20 -4
  124. package/src/MessageBox/MessageBox.test.tsx +2 -2
  125. package/src/MessageBox/MessageBox.tsx +23 -2
  126. package/src/MessageDivider/MessageDivider.scss +45 -0
  127. package/src/MessageDivider/MessageDivider.test.tsx +24 -0
  128. package/src/MessageDivider/MessageDivider.tsx +35 -0
  129. package/src/MessageDivider/index.ts +3 -0
  130. package/src/ResponseActions/ResponseActions.test.tsx +6 -1
  131. package/src/ResponseActions/ResponseActions.tsx +24 -3
  132. package/src/index.ts +3 -0
  133. package/src/main.scss +1 -52
@@ -0,0 +1,202 @@
1
+ // From Cursor, with aid
2
+ import React, { FunctionComponent, useState, useRef, useEffect } from 'react';
3
+ import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
4
+ import ChatbotConversationHistoryNav, {
5
+ Conversation
6
+ } from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
7
+ import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal';
8
+ import {
9
+ Checkbox,
10
+ DropdownItem,
11
+ DropdownList,
12
+ Button,
13
+ TextInput,
14
+ Form,
15
+ FormGroup,
16
+ ModalHeader,
17
+ ModalBody,
18
+ ModalFooter
19
+ } from '@patternfly/react-core';
20
+
21
+ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
22
+ const [isDrawerOpen, setIsDrawerOpen] = useState(true);
23
+ const displayMode = ChatbotDisplayMode.embedded;
24
+
25
+ // Modal state
26
+ const [isModalOpen, setIsModalOpen] = useState(false);
27
+ const [editingConversationId, setEditingConversationId] = useState<string | number | null>(null);
28
+ const [editingText, setEditingText] = useState('');
29
+
30
+ // Ref for the text input
31
+ const textInputRef = useRef<HTMLInputElement>(null);
32
+
33
+ // Focus the text input when modal opens
34
+ useEffect(() => {
35
+ if (isModalOpen && textInputRef.current) {
36
+ textInputRef.current.focus();
37
+ // Move cursor to the end of the text
38
+ const length = textInputRef.current.value.length;
39
+ textInputRef.current.setSelectionRange(length, length);
40
+ }
41
+ }, [isModalOpen]);
42
+
43
+ const findConversationAndGroup = (conversations: { [key: string]: Conversation[] }, itemId: string | number) => {
44
+ for (const [groupKey, conversationList] of Object.entries(conversations)) {
45
+ const conversationIndex = conversationList.findIndex((conv) => conv.id === itemId);
46
+ if (conversationIndex !== -1) {
47
+ return { groupKey, conversationIndex, conversation: conversationList[conversationIndex] };
48
+ }
49
+ }
50
+ return null;
51
+ };
52
+
53
+ const onRenameClick = (itemId: string | number) => {
54
+ const result = findConversationAndGroup(conversations, itemId);
55
+ if (result) {
56
+ setEditingConversationId(itemId);
57
+ setEditingText(result.conversation.text);
58
+ setIsModalOpen(true);
59
+ }
60
+ };
61
+
62
+ const handleModalSave = () => {
63
+ if (editingConversationId) {
64
+ setConversations((prevConversations) => {
65
+ const result = findConversationAndGroup(prevConversations, editingConversationId);
66
+ if (!result) {
67
+ return prevConversations;
68
+ }
69
+
70
+ const { groupKey, conversationIndex } = result;
71
+ const newConversations = { ...prevConversations };
72
+ const newGroup = [...newConversations[groupKey]];
73
+
74
+ newGroup[conversationIndex] = { ...newGroup[conversationIndex], text: editingText };
75
+ newConversations[groupKey] = newGroup;
76
+
77
+ return newConversations;
78
+ });
79
+ }
80
+ handleModalClose();
81
+ };
82
+
83
+ const handleModalCancel = () => {
84
+ handleModalClose();
85
+ };
86
+
87
+ const handleModalClose = () => {
88
+ setIsModalOpen(false);
89
+ setEditingConversationId(null);
90
+ setEditingText('');
91
+ };
92
+
93
+ const handleTextInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
94
+ if (event.key === 'Enter') {
95
+ event.preventDefault();
96
+ handleModalSave();
97
+ }
98
+ };
99
+
100
+ const renderMenuItems = (itemId: string | number) => [
101
+ <DropdownList key={`list-${itemId}`}>
102
+ <DropdownItem value="Download" id="Download">
103
+ Download
104
+ </DropdownItem>
105
+ <DropdownItem value="Rename" id="Rename" onClick={() => onRenameClick(itemId)}>
106
+ Rename
107
+ </DropdownItem>
108
+ <DropdownItem value="Archive" id="Archive">
109
+ Archive
110
+ </DropdownItem>
111
+ <DropdownItem value="Delete" id="Delete">
112
+ Delete
113
+ </DropdownItem>
114
+ </DropdownList>
115
+ ];
116
+
117
+ const initialConversations: { [key: string]: Conversation[] } = {
118
+ Today: [{ id: '1', text: 'Red Hat products and services' }],
119
+ 'This month': [
120
+ {
121
+ id: '2',
122
+ text: 'Enterprise Linux installation and setup'
123
+ },
124
+ { id: '3', text: 'Troubleshoot system crash' }
125
+ ],
126
+ March: [
127
+ { id: '4', text: 'Ansible security and updates' },
128
+ { id: '5', text: 'Red Hat certification' },
129
+ { id: '6', text: 'Lightspeed user documentation' }
130
+ ],
131
+ February: [
132
+ { id: '7', text: 'Crashing pod assistance' },
133
+ { id: '8', text: 'OpenShift AI pipelines' },
134
+ { id: '9', text: 'Updating subscription plan' },
135
+ { id: '10', text: 'Red Hat licensing options' }
136
+ ],
137
+ January: [
138
+ { id: '11', text: 'RHEL system performance' },
139
+ { id: '12', text: 'Manage user accounts' }
140
+ ]
141
+ };
142
+
143
+ const [conversations, setConversations] = useState(initialConversations);
144
+
145
+ // Create conversations with menu items dynamically
146
+ const conversationsWithMenuItems = () => {
147
+ const newConversations = { ...conversations };
148
+ Object.keys(newConversations).forEach((groupKey) => {
149
+ newConversations[groupKey] = newConversations[groupKey].map((conv) => ({
150
+ ...conv,
151
+ menuItems: renderMenuItems(conv.id)
152
+ }));
153
+ });
154
+ return newConversations;
155
+ };
156
+
157
+ return (
158
+ <>
159
+ <Checkbox
160
+ label="Display drawer"
161
+ isChecked={isDrawerOpen}
162
+ onChange={() => setIsDrawerOpen(!isDrawerOpen)}
163
+ id="drawer-actions-visible"
164
+ name="drawer-actions-visible"
165
+ ></Checkbox>
166
+ <ChatbotConversationHistoryNav
167
+ displayMode={displayMode}
168
+ onDrawerToggle={() => setIsDrawerOpen(!isDrawerOpen)}
169
+ isDrawerOpen={isDrawerOpen}
170
+ setIsDrawerOpen={setIsDrawerOpen}
171
+ conversations={conversationsWithMenuItems()}
172
+ drawerContent={<div>Drawer content</div>}
173
+ />
174
+
175
+ <ChatbotModal displayMode={displayMode} isOpen={isModalOpen} onClose={handleModalClose}>
176
+ <ModalHeader title="Rename Conversation" />
177
+ <ModalBody>
178
+ <Form>
179
+ <FormGroup label="Conversation Name" fieldId="conversation-name" isRequired>
180
+ <TextInput
181
+ isRequired
182
+ ref={textInputRef}
183
+ value={editingText}
184
+ onChange={(_, value) => setEditingText(value)}
185
+ onKeyDown={handleTextInputKeyDown}
186
+ id="conversation-name"
187
+ />
188
+ </FormGroup>
189
+ </Form>
190
+ </ModalBody>
191
+ <ModalFooter>
192
+ <Button key="save" variant="primary" onClick={handleModalSave}>
193
+ Save
194
+ </Button>
195
+ <Button key="cancel" variant="link" onClick={handleModalCancel}>
196
+ Cancel
197
+ </Button>
198
+ </ModalFooter>
199
+ </ChatbotModal>
200
+ </>
201
+ );
202
+ };
@@ -17,7 +17,8 @@ import {
17
17
  ChatbotHeaderActions,
18
18
  ChatbotHeaderTitle,
19
19
  ChatbotHeaderOptionsDropdown,
20
- ChatbotHeaderSelectorDropdown
20
+ ChatbotHeaderSelectorDropdown,
21
+ ChatbotHeaderNewChatButton
21
22
  } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
22
23
  import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
23
24
  import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
@@ -31,6 +32,7 @@ export const BasicDemo: FunctionComponent = () => {
31
32
  const [selectedModel, setSelectedModel] = useState('Granite Code 7B');
32
33
  const [showAll, setShowAll] = useState<boolean>(true);
33
34
  const [showMenu, setShowMenu] = useState<boolean>(true);
35
+ const [showNewChatButton, setShowNewChatButton] = useState<boolean>(true);
34
36
  const [showLogo, setShowLogo] = useState<boolean>(false);
35
37
  const [showCenteredLogo, setShowCenteredLogo] = useState<boolean>(true);
36
38
  const [showSelectorDropdown, setShowSelectorDropdown] = useState<boolean>(true);
@@ -58,9 +60,10 @@ export const BasicDemo: FunctionComponent = () => {
58
60
  <Stack hasGutter>
59
61
  <FormGroup role="radiogroup" isInline fieldId="header-variant-form-radio-group" label="Variant">
60
62
  <Checkbox
61
- isChecked={showMenu && showCenteredLogo && showSelectorDropdown && showOptionsDropdown}
63
+ isChecked={showMenu && showNewChatButton && showCenteredLogo && showSelectorDropdown && showOptionsDropdown}
62
64
  onChange={() => {
63
65
  setShowMenu(true);
66
+ setShowNewChatButton(true);
64
67
  setShowCenteredLogo(true);
65
68
  setShowSelectorDropdown(true);
66
69
  setShowOptionsDropdown(true);
@@ -80,6 +83,16 @@ export const BasicDemo: FunctionComponent = () => {
80
83
  label="With menu"
81
84
  id="menu"
82
85
  />
86
+ <Checkbox
87
+ isChecked={showNewChatButton}
88
+ onChange={() => {
89
+ setShowNewChatButton(!showNewChatButton);
90
+ showAll && setShowAll(!showAll);
91
+ }}
92
+ name="basic-inline-radio"
93
+ label="With new chat button"
94
+ id="new-chat-button"
95
+ />
83
96
  <Checkbox
84
97
  isChecked={showLogo}
85
98
  onChange={() => {
@@ -124,9 +137,10 @@ export const BasicDemo: FunctionComponent = () => {
124
137
  </FormGroup>
125
138
 
126
139
  <ChatbotHeader>
127
- {(showMenu || showLogo || showCenteredLogo) && (
140
+ {(showMenu || showNewChatButton || showLogo || showCenteredLogo) && (
128
141
  <ChatbotHeaderMain>
129
142
  {showMenu && <ChatbotHeaderMenu onMenuToggle={() => alert('Menu toggle clicked')} />}
143
+ {showNewChatButton && <ChatbotHeaderNewChatButton onClick={() => alert('New chat button clicked')} />}
130
144
  {(showLogo || showCenteredLogo) && (
131
145
  <ChatbotHeaderTitle>{showCenteredLogo ? <Bullseye>{title}</Bullseye> : title}</ChatbotHeaderTitle>
132
146
  )}
@@ -1,4 +1,4 @@
1
- import { useState, FunctionComponent } from 'react';
1
+ import { useState, useEffect, useRef, FunctionComponent } from 'react';
2
2
  import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
3
3
  import ChatbotConversationHistoryNav, {
4
4
  Conversation
@@ -62,6 +62,7 @@ const EMPTY_STATE = {
62
62
  export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
63
63
  const [isOpen, setIsOpen] = useState(true);
64
64
  const [isButtonOrderReversed, setIsButtonOrderReversed] = useState(false);
65
+ const [isNewChatButtonDisabled, setIsNewChatButtonDisabled] = useState(false);
65
66
  const [isCompact, setIsCompact] = useState(false);
66
67
  const [conversations, setConversations] = useState<Conversation[] | { [key: string]: Conversation[] }>(
67
68
  initialConversations
@@ -70,8 +71,28 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
70
71
  const [hasError, setHasError] = useState(false);
71
72
  const [isEmpty, setIsEmpty] = useState(false);
72
73
  const [hasNoResults, setHasNoResults] = useState(false);
74
+ const [announcement, setAnnouncement] = useState('');
75
+ const [debouncedAnnouncement, setDebouncedAnnouncement] = useState('');
76
+ const announcementTimeoutRef = useRef<NodeJS.Timeout>();
73
77
  const displayMode = ChatbotDisplayMode.embedded;
74
78
 
79
+ // Debounce announcement updates to prevent screen reader overload
80
+ useEffect(() => {
81
+ if (announcementTimeoutRef.current) {
82
+ clearTimeout(announcementTimeoutRef.current);
83
+ }
84
+
85
+ announcementTimeoutRef.current = setTimeout(() => {
86
+ setDebouncedAnnouncement(announcement);
87
+ }, 500);
88
+
89
+ return () => {
90
+ if (announcementTimeoutRef.current) {
91
+ clearTimeout(announcementTimeoutRef.current);
92
+ }
93
+ };
94
+ }, [announcement]);
95
+
75
96
  const findMatchingItems = (targetValue: string) => {
76
97
  const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
77
98
  const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
@@ -109,6 +130,13 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
109
130
  id="drawer-actions-visible"
110
131
  name="drawer-actions-visible"
111
132
  ></Checkbox>
133
+ <Checkbox
134
+ label="Disable new chat button"
135
+ isChecked={isNewChatButtonDisabled}
136
+ onChange={() => setIsNewChatButtonDisabled(!isNewChatButtonDisabled)}
137
+ id="drawer-actions-disabled"
138
+ name="drawer-actions-disabled"
139
+ ></Checkbox>
112
140
  <Checkbox
113
141
  label="Show loading state"
114
142
  isChecked={isLoading}
@@ -152,6 +180,7 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
152
180
  // eslint-disable-next-line no-console
153
181
  onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)}
154
182
  conversations={conversations}
183
+ newChatButtonProps={{ isDisabled: isNewChatButtonDisabled }}
155
184
  onNewChat={() => {
156
185
  setIsOpen(!isOpen);
157
186
  }}
@@ -159,12 +188,23 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
159
188
  handleTextInputChange={(value: string) => {
160
189
  if (value === '') {
161
190
  setConversations(initialConversations);
191
+ setAnnouncement('');
192
+ setDebouncedAnnouncement('');
193
+ setHasNoResults(false);
194
+ } else {
195
+ // this is where you would perform search on the items in the drawer
196
+ // and update the state
197
+ const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
198
+ const totalCount = Object.values(newConversations).flat().length;
199
+ const newAnnouncement =
200
+ totalCount === 1
201
+ ? `${totalCount} conversation matches "${value}"`
202
+ : `${totalCount} conversations match "${value}"`;
203
+ setAnnouncement(newAnnouncement);
204
+ setConversations(newConversations);
162
205
  }
163
- // this is where you would perform search on the items in the drawer
164
- // and update the state
165
- const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
166
- setConversations(newConversations);
167
206
  }}
207
+ searchInputScreenReaderText={debouncedAnnouncement}
168
208
  drawerContent={<div>Drawer content</div>}
169
209
  isLoading={isLoading}
170
210
  errorState={hasError ? ERROR : undefined}
@@ -0,0 +1,206 @@
1
+ // Assisted by: Cursor
2
+ import React, { FunctionComponent, useState } from 'react';
3
+ import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
4
+ import ChatbotConversationHistoryNav, {
5
+ Conversation
6
+ } from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
7
+ import { Checkbox, DropdownItem, DropdownList } from '@patternfly/react-core';
8
+ import { ThumbtackIcon } from '@patternfly/react-icons';
9
+
10
+ // Sample conversations
11
+ const initialConversations: { [key: string]: Conversation[] } = {
12
+ Today: [
13
+ {
14
+ id: '1',
15
+ text: 'Red Hat products and services',
16
+ label: 'Conversation options for "Red Hat products and services"'
17
+ }
18
+ ],
19
+ 'This month': [
20
+ {
21
+ id: '2',
22
+ text: 'Enterprise Linux installation and setup',
23
+ label: 'Conversation options for "Enterprise Linux installation and setup"'
24
+ },
25
+ {
26
+ id: '3',
27
+ text: 'Troubleshoot system crash',
28
+ label: 'Conversation options for "Troubleshoot system crash"'
29
+ }
30
+ ],
31
+ March: [
32
+ {
33
+ id: '4',
34
+ text: 'Ansible security and updates',
35
+ label: 'Conversation options for "Ansible security and updates"'
36
+ },
37
+ {
38
+ id: '5',
39
+ text: 'Red Hat certification',
40
+ label: 'Conversation options for "Red Hat certification"'
41
+ },
42
+ {
43
+ id: '6',
44
+ text: 'Lightspeed user documentation',
45
+ label: 'Conversation options for "Lightspeed user documentation"'
46
+ }
47
+ ],
48
+ February: [
49
+ {
50
+ id: '7',
51
+ text: 'Crashing pod assistance',
52
+ label: 'Conversation options for "Crashing pod assistance"'
53
+ },
54
+ {
55
+ id: '8',
56
+ text: 'OpenShift AI pipelines',
57
+ label: 'Conversation options for "OpenShift AI pipelines"'
58
+ },
59
+ {
60
+ id: '9',
61
+ text: 'Updating subscription plan',
62
+ label: 'Conversation options for "Updating subscription plan"'
63
+ },
64
+ {
65
+ id: '10',
66
+ text: 'Red Hat licensing options',
67
+ label: 'Conversation options for "Red Hat licensing options"'
68
+ }
69
+ ],
70
+ January: [
71
+ {
72
+ id: '11',
73
+ text: 'RHEL system performance',
74
+ label: 'Conversation options for "RHEL system performance"'
75
+ },
76
+ {
77
+ id: '12',
78
+ text: 'Manage user accounts',
79
+ label: 'Conversation options for "Manage user accounts"'
80
+ }
81
+ ]
82
+ };
83
+
84
+ export const ChatbotHeaderPinDemo: FunctionComponent = () => {
85
+ const [isDrawerOpen, setIsDrawerOpen] = useState(true);
86
+ const [isCompact, setIsCompact] = useState(false);
87
+ const [pinnedConversations, setPinnedConversations] = useState<Set<string>>(new Set());
88
+ const displayMode = ChatbotDisplayMode.embedded;
89
+
90
+ const handlePinToggle = (conversationId: string) => {
91
+ setPinnedConversations((prev) => {
92
+ const newPinned = new Set(prev);
93
+ if (newPinned.has(conversationId)) {
94
+ newPinned.delete(conversationId);
95
+ } else {
96
+ newPinned.add(conversationId);
97
+ }
98
+ return newPinned;
99
+ });
100
+
101
+ // Focus the conversation input after pin/unpin action
102
+ setTimeout(() => {
103
+ const dropdown = document.getElementById(`pin-demo-${conversationId}-dropdown`);
104
+ if (dropdown) {
105
+ dropdown.focus();
106
+ }
107
+ }, 100);
108
+ };
109
+
110
+ const createMenuItems = (conversationId: string) => {
111
+ const isPinned = pinnedConversations.has(conversationId);
112
+
113
+ return [
114
+ <DropdownList key={`${conversationId}-menu`} aria-label="Conversation options">
115
+ <DropdownItem
116
+ value={isPinned ? 'Unpin' : 'Pin'}
117
+ id={isPinned ? 'Unpin' : 'Pin'}
118
+ onClick={() => handlePinToggle(conversationId)}
119
+ >
120
+ {isPinned ? 'Unpin' : 'Pin'}
121
+ </DropdownItem>
122
+ <DropdownItem value="Delete" id="Delete">
123
+ Delete
124
+ </DropdownItem>
125
+ <DropdownItem value="Rename" id="Rename">
126
+ Rename
127
+ </DropdownItem>
128
+ <DropdownItem value="Archive" id="Archive">
129
+ Archive
130
+ </DropdownItem>
131
+ </DropdownList>
132
+ ];
133
+ };
134
+
135
+ // Reorganize conversations to show pinned ones at the top
136
+ const organizeConversations = () => {
137
+ const organized: { [key: string]: Conversation[] } = {};
138
+ const pinnedItems: Conversation[] = [];
139
+
140
+ // Collect all pinned conversations first
141
+ Object.entries(initialConversations).forEach(([_period, conversations]) => {
142
+ conversations.forEach((conv) => {
143
+ if (pinnedConversations.has(conv.id)) {
144
+ pinnedItems.push({
145
+ ...conv,
146
+ menuItems: createMenuItems(conv.id),
147
+ icon: <ThumbtackIcon />,
148
+ dropdownId: `pin-demo-${conv.id}-dropdown`
149
+ });
150
+ }
151
+ });
152
+ });
153
+
154
+ // Add pinned section if there are pinned items
155
+ if (pinnedItems.length > 0) {
156
+ organized.Pinned = pinnedItems;
157
+ }
158
+
159
+ // Add unpinned conversations
160
+ Object.entries(initialConversations).forEach(([period, conversations]) => {
161
+ const unpinnedConversations = conversations
162
+ .filter((conv) => !pinnedConversations.has(conv.id))
163
+ .map((conv) => ({
164
+ ...conv,
165
+ menuItems: createMenuItems(conv.id),
166
+ dropdownId: `pin-demo-${conv.id}-dropdown`
167
+ }));
168
+
169
+ if (unpinnedConversations.length > 0) {
170
+ organized[period] = unpinnedConversations;
171
+ }
172
+ });
173
+
174
+ return organized;
175
+ };
176
+
177
+ const conversations = organizeConversations();
178
+
179
+ return (
180
+ <>
181
+ <Checkbox
182
+ label="Display drawer"
183
+ isChecked={isDrawerOpen}
184
+ onChange={() => setIsDrawerOpen(!isDrawerOpen)}
185
+ id="drawer-pin-visible"
186
+ name="drawer-pin-visible"
187
+ ></Checkbox>
188
+ <Checkbox
189
+ label="Show compact version"
190
+ isChecked={isCompact}
191
+ onChange={() => setIsCompact(!isCompact)}
192
+ id="drawer-pin-compact"
193
+ name="drawer-pin-compact"
194
+ ></Checkbox>
195
+ <ChatbotConversationHistoryNav
196
+ displayMode={displayMode}
197
+ onDrawerToggle={() => setIsDrawerOpen(!isDrawerOpen)}
198
+ isDrawerOpen={isDrawerOpen}
199
+ setIsDrawerOpen={setIsDrawerOpen}
200
+ conversations={conversations}
201
+ drawerContent={<div>Drawer content</div>}
202
+ isCompact={isCompact}
203
+ />
204
+ </>
205
+ );
206
+ };
@@ -61,14 +61,15 @@ ChatbotHeaderMenu,
61
61
  ChatbotHeaderActions,
62
62
  ChatbotHeaderTitle,
63
63
  ChatbotHeaderOptionsDropdown,
64
- ChatbotHeaderSelectorDropdown
64
+ ChatbotHeaderSelectorDropdown,
65
+ ChatbotHeaderNewChatButton
65
66
  } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
66
67
  import { ChatbotFooter, ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter';
67
68
  import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar';
68
69
  import SourceDetailsMenuItem from '@patternfly/chatbot/dist/dynamic/SourceDetailsMenuItem';
69
70
  import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal';
70
71
  import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings';
71
- import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, UploadIcon } from '@patternfly/react-icons';
72
+ import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons';
72
73
  import { useDropzone } from 'react-dropzone';
73
74
 
74
75
  import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
@@ -78,13 +79,14 @@ import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/ou
78
79
  import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
79
80
  import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
80
81
  import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
82
+ import PenToSquareIcon from '@patternfly/react-icons/dist/esm/icons/pen-to-square-icon';
81
83
  import PFHorizontalLogoColor from './PF-HorizontalLogo-Color.svg';
82
84
  import PFHorizontalLogoReverse from './PF-HorizontalLogo-Reverse.svg';
83
85
  import userAvatar from '../Messages/user_avatar.svg';
84
86
  import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
85
87
  import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';
86
88
  import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons';
87
- import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties} from 'react';
89
+ import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties, useEffect} from 'react';
88
90
 
89
91
  ## Structure
90
92
 
@@ -184,9 +186,10 @@ The ChatBot header is persistent, and contains the title for the ChatBot window,
184
186
 
185
187
  The `<ChatbotHeader>` has 2 sections:
186
188
 
187
- - `<ChatbotHeaderMain>` contains the title and an optional menu toggle:
189
+ - `<ChatbotHeaderMain>` contains the title and an optional menu toggle or new chat button:
188
190
  - `<ChatbotHeaderTitle>` handles the layout and display of a title or image at different responsive sizes.
189
191
  - `<ChatbotHeaderMenu>` (optional) is placed on the left side of the header and used to toggle a chat history menu.
192
+ - `<ChatbotHeaderNewChatButton>` (optional) is placed on the left side of the header and used to initiate a new chat.
190
193
  - `<ChatbotHeaderActions>` contains any additional controls:
191
194
  - The `<ChatbotHeaderSelectorDropdown>` component is a standard PatternFly dropdown that matches the ChatBot styles.
192
195
  - The `<ChatbotHeaderOptionsDropdown>` component is a dropdown with a menu toggle that is intended to be used to update ChatBot settings (like the display mode).
@@ -197,6 +200,7 @@ Your `<ChatbotHeader>` code structure should look like this:
197
200
  <ChatbotHeader>
198
201
  <ChatbotHeaderMain>
199
202
  <ChatbotHeaderMenu ... />
203
+ <ChatbotHeaderNewChatButton ... />
200
204
  <ChatbotHeaderTitle ... />
201
205
  </ChatbotHeaderMain>
202
206
  <ChatbotHeaderActions>
@@ -221,6 +225,7 @@ There are a variety of options and customizations you can make to the header, to
221
225
  In this example, select the respective checkbox to toggle these features:
222
226
 
223
227
  - **Menu:** Users can select the menu toggle to open a menu of additional options or actions.
228
+ - **New chat button:** Used to start a new chat session. The header button can be used in addition to or in place of a new chat button within the [conversation history drawer](/patternfly-ai/chatbot/ui/#drawer-with-search-and-new-chat-button).
224
229
  - **Left-aligned logo**
225
230
  - **Centered logo**
226
231
  - **Selector dropdown:** Users can choose from preselected options in a dropdown menu. For example, they can toggle between AI models.
@@ -361,6 +366,27 @@ Actions can be added to conversations with `menuItems`. Optionally, you can also
361
366
 
362
367
  ```
363
368
 
369
+ ### Pinning conversations
370
+
371
+ To help users track important conversations, add a "pin" option to the conversation action menus. This action moves a conversation to a dedicated "pinned" section at the top of the history drawer for quick access. Pinned items should contain an "unpin" option, so that users can remove pinned conversations as needed.
372
+
373
+ ```js file="./ChatbotHeaderDrawerWithPin.tsx"
374
+
375
+ ```
376
+
377
+ ### Renaming conversations in history drawer
378
+
379
+ You can allow users to rename a conversation in the history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
380
+
381
+ - When the modal opens, focus is placed at the end of the text input.
382
+ - When the modal closes, focus goes back to the action toggle that was previously opened.
383
+ - Changes can be canceled via the **<kbd>Escape</kbd>** key or clicking a "Cancel" button.
384
+ - Changes can be saved via the **<kbd>Enter</kbd>** key or by clicking a "Save" button.
385
+
386
+ ```js file="./ChatbotConversationEditing.tsx"
387
+
388
+ ```
389
+
364
390
  ### Drawer with active conversation
365
391
 
366
392
  If you're showing a conversation that is already active, you can set the `activeItemId` prop on your `<ChatbotConversationHistoryNav>` to apply an active visual state.