@jupyter/chat 0.16.0 → 0.18.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/components/input/chat-input.js +18 -14
- package/lib/components/input/toolbar-registry.d.ts +16 -1
- package/lib/components/input/toolbar-registry.js +10 -1
- package/lib/components/messages/messages.d.ts +1 -0
- package/lib/components/messages/messages.js +1 -1
- package/lib/input-model.d.ts +7 -6
- package/lib/input-model.js +3 -0
- package/lib/model.d.ts +20 -0
- package/lib/model.js +27 -1
- package/lib/widgets/chat-widget.js +17 -2
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +212 -0
- package/lib/widgets/multichat-panel.js +368 -0
- package/package.json +4 -3
- package/src/components/input/chat-input.tsx +27 -26
- package/src/components/input/toolbar-registry.tsx +21 -1
- package/src/components/messages/messages.tsx +1 -1
- package/src/input-model.ts +10 -6
- package/src/model.ts +41 -1
- package/src/widgets/chat-widget.tsx +22 -2
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +575 -0
- package/style/chat.css +1 -8
- package/style/input.css +66 -37
|
@@ -11,7 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { Message } from '@lumino/messaging';
|
|
12
12
|
import { Drag } from '@lumino/dragdrop';
|
|
13
13
|
|
|
14
|
-
import { Chat, IInputToolbarRegistry } from '../components';
|
|
14
|
+
import { Chat, IInputToolbarRegistry, MESSAGE_CLASS } from '../components';
|
|
15
15
|
import { chatIcon } from '../icons';
|
|
16
16
|
import { IChatModel } from '../model';
|
|
17
17
|
import {
|
|
@@ -40,7 +40,27 @@ export class ChatWidget extends ReactWidget {
|
|
|
40
40
|
|
|
41
41
|
this._chatOptions = options;
|
|
42
42
|
this.id = `jupyter-chat::widget::${options.model.name}`;
|
|
43
|
-
this.node.
|
|
43
|
+
this.node.addEventListener('click', (event: MouseEvent) => {
|
|
44
|
+
const target = event.target as HTMLElement;
|
|
45
|
+
if (this.node.contains(document.activeElement)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const message = target.closest(`.${MESSAGE_CLASS}`);
|
|
50
|
+
if (message) {
|
|
51
|
+
const selection = window.getSelection();
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
selection &&
|
|
55
|
+
selection.toString().trim() !== '' &&
|
|
56
|
+
message.contains(selection.anchorNode)
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.model.input.focus();
|
|
63
|
+
});
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
/**
|
package/src/widgets/index.ts
CHANGED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Multi-chat panel for @jupyter/chat
|
|
8
|
+
* Originally adapted from jupyterlab-chat's ChatPanel
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { InputDialog, IThemeManager } from '@jupyterlab/apputils';
|
|
12
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
13
|
+
import {
|
|
14
|
+
addIcon,
|
|
15
|
+
closeIcon,
|
|
16
|
+
HTMLSelect,
|
|
17
|
+
launchIcon,
|
|
18
|
+
PanelWithToolbar,
|
|
19
|
+
ReactWidget,
|
|
20
|
+
SidePanel,
|
|
21
|
+
Spinner,
|
|
22
|
+
ToolbarButton
|
|
23
|
+
} from '@jupyterlab/ui-components';
|
|
24
|
+
import { Debouncer } from '@lumino/polling';
|
|
25
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
26
|
+
import { AccordionPanel, Panel } from '@lumino/widgets';
|
|
27
|
+
import React, { useState } from 'react';
|
|
28
|
+
|
|
29
|
+
import { ChatWidget } from './chat-widget';
|
|
30
|
+
import {
|
|
31
|
+
Chat,
|
|
32
|
+
IInputToolbarRegistry,
|
|
33
|
+
IInputToolbarRegistryFactory
|
|
34
|
+
} from '../components';
|
|
35
|
+
import { chatIcon, readIcon } from '../icons';
|
|
36
|
+
import { IChatModel } from '../model';
|
|
37
|
+
import {
|
|
38
|
+
IAttachmentOpenerRegistry,
|
|
39
|
+
IChatCommandRegistry,
|
|
40
|
+
IMessageFooterRegistry
|
|
41
|
+
} from '../registers';
|
|
42
|
+
|
|
43
|
+
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
44
|
+
const ADD_BUTTON_CLASS = 'jp-chat-add';
|
|
45
|
+
const OPEN_SELECT_CLASS = 'jp-chat-open';
|
|
46
|
+
const SECTION_CLASS = 'jp-chat-section';
|
|
47
|
+
const TOOLBAR_CLASS = 'jp-chat-section-toolbar';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
51
|
+
*/
|
|
52
|
+
export class MultiChatPanel extends SidePanel {
|
|
53
|
+
constructor(options: MultiChatPanel.IOptions) {
|
|
54
|
+
super(options);
|
|
55
|
+
this.id = 'jupyter-chat::multi-chat-panel';
|
|
56
|
+
this.title.icon = chatIcon;
|
|
57
|
+
this.title.caption = 'Jupyter Chat'; // TODO: i18n/
|
|
58
|
+
|
|
59
|
+
this.addClass(SIDEPANEL_CLASS);
|
|
60
|
+
|
|
61
|
+
this._rmRegistry = options.rmRegistry;
|
|
62
|
+
this._themeManager = options.themeManager;
|
|
63
|
+
this._chatCommandRegistry = options.chatCommandRegistry;
|
|
64
|
+
this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
|
|
65
|
+
this._inputToolbarFactory = options.inputToolbarFactory;
|
|
66
|
+
this._messageFooterRegistry = options.messageFooterRegistry;
|
|
67
|
+
this._welcomeMessage = options.welcomeMessage;
|
|
68
|
+
|
|
69
|
+
this._getChatNames = options.getChatNames;
|
|
70
|
+
this._createModel = options.createModel;
|
|
71
|
+
this._openInMain = options.openInMain;
|
|
72
|
+
this._renameChat = options.renameChat;
|
|
73
|
+
|
|
74
|
+
if (this._createModel) {
|
|
75
|
+
// Add chat button calls the createChat callback
|
|
76
|
+
const addChat = new ToolbarButton({
|
|
77
|
+
onClick: async () => {
|
|
78
|
+
const addChatArgs = await this._createModel!();
|
|
79
|
+
this.addChat(addChatArgs);
|
|
80
|
+
},
|
|
81
|
+
icon: addIcon,
|
|
82
|
+
label: 'Chat',
|
|
83
|
+
tooltip: 'Add a new chat'
|
|
84
|
+
});
|
|
85
|
+
addChat.addClass(ADD_BUTTON_CLASS);
|
|
86
|
+
this.toolbar.addItem('createChat', addChat);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this._getChatNames && this._createModel) {
|
|
90
|
+
// Chat select dropdown
|
|
91
|
+
this._openChatWidget = ReactWidget.create(
|
|
92
|
+
<ChatSelect
|
|
93
|
+
chatNamesChanged={this._chatNamesChanged}
|
|
94
|
+
handleChange={this._chatSelected.bind(this)}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
this._openChatWidget.addClass(OPEN_SELECT_CLASS);
|
|
98
|
+
this.toolbar.addItem('openChat', this._openChatWidget);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const content = this.content as AccordionPanel;
|
|
102
|
+
content.expansionToggled.connect(this._onExpansionToggled, this);
|
|
103
|
+
|
|
104
|
+
this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The sections of the side panel.
|
|
109
|
+
*/
|
|
110
|
+
get sections(): ChatSection[] {
|
|
111
|
+
return this.widgets as ChatSection[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A signal emitting when a section is added to the panel.
|
|
116
|
+
*/
|
|
117
|
+
get sectionAdded(): ISignal<MultiChatPanel, ChatSection> {
|
|
118
|
+
return this._sectionAdded;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add a new widget to the chat panel.
|
|
123
|
+
*
|
|
124
|
+
* @param model - the model of the chat widget
|
|
125
|
+
* @param displayName - the name of the chat.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
addChat(args: MultiChatPanel.IAddChatArgs): ChatWidget | undefined {
|
|
129
|
+
const { model, displayName } = args;
|
|
130
|
+
if (!model) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.openIfExists(model.name)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const content = this.content as AccordionPanel;
|
|
139
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
140
|
+
content.collapse(i);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create the toolbar registry.
|
|
144
|
+
let inputToolbarRegistry: IInputToolbarRegistry | undefined;
|
|
145
|
+
if (this._inputToolbarFactory) {
|
|
146
|
+
inputToolbarRegistry = this._inputToolbarFactory.create();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create a new widget.
|
|
150
|
+
const widget = new ChatWidget({
|
|
151
|
+
model,
|
|
152
|
+
rmRegistry: this._rmRegistry,
|
|
153
|
+
themeManager: this._themeManager,
|
|
154
|
+
chatCommandRegistry: this._chatCommandRegistry,
|
|
155
|
+
attachmentOpenerRegistry: this._attachmentOpenerRegistry,
|
|
156
|
+
inputToolbarRegistry,
|
|
157
|
+
messageFooterRegistry: this._messageFooterRegistry,
|
|
158
|
+
welcomeMessage: this._welcomeMessage
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const section = new ChatSection({
|
|
162
|
+
widget,
|
|
163
|
+
openInMain: this._openInMain,
|
|
164
|
+
renameChat: this._renameChat,
|
|
165
|
+
displayName
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.addWidget(section);
|
|
169
|
+
content.expand(this.widgets.length - 1);
|
|
170
|
+
|
|
171
|
+
this._sectionAdded.emit(section);
|
|
172
|
+
return widget;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Invoke the update of the list of available chats.
|
|
177
|
+
*/
|
|
178
|
+
updateChatList() {
|
|
179
|
+
this._updateChatListDebouncer.invoke();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update the list of available chats.
|
|
184
|
+
*/
|
|
185
|
+
private _updateChatList = async (): Promise<void> => {
|
|
186
|
+
try {
|
|
187
|
+
const chatNames = await this._getChatNames?.();
|
|
188
|
+
this._chatNamesChanged.emit(chatNames ?? {});
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error('Error getting chat files', e);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Open a chat if it exists in the side panel.
|
|
196
|
+
*
|
|
197
|
+
* @param name - the name of the chat.
|
|
198
|
+
* @returns a boolean, whether the chat existed in the side panel or not.
|
|
199
|
+
*/
|
|
200
|
+
openIfExists(name: string): boolean {
|
|
201
|
+
const index = this._getChatIndex(name);
|
|
202
|
+
if (index > -1) {
|
|
203
|
+
this._expandChat(index);
|
|
204
|
+
}
|
|
205
|
+
return index > -1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A message handler invoked on an `'after-attach'` message.
|
|
210
|
+
*/
|
|
211
|
+
protected onAfterAttach(): void {
|
|
212
|
+
this._openChatWidget?.renderPromise?.then(() => this.updateChatList());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Return the index of the chat in the list (-1 if not opened).
|
|
217
|
+
*
|
|
218
|
+
* @param name - the chat name.
|
|
219
|
+
*/
|
|
220
|
+
private _getChatIndex(name: string) {
|
|
221
|
+
return this.sections.findIndex(section => section.model?.name === name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Expand the chat from its index.
|
|
226
|
+
*/
|
|
227
|
+
private _expandChat(index: number): void {
|
|
228
|
+
if (!this.widgets[index].isVisible) {
|
|
229
|
+
(this.content as AccordionPanel).expand(index);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle `change` events for the HTMLSelect component.
|
|
235
|
+
*/
|
|
236
|
+
private async _chatSelected(
|
|
237
|
+
event: React.ChangeEvent<HTMLSelectElement>
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const selection = event.target.value;
|
|
240
|
+
if (selection === '-') {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (this._createModel) {
|
|
244
|
+
const addChatArgs = await this._createModel(selection);
|
|
245
|
+
this.addChat(addChatArgs);
|
|
246
|
+
}
|
|
247
|
+
event.target.selectedIndex = 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Triggered when a section is toggled. If the section is opened, all others
|
|
252
|
+
* sections are closed.
|
|
253
|
+
*/
|
|
254
|
+
private _onExpansionToggled(panel: AccordionPanel, index: number) {
|
|
255
|
+
if (!this.widgets[index].isVisible) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
259
|
+
if (i !== index) {
|
|
260
|
+
panel.collapse(i);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private _chatNamesChanged = new Signal<this, { [name: string]: string }>(
|
|
266
|
+
this
|
|
267
|
+
);
|
|
268
|
+
private _sectionAdded = new Signal<MultiChatPanel, ChatSection>(this);
|
|
269
|
+
private _rmRegistry: IRenderMimeRegistry;
|
|
270
|
+
private _themeManager?: IThemeManager | null;
|
|
271
|
+
private _chatCommandRegistry?: IChatCommandRegistry;
|
|
272
|
+
private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
|
|
273
|
+
private _inputToolbarFactory?: IInputToolbarRegistryFactory;
|
|
274
|
+
private _messageFooterRegistry?: IMessageFooterRegistry;
|
|
275
|
+
private _welcomeMessage?: string;
|
|
276
|
+
private _updateChatListDebouncer: Debouncer;
|
|
277
|
+
|
|
278
|
+
private _createModel?: (
|
|
279
|
+
name?: string
|
|
280
|
+
) => Promise<MultiChatPanel.IAddChatArgs>;
|
|
281
|
+
private _getChatNames?: () => Promise<{ [name: string]: string }>;
|
|
282
|
+
private _openInMain?: (name: string) => void;
|
|
283
|
+
private _renameChat?: (oldName: string, newName: string) => Promise<boolean>;
|
|
284
|
+
|
|
285
|
+
private _openChatWidget?: ReactWidget;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The chat panel namespace.
|
|
290
|
+
*/
|
|
291
|
+
export namespace MultiChatPanel {
|
|
292
|
+
/**
|
|
293
|
+
* Options of the constructor of the chat panel.
|
|
294
|
+
*/
|
|
295
|
+
export interface IOptions
|
|
296
|
+
extends SidePanel.IOptions,
|
|
297
|
+
Omit<Chat.IOptions, 'model' | 'inputToolbarRegistry'> {
|
|
298
|
+
/**
|
|
299
|
+
* The input toolbar factory;
|
|
300
|
+
*/
|
|
301
|
+
inputToolbarFactory?: IInputToolbarRegistryFactory;
|
|
302
|
+
/**
|
|
303
|
+
* An optional callback to create a chat model.
|
|
304
|
+
*
|
|
305
|
+
* @param name - the name of the chat, optional.
|
|
306
|
+
* @return an object that can be passed to add a chat section.
|
|
307
|
+
*/
|
|
308
|
+
createModel?: (name?: string) => Promise<IAddChatArgs>;
|
|
309
|
+
/**
|
|
310
|
+
* An optional callback to get the list of existing chats.
|
|
311
|
+
*
|
|
312
|
+
* @returns an object with display name as key and the "full" name as value.
|
|
313
|
+
*/
|
|
314
|
+
getChatNames?: () => Promise<{ [name: string]: string }>;
|
|
315
|
+
/**
|
|
316
|
+
* An optional callback to open the chat in the main area.
|
|
317
|
+
*
|
|
318
|
+
* @param name - the name of the chat to move.
|
|
319
|
+
*/
|
|
320
|
+
openInMain?: (name: string) => void;
|
|
321
|
+
/**
|
|
322
|
+
* An optional callback to rename a chat.
|
|
323
|
+
*
|
|
324
|
+
* @param oldName - the old name of the chat.
|
|
325
|
+
* @param newName - the new name of the chat.
|
|
326
|
+
* @returns - a boolean, whether the chat has been renamed or not.
|
|
327
|
+
*/
|
|
328
|
+
renameChat?: (oldName: string, newName: string) => Promise<boolean>;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The options for the add chat method.
|
|
332
|
+
*/
|
|
333
|
+
export interface IAddChatArgs {
|
|
334
|
+
/**
|
|
335
|
+
* The model of the chat.
|
|
336
|
+
* No-op id undefined.
|
|
337
|
+
*/
|
|
338
|
+
model?: IChatModel;
|
|
339
|
+
/**
|
|
340
|
+
* The display name of the chat in the section title.
|
|
341
|
+
*/
|
|
342
|
+
displayName?: string;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The chat section containing a chat widget.
|
|
348
|
+
*/
|
|
349
|
+
export class ChatSection extends PanelWithToolbar {
|
|
350
|
+
/**
|
|
351
|
+
* Constructor of the chat section.
|
|
352
|
+
*/
|
|
353
|
+
constructor(options: ChatSection.IOptions) {
|
|
354
|
+
super(options);
|
|
355
|
+
this._chatWidget = options.widget;
|
|
356
|
+
this.addWidget(this._chatWidget);
|
|
357
|
+
this.addWidget(this._spinner);
|
|
358
|
+
this.addClass(SECTION_CLASS);
|
|
359
|
+
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
360
|
+
this._displayName =
|
|
361
|
+
options.displayName ?? options.widget.model.name ?? 'Chat';
|
|
362
|
+
this._updateTitle();
|
|
363
|
+
|
|
364
|
+
this._markAsRead = new ToolbarButton({
|
|
365
|
+
icon: readIcon,
|
|
366
|
+
iconLabel: 'Mark chat as read',
|
|
367
|
+
className: 'jp-mod-styled',
|
|
368
|
+
onClick: () => {
|
|
369
|
+
if (this.model) {
|
|
370
|
+
this.model.unreadMessages = [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
this.toolbar.addItem('markRead', this._markAsRead);
|
|
375
|
+
|
|
376
|
+
if (options.renameChat) {
|
|
377
|
+
const renameButton = new ToolbarButton({
|
|
378
|
+
iconClass: 'jp-EditIcon',
|
|
379
|
+
iconLabel: 'Rename chat',
|
|
380
|
+
className: 'jp-mod-styled',
|
|
381
|
+
onClick: async () => {
|
|
382
|
+
const oldName = this.model.name ?? 'Chat';
|
|
383
|
+
const result = await InputDialog.getText({
|
|
384
|
+
title: 'Rename Chat',
|
|
385
|
+
text: this.model.name,
|
|
386
|
+
placeholder: 'new-name'
|
|
387
|
+
});
|
|
388
|
+
if (!result.button.accept) {
|
|
389
|
+
return; // user cancelled
|
|
390
|
+
}
|
|
391
|
+
const newName = result.value;
|
|
392
|
+
if (this.model && newName && newName !== oldName) {
|
|
393
|
+
if (await options.renameChat?.(oldName, newName)) {
|
|
394
|
+
this.model.name = newName;
|
|
395
|
+
this._displayName = newName;
|
|
396
|
+
this._updateTitle();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
this.toolbar.addItem('rename', renameButton);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (options.openInMain) {
|
|
405
|
+
const moveToMain = new ToolbarButton({
|
|
406
|
+
icon: launchIcon,
|
|
407
|
+
iconLabel: 'Move the chat to the main area',
|
|
408
|
+
className: 'jp-mod-styled',
|
|
409
|
+
onClick: () => {
|
|
410
|
+
const name = this.model.name;
|
|
411
|
+
this.model.dispose();
|
|
412
|
+
options.openInMain?.(name);
|
|
413
|
+
this.dispose();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
this.toolbar.addItem('moveMain', moveToMain);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const closeButton = new ToolbarButton({
|
|
420
|
+
icon: closeIcon,
|
|
421
|
+
iconLabel: 'Close the chat',
|
|
422
|
+
className: 'jp-mod-styled',
|
|
423
|
+
onClick: () => {
|
|
424
|
+
this.model.dispose();
|
|
425
|
+
this.dispose();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
this.toolbar.addItem('close', closeButton);
|
|
430
|
+
|
|
431
|
+
this.model.unreadChanged?.connect(this._unreadChanged);
|
|
432
|
+
this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0;
|
|
433
|
+
|
|
434
|
+
options.widget.node.style.height = '100%';
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Remove the spinner when the chat is ready.
|
|
438
|
+
*/
|
|
439
|
+
this.model.ready.then(() => {
|
|
440
|
+
this._spinner.dispose();
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The display name.
|
|
446
|
+
*/
|
|
447
|
+
get displayName(): string {
|
|
448
|
+
return this._displayName;
|
|
449
|
+
}
|
|
450
|
+
set displayName(value: string) {
|
|
451
|
+
this._displayName = value;
|
|
452
|
+
this._updateTitle();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The chat widget of the section.
|
|
457
|
+
*/
|
|
458
|
+
get widget(): ChatWidget {
|
|
459
|
+
return this._chatWidget;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The model of the widget.
|
|
464
|
+
*/
|
|
465
|
+
get model(): IChatModel {
|
|
466
|
+
return this._chatWidget.model;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Dispose of the resources held by the widget.
|
|
471
|
+
*/
|
|
472
|
+
dispose(): void {
|
|
473
|
+
const model = this.model;
|
|
474
|
+
if (model) {
|
|
475
|
+
model.unreadChanged?.disconnect(this._unreadChanged);
|
|
476
|
+
}
|
|
477
|
+
super.dispose();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* * Update the section’s title based on the chat name.
|
|
482
|
+
* */
|
|
483
|
+
|
|
484
|
+
private _updateTitle(): void {
|
|
485
|
+
this.title.label = this._displayName;
|
|
486
|
+
this.title.caption = this._displayName;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Change the title when messages are unread.
|
|
491
|
+
*
|
|
492
|
+
* TODO: fix it upstream in @jupyterlab/ui-components.
|
|
493
|
+
* Updating the title create a new Title widget, but does not attach again the
|
|
494
|
+
* toolbar. The toolbar is attached only when the title widget is attached the first
|
|
495
|
+
* time.
|
|
496
|
+
*/
|
|
497
|
+
private _unreadChanged = (_: IChatModel, unread: number[]) => {
|
|
498
|
+
this._markAsRead.enabled = unread.length > 0;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
private _chatWidget: ChatWidget;
|
|
502
|
+
private _markAsRead: ToolbarButton;
|
|
503
|
+
private _spinner = new Spinner();
|
|
504
|
+
private _displayName: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The chat section namespace.
|
|
509
|
+
*/
|
|
510
|
+
export namespace ChatSection {
|
|
511
|
+
/**
|
|
512
|
+
* Options to build a chat section.
|
|
513
|
+
*/
|
|
514
|
+
export interface IOptions extends Panel.IOptions {
|
|
515
|
+
/**
|
|
516
|
+
* The widget to display in the section.
|
|
517
|
+
*/
|
|
518
|
+
widget: ChatWidget;
|
|
519
|
+
/**
|
|
520
|
+
* An optional callback to open the chat in the main area.
|
|
521
|
+
*
|
|
522
|
+
* @param name - the name of the chat to move.
|
|
523
|
+
*/
|
|
524
|
+
openInMain?: (name: string) => void;
|
|
525
|
+
/**
|
|
526
|
+
* An optional callback to rename a chat.
|
|
527
|
+
*
|
|
528
|
+
* @param oldName - the old name of the chat.
|
|
529
|
+
* @param newName - the new name of the chat.
|
|
530
|
+
* @returns - a boolean, whether the chat has been renamed or not.
|
|
531
|
+
*/
|
|
532
|
+
renameChat?: (oldName: string, newName: string) => Promise<boolean>;
|
|
533
|
+
/**
|
|
534
|
+
* The name to display in the section title.
|
|
535
|
+
*/
|
|
536
|
+
displayName?: string;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
type ChatSelectProps = {
|
|
541
|
+
/**
|
|
542
|
+
* A signal emitting when the list of chat changed.
|
|
543
|
+
*/
|
|
544
|
+
chatNamesChanged: ISignal<MultiChatPanel, { [name: string]: string }>;
|
|
545
|
+
/**
|
|
546
|
+
* The callback to call when the selection changed in the select.
|
|
547
|
+
*/
|
|
548
|
+
handleChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* A component to select a chat from the drive.
|
|
553
|
+
*/
|
|
554
|
+
function ChatSelect({
|
|
555
|
+
chatNamesChanged,
|
|
556
|
+
handleChange
|
|
557
|
+
}: ChatSelectProps): JSX.Element {
|
|
558
|
+
// An object associating a chat name to its path. Both are purely indicative, the name
|
|
559
|
+
// is the section title and the path is used as caption.
|
|
560
|
+
const [chatNames, setChatNames] = useState<{ [name: string]: string }>({});
|
|
561
|
+
|
|
562
|
+
// Update the chat list.
|
|
563
|
+
chatNamesChanged.connect((_, chatNames) => {
|
|
564
|
+
setChatNames(chatNames);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<HTMLSelect onChange={handleChange} value="-">
|
|
569
|
+
<option value="-">Open a chat</option>
|
|
570
|
+
{Object.keys(chatNames).map(name => (
|
|
571
|
+
<option value={chatNames[name]}>{name}</option>
|
|
572
|
+
))}
|
|
573
|
+
</HTMLSelect>
|
|
574
|
+
);
|
|
575
|
+
}
|
package/style/chat.css
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/*
|
|
22
|
-
*
|
|
23
22
|
* Selectors must be nested in `.jp-ThemedContainer` to have a higher
|
|
24
23
|
* specificity than selectors in rules provided by JupyterLab.
|
|
25
24
|
*
|
|
@@ -147,7 +146,6 @@
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
/* Keyframe animations */
|
|
150
|
-
|
|
151
149
|
@keyframes jp-chat-typing-bounce {
|
|
152
150
|
0%,
|
|
153
151
|
80%,
|
|
@@ -204,12 +202,7 @@
|
|
|
204
202
|
gap: 4px;
|
|
205
203
|
flex-wrap: wrap;
|
|
206
204
|
min-height: 1.5em;
|
|
207
|
-
padding:
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.jp-chat-attachments:empty {
|
|
211
|
-
padding: 8px 0;
|
|
212
|
-
min-height: 1.5em;
|
|
205
|
+
padding: 4px 0;
|
|
213
206
|
}
|
|
214
207
|
|
|
215
208
|
.jp-chat-attachment {
|