@jupyter/chat 0.19.0 → 0.20.0-alpha.0

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.
@@ -40,7 +40,6 @@ export function ChatBody(props) {
40
40
  (_a = model === null || model === void 0 ? void 0 : model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
41
41
  };
42
42
  }, [model]);
43
- // const horizontalPadding = props.area === 'main' ? 8 : 4;
44
43
  const horizontalPadding = 4;
45
44
  const contextValue = {
46
45
  ...props,
@@ -1,21 +1,18 @@
1
1
  import { PromiseDelegate } from '@lumino/coreutils';
2
2
  import React from 'react';
3
+ import { IMessageContent } from '../../types';
3
4
  /**
4
5
  * The type of the props for the MessageRenderer component.
5
6
  */
6
7
  type MessageRendererProps = {
7
8
  /**
8
- * The string to render.
9
+ * The string or rendermime bundle to render.
9
10
  */
10
- markdownStr: string;
11
+ message: IMessageContent;
11
12
  /**
12
13
  * The promise to resolve when the message is rendered.
13
14
  */
14
15
  rendered: PromiseDelegate<void>;
15
- /**
16
- * Whether to append the content to the existing content or not.
17
- */
18
- appendContent?: boolean;
19
16
  /**
20
17
  * The function to call to edit a message.
21
18
  */
@@ -2,51 +2,126 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- import React, { useState, useEffect } from 'react';
5
+ import { MessageLoop } from '@lumino/messaging';
6
+ import { Widget } from '@lumino/widgets';
7
+ import React, { useState, useEffect, useRef } from 'react';
6
8
  import { createPortal } from 'react-dom';
7
9
  import { MessageToolbar } from './toolbar';
8
10
  import { CodeToolbar } from '../code-blocks/code-toolbar';
9
11
  import { useChatContext } from '../../context';
10
- import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
12
+ import { replaceMentionToSpan } from '../../utils';
13
+ const RENDERED_CLASS = 'jp-chat-rendered-message';
14
+ const DEFAULT_MIME_TYPE = 'text/markdown';
11
15
  /**
12
16
  * The message renderer base component.
13
17
  */
14
18
  function MessageRendererBase(props) {
15
- const { markdownStr } = props;
19
+ const { message } = props;
16
20
  const { model, rmRegistry } = useChatContext();
17
- const appendContent = props.appendContent || false;
18
- const [renderedContent, setRenderedContent] = useState(null);
19
- // each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
21
+ const containerRef = useRef(null);
22
+ // Allow edition only on text messages.
23
+ const [canEdit, setCanEdit] = useState(false);
24
+ // Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
20
25
  const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
21
26
  useEffect(() => {
27
+ let node = null;
28
+ const container = containerRef.current;
29
+ if (!container) {
30
+ return;
31
+ }
22
32
  const renderContent = async () => {
23
- const renderer = await MarkdownRenderer.renderContent({
24
- content: markdownStr,
25
- rmRegistry
26
- });
27
- const newCodeToolbarDefns = [];
28
- // Attach CodeToolbar root element to each <pre> block
29
- const preBlocks = renderer.node.querySelectorAll('pre');
30
- preBlocks.forEach(preBlock => {
31
- var _a;
32
- const codeToolbarRoot = document.createElement('div');
33
- (_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
34
- newCodeToolbarDefns.push([
35
- codeToolbarRoot,
36
- { model: model, content: preBlock.textContent || '' }
37
- ]);
38
- });
39
- setCodeToolbarDefns(newCodeToolbarDefns);
40
- setRenderedContent(renderer.node);
33
+ var _a, _b;
34
+ let isMarkdownRenderer = true;
35
+ let renderer;
36
+ let mimeModel;
37
+ // Create the renderer and the mime model.
38
+ if (typeof message.body === 'string') {
39
+ // Allow editing content for text messages.
40
+ setCanEdit(true);
41
+ // Improve users display in markdown content.
42
+ let mdStr = message.body;
43
+ (_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
44
+ mdStr = replaceMentionToSpan(mdStr, user);
45
+ });
46
+ // Body is a string, use the markdown renderer.
47
+ renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
48
+ mimeModel = rmRegistry.createModel({
49
+ data: { [DEFAULT_MIME_TYPE]: mdStr }
50
+ });
51
+ }
52
+ else {
53
+ setCanEdit(false);
54
+ // This is a mime bundle.
55
+ let mimeContent = message.body;
56
+ let preferred = rmRegistry.preferredMimeType(mimeContent.data, 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
57
+ );
58
+ if (!preferred) {
59
+ preferred = DEFAULT_MIME_TYPE;
60
+ mimeContent = {
61
+ data: {
62
+ [DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
63
+ }
64
+ };
65
+ }
66
+ renderer = rmRegistry.createRenderer(preferred);
67
+ // Improve users display in markdown content.
68
+ if (preferred === DEFAULT_MIME_TYPE) {
69
+ let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
70
+ if (mdStr) {
71
+ (_b = message.mentions) === null || _b === void 0 ? void 0 : _b.forEach(user => {
72
+ mdStr = replaceMentionToSpan(mdStr, user);
73
+ });
74
+ mimeContent = {
75
+ ...mimeContent,
76
+ data: {
77
+ ...mimeContent.data,
78
+ [DEFAULT_MIME_TYPE]: mdStr
79
+ }
80
+ };
81
+ }
82
+ }
83
+ else {
84
+ isMarkdownRenderer = false;
85
+ }
86
+ mimeModel = rmRegistry.createModel(mimeContent);
87
+ }
88
+ await renderer.renderModel(mimeModel);
89
+ // Manually trigger the onAfterAttach of the renderer, because the widget will
90
+ // never been attached, only the node.
91
+ // This is necessary to render latex.
92
+ MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
93
+ // Add code toolbar if markdown has been rendered.
94
+ if (isMarkdownRenderer) {
95
+ const newCodeToolbarDefns = [];
96
+ // Attach CodeToolbar root element to each <pre> block
97
+ const preBlocks = renderer.node.querySelectorAll('pre');
98
+ preBlocks.forEach(preBlock => {
99
+ var _a;
100
+ const codeToolbarRoot = document.createElement('div');
101
+ (_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
102
+ newCodeToolbarDefns.push([
103
+ codeToolbarRoot,
104
+ { model: model, content: preBlock.textContent || '' }
105
+ ]);
106
+ });
107
+ setCodeToolbarDefns(newCodeToolbarDefns);
108
+ }
109
+ // Add the rendered node to the DOM.
110
+ node = renderer.node;
111
+ container.insertBefore(node, container.firstChild);
41
112
  // Resolve the rendered promise.
42
113
  props.rendered.resolve();
43
114
  };
44
115
  renderContent();
45
- }, [markdownStr, rmRegistry]);
46
- return (React.createElement("div", { className: MD_RENDERED_CLASS },
47
- renderedContent &&
48
- (appendContent ? (React.createElement("div", { ref: node => node && node.appendChild(renderedContent) })) : (React.createElement("div", { ref: node => node && node.replaceChildren(renderedContent) }))),
49
- React.createElement(MessageToolbar, { edit: props.edit, delete: props.delete }),
116
+ return () => {
117
+ if (node && container.contains(node)) {
118
+ container.removeChild(node);
119
+ }
120
+ node = null;
121
+ };
122
+ }, [message.body, message.mentions, rmRegistry]);
123
+ return (React.createElement("div", { className: RENDERED_CLASS, ref: containerRef },
124
+ React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
50
125
  // Render a `CodeToolbar` element underneath each code block.
51
126
  // We use ReactDOM.createPortal() so each `CodeToolbar` element is able
52
127
  // to use the context in the main React tree.
@@ -57,7 +57,7 @@ export const ChatMessage = forwardRef((props, ref) => {
57
57
  // Create an input model only if the message is edited.
58
58
  const startEdition = () => {
59
59
  var _a;
60
- if (!canEdit) {
60
+ if (!canEdit || !(typeof message.body === 'string')) {
61
61
  return;
62
62
  }
63
63
  let body = message.body;
@@ -111,7 +111,7 @@ export const ChatMessage = forwardRef((props, ref) => {
111
111
  };
112
112
  // Empty if the message has been deleted.
113
113
  return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
114
- edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, { markdownStr: message.body, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
114
+ edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, { message: message, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
115
115
  message.attachments && !edit && (
116
116
  // Display the attachments only if message is not edited, otherwise the
117
117
  // input component display them.
@@ -14,7 +14,7 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
14
14
  export function MessageToolbar(props) {
15
15
  const buttons = [];
16
16
  if (props.edit !== undefined) {
17
- const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
17
+ const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
18
18
  React.createElement(EditIcon, null)));
19
19
  buttons.push(editButton);
20
20
  }
@@ -2,11 +2,12 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- import { classes } from '@jupyterlab/ui-components';
5
+ import { MessageLoop } from '@lumino/messaging';
6
+ import { Widget } from '@lumino/widgets';
6
7
  import React, { useEffect, useRef } from 'react';
7
8
  import { useChatContext } from '../../context';
8
- import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
9
9
  const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
10
+ const MD_MIME_TYPE = 'text/markdown';
10
11
  /**
11
12
  * The welcome message component.
12
13
  * This message is displayed on top of the chat messages, and is rendered using a
@@ -17,26 +18,35 @@ export function WelcomeMessage(props) {
17
18
  const content = props.content + '\n----\n';
18
19
  // ref that tracks the content container to store the rendermime node in
19
20
  const renderingContainer = useRef(null);
20
- // ref that tracks whether the rendermime node has already been inserted
21
- const renderingInserted = useRef(false);
22
21
  /**
23
22
  * Effect: use Rendermime to render `props.markdownStr` into an HTML element,
24
23
  * and insert it into `renderingContainer` if not yet inserted.
25
24
  */
26
25
  useEffect(() => {
26
+ let node = null;
27
27
  const renderContent = async () => {
28
- const renderer = await MarkdownRenderer.renderContent({
29
- content,
30
- rmRegistry
28
+ var _a;
29
+ // Render the welcome message using markdown renderer.
30
+ const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
31
+ const mimeModel = rmRegistry.createModel({
32
+ data: { [MD_MIME_TYPE]: content }
31
33
  });
32
- // insert the rendering into renderingContainer if not yet inserted
33
- if (renderingContainer.current !== null && !renderingInserted.current) {
34
- renderingContainer.current.appendChild(renderer.node);
35
- renderingInserted.current = true;
36
- }
34
+ await renderer.renderModel(mimeModel);
35
+ // Manually trigger the onAfterAttach of the renderer, because the widget will
36
+ // never been attached, only the node.
37
+ // This is necessary to render latex.
38
+ MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
39
+ node = renderer.node;
40
+ (_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.append(node);
37
41
  };
38
42
  renderContent();
43
+ return () => {
44
+ var _a;
45
+ if (node && ((_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.contains(node))) {
46
+ renderingContainer.current.removeChild(node);
47
+ }
48
+ node = null;
49
+ };
39
50
  }, [content]);
40
- return (React.createElement("div", { className: classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS) },
41
- React.createElement("div", { ref: renderingContainer })));
51
+ return React.createElement("div", { className: WELCOME_MESSAGE_CLASS, ref: renderingContainer });
42
52
  }
package/lib/index.d.ts CHANGED
@@ -2,7 +2,6 @@ export * from './active-cell-manager';
2
2
  export * from './components';
3
3
  export * from './icons';
4
4
  export * from './input-model';
5
- export * from './markdown-renderer';
6
5
  export * from './model';
7
6
  export * from './registers';
8
7
  export * from './selection-watcher';
package/lib/index.js CHANGED
@@ -6,7 +6,6 @@ export * from './active-cell-manager';
6
6
  export * from './components';
7
7
  export * from './icons';
8
8
  export * from './input-model';
9
- export * from './markdown-renderer';
10
9
  export * from './model';
11
10
  export * from './registers';
12
11
  export * from './selection-watcher';
package/lib/message.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { IRenderMime } from '@jupyterlab/rendermime';
1
2
  import { ISignal } from '@lumino/signaling';
2
3
  import { IAttachment, IMessageContent, IMessage, IUser } from './types';
3
4
  /**
@@ -18,7 +19,7 @@ export declare class Message implements IMessage {
18
19
  * Getters for each attribute individually.
19
20
  */
20
21
  get type(): string;
21
- get body(): string;
22
+ get body(): string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
22
23
  get id(): string;
23
24
  get time(): number;
24
25
  get sender(): IUser;
package/lib/model.js CHANGED
@@ -7,7 +7,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
7
7
  import { Signal } from '@lumino/signaling';
8
8
  import { InputModel } from './input-model';
9
9
  import { Message } from './message';
10
- import { replaceMentionToSpan } from './utils';
11
10
  /**
12
11
  * An abstract implementation of IChatModel.
13
12
  *
@@ -289,10 +288,6 @@ export class AbstractChatModel {
289
288
  * Can be useful if some actions are required on the message.
290
289
  */
291
290
  formatChatMessage(message) {
292
- var _a;
293
- (_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
294
- message.body = replaceMentionToSpan(message.body, user);
295
- });
296
291
  return message;
297
292
  }
298
293
  /**
@@ -15,8 +15,9 @@ export async function pollUntilReady() {
15
15
  }
16
16
  export async function getJupyterLabTheme() {
17
17
  await pollUntilReady();
18
- const light = document.body.getAttribute('data-jp-theme-light');
18
+ const light = document.body.getAttribute('data-jp-theme-light') === 'true';
19
19
  return createTheme({
20
+ cssVariables: true,
20
21
  spacing: 4,
21
22
  components: {
22
23
  MuiButton: {
@@ -40,12 +41,12 @@ export async function getJupyterLabTheme() {
40
41
  // The default style for input toolbar button variant.
41
42
  props: { variant: 'input-toolbar' },
42
43
  style: {
43
- backgroundColor: 'var(--jp-brand-color1)',
44
- color: 'white',
44
+ backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
45
+ color: 'var(--jp-ui-inverse-font-color1)',
45
46
  borderRadius: '4px',
46
47
  boxShadow: 'none',
47
48
  '&:hover': {
48
- backgroundColor: 'var(--jp-brand-color0)',
49
+ backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
49
50
  boxShadow: 'none'
50
51
  },
51
52
  '&:disabled': {
@@ -97,12 +98,12 @@ export async function getJupyterLabTheme() {
97
98
  // The default style for input toolbar button variant.
98
99
  props: { variant: 'input-toolbar' },
99
100
  style: {
100
- backgroundColor: 'var(--jp-brand-color1)',
101
- color: 'white',
101
+ backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
102
+ color: 'var(--jp-ui-inverse-font-color1)',
102
103
  borderRadius: '4px',
103
104
  boxShadow: 'none',
104
105
  '&:hover': {
105
- backgroundColor: 'var(--jp-brand-color0)',
106
+ backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
106
107
  boxShadow: 'none'
107
108
  },
108
109
  '&:disabled': {
@@ -162,9 +163,9 @@ export async function getJupyterLabTheme() {
162
163
  paper: getCSSVariable('--jp-layout-color1'),
163
164
  default: getCSSVariable('--jp-layout-color1')
164
165
  },
165
- mode: light === 'true' ? 'light' : 'dark',
166
+ mode: light ? 'light' : 'dark',
166
167
  primary: {
167
- main: getCSSVariable('--jp-brand-color1'),
168
+ main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
168
169
  light: getCSSVariable('--jp-brand-color2'),
169
170
  dark: getCSSVariable('--jp-brand-color0')
170
171
  },
package/lib/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ISignal } from '@lumino/signaling';
2
+ import { IRenderMime } from '@jupyterlab/rendermime';
2
3
  /**
3
4
  * The user description.
4
5
  */
@@ -57,7 +58,7 @@ export interface IConfig {
57
58
  */
58
59
  export type IMessageContent<T = IUser, U = IAttachment> = {
59
60
  type: string;
60
- body: string;
61
+ body: string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
61
62
  id: string;
62
63
  time: number;
63
64
  sender: T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.19.0",
3
+ "version": "0.20.0-alpha.0",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -60,7 +60,6 @@ export function ChatBody(props: Chat.IChatProps): JSX.Element {
60
60
  };
61
61
  }, [model]);
62
62
 
63
- // const horizontalPadding = props.area === 'main' ? 8 : 4;
64
63
  const horizontalPadding = 4;
65
64
 
66
65
  const contextValue: Chat.IChatProps = {
@@ -3,31 +3,34 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { IRenderMime } from '@jupyterlab/rendermime';
6
7
  import { PromiseDelegate } from '@lumino/coreutils';
7
- import React, { useState, useEffect } from 'react';
8
+ import { MessageLoop } from '@lumino/messaging';
9
+ import { Widget } from '@lumino/widgets';
10
+ import React, { useState, useEffect, useRef } from 'react';
8
11
  import { createPortal } from 'react-dom';
9
12
 
10
13
  import { MessageToolbar } from './toolbar';
11
14
  import { CodeToolbar, CodeToolbarProps } from '../code-blocks/code-toolbar';
12
15
  import { useChatContext } from '../../context';
13
- import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
16
+ import { IMessageContent } from '../../types';
17
+ import { replaceMentionToSpan } from '../../utils';
18
+
19
+ const RENDERED_CLASS = 'jp-chat-rendered-message';
20
+ const DEFAULT_MIME_TYPE = 'text/markdown';
14
21
 
15
22
  /**
16
23
  * The type of the props for the MessageRenderer component.
17
24
  */
18
25
  type MessageRendererProps = {
19
26
  /**
20
- * The string to render.
27
+ * The string or rendermime bundle to render.
21
28
  */
22
- markdownStr: string;
29
+ message: IMessageContent;
23
30
  /**
24
31
  * The promise to resolve when the message is rendered.
25
32
  */
26
33
  rendered: PromiseDelegate<void>;
27
- /**
28
- * Whether to append the content to the existing content or not.
29
- */
30
- appendContent?: boolean;
31
34
  /**
32
35
  * The function to call to edit a message.
33
36
  */
@@ -42,60 +45,138 @@ type MessageRendererProps = {
42
45
  * The message renderer base component.
43
46
  */
44
47
  function MessageRendererBase(props: MessageRendererProps): JSX.Element {
45
- const { markdownStr } = props;
48
+ const { message } = props;
46
49
  const { model, rmRegistry } = useChatContext();
47
- const appendContent = props.appendContent || false;
48
- const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
49
- null
50
- );
51
50
 
52
- // each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
51
+ const containerRef = useRef<HTMLDivElement>(null);
52
+
53
+ // Allow edition only on text messages.
54
+ const [canEdit, setCanEdit] = useState<boolean>(false);
55
+
56
+ // Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
53
57
  const [codeToolbarDefns, setCodeToolbarDefns] = useState<
54
58
  Array<[HTMLDivElement, CodeToolbarProps]>
55
59
  >([]);
56
60
 
57
61
  useEffect(() => {
62
+ let node: HTMLElement | null = null;
63
+ const container = containerRef.current;
64
+ if (!container) {
65
+ return;
66
+ }
67
+
58
68
  const renderContent = async () => {
59
- const renderer = await MarkdownRenderer.renderContent({
60
- content: markdownStr,
61
- rmRegistry
62
- });
63
-
64
- const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
65
-
66
- // Attach CodeToolbar root element to each <pre> block
67
- const preBlocks = renderer.node.querySelectorAll('pre');
68
- preBlocks.forEach(preBlock => {
69
- const codeToolbarRoot = document.createElement('div');
70
- preBlock.parentNode?.insertBefore(
71
- codeToolbarRoot,
72
- preBlock.nextSibling
69
+ let isMarkdownRenderer = true;
70
+ let renderer: IRenderMime.IRenderer;
71
+ let mimeModel: IRenderMime.IMimeModel;
72
+
73
+ // Create the renderer and the mime model.
74
+ if (typeof message.body === 'string') {
75
+ // Allow editing content for text messages.
76
+ setCanEdit(true);
77
+
78
+ // Improve users display in markdown content.
79
+ let mdStr = message.body;
80
+ message.mentions?.forEach(user => {
81
+ mdStr = replaceMentionToSpan(mdStr, user);
82
+ });
83
+
84
+ // Body is a string, use the markdown renderer.
85
+ renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
86
+ mimeModel = rmRegistry.createModel({
87
+ data: { [DEFAULT_MIME_TYPE]: mdStr }
88
+ });
89
+ } else {
90
+ setCanEdit(false);
91
+ // This is a mime bundle.
92
+ let mimeContent = message.body;
93
+ let preferred = rmRegistry.preferredMimeType(
94
+ mimeContent.data,
95
+ 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
73
96
  );
74
- newCodeToolbarDefns.push([
75
- codeToolbarRoot,
76
- { model: model, content: preBlock.textContent || '' }
77
- ]);
78
- });
97
+ if (!preferred) {
98
+ preferred = DEFAULT_MIME_TYPE;
99
+ mimeContent = {
100
+ data: {
101
+ [DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
102
+ }
103
+ };
104
+ }
105
+ renderer = rmRegistry.createRenderer(preferred);
106
+
107
+ // Improve users display in markdown content.
108
+ if (preferred === DEFAULT_MIME_TYPE) {
109
+ let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
110
+ if (mdStr) {
111
+ message.mentions?.forEach(user => {
112
+ mdStr = replaceMentionToSpan(mdStr as string, user);
113
+ });
114
+ mimeContent = {
115
+ ...mimeContent,
116
+ data: {
117
+ ...mimeContent.data,
118
+ [DEFAULT_MIME_TYPE]: mdStr
119
+ }
120
+ };
121
+ }
122
+ } else {
123
+ isMarkdownRenderer = false;
124
+ }
79
125
 
80
- setCodeToolbarDefns(newCodeToolbarDefns);
81
- setRenderedContent(renderer.node);
126
+ mimeModel = rmRegistry.createModel(mimeContent);
127
+ }
128
+ await renderer.renderModel(mimeModel);
129
+
130
+ // Manually trigger the onAfterAttach of the renderer, because the widget will
131
+ // never been attached, only the node.
132
+ // This is necessary to render latex.
133
+ MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
134
+
135
+ // Add code toolbar if markdown has been rendered.
136
+ if (isMarkdownRenderer) {
137
+ const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
138
+
139
+ // Attach CodeToolbar root element to each <pre> block
140
+ const preBlocks = renderer.node.querySelectorAll('pre');
141
+ preBlocks.forEach(preBlock => {
142
+ const codeToolbarRoot = document.createElement('div');
143
+ preBlock.parentNode?.insertBefore(
144
+ codeToolbarRoot,
145
+ preBlock.nextSibling
146
+ );
147
+ newCodeToolbarDefns.push([
148
+ codeToolbarRoot,
149
+ { model: model, content: preBlock.textContent || '' }
150
+ ]);
151
+ });
152
+
153
+ setCodeToolbarDefns(newCodeToolbarDefns);
154
+ }
155
+
156
+ // Add the rendered node to the DOM.
157
+ node = renderer.node;
158
+ container.insertBefore(node, container.firstChild);
82
159
 
83
160
  // Resolve the rendered promise.
84
161
  props.rendered.resolve();
85
162
  };
86
163
 
87
164
  renderContent();
88
- }, [markdownStr, rmRegistry]);
165
+
166
+ return () => {
167
+ if (node && container.contains(node)) {
168
+ container.removeChild(node);
169
+ }
170
+ node = null;
171
+ };
172
+ }, [message.body, message.mentions, rmRegistry]);
89
173
 
90
174
  return (
91
- <div className={MD_RENDERED_CLASS}>
92
- {renderedContent &&
93
- (appendContent ? (
94
- <div ref={node => node && node.appendChild(renderedContent)} />
95
- ) : (
96
- <div ref={node => node && node.replaceChildren(renderedContent)} />
97
- ))}
98
- <MessageToolbar edit={props.edit} delete={props.delete} />
175
+ <div className={RENDERED_CLASS} ref={containerRef}>
176
+ <MessageToolbar
177
+ edit={canEdit ? props.edit : undefined}
178
+ delete={props.delete}
179
+ />
99
180
  {
100
181
  // Render a `CodeToolbar` element underneath each code block.
101
182
  // We use ReactDOM.createPortal() so each `CodeToolbar` element is able
@@ -85,7 +85,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
85
85
 
86
86
  // Create an input model only if the message is edited.
87
87
  const startEdition = (): void => {
88
- if (!canEdit) {
88
+ if (!canEdit || !(typeof message.body === 'string')) {
89
89
  return;
90
90
  }
91
91
  let body = message.body;
@@ -157,7 +157,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
157
157
  />
158
158
  ) : (
159
159
  <MessageRenderer
160
- markdownStr={message.body}
160
+ message={message}
161
161
  edit={canEdit ? startEdition : undefined}
162
162
  delete={canDelete ? () => deleteMessage(message.id) : undefined}
163
163
  rendered={props.renderedPromise}
@@ -21,7 +21,7 @@ export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
21
21
  if (props.edit !== undefined) {
22
22
  const editButton = (
23
23
  <TooltippedIconButton
24
- tooltip={'edit'}
24
+ tooltip={'Edit'}
25
25
  onClick={props.edit}
26
26
  aria-label={'Edit'}
27
27
  inputToolbar={false}
@@ -3,13 +3,14 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { classes } from '@jupyterlab/ui-components';
6
+ import { MessageLoop } from '@lumino/messaging';
7
+ import { Widget } from '@lumino/widgets';
7
8
  import React, { useEffect, useRef } from 'react';
8
9
 
9
10
  import { useChatContext } from '../../context';
10
- import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
11
11
 
12
12
  const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
13
+ const MD_MIME_TYPE = 'text/markdown';
13
14
 
14
15
  /**
15
16
  * The component props.
@@ -32,33 +33,40 @@ export function WelcomeMessage(props: IWelcomeMessageProps): JSX.Element {
32
33
 
33
34
  // ref that tracks the content container to store the rendermime node in
34
35
  const renderingContainer = useRef<HTMLDivElement | null>(null);
35
- // ref that tracks whether the rendermime node has already been inserted
36
- const renderingInserted = useRef<boolean>(false);
37
36
 
38
37
  /**
39
38
  * Effect: use Rendermime to render `props.markdownStr` into an HTML element,
40
39
  * and insert it into `renderingContainer` if not yet inserted.
41
40
  */
42
41
  useEffect(() => {
42
+ let node: HTMLElement | null = null;
43
+
43
44
  const renderContent = async () => {
44
- const renderer = await MarkdownRenderer.renderContent({
45
- content,
46
- rmRegistry
45
+ // Render the welcome message using markdown renderer.
46
+ const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
47
+ const mimeModel = rmRegistry.createModel({
48
+ data: { [MD_MIME_TYPE]: content }
47
49
  });
50
+ await renderer.renderModel(mimeModel);
48
51
 
49
- // insert the rendering into renderingContainer if not yet inserted
50
- if (renderingContainer.current !== null && !renderingInserted.current) {
51
- renderingContainer.current.appendChild(renderer.node);
52
- renderingInserted.current = true;
53
- }
52
+ // Manually trigger the onAfterAttach of the renderer, because the widget will
53
+ // never been attached, only the node.
54
+ // This is necessary to render latex.
55
+ MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
56
+
57
+ node = renderer.node;
58
+ renderingContainer.current?.append(node);
54
59
  };
55
60
 
56
61
  renderContent();
62
+
63
+ return () => {
64
+ if (node && renderingContainer.current?.contains(node)) {
65
+ renderingContainer.current.removeChild(node);
66
+ }
67
+ node = null;
68
+ };
57
69
  }, [content]);
58
70
 
59
- return (
60
- <div className={classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS)}>
61
- <div ref={renderingContainer} />
62
- </div>
63
- );
71
+ return <div className={WELCOME_MESSAGE_CLASS} ref={renderingContainer}></div>;
64
72
  }
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ export * from './active-cell-manager';
7
7
  export * from './components';
8
8
  export * from './icons';
9
9
  export * from './input-model';
10
- export * from './markdown-renderer';
11
10
  export * from './model';
12
11
  export * from './registers';
13
12
  export * from './selection-watcher';
package/src/message.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { IRenderMime } from '@jupyterlab/rendermime';
6
7
  import { ISignal, Signal } from '@lumino/signaling';
7
8
  import { IAttachment, IMessageContent, IMessage, IUser } from './types';
8
9
 
@@ -32,7 +33,9 @@ export class Message implements IMessage {
32
33
  get type(): string {
33
34
  return this._content.type;
34
35
  }
35
- get body(): string {
36
+ get body():
37
+ | string
38
+ | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>) {
36
39
  return this._content.body;
37
40
  }
38
41
  get id(): string {
package/src/model.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  INewMessage,
23
23
  IUser
24
24
  } from './types';
25
- import { replaceMentionToSpan } from './utils';
26
25
 
27
26
  /**
28
27
  * The chat model interface.
@@ -535,9 +534,6 @@ export abstract class AbstractChatModel implements IChatModel {
535
534
  * Can be useful if some actions are required on the message.
536
535
  */
537
536
  protected formatChatMessage(message: IMessageContent): IMessageContent {
538
- message.mentions?.forEach(user => {
539
- message.body = replaceMentionToSpan(message.body, user);
540
- });
541
537
  return message;
542
538
  }
543
539
 
@@ -40,8 +40,9 @@ export async function pollUntilReady(): Promise<void> {
40
40
 
41
41
  export async function getJupyterLabTheme(): Promise<Theme> {
42
42
  await pollUntilReady();
43
- const light = document.body.getAttribute('data-jp-theme-light');
43
+ const light = document.body.getAttribute('data-jp-theme-light') === 'true';
44
44
  return createTheme({
45
+ cssVariables: true,
45
46
  spacing: 4,
46
47
  components: {
47
48
  MuiButton: {
@@ -65,12 +66,12 @@ export async function getJupyterLabTheme(): Promise<Theme> {
65
66
  // The default style for input toolbar button variant.
66
67
  props: { variant: 'input-toolbar' },
67
68
  style: {
68
- backgroundColor: 'var(--jp-brand-color1)',
69
- color: 'white',
69
+ backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
70
+ color: 'var(--jp-ui-inverse-font-color1)',
70
71
  borderRadius: '4px',
71
72
  boxShadow: 'none',
72
73
  '&:hover': {
73
- backgroundColor: 'var(--jp-brand-color0)',
74
+ backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
74
75
  boxShadow: 'none'
75
76
  },
76
77
  '&:disabled': {
@@ -122,12 +123,12 @@ export async function getJupyterLabTheme(): Promise<Theme> {
122
123
  // The default style for input toolbar button variant.
123
124
  props: { variant: 'input-toolbar' },
124
125
  style: {
125
- backgroundColor: 'var(--jp-brand-color1)',
126
- color: 'white',
126
+ backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
127
+ color: 'var(--jp-ui-inverse-font-color1)',
127
128
  borderRadius: '4px',
128
129
  boxShadow: 'none',
129
130
  '&:hover': {
130
- backgroundColor: 'var(--jp-brand-color0)',
131
+ backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
131
132
  boxShadow: 'none'
132
133
  },
133
134
  '&:disabled': {
@@ -187,9 +188,9 @@ export async function getJupyterLabTheme(): Promise<Theme> {
187
188
  paper: getCSSVariable('--jp-layout-color1'),
188
189
  default: getCSSVariable('--jp-layout-color1')
189
190
  },
190
- mode: light === 'true' ? 'light' : 'dark',
191
+ mode: light ? 'light' : 'dark',
191
192
  primary: {
192
- main: getCSSVariable('--jp-brand-color1'),
193
+ main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
193
194
  light: getCSSVariable('--jp-brand-color2'),
194
195
  dark: getCSSVariable('--jp-brand-color0')
195
196
  },
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { ISignal } from '@lumino/signaling';
7
+ import { IRenderMime } from '@jupyterlab/rendermime';
7
8
 
8
9
  /**
9
10
  * The user description.
@@ -65,7 +66,10 @@ export interface IConfig {
65
66
  */
66
67
  export type IMessageContent<T = IUser, U = IAttachment> = {
67
68
  type: string;
68
- body: string;
69
+ body:
70
+ | string
71
+ // Should contain at least the data of the mime model.
72
+ | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
69
73
  id: string;
70
74
  time: number;
71
75
  sender: T;
package/style/chat.css CHANGED
@@ -3,16 +3,16 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- .jp-chat-rendered-markdown {
6
+ .jp-chat-rendered-message {
7
7
  position: relative;
8
8
  }
9
9
 
10
- .jp-chat-rendered-markdown hr {
10
+ .jp-chat-rendered-message hr {
11
11
  color: #00000026;
12
12
  background-color: transparent;
13
13
  }
14
14
 
15
- .jp-chat-rendered-markdown .jp-RenderedHTMLCommon > :last-child {
15
+ .jp-chat-message .jp-RenderedHTMLCommon > :last-child {
16
16
  margin-bottom: 0;
17
17
  }
18
18
 
@@ -23,15 +23,20 @@
23
23
  * See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
24
24
  * See also: https://github.com/jupyterlab/jupyter-ai/issues/1090
25
25
  */
26
- .jp-ThemedContainer .jp-chat-rendered-markdown.jp-chat-welcome-message {
26
+ .jp-ThemedContainer .jp-chat-welcome-message {
27
27
  padding: 0 1em;
28
28
  }
29
29
 
30
- .jp-ThemedContainer .jp-chat-rendered-markdown .jp-RenderedHTMLCommon {
30
+ .jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedHTMLCommon {
31
31
  padding-right: 0;
32
32
  }
33
33
 
34
- .jp-ThemedContainer .jp-chat-rendered-markdown pre {
34
+ .jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedJSON {
35
+ padding-left: 5px;
36
+ }
37
+
38
+ .jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedMarkdown pre,
39
+ .jp-ThemedContainer .jp-chat-welcome-message .jp-RenderedMarkdown pre {
35
40
  background-color: var(--jp-cell-editor-background);
36
41
  overflow-x: auto;
37
42
  white-space: pre;
@@ -40,13 +45,13 @@
40
45
  border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
41
46
  }
42
47
 
43
- .jp-ThemedContainer .jp-chat-rendered-markdown pre > code {
48
+ .jp-ThemedContainer .jp-RenderedMarkdown pre > code {
44
49
  background-color: inherit;
45
50
  overflow-x: inherit;
46
51
  white-space: inherit;
47
52
  }
48
53
 
49
- .jp-ThemedContainer .jp-chat-rendered-markdown mjx-container {
54
+ .jp-ThemedContainer .jp-RenderedHTMLCommon mjx-container {
50
55
  font-size: 119%;
51
56
  }
52
57
 
@@ -64,7 +69,7 @@
64
69
  color: var(--jp-ui-font-color2);
65
70
  }
66
71
 
67
- .jp-chat-rendered-markdown:hover .jp-chat-toolbar {
72
+ .jp-chat-rendered-message:hover .jp-chat-toolbar {
68
73
  visibility: visible;
69
74
  }
70
75
 
package/style/input.css CHANGED
@@ -20,7 +20,7 @@
20
20
  position: absolute;
21
21
  inset: 0;
22
22
  background: rgb(33 150 243 / 10%);
23
- border: 2px dashed var(--jp-brand-color1);
23
+ border: 2px dashed var(--mui-palette-primary-main);
24
24
  border-radius: 4px;
25
25
  pointer-events: none;
26
26
  z-index: 1;
@@ -32,7 +32,7 @@
32
32
  top: -24px;
33
33
  left: 50%;
34
34
  transform: translateX(-50%);
35
- color: var(--jp-brand-color1);
35
+ color: var(--mui-palette-primary-main);
36
36
  font-size: 12px;
37
37
  font-weight: 500;
38
38
  pointer-events: none;
@@ -47,5 +47,5 @@
47
47
  }
48
48
 
49
49
  .jp-chat-input-container:focus-within > div:first-of-type {
50
- border-color: var(--jp-brand-color1);
50
+ border-color: var(--mui-palette-primary-main);
51
51
  }
@@ -1,38 +0,0 @@
1
- import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
- export declare const MD_RENDERED_CLASS = "jp-chat-rendered-markdown";
3
- export declare const MD_MIME_TYPE = "text/markdown";
4
- /**
5
- * A namespace for the MarkdownRenderer.
6
- */
7
- export declare namespace MarkdownRenderer {
8
- /**
9
- * The options for the MarkdownRenderer.
10
- */
11
- interface IOptions {
12
- /**
13
- * The rendermime registry.
14
- */
15
- rmRegistry: IRenderMimeRegistry;
16
- /**
17
- * The markdown content.
18
- */
19
- content: string;
20
- }
21
- /**
22
- * A generic function to render a markdown string into a DOM element.
23
- *
24
- * @param content - the markdown content.
25
- * @param rmRegistry - the rendermime registry.
26
- * @returns a promise that resolves to the renderer.
27
- */
28
- function renderContent(options: IOptions): Promise<IRenderMime.IRenderer>;
29
- /**
30
- * Escapes backslashes in LaTeX delimiters such that they appear in the DOM
31
- * after the initial MarkDown render. For example, this function takes '\(` and
32
- * returns `\\(`.
33
- *
34
- * Required for proper rendering of MarkDown + LaTeX markup in the chat by
35
- * `ILatexTypesetter`.
36
- */
37
- function escapeLatexDelimiters(text: string): string;
38
- }
@@ -1,54 +0,0 @@
1
- /*
2
- * Copyright (c) Jupyter Development Team.
3
- * Distributed under the terms of the Modified BSD License.
4
- */
5
- export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
6
- export const MD_MIME_TYPE = 'text/markdown';
7
- /**
8
- * A namespace for the MarkdownRenderer.
9
- */
10
- export var MarkdownRenderer;
11
- (function (MarkdownRenderer) {
12
- /**
13
- * A generic function to render a markdown string into a DOM element.
14
- *
15
- * @param content - the markdown content.
16
- * @param rmRegistry - the rendermime registry.
17
- * @returns a promise that resolves to the renderer.
18
- */
19
- async function renderContent(options) {
20
- var _a;
21
- const { rmRegistry, content } = options;
22
- // initialize mime model
23
- const mdStr = escapeLatexDelimiters(content);
24
- const model = rmRegistry.createModel({
25
- data: { [MD_MIME_TYPE]: mdStr }
26
- });
27
- const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
28
- // step 1: render markdown
29
- await renderer.renderModel(model);
30
- if (!renderer.node) {
31
- throw new Error('Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.');
32
- }
33
- // step 2: render LaTeX via MathJax.
34
- (_a = rmRegistry.latexTypesetter) === null || _a === void 0 ? void 0 : _a.typeset(renderer.node);
35
- return renderer;
36
- }
37
- MarkdownRenderer.renderContent = renderContent;
38
- /**
39
- * Escapes backslashes in LaTeX delimiters such that they appear in the DOM
40
- * after the initial MarkDown render. For example, this function takes '\(` and
41
- * returns `\\(`.
42
- *
43
- * Required for proper rendering of MarkDown + LaTeX markup in the chat by
44
- * `ILatexTypesetter`.
45
- */
46
- function escapeLatexDelimiters(text) {
47
- return text
48
- .replace(/\\\(/g, '\\\\(')
49
- .replace(/\\\)/g, '\\\\)')
50
- .replace(/\\\[/g, '\\\\[')
51
- .replace(/\\\]/g, '\\\\]');
52
- }
53
- MarkdownRenderer.escapeLatexDelimiters = escapeLatexDelimiters;
54
- })(MarkdownRenderer || (MarkdownRenderer = {}));
@@ -1,78 +0,0 @@
1
- /*
2
- * Copyright (c) Jupyter Development Team.
3
- * Distributed under the terms of the Modified BSD License.
4
- */
5
-
6
- import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
-
8
- export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
9
- export const MD_MIME_TYPE = 'text/markdown';
10
-
11
- /**
12
- * A namespace for the MarkdownRenderer.
13
- */
14
- export namespace MarkdownRenderer {
15
- /**
16
- * The options for the MarkdownRenderer.
17
- */
18
- export interface IOptions {
19
- /**
20
- * The rendermime registry.
21
- */
22
- rmRegistry: IRenderMimeRegistry;
23
- /**
24
- * The markdown content.
25
- */
26
- content: string;
27
- }
28
-
29
- /**
30
- * A generic function to render a markdown string into a DOM element.
31
- *
32
- * @param content - the markdown content.
33
- * @param rmRegistry - the rendermime registry.
34
- * @returns a promise that resolves to the renderer.
35
- */
36
- export async function renderContent(
37
- options: IOptions
38
- ): Promise<IRenderMime.IRenderer> {
39
- const { rmRegistry, content } = options;
40
-
41
- // initialize mime model
42
- const mdStr = escapeLatexDelimiters(content);
43
- const model = rmRegistry.createModel({
44
- data: { [MD_MIME_TYPE]: mdStr }
45
- });
46
-
47
- const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
48
-
49
- // step 1: render markdown
50
- await renderer.renderModel(model);
51
- if (!renderer.node) {
52
- throw new Error(
53
- 'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
54
- );
55
- }
56
-
57
- // step 2: render LaTeX via MathJax.
58
- rmRegistry.latexTypesetter?.typeset(renderer.node);
59
-
60
- return renderer;
61
- }
62
-
63
- /**
64
- * Escapes backslashes in LaTeX delimiters such that they appear in the DOM
65
- * after the initial MarkDown render. For example, this function takes '\(` and
66
- * returns `\\(`.
67
- *
68
- * Required for proper rendering of MarkDown + LaTeX markup in the chat by
69
- * `ILatexTypesetter`.
70
- */
71
- export function escapeLatexDelimiters(text: string) {
72
- return text
73
- .replace(/\\\(/g, '\\\\(')
74
- .replace(/\\\)/g, '\\\\)')
75
- .replace(/\\\[/g, '\\\\[')
76
- .replace(/\\\]/g, '\\\\]');
77
- }
78
- }