@jupyter/chat 0.22.0 → 0.23.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 (61) hide show
  1. package/lib/__tests__/multichat-panel.spec.js +82 -1
  2. package/lib/components/attachments.d.ts +0 -1
  3. package/lib/components/avatar.d.ts +0 -1
  4. package/lib/components/chat.d.ts +0 -1
  5. package/lib/components/chat.js +8 -9
  6. package/lib/components/code-blocks/code-toolbar.d.ts +0 -1
  7. package/lib/components/code-blocks/copy-button.d.ts +0 -1
  8. package/lib/components/input/buttons/attach-button.d.ts +0 -1
  9. package/lib/components/input/buttons/cancel-button.d.ts +0 -1
  10. package/lib/components/input/buttons/save-edit-button.d.ts +0 -1
  11. package/lib/components/input/buttons/save-edit-button.js +2 -2
  12. package/lib/components/input/buttons/send-button.d.ts +0 -1
  13. package/lib/components/input/buttons/send-button.js +3 -10
  14. package/lib/components/input/buttons/stop-button.d.ts +0 -1
  15. package/lib/components/input/chat-input.d.ts +0 -1
  16. package/lib/components/input/chat-input.js +2 -3
  17. package/lib/components/input/submit-message.d.ts +22 -0
  18. package/lib/components/input/submit-message.js +13 -0
  19. package/lib/components/messages/chat-body-placeholder.d.ts +0 -1
  20. package/lib/components/messages/footer.d.ts +0 -1
  21. package/lib/components/messages/header.d.ts +3 -2
  22. package/lib/components/messages/header.js +2 -1
  23. package/lib/components/messages/message.d.ts +3 -6
  24. package/lib/components/messages/message.js +26 -14
  25. package/lib/components/messages/messages.d.ts +0 -1
  26. package/lib/components/messages/messages.js +11 -20
  27. package/lib/components/messages/preamble.d.ts +0 -1
  28. package/lib/components/messages/toolbar.d.ts +0 -1
  29. package/lib/components/messages/welcome.d.ts +0 -1
  30. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +0 -1
  31. package/lib/components/writing-indicator.d.ts +0 -1
  32. package/lib/context.d.ts +0 -1
  33. package/lib/message.d.ts +6 -0
  34. package/lib/message.js +23 -0
  35. package/lib/model.d.ts +8 -1
  36. package/lib/model.js +3 -0
  37. package/lib/registers/chat-commands.d.ts +0 -1
  38. package/lib/registers/footers.d.ts +0 -1
  39. package/lib/registers/preambles.d.ts +0 -1
  40. package/lib/tokens.d.ts +0 -1
  41. package/lib/types.d.ts +5 -0
  42. package/lib/utils.js +2 -2
  43. package/lib/widgets/chat-selector-popup.d.ts +0 -1
  44. package/lib/widgets/multichat-panel.d.ts +21 -5
  45. package/lib/widgets/multichat-panel.js +50 -10
  46. package/package.json +22 -22
  47. package/src/__tests__/multichat-panel.spec.ts +113 -1
  48. package/src/components/chat.tsx +10 -10
  49. package/src/components/input/buttons/save-edit-button.tsx +2 -2
  50. package/src/components/input/buttons/send-button.tsx +3 -13
  51. package/src/components/input/chat-input.tsx +2 -3
  52. package/src/components/input/submit-message.ts +38 -0
  53. package/src/components/messages/header.tsx +5 -1
  54. package/src/components/messages/message.tsx +129 -122
  55. package/src/components/messages/messages.tsx +14 -26
  56. package/src/message.ts +29 -0
  57. package/src/model.ts +10 -2
  58. package/src/types.ts +5 -0
  59. package/src/utils.ts +2 -2
  60. package/src/widgets/multichat-panel.tsx +62 -11
  61. package/style/chat.css +8 -4
@@ -15,6 +15,7 @@ import clsx from 'clsx';
15
15
  import React, { useEffect, useRef, useState } from 'react';
16
16
 
17
17
  import { InputToolbarRegistry } from './toolbar-registry';
18
+ import { submitInputMessage } from './submit-message';
18
19
  import { useChatCommands } from './use-chat-commands';
19
20
  import { AttachmentPreviewList } from '../attachments';
20
21
  import { useChatContext, useTranslator } from '../../context';
@@ -169,9 +170,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
169
170
 
