@jupyter/chat 0.19.0-alpha.3 → 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.
Files changed (56) hide show
  1. package/lib/__tests__/model.spec.js +2 -2
  2. package/lib/components/chat.js +29 -3
  3. package/lib/components/index.d.ts +1 -0
  4. package/lib/components/index.js +1 -0
  5. package/lib/components/input/chat-input.js +1 -25
  6. package/lib/components/input/index.d.ts +0 -1
  7. package/lib/components/input/index.js +0 -1
  8. package/lib/components/messages/footer.d.ts +2 -2
  9. package/lib/components/messages/header.d.ts +2 -2
  10. package/lib/components/messages/header.js +13 -6
  11. package/lib/components/messages/message-renderer.d.ts +3 -6
  12. package/lib/components/messages/message-renderer.js +104 -29
  13. package/lib/components/messages/message.d.ts +2 -2
  14. package/lib/components/messages/message.js +18 -3
  15. package/lib/components/messages/messages.js +3 -2
  16. package/lib/components/messages/toolbar.js +6 -14
  17. package/lib/components/messages/welcome.js +24 -14
  18. package/lib/components/writing-indicator.d.ts +20 -0
  19. package/lib/components/{input/writing-indicator.js → writing-indicator.js} +3 -2
  20. package/lib/index.d.ts +0 -1
  21. package/lib/index.js +0 -1
  22. package/lib/message.d.ts +42 -0
  23. package/lib/message.js +74 -0
  24. package/lib/model.d.ts +13 -13
  25. package/lib/model.js +9 -12
  26. package/lib/registers/footers.d.ts +2 -2
  27. package/lib/theme-provider.js +10 -9
  28. package/lib/types.d.ts +23 -4
  29. package/lib/widgets/chat-widget.js +18 -11
  30. package/package.json +1 -1
  31. package/src/__tests__/model.spec.ts +7 -7
  32. package/src/components/chat.tsx +35 -2
  33. package/src/components/index.ts +1 -0
  34. package/src/components/input/chat-input.tsx +0 -28
  35. package/src/components/input/index.ts +0 -1
  36. package/src/components/messages/footer.tsx +2 -2
  37. package/src/components/messages/header.tsx +20 -8
  38. package/src/components/messages/message-renderer.tsx +125 -44
  39. package/src/components/messages/message.tsx +25 -5
  40. package/src/components/messages/messages.tsx +9 -4
  41. package/src/components/messages/toolbar.tsx +14 -14
  42. package/src/components/messages/welcome.tsx +25 -17
  43. package/src/components/{input/writing-indicator.tsx → writing-indicator.tsx} +9 -4
  44. package/src/index.ts +0 -1
  45. package/src/message.ts +86 -0
  46. package/src/model.ts +25 -26
  47. package/src/registers/footers.ts +2 -2
  48. package/src/theme-provider.ts +10 -9
  49. package/src/types.ts +28 -4
  50. package/src/widgets/chat-widget.tsx +22 -14
  51. package/style/chat.css +14 -9
  52. package/style/input.css +3 -3
  53. package/lib/components/input/writing-indicator.d.ts +0 -15
  54. package/lib/markdown-renderer.d.ts +0 -38
  55. package/lib/markdown-renderer.js +0 -54
  56. package/src/markdown-renderer.ts +0 -78
@@ -52,7 +52,7 @@ describe('test chat model', () => {
52
52
  });
53
53
  model.messageAdded(msg);
54
54
  expect(messages).toHaveLength(1);
55
- expect(messages[0]).toBe(msg);
55
+ expect(messages[0].content).toBe(msg);
56
56
  });
57
57
  it('should format message', () => {
58
58
  model = new TestChat();
@@ -62,7 +62,7 @@ describe('test chat model', () => {
62
62
  });
63
63
  model.messageAdded({ ...msg });
64
64
  expect(messages).toHaveLength(1);
65
- expect(messages[0]).not.toBe(msg);
65
+ expect(messages[0].content).not.toBe(msg);
66
66
  expect(messages[0].body).toBe('formatted msg');
67
67
  });
68
68
  });
@@ -6,18 +6,40 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
6
6
  import SettingsIcon from '@mui/icons-material/Settings';
7
7
  import { IconButton } from '@mui/material';
8
8
  import { Box } from '@mui/system';
9
- import React, { useState } from 'react';
9
+ import React, { useEffect, useState } from 'react';
10
10
  import { ChatInput, InputToolbarRegistry } from './input';
11
11
  import { JlThemeProvider } from './jl-theme-provider';
12
12
  import { ChatMessages } from './messages';
13
+ import { WritingIndicator } from './writing-indicator';
13
14
  import { ChatReactContext } from '../context';
