jupyterlab-chat 0.7.0 → 0.8.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,5 +1,6 @@
1
- import { IAutocompletionRegistry } from '@jupyter/chat';
1
+ import { IAttachmentOpenerRegistry, IChatCommandRegistry } from '@jupyter/chat';
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
+ import { IDocumentManager } from '@jupyterlab/docmanager';
3
4
  import { ABCWidgetFactory, DocumentRegistry } from '@jupyterlab/docregistry';
4
5
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
6
  import { Contents } from '@jupyterlab/services';
@@ -49,18 +50,24 @@ export declare class ChatWidgetFactory extends ABCWidgetFactory<LabChatPanel, La
49
50
  protected createNewWidget(context: ChatWidgetFactory.IContext): LabChatPanel;
50
51
  private _themeManager;
51
52
  private _rmRegistry;
52
- private _autocompletionRegistry?;
53
+ private _documentManager?;
54
+ private _chatCommandRegistry?;
55
+ private _attachmentOpenerRegistry?;
53
56
  }
54
57
  export declare namespace ChatWidgetFactory {
55
58
  interface IContext extends DocumentRegistry.IContext<LabChatModel> {
56
59
  themeManager: IThemeManager | null;
57
60
  rmRegistry: IRenderMimeRegistry;
58
- autocompletionRegistry?: IAutocompletionRegistry;
61
+ documentManager?: IDocumentManager;
62
+ chatCommandRegistry?: IChatCommandRegistry;
63
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
59
64
  }
60
65
  interface IOptions<T extends LabChatPanel> extends DocumentRegistry.IWidgetFactoryOptions<T> {
61
66
  themeManager: IThemeManager | null;
62
67
  rmRegistry: IRenderMimeRegistry;
63
- autocompletionRegistry?: IAutocompletionRegistry;
68
+ documentManager?: IDocumentManager;
69
+ chatCommandRegistry?: IChatCommandRegistry;
70
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
64
71
  }
65
72
  }
66
73
  export declare class LabChatModelFactory implements DocumentRegistry.IModelFactory<LabChatModel> {
package/lib/factory.js CHANGED
@@ -50,7 +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._autocompletionRegistry = options.autocompletionRegistry;
53
+ this._documentManager = options.documentManager;
54
+ this._chatCommandRegistry = options.chatCommandRegistry;
55
+ this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
54
56
  }
55
57
  /**
56
58
  * Create a new widget given a context.
@@ -61,7 +63,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory {
61
63
  createNewWidget(context) {
62
64
  context.rmRegistry = this._rmRegistry;
63
65
  context.themeManager = this._themeManager;
64
- context.autocompletionRegistry = this._autocompletionRegistry;
66
+ context.documentManager = this._documentManager;
67
+ context.chatCommandRegistry = this._chatCommandRegistry;
68
+ context.attachmentOpenerRegistry = this._attachmentOpenerRegistry;
65
69
  return new LabChatPanel({
66
70
  context,
67
71
  content: new ChatWidget(context)
package/lib/model.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatModel, IChatMessage, INewMessage, IUser } from '@jupyter/chat';
1
+ import { ChatModel, IChatMessage, 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';
@@ -45,7 +45,7 @@ export declare class LabChatModel extends ChatModel implements DocumentRegistry.
45
45
  /**
46
46
  * Function called by the input on key pressed.
47
47
  */
