@patternfly/chatbot 6.4.0-prerelease.3 → 6.4.0-prerelease.5

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.
@@ -1,5 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react';
3
+ import { Options } from 'react-markdown';
3
4
  import { AlertProps, AvatarProps, ButtonProps, ExpandableSectionProps, ExpandableSectionToggleProps, FormProps, LabelGroupProps } from '@patternfly/react-core';
4
5
  import { ActionProps } from '../ResponseActions/ResponseActions';
5
6
  import { SourcesCardProps } from '../SourcesCard';
@@ -140,6 +141,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
140
141
  editFormProps?: FormProps;
141
142
  /** Sets message to compact styling. */
142
143
  isCompact?: boolean;
144
+ /** Disables markdown parsing for message, allowing only text input */
145
+ isMarkdownDisabled?: boolean;
146
+ /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */
147
+ reactMarkdownProps?: Options;
143
148
  }
144
149
  export declare const MessageBase: FunctionComponent<MessageProps>;
145
150
  declare const Message: import("react").ForwardRefExoticComponent<Omit<MessageProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -51,7 +51,7 @@ const ErrorMessage_1 = __importDefault(require("./ErrorMessage/ErrorMessage"));
51
51
  const MessageInput_1 = __importDefault(require("./MessageInput"));
52
52
  const rehypeMoveImagesOutOfParagraphs_1 = require("./Plugins/rehypeMoveImagesOutOfParagraphs");
53
53
  const MessageBase = (_a) => {
54
- var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact"]);
54
+ var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps"]);
55
55
  const [messageText, setMessageText] = (0, react_1.useState)(content);
56
56
  (0, react_1.useEffect)(() => {
57
57
  setMessageText(content);
@@ -73,6 +73,41 @@ const MessageBase = (_a) => {
73
73
  // Keep timestamps consistent between Timestamp component and aria-label
74
74
  const date = new Date();
75
75
  const dateString = timestamp !== null && timestamp !== void 0 ? timestamp : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
76
+ const handleMarkdown = () => {
77
+ if (isMarkdownDisabled) {
78
+ return ((0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.p }, props, { children: messageText })));
79
+ }
80
+ return ((0, jsx_runtime_1.jsx)(react_markdown_1.default, Object.assign({ components: {
81
+ p: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.p }, props)),
82
+ code: (_a) => {
83
+ var { children } = _a, props = __rest(_a, ["children"]);
84
+ return ((0, jsx_runtime_1.jsx)(CodeBlockMessage_1.default, Object.assign({}, props, codeBlockProps, { children: children })));
85
+ },
86
+ h1: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h1 }, props)),
87
+ h2: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h2 }, props)),
88
+ h3: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h3 }, props)),
89
+ h4: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h4 }, props)),
90
+ h5: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h5 }, props)),
91
+ h6: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h6 }, props)),
92
+ blockquote: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.blockquote }, props)),
93
+ ul: (props) => (0, jsx_runtime_1.jsx)(UnorderedListMessage_1.default, Object.assign({}, props)),
94
+ ol: (props) => (0, jsx_runtime_1.jsx)(OrderedListMessage_1.default, Object.assign({}, props)),
95
+ li: (props) => (0, jsx_runtime_1.jsx)(ListItemMessage_1.default, Object.assign({}, props)),
96
+ table: (props) => (0, jsx_runtime_1.jsx)(TableMessage_1.default, Object.assign({}, props, tableProps)),
97
+ tbody: (props) => (0, jsx_runtime_1.jsx)(TbodyMessage_1.default, Object.assign({}, props)),
98
+ thead: (props) => (0, jsx_runtime_1.jsx)(TheadMessage_1.default, Object.assign({}, props)),
99
+ tr: (props) => (0, jsx_runtime_1.jsx)(TrMessage_1.default, Object.assign({}, props)),
100
+ td: (props) => {
101
+ // Conflicts with Td type
102
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
+ const { width } = props, rest = __rest(props, ["width"]);
104
+ return (0, jsx_runtime_1.jsx)(TdMessage_1.default, Object.assign({}, rest));
105
+ },
106
+ th: (props) => (0, jsx_runtime_1.jsx)(ThMessage_1.default, Object.assign({}, props)),
107
+ img: (props) => (0, jsx_runtime_1.jsx)(ImageMessage_1.default, Object.assign({}, props)),
108
+ a: (props) => ((0, jsx_runtime_1.jsx)(LinkMessage_1.default, Object.assign({ href: props.href, rel: props.rel, target: props.target }, linkProps, { children: props.children })))
109
+ }, remarkPlugins: [remark_gfm_1.default], rehypePlugins: rehypePlugins }, reactMarkdownProps, { children: messageText })));
110
+ };
76
111
  const renderMessage = () => {
77
112
  if (isLoading) {
78
113
  return (0, jsx_runtime_1.jsx)(MessageLoading_1.default, { loadingWord: loadingWord });
@@ -83,36 +118,7 @@ const MessageBase = (_a) => {
83
118
  setMessageText(value);
84
119
  }, onEditCancel: onEditCancel, inputRef: inputRef }, editFormProps))] }));
