jupyterlab-chat 0.22.0 → 0.23.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
@@ -1,13 +1,41 @@
1
- "use strict";
2
1
  /*
3
2
  * Copyright (c) Jupyter Development Team.
4
3
  * Distributed under the terms of the Modified BSD License.
5
4
  */
6
- /**
7
- * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
8
- */
9
- describe('jupyterlab-chat', () => {
10
- it('should be tested', () => {
11
- expect(1 + 1).toEqual(2);
5
+ import { enforceAutosaveEnabled } from '../autosave';
6
+ describe('enforceAutosaveEnabled', () => {
7
+ it('should set autosave to true when it is missing', () => {
8
+ const state = {};
9
+ const awareness = {
10
+ getLocalState: () => state,
11
+ setLocalStateField: (field, value) => {
12
+ state[field] = value;
13
+ }
14
+ };
15
+ enforceAutosaveEnabled(awareness);
16
+ expect(state.autosave).toBe(true);
17
+ });
18
+ it('should set autosave to true when it is false', () => {
19
+ const state = { autosave: false };
20
+ const awareness = {
21
+ getLocalState: () => state,
22
+ setLocalStateField: (field, value) => {
23
+ state[field] = value;
24
+ }
25
+ };
26
+ enforceAutosaveEnabled(awareness);
27
+ expect(state.autosave).toBe(true);
28
+ });
29
+ it('should keep autosave true when already enabled', () => {
30
+ const state = { autosave: true };
31
+ let called = false;
32
+ const awareness = {
33
+ getLocalState: () => state,
34
+ setLocalStateField: () => {
35
+ called = true;
36
+ }
37
+ };
38
+ enforceAutosaveEnabled(awareness);
39
+ expect(called).toBe(false);
12
40
  });
13
41
  });
@@ -0,0 +1,8 @@
1
+ import type { IAwareness } from '@jupyter/ydoc';
2
+ type AutosaveAwareness = Pick<IAwareness, 'getLocalState' | 'setLocalStateField'>;
3
+ /**
4
+ * Chat files should always advertise autosave=true so server-side RTC saving
5
+ * remains enabled even when global autosave is disabled.
6
+ */
7
+ export declare function enforceAutosaveEnabled(awareness: AutosaveAwareness): void;
8
+ export {};
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Chat files should always advertise autosave=true so server-side RTC saving
7
+ * remains enabled even when global autosave is disabled.
8
+ */
9
+ export function enforceAutosaveEnabled(awareness) {
10
+ var _a;
11
+ if (((_a = awareness.getLocalState()) === null || _a === void 0 ? void 0 : _a.autosave) !== true) {
12
+ awareness.setLocalStateField('autosave', true);
13
+ }
14
+ }
package/lib/model.d.ts CHANGED
@@ -91,6 +91,7 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
91
91
  onAwarenessChange: () => void;
92
92
  private _buildMentionList;
93
93
  private _resetWritingStatus;
94
+ private _enforceAutosaveEnabled;
94
95
  private _onchange;
95
96
  readonly defaultKernelName: string;
96
97
  readonly defaultKernelLanguage: string;
package/lib/model.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { AbstractChatModel, AbstractChatContext } from '@jupyter/chat';
6
6
  import { UUID } from '@lumino/coreutils';
7
7
  import { Signal } from '@lumino/signaling';
8
+ import { enforceAutosaveEnabled } from './autosave';
8
9
  import { YChat } from './ychat';
9
10
  const WRITING_DELAY = 1000;
10
11
  /**
@@ -103,6 +104,9 @@ export class LabChatModel extends AbstractChatModel {
103
104
  }
104
105
  this.updateWriters(writers);
105
106
  };
107
+ this._enforceAutosaveEnabled = () => {
108
+ enforceAutosaveEnabled(this.sharedModel.awareness);
109
+ };
106
110
  this._onchange = async (_, changes) => {
107
111
  if (changes.messageListChanges) {
108
112
  const msgDelta = changes.messageListChanges;
@@ -221,6 +225,12 @@ export class LabChatModel extends AbstractChatModel {
221
225
  }
222
226
  });
223
227
  }
228
+ // Create a chat ID if not created when the document is not dirty.
229
+ if (changes.stateChange && !this._sharedModel.id) {
230
+ if (changes.stateChange.some(change => change.name === 'dirty' && !change.newValue)) {
231
+ this._sharedModel.id = UUID.uuid4();
232
+ }
233
+ }
224
234
  };
225
235
  this.defaultKernelName = '';
226
236
  this.defaultKernelLanguage = '';
@@ -238,12 +248,14 @@ export class LabChatModel extends AbstractChatModel {
238
248
  else {
239
249
  this._sharedModel = YChat.create();
240
250
  }
241
- this.sharedModel.changed.connect(this._onchange, this);
251
+ this._sharedModel.changed.connect(this._onchange, this);
242
252
  this.config = widgetConfig.config;
243
253
  widgetConfig.configChanged.connect((_, config) => {
244
254
  this.config = config;
245
255
  });
246
256
  this.sharedModel.awareness.on('change', this.onAwarenessChange);
257
+ this.sharedModel.awareness.on('change', this._enforceAutosaveEnabled);
258
+ this._enforceAutosaveEnabled();
247
259
  this.input.valueChanged.connect((_, value) => this.onInputChanged(value));
248
260
  this.messageEditionAdded.connect(this.onMessageEditionAdded);
249
261
  }
@@ -282,6 +294,8 @@ export class LabChatModel extends AbstractChatModel {
282
294
  return;
283
295
  }
284
296
  super.dispose();
297
+ this.sharedModel.awareness.off('change', this.onAwarenessChange);
298
+ this.sharedModel.awareness.off('change', this._enforceAutosaveEnabled);
285
299
  this._sharedModel.dispose();
286
300
  Signal.clearData(this);
287
301
  }
package/lib/ychat.d.ts CHANGED
@@ -76,6 +76,7 @@ export declare class YChat extends YDocument<IChatChanges> {
76
76
  */
77
77
  static create(options?: YDocument.IOptions): YChat;
78
78
  get id(): string;
79
+ set id(value: string);
79
80
  get users(): Record<string, IUser>;
80
81
  get messages(): string[];
81
82
  get attachments(): JSONObject;
package/lib/ychat.js CHANGED
@@ -185,6 +185,13 @@ export class YChat extends YDocument {
185
185
  get id() {
186
186
  return this._metadata.get('id') || '';
187
187
  }
188
+ set id(value) {
189
+ this.transact(() => {
190
+ if (!this._metadata.get('id')) {
191
+ this._metadata.set('id', value);
192
+ }
193
+ });
194
+ }
188
195
  get users() {
189
196
  const userDict = JSONExt.deepCopy(this._users.toJSON());
190
197
  return userDict;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-chat",
3
- "version": "0.22.0",
3
+ "version": "0.23.0-alpha.0",
4
4
  "description": "The library to build a chat based on shared document",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -42,31 +42,31 @@
42
42
  "watch:src": "tsc -w --sourceMap"
43
43
  },
44
44
  "dependencies": {
45
- "@jupyter/chat": "^0.22.0",
46
- "@jupyter/collaborative-drive": "^4.0.2",
47
- "@jupyter/ydoc": "^2.0.0 || ^3.0.0",
48
- "@jupyterlab/application": "^4.2.0",
49
- "@jupyterlab/apputils": "^4.3.0",
50
- "@jupyterlab/coreutils": "^6.2.0",
51
- "@jupyterlab/docmanager": "^4.2.0",
52
- "@jupyterlab/docregistry": "^4.2.0",
53
- "@jupyterlab/launcher": "^4.2.0",
54
- "@jupyterlab/notebook": "^4.2.0",
55
- "@jupyterlab/rendermime": "^4.2.0",
56
- "@jupyterlab/services": "^7.2.0",
57
- "@jupyterlab/settingregistry": "^4.2.0",
58
- "@jupyterlab/translation": "^4.2.0",
59
- "@jupyterlab/ui-components": "^4.2.0",
60
- "@lumino/commands": "^2.0.0",
61
- "@lumino/coreutils": "^2.0.0",
62
- "@lumino/signaling": "^2.0.0",
63
- "@lumino/widgets": "^2.0.0",
45
+ "@jupyter/chat": "^0.23.0-alpha.0",
46
+ "@jupyter/collaborative-drive": "^4.4.0",
47
+ "@jupyter/ydoc": "^3.0.0 || ^4.0.0",
48
+ "@jupyterlab/application": "^4.6.0",
49
+ "@jupyterlab/apputils": "^4.7.0",
50
+ "@jupyterlab/coreutils": "^6.6.0",
51
+ "@jupyterlab/docmanager": "^4.6.0",
52
+ "@jupyterlab/docregistry": "^4.6.0",
53
+ "@jupyterlab/launcher": "^4.6.0",
54
+ "@jupyterlab/notebook": "^4.6.0",
55
+ "@jupyterlab/rendermime": "^4.6.0",
56
+ "@jupyterlab/services": "^7.6.0",
57
+ "@jupyterlab/settingregistry": "^4.6.0",
58
+ "@jupyterlab/translation": "^4.6.0",
59
+ "@jupyterlab/ui-components": "^4.6.0",
60
+ "@lumino/commands": "^2.3.3",
61
+ "@lumino/coreutils": "^2.2.2",
62
+ "@lumino/signaling": "^2.1.5",
63
+ "@lumino/widgets": "^2.8.0",
64
64
  "react": "^18.2.0",
65
65
  "y-protocols": "^1.0.5",
66
66
  "yjs": "^13.5.40"
67
67
  },
68
68
  "devDependencies": {
69
- "@jupyterlab/testing": "^4.2.0",
69
+ "@jupyterlab/testing": "^4.6.0",
70
70
  "@types/jest": "^29.2.0",
71
71
  "@types/json-schema": "^7.0.11",
72
72
  "@types/react": "^18.2.0",
@@ -78,7 +78,7 @@
78
78
  "rimraf": "^5.0.1",
79
79
  "source-map-loader": "^1.0.2",
80
80
  "style-loader": "^3.3.1",
81
- "typescript": "~5.0.2",
81
+ "typescript": "~5.9.3",
82
82
  "yjs": "^13.5.0"
83
83
  },
84
84
  "sideEffects": [
@@ -3,12 +3,46 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- /**
7
- * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
8
- */
6
+ import { enforceAutosaveEnabled } from '../autosave';
7
+
8
+ describe('enforceAutosaveEnabled', () => {
9
+ it('should set autosave to true when it is missing', () => {
10
+ const state: Record<string, unknown> = {};
11
+ const awareness = {
12
+ getLocalState: () => state,
13
+ setLocalStateField: (field: string, value: unknown) => {
14
+ state[field] = value;
15
+ }
16
+ };
17
+
18
+ enforceAutosaveEnabled(awareness);
19
+ expect(state.autosave).toBe(true);
20
+ });
21
+
22
+ it('should set autosave to true when it is false', () => {
23
+ const state: Record<string, unknown> = { autosave: false };
24
+ const awareness = {
25
+ getLocalState: () => state,
26
+ setLocalStateField: (field: string, value: unknown) => {
27
+ state[field] = value;
28
+ }
29
+ };
30
+
31
+ enforceAutosaveEnabled(awareness);
32
+ expect(state.autosave).toBe(true);
33
+ });
34
+
35
+ it('should keep autosave true when already enabled', () => {
36
+ const state: Record<string, unknown> = { autosave: true };
37
+ let called = false;
38
+ const awareness = {
39
+ getLocalState: () => state,
40
+ setLocalStateField: () => {
41
+ called = true;
42
+ }
43
+ };
9
44
 
10
- describe('jupyterlab-chat', () => {
11
- it('should be tested', () => {
12
- expect(1 + 1).toEqual(2);
45
+ enforceAutosaveEnabled(awareness);
46
+ expect(called).toBe(false);
13
47
  });
14
48
  });
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import type { IAwareness } from '@jupyter/ydoc';
7
+
8
+ type AutosaveAwareness = Pick<
9
+ IAwareness,
10
+ 'getLocalState' | 'setLocalStateField'
11
+ >;
12
+
13
+ /**
14
+ * Chat files should always advertise autosave=true so server-side RTC saving
15
+ * remains enabled even when global autosave is disabled.
16
+ */
17
+ export function enforceAutosaveEnabled(awareness: AutosaveAwareness): void {
18
+ if (awareness.getLocalState()?.autosave !== true) {
19
+ awareness.setLocalStateField('autosave', true);
20
+ }
21
+ }
package/src/model.ts CHANGED
@@ -20,6 +20,7 @@ import { User } from '@jupyterlab/services';
20
20
  import { PartialJSONObject, UUID } from '@lumino/coreutils';
21
21
  import { ISignal, Signal } from '@lumino/signaling';
22
22
 
23
+ import { enforceAutosaveEnabled } from './autosave';
23
24
  import { IWidgetConfig } from './token';
24
25
  import { IChatChanges, IYmessage, YChat } from './ychat';
25
26
 
@@ -105,7 +106,7 @@ export class LabChatModel
105
106
  this._sharedModel = YChat.create();
106
107
  }
107
108
 
108
- this.sharedModel.changed.connect(this._onchange, this);
109
+ this._sharedModel.changed.connect(this._onchange, this);
109
110
 
110
111
  this.config = widgetConfig.config;
111
112
 
@@ -114,6 +115,8 @@ export class LabChatModel
114
115
  });
115
116
 
116
117
  this.sharedModel.awareness.on('change', this.onAwarenessChange);
118
+ this.sharedModel.awareness.on('change', this._enforceAutosaveEnabled);
119
+ this._enforceAutosaveEnabled();
117
120
 
118
121
  this.input.valueChanged.connect((_, value) => this.onInputChanged(value));
119
122
  this.messageEditionAdded.connect(this.onMessageEditionAdded);
@@ -163,6 +166,8 @@ export class LabChatModel
163
166
  return;
164
167
  }
165
168
  super.dispose();
169
+ this.sharedModel.awareness.off('change', this.onAwarenessChange);
170
+ this.sharedModel.awareness.off('change', this._enforceAutosaveEnabled);
166
171
  this._sharedModel.dispose();
167
172
  Signal.clearData(this);
168
173
  }
@@ -397,6 +402,10 @@ export class LabChatModel
397
402
  this._timeoutWriting = null;
398
403
  }
399
404
 
405
+ private _enforceAutosaveEnabled = () => {
406
+ enforceAutosaveEnabled(this.sharedModel.awareness);
407
+ };
408
+
400
409
  private _onchange = async (_: YChat, changes: IChatChanges) => {
401
410
  if (changes.messageListChanges) {
402
411
  const msgDelta = changes.messageListChanges;
@@ -530,6 +539,17 @@ export class LabChatModel
530
539
  }
531
540
  });
532
541
  }
542
+
543
+ // Create a chat ID if not created when the document is not dirty.
544
+ if (changes.stateChange && !this._sharedModel.id) {
545
+ if (
546
+ changes.stateChange.some(
547
+ change => change.name === 'dirty' && !change.newValue
548
+ )
549
+ ) {
550
+ this._sharedModel.id = UUID.uuid4();
551
+ }
552
+ }
533
553
  };
534
554
 
535
555
  readonly defaultKernelName: string = '';
package/src/ychat.ts CHANGED
@@ -109,6 +109,14 @@ export class YChat extends YDocument<IChatChanges> {
109
109
  return (this._metadata.get('id') as string) || '';
110
110
  }
111
111
 
112
+ set id(value: string) {
113
+ this.transact(() => {
114
+ if (!this._metadata.get('id')) {
115
+ this._metadata.set('id', value);
116
+ }
117
+ });
118
+ }
119
+
112
120
  get users(): Record<string, IUser> {
113
121
  const userDict = JSONExt.deepCopy(this._users.toJSON()) as Record<
114
122
  string,