jupyterlab-chat 0.24.0 → 0.24.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,151 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { UUID } from '@lumino/coreutils';
6
+ import { Signal } from '@lumino/signaling';
7
+ import { LabChatModel } from '../model';
8
+ import { YChat } from '../ychat';
9
+ const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
10
+ const TEST_USER = {
11
+ username: 'test-user',
12
+ name: 'Test User',
13
+ display_name: 'Test',
14
+ initials: 'TU',
15
+ color: '#aabbcc'
16
+ };
17
+ function makeWidgetConfig() {
18
+ const owner = {};
19
+ owner.config = {};
20
+ owner.configChanged = new Signal(owner);
21
+ return owner;
22
+ }
23
+ describe('LabChatModel', () => {
24
+ let sharedModel;
25
+ let model;
26
+ beforeEach(() => {
27
+ sharedModel = YChat.create();
28
+ model = new LabChatModel({
29
+ widgetConfig: makeWidgetConfig(),
30
+ user: TEST_USER,
31
+ sharedModel
32
+ });
33
+ // Resolve the ready promise so messagesInserted can proceed.
34
+ model.id = UUID.uuid4();
35
+ });
36
+ afterEach(() => {
37
+ Signal.clearData(model);
38
+ sharedModel.dispose();
39
+ });
40
+ describe('message attribute updates from shared model', () => {
41
+ it('should reflect a body change', async () => {
42
+ const id = UUID.uuid4();
43
+ sharedModel.addMessage({
44
+ type: 'msg',
45
+ id,
46
+ body: 'original',
47
+ time: 1000,
48
+ sender: TEST_USER.username
49
+ });
50
+ await flushPromises();
51
+ sharedModel.updateMessage(0, {
52
+ type: 'msg',
53
+ id,
54
+ body: 'updated',
55
+ time: 1000,
56
+ sender: TEST_USER.username
57
+ });
58
+ await flushPromises();
59
+ expect(model.messages[0].body).toBe('updated');
60
+ });
61
+ it('should reflect a mime_model change', async () => {
62
+ const id = UUID.uuid4();
63
+ sharedModel.addMessage({
64
+ type: 'msg',
65
+ id,
66
+ body: '',
67
+ time: 1000,
68
+ sender: TEST_USER.username
69
+ });
70
+ await flushPromises();
71
+ const mimeModel = { data: { 'text/plain': 'streamed output' } };
72
+ sharedModel.updateMessage(0, {
73
+ type: 'msg',
74
+ id,
75
+ body: '',
76
+ time: 1000,
77
+ sender: TEST_USER.username,
78
+ mime_model: mimeModel
79
+ });
80
+ await flushPromises();
81
+ expect(model.messages[0].mime_model).toEqual(mimeModel);
82
+ });
83
+ it('should not set edited when only mime_model changes', async () => {
84
+ const id = UUID.uuid4();
85
+ sharedModel.addMessage({
86
+ type: 'msg',
87
+ id,
88
+ body: '',
89
+ time: 1000,
90
+ sender: TEST_USER.username
91
+ });
92
+ await flushPromises();
93
+ sharedModel.updateMessage(0, {
94
+ type: 'msg',
95
+ id,
96
+ body: '',
97
+ time: 1000,
98
+ sender: TEST_USER.username,
99
+ mime_model: { data: { 'text/plain': 'streamed output' } }
100
+ });
101
+ await flushPromises();
102
+ expect(model.messages[0].edited).toBeUndefined();
103
+ });
104
+ });
105
+ describe('rerender tracking', () => {
106
+ it('should replace renderedDelegate when mime_model changes', async () => {
107
+ const id = UUID.uuid4();
108
+ sharedModel.addMessage({
109
+ type: 'msg',
110
+ id,
111
+ body: '',
112
+ time: 1000,
113
+ sender: TEST_USER.username
114
+ });
115
+ await flushPromises();
116
+ const delegateBefore = model.messages[0].renderedDelegate;
117
+ sharedModel.updateMessage(0, {
118
+ type: 'msg',
119
+ id,
120
+ body: '',
121
+ time: 1000,
122
+ sender: TEST_USER.username,
123
+ mime_model: { data: { 'text/plain': 'streamed output' } }
124
+ });
125
+ await flushPromises();
126
+ expect(model.messages[0].renderedDelegate).not.toBe(delegateBefore);
127
+ });
128
+ it('should not replace renderedDelegate when only edited changes', async () => {
129
+ const id = UUID.uuid4();
130
+ sharedModel.addMessage({
131
+ type: 'msg',
132
+ id,
133
+ body: 'hello',
134
+ time: 1000,
135
+ sender: TEST_USER.username
136
+ });
137
+ await flushPromises();
138
+ const delegateBefore = model.messages[0].renderedDelegate;
139
+ sharedModel.updateMessage(0, {
140
+ type: 'msg',
141
+ id,
142
+ body: 'hello',
143
+ time: 1000,
144
+ sender: TEST_USER.username,
145
+ edited: true
146
+ });
147
+ await flushPromises();
148
+ expect(model.messages[0].renderedDelegate).toBe(delegateBefore);
149
+ });
150
+ });
151
+ });
package/lib/model.js CHANGED
@@ -196,7 +196,8 @@ export class LabChatModel extends AbstractChatModel {
196
196
  'raw_time',
197
197
  'deleted',
198
198
  'edited',
199
- 'metadata'
199
+ 'metadata',
200
+ 'mime_model'
200
201
  ].includes(key)) {
201
202
  const update = {};
202
203
  update[key] = value;
@@ -353,16 +354,20 @@ export class LabChatModel extends AbstractChatModel {
353
354
  });
