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/lib/__tests__/jupyterlab_collaborative_chat.spec.d.ts +0 -0
- package/lib/__tests__/jupyterlab_collaborative_chat.spec.js +13 -0
- package/lib/factory.d.ts +118 -0
- package/lib/factory.js +146 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +9 -0
- package/lib/model.d.ts +64 -0
- package/lib/model.js +216 -0
- package/lib/token.d.ts +91 -0
- package/lib/token.js +59 -0
- package/lib/widget.d.ts +121 -0
- package/lib/widget.js +342 -0
- package/lib/ychat.d.ts +75 -0
- package/lib/ychat.js +148 -0
- package/package.json +205 -0
- package/src/__tests__/jupyterlab_collaborative_chat.spec.ts +14 -0
- package/src/factory.ts +207 -0
- package/src/index.ts +10 -0
- package/src/model.ts +273 -0
- package/src/token.ts +126 -0
- package/src/widget.tsx +479 -0
- package/src/ychat.ts +219 -0
- package/style/base.css +22 -0
- package/style/index.css +6 -0
- package/style/index.js +6 -0
package/src/widget.tsx
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChatWidget,
|
|
8
|
+
IAutocompletionRegistry,
|
|
9
|
+
IChatModel,
|
|
10
|
+
readIcon
|
|
11
|
+
} from '@jupyter/chat';
|
|
12
|
+
import { ICollaborativeDrive } from '@jupyter/docprovider';
|
|
13
|
+
import { IThemeManager } from '@jupyterlab/apputils';
|
|
14
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
15
|
+
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
16
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
17
|
+
import {
|
|
18
|
+
addIcon,
|
|
19
|
+
closeIcon,
|
|
20
|
+
CommandToolbarButton,
|
|
21
|
+
HTMLSelect,
|
|
22
|
+
launchIcon,
|
|
23
|
+
PanelWithToolbar,
|
|
24
|
+
ReactWidget,
|
|
25
|
+
SidePanel,
|
|
26
|
+
ToolbarButton
|
|
27
|
+
} from '@jupyterlab/ui-components';
|
|
28
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
29
|
+
import { Message } from '@lumino/messaging';
|
|
30
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
31
|
+
import { AccordionPanel, Panel } from '@lumino/widgets';
|
|
32
|
+
import React, { useState } from 'react';
|
|
33
|
+
|
|
34
|
+
import { LabChatModel } from './model';
|
|
35
|
+
import { CommandIDs, chatFileType } from './token';
|
|
36
|
+
|
|
37
|
+
const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel';
|
|
38
|
+
const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread';
|
|
39
|
+
const SIDEPANEL_CLASS = 'jp-lab-chat-sidepanel';
|
|
40
|
+
const ADD_BUTTON_CLASS = 'jp-lab-chat-add';
|
|
41
|
+
const OPEN_SELECT_CLASS = 'jp-lab-chat-open';
|
|
42
|
+
const SECTION_CLASS = 'jp-lab-chat-section';
|
|
43
|
+
const TOOLBAR_CLASS = 'jp-lab-chat-toolbar';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* DocumentWidget: widget that represents the view or editor for a file type.
|
|
47
|
+
*/
|
|
48
|
+
export class LabChatPanel extends DocumentWidget<ChatWidget, LabChatModel> {
|
|
49
|
+
constructor(options: DocumentWidget.IOptions<ChatWidget, LabChatModel>) {
|
|
50
|
+
super(options);
|
|
51
|
+
this.addClass(MAIN_PANEL_CLASS);
|
|
52
|
+
this.model.name = this.context.localPath;
|
|
53
|
+
this.model.unreadChanged.connect(this._unreadChanged);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dispose of the resources held by the widget.
|
|
58
|
+
*/
|
|
59
|
+
dispose(): void {
|
|
60
|
+
this.model.unreadChanged.disconnect(this._unreadChanged);
|
|
61
|
+
this.context.dispose();
|
|
62
|
+
this.content.dispose();
|
|
63
|
+
super.dispose();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The model for the widget.
|
|
68
|
+
*/
|
|
69
|
+
get model(): LabChatModel {
|
|
70
|
+
return this.context.model;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add class to tab when messages are unread.
|
|
75
|
+
*/
|
|
76
|
+
private _unreadChanged = (_: IChatModel, unread: number[]) => {
|
|
77
|
+
if (unread.length) {
|
|
78
|
+
if (!this.title.className.includes(TITLE_UNREAD_CLASS)) {
|
|
79
|
+
this.title.className += ` ${TITLE_UNREAD_CLASS}`;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
this.title.className = this.title.className.replace(
|
|
83
|
+
TITLE_UNREAD_CLASS,
|
|
84
|
+
''
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sidepanel widget including the chats and the add chat button.
|
|
92
|
+
*/
|
|
93
|
+
export class ChatPanel extends SidePanel {
|
|
94
|
+
/**
|
|
95
|
+
* The constructor of the chat panel.
|
|
96
|
+
*/
|
|
97
|
+
constructor(options: ChatPanel.IOptions) {
|
|
98
|
+
super(options);
|
|
99
|
+
this.addClass(SIDEPANEL_CLASS);
|
|
100
|
+
this._commands = options.commands;
|
|
101
|
+
this._drive = options.drive;
|
|
102
|
+
this._rmRegistry = options.rmRegistry;
|
|
103
|
+
this._themeManager = options.themeManager;
|
|
104
|
+
this._defaultDirectory = options.defaultDirectory;
|
|
105
|
+
this._autocompletionRegistry = options.autocompletionRegistry;
|
|
106
|
+
|
|
107
|
+
const addChat = new CommandToolbarButton({
|
|
108
|
+
commands: this._commands,
|
|
109
|
+
id: CommandIDs.createChat,
|
|
110
|
+
args: { inSidePanel: true },
|
|
111
|
+
icon: addIcon
|
|
112
|
+
});
|
|
113
|
+
addChat.addClass(ADD_BUTTON_CLASS);
|
|
114
|
+
this.toolbar.addItem('createChat', addChat);
|
|
115
|
+
|
|
116
|
+
this._openChat = ReactWidget.create(
|
|
117
|
+
<ChatSelect
|
|
118
|
+
chatNamesChanged={this._chatNamesChanged}
|
|
119
|
+
handleChange={this._chatSelected.bind(this)}
|
|
120
|
+
></ChatSelect>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
this._openChat.addClass(OPEN_SELECT_CLASS);
|
|
124
|
+
this.toolbar.addItem('openChat', this._openChat);
|
|
125
|
+
|
|
126
|
+
const content = this.content as AccordionPanel;
|
|
127
|
+
content.expansionToggled.connect(this._onExpansionToggled, this);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Getter and setter of the defaultDirectory.
|
|
132
|
+
*/
|
|
133
|
+
get defaultDirectory(): string {
|
|
134
|
+
return this._defaultDirectory;
|
|
135
|
+
}
|
|
136
|
+
set defaultDirectory(value: string) {
|
|
137
|
+
if (value === this._defaultDirectory) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this._defaultDirectory = value;
|
|
141
|
+
// Update the list of discoverable chat (in default directory)
|
|
142
|
+
this.updateChatList();
|
|
143
|
+
// Update the sections names.
|
|
144
|
+
this.widgets.forEach(w => {
|
|
145
|
+
(w as ChatSection).defaultDirectory = value;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a new widget to the chat panel.
|
|
151
|
+
*
|
|
152
|
+
* @param model - the model of the chat widget
|
|
153
|
+
* @param name - the name of the chat.
|
|
154
|
+
*/
|
|
155
|
+
addChat(model: IChatModel, path: string): void {
|
|
156
|
+
// Collapse all chats
|
|
157
|
+
const content = this.content as AccordionPanel;
|
|
158
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
159
|
+
content.collapse(i);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Set the name of the model.
|
|
163
|
+
model.name = path;
|
|
164
|
+
|
|
165
|
+
// Create a new widget.
|
|
166
|
+
const widget = new ChatWidget({
|
|
167
|
+
model: model,
|
|
168
|
+
rmRegistry: this._rmRegistry,
|
|
169
|
+
themeManager: this._themeManager,
|
|
170
|
+
autocompletionRegistry: this._autocompletionRegistry
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.addWidget(
|
|
174
|
+
new ChatSection({
|
|
175
|
+
widget,
|
|
176
|
+
commands: this._commands,
|
|
177
|
+
path,
|
|
178
|
+
defaultDirectory: this._defaultDirectory
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Update the list of available chats in the default directory.
|
|
185
|
+
*/
|
|
186
|
+
updateChatList = async (): Promise<void> => {
|
|
187
|
+
const extension = chatFileType.extensions[0];
|
|
188
|
+
this._drive
|
|
189
|
+
.get(this._defaultDirectory)
|
|
190
|
+
.then(contentModel => {
|
|
191
|
+
const chatsNames: { [name: string]: string } = {};
|
|
192
|
+
(contentModel.content as any[])
|
|
193
|
+
.filter(f => f.type === 'file' && f.name.endsWith(extension))
|
|
194
|
+
.forEach(f => {
|
|
195
|
+
chatsNames[PathExt.basename(f.name, extension)] = f.path;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this._chatNamesChanged.emit(chatsNames);
|
|
199
|
+
})
|
|
200
|
+
.catch(e => console.error('Error getting the chat files from drive', e));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Open a chat if it exists in the side panel.
|
|
205
|
+
*
|
|
206
|
+
* @param path - the path of the chat.
|
|
207
|
+
* @returns a boolean, whether the chat existed in the side panel or not.
|
|
208
|
+
*/
|
|
209
|
+
openIfExists(path: string): boolean {
|
|
210
|
+
const index = this._getChatIndex(path);
|
|
211
|
+
if (index > -1) {
|
|
212
|
+
this._expandChat(index);
|
|
213
|
+
}
|
|
214
|
+
return index > -1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A message handler invoked on an `'after-show'` message.
|
|
219
|
+
*/
|
|
220
|
+
protected onAfterShow(msg: Message): void {
|
|
221
|
+
// Wait for the component to be rendered.
|
|
222
|
+
this._openChat.renderPromise?.then(() => this.updateChatList());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Return the index of the chat in the list (-1 if not opened).
|
|
227
|
+
*
|
|
228
|
+
* @param name - the chat name.
|
|
229
|
+
*/
|
|
230
|
+
private _getChatIndex(path: string) {
|
|
231
|
+
return this.widgets.findIndex(w => (w as ChatSection).path === path);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Expand the chat from its index.
|
|
236
|
+
*/
|
|
237
|
+
private _expandChat(index: number): void {
|
|
238
|
+
if (!this.widgets[index].isVisible) {
|
|
239
|
+
(this.content as AccordionPanel).expand(index);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle `change` events for the HTMLSelect component.
|
|
245
|
+
*/
|
|
246
|
+
private _chatSelected = (
|
|
247
|
+
event: React.ChangeEvent<HTMLSelectElement>
|
|
248
|
+
): void => {
|
|
249
|
+
const select = event.target;
|
|
250
|
+
const path = select.value;
|
|
251
|
+
const name = select.options[select.selectedIndex].textContent;
|
|
252
|
+
if (name === '-') {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this._commands.execute(CommandIDs.openChat, {
|
|
257
|
+
filepath: path,
|
|
258
|
+
inSidePanel: true
|
|
259
|
+
});
|
|
260
|
+
event.target.selectedIndex = 0;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Triggered when a section is toogled. If the section is opened, all others
|
|
265
|
+
* sections are closed.
|
|
266
|
+
*/
|
|
267
|
+
private _onExpansionToggled(panel: AccordionPanel, index: number) {
|
|
268
|
+
if (!this.widgets[index].isVisible) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
272
|
+
if (i !== index) {
|
|
273
|
+
panel.collapse(i);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private _chatNamesChanged = new Signal<this, { [name: string]: string }>(
|
|
279
|
+
this
|
|
280
|
+
);
|
|
281
|
+
private _commands: CommandRegistry;
|
|
282
|
+
private _defaultDirectory: string;
|
|
283
|
+
private _drive: ICollaborativeDrive;
|
|
284
|
+
private _openChat: ReactWidget;
|
|
285
|
+
private _rmRegistry: IRenderMimeRegistry;
|
|
286
|
+
private _themeManager: IThemeManager | null;
|
|
287
|
+
private _autocompletionRegistry?: IAutocompletionRegistry;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The chat panel namespace.
|
|
292
|
+
*/
|
|
293
|
+
export namespace ChatPanel {
|
|
294
|
+
/**
|
|
295
|
+
* Options of the constructor of the chat panel.
|
|
296
|
+
*/
|
|
297
|
+
export interface IOptions extends SidePanel.IOptions {
|
|
298
|
+
commands: CommandRegistry;
|
|
299
|
+
drive: ICollaborativeDrive;
|
|
300
|
+
rmRegistry: IRenderMimeRegistry;
|
|
301
|
+
themeManager: IThemeManager | null;
|
|
302
|
+
defaultDirectory: string;
|
|
303
|
+
autocompletionRegistry?: IAutocompletionRegistry;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The chat section containing a chat widget.
|
|
309
|
+
*/
|
|
310
|
+
class ChatSection extends PanelWithToolbar {
|
|
311
|
+
/**
|
|
312
|
+
* Constructor of the chat section.
|
|
313
|
+
*/
|
|
314
|
+
constructor(options: ChatSection.IOptions) {
|
|
315
|
+
super(options);
|
|
316
|
+
|
|
317
|
+
this.addWidget(options.widget);
|
|
318
|
+
|
|
319
|
+
this.addClass(SECTION_CLASS);
|
|
320
|
+
this._defaultDirectory = options.defaultDirectory;
|
|
321
|
+
this._path = options.path;
|
|
322
|
+
this._updateTitle();
|
|
323
|
+
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
324
|
+
|
|
325
|
+
this._markAsRead = new ToolbarButton({
|
|
326
|
+
icon: readIcon,
|
|
327
|
+
iconLabel: 'Mark chat as read',
|
|
328
|
+
className: 'jp-mod-styled',
|
|
329
|
+
onClick: () => (this.model.unreadMessages = [])
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const moveToMain = new ToolbarButton({
|
|
333
|
+
icon: launchIcon,
|
|
334
|
+
iconLabel: 'Move the chat to the main area',
|
|
335
|
+
className: 'jp-mod-styled',
|
|
336
|
+
onClick: () => {
|
|
337
|
+
this.model.dispose();
|
|
338
|
+
options.commands.execute(CommandIDs.openChat, {
|
|
339
|
+
filepath: this._path
|
|
340
|
+
});
|
|
341
|
+
this.dispose();
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const closeButton = new ToolbarButton({
|
|
346
|
+
icon: closeIcon,
|
|
347
|
+
iconLabel: 'Close the chat',
|
|
348
|
+
className: 'jp-mod-styled',
|
|
349
|
+
onClick: () => {
|
|
350
|
+
this.model.dispose();
|
|
351
|
+
this.dispose();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
this.toolbar.addItem('jupyterlabChat-markRead', this._markAsRead);
|
|
356
|
+
this.toolbar.addItem('jupyterlabChat-moveMain', moveToMain);
|
|
357
|
+
this.toolbar.addItem('jupyterlabChat-close', closeButton);
|
|
358
|
+
|
|
359
|
+
this.model.unreadChanged?.connect(this._unreadChanged);
|
|
360
|
+
|
|
361
|
+
this._markAsRead.enabled = this.model.unreadMessages.length > 0;
|
|
362
|
+
|
|
363
|
+
options.widget.node.style.height = '100%';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The path of the chat.
|
|
368
|
+
*/
|
|
369
|
+
get path(): string {
|
|
370
|
+
return this._path;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Set the default directory property.
|
|
375
|
+
*/
|
|
376
|
+
set defaultDirectory(value: string) {
|
|
377
|
+
this._defaultDirectory = value;
|
|
378
|
+
this._updateTitle();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The model of the widget.
|
|
383
|
+
*/
|
|
384
|
+
get model(): IChatModel {
|
|
385
|
+
return (this.widgets[0] as ChatWidget).model;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Dispose of the resources held by the widget.
|
|
390
|
+
*/
|
|
391
|
+
dispose(): void {
|
|
392
|
+
this.model.unreadChanged?.disconnect(this._unreadChanged);
|
|
393
|
+
super.dispose();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Update the section's title, depending on the default directory and chat file name.
|
|
398
|
+
* If the chat file is in the default directory, the section's name is its relative
|
|
399
|
+
* path to that default directory. Otherwise, it is it absolute path.
|
|
400
|
+
*/
|
|
401
|
+
private _updateTitle(): void {
|
|
402
|
+
const inDefault = this._defaultDirectory
|
|
403
|
+
? !PathExt.relative(this._defaultDirectory, this._path).startsWith('..')
|
|
404
|
+
: true;
|
|
405
|
+
|
|
406
|
+
const pattern = new RegExp(`${chatFileType.extensions[0]}$`, 'g');
|
|
407
|
+
this.title.label = (
|
|
408
|
+
inDefault
|
|
409
|
+
? this._defaultDirectory
|
|
410
|
+
? PathExt.relative(this._defaultDirectory, this._path)
|
|
411
|
+
: this._path
|
|
412
|
+
: '/' + this._path
|
|
413
|
+
).replace(pattern, '');
|
|
414
|
+
this.title.caption = this._path;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Change the title when messages are unread.
|
|
419
|
+
*
|
|
420
|
+
* TODO: fix it upstream in @jupyterlab/ui-components.
|
|
421
|
+
* Updating the title create a new Title widget, but does not attach again the
|
|
422
|
+
* toolbar. The toolbar is attached only when the title widget is attached the first
|
|
423
|
+
* time.
|
|
424
|
+
*/
|
|
425
|
+
private _unreadChanged = (_: IChatModel, unread: number[]) => {
|
|
426
|
+
this._markAsRead.enabled = unread.length > 0;
|
|
427
|
+
// this.title.label = `${unread.length ? '* ' : ''}${this._name}`;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
private _defaultDirectory: string;
|
|
431
|
+
private _markAsRead: ToolbarButton;
|
|
432
|
+
private _path: string;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The chat section namespace.
|
|
437
|
+
*/
|
|
438
|
+
export namespace ChatSection {
|
|
439
|
+
/**
|
|
440
|
+
* Options to build a chat section.
|
|
441
|
+
*/
|
|
442
|
+
export interface IOptions extends Panel.IOptions {
|
|
443
|
+
commands: CommandRegistry;
|
|
444
|
+
defaultDirectory: string;
|
|
445
|
+
widget: ChatWidget;
|
|
446
|
+
path: string;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
type ChatSelectProps = {
|
|
451
|
+
chatNamesChanged: ISignal<ChatPanel, { [name: string]: string }>;
|
|
452
|
+
handleChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* A component to select a chat from the drive.
|
|
457
|
+
*/
|
|
458
|
+
function ChatSelect({
|
|
459
|
+
chatNamesChanged,
|
|
460
|
+
handleChange
|
|
461
|
+
}: ChatSelectProps): JSX.Element {
|
|
462
|
+
// An object associating a chat name to its path. Both are purely indicative, the name
|
|
463
|
+
// is the section title and the path is used as caption.
|
|
464
|
+
const [chatNames, setChatNames] = useState<{ [name: string]: string }>({});
|
|
465
|
+
|
|
466
|
+
// Update the chat list.
|
|
467
|
+
chatNamesChanged.connect((_, chatNames) => {
|
|
468
|
+
setChatNames(chatNames);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<HTMLSelect onChange={handleChange}>
|
|
473
|
+
<option value="-">Open a chat</option>
|
|
474
|
+
{Object.keys(chatNames).map(name => (
|
|
475
|
+
<option value={chatNames[name]}>{name}</option>
|
|
476
|
+
))}
|
|
477
|
+
</HTMLSelect>
|
|
478
|
+
);
|
|
479
|
+
}
|
package/src/ychat.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IChatMessage, IUser } from '@jupyter/chat';
|
|
7
|
+
import { Delta, DocumentChange, IMapChange, YDocument } from '@jupyter/ydoc';
|
|
8
|
+
import { JSONExt, JSONObject, PartialJSONValue } from '@lumino/coreutils';
|
|
9
|
+
import * as Y from 'yjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The type for a YMessage.
|
|
13
|
+
*/
|
|
14
|
+
export type IYmessage = IChatMessage<string>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The type for a YMessage.
|
|
18
|
+
*/
|
|
19
|
+
export type IMetadata = PartialJSONValue;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Definition of the shared Chat changes.
|
|
23
|
+
*/
|
|
24
|
+
export interface IChatChanges extends DocumentChange {
|
|
25
|
+
/**
|
|
26
|
+
* Changes in messages.
|
|
27
|
+
*/
|
|
28
|
+
messageChanges?: MessageChange;
|
|
29
|
+
/**
|
|
30
|
+
* Changes in users.
|
|
31
|
+
*/
|
|
32
|
+
userChanges?: UserChange[];
|
|
33
|
+
/**
|
|
34
|
+
* Changes in metadata.
|
|
35
|
+
*/
|
|
36
|
+
metadataChanges?: MetadataChange[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The message change type.
|
|
41
|
+
*/
|
|
42
|
+
export type MessageChange = Delta<IYmessage[]>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The user change type.
|
|
46
|
+
*/
|
|
47
|
+
export type UserChange = IMapChange<IUser>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The metadata change type.
|
|
51
|
+
*/
|
|
52
|
+
export type MetadataChange = IMapChange<IMetadata>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The jupyterlab chat shared document.
|
|
56
|
+
*/
|
|
57
|
+
export class YChat extends YDocument<IChatChanges> {
|
|
58
|
+
/**
|
|
59
|
+
* Create a new jupyterlab chat model.
|
|
60
|
+
*/
|
|
61
|
+
constructor(options?: YDocument.IOptions) {
|
|
62
|
+
super(options);
|
|
63
|
+
this._users = this.ydoc.getMap<IUser>('users');
|
|
64
|
+
this._users.observe(this._usersObserver);
|
|
65
|
+
|
|
66
|
+
this._messages = this.ydoc.getArray<IYmessage>('messages');
|
|
67
|
+
this._messages.observe(this._messagesObserver);
|
|
68
|
+
|
|
69
|
+
this._metadata = this.ydoc.getMap<IMetadata>('metadata');
|
|
70
|
+
this._metadata.observe(this._metadataObserver);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Document version
|
|
75
|
+
*/
|
|
76
|
+
readonly version: string = '1.0.0';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Static method to create instances on the sharedModel
|
|
80
|
+
*
|
|
81
|
+
* @returns The sharedModel instance
|
|
82
|
+
*/
|
|
83
|
+
static create(options?: YDocument.IOptions): YChat {
|
|
84
|
+
return new YChat(options);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get id(): string {
|
|
88
|
+
return (this._metadata.get('id') as string) || '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get users(): JSONObject {
|
|
92
|
+
return JSONExt.deepCopy(this._users.toJSON());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get messages(): string[] {
|
|
96
|
+
return JSONExt.deepCopy(this._messages.toJSON());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getUser(username: string | undefined): IUser | undefined {
|
|
100
|
+
if (!username) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this._users.get(username);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setUser(value: IUser): void {
|
|
108
|
+
this.transact(() => {
|
|
109
|
+
this._users.set(value.username, value);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getMessage(index: number): IYmessage | undefined {
|
|
114
|
+
return this._messages.get(index);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
addMessage(value: IYmessage): void {
|
|
118
|
+
this.transact(() => {
|
|
119
|
+
this._messages.push([value]);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateMessage(index: number, value: IYmessage): void {
|
|
124
|
+
this.transact(() => {
|
|
125
|
+
this._messages.delete(index);
|
|
126
|
+
this._messages.insert(index, [value]);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getMessageIndex(id: string): number {
|
|
131
|
+
return this._messages.toArray().findIndex(msg => msg.id === id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
deleteMessage(index: number): void {
|
|
135
|
+
this.transact(() => {
|
|
136
|
+
this._messages.delete(index);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _usersObserver = (event: Y.YMapEvent<IUser>): void => {
|
|
141
|
+
const userChange = new Array<UserChange>();
|
|
142
|
+
event.keysChanged.forEach(key => {
|
|
143
|
+
const change = event.changes.keys.get(key);
|
|
144
|
+
if (change) {
|
|
145
|
+
switch (change.action) {
|
|
146
|
+
case 'add':
|
|
147
|
+
userChange.push({
|
|
148
|
+
key,
|
|
149
|
+
newValue: this._users.get(key),
|
|
150
|
+
type: 'add'
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
case 'delete':
|
|
154
|
+
userChange.push({
|
|
155
|
+
key,
|
|
156
|
+
oldValue: change.oldValue,
|
|
157
|
+
type: 'remove'
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
case 'update':
|
|
161
|
+
userChange.push({
|
|
162
|
+
key: key,
|
|
163
|
+
oldValue: change.oldValue,
|
|
164
|
+
newValue: this._users.get(key),
|
|
165
|
+
type: 'change'
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this._changed.emit({ userChange: userChange } as Partial<IChatChanges>);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
private _messagesObserver = (event: Y.YArrayEvent<IYmessage>): void => {
|
|
176
|
+
const messageChanges = event.delta;
|
|
177
|
+
this._changed.emit({
|
|
178
|
+
messageChanges: messageChanges
|
|
179
|
+
} as Partial<IChatChanges>);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
private _metadataObserver = (event: Y.YMapEvent<IMetadata>): void => {
|
|
183
|
+
const metadataChange = new Array<MetadataChange>();
|
|
184
|
+
event.changes.keys.forEach((change, key) => {
|
|
185
|
+
switch (change.action) {
|
|
186
|
+
case 'add':
|
|
187
|
+
metadataChange.push({
|
|
188
|
+
key,
|
|
189
|
+
newValue: this._metadata.get(key),
|
|
190
|
+
type: 'add'
|
|
191
|
+
});
|
|
192
|
+
break;
|
|
193
|
+
case 'delete':
|
|
194
|
+
metadataChange.push({
|
|
195
|
+
key,
|
|
196
|
+
oldValue: change.oldValue,
|
|
197
|
+
type: 'remove'
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
case 'update':
|
|
201
|
+
metadataChange.push({
|
|
202
|
+
key: key,
|
|
203
|
+
oldValue: change.oldValue,
|
|
204
|
+
newValue: this._metadata.get(key),
|
|
205
|
+
type: 'change'
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this._changed.emit({
|
|
212
|
+
metadataChanges: metadataChange
|
|
213
|
+
} as Partial<IChatChanges>);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
private _users: Y.Map<IUser>;
|
|
217
|
+
private _messages: Y.Array<IYmessage>;
|
|
218
|
+
private _metadata: Y.Map<IMetadata>;
|
|
219
|
+
}
|
package/style/base.css
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
See the JupyterLab Developer Guide for useful CSS Patterns:
|
|
8
|
+
|
|
9
|
+
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@import url('~@jupyter/chat/style/index.css');
|
|
13
|
+
|
|
14
|
+
.jp-lab-chat-main-panel
|
|
15
|
+
.jp-ToolbarButtonComponent[data-command='jupyterlab-chat:moveToSide']
|
|
16
|
+
svg {
|
|
17
|
+
transform: rotate(180deg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.jp-lab-chat-title-unread .lm-TabBar-tabLabel::before {
|
|
21
|
+
content: '* ';
|
|
22
|
+
}
|