170
171
  // Finally, send the message when all other conditions are met.
171
172
  if (isSendCombination) {
172
- // Run all command providers
173
- await chatCommandRegistry?.onSubmit(model);
174
- model.send(model.value);
173
+ await submitInputMessage({ model, chatCommandRegistry });
175
174
  event.stopPropagation();
176
175
  event.preventDefault();
177
176
  }
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IInputModel } from '../../input-model';
7
+ import { IChatCommandRegistry } from '../../registers';
8
+
9
+ /**
10
+ * Submit the current input value as a chat message.
11
+ */
12
+ export async function submitInputMessage(
13
+ options: submitInputMessage.IOptions
14
+ ): Promise<void> {
15
+ const { model, chatCommandRegistry, body } = options;
16
+
17
+ await chatCommandRegistry?.onSubmit(model);
18
+
19
+ model.send(body ?? model.value);
20
+ model.focus();
21
+ }
22
+
23
+ export namespace submitInputMessage {
24
+ export interface IOptions {
25
+ /**
26
+ * The input model containing the message to send.
27
+ */
28
+ model: IInputModel;
29
+ /**
30
+ * Optional chat command registry used to preprocess message content.
31
+ */
32
+ chatCommandRegistry?: IChatCommandRegistry;
33
+ /**
34
+ * Optional message body to send instead of the current model value.
35
+ */
36
+ body?: string;
37
+ }
38
+ }
@@ -30,7 +30,9 @@ type ChatMessageHeaderProps = {
30
30
  /**
31
31
  * The message header component.
32
32
  */
33
- export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
33
+ export function ChatMessageHeaderBase(
34
+ props: ChatMessageHeaderProps
35
+ ): JSX.Element {
34
36
  const trans = useTranslator();
35
37
  const [message, setMessage] = useState<IMessageContent>(
36
38
  props.message.content
@@ -163,3 +165,5 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
163
165
  </Box>
164
166
  );
165
167
  }
168
+
169
+ export const ChatMessageHeader = React.memo(ChatMessageHeaderBase);
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { PromiseDelegate } from '@lumino/coreutils';
7
- import React, { forwardRef, useEffect, useState } from 'react';
7
+ import React, { useCallback, useEffect, useState } from 'react';
8
8
 
9
9
  import { MessageRenderer } from './message-renderer';
10
10
  import { AttachmentPreviewList } from '../attachments';
@@ -14,7 +14,7 @@ import { IInputModel, InputModel } from '../../input-model';
14
14
  import { IMessageContent, IMessage } from '../../types';
15
15
  import { replaceSpanToMention } from '../../utils';
16
16
 
17
- const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
17
+ export const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
18
18
 
19
19
  /**
20
20
  * The message component props.
@@ -28,103 +28,100 @@ type ChatMessageProps = {
28
28
  * The index of the message in the list.
29
29
  */
30
30
  index: number;
31
- /**
32
- * The promise to resolve when the message is rendered.
33
- */
34
- renderedPromise: PromiseDelegate<void>;
35
31
  };
36
32
 
37
33
  /**
38
34
  * The message component body.
39
35
  */