48
- inputChanged(input?: string): void;
48
+ onInputChanged: (_: IInputModel, value: string) => void;
49
49
  /**
50
50
  * Triggered when an awareness state changes.
51
51
  * Used to populate the writers list.
package/lib/model.js CHANGED
@@ -14,6 +14,22 @@ export class LabChatModel extends ChatModel {
14
14
  constructor(options) {
15
15
  super(options);
16
16
  this.collaborative = true;
17
+ /**
18
+ * Function called by the input on key pressed.
19
+ */
20
+ this.onInputChanged = (_, value) => {
21
+ if (!value || !this.config.sendTypingNotification) {
22
+ return;
23
+ }
24
+ const awareness = this.sharedModel.awareness;
25
+ if (this._timeoutWriting !== null) {
26
+ window.clearTimeout(this._timeoutWriting);
27
+ }
28
+ awareness.setLocalStateField('isWriting', true);
29
+ this._timeoutWriting = window.setTimeout(() => {
30
+ this._resetWritingStatus();
31
+ }, WRITING_DELAY);
32
+ };
17
33
  /**
18
34
  * Triggered when an awareness state changes.
19
35
  * Used to populate the writers list.
@@ -42,12 +58,27 @@ export class LabChatModel extends ChatModel {
42
58
  }
43
59
  else if (delta.insert) {
44
60
  const messages = delta.insert.map(ymessage => {
61
+ const { sender, attachments: attachmentIds, ...baseMessage } = ymessage;
62
+ // Build the base message with sender.
45
63
  const msg = {
46
- ...ymessage,
47
- sender: this.sharedModel.getUser(ymessage.sender) || {
64
+ ...baseMessage,
65
+ sender: this.sharedModel.getUser(sender) || {
48
66
  username: 'User undefined'
49
67
  }
50
68
  };
69
+ // Add attachments.
70
+ if (attachmentIds) {
71
+ const attachments = [];
72
+ attachmentIds.forEach(attachmentId => {
73
+ const attachment = this.sharedModel.getAttachment(attachmentId);
74
+ if (attachment) {
75
+ attachments.push(attachment);
76
+ }
77
+ });
78
+ if (attachments.length) {
79
+ msg.attachments = attachments;
80
+ }
81
+ }
51
82
  return msg;
52
83
  });
53
84
  this.messagesInserted(index, messages);
@@ -91,6 +122,7 @@ export class LabChatModel extends ChatModel {
91
122
  this.config = config;
92
123
  });
93
124
  this.sharedModel.awareness.on('change', this.onAwarenessChange);
125
+ this.input.valueChanged.connect(this.onInputChanged);
94
126
  }
95
127
  get user() {
96
128
  return this._user;
@@ -170,9 +202,16 @@ export class LabChatModel extends ChatModel {
170
202
  if (!(this.sharedModel.getUser(this._user.username) === this._user)) {
171
203
  this.sharedModel.setUser(this._user);
172
204
  }
205
+ // Add the attachments to the message.
206
+ if (this.input.attachments.length) {
207
+ const attachmentIds = this.input.attachments.map(attachment => this.sharedModel.setAttachment(attachment));
208
+ msg.attachments = attachmentIds;
209
+ this.input.clearAttachments();
210
+ }
173
211
  this.sharedModel.addMessage(msg);
174
212
  }
175
213
  updateMessage(id, updatedMessage) {
214
+ var _a;
176
215
  const index = this.sharedModel.getMessageIndex(id);
177
216
  let message = this.sharedModel.getMessage(index);
178
217
  if (message) {
@@ -190,6 +229,10 @@ export class LabChatModel extends ChatModel {
190
229
  edited: true
191
230
  };
192
231
  }
232
+ const attachmentIds = (_a = updatedMessage.attachments) === null || _a === void 0 ? void 0 : _a.map(attachment => this.sharedModel.setAttachment(attachment));
233
+ if (attachmentIds) {
234
+ message.attachments = attachmentIds;
235
+ }
193
236
  this.sharedModel.updateMessage(index, message);
194
237
  }
195
238
  deleteMessage(id) {
@@ -203,22 +246,6 @@ export class LabChatModel extends ChatModel {
203
246
  message.deleted = true;
204
247
  this.sharedModel.updateMessage(index, message);
205
248
  }
206
- /**
207
- * Function called by the input on key pressed.
208
- */
209
- inputChanged(input) {
210
- if (!input || !this.config.sendTypingNotification) {
211
- return;
212
- }
213
- const awareness = this.sharedModel.awareness;
214
- if (this._timeoutWriting !== null) {
215
- window.clearTimeout(this._timeoutWriting);
216
- }
217
- awareness.setLocalStateField('isWriting', true);
218
- this._timeoutWriting = window.setTimeout(() => {
219
- this._resetWritingStatus();
220
- }, WRITING_DELAY);
221
- }
222
249
  _resetWritingStatus() {
223
250
  const awareness = this.sharedModel.awareness;
224
251
  const states = awareness.getLocalState();
package/lib/token.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { IConfig, IActiveCellManager, ISelectionWatcher } from '@jupyter/chat';
2
- import { IWidgetTracker } from '@jupyterlab/apputils';
1
+ import { IConfig, IActiveCellManager, ISelectionWatcher, ChatWidget } from '@jupyter/chat';
2
+ import { WidgetTracker } from '@jupyterlab/apputils';
3
3
  import { DocumentRegistry } from '@jupyterlab/docregistry';
4
4
  import { Token } from '@lumino/coreutils';
5
5
  import { ISignal } from '@lumino/signaling';
@@ -32,7 +32,7 @@ export interface IChatFactory {
32
32
  /**
33
33
  * The chat panel tracker.
34
34
  */
35
- tracker: IWidgetTracker<LabChatPanel>;
35
+ tracker: WidgetTracker<LabChatPanel | ChatWidget>;
36
36
  }
37
37
  /**
38
38
  * The interface for the chats config.
@@ -50,7 +50,7 @@ export interface IWidgetConfig {
50
50
  /**
51
51
  * A signal emitting when the configuration for the chats has changed.
52
52
  */
53
- export interface IConfigChanged extends ISignal<IWidgetConfig, Partial<ILabChatConfig>> {
53
+ export interface IConfigChanged/* eslint-disable-line @typescript-eslint/no-empty-object-type */ extends ISignal<IWidgetConfig, Partial<ILabChatConfig>> {
54
54
  }
55
55
  /**
56
56
  * Command ids.
package/lib/widget.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatWidget, IAutocompletionRegistry, IChatModel } from '@jupyter/chat';
1
+ import { ChatWidget, IAttachmentOpenerRegistry, IChatCommandRegistry, IChatModel } from '@jupyter/chat';
2
2
  import { ICollaborativeDrive } from '@jupyter/collaborative-drive';
3
3
  import { IThemeManager } from '@jupyterlab/apputils';
4
4
  import { DocumentWidget } from '@jupyterlab/docregistry';
@@ -8,6 +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
12
  /**
12
13
  * DocumentWidget: widget that represents the view or editor for a file type.
13
14
  */
@@ -45,7 +46,7 @@ export declare class ChatPanel extends SidePanel {
45
46
  * @param model - the model of the chat widget
46
47
  * @param name - the name of the chat.
47
48
  */
48
- addChat(model: IChatModel, path: string): void;
49
+ addChat(model: IChatModel): ChatWidget;
49
50
  /**
50
51
  * Update the list of available chats in the default directory.
51
52
  */
@@ -87,7 +88,9 @@ export declare class ChatPanel extends SidePanel {
87
88
  private _openChat;
88
89
  private _rmRegistry;
89
90
  private _themeManager;
90
- private _autocompletionRegistry?;
91
+ private _documentManager?;
92
+ private _chatCommandRegistry?;
93
+ private _attachmentOpenerRegistry?;
91
94
  }
92
95
  /**
93
96
  * The chat panel namespace.
@@ -102,7 +105,9 @@ export declare namespace ChatPanel {
102
105
  rmRegistry: IRenderMimeRegistry;
103
106
  themeManager: IThemeManager | null;
104
107
  defaultDirectory: string;
105
- autocompletionRegistry?: IAutocompletionRegistry;
108
+ documentManager?: IDocumentManager;
109
+ chatCommandRegistry?: IChatCommandRegistry;
110
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
106
111
  }
107
112
  }
108
113
  /**
package/lib/widget.js CHANGED
@@ -105,7 +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._autocompletionRegistry = options.autocompletionRegistry;
108
+ this._documentManager = options.documentManager;
109
+ this._chatCommandRegistry = options.chatCommandRegistry;
110
+ this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
109
111
  const addChat = new CommandToolbarButton({
110
112
  commands: this._commands,
111
113
  id: CommandIDs.createChat,
@@ -144,27 +146,28 @@ export class ChatPanel extends SidePanel {
144
146
  * @param model - the model of the chat widget
145
147
  * @param name - the name of the chat.
146
148
  */
147
- addChat(model, path) {
149
+ addChat(model) {
148
150
  // Collapse all chats
149
151
  const content = this.content;
150
152
  for (let i = 0; i < this.widgets.length; i++) {
151
153
  content.collapse(i);
152
154
  }
153
- // Set the name of the model.
154
- model.name = path;
155
155
  // Create a new widget.
156
156
  const widget = new ChatWidget({
157
157
  model: model,
158
158
  rmRegistry: this._rmRegistry,
159
159
  themeManager: this._themeManager,
160
- autocompletionRegistry: this._autocompletionRegistry
160
+ documentManager: this._documentManager,
161
+ chatCommandRegistry: this._chatCommandRegistry,
162
+ attachmentOpenerRegistry: this._attachmentOpenerRegistry
161
163
  });
162
164
  this.addWidget(new ChatSection({
163
165
  widget,
164
166
  commands: this._commands,
165
- path,
167
+ path: model.name,
166
168
  defaultDirectory: this._defaultDirectory
167
169
  }));
170
+ return widget;
168
171
  }
169
172
  /**
170
173
  * Open a chat if it exists in the side panel.
package/lib/ychat.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { IChatMessage, IUser } from '@jupyter/chat';
1
+ import { IAttachment, IChatMessage, IUser } from '@jupyter/chat';
2
2
  import { Delta, DocumentChange, IMapChange, YDocument } from '@jupyter/ydoc';
3
3
  import { JSONObject, PartialJSONValue } from '@lumino/coreutils';
4
4
  /**
5
5
  * The type for a YMessage.
6
6
  */
7
- export type IYmessage = IChatMessage<string>;
7
+ export type IYmessage = IChatMessage<string, string>;
8
8
  /**
9
9
  * The type for a YMessage.
10
10
  */
@@ -21,6 +21,10 @@ export interface IChatChanges extends DocumentChange {
21
21
  * Changes in users.
22
22
  */
23
23
  userChanges?: UserChange[];
24
+ /**
25
+ * Changes in attachments.
26
+ */
27
+ attachmentChanges?: AttachmentChange[];
24
28
  /**
25
29
  * Changes in metadata.
26
30
  */
@@ -34,6 +38,10 @@ export type MessageChange = Delta<IYmessage[]>;
34
38
  * The user change type.
35
39
  */
36
40
  export type UserChange = IMapChange<IUser>;
41
+ /**
42
+ * The attachment change type.
43
+ */
44
+ export type AttachmentChange = IMapChange<IAttachment>;
37
45
  /**
38
46
  * The metadata change type.
39
47
  */
@@ -59,6 +67,7 @@ export declare class YChat extends YDocument<IChatChanges> {
59
67
  get id(): string;
60
68
  get users(): JSONObject;
61
69
  get messages(): string[];
70
+ get attachments(): JSONObject;
62
71
  getSource(): JSONObject;
63
72
  setSource(value: JSONObject): void;
64
73
  getUser(username: string | undefined): IUser | undefined;
@@ -68,10 +77,14 @@ export declare class YChat extends YDocument<IChatChanges> {
68
77
  updateMessage(index: number, value: IYmessage): void;
69
78
  getMessageIndex(id: string): number;
70
79
  deleteMessage(index: number): void;
80
+ getAttachment(id: string): IAttachment | undefined;
81
+ setAttachment(attachment: IAttachment): string;
71
82
  private _usersObserver;
72
83
  private _messagesObserver;
84
+ private _attachmentsObserver;
73
85
  private _metadataObserver;
74
86
  private _users;
75
87
  private _messages;
88
+ private _attachments;
76
89
  private _metadata;
77
90
  }
package/lib/ychat.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { YDocument } from '@jupyter/ydoc';
6
- import { JSONExt } from '@lumino/coreutils';
6
+ import { JSONExt, UUID } from '@lumino/coreutils';
7
7
  /**
8
8
  * The jupyterlab chat shared document.
9
9
  */
@@ -18,27 +18,27 @@ export class YChat extends YDocument {
18
18
  */
19
19
  this.version = '1.0.0';
20
20
  this._usersObserver = (event) => {
21
- const userChange = new Array();
21
+ const userChanges = new Array();
22
22
  event.keysChanged.forEach(key => {
23
23
  const change = event.changes.keys.get(key);
24
24
  if (change) {
25
25
  switch (change.action) {
26
26
  case 'add':
27
- userChange.push({
27
+ userChanges.push({
28
28
  key,
29
29
  newValue: this._users.get(key),
30
30
  type: 'add'
31
31
  });
32
32
  break;
33
33
  case 'delete':
34
- userChange.push({
34
+ userChanges.push({
35
35
  key,
36
36
  oldValue: change.oldValue,
37
37
  type: 'remove'
38
38
  });
39
39
  break;
40
40
  case 'update':
41
- userChange.push({
41
+ userChanges.push({
42
42
  key: key,
43
43
  oldValue: change.oldValue,
44
44
  newValue: this._users.get(key),
@@ -48,7 +48,7 @@ export class YChat extends YDocument {
48
48
  }
49
49
  }
50
50
  });
51
- this._changed.emit({ userChange: userChange });
51
+ this._changed.emit({ userChanges });
52
52
  };
53
53
  this._messagesObserver = (event) => {
54
54
  const messageChanges = event.delta;
@@ -56,26 +56,59 @@ export class YChat extends YDocument {
56
56
  messageChanges: messageChanges
57
57
  });
58
58
  };
59
+ this._attachmentsObserver = (event) => {
60
+ const attachmentChanges = new Array();
61
+ event.keysChanged.forEach(key => {
62
+ const change = event.changes.keys.get(key);
63
+ if (change) {
64
+ switch (change.action) {
65
+ case 'add':
66
+ attachmentChanges.push({
67
+ key,
68
+ newValue: this._attachments.get(key),
69
+ type: 'add'
70
+ });
71
+ break;
72
+ case 'delete':
73
+ attachmentChanges.push({
74
+ key,
75
+ oldValue: change.oldValue,
76
+ type: 'remove'
77
+ });
78
+ break;
79
+ case 'update':
80
+ attachmentChanges.push({
81
+ key: key,
82
+ oldValue: change.oldValue,
83
+ newValue: this._attachments.get(key),
84
+ type: 'change'
85
+ });
86
+ break;
87
+ }
88
+ }
89
+ });
90
+ this._changed.emit({ attachmentChanges });
91
+ };
59
92
  this._metadataObserver = (event) => {
60
- const metadataChange = new Array();
93
+ const metadataChanges = new Array();
61
94
  event.changes.keys.forEach((change, key) => {
62
95
  switch (change.action) {
63
96
  case 'add':
64
- metadataChange.push({
97
+ metadataChanges.push({
65
98
  key,
66
99
  newValue: this._metadata.get(key),
67
100
  type: 'add'
68
101
  });
69
102
  break;
70
103
  case 'delete':
71
- metadataChange.push({
104
+ metadataChanges.push({
72
105
  key,
73
106
  oldValue: change.oldValue,
74
107
  type: 'remove'
75
108
  });
76
109
  break;
77
110
  case 'update':
78
- metadataChange.push({
111
+ metadataChanges.push({
79
112
  key: key,
80
113
  oldValue: change.oldValue,
81
114
  newValue: this._metadata.get(key),
@@ -84,14 +117,14 @@ export class YChat extends YDocument {
84
117
  break;
85
118
  }
86
119
  });
87
- this._changed.emit({
88
- metadataChanges: metadataChange
89
- });
120
+ this._changed.emit({ metadataChanges });
90
121
  };
91
122
  this._users = this.ydoc.getMap('users');
92
123
  this._users.observe(this._usersObserver);
93
124
  this._messages = this.ydoc.getArray('messages');
94
125
  this._messages.observe(this._messagesObserver);
126
+ this._attachments = this.ydoc.getMap('attachments');
127
+ this._attachments.observe(this._attachmentsObserver);
95
128
  this._metadata = this.ydoc.getMap('metadata');
96
129
  this._metadata.observe(this._metadataObserver);
97
130
  }
@@ -112,6 +145,9 @@ export class YChat extends YDocument {
112
145
  get messages() {
113
146
  return JSONExt.deepCopy(this._messages.toJSON());
114
147
  }
148
+ get attachments() {
149
+ return JSONExt.deepCopy(this._attachments.toJSON());
150
+ }
115
151
  getSource() {
116
152
  const users = this._users.toJSON();
117
153
  const messages = this._messages.toJSON();
@@ -167,4 +203,16 @@ export class YChat extends YDocument {
167
203
  this._messages.delete(index);
168
204
  });
169
205
  }
206
+ getAttachment(id) {
207
+ return this._attachments.get(id);
208
+ }
209
+ setAttachment(attachment) {
210
+ var _a;
211
+ // Search if the attachment already exist to update it, otherwise add it.
212
+ const id = ((_a = Array.from(this._attachments.entries()).find(([_, att]) => att.type === attachment.type && att.value === attachment.value)) === null || _a === void 0 ? void 0 : _a[0]) || UUID.uuid4();
213
+ this.transact(() => {
214
+ this._attachments.set(id, attachment);
215
+ });
216
+ return id;
217
+ }
170
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-chat",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The library to build a chat based on shared document",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -37,26 +37,18 @@
37
37
  "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
38
38
  "clean:lintcache": "rimraf .eslintcache .stylelintcache",
39
39
  "clean:all": "jlpm clean:lib && jlpm clean:lintcache",
40
- "eslint": "jlpm eslint:check --fix",
41
- "eslint:check": "eslint . --cache --ext .ts,.tsx",
42
40
  "install:extension": "jlpm build",
43
- "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
44
- "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
45
- "prettier": "jlpm prettier:base --write --list-different",
46
- "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
47
- "prettier:check": "jlpm prettier:base --check",
48
- "stylelint": "jlpm stylelint:check --fix",
49
- "stylelint:check": "stylelint --cache \"style/**/*.css\"",
50
41
  "test": "jest --coverage",
51
42
  "watch:src": "tsc -w --sourceMap"
52
43
  },
53
44
  "dependencies": {
54
- "@jupyter/chat": "^0.7.0",
45
+ "@jupyter/chat": "^0.8.0",
55
46
  "@jupyter/collaborative-drive": "^3.0.0",
56
47
  "@jupyter/ydoc": "^2.0.0 || ^3.0.0",
57
48
  "@jupyterlab/application": "^4.2.0",
58
49
  "@jupyterlab/apputils": "^4.3.0",
59
50
  "@jupyterlab/coreutils": "^6.2.0",
51
+ "@jupyterlab/docmanager": "^4.2.0",
60
52
  "@jupyterlab/docregistry": "^4.2.0",
61
53
  "@jupyterlab/launcher": "^4.2.0",
62
54
  "@jupyterlab/notebook": "^4.2.0",
@@ -79,24 +71,13 @@
79
71
  "@types/json-schema": "^7.0.11",
80
72
  "@types/react": "^18.2.0",
81
73
  "@types/react-addons-linked-state-mixin": "^0.14.22",
82
- "@typescript-eslint/eslint-plugin": "^6.1.0",
83
- "@typescript-eslint/parser": "^6.1.0",
84
74
  "css-loader": "^6.7.1",
85
- "eslint": "^8.36.0",
86
- "eslint-config-prettier": "^8.8.0",
87
- "eslint-plugin-prettier": "^5.0.0",
88
75
  "jest": "^29.2.0",
89
76
  "mkdirp": "^1.0.3",
90
77
  "npm-run-all": "^4.1.5",
91
- "prettier": "^3.0.0",
92
78
  "rimraf": "^5.0.1",
93
79
  "source-map-loader": "^1.0.2",
94
80
  "style-loader": "^3.3.1",
95
- "stylelint": "^15.10.1",
96
- "stylelint-config-recommended": "^13.0.0",
97
- "stylelint-config-standard": "^34.0.0",
98
- "stylelint-csstree-validator": "^3.0.0",
99
- "stylelint-prettier": "^4.0.0",
100
81
  "typescript": "~5.0.2",
101
82
  "yjs": "^13.5.0"
102
83
  },
@@ -107,98 +88,5 @@
107
88
  "styleModule": "style/index.js",
108
89
  "publishConfig": {
109
90
  "access": "public"
110
- },
111
- "eslintIgnore": [
112
- "node_modules",
113
- "dist",
114
- "coverage",
115
- "**/*.d.ts",
116
- "tests",
117
- "**/__tests__"
118
- ],
119
- "eslintConfig": {
120
- "extends": [
121
- "eslint:recommended",
122
- "plugin:@typescript-eslint/eslint-recommended",
123
- "plugin:@typescript-eslint/recommended",
124
- "plugin:prettier/recommended"
125
- ],
126
- "parser": "@typescript-eslint/parser",
127
- "parserOptions": {
128
- "project": "tsconfig.json",
129
- "sourceType": "module"
130
- },
131
- "plugins": [
132
- "@typescript-eslint"
133
- ],
134
- "rules": {
135
- "@typescript-eslint/naming-convention": [
136
- "error",
137
- {
138
- "selector": "interface",
139
- "format": [
140
- "PascalCase"
141
- ],
142
- "custom": {
143
- "regex": "^I[A-Z]",
144
- "match": true
145
- }
146
- }
147
- ],
148
- "@typescript-eslint/no-unused-vars": [
149
- "warn",
150
- {
151
- "args": "none"
152
- }
153
- ],
154
- "@typescript-eslint/no-explicit-any": "off",
155
- "@typescript-eslint/no-namespace": "off",
156
- "@typescript-eslint/no-use-before-define": "off",
157
- "@typescript-eslint/quotes": [
158
- "error",
159
- "single",
160
- {
161
- "avoidEscape": true,
162
- "allowTemplateLiterals": false
163
- }
164
- ],
165
- "curly": [
166
- "error",
167
- "all"
168
- ],
169
- "eqeqeq": "error",
170
- "prefer-arrow-callback": "error"
171
- }
172
- },
173
- "prettier": {
174
- "singleQuote": true,
175
- "trailingComma": "none",
176
- "arrowParens": "avoid",
177
- "endOfLine": "auto",
178
- "overrides": [
179
- {
180
- "files": "package.json",
181
- "options": {
182
- "tabWidth": 2
183
- }
184
- }
185
- ]
186
- },
187
- "stylelint": {
188
- "extends": [
189
- "stylelint-config-recommended",
190
- "stylelint-config-standard",
191
- "stylelint-prettier/recommended"
192
- ],
193
- "plugins": [
194
- "stylelint-csstree-validator"
195
- ],
196
- "rules": {
197
- "csstree/validator": true,
198
- "property-no-vendor-prefix": null,
199
- "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
200
- "selector-no-vendor-prefix": null,
201
- "value-no-vendor-prefix": null
202
- }
203
91
  }
204
92
  }
package/src/factory.ts CHANGED
@@ -6,10 +6,12 @@
6
6
  import {
7
7
  ChatWidget,
8
8
  IActiveCellManager,
9
- IAutocompletionRegistry,
9
+ IAttachmentOpenerRegistry,
10
+ IChatCommandRegistry,
10
11
  ISelectionWatcher
11
12
  } from '@jupyter/chat';
12
13
  import { IThemeManager } from '@jupyterlab/apputils';
14
+ import { IDocumentManager } from '@jupyterlab/docmanager';
13
15
  import { ABCWidgetFactory, DocumentRegistry } from '@jupyterlab/docregistry';
14
16
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
15
17
  import { Contents, User } from '@jupyterlab/services';
@@ -74,7 +76,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
74
76
  super(options);
75
77
  this._themeManager = options.themeManager;
76
78
  this._rmRegistry = options.rmRegistry;
77
- this._autocompletionRegistry = options.autocompletionRegistry;
79
+ this._documentManager = options.documentManager;
80
+ this._chatCommandRegistry = options.chatCommandRegistry;
81
+ this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
78
82
  }
79
83
 
80
84
  /**
@@ -86,7 +90,9 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
86
90
  protected createNewWidget(context: ChatWidgetFactory.IContext): LabChatPanel {
87
91
  context.rmRegistry = this._rmRegistry;
88
92
  context.themeManager = this._themeManager;
89
- context.autocompletionRegistry = this._autocompletionRegistry;
93
+ context.documentManager = this._documentManager;
94
+ context.chatCommandRegistry = this._chatCommandRegistry;
95
+ context.attachmentOpenerRegistry = this._attachmentOpenerRegistry;
90
96
  return new LabChatPanel({
91
97
  context,
92
98
  content: new ChatWidget(context)
@@ -95,21 +101,27 @@ export class ChatWidgetFactory extends ABCWidgetFactory<
95
101
 
96
102
  private _themeManager: IThemeManager | null;
97
103
  private _rmRegistry: IRenderMimeRegistry;
98
- private _autocompletionRegistry?: IAutocompletionRegistry;
104
+ private _documentManager?: IDocumentManager;
105
+ private _chatCommandRegistry?: IChatCommandRegistry;
106
+ private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
99
107
  }
100
108
 
101
109
  export namespace ChatWidgetFactory {
102
110
  export interface IContext extends DocumentRegistry.IContext<LabChatModel> {
103
111
  themeManager: IThemeManager | null;
104
112
  rmRegistry: IRenderMimeRegistry;
105
- autocompletionRegistry?: IAutocompletionRegistry;
113
+ documentManager?: IDocumentManager;
114
+ chatCommandRegistry?: IChatCommandRegistry;
115
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
106
116
  }
107
117
 
108
118
  export interface IOptions<T extends LabChatPanel>
109
119
  extends DocumentRegistry.IWidgetFactoryOptions<T> {
110
120
  themeManager: IThemeManager | null;
111
121
  rmRegistry: IRenderMimeRegistry;
112
- autocompletionRegistry?: IAutocompletionRegistry;
122
+ documentManager?: IDocumentManager;
123
+ chatCommandRegistry?: IChatCommandRegistry;
124
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
113
125
  }
114
126
  }
115
127
 
package/src/model.ts CHANGED
@@ -3,7 +3,14 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { ChatModel, IChatMessage, INewMessage, IUser } from '@jupyter/chat';
6
+ import {
7
+ ChatModel,
8
+ IAttachment,
9
+ IChatMessage,
10
+ IInputModel,
11
+ INewMessage,
12
+ IUser
13
+ } from '@jupyter/chat';
7
14
  import { IChangedArgs } from '@jupyterlab/coreutils';
8
15
  import { DocumentRegistry } from '@jupyterlab/docregistry';
9
16
  import { User } from '@jupyterlab/services';
@@ -53,6 +60,8 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
53
60
  });
54
61
 
55
62
  this.sharedModel.awareness.on('change', this.onAwarenessChange);
63
+
64
+ this.input.valueChanged.connect(this.onInputChanged);
56
65
  }
57
66
 
58
67
  readonly collaborative = true;
@@ -137,6 +146,7 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
137
146
  if (this._timeoutWriting !== null) {
138
147
  window.clearTimeout(this._timeoutWriting);
139
148
  }
149
+
140
150
  const msg: IYmessage = {
141
151
  type: 'msg',
142
152
  id: UUID.uuid4(),
@@ -150,6 +160,16 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
150
160
  if (!(this.sharedModel.getUser(this._user.username) === this._user)) {
151
161
  this.sharedModel.setUser(this._user);
152
162
  }
163
+
164
+ // 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
+ );
169
+ msg.attachments = attachmentIds;
170
+ this.input.clearAttachments();
171
+ }
172
+
153
173
  this.sharedModel.addMessage(msg);
154
174
  }
155
175
 
@@ -174,6 +194,12 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
174
194
  edited: true
175
195
  };
176
196
  }
197
+ const attachmentIds = updatedMessage.attachments?.map(attachment =>
198
+ this.sharedModel.setAttachment(attachment)
199
+ );
200
+ if (attachmentIds) {
201
+ message.attachments = attachmentIds;
202
+ }
177
203
  this.sharedModel.updateMessage(index, message as IYmessage);
178
204
  }
179
205
 
@@ -192,8 +218,8 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
192
218
  /**
193
219
  * Function called by the input on key pressed.
194
220
  */
195
- inputChanged(input?: string): void {
196
- if (!input || !this.config.sendTypingNotification) {
221
+ onInputChanged = (_: IInputModel, value: string): void => {
222
+ if (!value || !this.config.sendTypingNotification) {
197
223
  return;
198
224
  }
199
225
  const awareness = this.sharedModel.awareness;
@@ -204,7 +230,7 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
204
230
  this._timeoutWriting = window.setTimeout(() => {
205
231
  this._resetWritingStatus();
206
232
  }, WRITING_DELAY);
207
- }
233
+ };
208
234
 
209
235
  /**
210
236
  * Triggered when an awareness state changes.
@@ -242,13 +268,34 @@ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
242
268
  index += delta.retain;
243
269
  } else if (delta.insert) {
244
270
  const messages = delta.insert.map(ymessage => {
271
+ const {
272
+ sender,
273
+ attachments: attachmentIds,
274
+ ...baseMessage
275
+ } = ymessage;
276
+
277
+ // Build the base message with sender.
245
278
  const msg: IChatMessage = {
246
- ...ymessage,
247
- sender: this.sharedModel.getUser(ymessage.sender) || {
279
+ ...baseMessage,
280
+ sender: this.sharedModel.getUser(sender) || {
248
281
  username: 'User undefined'
249
282
  }
250
283
  };
251
284
 
285
+ // Add attachments.
286
+ if (attachmentIds) {
287
+ const attachments: IAttachment[] = [];
288
+ attachmentIds.forEach(attachmentId => {
289
+ const attachment = this.sharedModel.getAttachment(attachmentId);
290
+ if (attachment) {
291
+ attachments.push(attachment);
292
+ }
293
+ });
294
+ if (attachments.length) {
295
+ msg.attachments = attachments;
296
+ }
297
+ }
298
+
252
299
  return msg;
253
300
  });
254
301
  this.messagesInserted(index, messages);
package/src/token.ts CHANGED
@@ -7,9 +7,10 @@ import {
7
7
  IConfig,
8
8
  chatIcon,
9
9
  IActiveCellManager,
10
- ISelectionWatcher
10
+ ISelectionWatcher,
11
+ ChatWidget
11
12
  } from '@jupyter/chat';
12
- import { IWidgetTracker } from '@jupyterlab/apputils';
13
+ import { WidgetTracker } from '@jupyterlab/apputils';
13
14
  import { DocumentRegistry } from '@jupyterlab/docregistry';
14
15
  import { Token } from '@lumino/coreutils';
15
16
  import { ISignal } from '@lumino/signaling';
@@ -56,7 +57,7 @@ export interface IChatFactory {
56
57
  /**
57
58
  * The chat panel tracker.
58
59
  */
59
- tracker: IWidgetTracker<LabChatPanel>;
60
+ tracker: WidgetTracker<LabChatPanel | ChatWidget>;
60
61
  }
61
62
 
62
63
  /**
@@ -77,7 +78,7 @@ export interface IWidgetConfig {
77
78
  /**
78
79
  * A signal emitting when the configuration for the chats has changed.
79
80
  */
80
- export interface IConfigChanged
81
+ export interface IConfigChanged /* eslint-disable-line @typescript-eslint/no-empty-object-type */
81
82
  extends ISignal<IWidgetConfig, Partial<ILabChatConfig>> {}
82
83
 
83
84
  /**
package/src/widget.tsx CHANGED
@@ -5,7 +5,8 @@
5
5
 
6
6
  import {
7
7
  ChatWidget,
8
- IAutocompletionRegistry,
8
+ IAttachmentOpenerRegistry,
9
+ IChatCommandRegistry,
9
10
  IChatModel,
10
11
  readIcon
11
12
  } from '@jupyter/chat';
@@ -33,6 +34,7 @@ import React, { useState } from 'react';
33
34
 
34
35
  import { LabChatModel } from './model';
35
36
  import { CommandIDs, chatFileType } from './token';
37
+ import { IDocumentManager } from '@jupyterlab/docmanager';
36
38
 
37
39
  const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel';
38
40
  const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread';
@@ -102,7 +104,9 @@ export class ChatPanel extends SidePanel {
102
104
  this._rmRegistry = options.rmRegistry;
103
105
  this._themeManager = options.themeManager;
104
106
  this._defaultDirectory = options.defaultDirectory;
105
- this._autocompletionRegistry = options.autocompletionRegistry;
107
+ this._documentManager = options.documentManager;
108
+ this._chatCommandRegistry = options.chatCommandRegistry;
109
+ this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
106
110
 
107
111
  const addChat = new CommandToolbarButton({
108
112
  commands: this._commands,
@@ -152,32 +156,33 @@ export class ChatPanel extends SidePanel {
152
156
  * @param model - the model of the chat widget
153
157
  * @param name - the name of the chat.
154
158
  */
155
- addChat(model: IChatModel, path: string): void {
159
+ addChat(model: IChatModel): ChatWidget {
156
160
  // Collapse all chats
157
161
  const content = this.content as AccordionPanel;
158
162
  for (let i = 0; i < this.widgets.length; i++) {
159
163
  content.collapse(i);
160
164
  }
161
165
 
162
- // Set the name of the model.
163
- model.name = path;
164
-
165
166
  // Create a new widget.
166
167
  const widget = new ChatWidget({
167
168
  model: model,
168
169
  rmRegistry: this._rmRegistry,
169
170
  themeManager: this._themeManager,
170
- autocompletionRegistry: this._autocompletionRegistry
171
+ documentManager: this._documentManager,
172
+ chatCommandRegistry: this._chatCommandRegistry,
173
+ attachmentOpenerRegistry: this._attachmentOpenerRegistry
171
174
  });
172
175
 
173
176
  this.addWidget(
174
177
  new ChatSection({
175
178
  widget,
176
179
  commands: this._commands,
177
- path,
180
+ path: model.name,
178
181
  defaultDirectory: this._defaultDirectory
179
182
  })
180
183
  );
184
+
185
+ return widget;
181
186
  }
182
187
 
183
188
  /**
@@ -284,7 +289,9 @@ export class ChatPanel extends SidePanel {
284
289
  private _openChat: ReactWidget;
285
290
  private _rmRegistry: IRenderMimeRegistry;
286
291
  private _themeManager: IThemeManager | null;
287
- private _autocompletionRegistry?: IAutocompletionRegistry;
292
+ private _documentManager?: IDocumentManager;
293
+ private _chatCommandRegistry?: IChatCommandRegistry;
294
+ private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
288
295
  }
289
296
 
290
297
  /**
@@ -300,7 +307,9 @@ export namespace ChatPanel {
300
307
  rmRegistry: IRenderMimeRegistry;
301
308
  themeManager: IThemeManager | null;
302
309
  defaultDirectory: string;
303
- autocompletionRegistry?: IAutocompletionRegistry;
310
+ documentManager?: IDocumentManager;
311
+ chatCommandRegistry?: IChatCommandRegistry;
312
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
304
313
  }
305
314
  }
306
315
 
package/src/ychat.ts CHANGED
@@ -3,15 +3,15 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { IChatMessage, IUser } from '@jupyter/chat';
6
+ import { IAttachment, IChatMessage, IUser } from '@jupyter/chat';
7
7
  import { Delta, DocumentChange, IMapChange, YDocument } from '@jupyter/ydoc';
8
- import { JSONExt, JSONObject, PartialJSONValue } from '@lumino/coreutils';
8
+ import { JSONExt, JSONObject, PartialJSONValue, UUID } from '@lumino/coreutils';
9
9
  import * as Y from 'yjs';
10
10
 
11
11
  /**
12
12
  * The type for a YMessage.
13
13
  */
14
- export type IYmessage = IChatMessage<string>;
14
+ export type IYmessage = IChatMessage<string, string>;
15
15
 
16
16
  /**
17
17
  * The type for a YMessage.
@@ -30,6 +30,10 @@ export interface IChatChanges extends DocumentChange {
30
30
  * Changes in users.
31
31
  */
32
32
  userChanges?: UserChange[];
33
+ /**
34
+ * Changes in attachments.
35
+ */
36
+ attachmentChanges?: AttachmentChange[];
33
37
  /**
34
38
  * Changes in metadata.
35
39
  */
@@ -46,6 +50,11 @@ export type MessageChange = Delta<IYmessage[]>;
46
50
  */
47
51
  export type UserChange = IMapChange<IUser>;
48
52
 
53
+ /**
54
+ * The attachment change type.
55
+ */
56
+ export type AttachmentChange = IMapChange<IAttachment>;
57
+
49
58
  /**
50
59
  * The metadata change type.
51
60
  */
@@ -66,6 +75,9 @@ export class YChat extends YDocument<IChatChanges> {
66
75
  this._messages = this.ydoc.getArray<IYmessage>('messages');
67
76
  this._messages.observe(this._messagesObserver);
68
77
 
78
+ this._attachments = this.ydoc.getMap<IAttachment>('attachments');
79
+ this._attachments.observe(this._attachmentsObserver);
80
+
69
81
  this._metadata = this.ydoc.getMap<IMetadata>('metadata');
70
82
  this._metadata.observe(this._metadataObserver);
71
83
  }
@@ -96,6 +108,10 @@ export class YChat extends YDocument<IChatChanges> {
96
108
  return JSONExt.deepCopy(this._messages.toJSON());
97
109
  }
98
110
 
111
+ get attachments(): JSONObject {
112
+ return JSONExt.deepCopy(this._attachments.toJSON());
113
+ }
114
+
99
115
  getSource(): JSONObject {
100
116
  const users = this._users.toJSON();
101
117
  const messages = this._messages.toJSON();
@@ -168,28 +184,46 @@ export class YChat extends YDocument<IChatChanges> {
168
184
  });
169
185
  }
170
186
 
187
+ getAttachment(id: string): IAttachment | undefined {
188
+ return this._attachments.get(id);
189
+ }
190
+
191
+ setAttachment(attachment: IAttachment): string {
192
+ // Search if the attachment already exist to update it, otherwise add it.
193
+ const id =
194
+ Array.from(this._attachments.entries()).find(
195
+ ([_, att]) =>
196
+ att.type === attachment.type && att.value === attachment.value
197
+ )?.[0] || UUID.uuid4();
198
+
199
+ this.transact(() => {
200
+ this._attachments.set(id, attachment);
201
+ });
202
+ return id;
203
+ }
204
+
171
205
  private _usersObserver = (event: Y.YMapEvent<IUser>): void => {
172
- const userChange = new Array<UserChange>();
206
+ const userChanges = new Array<UserChange>();
173
207
  event.keysChanged.forEach(key => {
174
208
  const change = event.changes.keys.get(key);
175
209
  if (change) {
176
210
  switch (change.action) {
177
211
  case 'add':
178
- userChange.push({
212
+ userChanges.push({
179
213
  key,
180
214
  newValue: this._users.get(key),
181
215
  type: 'add'
182
216
  });
183
217
  break;
184
218
  case 'delete':
185
- userChange.push({
219
+ userChanges.push({
186
220
  key,
187
221
  oldValue: change.oldValue,
188
222
  type: 'remove'
189
223
  });
190
224
  break;
191
225
  case 'update':
192
- userChange.push({
226
+ userChanges.push({
193
227
  key: key,
194
228
  oldValue: change.oldValue,
195
229
  newValue: this._users.get(key),
@@ -200,7 +234,7 @@ export class YChat extends YDocument<IChatChanges> {
200
234
  }
201
235
  });
202
236
 
203
- this._changed.emit({ userChange: userChange } as Partial<IChatChanges>);
237
+ this._changed.emit({ userChanges } as Partial<IChatChanges>);
204
238
  };
205
239
 
206
240
  private _messagesObserver = (event: Y.YArrayEvent<IYmessage>): void => {
@@ -210,26 +244,61 @@ export class YChat extends YDocument<IChatChanges> {
210
244
  } as Partial<IChatChanges>);
211
245
  };
212
246
 
247
+ private _attachmentsObserver = (event: Y.YMapEvent<IAttachment>): void => {
248
+ const attachmentChanges = new Array<AttachmentChange>();
249
+ event.keysChanged.forEach(key => {
250
+ const change = event.changes.keys.get(key);
251
+ if (change) {
252
+ switch (change.action) {
253
+ case 'add':
254
+ attachmentChanges.push({
255
+ key,
256
+ newValue: this._attachments.get(key),
257
+ type: 'add'
258
+ });
259
+ break;
260
+ case 'delete':
261
+ attachmentChanges.push({
262
+ key,
263
+ oldValue: change.oldValue,
264
+ type: 'remove'
265
+ });
266
+ break;
267
+ case 'update':
268
+ attachmentChanges.push({
269
+ key: key,
270
+ oldValue: change.oldValue,
271
+ newValue: this._attachments.get(key),
272
+ type: 'change'
273
+ });
274
+ break;
275
+ }
276
+ }
277
+ });
278
+
279
+ this._changed.emit({ attachmentChanges } as Partial<IChatChanges>);
280
+ };
281
+
213
282
  private _metadataObserver = (event: Y.YMapEvent<IMetadata>): void => {
214
- const metadataChange = new Array<MetadataChange>();
283
+ const metadataChanges = new Array<MetadataChange>();
215
284
  event.changes.keys.forEach((change, key) => {
216
285
  switch (change.action) {
217
286
  case 'add':
218
- metadataChange.push({
287
+ metadataChanges.push({
219
288
  key,
220
289
  newValue: this._metadata.get(key),
221
290
  type: 'add'
222
291
  });
223
292
  break;
224
293
  case 'delete':
225
- metadataChange.push({
294
+ metadataChanges.push({
226
295
  key,
227
296
  oldValue: change.oldValue,
228
297
  type: 'remove'
229
298
  });
230
299
  break;
231
300
  case 'update':
232
- metadataChange.push({
301
+ metadataChanges.push({
233
302
  key: key,
234
303
  oldValue: change.oldValue,
235
304
  newValue: this._metadata.get(key),
@@ -239,12 +308,11 @@ export class YChat extends YDocument<IChatChanges> {
239
308
  }
240
309
  });
241
310
 
242
- this._changed.emit({
243
- metadataChanges: metadataChange
244
- } as Partial<IChatChanges>);
311
+ this._changed.emit({ metadataChanges } as Partial<IChatChanges>);
245
312
  };
246
313
 
247
314
  private _users: Y.Map<IUser>;
248
315
  private _messages: Y.Array<IYmessage>;
316
+ private _attachments: Y.Map<IAttachment>;
249
317
  private _metadata: Y.Map<IMetadata>;
250
318
  }
package/style/base.css CHANGED
@@ -20,3 +20,13 @@
20
20
  .jp-lab-chat-title-unread .lm-TabBar-tabLabel::before {
21
21
  content: '* ';
22
22
  }
23
+
24
+ .jp-chat-command-name {
25
+ font-weight: normal;
26
+ margin: 5px;
27
+ }
28
+
29
+ .jp-chat-command-description {
30
+ color: gray;
31
+ margin: 5px;
32
+ }