85
120
  }
86
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [beforeMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: beforeMainContent }), error ? ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, Object.assign({}, error))) : ((0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: {
87
- p: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.p }, props)),
88
- code: (_a) => {
89
- var { children } = _a, props = __rest(_a, ["children"]);
90
- return ((0, jsx_runtime_1.jsx)(CodeBlockMessage_1.default, Object.assign({}, props, codeBlockProps, { children: children })));
91
- },
92
- h1: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h1 }, props)),
93
- h2: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h2 }, props)),
94
- h3: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h3 }, props)),
95
- h4: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h4 }, props)),
96
- h5: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h5 }, props)),
97
- h6: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.h6 }, props)),
98
- blockquote: (props) => (0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.blockquote }, props)),
99
- ul: (props) => (0, jsx_runtime_1.jsx)(UnorderedListMessage_1.default, Object.assign({}, props)),
100
- ol: (props) => (0, jsx_runtime_1.jsx)(OrderedListMessage_1.default, Object.assign({}, props)),
101
- li: (props) => (0, jsx_runtime_1.jsx)(ListItemMessage_1.default, Object.assign({}, props)),
102
- table: (props) => (0, jsx_runtime_1.jsx)(TableMessage_1.default, Object.assign({}, props, tableProps)),
103
- tbody: (props) => (0, jsx_runtime_1.jsx)(TbodyMessage_1.default, Object.assign({}, props)),
104
- thead: (props) => (0, jsx_runtime_1.jsx)(TheadMessage_1.default, Object.assign({}, props)),
105
- tr: (props) => (0, jsx_runtime_1.jsx)(TrMessage_1.default, Object.assign({}, props)),
106
- td: (props) => {
107
- // Conflicts with Td type
108
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
109
- const { width } = props, rest = __rest(props, ["width"]);
110
- return (0, jsx_runtime_1.jsx)(TdMessage_1.default, Object.assign({}, rest));
111
- },
112
- th: (props) => (0, jsx_runtime_1.jsx)(ThMessage_1.default, Object.assign({}, props)),
113
- img: (props) => (0, jsx_runtime_1.jsx)(ImageMessage_1.default, Object.assign({}, props)),
114
- a: (props) => ((0, jsx_runtime_1.jsx)(LinkMessage_1.default, Object.assign({ href: props.href, rel: props.rel, target: props.target }, linkProps, { children: props.children })))
115
- }, remarkPlugins: [remark_gfm_1.default], rehypePlugins: rehypePlugins, children: messageText }))] }));
121
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [beforeMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: beforeMainContent }), error ? (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, Object.assign({}, error)) : handleMarkdown()] }));
116
122
  };
117
123
  return ((0, jsx_runtime_1.jsxs)("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [(0, jsx_runtime_1.jsx)(react_core_1.Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps)), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-contents", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-meta", children: [name && ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__message-name", children: (0, jsx_runtime_1.jsx)(react_core_1.Truncate, { content: name }) })), role === 'bot' && ((0, jsx_runtime_1.jsx)(react_core_1.Label, { variant: "outline", isCompact: true, children: botWord })), (0, jsx_runtime_1.jsx)(react_core_1.Timestamp, { date: date, children: timestamp })] }), (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-response", children: [(0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: afterMainContent }), !isLoading && sources && (0, jsx_runtime_1.jsx)(SourcesCard_1.default, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && ((0, jsx_runtime_1.jsx)(QuickStartTile_1.default, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && (0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions }), userFeedbackForm && (0, jsx_runtime_1.jsx)(UserFeedback_1.default, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact })), userFeedbackComplete && ((0, jsx_runtime_1.jsx)(UserFeedbackComplete_1.default, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && ((0, jsx_runtime_1.jsx)(QuickResponse_1.default, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
118
124
  var _a;
@@ -739,4 +739,15 @@ describe('Message', () => {
739
739
  const form = container.querySelector('form');
740
740
  expect(form).toHaveClass('test');
741
741
  });
742
+ it('should be able to disable markdown parsing', () => {
743
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: CODE_MESSAGE, isMarkdownDisabled: true }));
744
+ // this is looking for markdown syntax that is ordinarily stripped
745
+ expect(react_2.screen.getByText(/~~~yaml/i)).toBeTruthy();
746
+ });
747
+ it('should be able to pass props to react-markdown, such as disabling tags', () => {
748
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: CODE_MESSAGE, reactMarkdownProps: { disallowedElements: ['code'] } }));
749
+ expect(react_2.screen.getByText('Here is some YAML code:')).toBeTruthy();
750
+ // code block isn't rendering
751
+ expect(react_2.screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
752
+ });
742
753
  });