14
15
  export function ChatBody(props) {
15
16
  const { model } = props;
17
+ const [writers, setWriters] = useState([]);
16
18
  let { inputToolbarRegistry } = props;
17
19
  if (!inputToolbarRegistry) {
18
20
  inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
19
21
  }
20
- // const horizontalPadding = props.area === 'main' ? 8 : 4;
22
+ /**
23
+ * Handle the changes in the writers list.
24
+ */
25
+ useEffect(() => {
26
+ var _a;
27
+ if (!model) {
28
+ return;
29
+ }
30
+ const updateWriters = (_, writers) => {
31
+ // Show all writers for now - AI generating responses will have messageID
32
+ setWriters(writers);
33
+ };
34
+ // Set initial writers state
35
+ const initialWriters = model.writers;
36
+ setWriters(initialWriters);
37
+ (_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(updateWriters);
38
+ return () => {
39
+ var _a;
40
+ (_a = model === null || model === void 0 ? void 0 : model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
41
+ };
42
+ }, [model]);
21
43
  const horizontalPadding = 4;
22
44
  const contextValue = {
23
45
  ...props,
@@ -30,7 +52,11 @@ export function ChatBody(props) {
30
52
  paddingRight: horizontalPadding,
31
53
  paddingTop: 0,
32
54
  paddingBottom: 0
33
- }, model: model.input })));
55
+ }, model: model.input }),
56
+ React.createElement(WritingIndicator, { sx: {
57
+ paddingLeft: horizontalPadding,
58
+ paddingRight: horizontalPadding
59
+ }, writers: writers })));
34
60
  }
35
61
  export function Chat(props) {
36
62
  var _a;
@@ -6,3 +6,4 @@ export * from './jl-theme-provider';
6
6
  export * from './messages';
7
7
  export * from './mui-extras';
8
8
  export * from './scroll-container';
9
+ export * from './writing-indicator';
@@ -10,3 +10,4 @@ export * from './jl-theme-provider';
10
10
  export * from './messages';
11
11
  export * from './mui-extras';
12
12
  export * from './scroll-container';
13
+ export * from './writing-indicator';
@@ -8,7 +8,6 @@ import React, { useEffect, useRef, useState } from 'react';
8
8
  import { useChatCommands } from './use-chat-commands';
9
9
  import { AttachmentPreviewList } from '../attachments';
10
10
  import { useChatContext } from '../../context';
11
- import { InputWritingIndicator } from './writing-indicator';
12
11
  const INPUT_BOX_CLASS = 'jp-chat-input-container';
13
12
  const INPUT_TEXTFIELD_CLASS = 'jp-chat-input-textfield';
14
13
  const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
@@ -23,7 +22,6 @@ export function ChatInput(props) {
23
22
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
24
23
  const [attachments, setAttachments] = useState(model.attachments);
25
24
  const [toolbarElements, setToolbarElements] = useState([]);
26
- const [writers, setWriters] = useState([]);
27
25
  /**
28
26
  * Auto-focus the input when the component is first mounted.
29
27
  */
@@ -79,27 +77,6 @@ export function ChatInput(props) {
79
77
  inputToolbarRegistry === null || inputToolbarRegistry === void 0 ? void 0 : inputToolbarRegistry.itemsChanged.disconnect(updateToolbar);
80
78
  };
81
79
  }, [inputToolbarRegistry]);