354
355
  }
355
356
  sendMessage(message) {
356
- var _a, _b, _c, _d;
357
- if (!message.body && !message.mime_model && !((_a = message.attachments) === null || _a === void 0 ? void 0 : _a.length)) {
357
+ var _a, _b, _c, _d, _e;
358
+ // Allow empty message for bot only, as it may be streamed later.
359
+ if (!message.body &&
360
+ !message.mime_model &&
361
+ !((_a = message.attachments) === null || _a === void 0 ? void 0 : _a.length) &&
362
+ !((_b = message.sender) === null || _b === void 0 ? void 0 : _b.bot)) {
358
363
  return null;
359
364
  }
360
365
  this._resetWritingStatus();
361
366
  if (this._timeoutWriting !== null) {
362
367
  window.clearTimeout(this._timeoutWriting);
363
368
  }
364
- const body = (_b = message.body) !== null && _b !== void 0 ? _b : '';
365
- const user = (_c = message.sender) !== null && _c !== void 0 ? _c : this._user;
369
+ const body = (_c = message.body) !== null && _c !== void 0 ? _c : '';
370
+ const user = (_d = message.sender) !== null && _d !== void 0 ? _d : this._user;
366
371
  const msg = {
367
372
  type: 'msg',
368
373
  id: UUID.uuid4(),
@@ -380,7 +385,7 @@ export class LabChatModel extends AbstractChatModel {
380
385
  this.sharedModel.setUser(user);
381
386
  }
382
387
  // Add the attachments to the message.
383
- const attachmentIds = (_d = message.attachments) === null || _d === void 0 ? void 0 : _d.map(attachment => this.sharedModel.setAttachment(attachment));
388
+ const attachmentIds = (_e = message.attachments) === null || _e === void 0 ? void 0 : _e.map(attachment => this.sharedModel.setAttachment(attachment));
384
389
  if (attachmentIds === null || attachmentIds === void 0 ? void 0 : attachmentIds.length) {
385
390
  msg.attachments = attachmentIds;
386
391
  }
@@ -421,6 +426,10 @@ export class LabChatModel extends AbstractChatModel {
421
426
  edited: true
422
427
  };
423
428
  }
429
+ // Update the mime model.
430
+ if (updatedMessage.mime_model) {
431
+ message.mime_model = updatedMessage.mime_model;
432
+ }
424
433
  // Update the attachments.
425
434
  const attachmentIds = (_a = updatedMessage.attachments) === null || _a === void 0 ? void 0 : _a.map(attachment => this.sharedModel.setAttachment(attachment));
426
435
  if (attachmentIds === null || attachmentIds === void 0 ? void 0 : attachmentIds.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-chat",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
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.24.0",
45
+ "@jupyter/chat": "^0.24.1",
46
46
  "@jupyter/collaborative-drive": "^4.4.0 || ^5.0.0",
47
47
  "@jupyter/ydoc": "^3.0.0 || ^4.0.0",
48
48
  "@jupyterlab/application": "^4.5.0",
@@ -0,0 +1,177 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { UUID } from '@lumino/coreutils';
7
+ import { Signal } from '@lumino/signaling';
8
+
9
+ import { LabChatModel } from '../model';
10
+ import { IWidgetConfig } from '../token';
11
+ import { YChat } from '../ychat';
12
+
13
+ const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
14
+
15
+ const TEST_USER = {
16
+ username: 'test-user',
17
+ name: 'Test User',
18
+ display_name: 'Test',
19
+ initials: 'TU',
20
+ color: '#aabbcc'
21
+ };
22
+
23
+ function makeWidgetConfig(): IWidgetConfig {
24
+ const owner = {} as IWidgetConfig;
25
+ owner.config = {};
26
+ (owner as any).configChanged = new Signal<IWidgetConfig, any>(owner);
27
+ return owner;
28
+ }
29
+
30
+ describe('LabChatModel', () => {
31
+ let sharedModel: YChat;
32
+ let model: LabChatModel;
33
+
34
+ beforeEach(() => {
35
+ sharedModel = YChat.create();
36
+ model = new LabChatModel({
37
+ widgetConfig: makeWidgetConfig(),
38
+ user: TEST_USER,
39
+ sharedModel
40
+ });
41
+ // Resolve the ready promise so messagesInserted can proceed.
42
+ model.id = UUID.uuid4();
43
+ });
44
+
45
+ afterEach(() => {
46
+ Signal.clearData(model);
47
+ sharedModel.dispose();
48
+ });
49
+
50
+ describe('message attribute updates from shared model', () => {
51
+ it('should reflect a body change', async () => {
52
+ const id = UUID.uuid4();
53
+ sharedModel.addMessage({
54
+ type: 'msg',
55
+ id,
56
+ body: 'original',
57
+ time: 1000,
58
+ sender: TEST_USER.username
59
+ });
60
+ await flushPromises();
61
+
62
+ sharedModel.updateMessage(0, {
63
+ type: 'msg',
64
+ id,
65
+ body: 'updated',
66
+ time: 1000,
67
+ sender: TEST_USER.username
68
+ });
69
+ await flushPromises();
70
+
71
+ expect(model.messages[0].body).toBe('updated');
72
+ });
73
+
74
+ it('should reflect a mime_model change', async () => {
75
+ const id = UUID.uuid4();
76
+ sharedModel.addMessage({
77
+ type: 'msg',
78
+ id,
79
+ body: '',
80
+ time: 1000,
81
+ sender: TEST_USER.username
82
+ });
83
+ await flushPromises();
84
+
85
+ const mimeModel = { data: { 'text/plain': 'streamed output' } };
86
+ sharedModel.updateMessage(0, {
87
+ type: 'msg',
88
+ id,
89
+ body: '',
90
+ time: 1000,
91
+ sender: TEST_USER.username,
92
+ mime_model: mimeModel
93
+ });
94
+ await flushPromises();
95
+
96
+ expect(model.messages[0].mime_model).toEqual(mimeModel);
97
+ });
98
+
99
+ it('should not set edited when only mime_model changes', async () => {
100
+ const id = UUID.uuid4();
101
+ sharedModel.addMessage({
102
+ type: 'msg',
103
+ id,
104
+ body: '',
105
+ time: 1000,
106
+ sender: TEST_USER.username
107
+ });
108
+ await flushPromises();
109
+
110
+ sharedModel.updateMessage(0, {
111
+ type: 'msg',
112
+ id,
113
+ body: '',
114
+ time: 1000,
115
+ sender: TEST_USER.username,
116
+ mime_model: { data: { 'text/plain': 'streamed output' } }
117
+ });
118
+ await flushPromises();
119
+
120
+ expect(model.messages[0].edited).toBeUndefined();
121
+ });
122
+ });
123
+
124
+ describe('rerender tracking', () => {
125
+ it('should replace renderedDelegate when mime_model changes', async () => {
126
+ const id = UUID.uuid4();
127
+ sharedModel.addMessage({
128
+ type: 'msg',
129
+ id,
130
+ body: '',
131
+ time: 1000,
132
+ sender: TEST_USER.username
133
+ });
134
+ await flushPromises();
135
+
136
+ const delegateBefore = model.messages[0].renderedDelegate;
137
+
138
+ sharedModel.updateMessage(0, {
139
+ type: 'msg',
140
+ id,
141
+ body: '',
142
+ time: 1000,
143
+ sender: TEST_USER.username,
144
+ mime_model: { data: { 'text/plain': 'streamed output' } }
145
+ });
146
+ await flushPromises();
147
+
148
+ expect(model.messages[0].renderedDelegate).not.toBe(delegateBefore);
149
+ });
150
+
151
+ it('should not replace renderedDelegate when only edited changes', async () => {
152
+ const id = UUID.uuid4();
153
+ sharedModel.addMessage({
154
+ type: 'msg',
155
+ id,
156
+ body: 'hello',
157
+ time: 1000,
158
+ sender: TEST_USER.username
159
+ });
160
+ await flushPromises();
161
+
162
+ const delegateBefore = model.messages[0].renderedDelegate;
163
+
164
+ sharedModel.updateMessage(0, {
165
+ type: 'msg',
166
+ id,
167
+ body: 'hello',
168
+ time: 1000,
169
+ sender: TEST_USER.username,
170
+ edited: true
171
+ });
172
+ await flushPromises();
173
+
174
+ expect(model.messages[0].renderedDelegate).toBe(delegateBefore);
175
+ });
176
+ });
177
+ });
package/src/model.ts CHANGED
@@ -234,7 +234,13 @@ export class LabChatModel
234
234
  }
235
235
 
236
236
  sendMessage(message: INewMessage): string | null {
237
- if (!message.body && !message.mime_model && !message.attachments?.length) {
237
+ // Allow empty message for bot only, as it may be streamed later.
238
+ if (
239
+ !message.body &&
240
+ !message.mime_model &&
241
+ !message.attachments?.length &&
242
+ !message.sender?.bot
243
+ ) {
238
244
  return null;
239
245
  }
240
246
  this._resetWritingStatus();
@@ -316,6 +322,11 @@ export class LabChatModel
316
322
  };
317
323
  }
318
324
 
325
+ // Update the mime model.
326
+ if (updatedMessage.mime_model) {
327
+ message.mime_model = updatedMessage.mime_model;
328
+ }
329
+
319
330
  // Update the attachments.
320
331
  const attachmentIds = updatedMessage.attachments?.map(attachment =>
321
332
  this.sharedModel.setAttachment(attachment)
@@ -551,7 +562,8 @@ export class LabChatModel
551
562
  'raw_time',
552
563
  'deleted',
553
564
  'edited',
554
- 'metadata'
565
+ 'metadata',
566
+ 'mime_model'
555
567
  ].includes(key)
556
568
  ) {
557
569
  const update: Partial<IMessageContent> = {};