@@ -32,6 +32,7 @@ const MessageBarBase = (_a) => {
32
32
  const [message, setMessage] = (0, react_1.useState)(value !== null && value !== void 0 ? value : '');
33
33
  const [isListeningMessage, setIsListeningMessage] = (0, react_1.useState)(false);
34
34
  const [hasSentMessage, setHasSentMessage] = (0, react_1.useState)(false);
35
+ const [isComposing, setIsComposing] = (0, react_1.useState)(false);
35
36
  const inputRef = (0, react_1.useRef)(null);
36
37
  const textareaRef = (_b = innerRef) !== null && _b !== void 0 ? _b : inputRef;
37
38
  const attachButtonRef = (0, react_1.useRef)(null);
@@ -157,18 +158,32 @@ const MessageBarBase = (_a) => {
157
158
  setMessage('');
158
159
  }, [onSendMessage]);
159
160
  const handleKeyDown = (0, react_1.useCallback)((event) => {
160
- if (event.key === 'Enter' && !event.shiftKey) {
161
+ // Japanese and other languages may use IME for character input.
162
+ // In these cases, enter is used to select the final input, so we need to check for composition end instead.
163
+ // See more info at https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/
164
+ // Chrome, Edge, and Firefox seem to work well with just the compose event.
165
+ // Safari is a little bit special. We need to handle 229 as well in this case.
166
+ const nativeEvent = event.nativeEvent;
167
+ const isCompositionKey = nativeEvent.which === 229;
168
+ const isCurrentlyComposing = isComposing || isCompositionKey;
169
+ if (event.key === 'Enter' && !isCurrentlyComposing && !event.shiftKey) {
161
170
  event.preventDefault();
162
171
  if (!isSendButtonDisabled && !hasStopButton) {
163
172
  handleSend(message);
164
173
  }
165
174
  }
166
- if (event.key === 'Enter' && event.shiftKey) {
175
+ if (event.key === 'Enter' && !isCurrentlyComposing && event.shiftKey) {
167
176
  if (textareaRef.current) {
168
177
  handleNewLine(textareaRef.current);
169
178
  }
170
179
  }
171
- }, [isSendButtonDisabled, hasStopButton, handleSend, message]);
180
+ }, [isSendButtonDisabled, hasStopButton, handleSend, message, isComposing]);
181
+ const handleCompositionStart = (0, react_1.useCallback)(() => {
182
+ setIsComposing(true);
183
+ }, []);
184
+ const handleCompositionEnd = (0, react_1.useCallback)(() => {
185
+ setIsComposing(false);
186
+ }, []);
172
187
  const handleAttachMenuToggle = () => {
173
188
  (attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.setIsAttachMenuOpen) && (attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.setIsAttachMenuOpen(!(attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.isAttachMenuOpen)));
174
189
  attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.onAttachMenuToggleClick();
@@ -184,7 +199,7 @@ const MessageBarBase = (_a) => {
184
199
  }
185
200
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [attachMenuProps && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && ((0, jsx_runtime_1.jsx)(MicrophoneButton_1.default, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && ((0, jsx_runtime_1.jsx)(SendButton_1.default, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
186
201
  };
187
- const messageBarContents = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: (0, jsx_runtime_1.jsx)(react_core_1.TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown }, props)) }), (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
202
+ const messageBarContents = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: (0, jsx_runtime_1.jsx)(react_core_1.TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd }, props)) }), (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
188
203
  if (attachMenuProps) {
189
204
  return ((0, jsx_runtime_1.jsx)(AttachMenu_1.default, Object.assign({ toggle: (toggleRef) => ((0, jsx_runtime_1.jsx)("div", { ref: toggleRef, className: `pf-chatbot__message-bar ${className !== null && className !== void 0 ? className : ''}`, children: messageBarContents })), filteredItems: attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.attachMenuItems }, (attachMenuProps && { isOpen: attachMenuProps.isAttachMenuOpen }), { onOpenChange: (isAttachMenuOpen) => {
190
205
  var _a;
@@ -1,5 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react';
3
+ import { Options } from 'react-markdown';
3
4
  import { AlertProps, AvatarProps, ButtonProps, ExpandableSectionProps, ExpandableSectionToggleProps, FormProps, LabelGroupProps } from '@patternfly/react-core';
4
5
  import { ActionProps } from '../ResponseActions/ResponseActions';
5
6
  import { SourcesCardProps } from '../SourcesCard';
@@ -140,6 +141,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
140
141
  editFormProps?: FormProps;
141
142
  /** Sets message to compact styling. */
142
143
  isCompact?: boolean;
144
+ /** Disables markdown parsing for message, allowing only text input */
145
+ isMarkdownDisabled?: boolean;
146
+ /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */
147
+ reactMarkdownProps?: Options;
143
148
  }
144
149
  export declare const MessageBase: FunctionComponent<MessageProps>;
145
150
  declare const Message: import("react").ForwardRefExoticComponent<Omit<MessageProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -45,7 +45,7 @@ import ErrorMessage from './ErrorMessage/ErrorMessage';
45
45
  import MessageInput from './MessageInput';
46
46
  import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs';
47
47
  export const MessageBase = (_a) => {
48
- var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact"]);
48
+ var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps"]);
49
49
  const [messageText, setMessageText] = useState(content);
50
50
  useEffect(() => {
51
51
  setMessageText(content);
@@ -67,6 +67,41 @@ export const MessageBase = (_a) => {
67
67
  // Keep timestamps consistent between Timestamp component and aria-label
68
68
  const date = new Date();
69
69
  const dateString = timestamp !== null && timestamp !== void 0 ? timestamp : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
70
+ const handleMarkdown = () => {
71
+ if (isMarkdownDisabled) {
72
+ return (_jsx(TextMessage, Object.assign({ component: ContentVariants.p }, props, { children: messageText })));
73
+ }
74
+ return (_jsx(Markdown, Object.assign({ components: {
75
+ p: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.p }, props)),
76
+ code: (_a) => {
77
+ var { children } = _a, props = __rest(_a, ["children"]);
78
+ return (_jsx(CodeBlockMessage, Object.assign({}, props, codeBlockProps, { children: children })));
79
+ },
80
+ h1: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h1 }, props)),
81
+ h2: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h2 }, props)),
82
+ h3: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h3 }, props)),
83
+ h4: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h4 }, props)),
84
+ h5: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h5 }, props)),
85
+ h6: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h6 }, props)),
86
+ blockquote: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.blockquote }, props)),
87
+ ul: (props) => _jsx(UnorderedListMessage, Object.assign({}, props)),
88
+ ol: (props) => _jsx(OrderedListMessage, Object.assign({}, props)),
89
+ li: (props) => _jsx(ListItemMessage, Object.assign({}, props)),
90
+ table: (props) => _jsx(TableMessage, Object.assign({}, props, tableProps)),
91
+ tbody: (props) => _jsx(TbodyMessage, Object.assign({}, props)),
92
+ thead: (props) => _jsx(TheadMessage, Object.assign({}, props)),
93
+ tr: (props) => _jsx(TrMessage, Object.assign({}, props)),
94
+ td: (props) => {
95
+ // Conflicts with Td type
96
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
97
+ const { width } = props, rest = __rest(props, ["width"]);
98
+ return _jsx(TdMessage, Object.assign({}, rest));
99
+ },
100
+ th: (props) => _jsx(ThMessage, Object.assign({}, props)),
101
+ img: (props) => _jsx(ImageMessage, Object.assign({}, props)),
102
+ a: (props) => (_jsx(LinkMessage, Object.assign({ href: props.href, rel: props.rel, target: props.target }, linkProps, { children: props.children })))
103
+ }, remarkPlugins: [remarkGfm], rehypePlugins: rehypePlugins }, reactMarkdownProps, { children: messageText })));
104
+ };
70
105
  const renderMessage = () => {
71
106
  if (isLoading) {
72
107
  return _jsx(MessageLoading, { loadingWord: loadingWord });
@@ -77,36 +112,7 @@ export const MessageBase = (_a) => {
77
112
  setMessageText(value);
78
113
  }, onEditCancel: onEditCancel, inputRef: inputRef }, editFormProps))] }));
