@patternfly/chatbot 6.4.0-prerelease.19 → 6.4.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 (34) hide show
  1. package/dist/cjs/Message/LinkMessage/LinkMessage.d.ts +2 -1
  2. package/dist/cjs/Message/LinkMessage/LinkMessage.js +7 -3
  3. package/dist/cjs/Message/ListMessage/ListItemMessage.d.ts +1 -1
  4. package/dist/cjs/Message/ListMessage/ListItemMessage.js +16 -1
  5. package/dist/cjs/Message/Message.d.ts +4 -0
  6. package/dist/cjs/Message/Message.js +111 -22
  7. package/dist/cjs/Message/Message.test.js +35 -0
  8. package/dist/cjs/Message/SuperscriptMessage/SuperscriptMessage.d.ts +3 -0
  9. package/dist/cjs/Message/SuperscriptMessage/SuperscriptMessage.js +5 -0
  10. package/dist/css/main.css +142 -12
  11. package/dist/css/main.css.map +1 -1
  12. package/dist/esm/Message/LinkMessage/LinkMessage.d.ts +2 -1
  13. package/dist/esm/Message/LinkMessage/LinkMessage.js +7 -3
  14. package/dist/esm/Message/ListMessage/ListItemMessage.d.ts +1 -1
  15. package/dist/esm/Message/ListMessage/ListItemMessage.js +16 -1
  16. package/dist/esm/Message/Message.d.ts +4 -0
  17. package/dist/esm/Message/Message.js +111 -22
  18. package/dist/esm/Message/Message.test.js +35 -0
  19. package/dist/esm/Message/SuperscriptMessage/SuperscriptMessage.d.ts +3 -0
  20. package/dist/esm/Message/SuperscriptMessage/SuperscriptMessage.js +3 -0
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +1 -1
  23. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +101 -3
  24. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +107 -2
  25. package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +2 -1
  26. package/src/Message/LinkMessage/LinkMessage.tsx +6 -2
  27. package/src/Message/ListMessage/ListItemMessage.tsx +5 -1
  28. package/src/Message/ListMessage/ListMessage.scss +17 -0
  29. package/src/Message/Message.scss +44 -0
  30. package/src/Message/Message.test.tsx +36 -0
  31. package/src/Message/Message.tsx +127 -28
  32. package/src/Message/SuperscriptMessage/SuperscriptMessage.scss +8 -0
  33. package/src/Message/SuperscriptMessage/SuperscriptMessage.tsx +13 -0
  34. package/src/Message/TextMessage/TextMessage.scss +46 -5
@@ -1,4 +1,14 @@
1
- import { Fragment, useState, useRef, useEffect, CSSProperties, FunctionComponent, MouseEvent, Ref } from 'react';
1
+ import {
2
+ Fragment,
3
+ useState,
4
+ useRef,
5
+ useEffect,
6
+ CSSProperties,
7
+ FunctionComponent,
8
+ MouseEvent as ReactMouseEvent,
9
+ KeyboardEvent as ReactKeyboardEvent,
10
+ Ref
11
+ } from 'react';
2
12
  import Message from '@patternfly/chatbot/dist/dynamic/Message';
3
13
  import userAvatar from './user_avatar.svg';