40
- export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
41
- (props, ref): JSX.Element => {
42
- const { model } = useChatContext();
43
- const [message, setMessage] = useState<IMessageContent>(
44
- props.message.content
45
- );
46
- const [edit, setEdit] = useState<boolean>(false);
47
- const [deleted, setDeleted] = useState<boolean>(false);
48
- const [canEdit, setCanEdit] = useState<boolean>(false);
49
- const [canDelete, setCanDelete] = useState<boolean>(false);
50
-
51
- // Look if the message can be deleted or edited.
52
- useEffect(() => {
53
- // Init canDelete and canEdit state.
54
- setDeleted(message.deleted ?? false);
55
- if (model.user !== undefined && !message.deleted) {
56
- if (model.user.username === message.sender.username) {
57
- setCanEdit(model.updateMessage !== undefined);
58
- setCanDelete(model.deleteMessage !== undefined);
59
- return;
60
- }
61
- if (message.sender.bot) {
62
- setCanDelete(model.deleteMessage !== undefined);
63
- }
64
- } else {
65
- setCanEdit(false);
66
- setCanDelete(false);
67
- }
68
- }, [model, message]);
36
+ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
37
+ const { model } = useChatContext();
38
+ const [message, setMessage] = useState<IMessageContent>(
39
+ props.message.content
40
+ );
41
+ const [renderedDelegate, setRenderedDelegate] = useState<
42
+ PromiseDelegate<void>
43
+ >(props.message.renderedDelegate);
44
+ const [edit, setEdit] = useState<boolean>(false);
45
+ const [deleted, setDeleted] = useState<boolean>(false);
46
+ const [canEdit, setCanEdit] = useState<boolean>(false);
47
+ const [canDelete, setCanDelete] = useState<boolean>(false);
69
48
 
70
- // Listen for changes in the current message.
71
- useEffect(() => {
72
- function messageChanged() {
73
- setMessage(props.message.content);
49
+ // Look if the message can be deleted or edited.
50
+ useEffect(() => {
51
+ // Init canDelete and canEdit state.
52
+ setDeleted(message.deleted ?? false);
53
+ if (model.user !== undefined && !message.deleted) {
54
+ if (model.user.username === message.sender.username) {
55
+ setCanEdit(model.updateMessage !== undefined);
56
+ setCanDelete(model.deleteMessage !== undefined);
57
+ return;
74
58
  }
59
+ if (message.sender.bot) {
60
+ setCanDelete(model.deleteMessage !== undefined);
61
+ }
62
+ } else {
63
+ setCanEdit(false);
64
+ setCanDelete(false);
65
+ }
66
+ }, [model, message]);
75
67
 
76
- props.message.changed.connect(messageChanged);
77
-
78
- // Initialize the message when the message is re-rendered.
79
- // FIX ? This seems to be required for outofband change, to get the new value,
80
- // even if when an outofband change occurs, all the messages are deleted and
81
- // recreated.
68
+ // Listen for changes in the current message.
69
+ useEffect(() => {
70
+ function messageChanged() {
82
71
  setMessage(props.message.content);
83
- return () => {
84
- props.message.changed.disconnect(messageChanged);
85
- };
86
- }, [props.message]);
87
-
88
- // Create an input model only if the message is edited.
89
- const startEdition = (): void => {
90
- if (!canEdit || !(typeof message.body === 'string')) {
91
- return;
92
- }
93
- let body = message.body;
94
- message.mentions?.forEach(user => {
95
- body = replaceSpanToMention(body, user);
96
- });
97
- const inputModel = new InputModel({
98
- chatContext: model.createChatContext(),
99
- onSend: (input: string, model?: IInputModel) =>
100
- updateMessage(message.id, input, model),
101
- onCancel: () => cancelEdition(),
102
- value: body,
103
- activeCellManager: model.activeCellManager,
104
- selectionWatcher: model.selectionWatcher,
105
- documentManager: model.documentManager,
106
- config: {
107
- sendWithShiftEnter: model.config.sendWithShiftEnter
108
- },
109
- attachments: structuredClone(message.attachments ?? []),
110
- mentions: message.mentions
111
- });
112
- model.addEditionModel(message.id, inputModel);
113
- setEdit(true);
114
- };
72
+ setRenderedDelegate(props.message.renderedDelegate);
73
+ }
115
74
 
116
- // Cancel the current edition of the message.
117
- const cancelEdition = (): void => {
118
- model.getEditionModel(message.id)?.dispose();
119
- setEdit(false);
75
+ props.message.changed.connect(messageChanged);
76
+
77
+ // Initialize the message when the message is re-rendered.
78
+ // FIX ? This seems to be required for outofband change, to get the new value,
79
+ // even if when an outofband change occurs, all the messages are deleted and
80
+ // recreated.
81
+ setMessage(props.message.content);
82
+ setRenderedDelegate(props.message.renderedDelegate);
83
+ return () => {
84
+ props.message.changed.disconnect(messageChanged);
120
85
  };
86
+ }, [props.message]);
87
+
88
+ // Create an input model only if the message is edited.
89
+ const startEdition = useCallback((): void => {
90
+ if (!canEdit || !(typeof message.body === 'string')) {
91
+ return;
92
+ }
93
+ let body = message.body;
94
+ message.mentions?.forEach(user => {
95
+ body = replaceSpanToMention(body, user);
96
+ });
97
+ const inputModel = new InputModel({
98
+ chatContext: model.createChatContext(),
99
+ onSend: (input: string, model?: IInputModel) =>
100
+ updateMessage(message.id, input, model),
101
+ onCancel: () => cancelEdition(),
102
+ value: body,
103
+ activeCellManager: model.activeCellManager,
104
+ selectionWatcher: model.selectionWatcher,
105
+ documentManager: model.documentManager,
106
+ config: {
107
+ sendWithShiftEnter: model.config.sendWithShiftEnter
108
+ },
109
+ attachments: structuredClone(message.attachments ?? []),
110
+ mentions: message.mentions
111
+ });
112
+ model.addEditionModel(message.id, inputModel);
113
+ setEdit(true);
114
+ }, [canEdit, model, message]);
121
115
 
122
- // Update the content of the message.
123
- const updateMessage = (
124
- id: string,
125
- input: string,
126
- inputModel?: IInputModel
127
- ): void => {
116
+ // Cancel the current edition of the message.
117
+ const cancelEdition = useCallback((): void => {
118
+ model.getEditionModel(message.id)?.dispose();
119
+ setEdit(false);
120
+ }, [model, message]);
121
+
122
+ // Update the content of the message.
123
+ const updateMessage = useCallback(
124
+ (id: string, input: string, inputModel?: IInputModel): void => {
128
125
  if (!canEdit || !inputModel) {
129
126
  return;
130
127
  }
@@ -136,45 +133,55 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
136
133
  model.updateMessage!(id, updatedMessage);
137
134
  model.getEditionModel(message.id)?.dispose();
138
135
  setEdit(false);
139
- };
136
+ },
137
+ [model, message, canEdit]
138
+ );
140
139
 
141
- // Delete the message.
142
- const deleteMessage = (id: string): void => {
140
+ // Delete the message.
141
+ const deleteMessage = useCallback(
142
+ (id: string): void => {
143
143
  if (!canDelete) {
144
144
  return;
145
145
  }
146
146
  model.deleteMessage!(id);
147
- };
147
+ },
148
+ [model, canDelete]
149
+ );
150
+
151
+ useEffect(() => {
152
+ if (deleted) {
153
+ renderedDelegate.resolve();
154
+ }
155
+ }, [deleted, renderedDelegate]);
148
156
 
149
- // Empty if the message has been deleted.
150
- return deleted ? (
151
- <div ref={ref} data-index={props.index}></div>
152
- ) : (
153
- <div
154
- ref={ref}
155
- data-index={props.index}
156
- className={MESSAGE_CONTAINER_CLASS}
157
- >
158
- {edit && canEdit && model.getEditionModel(message.id) ? (
159
- <ChatInput
160
- onCancel={() => cancelEdition()}
161
- model={model.getEditionModel(message.id)!}
162
- edit={true}
163
- />
164
- ) : (
165
- <MessageRenderer
166
- message={message}
167
- edit={canEdit ? startEdition : undefined}
168
- delete={canDelete ? () => deleteMessage(message.id) : undefined}
169
- rendered={props.renderedPromise}
170
- />
171
- )}
172
- {message.attachments && !edit && (
173
- // Display the attachments only if message is not edited, otherwise the
174
- // input component display them.
175
- <AttachmentPreviewList attachments={message.attachments} />
176
- )}
177
- </div>
178
- );
157
+ // Empty if the message has been deleted.
158
+ if (deleted) {
159
+ return <div data-index={props.index}></div>;
179
160
  }
180
- );
161
+
162
+ return (
163
+ <div data-index={props.index} className={MESSAGE_CONTAINER_CLASS}>
164
+ {edit && canEdit && model.getEditionModel(message.id) ? (
165
+ <ChatInput
166
+ onCancel={() => cancelEdition()}
167
+ model={model.getEditionModel(message.id)!}
168
+ edit={true}
169
+ />
170
+ ) : (
171
+ <MessageRenderer
172
+ message={message}
173
+ edit={canEdit ? startEdition : undefined}
174
+ delete={canDelete ? () => deleteMessage(message.id) : undefined}
175
+ rendered={renderedDelegate}
176
+ />
177
+ )}
178
+ {message.attachments && !edit && (
179
+ // Display the attachments only if message is not edited, otherwise the
180
+ // input component display them.
181
+ <AttachmentPreviewList attachments={message.attachments} />
182
+ )}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export const ChatMessage = React.memo(ChatMessageBase);
@@ -3,14 +3,13 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { PromiseDelegate } from '@lumino/coreutils';
7
6
  import { Box } from '@mui/material';
8
7
  import clsx from 'clsx';
9
8
  import React, { useEffect, useState, useRef } from 'react';
10
9
 
11
10
  import { MessageFooterComponent } from './footer';
12
11
  import { ChatMessageHeader } from './header';
13
- import { ChatMessage } from './message';
12
+ import { ChatMessage, MESSAGE_CONTAINER_CLASS } from './message';
14
13
  import { MessagePreambleComponent } from './preamble';
15
14
  import { Navigation } from './navigation';
16
15
  import { WelcomeMessage } from './welcome';
@@ -44,10 +43,6 @@ export function ChatMessages(): JSX.Element {
44
43
  model.config.showDeleted ?? false
45
44
  );
46
45
 
47
- // The list of message DOM and their rendered promises.
48
- const listRef = useRef<(HTMLDivElement | null)[]>([]);
49
- const renderedPromise = useRef<PromiseDelegate<void>[]>([]);
50
-
51
46
  /**
52
47
  * Effect: fetch history and config on initial render
53
48
  */
@@ -159,9 +154,12 @@ export function ChatMessages(): JSX.Element {
159
154
  */
160
155
  useEffect(() => {
161
156
  const observer = new IntersectionObserver(entries => {
162
- // Used on first rendering, to ensure all the message as been rendered once.
157
+ // Used on first rendering, to ensure all the messages have been rendered once.
163
158
  if (!allRendered) {
164
- Promise.all(renderedPromise.current.map(p => p.promise)).then(() => {
159
+ const promises = (
160
+ showDeleted ? messages : messages.filter(message => !message.deleted)
161
+ ).map(msg => msg.renderedDelegate.promise);
162
+ Promise.all(promises).then(() => {
165
163
  setAllRendered(true);
166
164
  });
167
165
  }
@@ -191,7 +189,7 @@ export function ChatMessages(): JSX.Element {
191
189
  model.messagesInViewport = inViewport;
192
190
 
193
191
  // Ensure that all messages are rendered before updating unread messages, otherwise
194
- // it can lead to wrong assumption , because more message are in the viewport
192
+ // it can lead to wrong assumption, because more messages are in the viewport
195
193
  // before they are rendered.
196
194
  if (allRendered && unreadModified) {
197
195
  model.unreadMessages = unread;
@@ -201,20 +199,16 @@ export function ChatMessages(): JSX.Element {
201
199
  /**
202
200
  * Observe the messages.
203
201
  */
204
- listRef.current.forEach(item => {
205
- if (item) {
202
+ refMsgBox.current
203
+ ?.querySelectorAll(`.${MESSAGE_CONTAINER_CLASS}`)
204
+ .forEach(item => {
206
205
  observer.observe(item);
207
- }
208
- });
206
+ });
209
207
 
210
208
  return () => {
211
- listRef.current.forEach(item => {
212
- if (item) {
213
- observer.unobserve(item);
214
- }
215
- });
209
+ observer.disconnect();
216
210
  };
217
- }, [messages, allRendered]);
211
+ }, [messages, showDeleted, allRendered]);
218
212
 
219
213
  const horizontalPadding = area === 'main' ? 8 : 4;
220
214
  return (
@@ -240,7 +234,6 @@ export function ChatMessages(): JSX.Element {
240
234
  ? messages
241
235
  : messages.filter(message => !message.deleted)
242
236
  ).map((message, i) => {
243
- renderedPromise.current[i] = new PromiseDelegate();
244
237
  const isCurrentUser =
245
238
  model.user !== undefined &&
246
239
  model.user.username === message.sender.username;
@@ -269,12 +262,7 @@ export function ChatMessages(): JSX.Element {
269
262
  {messagePreambleRegistry && (
270
263
  <MessagePreambleComponent message={message} />
271
264
  )}
272
- <ChatMessage
273
- message={message}
274
- index={i}
275
- renderedPromise={renderedPromise.current[i]}
276
- ref={el => (listRef.current[i] = el)}
277
- />
265
+ <ChatMessage message={message} index={i} />
278
266
  {messageFooterRegistry && (
279
267
  <MessageFooterComponent message={message} />
280
268
  )}
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 { PromiseDelegate } from '@lumino/coreutils';
6
7
  import { ISignal, Signal } from '@lumino/signaling';
7
8
  import {
8
9
  IAttachment,
@@ -83,14 +84,42 @@ export class Message implements IMessage {
83
84
  return this._changed;
84
85
  }
85
86
 
87
+ /**
88
+ * A promise delegate that resolve when the message is rendered.
89
+ */
90
+ get renderedDelegate(): PromiseDelegate<void> {
91
+ return this._rendered;
92
+ }
93
+
86
94
  /**
87
95
  * Update one or several fields of the message.
88
96
  */
89
97
  update(updated: Partial<IMessageContent>) {
98
+ // Check if the message will be rendered again.
99
+ const triggerRerender = new Set([
100
+ 'body',
101
+ 'deleted',
102
+ 'mentions',
103
+ 'mime_model'
104
+ ] as (keyof IMessageContent)[]);
105
+ const updatedAttributes = Object.keys(updated) as (keyof IMessageContent)[];
106
+ const shouldRerender = updatedAttributes.some(
107
+ attribute =>
108
+ triggerRerender.has(attribute) &&
109
+ updated[attribute] !== this._content[attribute]
110
+ );
111
+
112
+ // Update the message content.
90
113
  this._content = { ...this._content, ...updated };
114
+
115
+ // Update the promise delegate if the message will be rendered again.
116
+ if (shouldRerender) {
117
+ this._rendered = new PromiseDelegate<void>();
118
+ }
91
119
  this._changed.emit();
92
120
  }
93
121
 
94
122
  private _content: IMessageContent;
95
123
  private _changed = new Signal<IMessage, void>(this);
124
+ private _rendered = new PromiseDelegate<void>();
96
125
  }
package/src/model.ts CHANGED
@@ -175,6 +175,11 @@ export interface IChatModel extends IDisposable {
175
175
  */
176
176
  isDisposed: boolean;
177
177
 
178
+ /**
179
+ * A signal emitting when disposing of the model.
180
+ */
181
+ readonly disposed: ISignal<IChatModel, void>;
182
+
178
183
  /**
179
184
  * Function to call when a message is received.
180
185
  *
@@ -306,7 +311,10 @@ export abstract class AbstractChatModel implements IChatModel {
306
311
  this._name = value;
307
312
  }
308
313
 
309
- get disposed(): ISignal<AbstractChatModel, void> {
314
+ /**
315
+ * A signal emitting when disposing of the model.
316
+ */
317
+ get disposed(): ISignal<IChatModel, void> {
310
318
  return this._disposed;
311
319
  }
312
320
 
@@ -769,7 +777,7 @@ export abstract class AbstractChatModel implements IChatModel {
769
777
  protected _trans: TranslationBundle;
770
778
  private _readyDelegate = new PromiseDelegate<void>();
771
779
  private _inputModel: IInputModel;
772
- private _disposed = new Signal<this, void>(this);
780
+ private _disposed = new Signal<IChatModel, void>(this);
773
781
  private _isDisposed = false;
774
782
  private _commands?: CommandRegistry;
775
783
  private _activeCellManager: IActiveCellManager | null;
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { PromiseDelegate } from '@lumino/coreutils';
6
7
  import { ISignal } from '@lumino/signaling';
7
8
  import { IRenderMime } from '@jupyterlab/rendermime';
8
9
 
@@ -157,6 +158,10 @@ export interface IMessage extends IMessageContent {
157
158
  * A signal emitting when the message has been updated.
158
159
  */
159
160
  changed: ISignal<IMessage, void>;
161
+ /**
162
+ * A promise delegate that resolve when the message is rendered.
163
+ */
164
+ renderedDelegate: PromiseDelegate<void>;
160
165
  }
161
166
 
162
167
  /**
package/src/utils.ts CHANGED
@@ -62,7 +62,7 @@ export function replaceMentionToSpan(content: string, user: IUser): string {
62
62
  }
63
63
  const mention = '@' + user.mention_name;
64
64
  const regex = new RegExp(mention, 'g');
65
- const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
65
+ const mentionEl = `<span class="${MENTION_CLASS}"> ${mention} </span>`;
66
66
  return content.replace(regex, mentionEl);
67
67
  }
68
68
 
@@ -77,7 +77,7 @@ export function replaceSpanToMention(content: string, user: IUser): string {
77
77
  return content;
78
78
  }
79
79
  const mention = '@' + user.mention_name;
80
- const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
80
+ const mentionEl = `<span class="${MENTION_CLASS}"> ${mention} </span>`;
81
81
  const regex = new RegExp(mentionEl, 'g');
82
82
  return content.replace(regex, mention);
83
83
  }