@patternfly/chatbot 6.5.0-prerelease.2 → 6.5.0-prerelease.20

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 (152) hide show
  1. package/dist/cjs/AttachMenu/AttachMenu.d.ts +8 -2
  2. package/dist/cjs/AttachMenu/AttachMenu.js +2 -2
  3. package/dist/cjs/ChatbotFooter/ChatbotFooter.d.ts +5 -2
  4. package/dist/cjs/ChatbotFooter/ChatbotFooter.js +2 -2
  5. package/dist/cjs/ChatbotFooter/ChatbotFooter.test.js +5 -1
  6. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +29 -2
  7. package/dist/cjs/CodeModal/CodeModal.d.ts +2 -0
  8. package/dist/cjs/CodeModal/CodeModal.js +53 -8
  9. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.d.ts +2 -1
  10. package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
  11. package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  12. package/dist/cjs/Message/Message.d.ts +7 -1
  13. package/dist/cjs/Message/Message.js +11 -8
  14. package/dist/cjs/Message/Message.test.js +36 -0
  15. package/dist/cjs/Message/MessageLoading.d.ts +2 -1
  16. package/dist/cjs/Message/MessageLoading.js +1 -1
  17. package/dist/cjs/Message/TableMessage/TableMessage.d.ts +4 -1
  18. package/dist/cjs/Message/TableMessage/TableMessage.js +2 -2
  19. package/dist/cjs/Message/TextMessage/TextMessage.d.ts +4 -1
  20. package/dist/cjs/Message/TextMessage/TextMessage.js +2 -2
  21. package/dist/cjs/MessageBar/AttachButton.d.ts +2 -0
  22. package/dist/cjs/MessageBar/AttachButton.js +2 -2
  23. package/dist/cjs/MessageBar/AttachButton.test.js +4 -0
  24. package/dist/cjs/MessageBar/MessageBar.d.ts +16 -6
  25. package/dist/cjs/MessageBar/MessageBar.js +6 -5
  26. package/dist/cjs/MessageBar/MessageBar.test.js +62 -0
  27. package/dist/cjs/Onboarding/Onboarding.d.ts +36 -0
  28. package/dist/cjs/Onboarding/Onboarding.js +37 -0
  29. package/dist/cjs/Onboarding/Onboarding.test.d.ts +1 -0
  30. package/dist/cjs/Onboarding/Onboarding.test.js +80 -0
  31. package/dist/cjs/Onboarding/index.d.ts +2 -0
  32. package/dist/cjs/Onboarding/index.js +23 -0
  33. package/dist/cjs/ResponseActions/ResponseActions.d.ts +3 -0
  34. package/dist/cjs/ResponseActions/ResponseActions.js +28 -7
  35. package/dist/cjs/ResponseActions/ResponseActions.test.js +67 -12
  36. package/dist/cjs/__mocks__/monaco-editor.d.ts +11 -0
  37. package/dist/cjs/__mocks__/monaco-editor.js +18 -0
  38. package/dist/cjs/__mocks__/rehype-highlight.d.ts +2 -0
  39. package/dist/cjs/__mocks__/rehype-highlight.js +4 -0
  40. package/dist/cjs/index.d.ts +2 -0
  41. package/dist/cjs/index.js +4 -1
  42. package/dist/css/main.css +219 -21
  43. package/dist/css/main.css.map +1 -1
  44. package/dist/dynamic/Onboarding/package.json +1 -0
  45. package/dist/esm/AttachMenu/AttachMenu.d.ts +8 -2
  46. package/dist/esm/AttachMenu/AttachMenu.js +2 -2
  47. package/dist/esm/ChatbotFooter/ChatbotFooter.d.ts +5 -2
  48. package/dist/esm/ChatbotFooter/ChatbotFooter.js +2 -2
  49. package/dist/esm/ChatbotFooter/ChatbotFooter.test.js +5 -1
  50. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +30 -3
  51. package/dist/esm/CodeModal/CodeModal.d.ts +2 -0
  52. package/dist/esm/CodeModal/CodeModal.js +54 -9
  53. package/dist/esm/FileDetailsLabel/FileDetailsLabel.d.ts +2 -1
  54. package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
  55. package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  56. package/dist/esm/Message/Message.d.ts +7 -1
  57. package/dist/esm/Message/Message.js +11 -8
  58. package/dist/esm/Message/Message.test.js +36 -0
  59. package/dist/esm/Message/MessageLoading.d.ts +2 -1
  60. package/dist/esm/Message/MessageLoading.js +1 -1
  61. package/dist/esm/Message/TableMessage/TableMessage.d.ts +4 -1
  62. package/dist/esm/Message/TableMessage/TableMessage.js +2 -2
  63. package/dist/esm/Message/TextMessage/TextMessage.d.ts +4 -1
  64. package/dist/esm/Message/TextMessage/TextMessage.js +2 -2
  65. package/dist/esm/MessageBar/AttachButton.d.ts +2 -0
  66. package/dist/esm/MessageBar/AttachButton.js +2 -2
  67. package/dist/esm/MessageBar/AttachButton.test.js +4 -0
  68. package/dist/esm/MessageBar/MessageBar.d.ts +16 -6
  69. package/dist/esm/MessageBar/MessageBar.js +6 -5
  70. package/dist/esm/MessageBar/MessageBar.test.js +62 -0
  71. package/dist/esm/Onboarding/Onboarding.d.ts +36 -0
  72. package/dist/esm/Onboarding/Onboarding.js +30 -0
  73. package/dist/esm/Onboarding/Onboarding.test.d.ts +1 -0
  74. package/dist/esm/Onboarding/Onboarding.test.js +75 -0
  75. package/dist/esm/Onboarding/index.d.ts +2 -0
  76. package/dist/esm/Onboarding/index.js +2 -0
  77. package/dist/esm/ResponseActions/ResponseActions.d.ts +3 -0
  78. package/dist/esm/ResponseActions/ResponseActions.js +28 -7
  79. package/dist/esm/ResponseActions/ResponseActions.test.js +67 -12
  80. package/dist/esm/__mocks__/monaco-editor.d.ts +11 -0
  81. package/dist/esm/__mocks__/monaco-editor.js +18 -0
  82. package/dist/esm/__mocks__/rehype-highlight.d.ts +2 -0
  83. package/dist/esm/__mocks__/rehype-highlight.js +2 -0
  84. package/dist/esm/index.d.ts +2 -0
  85. package/dist/esm/index.js +2 -0
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +14 -2
  88. package/patternfly-docs/content/extensions/chatbot/chatbot.md +57 -0
  89. package/patternfly-docs/content/extensions/chatbot/design-guidelines.md +12 -12
  90. package/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +1 -1
  91. package/patternfly-docs/content/extensions/chatbot/examples/Customizing Messages/Customizing Messages.md +1 -1
  92. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +1 -0
  93. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithPersistedActions.tsx +22 -0
  94. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithQuickResponses.tsx +11 -0
  95. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +18 -4
  96. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +1 -0
  97. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessageWithExtraContent.tsx +3 -1
  98. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarIndicatorThinking.tsx +15 -0
  99. package/patternfly-docs/content/extensions/chatbot/examples/UI/CompactOnboarding.tsx +141 -0
  100. package/patternfly-docs/content/extensions/chatbot/examples/UI/Onboarding.tsx +151 -0
  101. package/patternfly-docs/content/extensions/chatbot/examples/UI/RH-Hat-Image.svg +9 -0
  102. package/patternfly-docs/content/extensions/chatbot/examples/UI/Settings.tsx +1 -1
  103. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +55 -27
  104. package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +18 -18
  105. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +29 -21
  106. package/patternfly-docs/content/extensions/chatbot/examples/demos/WhiteEmbeddedChatbot.tsx +451 -0
  107. package/patternfly-docs/patternfly-docs.config.js +2 -1
  108. package/patternfly-docs/patternfly-docs.source.js +1 -1
  109. package/src/AttachMenu/AttachMenu.tsx +26 -11
  110. package/src/Chatbot/Chatbot.scss +23 -1
  111. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +25 -0
  112. package/src/ChatbotFooter/ChatbotFooter.scss +21 -0
  113. package/src/ChatbotFooter/ChatbotFooter.test.tsx +10 -1
  114. package/src/ChatbotFooter/ChatbotFooter.tsx +10 -3
  115. package/src/ChatbotHeader/ChatbotHeader.scss +19 -0
  116. package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +56 -14
  117. package/src/ChatbotModal/ChatbotModal.scss +3 -0
  118. package/src/CodeModal/CodeModal.tsx +72 -23
  119. package/src/DeepThinking/DeepThinking.scss +1 -1
  120. package/src/FileDetailsLabel/FileDetailsLabel.tsx +2 -2
  121. package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +12 -0
  122. package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +4 -1
  123. package/src/Message/Message.scss +11 -7
  124. package/src/Message/Message.test.tsx +41 -0
  125. package/src/Message/Message.tsx +28 -13
  126. package/src/Message/MessageLoading.scss +7 -0
  127. package/src/Message/MessageLoading.tsx +2 -2
  128. package/src/Message/TableMessage/TableMessage.scss +6 -1
  129. package/src/Message/TableMessage/TableMessage.tsx +6 -2
  130. package/src/Message/TextMessage/TextMessage.scss +10 -0
  131. package/src/Message/TextMessage/TextMessage.tsx +11 -2
  132. package/src/Message/UserFeedback/UserFeedback.scss +2 -1
  133. package/src/MessageBar/AttachButton.test.tsx +4 -0
  134. package/src/MessageBar/AttachButton.tsx +4 -1
  135. package/src/MessageBar/MessageBar.scss +40 -3
  136. package/src/MessageBar/MessageBar.test.tsx +102 -1
  137. package/src/MessageBar/MessageBar.tsx +44 -11
  138. package/src/Onboarding/Onboarding.scss +101 -0
  139. package/src/Onboarding/Onboarding.test.tsx +148 -0
  140. package/src/Onboarding/Onboarding.tsx +126 -0
  141. package/src/Onboarding/index.ts +3 -0
  142. package/src/ResponseActions/ResponseActions.scss +1 -1
  143. package/src/ResponseActions/ResponseActions.test.tsx +111 -12
  144. package/src/ResponseActions/ResponseActions.tsx +38 -10
  145. package/src/ToolCall/ToolCall.scss +1 -1
  146. package/src/ToolResponse/ToolResponse.scss +3 -3
  147. package/src/__mocks__/monaco-editor.ts +19 -0
  148. package/src/__mocks__/rehype-highlight.ts +3 -0
  149. package/src/index.ts +3 -0
  150. package/src/main.scss +1 -0
  151. package/tsconfig.json +1 -1
  152. package/patternfly-docs/content/extensions/chatbot/about-chatbot.md +0 -44