82
- /**
83
- * Handle the changes in the writers list.
84
- */
85
- useEffect(() => {
86
- var _a;
87
- if (!chatModel) {
88
- return;
89
- }
90
- const updateWriters = (_, writers) => {
91
- // Show all writers for now - AI generating responses will have messageID
92
- setWriters(writers);
93
- };
94
- // Set initial writers state
95
- const initialWriters = chatModel.writers;
96
- setWriters(initialWriters);
97
- (_a = chatModel.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(updateWriters);
98
- return () => {
99
- var _a;
100
- (_a = chatModel === null || chatModel === void 0 ? void 0 : chatModel.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
101
- };
102
- }, [chatModel]);
103
80
  const inputExists = !!input.trim();
104
81
  /**
105
82
  * `handleKeyDown()`: callback invoked when the user presses any key in the
@@ -232,6 +209,5 @@ export function ChatInput(props) {
232
209
  borderColor: 'var(--jp-border-color1)',
233
210
  backgroundColor: 'var(--jp-layout-color0)',
234
211
  transition: 'background-color 0.2s ease'
235
- } }, toolbarElements.map((item, index) => (React.createElement(item.element, { key: index, model: model, chatCommandRegistry: chatCommandRegistry, chatModel: chatModel, edit: props.edit }))))),
236
- React.createElement(InputWritingIndicator, { writers: writers })));
212
+ } }, toolbarElements.map((item, index) => (React.createElement(item.element, { key: index, model: model, chatCommandRegistry: chatCommandRegistry, chatModel: chatModel, edit: props.edit })))))));
237
213
  }
@@ -2,4 +2,3 @@ export * from './buttons';
2
2
  export * from './chat-input';
3
3
  export * from './toolbar-registry';
4
4
  export * from './use-chat-commands';
5
- export * from './writing-indicator';
@@ -6,4 +6,3 @@ export * from './buttons';
6
6
  export * from './chat-input';
7
7
  export * from './toolbar-registry';
8
8
  export * from './use-chat-commands';
9
- export * from './writing-indicator';
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { IChatMessage } from '../../types';
2
+ import { IMessageContent } from '../../types';
3
3
  /**
4
4
  * The chat footer component properties.
5
5
  */
@@ -7,7 +7,7 @@ export interface IMessageFootersProps {
7
7
  /**
8
8
  * The chat model.
9
9
  */
10
- message: IChatMessage;
10
+ message: IMessageContent;
11
11
  }
12
12
  /**
13
13
  * The chat footer component, which displays footer components on a row according to
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { IChatMessage } from '../../types';
2
+ import { IMessage } from '../../types';
3
3
  /**
4
4
  * The message header props.
5
5
  */
@@ -7,7 +7,7 @@ type ChatMessageHeaderProps = {
7
7
  /**
8
8
  * The chat message.
9
9
  */
10
- message: IChatMessage;
10
+ message: IMessage;
11
11
  /**
12
12
  * Whether this message is from the current user.
13
13
  */
@@ -12,11 +12,7 @@ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
12
12
  */
13
13
  export function ChatMessageHeader(props) {
14
14
  var _a, _b;
15
- const message = props.message;
16
- // Don't render header for stacked messages not deleted or edited.
17
- if (message.stacked && !message.deleted && !message.edited) {
18
- return React.createElement(React.Fragment, null);
19
- }
15
+ const [message, setMessage] = useState(props.message.content);
20
16
  // Flag to display only the deleted or edited information (stacked message).
21
17
  const onlyState = message.stacked && (message.deleted || message.edited);
22
18
  const [datetime, setDatetime] = useState({});
@@ -55,9 +51,20 @@ export function ChatMessageHeader(props) {
55
51
  setDatetime(newDatetime);
56
52
  }
57
53
  });
54
+ // Listen for changes in the current message.
55
+ useEffect(() => {
56
+ function messageChanged() {
57
+ setMessage(props.message.content);
58
+ }
59
+ props.message.changed.connect(messageChanged);
60
+ return () => {
61
+ props.message.changed.disconnect(messageChanged);
62
+ };
63
+ }, [props.message]);
58
64
  const avatar = message.stacked ? null : Avatar({ user: sender });
59
65
  const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || 'User undefined');
60
- return (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
66
+ // Don't render header for stacked messages not deleted or edited.
67
+ return message.stacked && !message.deleted && !message.edited ? (React.createElement(React.Fragment, null)) : (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
61
68
  display: 'flex',
62
69
  alignItems: 'center',
63
70
  '& > :not(:last-child)': {
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import { PromiseDelegate } from '@lumino/coreutils';
2
2
  import React from 'react';
3
- import { IChatMessage } from '../../types';
3
+ import { IMessage } from '../../types';
4
4
  /**
5
5
  * The message component props.
6
6
  */
@@ -8,7 +8,7 @@ type ChatMessageProps = {
8
8
  /**
9
9
  * The message to display.
10
10
  */
11
- message: IChatMessage;
11
+ message: IMessage;
12
12
  /**
13
13
  * The index of the message in the list.
14
14
  */
@@ -13,8 +13,8 @@ import { replaceSpanToMention } from '../../utils';
13
13
  * The message component body.
14
14
  */
15
15
  export const ChatMessage = forwardRef((props, ref) => {
16
- const { message } = props;
17
16
  const { model } = useChatContext();
17
+ const [message, setMessage] = useState(props.message.content);
18
18
  const [edit, setEdit] = useState(false);
19
19
  const [deleted, setDeleted] = useState(false);
20
20
  const [canEdit, setCanEdit] = useState(false);
@@ -39,10 +39,25 @@ export const ChatMessage = forwardRef((props, ref) => {
39
39
  setCanDelete(false);
40
40
  }
41
41
  }, [model, message]);
42
+ // Listen for changes in the current message.
43
+ useEffect(() => {
44
+ function messageChanged() {
45
+ setMessage(props.message.content);
46
+ }
47
+ props.message.changed.connect(messageChanged);
48
+ // Initialize the message when the message is re-rendered.
49
+ // FIX ? This seems to be required for outofband change, to get the new value,
50
+ // even if when an outofband change occurs, all the messages are deleted and
51
+ // recreated.
52
+ setMessage(props.message.content);
53
+ return () => {
54
+ props.message.changed.disconnect(messageChanged);
55
+ };
56
+ }, [props.message]);
42
57
  // Create an input model only if the message is edited.
43
58
  const startEdition = () => {
44
59
  var _a;
45
- if (!canEdit) {
60
+ if (!canEdit || !(typeof message.body === 'string')) {
46
61
  return;
47
62
  }
48
63
  let body = message.body;
@@ -96,7 +111,7 @@ export const ChatMessage = forwardRef((props, ref) => {
96
111
  };
97
112
  // Empty if the message has been deleted.
98
113
  return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
99
- 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 })),
100
115
  message.attachments && !edit && (
101
116
  // Display the attachments only if message is not edited, otherwise the
102
117
  // input component display them.
@@ -13,6 +13,7 @@ import { Navigation } from './navigation';
13
13
  import { WelcomeMessage } from './welcome';
14
14
  import { ScrollContainer } from '../scroll-container';
15
15
  import { useChatContext } from '../../context';
16
+ import { Message } from '../../message';
16
17
  export const MESSAGE_CLASS = 'jp-chat-message';
17
18
  const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
18
19
  const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
@@ -39,7 +40,7 @@ export function ChatMessages() {
39
40
  }
40
41
  model
41
42
  .getHistory()
42
- .then(history => setMessages(history.messages))
43
+ .then(history => setMessages(history.messages.map(message => new Message({ ...message }))))
43
44
  .catch(e => console.error(e));
44
45
  }
45
46
  fetchHistory();
@@ -150,7 +151,7 @@ export function ChatMessages() {
150
151
  model.user.username === message.sender.username;
151
152
  return (
152
153
  // extra div needed to ensure each bubble is on a new line
153
- React.createElement(Box, { key: i, sx: {
154
+ React.createElement(Box, { key: message.id, sx: {
154
155
  ...(isCurrentUser && {
155
156
  marginLeft: area === 'main' ? '25%' : '10%',
156
157
  backgroundColor: 'var(--jp-layout-color2)',
@@ -2,8 +2,8 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- // import EditIcon from '@mui/icons-material/Edit';
6
5
  import DeleteIcon from '@mui/icons-material/Delete';
6
+ import EditIcon from '@mui/icons-material/Edit';
7
7
  import { Box } from '@mui/material';
8
8
  import React from 'react';
9
9
  import { TooltippedIconButton } from '../mui-extras';
@@ -13,19 +13,11 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
13
13
  */
14
14
  export function MessageToolbar(props) {
15
15
  const buttons = [];
16
- // if (props.edit !== undefined) {
17
- // const editButton = (
18
- // <TooltippedIconButton
19
- // tooltip={'edit'}
20
- // onClick={props.edit}
21
- // aria-label={'Edit'}
22
- // inputToolbar={false}
23
- // >
24
- // <EditIcon />
25
- // </TooltippedIconButton>
26
- // );
27
- // buttons.push(editButton);
28
- // }
16
+ if (props.edit !== undefined) {
17
+ const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
18
+ React.createElement(EditIcon, null)));
19
+ buttons.push(editButton);
20
+ }
29
21
  if (props.delete !== undefined) {
30
22
  const deleteButton = (React.createElement(TooltippedIconButton, { tooltip: 'Delete', onClick: props.delete, "aria-label": 'Delete', inputToolbar: false },
31
23
  React.createElement(DeleteIcon, null)));
@@ -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
  }
@@ -0,0 +1,20 @@
1
+ /// <reference types="react" />
2
+ import { SxProps, Theme } from '@mui/material';
3
+ import { IChatModel } from '../model';
4
+ /**
5
+ * The input writing indicator component props.
6
+ */
7
+ export interface IInputWritingIndicatorProps {
8
+ /**
9
+ * The list of users currently writing.
10
+ */
11
+ writers: IChatModel.IWriter[];
12
+ /**
13
+ * Custom mui/material styles.
14
+ */
15
+ sx?: SxProps<Theme>;
16
+ }
17
+ /**
18
+ * The writing indicator component, displaying typing status.
19
+ */
20
+ export declare function WritingIndicator(props: IInputWritingIndicatorProps): JSX.Element;
@@ -30,13 +30,14 @@ function formatWritersText(writers) {
30
30
  }
31
31
  }
32
32
  /**
33
- * The input writing indicator component, displaying typing status in the chat input area.
33
+ * The writing indicator component, displaying typing status.
34
34
  */
35
- export function InputWritingIndicator(props) {
35
+ export function WritingIndicator(props) {
36
36
  const { writers } = props;
37
37
  // Always render the container to reserve space, even if no writers
38
38
  const writersText = writers.length > 0 ? formatWritersText(writers) : '';
39
39
  return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME, sx: {
40
+ ...props.sx,
40
41
  minHeight: '16px'
41
42
  } },
42
43
  React.createElement(Typography, { variant: "caption", sx: {
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';