jupyterlab-chat 0.6.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/src/model.ts ADDED
@@ -0,0 +1,273 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { ChatModel, IChatMessage, INewMessage, IUser } from '@jupyter/chat';
7
+ import { IChangedArgs } from '@jupyterlab/coreutils';
8
+ import { DocumentRegistry } from '@jupyterlab/docregistry';
9
+ import { User } from '@jupyterlab/services';
10
+ import { PartialJSONObject, UUID } from '@lumino/coreutils';
11
+ import { ISignal, Signal } from '@lumino/signaling';
12
+
13
+ import { IWidgetConfig } from './token';
14
+ import { IChatChanges, IYmessage, YChat } from './ychat';
15
+
16
+ const WRITING_DELAY = 1000;
17
+
18
+ /**
19
+ * Chat model namespace.
20
+ */
21
+ export namespace LabChatModel {
22
+ export interface IOptions extends ChatModel.IOptions {
23
+ widgetConfig: IWidgetConfig;
24
+ user: User.IIdentity | null;
25
+ sharedModel?: YChat;
26
+ languagePreference?: string;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The chat model.
32
+ */
33
+ export class LabChatModel extends ChatModel implements DocumentRegistry.IModel {
34
+ constructor(options: LabChatModel.IOptions) {
35
+ super(options);
36
+
37
+ this._user = options.user || { username: 'user undefined' };
38
+
39
+ const { widgetConfig, sharedModel } = options;
40
+
41
+ if (sharedModel) {
42
+ this._sharedModel = sharedModel;
43
+ } else {
44
+ this._sharedModel = YChat.create();
45
+ }
46
+
47
+ this.id = this._sharedModel.id;
48
+
49
+ this.sharedModel.changed.connect(this._onchange, this);
50
+
51
+ this.config = widgetConfig.config;
52
+
53
+ widgetConfig.configChanged.connect((_, config) => {
54
+ this.config = config;
55
+ });
56
+
57
+ this.sharedModel.awareness.on('change', this.onAwarenessChange);
58
+ }
59
+
60
+ readonly collaborative = true;
61
+
62
+ get user(): IUser {
63
+ return this._user;
64
+ }
65
+
66
+ get sharedModel(): YChat {
67
+ return this._sharedModel;
68
+ }
69
+
70
+ get contentChanged(): ISignal<this, void> {
71
+ return this._contentChanged;
72
+ }
73
+
74
+ get stateChanged(): ISignal<this, IChangedArgs<any, any, string>> {
75
+ return this._stateChanged;
76
+ }
77
+
78
+ get dirty(): boolean {
79
+ return this._dirty;
80
+ }
81
+ set dirty(value: boolean) {
82
+ this._dirty = value;
83
+ }
84
+
85
+ get readOnly(): boolean {
86
+ return this._readOnly;
87
+ }
88
+ set readOnly(value: boolean) {
89
+ this._readOnly = value;
90
+ }
91
+
92
+ get disposed(): ISignal<LabChatModel, void> {
93
+ return this._disposed;
94
+ }
95
+
96
+ dispose(): void {
97
+ if (this.isDisposed) {
98
+ return;
99
+ }
100
+ super.dispose();
101
+ this._sharedModel.dispose();
102
+ this._disposed.emit();
103
+ Signal.clearData(this);
104
+ }
105
+
106
+ toString(): string {
107
+ return JSON.stringify({}, null, 2);
108
+ }
109
+
110
+ fromString(data: string): void {
111
+ /** */
112
+ }
113
+
114
+ toJSON(): PartialJSONObject {
115
+ return JSON.parse(this.toString());
116
+ }
117
+
118
+ fromJSON(data: PartialJSONObject): void {
119
+ // nothing to do
120
+ }
121
+
122
+ sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void {
123
+ this._resetWritingStatus();
124
+ if (this._timeoutWriting !== null) {
125
+ window.clearTimeout(this._timeoutWriting);
126
+ }
127
+ const msg: IYmessage = {
128
+ type: 'msg',
129
+ id: UUID.uuid4(),
130
+ body: message.body,
131
+ time: Date.now() / 1000,
132
+ sender: this._user.username,
133
+ raw_time: true
134
+ };
135
+
136
+ // Add the user if it does not exist or has changed
137
+ if (!(this.sharedModel.getUser(this._user.username) === this._user)) {
138
+ this.sharedModel.setUser(this._user);
139
+ }
140
+ this.sharedModel.addMessage(msg);
141
+ }
142
+
143
+ updateMessage(
144
+ id: string,
145
+ updatedMessage: IChatMessage
146
+ ): Promise<boolean | void> | boolean | void {
147
+ const index = this.sharedModel.getMessageIndex(id);
148
+ let message = this.sharedModel.getMessage(index);
149
+ if (message) {
150
+ message.body = updatedMessage.body;
151
+ message.edited = true;
152
+ } else {
153
+ const sender = updatedMessage.sender.username;
154
+
155
+ message = {
156
+ type: 'msg',
157
+ id: id || UUID.uuid4(),
158
+ body: updatedMessage.body,
159
+ time: updatedMessage.time || Date.now() / 1000,
160
+ sender: sender,
161
+ edited: true
162
+ };
163
+ }
164
+ this.sharedModel.updateMessage(index, message as IYmessage);
165
+ }
166
+
167
+ deleteMessage(id: string): Promise<boolean | void> | boolean | void {
168
+ const index = this.sharedModel.getMessageIndex(id);
169
+ const message = this.sharedModel.getMessage(index);
170
+ if (!message) {
171
+ console.error('The message to delete does not exist');
172
+ return;
173
+ }
174
+ message.body = '';
175
+ message.deleted = true;
176
+ this.sharedModel.updateMessage(index, message);
177
+ }
178
+
179
+ /**
180
+ * Function called by the input on key pressed.
181
+ */
182
+ inputChanged(input?: string): void {
183
+ if (!input || !this.config.sendTypingNotification) {
184
+ return;
185
+ }
186
+ const awareness = this.sharedModel.awareness;
187
+ if (this._timeoutWriting !== null) {
188
+ window.clearTimeout(this._timeoutWriting);
189
+ }
190
+ awareness.setLocalStateField('isWriting', true);
191
+ this._timeoutWriting = window.setTimeout(() => {
192
+ this._resetWritingStatus();
193
+ }, WRITING_DELAY);
194
+ }
195
+
196
+ /**
197
+ * Triggered when an awareness state changes.
198
+ * Used to populate the writers list.
199
+ */
200
+ onAwarenessChange = () => {
201
+ const writers: IUser[] = [];
202
+ const states = this.sharedModel.awareness.getStates();
203
+ for (const stateID of states.keys()) {
204
+ const state = states.get(stateID);
205
+ if (!state || !state.user || state.user.username === this.user.username) {
206
+ continue;
207
+ }
208
+ if (state.isWriting) {
209
+ writers.push(state.user);
210
+ }
211
+ }
212
+ this.updateWriters(writers);
213
+ };
214
+
215
+ private _resetWritingStatus() {
216
+ const awareness = this.sharedModel.awareness;
217
+ const states = awareness.getLocalState();
218
+ delete states?.isWriting;
219
+ awareness.setLocalState(states);
220
+ this._timeoutWriting = null;
221
+ }
222
+
223
+ private _onchange = (_: YChat, changes: IChatChanges) => {
224
+ if (changes.messageChanges) {
225
+ const msgDelta = changes.messageChanges;
226
+ let index = 0;
227
+ msgDelta.forEach(delta => {
228
+ if (delta.retain) {
229
+ index += delta.retain;
230
+ } else if (delta.insert) {
231
+ const messages = delta.insert.map(ymessage => {
232
+ const msg: IChatMessage = {
233
+ ...ymessage,
234
+ sender: this.sharedModel.getUser(ymessage.sender) || {
235
+ username: 'User undefined'
236
+ }
237
+ };
238
+
239
+ return msg;
240
+ });
241
+ this.messagesInserted(index, messages);
242
+ index += messages.length;
243
+ } else if (delta.delete) {
244
+ this.messagesDeleted(index, delta.delete);
245
+ }
246
+ });
247
+ }
248
+
249
+ if (changes.metadataChanges) {
250
+ changes.metadataChanges.forEach(change => {
251
+ // no need to search for update or add, if the new value contains ID, let's
252
+ // update the model ID.
253
+ if (change.key === 'id') {
254
+ this.id = change.newValue as string;
255
+ }
256
+ });
257
+ }
258
+ };
259
+
260
+ readonly defaultKernelName: string = '';
261
+ readonly defaultKernelLanguage: string = '';
262
+
263
+ private _sharedModel: YChat;
264
+
265
+ private _dirty = false;
266
+ private _readOnly = false;
267
+ private _disposed = new Signal<this, void>(this);
268
+ private _contentChanged = new Signal<this, void>(this);
269
+ private _stateChanged = new Signal<this, IChangedArgs<any>>(this);
270
+ private _timeoutWriting: number | null = null;
271
+
272
+ private _user: IUser;
273
+ }
package/src/token.ts ADDED
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import {
7
+ IConfig,
8
+ chatIcon,
9
+ IActiveCellManager,
10
+ ISelectionWatcher
11
+ } from '@jupyter/chat';
12
+ import { IWidgetTracker } from '@jupyterlab/apputils';
13
+ import { DocumentRegistry } from '@jupyterlab/docregistry';
14
+ import { Token } from '@lumino/coreutils';
15
+ import { ISignal } from '@lumino/signaling';
16
+ import { ChatPanel, LabChatPanel } from './widget';
17
+
18
+ /**
19
+ * The file type for a chat document.
20
+ */
21
+ export const chatFileType: DocumentRegistry.IFileType = {
22
+ name: 'chat',
23
+ displayName: 'Chat',
24
+ mimeTypes: ['text/json', 'application/json'],
25
+ extensions: ['.chat'],
26
+ fileFormat: 'text',
27
+ contentType: 'chat',
28
+ icon: chatIcon
29
+ };
30
+
31
+ /**
32
+ * The token for the chat widget factory.
33
+ */
34
+ export const IChatFactory = new Token<IChatFactory>(
35
+ 'jupyterlab-chat:IChatFactory'
36
+ );
37
+
38
+ /**
39
+ * The chat configs.
40
+ */
41
+ export interface ILabChatConfig extends IConfig {
42
+ /**
43
+ * The default directory where to create and look for chat.
44
+ */
45
+ defaultDirectory?: string;
46
+ }
47
+
48
+ /**
49
+ * The interface for the chat factory objects.
50
+ */
51
+ export interface IChatFactory {
52
+ /**
53
+ * The chat widget config.
54
+ */
55
+ widgetConfig: IWidgetConfig;
56
+ /**
57
+ * The chat panel tracker.
58
+ */
59
+ tracker: IWidgetTracker<LabChatPanel>;
60
+ }
61
+
62
+ /**
63
+ * The interface for the chats config.
64
+ */
65
+ export interface IWidgetConfig {
66
+ /**
67
+ * The widget config
68
+ */
69
+ config: Partial<ILabChatConfig>;
70
+
71
+ /**
72
+ * A signal emitting when the configuration for the chats has changed.
73
+ */
74
+ configChanged: IConfigChanged;
75
+ }
76
+
77
+ /**
78
+ * A signal emitting when the configuration for the chats has changed.
79
+ */
80
+ export interface IConfigChanged
81
+ extends ISignal<IWidgetConfig, Partial<ILabChatConfig>> {}
82
+
83
+ /**
84
+ * Command ids.
85
+ */
86
+ export const CommandIDs = {
87
+ /**
88
+ * Create a chat file.
89
+ */
90
+ createChat: 'jupyterlab-chat:create',
91
+ /**
92
+ * Open a chat file.
93
+ */
94
+ openChat: 'jupyterlab-chat:open',
95
+ /**
96
+ * Move a main widget to the side panel.
97
+ */
98
+ moveToSide: 'jupyterlab-chat:moveToSide',
99
+ /**
100
+ * Mark as read.
101
+ */
102
+ markAsRead: 'jupyterlab-chat:markAsRead',
103
+ /**
104
+ * Focus the input of the current chat.
105
+ */
106
+ focusInput: 'jupyterlab-chat:focusInput'
107
+ };
108
+
109
+ /**
110
+ * The chat panel token.
111
+ */
112
+ export const IChatPanel = new Token<ChatPanel>('jupyterlab-chat:IChatPanel');
113
+
114
+ /**
115
+ * The active cell manager plugin.
116
+ */
117
+ export const IActiveCellManagerToken = new Token<IActiveCellManager>(
118
+ 'jupyterlab-chat:IActiveCellManager'
119
+ );
120
+
121
+ /**
122
+ * The selection watcher plugin.
123
+ */
124
+ export const ISelectionWatcherToken = new Token<ISelectionWatcher>(
125
+ 'jupyterlab-chat:ISelectionWatcher'
126
+ );