@@ -15,10 +15,19 @@ describe('ChatbotFooter', () => {
15
15
 
16
16
  it('should handle isCompact', () => {
17
17
  render(
18
- <ChatbotFooter className="custom-class" isCompact data-testid="footer">
18
+ <ChatbotFooter isCompact data-testid="footer">
19
19
  Chatbot Content
20
20
  </ChatbotFooter>
21
21
  );
22
22
  expect(screen.getByTestId('footer')).toHaveClass('pf-m-compact');
23
23
  });
24
+
25
+ it('should handle isPrimary', () => {
26
+ render(
27
+ <ChatbotFooter isPrimary data-testid="footer">
28
+ Chatbot Content
29
+ </ChatbotFooter>
30
+ );
31
+ expect(screen.getByTestId('footer')).toHaveClass('pf-m-primary');
32
+ });
24
33
  });
@@ -13,20 +13,27 @@ import type { HTMLProps, FunctionComponent } from 'react';
13
13
  import { Divider } from '@patternfly/react-core';
14
14
 
15
15
  export interface ChatbotFooterProps extends HTMLProps<HTMLDivElement> {
16
- /** Children for the Footer that supports MessageBar and FootNote components*/
16
+ /** Children for the footer - supports MessageBar and FootNote components*/
17
17
  children?: React.ReactNode;
18
- /** Custom classname for the Footer component */
18
+ /** Custom classname for the footer component */
19
19
  className?: string;
20
+ /** Sets footer to compact styling. */
20
21
  isCompact?: boolean;
22
+ /** Sets background color to primary */
23
+ isPrimary?: boolean;
21
24
  }