79
114
  }
80
- return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? (_jsx(ErrorMessage, Object.assign({}, error))) : (_jsx(Markdown, { components: {
81
- p: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.p }, props)),
82
- code: (_a) => {
83
- var { children } = _a, props = __rest(_a, ["children"]);
84
- return (_jsx(CodeBlockMessage, Object.assign({}, props, codeBlockProps, { children: children })));
85
- },
86
- h1: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h1 }, props)),
87
- h2: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h2 }, props)),
88
- h3: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h3 }, props)),
89
- h4: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h4 }, props)),
90
- h5: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h5 }, props)),
91
- h6: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.h6 }, props)),
92
- blockquote: (props) => _jsx(TextMessage, Object.assign({ component: ContentVariants.blockquote }, props)),
93
- ul: (props) => _jsx(UnorderedListMessage, Object.assign({}, props)),
94
- ol: (props) => _jsx(OrderedListMessage, Object.assign({}, props)),
95
- li: (props) => _jsx(ListItemMessage, Object.assign({}, props)),
96
- table: (props) => _jsx(TableMessage, Object.assign({}, props, tableProps)),
97
- tbody: (props) => _jsx(TbodyMessage, Object.assign({}, props)),
98
- thead: (props) => _jsx(TheadMessage, Object.assign({}, props)),
99
- tr: (props) => _jsx(TrMessage, Object.assign({}, props)),
100
- td: (props) => {
101
- // Conflicts with Td type
102
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
- const { width } = props, rest = __rest(props, ["width"]);
104
- return _jsx(TdMessage, Object.assign({}, rest));
105
- },
106
- th: (props) => _jsx(ThMessage, Object.assign({}, props)),
107
- img: (props) => _jsx(ImageMessage, Object.assign({}, props)),
108
- a: (props) => (_jsx(LinkMessage, Object.assign({ href: props.href, rel: props.rel, target: props.target }, linkProps, { children: props.children })))
109
- }, remarkPlugins: [remarkGfm], rehypePlugins: rehypePlugins, children: messageText }))] }));
115
+ return (_jsxs(_Fragment, { children: [beforeMainContent && _jsx(_Fragment, { children: beforeMainContent }), error ? _jsx(ErrorMessage, Object.assign({}, error)) : handleMarkdown()] }));
110
116
  };
