jupyterlab-chat 0.8.1 → 0.10.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.
package/lib/factory.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IAttachmentOpenerRegistry, IChatCommandRegistry } from '@jupyter/chat';
1
+ import { IAttachmentOpenerRegistry, IChatCommandRegistry, IInputToolbarRegistry } from '@jupyter/chat';
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { IDocumentManager } from '@jupyterlab/docmanager';
4
4
  import { ABCWidgetFactory, DocumentRegistry } from '@jupyterlab/docregistry';
@@ -8,7 +8,7 @@ import { ISignal } from '@lumino/signaling';
8
8
  import { LabChatModel } from './model';
9
9
  import { LabChatPanel } from './widget';
10
10
  import { YChat } from './ychat';
11
- import { ILabChatConfig, IWidgetConfig } from './token';
11
+ import { IInputToolbarRegistryFactory, ILabChatConfig, IWidgetConfig } from './token';
12
12
  /**
13
13
  * The object provided by the chatDocument extension.
14
14
  * It is used to set the current config (from settings) to newly created chat widget,
@@ -50,9 +50,9 @@ export declare class ChatWidgetFactory extends ABCWidgetFactory<LabChatPanel, La
50
50
  protected createNewWidget(context: ChatWidgetFactory.IContext): LabChatPanel;
51
51
  private _themeManager;
52
52
  private _rmRegistry;
53
- private _documentManager?;
54
53
  private _chatCommandRegistry?;
55
54
  private _attachmentOpenerRegistry?;
55
+ private _inputToolbarFactory?;
56
56
  }
57
57
  export declare namespace ChatWidgetFactory {
58
58
  interface IContext extends DocumentRegistry.IContext<LabChatModel> {
@@ -61,13 +61,14 @@ export declare namespace ChatWidgetFactory {
61
61
  documentManager?: IDocumentManager;
62
62
  chatCommandRegistry?: IChatCommandRegistry;
63
63
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
64
+ inputToolbarRegistry?: IInputToolbarRegistry;
64
65
  }
65
66
  interface IOptions<T extends LabChatPanel> extends DocumentRegistry.IWidgetFactoryOptions<T> {
66
67
  themeManager: IThemeManager | null;
67
68
  rmRegistry: IRenderMimeRegistry;
68
- documentManager?: IDocumentManager;
69
69
  chatCommandRegistry?: IChatCommandRegistry;
70
70
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
71
+ inputToolbarFactory?: IInputToolbarRegistryFactory;
71
72
  }
72
73
  }
73
74
  export declare class LabChatModelFactory implements DocumentRegistry.IModelFactory<LabChatModel> {
@@ -122,4 +123,5 @@ export declare class LabChatModelFactory implements DocumentRegistry.IModelFacto
122
123
  private _commands?;
123
124
  private _activeCellManager;
124
125
  private _selectionWatcher;
126
+ private _documentManager;
125
127
  }
package/lib/factory.js CHANGED
@@ -50,9 +50,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory {
50
50
  super(options);
51
51
  this._themeManager = options.themeManager;
52
52
  this._rmRegistry = options.rmRegistry;
53
- this._documentManager = options.documentManager;
54
53
  this._chatCommandRegistry = options.chatCommandRegistry;
55
54
  this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
55
+ this._inputToolbarFactory = options.inputToolbarFactory;
56
56
  }
57
57
  /**
58
58
  * Create a new widget given a context.
@@ -63,9 +63,11 @@ export class ChatWidgetFactory extends ABCWidgetFactory {
63
63
  createNewWidget(context) {
64
64
  context.rmRegistry = this._rmRegistry;
65
65
  context.themeManager = this._themeManager;
66
- context.documentManager = this._documentManager;
67
66
  context.chatCommandRegistry = this._chatCommandRegistry;
68
67
  context.attachmentOpenerRegistry = this._attachmentOpenerRegistry;
68
+ if (this._inputToolbarFactory) {
69
+ context.inputToolbarRegistry = this._inputToolbarFactory.create();
70
+ }
69
71
  return new LabChatPanel({
70
72
  context,
71
73
  content: new ChatWidget(context)
@@ -74,7 +76,7 @@ export class ChatWidgetFactory extends ABCWidgetFactory {
74
76
  }
75
77
  export class LabChatModelFactory {
76
78
  constructor(options) {
77
- var _a, _b;
79
+ var _a, _b, _c;
78
80
  this.collaborative = true;
79
81
  this._disposed = false;
80
82
  this._user = options.user;
@@ -82,6 +84,7 @@ export class LabChatModelFactory {
82
84
  this._commands = options.commands;
83
85
  this._activeCellManager = (_a = options.activeCellManager) !== null && _a !== void 0 ? _a : null;
84
86
  this._selectionWatcher = (_b = options.selectionWatcher) !== null && _b !== void 0 ? _b : null;
87
+ this._documentManager = (_c = options.documentManager) !== null && _c !== void 0 ? _c : null;
85
88
  }
86
89
  /**
87
90
  * The name of the model.
@@ -144,7 +147,8 @@ export class LabChatModelFactory {
144
147
  widgetConfig: this._widgetConfig,
145
148
  commands: this._commands,
146
149
  activeCellManager: this._activeCellManager,
147
- selectionWatcher: this._selectionWatcher
150
+ selectionWatcher: this._selectionWatcher,
151
+ documentManager: this._documentManager
148
152
  });
149
153
  }
150
154
  }
package/lib/model.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatModel, IChatMessage, IInputModel, INewMessage, IUser } from '@jupyter/chat';
1
+ import { AbstractChatModel, AbstractChatContext, IChatContext, IChatMessage, IChatModel, IInputModel, INewMessage, IUser } from '@jupyter/chat';
2
2
  import { IChangedArgs } from '@jupyterlab/coreutils';
3
3
  import { DocumentRegistry } from '@jupyterlab/docregistry';
4
4
  import { User } from '@jupyterlab/services';
@@ -10,7 +10,7 @@ import { YChat } from './ychat';
10
10
  * Chat model namespace.
11
11
  */