4
14
  import {
@@ -64,6 +74,8 @@ export const UserMessageExample: FunctionComponent = () => {
64
74
  return table;
65
75
  case 'Image':
66
76
  return image;
77
+ case 'Footnote':
78
+ return footnote;
67
79
  default:
68
80
  return '';
69
81
  }
@@ -170,6 +182,18 @@ _Italic text, formatted with single underscores_
170
182
 
171
183
  const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;
172
184
 
185
+ const footnote = `This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote]
186
+
187
+ [^1]: This is a short footnote. To return the highlight to the original message, click the arrow.
188
+
189
+ [^bignote]: This is a long footnote with multiple paragraphs and formatting.
190
+
191
+ To break long footnotes into paragraphs, indent the text.
192
+
193
+ Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`.
194
+
195
+ > You can even include blockquotes in footnotes!`;
196
+
173
197
  const error = {
174
198
  title: 'Could not load chat',
175
199
  children: 'Wait a few minutes and check your network settings. If the issue persists: ',
@@ -185,7 +209,7 @@ _Italic text, formatted with single underscores_
185
209
  )
186
210
  };
187
211
 
188
- const onSelect = (_event: MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
212
+ const onSelect = (_event: ReactMouseEvent<Element> | undefined, value: string | number | undefined) => {
189
213
  setVariant(value);
190
214
  setSelected(value as string);
191
215
  setIsOpen(false);
@@ -221,6 +245,78 @@ _Italic text, formatted with single underscores_
221
245
  </MenuToggle>
222
246
  );
223
247
 
248
+ const handleFootnoteNavigation = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
249
+ const target = event.target as HTMLElement;
250
+
251
+ // Depending on whether it is a click event or keyboard event, target may be a link or something like a span
252
+ // Look for the closest anchor element (could be a parent)
253
+ const anchorElement = target.closest('a');
254
+ const href = anchorElement?.getAttribute('href');
255
+
256
+ // Check if this is a footnote link - we only have internal links in this example, so this is all we need here
257
+ if (href && href.startsWith('#')) {
258
+ // Prevent default behavior to avoid page re-render on click in PatternFly docs framework
259
+ event.preventDefault();
260
+
261
+ let targetElement: HTMLElement | null = null;
262
+ const targetId = href.replace('#', '');
263
+ targetElement = document.querySelector(`[id="${targetId}"]`);
264
+
265
+ if (targetElement) {
266
+ let focusTarget = targetElement;
267
+
268
+ // If we found a footnote definition container, focus on the parent li element
269
+ if (targetElement.id?.startsWith('user-message-fn-')) {
270
+ // Find the parent li element that contains the footnote
271
+ const parentLi = targetElement.closest('li');
272
+ if (parentLi) {
273
+ focusTarget = parentLi as HTMLElement;
274
+ }
275
+ }
276
+
277
+ focusTarget.focus();
278
+
279
+ let elementToHighlight = targetElement;
280
+ const searchStartElement = targetElement;
281
+ let elementToHighlightContainer: HTMLElement | null = null;
282
+
283
+ // For footnote references, look for an appropriate container
284
+ if (!targetElement.id?.startsWith('user-message-fn-')) {
285
+ let parent = searchStartElement.parentElement;
286
+ while (
287
+ parent &&
288
+ !(parent.tagName.toLowerCase() === 'span' && parent.classList.contains('pf-chatbot__message-text')) &&
289
+ parent !== document.body
290
+ ) {
291
+ parent = parent.parentElement;
292
+ }
293
+ elementToHighlightContainer = parent;
294
+ }
295
+
296
+ // Use the found container if available, otherwise fall back to the target element
297
+ elementToHighlight = elementToHighlightContainer || targetElement;
298
+
299
+ // Briefly highlight the target element for fun to show what you can do
300
+ const originalBackground = elementToHighlight.style.backgroundColor;
301
+ const originalTransition = elementToHighlight.style.transition;
302
+
303
+ elementToHighlight.style.transition = 'background-color 0.3s ease';
304
+ elementToHighlight.style.backgroundColor = 'var(--pf-t--global--icon--color--brand--hover)';
305
+
306
+ setTimeout(() => {
307
+ elementToHighlight.style.backgroundColor = originalBackground;
308
+ setTimeout(() => {
309
+ elementToHighlight.style.transition = originalTransition;
310
+ }, 300);
311
+ }, 1000);
312
+ }
313
+ }
314
+ };
315
+
316
+ const onClick = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
317
+ handleFootnoteNavigation(event);
318
+ };
319
+
224
320
  return (
225
321
  <>
226
322
  <Message
@@ -270,6 +366,7 @@ _Italic text, formatted with single underscores_
270
366
  <SelectOption value="More complex list">More complex list</SelectOption>
271
367
  <SelectOption value="Table">Table</SelectOption>
272
368
  <SelectOption value="Image">Image</SelectOption>
369
+ <SelectOption value="Footnote">Footnote</SelectOption>
273
370
  <SelectOption value="Error">Error</SelectOption>
274
371
  </SelectList>
275
372
  </Select>
@@ -287,6 +384,14 @@ _Italic text, formatted with single underscores_
287
384
  // The purpose of this plugin is to provide unique link names for the code blocks
288
385
  // Because they are in the same message, this requires a custom plugin to parse the syntax tree
289
386
  additionalRehypePlugins={[rehypeCodeBlockToggle]}
387
+ linkProps={{ onClick }}
388
+ // clobberPrefix controls the label ids
389
+ reactMarkdownProps={{
390
+ remarkRehypeOptions: {
391
+ footnoteLabel: 'User message footnotes',
392
+ clobberPrefix: 'user-message-'
393
+ }
394
+ }}
290
395
  />
291
396
  </>
292
397
  );
@@ -77,8 +77,9 @@
77
77
  }
78
78
 
79
79
  .pf-chatbot__message-inline-code {
80
+ --pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--body--default);
80
81
  background-color: var(--pf-t--global--background--color--tertiary--default);
81
- font-size: var(--pf-t--global--font--size--body--default);
82
+ font-size: var(--pf-chatbot-message-text-inline-code-font-size);
82
83
  }
83
84
 
84
85
  .pf-chatbot__message-code-toggle {
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { Button, ButtonProps } from '@patternfly/react-core';
6
6
  import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
7
+ import { ExtraProps } from 'react-markdown';
7
8
 
8
- const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
9
+ const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & ExtraProps) => {
9
10
  if (target === '_blank') {
10
11
  return (
11
12
  <Button
@@ -16,6 +17,8 @@ const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
16
17
  iconPosition="end"
17
18
  isInline
18
19
  target={target}
20
+ // need to explicitly call this out or id doesn't seem to get passed - required for footnotes
21
+ id={id}
19
22
  {...props}
20
23
  >
21
24
  {children}
@@ -24,7 +27,8 @@ const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
24
27
  }
25
28
 
26
29
  return (
27
- <Button isInline component="a" href={href} variant="link" {...props}>
30
+ // need to explicitly call this out or id doesn't seem to get passed - required for footnotes
31
+ <Button isInline component="a" href={href} variant="link" id={id} {...props}>
28
32
  {children}
29
33
  </Button>
30
34
  );
@@ -5,6 +5,10 @@
5
5
  import { ExtraProps } from 'react-markdown';
6
6
  import { ListItem } from '@patternfly/react-core';
7
7
 
8
- const ListItemMessage = ({ children }: JSX.IntrinsicElements['li'] & ExtraProps) => <ListItem>{children}</ListItem>;
8
+ const ListItemMessage = ({ children, ...props }: JSX.IntrinsicElements['li'] & ExtraProps) => (
9
+ <ListItem {...props} tabIndex={props?.id?.includes('fn-') ? -1 : props?.tabIndex}>
10
+ {children}
11
+ </ListItem>
12
+ );
9
13
 
10
14
  export default ListItemMessage;
@@ -21,5 +21,22 @@
21
21
  background-color: var(--pf-t--global--color--brand--default);
22
22
  color: var(--pf-t--global--text--color--on-brand--default);
23
23
  padding: var(--pf-t--global--spacer--sm);
24
+
25
+ // prevents issues when highlighting things like footnotes - don't have blue on blue
26
+ .pf-chatbot__message-text {
27
+ background-color: initial;
28
+ }
29
+ }
30
+
31
+ // targets footnotes specifically and prevents misalignment problems
32
+ .footnotes {
33
+ li > span {
34
+ display: inline-flex;
35
+ flex-direction: column;
36
+ }
37
+ }
38
+
39
+ li a {
40
+ color: var(--pf-t--global--text--color--on-brand--default);
24
41
  }
25
42
  }
@@ -89,6 +89,49 @@
89
89
  display: grid;
90
90
  gap: var(--pf-t--global--spacer--sm);
91
91
  }
92
+
93
+ // targets footnotes specifically
94
+ .footnotes,
95
+ .pf-chatbot__message-text.footnotes {
96
+ padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--sm) 0 var(--pf-t--global--spacer--sm);
97
+ --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--xs);
98
+ --pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--xs);
99
+
100
+ .pf-chatbot__message-text h1,
101
+ h2,
102
+ h3,
103
+ h4,
104
+ h5,
105
+ h6 {
106
+ --pf-v6-c-content--h1--FontSize: var(--pf-t--global--font--size--md);
107
+ --pf-v6-c-content--h2--FontSize: var(--pf-t--global--font--size--md);
108
+ --pf-v6-c-content--h3--FontSize: var(--pf-t--global--font--size--md);
109
+ --pf-v6-c-content--h4--FontSize: var(--pf-t--global--font--size--md);
110
+ --pf-v6-c-content--h5--FontSize: var(--pf-t--global--font--size--md);
111
+ --pf-v6-c-content--h6--FontSize: var(--pf-t--global--font--size--md);
112
+ }
113
+ .pf-chatbot__message-text .pf-v6-c-content,
114
+ .pf-chatbot__message-text .pf-v6-c-content--small,
115
+ .pf-chatbot__message-text .pf-v6-c-content--blockquote,
116
+ .pf-chatbot__message-text p,
117
+ .pf-chatbot__message-text a {
118
+ --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--xs);
119
+ }
120
+ .pf-chatbot__message-inline-code,
121
+ .pf-chatbot__message-text .pf-v6-c-button.pf-m-link,
122
+ .pf-chatbot__message-ordered-list .pf-v6-c-list,
123
+ .pf-chatbot__message-ordered-list ul,
124
+ .pf-chatbot__message-ordered-list li,
125
+ .pf-chatbot__message-unordered-list .pf-v6-c-list,
126
+ .pf-chatbot__message-unordered-list ul,
127
+ .pf-chatbot__message-unordered-list li {
128
+ font-size: var(--pf-t--global--font--size--xs);
129
+ }
130
+ }
131
+
132
+ .footnotes {
133
+ background-color: var(--pf-t--global--background--color--tertiary--default);
134
+ }
92
135
  }
93
136
 
94
137
  // Attachments
@@ -106,6 +149,7 @@
106
149
  @import './MessageLoading';
107
150
  @import './CodeBlockMessage/CodeBlockMessage';
108
151
  @import './TextMessage/TextMessage';
152
+ @import './SuperscriptMessage/SuperscriptMessage.scss';
109
153
 
110
154
  // ============================================================================
111
155
  // Information density styles
@@ -142,6 +142,20 @@ const EMPTY_TABLE = `
142
142
 
143
143
  `;
144
144
 
145
+ const FOOTNOTE = `This is some text with a footnote[^1] and here's a longer one.[^bignote]
146
+
147
+ You can also reference the same footnote multiple times[^1].
148
+
149
+ [^1]: This is the full footnote text. You can click the arrow to go back up.
150
+
151
+ [^bignote]: Here's one with multiple paragraphs and **formatting**.
152
+
153
+ Indent paragraphs to include them in the footnote.
154
+
155
+ Add as many paragraphs as you like. You can include *italic text*, **bold text**, and even \`code\`.
156
+
157
+ > You can even include blockquotes in footnotes!`;
158
+
145
159
  const IMAGE = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;
146
160
 
147
161
  const INLINE_IMAGE = `inline text ![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;
@@ -769,6 +783,28 @@ describe('Message', () => {
769
783
  render(<Message avatar="./img" role="user" name="User" content={TABLE} tableProps={{ 'aria-label': 'Test' }} />);
770
784
  expect(screen.getByRole('grid', { name: /Test/i })).toBeTruthy();
771
785
  });
786
+ it('should render footnote correctly', () => {
787
+ render(<Message avatar="./img" role="user" name="User" content={FOOTNOTE} />);
788
+ expect(screen.getByText(/This is some text with a footnote/i)).toBeTruthy();
789
+ expect(screen.getByText(/and here's a longer one./i)).toBeTruthy();
790
+ expect(screen.getByText(/You can also reference the same footnote multiple times./i)).toBeTruthy();
791
+ expect(screen.getByRole('heading', { name: /Footnotes/i })).toBeTruthy();
792
+ expect(screen.getByText(/This is the full footnote text. You can click the arrow to go back up./i)).toBeTruthy();
793
+ expect(screen.getByText(/Here's one with multiple paragraphs and/i)).toBeTruthy();
794
+ expect(screen.getByText(/formatting/i)).toBeTruthy();
795
+ expect(screen.getByText(/Indent paragraphs to include them in the footnote./i)).toBeTruthy();
796
+ expect(screen.getByText(/Add as many paragraphs as you like. You can include/i)).toBeTruthy();
797
+ expect(screen.getByText(/italic text/i)).toBeTruthy();
798
+ expect(screen.getByText(/bold text/i)).toBeTruthy();
799
+ expect(screen.getByText(/, and even/i)).toBeTruthy();
800
+ expect(screen.getByText(/code/i)).toBeTruthy();
801
+ expect(screen.getByText(/You can even include blockquotes in footnotes!/i)).toBeTruthy();
802
+ expect(screen.getAllByRole('link', { name: '1' })).toHaveLength(2);
803
+ expect(screen.getAllByRole('link', { name: '2' })).toBeTruthy();
804
+ expect(screen.getByRole('link', { name: 'Back to reference 1' })).toBeTruthy();
805
+ expect(screen.getByRole('link', { name: 'Back to reference 1-2' })).toBeTruthy();
806
+ expect(screen.getByRole('link', { name: /Back to reference 2/i })).toBeTruthy();
807
+ });
772
808
  it('should render beforeMainContent with main content', () => {
773
809
  const mainContent = 'Main message content';
774
810
  const beforeMainContentText = 'Before main content';
@@ -51,6 +51,7 @@ import MessageInput from './MessageInput';
51
51
  import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs';
52
52
  import ToolResponse, { ToolResponseProps } from '../ToolResponse';
53
53
  import DeepThinking, { DeepThinkingProps } from '../DeepThinking';
54
+ import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage';
54
55
 
55
56
  export interface MessageAttachment {
56
57
  /** Name of file attached to the message */
@@ -163,6 +164,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
163
164
  tableProps?: Required<Pick<TableProps, 'aria-label'>> & TableProps;
164
165
  /** Additional rehype plugins passed from the consumer */
165
166
  additionalRehypePlugins?: PluggableList;
167
+ /** Additional remark plugins passed from the consumer */
168
+ additionalRemarkPlugins?: PluggableList;
166
169
  /** Whether to open links in message in new tab. */
167
170
  openLinkInNewTab?: boolean;
168
171
  /** Optional inline error message that can be displayed in the message */
@@ -195,6 +198,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
195
198
  toolResponse?: ToolResponseProps;
196
199
  /** Props for deep thinking card */
197
200
  deepThinking?: DeepThinkingProps;
201
+ /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */
202
+ remarkGfmProps?: Options;
198
203
  }
199
204
 
200
205
  export const MessageBase: FunctionComponent<MessageProps> = ({
@@ -223,6 +228,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
223
228
  tableProps,
224
229
  openLinkInNewTab = true,
225
230
  additionalRehypePlugins = [],
231
+ additionalRemarkPlugins = [],
226
232
  linkProps,
227
233
  error,
228
234
  isEditable,
@@ -238,6 +244,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
238
244
  reactMarkdownProps,
239
245
  toolResponse,
240
246
  deepThinking,
247
+ remarkGfmProps,
241
248
  ...props
242
249
  }: MessageProps) => {
243
250
  const [messageText, setMessageText] = useState(content);
@@ -275,43 +282,135 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
275
282
  return (
276
283
  <Markdown
277
284
  components={{
278
- p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
279
- code: ({ children, ...props }) => (
280
- <CodeBlockMessage {...props} {...codeBlockProps}>
281
- {children}
282
- </CodeBlockMessage>
283
- ),
284
- h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
285
- h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
286
- h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
287
- h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
288
- h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
289
- h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
290
- blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
291
- ul: (props) => <UnorderedListMessage {...props} />,
292
- ol: (props) => <OrderedListMessage {...props} />,
293
- li: (props) => <ListItemMessage {...props} />,
285
+ section: (props) => {
286
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
287
+ const { node, ...rest } = props;
288
+ return <section {...rest} className={`pf-chatbot__message-text ${rest?.className}`} />;
289
+ },
290
+ p: (props) => {
291
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
292
+ const { node, ...rest } = props;
293
+ return <TextMessage component={ContentVariants.p} {...rest} />;
294
+ },
295
+ code: ({ children, ...props }) => {
296
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
297
+ const { node, ...codeProps } = props;
298
+ return (
299
+ <CodeBlockMessage {...codeProps} {...codeBlockProps}>
300
+ {children}
301
+ </CodeBlockMessage>
302
+ );
303
+ },
304
+ h1: (props) => {
305
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
306
+ const { node, ...rest } = props;
307
+ return <TextMessage component={ContentVariants.h1} {...rest} />;
308
+ },
309
+ h2: (props) => {
310
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
311
+ const { node, ...rest } = props;
312
+ return <TextMessage component={ContentVariants.h2} {...rest} />;
313
+ },
314
+ h3: (props) => {
315
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
316
+ const { node, ...rest } = props;
317
+ return <TextMessage component={ContentVariants.h3} {...rest} />;
318
+ },
319
+ h4: (props) => {
320
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
321
+ const { node, ...rest } = props;
322
+ return <TextMessage component={ContentVariants.h4} {...rest} />;
323
+ },
324
+ h5: (props) => {
325
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
326
+ const { node, ...rest } = props;
327
+ return <TextMessage component={ContentVariants.h5} {...rest} />;
328
+ },
329
+ h6: (props) => {
330
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
331
+ const { node, ...rest } = props;
332
+ return <TextMessage component={ContentVariants.h6} {...rest} />;
333
+ },
334
+ blockquote: (props) => {
335
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
336
+ const { node, ...rest } = props;
337
+ return <TextMessage component={ContentVariants.blockquote} {...rest} />;
338
+ },
339
+ ul: (props) => {
340
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
341
+ const { node, ...rest } = props;
342
+ return <UnorderedListMessage {...rest} />;
343
+ },
344
+ ol: (props) => {
345
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
346
+ const { node, ...rest } = props;
347
+ return <OrderedListMessage {...rest} />;
348
+ },
349
+ li: (props) => {
350
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
351
+ const { node, ...rest } = props;
352
+ return <ListItemMessage {...rest} />;
353
+ },
354
+ // table requires node attribute for calculating headers for mobile breakpoint
294
355
  table: (props) => <TableMessage {...props} {...tableProps} />,
295
- tbody: (props) => <TbodyMessage {...props} />,
296
- thead: (props) => <TheadMessage {...props} />,
297
- tr: (props) => <TrMessage {...props} />,
356
+ tbody: (props) => {
357
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
358
+ const { node, ...rest } = props;
359
+ return <TbodyMessage {...rest} />;
360
+ },
361
+ thead: (props) => {
362
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
363
+ const { node, ...rest } = props;
364
+ return <TheadMessage {...rest} />;
365
+ },
366
+ tr: (props) => {
367
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
368
+ const { node, ...rest } = props;
369
+ return <TrMessage {...rest} />;
370
+ },
298
371
  td: (props) => {
299
372
  // Conflicts with Td type
300
373
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
301
- const { width, ...rest } = props;
374
+ const { node, width, ...rest } = props;
302
375
  return <TdMessage {...rest} />;
303
376
  },
304
- th: (props) => <ThMessage {...props} />,
305
- img: (props) => <ImageMessage {...props} />,
306
- a: (props) => (
307
- <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
308
- {props.children}
309
- </LinkMessage>
310
- )
377
+ th: (props) => {
378
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
379
+ const { node, ...rest } = props;
380
+ return <ThMessage {...rest} />;
381
+ },
382
+ img: (props) => {
383
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
384
+ const { node, ...rest } = props;
385
+ return <ImageMessage {...rest} />;
386
+ },
387
+ a: (props) => {
388
+ // node is just the details of the document structure - not needed
389
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
390
+ const { node, ...rest } = props;
391
+ return (
392
+ // some a types conflict with ButtonProps, but it's ok because we are using an a tag
393
+ // there are too many to handle manually
394
+ <LinkMessage {...(rest as any)} {...linkProps}>
395
+ {props.children}
396
+ </LinkMessage>
397
+ );
398
+ },
399
+ // used for footnotes
400
+ sup: (props) => {
401
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
402
+ const { node, ...rest } = props;
403
+ return <SuperscriptMessage {...rest} />;
404
+ }
311
405
  }}
312
- remarkPlugins={[remarkGfm]}
406
+ remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]}
313
407
  rehypePlugins={rehypePlugins}
314
408
  {...reactMarkdownProps}
409
+ remarkRehypeOptions={{
410
+ // removes sr-only class from footnote labels applied by default
411
+ footnoteLabelProperties: { className: [''] },
412
+ ...reactMarkdownProps?.remarkRehypeOptions
413
+ }}
315
414
  >
316
415
  {messageText}
317
416
  </Markdown>
@@ -0,0 +1,8 @@
1
+ .pf-chatbot__message-superscript {
2
+ font-size: smaller;
3
+ vertical-align: super;
4
+ .pf-v6-c-button.pf-m-link.pf-m-inline {
5
+ font-size: inherit;
6
+ vertical-align: inherit;
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Message - Content - Superscript (like for footnotes)
3
+ // ============================================================================
4
+
5
+ import { ExtraProps } from 'react-markdown';
6
+
7
+ const SuperscriptMessage = ({ children }: JSX.IntrinsicElements['sup'] & ExtraProps) => (
8
+ <span className="pf-chatbot__message-superscript">
9
+ <sup>{children}</sup>
10
+ </span>
11
+ );
12
+
13
+ export default SuperscriptMessage;
@@ -17,9 +17,10 @@
17
17
  width: fit-content;
18
18
  padding: var(--pf-t--global--spacer--sm) 0 var(--pf-t--global--spacer--sm) 0;
19
19
  border-radius: var(--pf-t--global--border--radius--small);
20
+ --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--md);
20
21
 
21
22
  .pf-v6-c-button.pf-m-link {
22
- font-size: var(--pf-t--global--font--size--md);
23
+ font-size: var(--pf-chatbot-message-text-font-size);
23
24
  }
24
25
 
25
26
  .pf-v6-c-content,
@@ -27,15 +28,49 @@
27
28
  .pf-v6-c-content--blockquote,
28
29
  p,
29
30
  a {
30
- --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--md);
31
+ --pf-v6-c-content--FontSize: var(--pf-chatbot-message-text-font-size);
31
32
  }
32
33
 
33
34
  code {
34
35
  background-color: var(--pf-t--global--background--color--tertiary--default);
35
- font-size: var(--pf-t--global--font--size--body--default);
36
+ font-size: var(--pf-chatbot-message-text-inline-code-font-size);
37
+ }
38
+
39
+ // Hide message text that contains sr-only content
40
+ // https://css-tricks.com/inclusively-hidden/
41
+ &:has(.sr-only) {
42
+ clip: rect(0 0 0 0);
43
+ clip-path: inset(50%);
44
+ height: 1px;
45
+ overflow: hidden;
46
+ position: absolute;
47
+ white-space: nowrap;
48
+ width: 1px;
36
49
  }
37
50
  }
38
51
 
52
+ // ============================================================================
53
+ // Footnote spacing styles
54
+ // ============================================================================
55
+
56
+ // Add spacing to paragraphs in multi-paragraph footnotes
57
+ // Only target p tags that are direct children of message-text spans (not inside blockquotes, etc.)
58
+ li[id*='user-content-fn-']:has(> span > .pf-chatbot__message-text + .pf-chatbot__message-text)
59
+ > span
60
+ > .pf-chatbot__message-text
61
+ > p {
62
+ margin-block-end: var(--pf-t--global--spacer--md);
63
+ }
64
+
65
+ // Handle user message footnotes which may have extra span wrappers
66
+ li[id*='user-content-fn-']:has(> span > span > .pf-chatbot__message-text + .pf-chatbot__message-text)
67
+ > span
68
+ > span
69
+ > .pf-chatbot__message-text
70
+ > p {
71
+ margin-block-end: var(--pf-t--global--spacer--md);
72
+ }
73
+
39
74
  .pf-chatbot__message--user {
40
75
  .pf-chatbot__message-text {
41
76
  background-color: var(--pf-t--global--color--brand--default);
@@ -54,6 +89,11 @@
54
89
  color: var(--pf-t--global--text--color--on-brand--default);
55
90
  }
56
91
  }
92
+
93
+ .pf-chatbot__message-text > .pf-chatbot__message-text {
94
+ background-color: initial;
95
+ padding: initial;
96
+ }
57
97
  }
58
98
 
59
99
  // ============================================================================
@@ -62,8 +102,9 @@
62
102
  .pf-chatbot.pf-m-compact {
63
103
  // Need to inline shorter text
64
104
  .pf-chatbot__message-text {
105
+ --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--sm);
65
106
  .pf-v6-c-button.pf-m-link {
66
- font-size: var(--pf-t--global--font--size--sm);
107
+ font-size: var(--pf-chatbot-message-text-font-size);
67
108
  }
68
109
 
69
110
  .pf-v6-c-content,
@@ -71,7 +112,7 @@
71
112
  .pf-v6-c-content--blockquote,
72
113
  p,
73
114
  a {
74
- --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--sm);
115
+ --pf-v6-c-content--FontSize: var(--pf-chatbot-message-text-font-size);
75
116
  }
76
117
 
77
118
  .pf-v6-c-content--blockquote {