111
117
  return (_jsxs("section", Object.assign({ "aria-label": `Message from ${role} - ${dateString}`, className: `pf-chatbot__message pf-chatbot__message--${role}`, "aria-live": isLiveRegion ? 'polite' : undefined, "aria-atomic": isLiveRegion ? false : undefined, ref: innerRef }, props, { children: [_jsx(Avatar, Object.assign({ className: `pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`, src: avatar, alt: "" }, avatarProps)), _jsxs("div", { className: "pf-chatbot__message-contents", children: [_jsxs("div", { className: "pf-chatbot__message-meta", children: [name && (_jsx("span", { className: "pf-chatbot__message-name", children: _jsx(Truncate, { content: name }) })), role === 'bot' && (_jsx(Label, { variant: "outline", isCompact: true, children: botWord })), _jsx(Timestamp, { date: date, children: timestamp })] }), _jsxs("div", { className: "pf-chatbot__message-response", children: [_jsxs("div", { className: "pf-chatbot__message-and-actions", children: [renderMessage(), afterMainContent && _jsx(_Fragment, { children: afterMainContent }), !isLoading && sources && _jsx(SourcesCard, Object.assign({}, sources, { isCompact: isCompact })), quickStarts && quickStarts.quickStart && (_jsx(QuickStartTile, { quickStart: quickStarts.quickStart, onSelectQuickStart: quickStarts.onSelectQuickStart, minuteWord: quickStarts.minuteWord, minuteWordPlural: quickStarts.minuteWordPlural, prerequisiteWord: quickStarts.prerequisiteWord, prerequisiteWordPlural: quickStarts.prerequisiteWordPlural, quickStartButtonAriaLabel: quickStarts.quickStartButtonAriaLabel, isCompact: isCompact })), !isLoading && !isEditable && actions && _jsx(ResponseActions, { actions: actions }), userFeedbackForm && _jsx(UserFeedback, Object.assign({}, userFeedbackForm, { timestamp: dateString, isCompact: isCompact })), userFeedbackComplete && (_jsx(UserFeedbackComplete, Object.assign({}, userFeedbackComplete, { timestamp: dateString, isCompact: isCompact }))), !isLoading && quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, isCompact: isCompact }))] }), attachments && (_jsx("div", { className: "pf-chatbot__message-attachments-container", children: attachments.map((attachment) => {
112
118
  var _a;
@@ -734,4 +734,15 @@ describe('Message', () => {
734
734
  const form = container.querySelector('form');
735
735
  expect(form).toHaveClass('test');
736
736
  });
737
+ it('should be able to disable markdown parsing', () => {
738
+ render(_jsx(Message, { avatar: "./img", role: "user", name: "User", content: CODE_MESSAGE, isMarkdownDisabled: true }));
739
+ // this is looking for markdown syntax that is ordinarily stripped
740
+ expect(screen.getByText(/~~~yaml/i)).toBeTruthy();
741
+ });
742
+ it('should be able to pass props to react-markdown, such as disabling tags', () => {
743
+ render(_jsx(Message, { avatar: "./img", role: "user", name: "User", content: CODE_MESSAGE, reactMarkdownProps: { disallowedElements: ['code'] } }));
744
+ expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
745
+ // code block isn't rendering
746
+ expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
747
+ });
737
748
  });
@@ -26,6 +26,7 @@ export const MessageBarBase = (_a) => {
26
26
  const [message, setMessage] = useState(value !== null && value !== void 0 ? value : '');
27
27
  const [isListeningMessage, setIsListeningMessage] = useState(false);
28
28
  const [hasSentMessage, setHasSentMessage] = useState(false);
29
+ const [isComposing, setIsComposing] = useState(false);
29
30
  const inputRef = useRef(null);
30
31
  const textareaRef = (_b = innerRef) !== null && _b !== void 0 ? _b : inputRef;
31
32
  const attachButtonRef = useRef(null);
@@ -151,18 +152,32 @@ export const MessageBarBase = (_a) => {
151
152
  setMessage('');
152
153
  }, [onSendMessage]);
153
154
  const handleKeyDown = useCallback((event) => {
154
- if (event.key === 'Enter' && !event.shiftKey) {
155
+ // Japanese and other languages may use IME for character input.
156
+ // In these cases, enter is used to select the final input, so we need to check for composition end instead.
157
+ // See more info at https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/
158
+ // Chrome, Edge, and Firefox seem to work well with just the compose event.
159
+ // Safari is a little bit special. We need to handle 229 as well in this case.
160
+ const nativeEvent = event.nativeEvent;
161
+ const isCompositionKey = nativeEvent.which === 229;
162
+ const isCurrentlyComposing = isComposing || isCompositionKey;
163
+ if (event.key === 'Enter' && !isCurrentlyComposing && !event.shiftKey) {
155
164
  event.preventDefault();
156
165
  if (!isSendButtonDisabled && !hasStopButton) {
157
166
  handleSend(message);
158
167
  }
159
168
  }
160
- if (event.key === 'Enter' && event.shiftKey) {
169
+ if (event.key === 'Enter' && !isCurrentlyComposing && event.shiftKey) {
161
170
  if (textareaRef.current) {
162
171
  handleNewLine(textareaRef.current);
163
172
  }
164
173
  }
165
- }, [isSendButtonDisabled, hasStopButton, handleSend, message]);
174
+ }, [isSendButtonDisabled, hasStopButton, handleSend, message, isComposing]);
175
+ const handleCompositionStart = useCallback(() => {
176
+ setIsComposing(true);
177
+ }, []);
178
+ const handleCompositionEnd = useCallback(() => {
179
+ setIsComposing(false);
180
+ }, []);
166
181
  const handleAttachMenuToggle = () => {
167
182
  (attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.setIsAttachMenuOpen) && (attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.setIsAttachMenuOpen(!(attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.isAttachMenuOpen)));
168
183
  attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.onAttachMenuToggleClick();
@@ -178,7 +193,7 @@ export const MessageBarBase = (_a) => {
178
193
  }
179
194
  return (_jsxs(_Fragment, { children: [attachMenuProps && (_jsx(AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && (_jsx(AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && (_jsx(MicrophoneButton, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && (_jsx(SendButton, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
180
195
  };
181
- const messageBarContents = (_jsxs(_Fragment, { children: [_jsx("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: _jsx(TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown }, props)) }), _jsx("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
196
+ const messageBarContents = (_jsxs(_Fragment, { children: [_jsx("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: _jsx(TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd }, props)) }), _jsx("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
182
197
  if (attachMenuProps) {
183
198
  return (_jsx(AttachMenu, Object.assign({ toggle: (toggleRef) => (_jsx("div", { ref: toggleRef, className: `pf-chatbot__message-bar ${className !== null && className !== void 0 ? className : ''}`, children: messageBarContents })), filteredItems: attachMenuProps === null || attachMenuProps === void 0 ? void 0 : attachMenuProps.attachMenuItems }, (attachMenuProps && { isOpen: attachMenuProps.isAttachMenuOpen }), { onOpenChange: (isAttachMenuOpen) => {
184
199
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.4.0-prerelease.3",
3
+ "version": "6.4.0-prerelease.5",
4
4
  "description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -962,4 +962,23 @@ describe('Message', () => {
962
962
  const form = container.querySelector('form');
963
963
  expect(form).toHaveClass('test');
964
964
  });
965
+ it('should be able to disable markdown parsing', () => {
966
+ render(<Message avatar="./img" role="user" name="User" content={CODE_MESSAGE} isMarkdownDisabled />);
967
+ // this is looking for markdown syntax that is ordinarily stripped
968
+ expect(screen.getByText(/~~~yaml/i)).toBeTruthy();
969
+ });
970
+ it('should be able to pass props to react-markdown, such as disabling tags', () => {
971
+ render(
972
+ <Message
973
+ avatar="./img"
974
+ role="user"
975
+ name="User"
976
+ content={CODE_MESSAGE}
977
+ reactMarkdownProps={{ disallowedElements: ['code'] }}
978
+ />
979
+ );
980
+ expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
981
+ // code block isn't rendering
982
+ expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
983
+ });
965
984
  });
@@ -3,7 +3,7 @@
3
3
  // ============================================================================
4
4
  import { forwardRef, ReactNode, useEffect, useState } from 'react';
5
5
  import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react';
6
- import Markdown from 'react-markdown';
6
+ import Markdown, { Options } from 'react-markdown';
7
7
  import remarkGfm from 'remark-gfm';
8
8
  import {
9
9
  AlertProps,
@@ -185,6 +185,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
185
185
  editFormProps?: FormProps;
186
186
  /** Sets message to compact styling. */
187
187
  isCompact?: boolean;
188
+ /** Disables markdown parsing for message, allowing only text input */
189
+ isMarkdownDisabled?: boolean;
190
+ /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */
191
+ reactMarkdownProps?: Options;
188
192
  }
189
193
 
190
194
  export const MessageBase: FunctionComponent<MessageProps> = ({
@@ -224,6 +228,8 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
224
228
  inputRef,
225
229
  editFormProps,
226
230
  isCompact,
231
+ isMarkdownDisabled,
232
+ reactMarkdownProps,
227
233
  ...props
228
234
  }: MessageProps) => {
229
235
  const [messageText, setMessageText] = useState(content);
@@ -250,6 +256,60 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
250
256
  const date = new Date();
251
257
  const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
252
258
 
259
+ const handleMarkdown = () => {
260
+ if (isMarkdownDisabled) {
261
+ return (
262
+ <TextMessage component={ContentVariants.p} {...props}>
263
+ {messageText}
264
+ </TextMessage>
265
+ );
266
+ }
267
+ return (
268
+ <Markdown
269
+ components={{
270
+ p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
271
+ code: ({ children, ...props }) => (
272
+ <CodeBlockMessage {...props} {...codeBlockProps}>
273
+ {children}
274
+ </CodeBlockMessage>
275
+ ),
276
+ h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
277
+ h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
278
+ h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
279
+ h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
280
+ h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
281
+ h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
282
+ blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
283
+ ul: (props) => <UnorderedListMessage {...props} />,
284
+ ol: (props) => <OrderedListMessage {...props} />,
285
+ li: (props) => <ListItemMessage {...props} />,
286
+ table: (props) => <TableMessage {...props} {...tableProps} />,
287
+ tbody: (props) => <TbodyMessage {...props} />,
288
+ thead: (props) => <TheadMessage {...props} />,
289
+ tr: (props) => <TrMessage {...props} />,
290
+ td: (props) => {
291
+ // Conflicts with Td type
292
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
293
+ const { width, ...rest } = props;
294
+ return <TdMessage {...rest} />;
295
+ },
296
+ th: (props) => <ThMessage {...props} />,
297
+ img: (props) => <ImageMessage {...props} />,
298
+ a: (props) => (
299
+ <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
300
+ {props.children}
301
+ </LinkMessage>
302
+ )
303
+ }}
304
+ remarkPlugins={[remarkGfm]}
305
+ rehypePlugins={rehypePlugins}
306
+ {...reactMarkdownProps}
307
+ >
308
+ {messageText}
309
+ </Markdown>
310
+ );
311
+ };
312
+
253
313
  const renderMessage = () => {
254
314
  if (isLoading) {
255
315
  return <MessageLoading loadingWord={loadingWord} />;
@@ -277,51 +337,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
277
337
  return (
278
338
  <>
279
339
  {beforeMainContent && <>{beforeMainContent}</>}
280
- {error ? (
281
- <ErrorMessage {...error} />
282
- ) : (
283
- <Markdown
284
- components={{
285
- p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
286
- code: ({ children, ...props }) => (
287
- <CodeBlockMessage {...props} {...codeBlockProps}>
288
- {children}
289
- </CodeBlockMessage>
290
- ),
291
- h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
292
- h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
293
- h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
294
- h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
295
- h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
296
- h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
297
- blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
298
- ul: (props) => <UnorderedListMessage {...props} />,
299
- ol: (props) => <OrderedListMessage {...props} />,
300
- li: (props) => <ListItemMessage {...props} />,
301
- table: (props) => <TableMessage {...props} {...tableProps} />,
302
- tbody: (props) => <TbodyMessage {...props} />,
303
- thead: (props) => <TheadMessage {...props} />,
304
- tr: (props) => <TrMessage {...props} />,
305
- td: (props) => {
306
- // Conflicts with Td type
307
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
308
- const { width, ...rest } = props;
309
- return <TdMessage {...rest} />;
310
- },
311
- th: (props) => <ThMessage {...props} />,
312
- img: (props) => <ImageMessage {...props} />,
313
- a: (props) => (
314
- <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
315
- {props.children}
316
- </LinkMessage>
317
- )
318
- }}
319
- remarkPlugins={[remarkGfm]}
320
- rehypePlugins={rehypePlugins}
321
- >
322
- {messageText}
323
- </Markdown>
324
- )}
340
+ {error ? <ErrorMessage {...error} /> : handleMarkdown()}
325
341
  </>
326
342
  );
327
343
  };
@@ -141,6 +141,7 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
141
141
  const [message, setMessage] = useState<string | number>(value ?? '');
142
142
  const [isListeningMessage, setIsListeningMessage] = useState<boolean>(false);
143
143
  const [hasSentMessage, setHasSentMessage] = useState(false);
144
+ const [isComposing, setIsComposing] = useState(false);
144
145
  const inputRef = useRef<HTMLTextAreaElement>(null);
145
146
  const textareaRef = (innerRef as React.RefObject<HTMLTextAreaElement>) ?? inputRef;
146
147
  const attachButtonRef = useRef<HTMLButtonElement>(null);
@@ -285,21 +286,38 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
285
286
 
286
287
  const handleKeyDown = useCallback(
287
288
  (event: ReactKeyboardEvent) => {
288
- if (event.key === 'Enter' && !event.shiftKey) {
289
+ // Japanese and other languages may use IME for character input.
290
+ // In these cases, enter is used to select the final input, so we need to check for composition end instead.
291
+ // See more info at https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/
292
+ // Chrome, Edge, and Firefox seem to work well with just the compose event.
293
+ // Safari is a little bit special. We need to handle 229 as well in this case.
294
+ const nativeEvent = event.nativeEvent as KeyboardEvent;
295
+ const isCompositionKey = nativeEvent.which === 229;
296
+ const isCurrentlyComposing = isComposing || isCompositionKey;
297
+
298
+ if (event.key === 'Enter' && !isCurrentlyComposing && !event.shiftKey) {
289
299
  event.preventDefault();
290
300
  if (!isSendButtonDisabled && !hasStopButton) {
291
301
  handleSend(message);
292
302
  }
293
303
  }
294
- if (event.key === 'Enter' && event.shiftKey) {
304
+ if (event.key === 'Enter' && !isCurrentlyComposing && event.shiftKey) {
295
305
  if (textareaRef.current) {
296
306
  handleNewLine(textareaRef.current);
297
307
  }
298
308
  }
299
309
  },
300
- [isSendButtonDisabled, hasStopButton, handleSend, message]
310
+ [isSendButtonDisabled, hasStopButton, handleSend, message, isComposing]
301
311
  );
302
312
 
313
+ const handleCompositionStart = useCallback(() => {
314
+ setIsComposing(true);
315
+ }, []);
316
+
317
+ const handleCompositionEnd = useCallback(() => {
318
+ setIsComposing(false);
319
+ }, []);
320
+
303
321
  const handleAttachMenuToggle = () => {
304
322
  attachMenuProps?.setIsAttachMenuOpen && attachMenuProps?.setIsAttachMenuOpen(!attachMenuProps?.isAttachMenuOpen);
305
323
  attachMenuProps?.onAttachMenuToggleClick();
@@ -402,6 +420,8 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
402
420
  placeholder={isListeningMessage ? listeningText : placeholder}
403
421
  ref={textareaRef}
404
422
  onKeyDown={handleKeyDown}
423
+ onCompositionStart={handleCompositionStart}
424
+ onCompositionEnd={handleCompositionEnd}
405
425
  {...props}
406
426
  />
407
427
  </div>