22
25
 
23
26
  export const ChatbotFooter: FunctionComponent<ChatbotFooterProps> = ({
24
27
  children,
25
28
  className,
26
29
  isCompact,
30
+ isPrimary,
27
31
  ...props
28
32
  }: ChatbotFooterProps) => (
29
- <div className={`pf-chatbot__footer ${isCompact ? 'pf-m-compact' : ''} ${className ?? ''}`} {...props}>
33
+ <div
34
+ className={`pf-chatbot__footer ${isCompact ? 'pf-m-compact' : ''} ${isPrimary ? 'pf-m-primary' : ''} ${className ?? ''}`}
35
+ {...props}
36
+ >
30
37
  <Divider />
31
38
  <div className="pf-chatbot__footer-container">{children}</div>
32
39
  </div>
@@ -91,6 +91,10 @@
91
91
  align-items: center;
92
92
  justify-content: center;
93
93
 
94
+ &::before {
95
+ border-radius: inherit;
96
+ }
97
+
94
98
  .pf-v6-c-button__icon,
95
99
  .pf-v6-c-menu-toggle__icon,
96
100
  .pf-v6-c-icon__content {
@@ -162,3 +166,18 @@
162
166
  .pf-chatbot__header .pf-chatbot__actions .pf-v6-c-menu-toggle.pf-m-secondary.pf-m-compact {
163
167
  width: initial;
164
168
  }
169
+
170
+ // ============================================================================
171
+ // High contrast
172
+ // ============================================================================
173
+ :root:where(.pf-v6-theme-high-contrast) {
174
+ // Chatbot Display Mode - Fullscreen and Embedded
175
+ @media screen and (min-width: 64rem) {
176
+ .pf-chatbot--fullscreen,
177
+ .pf-chatbot--embedded {
178
+ .pf-chatbot__header__divider {
179
+ display: var(--pf-v6-hidden-visible--Display);
180
+ }
181
+ }
182
+ }
183
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Ref, FunctionComponent } from 'react';
2
- import { forwardRef } from 'react';
2
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
3
3
 
4
4
  import { Button, ButtonProps, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
5
5
  import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
@@ -30,21 +30,43 @@ const ChatbotHeaderMenuBase: FunctionComponent<ChatbotHeaderMenuProps> = ({
30
30
  tooltipContent = 'Chat history menu',
31
31
  isCompact,
32
32
  ...props
33
- }: ChatbotHeaderMenuProps) => (
34
- <div className={`pf-chatbot__menu ${className}`}>
35
- <Tooltip
36
- content={tooltipContent}
37
- position="bottom"
38
- // prevents VO announcements of both aria label and tooltip
39
- aria="none"
40
- {...tooltipProps}
41
- >
33
+ }: ChatbotHeaderMenuProps) => {
34
+ const [isDrawerAnimating, setIsDrawerAnimating] = useState(false);
35
+ // I'd like to use a prop here later if this works
36
+ const drawerState = props['aria-expanded'];
37
+ const isDrawerOpen = drawerState === true;
38
+ const prevDrawerStateRef = useRef<boolean | undefined>(isDrawerOpen);
39
+ const buttonRef = useRef<HTMLButtonElement | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (drawerState !== undefined) {
43
+ const wasDrawerOpen = prevDrawerStateRef.current === true;
44
+ const isDrawerClosing = wasDrawerOpen && !isDrawerOpen;
45
+
46
+ setIsDrawerAnimating(true);
47
+ const timeout = setTimeout(() => {
48
+ setIsDrawerAnimating(false);
49
+
50
+ if (isDrawerClosing) {
51
+ requestAnimationFrame(() => {
52
+ buttonRef.current?.focus();
53
+ });
54
+ }
55
+ }, 350);
56
+
57
+ prevDrawerStateRef.current = isDrawerOpen;
58
+ return () => clearTimeout(timeout);
59
+ }
60
+ }, [drawerState, isDrawerOpen]);
61
+
62
+ const button = useMemo(
63
+ () => (
42
64
  <Button
43
65
  className={`pf-chatbot__button--toggle-menu ${isCompact ? 'pf-m-compact' : ''}`}
44
66
  variant="plain"
45
67
  onClick={onMenuToggle}
46
68
  aria-label={menuAriaLabel}
47
- ref={innerRef}
69
+ ref={innerRef ?? buttonRef}
48
70
  icon={
49
71
  <Icon size={isCompact ? 'lg' : 'xl'} isInline>
50
72
  <BarsIcon />
@@ -53,9 +75,29 @@ const ChatbotHeaderMenuBase: FunctionComponent<ChatbotHeaderMenuProps> = ({
53
75
  size={isCompact ? 'sm' : undefined}
54
76
  {...props}
55
77
  />
56
- </Tooltip>
57
- </div>
58
- );
78
+ ),
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ [isCompact, menuAriaLabel, onMenuToggle, innerRef, buttonRef]
81
+ );
82
+
83
+ return (
84
+ <div className={`pf-chatbot__menu ${className}`}>
85
+ {isDrawerAnimating ? (
86
+ button
87
+ ) : (
88
+ <Tooltip
89
+ content={tooltipContent}
90
+ position="bottom"
91
+ // prevents VO announcements of both aria label and tooltip
92
+ aria="none"
93
+ {...tooltipProps}
94
+ >
95
+ {button}
96
+ </Tooltip>
97
+ )}
98
+ </div>
99
+ );
100
+ };
59
101
 
60
102
  export const ChatbotHeaderMenu = forwardRef((props: ChatbotHeaderMenuProps, ref: Ref<HTMLButtonElement>) => (
61
103
  <ChatbotHeaderMenuBase innerRef={ref} {...props} />
@@ -18,7 +18,10 @@
18
18
  .pf-v6-c-modal-box__footer {
19
19
  padding-block-start: var(--pf-t--global--spacer--xl);
20
20
  padding-block-end: var(--pf-t--global--spacer--xl);
21
+ border-top: var(--pf-t--global--border--width--high-contrast--regular) solid
22
+ var(--pf-t--global--border--color--high-contrast);
21
23
  }
24
+
22
25
  .pf-v6-c-modal-box__header {
23
26
  padding-block-end: var(--pf-t--global--spacer--sm);
24
27
  }
@@ -5,16 +5,17 @@
5
5
  import type { FunctionComponent, MouseEvent } from 'react';
6
6
  import { useState, useEffect, useRef } from 'react';
7
7
  import path from 'path-browserify';
8
- import type monaco from 'monaco-editor';
9
8
 
10
9
  // Import PatternFly components
11
10
  import { CodeEditor } from '@patternfly/react-code-editor';
12
11
  import {
12
+ Bullseye,
13
13
  Button,
14
14
  getResizeObserver,
15
15
  ModalBody,
16
16
  ModalFooter,
17
17
  ModalHeader,
18
+ Spinner,
18
19
  Stack,
19
20
  StackItem
20
21
  } from '@patternfly/react-core';
@@ -22,6 +23,17 @@ import FileDetails, { extensionToLanguage } from '../FileDetails';
22
23
  import { ChatbotDisplayMode } from '../Chatbot';
23
24
  import ChatbotModal from '../ChatbotModal/ChatbotModal';
24
25
 
26
+ // Try to lazy load - some consumers need to be below a certain bundle size, but can't use the CDN and don't have webpack
27
+ let monacoInstance: typeof import('monaco-editor') | null = null;
28
+ const loadMonaco = async () => {
29
+ if (!monacoInstance) {
30
+ const [monaco, { loader }] = await Promise.all([import('monaco-editor'), import('@monaco-editor/react')]);
31
+ monacoInstance = monaco;
32
+ loader.config({ monaco });
33
+ }
34
+ return monacoInstance;
35
+ };
36
+
25
37
  export interface CodeModalProps {
26
38
  /** Class applied to code editor */
27
39
  codeEditorControlClassName?: string;
@@ -59,6 +71,8 @@ export interface CodeModalProps {
59
71
  modalBodyClassName?: string;
60
72
  /** Class applied to modal footer */
61
73
  modalFooterClassName?: string;
74
+ /** Aria label applied to spinner when loading Monaco */
75
+ spinnerAriaLabel?: string;
62
76
  }
63
77
 
64
78
  export const CodeModal: FunctionComponent<CodeModalProps> = ({
@@ -80,13 +94,32 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
80
94
  modalHeaderClassName,
81
95
  modalBodyClassName,
82
96
  modalFooterClassName,
97
+ spinnerAriaLabel = 'Loading',
83
98
  ...props
84
99
  }: CodeModalProps) => {
85
100
  const [newCode, setNewCode] = useState(code);
86
- const [editorInstance, setEditorInstance] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
101
+ const [editorInstance, setEditorInstance] = useState<any>(null);
87
102
  const [isEditorReady, setIsEditorReady] = useState(false);
103
+ const [isMonacoLoading, setIsMonacoLoading] = useState(false);
104
+ const [isMonacoLoaded, setIsMonacoLoaded] = useState(false);
88
105
  const containerRef = useRef<HTMLDivElement>(null);
89
106
 
107
+ useEffect(() => {
108
+ if (isModalOpen && !isMonacoLoaded && !isMonacoLoading) {
109
+ setIsMonacoLoading(true);
110
+ loadMonaco()
111
+ .then(() => {
112
+ setIsMonacoLoaded(true);
113
+ setIsMonacoLoading(false);
114
+ })
115
+ .catch((error) => {
116
+ // eslint-disable-next-line no-console
117
+ console.error('Failed to load Monaco editor:', error);
118
+ setIsMonacoLoading(false);
119
+ });
120
+ }
121
+ }, [isModalOpen, isMonacoLoaded, isMonacoLoading]);
122
+
90
123
  useEffect(() => {
91
124
  if (!isModalOpen || !isEditorReady || !editorInstance || !containerRef.current) {
92
125
  return;
@@ -144,6 +177,42 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
144
177
  }
145
178
  };
146
179
 
180
+ const renderMonacoEditor = () => {
181
+ if (isMonacoLoading) {
182
+ return (
183
+ <Bullseye>
184
+ <Spinner aria-label={spinnerAriaLabel} />
185
+ </Bullseye>
186
+ );
187
+ }
188
+ if (isMonacoLoaded) {
189
+ return (
190
+ <CodeEditor
191
+ isDarkTheme
192
+ isLineNumbersVisible={isLineNumbersVisible}
193
+ isLanguageLabelVisible
194
+ isCopyEnabled={isCopyEnabled}
195
+ isReadOnly={isReadOnly}
196
+ code={newCode}
197
+ language={extensionToLanguage[path.extname(fileName).slice(1)]}
198
+ onEditorDidMount={onEditorDidMount}
199
+ onCodeChange={onCodeChange}
200
+ className={codeEditorClassName}
201
+ isFullHeight
202
+ options={{
203
+ glyphMargin: false,
204
+ folding: false,
205
+ // prevents Monaco from handling resizing itself
206
+ // was causing ResizeObserver issues
207
+ automaticLayout: false
208
+ }}
209
+ {...props}
210
+ />
211
+ );
212
+ }
213
+ return null;
214
+ };
215
+
147
216
  const modal = (
148
217
  <ChatbotModal
149
218
  isOpen={isModalOpen}
@@ -162,27 +231,7 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
162
231
  <FileDetails fileName={fileName} />
163
232
  </StackItem>
164
233
  <div className="pf-v6-l-stack__item pf-chatbot__code-modal-editor" ref={containerRef}>
165
- <CodeEditor
166
- isDarkTheme
167
- isLineNumbersVisible={isLineNumbersVisible}
168
- isLanguageLabelVisible
169
- isCopyEnabled={isCopyEnabled}
170
- isReadOnly={isReadOnly}
171
- code={newCode}
172
- language={extensionToLanguage[path.extname(fileName).slice(1)]}
173
- onEditorDidMount={onEditorDidMount}
174
- onCodeChange={onCodeChange}
175
- className={codeEditorClassName}
176
- isFullHeight
177
- options={{
178
- glyphMargin: false,
179
- folding: false,
180
- // prevents Monaco from handling resizing itself
181
- // was causing ResizeObserver issues
182
- automaticLayout: false
183
- }}
184
- {...props}
185
- />
234
+ {renderMonacoEditor()}
186
235
  </div>
187
236
  </Stack>
188
237
  </ModalBody>
@@ -1,5 +1,5 @@
1
1
  .pf-chatbot__deep-thinking {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
3
3
  overflow: unset;
4
4
  }
5
5
 
@@ -1,10 +1,10 @@
1
1
  import { PropsWithChildren } from 'react';
2
- import { Button, Label } from '@patternfly/react-core';
2
+ import { Button, Label, LabelProps } from '@patternfly/react-core';
3
3
  import FileDetails from '../FileDetails';
4
4
  import { Spinner } from '@patternfly/react-core';
5
5
  import { TimesIcon } from '@patternfly/react-icons';
6
6
 
7
- export interface FileDetailsLabelProps {
7
+ export interface FileDetailsLabelProps extends Omit<LabelProps, 'onClose' | 'onClick'> {
8
8
  /** Name of file, including extension */
9
9
  fileName: string;
10
10
  /** Unique id of file */
@@ -79,8 +79,20 @@
79
79
 
80
80
  .pf-chatbot__message-inline-code {
81
81
  --pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--body--default);
82
+
82
83
  background-color: var(--pf-t--global--background--color--tertiary--default);
83
84
  font-size: var(--pf-chatbot-message-text-inline-code-font-size);
85
+ border: var(--pf-t--global--border--width--high-contrast--regular) solid
86
+ var(--pf-t--global--border--color--high-contrast);
87
+ border-radius: var(--pf-t--global--border--radius--small);
88
+ padding-block-start: var(--pf-t--global--spacer--xs);
89
+ padding-block-end: var(--pf-t--global--spacer--xs);
90
+ padding-inline-start: var(--pf-t--global--spacer--xs);
91
+ padding-inline-end: var(--pf-t--global--spacer--xs);
92
+
93
+ &.pf-m-primary {
94
+ background-color: var(--pf-t--global--background--color--secondary--default);
95
+ }
84
96
  }
85
97
 
86
98
  .pf-chatbot__message-code-toggle {
@@ -39,6 +39,8 @@ export interface CodeBlockMessageProps {
39
39
  collapsedText?: string;
40
40
  /** Custom actions added to header of code block, after any default actions such as the "copy" action. */
41
41
  customActions?: React.ReactNode;
42
+ /** Sets background colors to be appropriate on primary chatbot background */
43
+ isPrimary?: boolean;
42
44
  }
43
45
 
44
46
  const DEFAULT_EXPANDED_TEXT = 'Show less';
@@ -54,6 +56,7 @@ const CodeBlockMessage = ({
54
56
  expandedText = DEFAULT_EXPANDED_TEXT,
55
57
  collapsedText = DEFAULT_COLLAPSED_TEXT,
56
58
  customActions,
59
+ isPrimary,
57
60
  ...props
58
61
  }: CodeBlockMessageProps) => {
59
62
  const [copied, setCopied] = useState(false);
@@ -108,7 +111,7 @@ const CodeBlockMessage = ({
108
111
 
109
112
  if (!String(children).includes('\n')) {
110
113
  return (
111
- <code {...props} className="pf-chatbot__message-inline-code">
114
+ <code {...props} className={`pf-chatbot__message-inline-code ${isPrimary ? 'pf-m-primary' : ''}`}>
112
115
  {children}
113
116
  </code>
114
117
  );
@@ -5,7 +5,7 @@
5
5
  display: flex;
6
6
  align-items: flex-start;
7
7
  gap: var(--pf-t--global--spacer--lg);
8
- padding-bottom: var(--pf-t--global--spacer--xl);
8
+ padding-bottom: var(--pf-t--global--spacer--lg);
9
9
 
10
10
  // Avatar
11
11
  // --------------------------------------------------------------------------
@@ -18,8 +18,10 @@
18
18
  }
19
19
 
20
20
  &-avatar.pf-chatbot__message-avatar--round.pf-v6-c-avatar {
21
- --pf-v6-c-avatar--Width: 3rem;
22
- --pf-v6-c-avatar--Height: 3rem;
21
+ &:not(.pf-m-xl, .pf-m-lg, .pf-m-md, .pf-m-sm) {
22
+ --pf-v6-c-avatar--Width: 3rem;
23
+ --pf-v6-c-avatar--Height: 3rem;
24
+ }
23
25
  --pf-v6-c-avatar--BorderRadius: var(--pf-t--global--border--radius--pill);
24
26
  }
25
27
 
@@ -156,8 +158,8 @@
156
158
  // ============================================================================
157
159
  .pf-chatbot.pf-m-compact {
158
160
  .pf-chatbot__message {
159
- gap: var(--pf-t--global--spacer--md);
160
- padding-bottom: var(--pf-t--global--spacer--sm);
161
+ gap: var(--pf-t--global--spacer--sm);
162
+ padding-bottom: var(--pf-t--global--spacer--md);
161
163
 
162
164
  .pf-chatbot__message-contents  {
163
165
  gap: var(--pf-t--global--spacer--xs);
@@ -169,8 +171,10 @@
169
171
  }
170
172
 
171
173
  .pf-chatbot__message-avatar.pf-chatbot__message-avatar--round.pf-v6-c-avatar {
172
- --pf-v6-c-avatar--Width: 2rem;
173
- --pf-v6-c-avatar--Height: 2rem;
174
+ &:not(.pf-m-xl, .pf-m-lg, .pf-m-md, .pf-m-sm) {
175
+ --pf-v6-c-avatar--Width: 1.5rem;
176
+ --pf-v6-c-avatar--Height: 1.5rem;
177
+ }
174
178
  }
175
179
 
176
180
  .pf-chatbot__message-contents {
@@ -228,6 +228,10 @@ describe('Message', () => {
228
228
  render(<Message avatar="./testImg" role="bot" name="Bot" content="Hi" />);
229
229
  expect(screen.getByRole('img')).toHaveAttribute('src', './testImg');
230
230
  });
231
+ it('should not render avatar if no avatar prop is passed', () => {
232
+ render(<Message role="bot" name="Bot" content="Hi" />);
233
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
234
+ });
231
235
  it('should render botWord correctly', () => {
232
236
  render(<Message avatar="./img" role="bot" name="Bot" content="Hi" botWord="人工知能" />);
233
237
  expect(screen.getByText('Bot')).toBeTruthy();
@@ -1093,4 +1097,41 @@ describe('Message', () => {
1093
1097
  expect(screen.getByText('Thought for 3 seconds')).toBeTruthy();
1094
1098
  expect(screen.getByText("Here's why I said this.")).toBeTruthy();
1095
1099
  });
1100
+ it('should handle isPrimary correctly for inline code when it is true', () => {
1101
+ const { container } = render(<Message avatar="./img" role="user" name="User" content={INLINE_CODE} isPrimary />);
1102
+ expect(container.querySelector('.pf-m-primary')).toBeTruthy();
1103
+ });
1104
+ it('should handle isPrimary correctly for inline code when it is false', () => {
1105
+ const { container } = render(<Message avatar="./img" role="user" name="User" content={INLINE_CODE} />);
1106
+ expect(container.querySelector('.pf-m-primary')).toBeFalsy();
1107
+ });
1108
+ it('should handle isPrimary correctly for table when it is true', () => {
1109
+ const { container } = render(<Message avatar="./img" role="user" name="User" content={TABLE} isPrimary />);
1110
+ expect(container.querySelector('.pf-m-primary')).toBeTruthy();
1111
+ });
1112
+ it('should handle isPrimary correctly for table when it is false', () => {
1113
+ const { container } = render(<Message avatar="./img" role="user" name="User" content={TABLE} />);
1114
+ expect(container.querySelector('.pf-m-primary')).toBeFalsy();
1115
+ });
1116
+ it('should handle isPrimary correctly for loading when it is true', () => {
1117
+ const { container } = render(<Message avatar="./img" role="user" name="User" content="" isPrimary isLoading />);
1118
+ expect(container.querySelector('.pf-m-primary')).toBeTruthy();
1119
+ });
1120
+ it('should handle isPrimary correctly for loading when it is false', () => {
1121
+ const { container } = render(<Message avatar="./img" role="user" name="User" content="" isLoading />);
1122
+
1123
+ expect(container.querySelector('.pf-m-primary')).toBeFalsy();
1124
+ });
1125
+ it('should handle isPrimary correctly for attachments when it is true', () => {
1126
+ const { container } = render(
1127
+ <Message avatar="./img" role="user" name="User" content="" isPrimary attachments={[{ name: 'testAttachment' }]} />
1128
+ );
1129
+ expect(container.querySelector('.pf-m-outline')).toBeTruthy();
1130
+ });
1131
+ it('should handle isPrimary correctly for attachments when it is false', () => {
1132
+ const { container } = render(
1133
+ <Message avatar="./img" role="user" name="User" content="" attachments={[{ name: 'testAttachment' }]} />
1134
+ );
1135
+ expect(container.querySelector('.pf-m-outline')).toBeFalsy();
1136
+ });
1096
1137
  });
@@ -42,6 +42,9 @@ import ImageMessage from './ImageMessage/ImageMessage';
42
42
  import rehypeUnwrapImages from 'rehype-unwrap-images';
43
43
  import rehypeExternalLinks from 'rehype-external-links';
44
44
  import rehypeSanitize from 'rehype-sanitize';
45
+ import rehypeHighlight from 'rehype-highlight';
46
+ // see the full list of styles here: https://highlightjs.org/examples
47
+ import 'highlight.js/styles/vs2015.css';
45
48
  import { PluggableList } from 'unified';
46
49
  import LinkMessage from './LinkMessage/LinkMessage';
47
50
  import ErrorMessage from './ErrorMessage/ErrorMessage';
@@ -94,7 +97,7 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
94
97
  /** Name of the user */
95
98
  name?: string;
96
99
  /** Avatar src for the user */
97
- avatar: string;
100
+ avatar?: string;
98
101
  /** Timestamp for the message */
99
102
  timestamp?: string;
100
103
  /** Set this to true if message is being loaded */
@@ -105,6 +108,9 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
105
108
  actions?: {
106
109
  [key: string]: ActionProps;
107
110
  };
111
+ /** When true, the selected action will persist even when clicking outside the component.
112
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
113
+ persistActionSelection?: boolean;
108
114
  /** Sources for message */
109
115
  sources?: SourcesCardProps;
110
116
  /** Label for the English word "AI," used to tag messages with role "bot" */
@@ -186,6 +192,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
186
192
  toolCall?: ToolCallProps;
187
193
  /** Whether user messages default to stripping out images in markdown */
188
194
  hasNoImagesInUserMessages?: boolean;
195
+ /** Sets background colors to be appropriate on primary chatbot background */
196
+ isPrimary?: boolean;
189
197
  }
190
198
 
191
199
  export const MessageBase: FunctionComponent<MessageProps> = ({
@@ -197,6 +205,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
197
205
  timestamp,
198
206
  isLoading,
199
207
  actions,
208
+ persistActionSelection,
200
209
  sources,
201
210
  botWord = 'AI',
202
211
  loadingWord = 'Loading message',
@@ -233,6 +242,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
233
242
  remarkGfmProps,
234
243
  toolCall,
235
244
  hasNoImagesInUserMessages = true,
245
+ isPrimary,
236
246
  ...props
237
247
  }: MessageProps) => {
238
248
  const [messageText, setMessageText] = useState(content);
@@ -242,7 +252,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
242
252
  }, [content]);
243
253
 
244
254
  const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
245
- let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs];
255
+ let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight];
246
256
  if (openLinkInNewTab) {
247
257
  rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]);
248
258
  }
@@ -283,13 +293,13 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
283
293
  p: (props) => {
284
294
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
285
295
  const { node, ...rest } = props;
286
- return <TextMessage component={ContentVariants.p} {...rest} />;
296
+ return <TextMessage component={ContentVariants.p} {...rest} isPrimary={isPrimary} />;
287
297
  },
288
298
  code: ({ children, ...props }) => {
289
299
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
290
300
  const { node, ...codeProps } = props;
291
301
  return (
292
- <CodeBlockMessage {...codeProps} {...codeBlockProps}>
302
+ <CodeBlockMessage {...codeProps} {...codeBlockProps} isPrimary={isPrimary}>
293
303
  {children}
294
304
  </CodeBlockMessage>
295
305
  );
@@ -345,7 +355,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
345
355
  return <ListItemMessage {...rest} />;
346
356
  },
347
357
  // table requires node attribute for calculating headers for mobile breakpoint
348
- table: (props) => <TableMessage {...props} {...tableProps} />,
358
+ table: (props) => <TableMessage {...props} {...tableProps} isPrimary={isPrimary} />,
349
359
  tbody: (props) => {
350
360
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
351
361
  const { node, ...rest } = props;
@@ -413,7 +423,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
413
423
 
414
424
  const renderMessage = () => {
415
425
  if (isLoading) {
416
- return <MessageLoading loadingWord={loadingWord} />;
426
+ return <MessageLoading loadingWord={loadingWord} isPrimary={isPrimary} />;
417
427
  }
418
428
  if (isEditable) {
419
429
  return (
@@ -453,12 +463,14 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
453
463
  {...props}
454
464
  >
455
465
  {/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */}
456
- <Avatar
457
- className={`pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`}
458
- src={avatar}
459
- alt=""
460
- {...avatarProps}
461
- />
466
+ {avatar && (
467
+ <Avatar
468
+ className={`pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`}
469
+ src={avatar}
470
+ alt=""
471
+ {...avatarProps}
472
+ />
473
+ )}
462
474
  <div className="pf-chatbot__message-contents">
463
475
  <div className="pf-chatbot__message-meta">
464
476
  {name && (
@@ -493,7 +505,9 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
493
505
  isCompact={isCompact}
494
506
  />
495
507
  )}
496
- {!isLoading && !isEditable && actions && <ResponseActions actions={actions} />}
508
+ {!isLoading && !isEditable && actions && (
509
+ <ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
510
+ )}
497
511
  {userFeedbackForm && <UserFeedback {...userFeedbackForm} timestamp={dateString} isCompact={isCompact} />}
498
512
  {userFeedbackComplete && (
499
513
  <UserFeedbackComplete {...userFeedbackComplete} timestamp={dateString} isCompact={isCompact} />
@@ -519,6 +533,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
519
533
  closeButtonAriaLabel={attachment.closeButtonAriaLabel}
520
534
  languageTestId={attachment.languageTestId}
521
535
  spinnerTestId={attachment.spinnerTestId}
536
+ variant={isPrimary ? 'outline' : undefined}
522
537
  />
523
538
  </div>
524
539
  ))}
@@ -6,6 +6,9 @@
6
6
  padding: var(--pf-t--global--spacer--sm);
7
7
  background-color: var(--pf-t--global--background--color--tertiary--default);
8
8
  border-radius: var(--pf-t--global--border--radius--small);
9
+ // for high contrast support
10
+ border: var(--pf-t--global--border--width--high-contrast--regular) solid
11
+ var(--pf-t--global--border--color--high-contrast);
9
12
 
10
13
  &-dots {
11
14
  position: relative;
@@ -50,4 +53,8 @@
50
53
  background-color: rgba(41, 41, 41, 0.25);
51
54
  }
52
55
  }
56
+
57
+ &.pf-m-primary {
58
+ background-color: var(--pf-t--global--background--color--secondary--default);
59
+ }
53
60
  }