12
12
  export declare namespace LabChatModel {
13
- interface IOptions extends ChatModel.IOptions {
13
+ interface IOptions extends IChatModel.IOptions {
14
14
  widgetConfig: IWidgetConfig;
15
15
  user: User.IIdentity | null;
16
16
  sharedModel?: YChat;
@@ -20,7 +20,7 @@ export declare namespace LabChatModel {
20
20
  /**
21
21
  * The chat model.
22
22
  */
23
- export declare class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
23
+ export declare class LabChatModel extends AbstractChatModel implements DocumentRegistry.IModel {
24
24
  constructor(options: LabChatModel.IOptions);
25
25
  readonly collaborative = true;
26
26
  get user(): IUser;
@@ -38,8 +38,13 @@ export declare class LabChatModel extends ChatModel implements DocumentRegistry.
38
38
  fromString(data: string): void;
39
39
  toJSON(): PartialJSONObject;
40
40
  fromJSON(data: PartialJSONObject): void;
41
- messagesInserted(index: number, messages: IChatMessage[]): void;
41
+ createChatContext(): IChatContext;
42
+ messagesInserted(index: number, messages: IChatMessage[]): Promise<void>;
42
43
  sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
44
+ /**
45
+ * Override the clear messages method.
46
+ */
47
+ clearMessages(): void;
43
48
  updateMessage(id: string, updatedMessage: IChatMessage): Promise<boolean | void> | boolean | void;
44
49
  deleteMessage(id: string): Promise<boolean | void> | boolean | void;
45
50
  /**
@@ -51,6 +56,7 @@ export declare class LabChatModel extends ChatModel implements DocumentRegistry.
51
56
  * Used to populate the writers list.
52
57
  */
53
58
  onAwarenessChange: () => void;
59
+ private _buildMentionList;
54
60
  private _resetWritingStatus;
55
61
  private _onchange;
56
62
  readonly defaultKernelName: string;
@@ -65,3 +71,12 @@ export declare class LabChatModel extends ChatModel implements DocumentRegistry.
65
71
  private _timeoutWriting;
66
72
  private _user;
67
73
  }
74
+ /**
75
+ * The chat context to be sent to the input model.
76
+ */
77
+ export declare class LabChatContext extends AbstractChatContext {
78
+ /**
79
+ * The list of users who have connected to this chat.
80
+ */
81
+ get users(): IUser[];
82
+ }
package/lib/model.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- import { ChatModel } from '@jupyter/chat';
5
+ import { AbstractChatModel, AbstractChatContext } from '@jupyter/chat';
6
6
  import { PromiseDelegate, UUID } from '@lumino/coreutils';
7
7
  import { Signal } from '@lumino/signaling';
8
8
  import { YChat } from './ychat';
@@ -10,7 +10,7 @@ const WRITING_DELAY = 1000;
10
10
  /**
11
11
  * The chat model.
12
12
  */
13
- export class LabChatModel extends ChatModel {
13
+ export class LabChatModel extends AbstractChatModel {
14
14
  constructor(options) {
15
15
  super(options);
16
16
  this.collaborative = true;
@@ -48,17 +48,17 @@ export class LabChatModel extends ChatModel {
48
48
  }
49
49
  this.updateWriters(writers);
50
50
  };
51
- this._onchange = (_, changes) => {
51
+ this._onchange = async (_, changes) => {
52
52
  if (changes.messageChanges) {
53
53
  const msgDelta = changes.messageChanges;
54
54
  let index = 0;
55
- msgDelta.forEach(delta => {
55
+ for (const delta of msgDelta) {
56
56
  if (delta.retain) {
57
57
  index += delta.retain;
58
58
  }
59
59
  else if (delta.insert) {
60
60
  const messages = delta.insert.map(ymessage => {
61
- const { sender, attachments: attachmentIds, ...baseMessage } = ymessage;
61
+ const { sender, attachments: attachmentIds, mentions: mentionsIds, ...baseMessage } = ymessage;
62
62
  // Build the base message with sender.
63
63
  const msg = {
64
64
  ...baseMessage,
@@ -79,15 +79,21 @@ export class LabChatModel extends ChatModel {
79
79
  msg.attachments = attachments;
80
80
  }
81
81
  }
82
+ const mentions = mentionsIds === null || mentionsIds === void 0 ? void 0 : mentionsIds.map(user => this.sharedModel.getUser(user) || {
83
+ username: 'User undefined'
84
+ });
85
+ if (mentions === null || mentions === void 0 ? void 0 : mentions.length) {
86
+ msg.mentions = mentions;
87
+ }
82
88
  return msg;
83
89
  });
84
- this.messagesInserted(index, messages);
90
+ await this.messagesInserted(index, messages);
85
91
  index += messages.length;
86
92
  }
87
93
  else if (delta.delete) {
88
94
  this.messagesDeleted(index, delta.delete);
89
95
  }
90
- });
96
+ }
91
97
  }
92
98
  if (changes.metadataChanges) {
93
99
  changes.metadataChanges.forEach(change => {
@@ -98,6 +104,14 @@ export class LabChatModel extends ChatModel {
98
104
  }
99
105
  });
100
106
  }
107
+ if (changes.userChanges) {
108
+ // Update the current user if it changes (if it has been mentioned for example).
109
+ changes.userChanges.forEach(change => {
110
+ if (change.key === this._user.username && change.newValue) {
111
+ this._user = change.newValue;
112
+ }
113
+ });
114
+ }
101
115
  };
102
116
  this.defaultKernelName = '';
103
117
  this.defaultKernelLanguage = '';
@@ -178,14 +192,18 @@ export class LabChatModel extends ChatModel {
178
192
  fromJSON(data) {
179
193
  // nothing to do
180
194
  }
181
- messagesInserted(index, messages) {
195
+ createChatContext() {
196
+ return new LabChatContext({ model: this });
197
+ }
198
+ async messagesInserted(index, messages) {
182
199
  // Ensure the chat has an ID before inserting the messages, to properly catch the
183
200
  // unread messages (the last read message is saved using the chat ID).
184
- this._ready.promise.then(() => {
201
+ return this._ready.promise.then(() => {
185
202
  super.messagesInserted(index, messages);
186
203
  });
187
204
  }
188
205
  sendMessage(message) {
206
+ var _a;
189
207
  this._resetWritingStatus();
190
208
  if (this._timeoutWriting !== null) {
191
209
  window.clearTimeout(this._timeoutWriting);
@@ -203,13 +221,25 @@ export class LabChatModel extends ChatModel {
203
221
  this.sharedModel.setUser(this._user);
204
222
  }
205
223
  // Add the attachments to the message.
206
- if (this.input.attachments.length) {
207
- const attachmentIds = this.input.attachments.map(attachment => this.sharedModel.setAttachment(attachment));
224
+ const attachmentIds = (_a = this.input.attachments) === null || _a === void 0 ? void 0 : _a.map(attachment => this.sharedModel.setAttachment(attachment));
225
+ if (attachmentIds === null || attachmentIds === void 0 ? void 0 : attachmentIds.length) {
208
226
  msg.attachments = attachmentIds;
209
- this.input.clearAttachments();
210
227
  }
228
+ this.input.clearAttachments();
229
+ // Add the mentioned users.
230
+ const mentions = this._buildMentionList(this.input.mentions, message.body);
231
+ if (mentions.length) {
232
+ msg.mentions = mentions;
233
+ }
234
+ this.input.clearMentions();
211
235
  this.sharedModel.addMessage(msg);
212
236
  }
237
+ /**
238
+ * Override the clear messages method.
239
+ */
240
+ clearMessages() {
241
+ // No-op as we may not need to clear the messages in file based chat.
242
+ }
213
243
  updateMessage(id, updatedMessage) {
214
244
  var _a;
215
245
  const index = this.sharedModel.getMessageIndex(id);
@@ -229,10 +259,22 @@ export class LabChatModel extends ChatModel {
229
259
  edited: true
230
260
  };
231
261
  }
262
+ // Update the attachments.
232
263
  const attachmentIds = (_a = updatedMessage.attachments) === null || _a === void 0 ? void 0 : _a.map(attachment => this.sharedModel.setAttachment(attachment));
233
- if (attachmentIds) {
264
+ if (attachmentIds === null || attachmentIds === void 0 ? void 0 : attachmentIds.length) {
234
265
  message.attachments = attachmentIds;
235
266
  }
267
+ else {
268
+ delete message.attachments;
269
+ }
270
+ // Update the mentioned users.
271
+ const mentions = this._buildMentionList(updatedMessage.mentions, updatedMessage.body);
272
+ if (mentions.length) {
273
+ message.mentions = mentions;
274
+ }
275
+ else {
276
+ delete message.mentions;
277
+ }
236
278
  this.sharedModel.updateMessage(index, message);
237
279
  }
238
280
  deleteMessage(id) {
@@ -246,6 +288,28 @@ export class LabChatModel extends ChatModel {
246
288
  message.deleted = true;
247
289
  this.sharedModel.updateMessage(index, message);
248
290
  }
291
+ _buildMentionList(userMentions, body) {
292
+ if (!userMentions) {
293
+ return [];
294
+ }
295
+ const mentions = [];
296
+ userMentions.forEach(user => {
297
+ // Make sure the user is still mentioned.
298
+ if (!user.mention_name) {
299
+ return;
300
+ }
301
+ const regex = new RegExp(user.mention_name);
302
+ if (!regex.exec(body)) {
303
+ return;
304
+ }
305
+ // Save the mention name if necessary.
306
+ if (!(this.sharedModel.getUser(user.username) === user)) {
307
+ this.sharedModel.setUser(user);
308
+ }
309
+ mentions.push(user.username);
310
+ });
311
+ return mentions;
312
+ }
249
313
  _resetWritingStatus() {
250
314
  const awareness = this.sharedModel.awareness;
251
315
  const states = awareness.getLocalState();
@@ -254,3 +318,28 @@ export class LabChatModel extends ChatModel {
254
318
  this._timeoutWriting = null;
255
319
  }
256
320
  }
321
+ /**
322
+ * The chat context to be sent to the input model.
323
+ */
324
+ export class LabChatContext extends AbstractChatContext {
325
+ /**
326
+ * The list of users who have connected to this chat.
327
+ */
328
+ get users() {
329
+ const model = this._model;
330
+ const users = new Set();
331
+ // Get the user list from the shared YChat
332
+ Object.values(model.sharedModel.users).forEach(userObject => {
333
+ users.add(userObject);
334
+ });
335
+ // Add the users connected to the chat (even if they never sent a message).
336
+ model.sharedModel.awareness.getStates().forEach(value => {
337
+ const user = value.user;
338
+ if (!user) {
339
+ return;
340
+ }
341
+ users.add(user);
342
+ });
343
+ return Array.from(users);
344
+ }
345
+ }
package/lib/token.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IConfig, IActiveCellManager, ISelectionWatcher, ChatWidget } from '@jupyter/chat';
1
+ import { IConfig, IActiveCellManager, ISelectionWatcher, ChatWidget, IInputToolbarRegistry } from '@jupyter/chat';
2
2
  import { WidgetTracker } from '@jupyterlab/apputils';
3
3
  import { DocumentRegistry } from '@jupyterlab/docregistry';
4
4
  import { Token } from '@lumino/coreutils';
@@ -89,3 +89,16 @@ export declare const IActiveCellManagerToken: Token<IActiveCellManager>;
89
89
  * The selection watcher plugin.
90
90
  */
91
91
  export declare const ISelectionWatcherToken: Token<ISelectionWatcher>;
92
+ /**
93
+ * The input toolbar registry factory.
94
+ */
95
+ export interface IInputToolbarRegistryFactory {
96
+ /**
97
+ * Create an input toolbar registry.
98
+ */
99
+ create: () => IInputToolbarRegistry;
100
+ }
101
+ /**
102
+ * The token of the factory to create an input toolbar registry.
103
+ */
104
+ export declare const IInputToolbarRegistryFactory: Token<IInputToolbarRegistryFactory>;
package/lib/token.js CHANGED
@@ -57,3 +57,7 @@ export const IActiveCellManagerToken = new Token('jupyterlab-chat:IActiveCellMan
57
57
  * The selection watcher plugin.
58
58
  */
59
59
  export const ISelectionWatcherToken = new Token('jupyterlab-chat:ISelectionWatcher');
60
+ /**
61
+ * The token of the factory to create an input toolbar registry.
62
+ */
63
+ export const IInputToolbarRegistryFactory = new Token('jupyterlab-chat:IInputToolbarRegistryFactory');
package/lib/widget.d.ts CHANGED
@@ -8,7 +8,7 @@ import { CommandRegistry } from '@lumino/commands';
8
8
  import { Message } from '@lumino/messaging';
9
9
  import { Panel } from '@lumino/widgets';
10
10
  import { LabChatModel } from './model';
11
- import { IDocumentManager } from '@jupyterlab/docmanager';
11
+ import { IInputToolbarRegistryFactory } from './token';
12
12
  /**
13
13
  * DocumentWidget: widget that represents the view or editor for a file type.
14
14
  */
@@ -88,9 +88,9 @@ export declare class ChatPanel extends SidePanel {
88
88
  private _openChat;
89
89
  private _rmRegistry;
90
90
  private _themeManager;
91
- private _documentManager?;
92
91
  private _chatCommandRegistry?;
93
92
  private _attachmentOpenerRegistry?;
93
+ private _inputToolbarFactory?;
94
94
  }
95
95
  /**
96
96
  * The chat panel namespace.
@@ -105,9 +105,9 @@ export declare namespace ChatPanel {
105
105
  rmRegistry: IRenderMimeRegistry;
106
106
  themeManager: IThemeManager | null;
107
107
  defaultDirectory: string;
108
- documentManager?: IDocumentManager;
109
108
  chatCommandRegistry?: IChatCommandRegistry;
110
109
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
110
+ inputToolbarFactory?: IInputToolbarRegistryFactory;
111
111
  }
112
112
  }
113
113
  /**
package/lib/widget.js CHANGED
@@ -105,9 +105,9 @@ export class ChatPanel extends SidePanel {
105
105
  this._rmRegistry = options.rmRegistry;
106
106
  this._themeManager = options.themeManager;
107
107
  this._defaultDirectory = options.defaultDirectory;
108
- this._documentManager = options.documentManager;
109
108
  this._chatCommandRegistry = options.chatCommandRegistry;
110
109
  this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
110
+ this._inputToolbarFactory = options.inputToolbarFactory;
111
111
  const addChat = new CommandToolbarButton({
112
112
  commands: this._commands,
113
113
  id: CommandIDs.createChat,
@@ -152,14 +152,19 @@ export class ChatPanel extends SidePanel {
152
152
  for (let i = 0; i < this.widgets.length; i++) {
153
153
  content.collapse(i);
154
154
  }
155
+ // Create the toolbar registry.
156
+ let inputToolbarRegistry;
157
+ if (this._inputToolbarFactory) {
158
+ inputToolbarRegistry = this._inputToolbarFactory.create();
159
+ }
155
160
  // Create a new widget.
156
161
  const widget = new ChatWidget({
157
162
  model: model,
158
163
  rmRegistry: this._rmRegistry,
159
164
  themeManager: this._themeManager,
160
- documentManager: this._documentManager,
161
165
  chatCommandRegistry: this._chatCommandRegistry,
162
- attachmentOpenerRegistry: this._attachmentOpenerRegistry
166
+ attachmentOpenerRegistry: this._attachmentOpenerRegistry,
167
+ inputToolbarRegistry
163
168
  });
164
169
  this.addWidget(new ChatSection({
165
170
  widget,
package/lib/ychat.d.ts CHANGED
@@ -65,7 +65,7 @@ export declare class YChat extends YDocument<IChatChanges> {
65
65
  */
66
66
  static create(options?: YDocument.IOptions): YChat;
67
67
  get id(): string;
68
- get users(): JSONObject;
68
+ get users(): Record<string, IUser>;
69
69
  get messages(): string[];
70
70
  get attachments(): JSONObject;
71
71
  getSource(): JSONObject;
package/lib/ychat.js CHANGED
@@ -140,7 +140,8 @@ export class YChat extends YDocument {
140
140
  return this._metadata.get('id') || '';
141
141
  }
142
142
  get users() {
143
- return JSONExt.deepCopy(this._users.toJSON());
143
+ const userDict = JSONExt.deepCopy(this._users.toJSON());
144
+ return userDict;
144
145
  }
145
146
  get messages() {
146
147
  return JSONExt.deepCopy(this._messages.toJSON());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-chat",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "The library to build a chat based on shared document",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -42,7 +42,7 @@
42
42
  "watch:src": "tsc -w --sourceMap"
43
43
  },
44
44
  "dependencies": {
45
- "@jupyter/chat": "^0.8.1",
45
+ "@jupyter/chat": "^0.10.0",
46
46
  "@jupyter/collaborative-drive": "^3.0.0",
47
47
  "@jupyter/ydoc": "^2.0.0 || ^3.0.0",
48
48
  "@jupyterlab/application": "^4.2.0",
package/src/factory.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  IActiveCellManager,
9
9
  IAttachmentOpenerRegistry,
10
10
  IChatCommandRegistry,
11
+ IInputToolbarRegistry,
11
12
  ISelectionWatcher
12
13
  } from '@jupyter/chat';
13
14
  import { IThemeManager } from '@jupyterlab/apputils';
@@ -21,7 +22,11 @@ import { ISignal, Signal } from '@lumino/signaling';
21
22
  import { LabChatModel } from './model';
22
23
  import { LabChatPanel } from './widget';
23
24
  import { YChat } from './ychat';
24
- import { ILabChatConfig, IWidgetConfig } from './token';
25
+ import {
26
+ IInputToolbarRegistryFactory,
27
+ ILabChatConfig,
28
+ IWidgetConfig
29
+ } from './token';
25
30
 
26
31
  /**
27
32
  * The object provided by the chatDocument extension.
@@ -76,9 +81,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
76
81
  super(options);
77
82
  this._themeManager = options.themeManager;
78
83
  this._rmRegistry = options.rmRegistry;
79
- this._documentManager = options.documentManager;
80
84
  this._chatCommandRegistry = options.chatCommandRegistry;
81
85
  this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
86
+ this._inputToolbarFactory = options.inputToolbarFactory;
82
87
  }
83
88
 
84
89
  /**
@@ -90,9 +95,11 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
90
95
  protected createNewWidget(context: ChatWidgetFactory.IContext): LabChatPanel {
91
96
  context.rmRegistry = this._rmRegistry;
92
97
  context.themeManager = this._themeManager;
93
- context.documentManager = this._documentManager;
94
98
  context.chatCommandRegistry = this._chatCommandRegistry;
95
99
  context.attachmentOpenerRegistry = this._attachmentOpenerRegistry;
100
+ if (this._inputToolbarFactory) {
101
+ context.inputToolbarRegistry = this._inputToolbarFactory.create();
102
+ }
96
103
  return new LabChatPanel({
97
104
  context,
98
105
  content: new ChatWidget(context)
@@ -101,9 +108,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
101
108
 
102
109
  private _themeManager: IThemeManager | null;
103
110
  private _rmRegistry: IRenderMimeRegistry;
104
- private _documentManager?: IDocumentManager;
105
111
  private _chatCommandRegistry?: IChatCommandRegistry;
106
112
  private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
113
+ private _inputToolbarFactory?: IInputToolbarRegistryFactory;
107
114
  }
108
115
 
109
116
  export namespace ChatWidgetFactory {
@@ -113,15 +120,16 @@ export namespace ChatWidgetFactory {
113
120
  documentManager?: IDocumentManager;
114
121
  chatCommandRegistry?: IChatCommandRegistry;
115
122
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
123
+ inputToolbarRegistry?: IInputToolbarRegistry;
116
124
  }
117
125
 
118
126
  export interface IOptions<T extends LabChatPanel>
119
127
  extends DocumentRegistry.IWidgetFactoryOptions<T> {
120
128
  themeManager: IThemeManager | null;
121
129
  rmRegistry: IRenderMimeRegistry;
122
- documentManager?: IDocumentManager;
123
130
  chatCommandRegistry?: IChatCommandRegistry;
124
131
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
132
+ inputToolbarFactory?: IInputToolbarRegistryFactory;
125
133
  }
126
134
  }
127
135
 
@@ -134,6 +142,7 @@ export class LabChatModelFactory
134
142
  this._commands = options.commands;
135
143
  this._activeCellManager = options.activeCellManager ?? null;
136
144
  this._selectionWatcher = options.selectionWatcher ?? null;
145
+ this._documentManager = options.documentManager ?? null;
137
146
  }
138
147
 
139
148
  collaborative = true;
@@ -206,7 +215,8 @@ export class LabChatModelFactory
206
215
  widgetConfig: this._widgetConfig,
207
216
  commands: this._commands,
208
217
  activeCellManager: this._activeCellManager,
209
- selectionWatcher: this._selectionWatcher
218
+ selectionWatcher: this._selectionWatcher,
219
+ documentManager: this._documentManager
210
220
  });
211
221
  }
212
222
 
@@ -216,4 +226,5 @@ export class LabChatModelFactory
216
226
  private _commands?: CommandRegistry;
217
227
  private _activeCellManager: IActiveCellManager | null;
218
228
  private _selectionWatcher: ISelectionWatcher | null;
229
+ private _documentManager: IDocumentManager | null;
219
230
  }
package/src/model.ts CHANGED
@@ -4,9 +4,12 @@
4
4
  */
5
5
 
6
6
  import {
7
- ChatModel,
7
+ AbstractChatModel,
8
+ AbstractChatContext,
8
9
  IAttachment,
10
+ IChatContext,
9
11
  IChatMessage,
12
+ IChatModel,
10
13
  IInputModel,
11
14
  INewMessage,
12
15
  IUser
@@ -26,7 +29,7 @@ const WRITING_DELAY = 1000;
26
29
  * Chat model namespace.
27
30
  */
28
31
  export namespace LabChatModel {
29
- export interface IOptions extends ChatModel.IOptions {
32
+ export interface IOptions extends IChatModel.IOptions {
30
33
  widgetConfig: IWidgetConfig;
31
34
  user: User.IIdentity | null;
32
35
  sharedModel?: YChat;
@@ -37,7 +40,10 @@ export namespace LabChatModel {
37
40
  /**
38
41
  * The chat model.
39
42
  */
40
- export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
43
+ export class LabChatModel
44
+ extends AbstractChatModel
45
+ implements DocumentRegistry.IModel
46
+ {
41
47
  constructor(options: LabChatModel.IOptions) {
42
48
  super(options);
43
49
 
@@ -133,10 +139,17 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
133
139
  // nothing to do
134
140
  }
135
141
 
136
- messagesInserted(index: number, messages: IChatMessage[]): void {
142
+ createChatContext(): IChatContext {
143
+ return new LabChatContext({ model: this });
144
+ }
145
+
146
+ async messagesInserted(
147
+ index: number,
148
+ messages: IChatMessage[]
149
+ ): Promise<void> {
137
150
  // Ensure the chat has an ID before inserting the messages, to properly catch the
138
151
  // unread messages (the last read message is saved using the chat ID).
139
- this._ready.promise.then(() => {
152
+ return this._ready.promise.then(() => {
140
153
  super.messagesInserted(index, messages);
141
154
  });
142
155
  }
@@ -162,17 +175,31 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
162
175
  }
163
176
 
164
177
  // Add the attachments to the message.
165
- if (this.input.attachments.length) {
166
- const attachmentIds = this.input.attachments.map(attachment =>
167
- this.sharedModel.setAttachment(attachment)
168
- );
178
+ const attachmentIds = this.input.attachments?.map(attachment =>
179
+ this.sharedModel.setAttachment(attachment)
180
+ );
181
+ if (attachmentIds?.length) {
169
182
  msg.attachments = attachmentIds;
170
- this.input.clearAttachments();
171
183
  }
184
+ this.input.clearAttachments();
185
+
186
+ // Add the mentioned users.
187
+ const mentions = this._buildMentionList(this.input.mentions, message.body);
188
+ if (mentions.length) {
189
+ msg.mentions = mentions;
190
+ }
191
+ this.input.clearMentions();
172
192
 
173
193
  this.sharedModel.addMessage(msg);
174
194
  }
175
195
 
196
+ /**
197
+ * Override the clear messages method.
198
+ */
199
+ clearMessages(): void {
200
+ // No-op as we may not need to clear the messages in file based chat.
201
+ }
202
+
176
203
  updateMessage(
177
204
  id: string,
178
205
  updatedMessage: IChatMessage
@@ -194,12 +221,28 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
194
221
  edited: true
195
222
  };
196
223
  }
224
+
225
+ // Update the attachments.
197
226
  const attachmentIds = updatedMessage.attachments?.map(attachment =>
198
227
  this.sharedModel.setAttachment(attachment)
199
228
  );
200
- if (attachmentIds) {
229
+ if (attachmentIds?.length) {
201
230
  message.attachments = attachmentIds;
231
+ } else {
232
+ delete message.attachments;
202
233
  }
234
+
235
+ // Update the mentioned users.
236
+ const mentions = this._buildMentionList(
237
+ updatedMessage.mentions,
238
+ updatedMessage.body
239
+ );
240
+ if (mentions.length) {
241
+ message.mentions = mentions;
242
+ } else {
243
+ delete message.mentions;
244
+ }
245
+
203
246
  this.sharedModel.updateMessage(index, message as IYmessage);
204
247
  }
205
248
 
@@ -251,6 +294,33 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
251
294
  this.updateWriters(writers);
252
295
  };
253
296
 
297
+ private _buildMentionList(
298
+ userMentions: IUser[] | undefined,
299
+ body: string
300
+ ): string[] {
301
+ if (!userMentions) {
302
+ return [];
303
+ }
304
+ const mentions: string[] = [];
305
+ userMentions.forEach(user => {
306
+ // Make sure the user is still mentioned.
307
+ if (!user.mention_name) {
308
+ return;
309
+ }
310
+ const regex = new RegExp(user.mention_name);
311
+ if (!regex.exec(body)) {
312
+ return;
313
+ }
314
+
315
+ // Save the mention name if necessary.
316
+ if (!(this.sharedModel.getUser(user.username) === user)) {
317
+ this.sharedModel.setUser(user);
318
+ }
319
+ mentions.push(user.username);
320
+ });
321
+ return mentions;
322
+ }
323
+
254
324
  private _resetWritingStatus() {
255
325
  const awareness = this.sharedModel.awareness;
256
326
  const states = awareness.getLocalState();
@@ -259,11 +329,11 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
259
329
  this._timeoutWriting = null;
260
330
  }
261
331
 
262
- private _onchange = (_: YChat, changes: IChatChanges) => {
332
+ private _onchange = async (_: YChat, changes: IChatChanges) => {
263
333
  if (changes.messageChanges) {
264
334
  const msgDelta = changes.messageChanges;
265
335
  let index = 0;
266
- msgDelta.forEach(delta => {
336
+ for (const delta of msgDelta) {
267
337
  if (delta.retain) {
268
338
  index += delta.retain;
269
339
  } else if (delta.insert) {
@@ -271,6 +341,7 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
271
341
  const {
272
342
  sender,
273
343
  attachments: attachmentIds,
344
+ mentions: mentionsIds,
274
345
  ...baseMessage
275
346
  } = ymessage;
276
347
 
@@ -296,14 +367,24 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
296
367
  }
297
368
  }
298
369
 
370
+ const mentions = mentionsIds?.map(
371
+ user =>
372
+ this.sharedModel.getUser(user) || {
373
+ username: 'User undefined'
374
+ }
375
+ );
376
+
377
+ if (mentions?.length) {
378
+ msg.mentions = mentions;
379
+ }
299
380
  return msg;
300
381
  });
301
- this.messagesInserted(index, messages);
382
+ await this.messagesInserted(index, messages);
302
383
  index += messages.length;
303
384
  } else if (delta.delete) {
304
385
  this.messagesDeleted(index, delta.delete);
305
386
  }
306
- });
387
+ }
307
388
  }
308
389
 
309
390
  if (changes.metadataChanges) {
@@ -315,6 +396,15 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
315
396
  }
316
397
  });
317
398
  }
399
+
400
+ if (changes.userChanges) {
401
+ // Update the current user if it changes (if it has been mentioned for example).
402
+ changes.userChanges.forEach(change => {
403
+ if (change.key === this._user.username && change.newValue) {
404
+ this._user = change.newValue;
405
+ }
406
+ });
407
+ }
318
408
  };
319
409
 
320
410
  readonly defaultKernelName: string = '';
@@ -332,3 +422,32 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
332
422
 
333
423
  private _user: IUser;
334
424
  }
425
+
426
+ /**
427
+ * The chat context to be sent to the input model.
428
+ */
429
+ export class LabChatContext extends AbstractChatContext {
430
+ /**
431
+ * The list of users who have connected to this chat.
432
+ */
433
+ get users(): IUser[] {
434
+ const model = this._model as LabChatModel;
435
+ const users = new Set<IUser>();
436
+
437
+ // Get the user list from the shared YChat
438
+ Object.values(model.sharedModel.users).forEach(userObject => {
439
+ users.add(userObject);
440
+ });
441
+
442
+ // Add the users connected to the chat (even if they never sent a message).
443
+ model.sharedModel.awareness.getStates().forEach(value => {
444
+ const user = value.user as IUser;
445
+ if (!user) {
446
+ return;
447
+ }
448
+ users.add(user);
449
+ });
450
+
451
+ return Array.from(users);
452
+ }
453
+ }
package/src/token.ts CHANGED
@@ -8,7 +8,8 @@ import {
8
8
  chatIcon,
9
9
  IActiveCellManager,
10
10
  ISelectionWatcher,
11
- ChatWidget
11
+ ChatWidget,
12
+ IInputToolbarRegistry
12
13
  } from '@jupyter/chat';
13
14
  import { WidgetTracker } from '@jupyterlab/apputils';
14
15
  import { DocumentRegistry } from '@jupyterlab/docregistry';
@@ -125,3 +126,21 @@ export const IActiveCellManagerToken = new Token<IActiveCellManager>(
125
126
  export const ISelectionWatcherToken = new Token<ISelectionWatcher>(
126
127
  'jupyterlab-chat:ISelectionWatcher'
127
128
  );
129
+
130
+ /**
131
+ * The input toolbar registry factory.
132
+ */
133
+ export interface IInputToolbarRegistryFactory {
134
+ /**
135
+ * Create an input toolbar registry.
136
+ */
137
+ create: () => IInputToolbarRegistry;
138
+ }
139
+
140
+ /**
141
+ * The token of the factory to create an input toolbar registry.
142
+ */
143
+ export const IInputToolbarRegistryFactory =
144
+ new Token<IInputToolbarRegistryFactory>(
145
+ 'jupyterlab-chat:IInputToolbarRegistryFactory'
146
+ );
package/src/widget.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  IAttachmentOpenerRegistry,
9
9
  IChatCommandRegistry,
10
10
  IChatModel,
11
+ IInputToolbarRegistry,
11
12
  readIcon
12
13
  } from '@jupyter/chat';
13
14
  import { ICollaborativeDrive } from '@jupyter/collaborative-drive';
@@ -33,8 +34,11 @@ import { AccordionPanel, Panel } from '@lumino/widgets';
33
34
  import React, { useState } from 'react';
34
35
 
35
36
  import { LabChatModel } from './model';
36
- import { CommandIDs, chatFileType } from './token';
37
- import { IDocumentManager } from '@jupyterlab/docmanager';
37
+ import {
38
+ CommandIDs,
39
+ IInputToolbarRegistryFactory,
40
+ chatFileType
41
+ } from './token';
38
42
 
39
43
  const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel';
40
44
  const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread';
@@ -104,9 +108,9 @@ export class ChatPanel extends SidePanel {
104
108
  this._rmRegistry = options.rmRegistry;
105
109
  this._themeManager = options.themeManager;
106
110
  this._defaultDirectory = options.defaultDirectory;
107
- this._documentManager = options.documentManager;
108
111
  this._chatCommandRegistry = options.chatCommandRegistry;
109
112
  this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
113
+ this._inputToolbarFactory = options.inputToolbarFactory;
110
114
 
111
115
  const addChat = new CommandToolbarButton({
112
116
  commands: this._commands,
@@ -163,14 +167,20 @@ export class ChatPanel extends SidePanel {
163
167
  content.collapse(i);
164
168
  }
165
169
 
170
+ // Create the toolbar registry.
171
+ let inputToolbarRegistry: IInputToolbarRegistry | undefined;
172
+ if (this._inputToolbarFactory) {
173
+ inputToolbarRegistry = this._inputToolbarFactory.create();
174
+ }
175
+
166
176
  // Create a new widget.
167
177
  const widget = new ChatWidget({
168
178
  model: model,
169
179
  rmRegistry: this._rmRegistry,
170
180
  themeManager: this._themeManager,
171
- documentManager: this._documentManager,
172
181
  chatCommandRegistry: this._chatCommandRegistry,
173
- attachmentOpenerRegistry: this._attachmentOpenerRegistry
182
+ attachmentOpenerRegistry: this._attachmentOpenerRegistry,
183
+ inputToolbarRegistry
174
184
  });
175
185
 
176
186
  this.addWidget(
@@ -289,9 +299,9 @@ export class ChatPanel extends SidePanel {
289
299
  private _openChat: ReactWidget;
290
300
  private _rmRegistry: IRenderMimeRegistry;
291
301
  private _themeManager: IThemeManager | null;
292
- private _documentManager?: IDocumentManager;
293
302
  private _chatCommandRegistry?: IChatCommandRegistry;
294
303
  private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
304
+ private _inputToolbarFactory?: IInputToolbarRegistryFactory;
295
305
  }
296
306
 
297
307
  /**
@@ -307,9 +317,9 @@ export namespace ChatPanel {
307
317
  rmRegistry: IRenderMimeRegistry;
308
318
  themeManager: IThemeManager | null;
309
319
  defaultDirectory: string;
310
- documentManager?: IDocumentManager;
311
320
  chatCommandRegistry?: IChatCommandRegistry;
312
321
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
322
+ inputToolbarFactory?: IInputToolbarRegistryFactory;
313
323
  }
314
324
  }
315
325
 
package/src/ychat.ts CHANGED
@@ -100,8 +100,13 @@ export class YChat extends YDocument<IChatChanges> {
100
100
  return (this._metadata.get('id') as string) || '';
101
101
  }
102
102
 
103
- get users(): JSONObject {
104
- return JSONExt.deepCopy(this._users.toJSON());
103
+ get users(): Record<string, IUser> {
104
+ const userDict = JSONExt.deepCopy(this._users.toJSON()) as Record<
105
+ string,
106
+ IUser
107
+ >;
108
+
109
+ return userDict;
105
110
  }
106
111
 
107
112
  get messages(): string[] {