@jupyter/chat 0.20.0-alpha.2 → 0.20.0-alpha.3
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/widgets/chat-selector-popup.d.ts +102 -0
- package/lib/widgets/chat-selector-popup.js +293 -0
- package/lib/widgets/chat-widget.js +1 -0
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +100 -73
- package/lib/widgets/multichat-panel.js +356 -159
- package/package.json +1 -1
- package/src/widgets/chat-selector-popup.tsx +440 -0
- package/src/widgets/chat-widget.tsx +1 -0
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +434 -207
- package/style/base.css +1 -0
- package/style/chat-selector.css +161 -0
- package/style/chat.css +34 -2
|
@@ -12,20 +12,21 @@ import { InputDialog } from '@jupyterlab/apputils';
|
|
|
12
12
|
import {
|
|
13
13
|
addIcon,
|
|
14
14
|
closeIcon,
|
|
15
|
-
HTMLSelect,
|
|
16
15
|
launchIcon,
|
|
17
16
|
PanelWithToolbar,
|
|
18
17
|
ReactWidget,
|
|
19
|
-
SidePanel,
|
|
20
18
|
Spinner,
|
|
21
19
|
ToolbarButton
|
|
22
20
|
} from '@jupyterlab/ui-components';
|
|
21
|
+
import { ArrayExt } from '@lumino/algorithm';
|
|
22
|
+
import { Message } from '@lumino/messaging';
|
|
23
23
|
import { Debouncer } from '@lumino/polling';
|
|
24
24
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
25
|
-
import {
|
|
26
|
-
import React, { useState } from 'react';
|
|
25
|
+
import { Widget } from '@lumino/widgets';
|
|
26
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
27
27
|
|
|
28
28
|
import { ChatWidget } from './chat-widget';
|
|
29
|
+
import { ChatSelectorPopup } from './chat-selector-popup';
|
|
29
30
|
import {
|
|
30
31
|
Chat,
|
|
31
32
|
IInputToolbarRegistry,
|
|
@@ -37,13 +38,16 @@ import { IChatModel } from '../model';
|
|
|
37
38
|
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
38
39
|
const ADD_BUTTON_CLASS = 'jp-chat-add';
|
|
39
40
|
const OPEN_SELECT_CLASS = 'jp-chat-open';
|
|
40
|
-
const
|
|
41
|
-
const TOOLBAR_CLASS = 'jp-chat-
|
|
41
|
+
const SIDEPANEL_WIDGET_CLASS = 'jp-chat-sidepanel-widget';
|
|
42
|
+
const TOOLBAR_CLASS = 'jp-chat-sidepanel-widget-toolbar';
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
45
46
|
*/
|
|
46
|
-
export class MultiChatPanel extends
|
|
47
|
+
export class MultiChatPanel extends PanelWithToolbar {
|
|
48
|
+
/**
|
|
49
|
+
* The constructor of the multichat panel.
|
|
50
|
+
*/
|
|
47
51
|
constructor(options: MultiChatPanel.IOptions) {
|
|
48
52
|
super(options);
|
|
49
53
|
this.id = 'jupyter-chat::multi-chat-panel';
|
|
@@ -65,68 +69,147 @@ export class MultiChatPanel extends SidePanel {
|
|
|
65
69
|
const addChat = new ToolbarButton({
|
|
66
70
|
onClick: async () => {
|
|
67
71
|
const addChatArgs = await this._createModel!();
|
|
68
|
-
this.
|
|
72
|
+
this.open(addChatArgs);
|
|
69
73
|
},
|
|
70
74
|
icon: addIcon,
|
|
71
|
-
|
|
72
|
-
tooltip: 'Add a new chat'
|
|
75
|
+
tooltip: 'Create a new chat'
|
|
73
76
|
});
|
|
74
77
|
addChat.addClass(ADD_BUTTON_CLASS);
|
|
75
78
|
this.toolbar.addItem('createChat', addChat);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
if (this._getChatNames && this._createModel) {
|
|
79
|
-
// Chat
|
|
82
|
+
// Chat selector with search input
|
|
80
83
|
this._openChatWidget = ReactWidget.create(
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
<ChatSearchInput
|
|
85
|
+
selectChat={this._onSelectChat}
|
|
86
|
+
getPopup={() => this._chatSelectorPopup}
|
|
87
|
+
chatOpened={this._chatOpened}
|
|
84
88
|
/>
|
|
85
89
|
);
|
|
86
90
|
this._openChatWidget.addClass(OPEN_SELECT_CLASS);
|
|
87
91
|
this.toolbar.addItem('openChat', this._openChatWidget);
|
|
88
|
-
}
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
// Create the popup widget (attached to document body)
|
|
94
|
+
this._chatSelectorPopup = new ChatSelectorPopup({
|
|
95
|
+
chatNames: [],
|
|
96
|
+
onSelect: this._onSelectChat,
|
|
97
|
+
onClose: (name: string) => {
|
|
98
|
+
this.disposeLoadedModel(name);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
92
102
|
|
|
103
|
+
// Insert the toolbar as first child.
|
|
104
|
+
this.insertWidget(0, this.toolbar);
|
|
93
105
|
this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/**
|
|
97
|
-
* The
|
|
109
|
+
* The currently displayed chat widget.
|
|
98
110
|
*/
|
|
99
|
-
get
|
|
100
|
-
return this.
|
|
111
|
+
get current(): SidePanelWidget | undefined {
|
|
112
|
+
return this._currentWidget;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
/**
|
|
104
|
-
* A signal emitting when a
|
|
116
|
+
* A signal emitting when a chat widget is opened in the panel.
|
|
105
117
|
*/
|
|
106
|
-
get
|
|
107
|
-
return this.
|
|
118
|
+
get chatOpened(): ISignal<MultiChatPanel, ChatWidget> {
|
|
119
|
+
return this._chatOpened;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @param model - the model of the chat widget
|
|
114
|
-
* @param displayName - the name of the chat.
|
|
123
|
+
* A signal emitting when the panel visibility changed.
|
|
115
124
|
*/
|
|
125
|
+
get visibilityChanged(): ISignal<MultiChatPanel, boolean> {
|
|
126
|
+
return this._visibilityChanged;
|
|
127
|
+
}
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Add a chat to the panel by creating or showing its widget.
|
|
131
|
+
*
|
|
132
|
+
* @param args - the chat args including model and display name.
|
|
133
|
+
*/
|
|
134
|
+
open(args: MultiChatPanel.IOpenChatArgs): ChatWidget | undefined {
|
|
135
|
+
const { model } = args;
|
|
119
136
|
if (!model) {
|
|
120
137
|
return;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
|
|
140
|
+
const displayName = args.displayName ?? model.name;
|
|
141
|
+
|
|
142
|
+
// Add model to loaded models
|
|
143
|
+
if (!this._loadedModels.has(displayName)) {
|
|
144
|
+
this._loadedModels.set(displayName, model);
|
|
145
|
+
this._chatSelectorPopup?.setLoadedModels(this.getLoadedModelNames());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Open this chat (will create widget)
|
|
149
|
+
return this._open(displayName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get a loaded model by name, or undefined if not loaded.
|
|
154
|
+
*/
|
|
155
|
+
getLoadedModel(name: string): IChatModel | undefined {
|
|
156
|
+
return this._loadedModels.get(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get all loaded model names.
|
|
161
|
+
*/
|
|
162
|
+
getLoadedModelNames(): string[] {
|
|
163
|
+
return Array.from(this._loadedModels.keys());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Dispose a model, removing it from loaded models.
|
|
168
|
+
*/
|
|
169
|
+
disposeLoadedModel(name: string): void {
|
|
170
|
+
const model = this._loadedModels.get(name);
|
|
171
|
+
if (model) {
|
|
172
|
+
// If this is the currently displayed chat, remove it.
|
|
173
|
+
if (this._currentWidget?.model === model) {
|
|
174
|
+
this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
|
|
175
|
+
this._currentWidget.dispose();
|
|
176
|
+
this._currentWidget = undefined;
|
|
177
|
+
|
|
178
|
+
// Clear current chat in selector
|
|
179
|
+
if (this._chatSelectorPopup) {
|
|
180
|
+
this._chatSelectorPopup.setCurrentChat(null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
model.dispose();
|
|
185
|
+
this._loadedModels.delete(name);
|
|
186
|
+
this._chatSelectorPopup?.setLoadedModels(this.getLoadedModelNames());
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Emit a signal when the panel visibility changed.
|
|
192
|
+
*/
|
|
193
|
+
protected onAfterShow(msg: Message): void {
|
|
194
|
+
this._visibilityChanged.emit(true);
|
|
195
|
+
}
|
|
196
|
+
protected onBeforeHide(msg: Message): void {
|
|
197
|
+
this._visibilityChanged.emit(false);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Open a specific chat by name, creating a new sidepanel widget.
|
|
202
|
+
*/
|
|
203
|
+
private _open(name: string): ChatWidget | undefined {
|
|
204
|
+
const model = this._loadedModels.get(name);
|
|
205
|
+
if (!model) {
|
|
124
206
|
return;
|
|
125
207
|
}
|
|
126
208
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
209
|
+
// Dispose current chat widget if any
|
|
210
|
+
if (this._currentWidget) {
|
|
211
|
+
this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
|
|
212
|
+
this._currentWidget.dispose();
|
|
130
213
|
}
|
|
131
214
|
|
|
132
215
|
// Create the toolbar registry.
|
|
@@ -135,34 +218,47 @@ export class MultiChatPanel extends SidePanel {
|
|
|
135
218
|
inputToolbarRegistry = this._inputToolbarFactory.create();
|
|
136
219
|
}
|
|
137
220
|
|
|
138
|
-
// Create a new widget
|
|
139
|
-
const
|
|
221
|
+
// Create a new widget for this model
|
|
222
|
+
const chatWidget = new ChatWidget({
|
|
140
223
|
model,
|
|
141
224
|
...this._chatOptions,
|
|
142
225
|
inputToolbarRegistry,
|
|
143
226
|
area: 'sidebar'
|
|
144
227
|
});
|
|
145
228
|
|
|
146
|
-
|
|
147
|
-
|
|
229
|
+
// Create a chat with toolbar
|
|
230
|
+
const widget = new SidePanelWidget({
|
|
231
|
+
widget: chatWidget,
|
|
232
|
+
displayName: name,
|
|
148
233
|
openInMain: this._openInMain,
|
|
149
234
|
renameChat: this._renameChat,
|
|
150
|
-
|
|
235
|
+
onClose: (name: string) => {
|
|
236
|
+
this.disposeLoadedModel(name);
|
|
237
|
+
}
|
|
151
238
|
});
|
|
152
239
|
|
|
153
|
-
|
|
154
|
-
|
|
240
|
+
// Add to content panel
|
|
241
|
+
this.addWidget(widget);
|
|
242
|
+
this.update();
|
|
243
|
+
this._currentWidget = widget;
|
|
155
244
|
|
|
156
|
-
this.
|
|
157
|
-
|
|
245
|
+
this._currentWidget.nameChanged.connect(this._modelNameChanged);
|
|
246
|
+
|
|
247
|
+
// Update selector to show current chat
|
|
248
|
+
if (this._chatSelectorPopup) {
|
|
249
|
+
this._chatSelectorPopup.setCurrentChat(name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._chatOpened.emit(chatWidget);
|
|
253
|
+
return chatWidget;
|
|
158
254
|
}
|
|
159
255
|
|
|
160
256
|
/**
|
|
161
257
|
* Invoke the update of the list of available chats.
|
|
162
258
|
*/
|
|
163
|
-
updateChatList() {
|
|
259
|
+
updateChatList = (): void => {
|
|
164
260
|
this._updateChatListDebouncer.invoke();
|
|
165
|
-
}
|
|
261
|
+
};
|
|
166
262
|
|
|
167
263
|
/**
|
|
168
264
|
* Update the list of available chats.
|
|
@@ -170,24 +266,33 @@ export class MultiChatPanel extends SidePanel {
|
|
|
170
266
|
private _updateChatList = async (): Promise<void> => {
|
|
171
267
|
try {
|
|
172
268
|
const chatNames = await this._getChatNames?.();
|
|
173
|
-
|
|
269
|
+
if (
|
|
270
|
+
!ArrayExt.shallowEqual(
|
|
271
|
+
Object.keys(chatNames ?? {}),
|
|
272
|
+
Object.keys(this._chatNames)
|
|
273
|
+
)
|
|
274
|
+
) {
|
|
275
|
+
this._chatNames = chatNames ?? {};
|
|
276
|
+
this._chatSelectorPopup?.updateChats(Object.keys(this._chatNames));
|
|
277
|
+
}
|
|
174
278
|
} catch (e) {
|
|
175
279
|
console.error('Error getting chat files', e);
|
|
176
280
|
}
|
|
177
281
|
};
|
|
178
282
|
|
|
179
283
|
/**
|
|
180
|
-
* Open a chat if
|
|
284
|
+
* Open a chat if its model is already loaded.
|
|
181
285
|
*
|
|
182
286
|
* @param name - the name of the chat.
|
|
183
|
-
* @returns a boolean, whether the chat
|
|
287
|
+
* @returns a boolean, whether the chat model was already loaded or not.
|
|
184
288
|
*/
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
this.
|
|
289
|
+
openIfLoaded(name: string): boolean {
|
|
290
|
+
const model = this._loadedModels.get(name);
|
|
291
|
+
if (model) {
|
|
292
|
+
this._open(name);
|
|
293
|
+
return true;
|
|
189
294
|
}
|
|
190
|
-
return
|
|
295
|
+
return false;
|
|
191
296
|
}
|
|
192
297
|
|
|
193
298
|
/**
|
|
@@ -195,74 +300,95 @@ export class MultiChatPanel extends SidePanel {
|
|
|
195
300
|
*/
|
|
196
301
|
protected onAfterAttach(): void {
|
|
197
302
|
this._openChatWidget?.renderPromise?.then(() => this.updateChatList());
|
|
303
|
+
|
|
304
|
+
// Attach the popup to the document body
|
|
305
|
+
if (this._chatSelectorPopup && !this._chatSelectorPopup.isAttached) {
|
|
306
|
+
Widget.attach(this._chatSelectorPopup, document.body);
|
|
307
|
+
}
|
|
198
308
|
}
|
|
199
309
|
|
|
200
310
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* @param name - the chat name.
|
|
311
|
+
* A message handler invoked on an `'before-detach'` message.
|
|
204
312
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
313
|
+
protected onBeforeDetach(): void {
|
|
314
|
+
// Detach the popup
|
|
315
|
+
if (this._chatSelectorPopup && this._chatSelectorPopup.isAttached) {
|
|
316
|
+
Widget.detach(this._chatSelectorPopup);
|
|
317
|
+
}
|
|
207
318
|
}
|
|
208
319
|
|
|
209
320
|
/**
|
|
210
|
-
*
|
|
321
|
+
* Dispose of the resources held by the widget.
|
|
211
322
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
323
|
+
dispose(): void {
|
|
324
|
+
// Dispose all loaded models
|
|
325
|
+
for (const model of this._loadedModels.values()) {
|
|
326
|
+
model.dispose();
|
|
215
327
|
}
|
|
328
|
+
this._loadedModels.clear();
|
|
329
|
+
|
|
330
|
+
if (this._chatSelectorPopup) {
|
|
331
|
+
this._chatSelectorPopup.dispose();
|
|
332
|
+
this._chatSelectorPopup = undefined;
|
|
333
|
+
}
|
|
334
|
+
super.dispose();
|
|
216
335
|
}
|
|
217
336
|
|
|
218
337
|
/**
|
|
219
|
-
*
|
|
338
|
+
* Update loaded model when the current widget updates its name.
|
|
220
339
|
*/
|
|
221
|
-
private
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
340
|
+
private _modelNameChanged = (
|
|
341
|
+
_: SidePanelWidget,
|
|
342
|
+
change: { old: string; new: string }
|
|
343
|
+
) => {
|
|
344
|
+
const model = this.getLoadedModel(change.old);
|
|
345
|
+
if (model) {
|
|
346
|
+
this._loadedModels.set(change.new, model);
|
|
347
|
+
this._loadedModels.delete(change.old);
|
|
348
|
+
this._chatSelectorPopup?.setLoadedModels(this.getLoadedModelNames());
|
|
349
|
+
if (this._currentWidget?.model.name === model.name) {
|
|
350
|
+
this._chatSelectorPopup?.setCurrentChat(change.new);
|
|
351
|
+
}
|
|
231
352
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
353
|
+
};
|
|
234
354
|
|
|
235
355
|
/**
|
|
236
|
-
*
|
|
237
|
-
* sections are closed.
|
|
356
|
+
* Handle chat selection from the popup.
|
|
238
357
|
*/
|
|
239
|
-
private
|
|
240
|
-
if
|
|
241
|
-
|
|
358
|
+
private _onSelectChat = async (name: string): Promise<void> => {
|
|
359
|
+
// Check if model is already loaded
|
|
360
|
+
let openChatArgs: MultiChatPanel.IOpenChatArgs = {
|
|
361
|
+
model: this.getLoadedModel(name),
|
|
362
|
+
displayName: name
|
|
363
|
+
};
|
|
364
|
+
// If not, create the model.
|
|
365
|
+
if (!openChatArgs.model && this._createModel) {
|
|
366
|
+
const chatID = this._chatNames[name];
|
|
367
|
+
openChatArgs = await this._createModel(chatID);
|
|
242
368
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
panel.collapse(i);
|
|
246
|
-
}
|
|
369
|
+
if (openChatArgs.model) {
|
|
370
|
+
this.open(openChatArgs);
|
|
247
371
|
}
|
|
248
|
-
|
|
372
|
+
this._chatSelectorPopup?.hide();
|
|
373
|
+
};
|
|
249
374
|
|
|
250
|
-
private
|
|
251
|
-
this
|
|
252
|
-
);
|
|
253
|
-
private _sectionAdded = new Signal<MultiChatPanel, ChatSection>(this);
|
|
375
|
+
private _chatOpened = new Signal<MultiChatPanel, ChatWidget>(this);
|
|
254
376
|
private _chatOptions: Omit<Chat.IOptions, 'model' | 'inputToolbarRegistry'>;
|
|
255
377
|
private _inputToolbarFactory?: IInputToolbarRegistryFactory;
|
|
256
378
|
private _updateChatListDebouncer: Debouncer;
|
|
257
379
|
|
|
258
380
|
private _createModel?: (
|
|
259
381
|
name?: string
|
|
260
|
-
) => Promise<MultiChatPanel.
|
|
382
|
+
) => Promise<MultiChatPanel.IOpenChatArgs>;
|
|
261
383
|
private _getChatNames?: () => Promise<{ [name: string]: string }>;
|
|
262
384
|
private _openInMain?: (name: string) => Promise<boolean>;
|
|
263
|
-
private _renameChat?: (oldName: string
|
|
264
|
-
|
|
385
|
+
private _renameChat?: boolean | ((oldName: string) => Promise<string | null>);
|
|
265
386
|
private _openChatWidget?: ReactWidget;
|
|
387
|
+
private _chatSelectorPopup?: ChatSelectorPopup;
|
|
388
|
+
private _loadedModels: Map<string, IChatModel> = new Map();
|
|
389
|
+
private _currentWidget?: SidePanelWidget;
|
|
390
|
+
private _chatNames: { [name: string]: string } = {};
|
|
391
|
+
private _visibilityChanged = new Signal<MultiChatPanel, boolean>(this);
|
|
266
392
|
}
|
|
267
393
|
|
|
268
394
|
/**
|
|
@@ -273,7 +399,7 @@ export namespace MultiChatPanel {
|
|
|
273
399
|
* Options of the constructor of the chat panel.
|
|
274
400
|
*/
|
|
275
401
|
export interface IOptions
|
|
276
|
-
extends
|
|
402
|
+
extends PanelWithToolbar.IOptions,
|
|
277
403
|
Omit<Chat.IOptions, 'model' | 'inputToolbarRegistry'> {
|
|
278
404
|
/**
|
|
279
405
|
* The input toolbar factory;
|
|
@@ -283,13 +409,13 @@ export namespace MultiChatPanel {
|
|
|
283
409
|
* An optional callback to create a chat model.
|
|
284
410
|
*
|
|
285
411
|
* @param name - the name of the chat, optional.
|
|
286
|
-
* @return an object that can be passed to
|
|
412
|
+
* @return an object that can be passed to open the chat.
|
|
287
413
|
*/
|
|
288
|
-
createModel?: (name?: string) => Promise<
|
|
414
|
+
createModel?: (name?: string) => Promise<IOpenChatArgs>;
|
|
289
415
|
/**
|
|
290
416
|
* An optional callback to get the list of existing chats.
|
|
291
417
|
*
|
|
292
|
-
* @returns an object
|
|
418
|
+
* @returns an object mapping chat display names to identifiers.
|
|
293
419
|
*/
|
|
294
420
|
getChatNames?: () => Promise<{ [name: string]: string }>;
|
|
295
421
|
/**
|
|
@@ -305,42 +431,50 @@ export namespace MultiChatPanel {
|
|
|
305
431
|
* @param newName - the new name of the chat.
|
|
306
432
|
* @returns - a boolean, whether the chat has been renamed or not.
|
|
307
433
|
*/
|
|
308
|
-
renameChat?: (oldName: string
|
|
434
|
+
renameChat?: boolean | ((oldName: string) => Promise<string | null>);
|
|
309
435
|
}
|
|
310
436
|
/**
|
|
311
437
|
* The options for the add chat method.
|
|
312
438
|
*/
|
|
313
|
-
export interface
|
|
439
|
+
export interface IOpenChatArgs {
|
|
314
440
|
/**
|
|
315
441
|
* The model of the chat.
|
|
316
|
-
* No-op
|
|
442
|
+
* No-op if undefined.
|
|
317
443
|
*/
|
|
318
444
|
model?: IChatModel;
|
|
319
445
|
/**
|
|
320
|
-
* The display name of the chat in the
|
|
446
|
+
* The display name of the chat, shown in the toolbar.
|
|
321
447
|
*/
|
|
322
448
|
displayName?: string;
|
|
323
449
|
}
|
|
324
450
|
}
|
|
325
451
|
|
|
326
452
|
/**
|
|
327
|
-
*
|
|
453
|
+
* A widget containing the chat and its toolbar.
|
|
328
454
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*/
|
|
333
|
-
constructor(options: ChatSection.IOptions) {
|
|
334
|
-
super(options);
|
|
455
|
+
class SidePanelWidget extends PanelWithToolbar {
|
|
456
|
+
constructor(options: SidePanelWidget.IOptions) {
|
|
457
|
+
super();
|
|
335
458
|
this._chatWidget = options.widget;
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
this.addClass(
|
|
459
|
+
this._displayName = options.displayName ?? options.widget.model.name;
|
|
460
|
+
|
|
461
|
+
this.addClass(SIDEPANEL_WIDGET_CLASS);
|
|
339
462
|
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
340
|
-
this._displayName =
|
|
341
|
-
options.displayName ?? options.widget.model.name ?? 'Chat';
|
|
342
463
|
this._updateTitle();
|
|
343
464
|
|
|
465
|
+
this.addWidget(this.toolbar);
|
|
466
|
+
|
|
467
|
+
// Add spinner while loading
|
|
468
|
+
const spinner = new Spinner();
|
|
469
|
+
this.addWidget(spinner);
|
|
470
|
+
this._chatWidget.model.ready.then(() => {
|
|
471
|
+
spinner.dispose();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Add the chat widget
|
|
475
|
+
this.addWidget(this._chatWidget);
|
|
476
|
+
|
|
477
|
+
// Add toolbar buttons
|
|
344
478
|
this._markAsRead = new ToolbarButton({
|
|
345
479
|
icon: readIcon,
|
|
346
480
|
iconLabel: 'Mark chat as read',
|
|
@@ -359,22 +493,30 @@ export class ChatSection extends PanelWithToolbar {
|
|
|
359
493
|
iconLabel: 'Rename chat',
|
|
360
494
|
className: 'jp-mod-styled',
|
|
361
495
|
onClick: async () => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
title: 'Rename Chat',
|
|
365
|
-
text: this.model.name,
|
|
366
|
-
placeholder: 'new-name'
|
|
367
|
-
});
|
|
368
|
-
if (!result.button.accept) {
|
|
369
|
-
return; // user cancelled
|
|
496
|
+
if (!options.renameChat) {
|
|
497
|
+
return;
|
|
370
498
|
}
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
499
|
+
let newName: string | null;
|
|
500
|
+
if (options.renameChat === true) {
|
|
501
|
+
// If rename chat is true, let's provide a input to select new name.
|
|
502
|
+
const result = await InputDialog.getText({
|
|
503
|
+
title: 'Rename Chat',
|
|
504
|
+
text: this.model.name,
|
|
505
|
+
placeholder: 'new-name'
|
|
506
|
+
});
|
|
507
|
+
if (!result.button.accept && result.value) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
newName = result.value;
|
|
511
|
+
if (newName) {
|
|
374
512
|
this.model.name = newName;
|
|
375
|
-
this._displayName = newName;
|
|
376
|
-
this._updateTitle();
|
|
377
513
|
}
|
|
514
|
+
} else {
|
|
515
|
+
// Rename chat is a function, let's call it.
|
|
516
|
+
newName = await options.renameChat(this.model.name);
|
|
517
|
+
}
|
|
518
|
+
if (newName) {
|
|
519
|
+
this.name = newName;
|
|
378
520
|
}
|
|
379
521
|
}
|
|
380
522
|
});
|
|
@@ -389,8 +531,7 @@ export class ChatSection extends PanelWithToolbar {
|
|
|
389
531
|
onClick: async () => {
|
|
390
532
|
const name = this.model.name;
|
|
391
533
|
if (await options.openInMain?.(name)) {
|
|
392
|
-
this.
|
|
393
|
-
this.dispose();
|
|
534
|
+
options.onClose(this._displayName);
|
|
394
535
|
}
|
|
395
536
|
}
|
|
396
537
|
});
|
|
@@ -402,49 +543,51 @@ export class ChatSection extends PanelWithToolbar {
|
|
|
402
543
|
iconLabel: 'Close the chat',
|
|
403
544
|
className: 'jp-mod-styled',
|
|
404
545
|
onClick: () => {
|
|
405
|
-
this.
|
|
406
|
-
this.dispose();
|
|
546
|
+
options.onClose(this._displayName);
|
|
407
547
|
}
|
|
408
548
|
});
|
|
409
|
-
|
|
410
549
|
this.toolbar.addItem('close', closeButton);
|
|
411
550
|
|
|
551
|
+
// Update mark as read button state
|
|
412
552
|
this.model.unreadChanged?.connect(this._unreadChanged);
|
|
413
553
|
this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0;
|
|
554
|
+
}
|
|
414
555
|
|
|
415
|
-
|
|
556
|
+
/**
|
|
557
|
+
* The chat widget embedded in the sidepanel widget.
|
|
558
|
+
*/
|
|
559
|
+
get widget(): ChatWidget {
|
|
560
|
+
return this._chatWidget;
|
|
561
|
+
}
|
|
416
562
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
});
|
|
563
|
+
/**
|
|
564
|
+
* The model of the widget.
|
|
565
|
+
*/
|
|
566
|
+
get model(): IChatModel {
|
|
567
|
+
return this._chatWidget.model;
|
|
423
568
|
}
|
|
424
569
|
|
|
425
570
|
/**
|
|
426
|
-
* The
|
|
571
|
+
* The displayed name of the widget.
|
|
427
572
|
*/
|
|
428
|
-
get
|
|
573
|
+
get name(): string {
|
|
429
574
|
return this._displayName;
|
|
430
575
|
}
|
|
431
|
-
set
|
|
576
|
+
set name(value: string) {
|
|
577
|
+
const old = this._displayName;
|
|
578
|
+
if (old === value) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
432
581
|
this._displayName = value;
|
|
433
582
|
this._updateTitle();
|
|
583
|
+
this._nameChanged.emit({ old, new: value });
|
|
434
584
|
}
|
|
435
585
|
|
|
436
586
|
/**
|
|
437
|
-
*
|
|
438
|
-
*/
|
|
439
|
-
get widget(): ChatWidget {
|
|
440
|
-
return this._chatWidget;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* The model of the widget.
|
|
587
|
+
* A signal emitting when the name has changed.
|
|
445
588
|
*/
|
|
446
|
-
get
|
|
447
|
-
return this.
|
|
589
|
+
get nameChanged(): ISignal<SidePanelWidget, { old: string; new: string }> {
|
|
590
|
+
return this._nameChanged;
|
|
448
591
|
}
|
|
449
592
|
|
|
450
593
|
/**
|
|
@@ -459,21 +602,29 @@ export class ChatSection extends PanelWithToolbar {
|
|
|
459
602
|
}
|
|
460
603
|
|
|
461
604
|
/**
|
|
462
|
-
*
|
|
463
|
-
|
|
464
|
-
|
|
605
|
+
* Update the title based on the chat name.
|
|
606
|
+
*/
|
|
465
607
|
private _updateTitle(): void {
|
|
466
|
-
this.title.label = this.
|
|
608
|
+
this.title.label = this.model.name;
|
|
467
609
|
this.title.caption = this._displayName;
|
|
610
|
+
|
|
611
|
+
const titleElement = document.createElement('span');
|
|
612
|
+
titleElement.classList.add('jp-chat-sidepanel-widget-title');
|
|
613
|
+
titleElement.title = this._displayName;
|
|
614
|
+
titleElement.textContent = this._displayName;
|
|
615
|
+
|
|
616
|
+
// Dispose of the previous widget.
|
|
617
|
+
if (this._titleWidget) {
|
|
618
|
+
this._titleWidget.dispose();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Insert the new title widget in toolbar.
|
|
622
|
+
this._titleWidget = new Widget({ node: titleElement });
|
|
623
|
+
this.toolbar.insertItem(0, 'title', this._titleWidget);
|
|
468
624
|
}
|
|
469
625
|
|
|
470
626
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* TODO: fix it upstream in @jupyterlab/ui-components.
|
|
474
|
-
* Updating the title create a new Title widget, but does not attach again the
|
|
475
|
-
* toolbar. The toolbar is attached only when the title widget is attached the first
|
|
476
|
-
* time.
|
|
627
|
+
* Enable/disable unread icon.
|
|
477
628
|
*/
|
|
478
629
|
private _unreadChanged = (_: IChatModel, unread: number[]) => {
|
|
479
630
|
this._markAsRead.enabled = unread.length > 0;
|
|
@@ -481,82 +632,158 @@ export class ChatSection extends PanelWithToolbar {
|
|
|
481
632
|
|
|
482
633
|
private _chatWidget: ChatWidget;
|
|
483
634
|
private _markAsRead: ToolbarButton;
|
|
484
|
-
private _spinner = new Spinner();
|
|
485
635
|
private _displayName: string;
|
|
636
|
+
private _titleWidget: Widget | undefined;
|
|
637
|
+
private _nameChanged = new Signal<
|
|
638
|
+
SidePanelWidget,
|
|
639
|
+
{ old: string; new: string }
|
|
640
|
+
>(this);
|
|
486
641
|
}
|
|
487
642
|
|
|
488
643
|
/**
|
|
489
|
-
* The
|
|
644
|
+
* The sidepanel widget namespace.
|
|
490
645
|
*/
|
|
491
|
-
|
|
646
|
+
namespace SidePanelWidget {
|
|
492
647
|
/**
|
|
493
|
-
*
|
|
648
|
+
* The sidepanel widget constructor options.
|
|
494
649
|
*/
|
|
495
|
-
export interface IOptions
|
|
650
|
+
export interface IOptions {
|
|
496
651
|
/**
|
|
497
|
-
* The widget
|
|
652
|
+
* The chat widget.
|
|
498
653
|
*/
|
|
499
654
|
widget: ChatWidget;
|
|
500
655
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* @param name - the name of the chat to move.
|
|
656
|
+
* The callback when closing the chat.
|
|
504
657
|
*/
|
|
505
|
-
|
|
658
|
+
onClose: (name: string) => void;
|
|
506
659
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* @param oldName - the old name of the chat.
|
|
510
|
-
* @param newName - the new name of the chat.
|
|
511
|
-
* @returns - a boolean, whether the chat has been renamed or not.
|
|
660
|
+
* The displayed name of the chat.
|
|
512
661
|
*/
|
|
513
|
-
|
|
662
|
+
displayName?: string;
|
|
514
663
|
/**
|
|
515
|
-
* The
|
|
664
|
+
* The callback to open the chat in main area.
|
|
516
665
|
*/
|
|
517
|
-
|
|
666
|
+
openInMain?: (name: string) => Promise<boolean>;
|
|
667
|
+
/**
|
|
668
|
+
* The callback to rename the chat.
|
|
669
|
+
*/
|
|
670
|
+
renameChat?: boolean | ((oldName: string) => Promise<string | null>);
|
|
518
671
|
}
|
|
519
672
|
}
|
|
520
673
|
|
|
521
|
-
type
|
|
674
|
+
type ChatSearchInputProps = {
|
|
522
675
|
/**
|
|
523
|
-
*
|
|
676
|
+
* The callback to call when a chat is selected.
|
|
524
677
|
*/
|
|
525
|
-
|
|
678
|
+
selectChat: (value: string) => void;
|
|
526
679
|
/**
|
|
527
|
-
*
|
|
680
|
+
* Function to get the popup widget.
|
|
528
681
|
*/
|
|
529
|
-
|
|
682
|
+
getPopup: () => ChatSelectorPopup | undefined;
|
|
683
|
+
/**
|
|
684
|
+
* Signal emitting when a chat is opened.
|
|
685
|
+
*/
|
|
686
|
+
chatOpened: ISignal<MultiChatPanel, ChatWidget>;
|
|
530
687
|
};
|
|
531
688
|
|
|
532
689
|
/**
|
|
533
|
-
* A component
|
|
690
|
+
* A search input component for selecting a chat.
|
|
534
691
|
*/
|
|
535
|
-
function
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
692
|
+
function ChatSearchInput({
|
|
693
|
+
selectChat,
|
|
694
|
+
getPopup,
|
|
695
|
+
chatOpened
|
|
696
|
+
}: ChatSearchInputProps): JSX.Element {
|
|
697
|
+
const [query, setQuery] = useState<string>('');
|
|
698
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
699
|
+
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
const resetQuery = () => {
|
|
702
|
+
setQuery('');
|
|
703
|
+
};
|
|
704
|
+
chatOpened.connect(resetQuery);
|
|
705
|
+
|
|
706
|
+
return () => {
|
|
707
|
+
chatOpened.disconnect(resetQuery);
|
|
708
|
+
};
|
|
709
|
+
}, [chatOpened]);
|
|
710
|
+
|
|
711
|
+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
712
|
+
const value = event.target.value;
|
|
713
|
+
setQuery(value);
|
|
714
|
+
const popup = getPopup();
|
|
715
|
+
if (popup) {
|
|
716
|
+
popup.setQuery(value);
|
|
717
|
+
if (!popup.isVisible && value) {
|
|
718
|
+
popup.show();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const handleInputFocus = () => {
|
|
724
|
+
const popup = getPopup();
|
|
725
|
+
if (popup && inputRef.current) {
|
|
726
|
+
// Set anchor element before showing
|
|
727
|
+
popup.anchor = inputRef.current;
|
|
728
|
+
popup.setQuery(query);
|
|
729
|
+
popup.show();
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const handleInputClick = () => {
|
|
734
|
+
const popup = getPopup();
|
|
735
|
+
if (popup && inputRef.current && !popup.isVisible) {
|
|
736
|
+
popup.anchor = inputRef.current;
|
|
737
|
+
popup.setQuery(query);
|
|
738
|
+
popup.show();
|
|
739
|
+
}
|
|
740
|
+
// Force focus on input.
|
|
741
|
+
inputRef.current?.focus();
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
745
|
+
const popup = getPopup();
|
|
746
|
+
if (!popup || !popup.isVisible) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let value: string | null;
|
|
751
|
+
switch (event.key) {
|
|
752
|
+
case 'ArrowDown':
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
popup.selectNext();
|
|
755
|
+
break;
|
|
756
|
+
case 'ArrowUp':
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
popup.selectPrevious();
|
|
759
|
+
break;
|
|
760
|
+
case 'Enter':
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
value = popup.getSelectedValue();
|
|
763
|
+
if (value) {
|
|
764
|
+
selectChat(value);
|
|
765
|
+
popup.hide();
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
case 'Escape':
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
popup.hide();
|
|
771
|
+
setQuery('');
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
547
775
|
|
|
548
776
|
return (
|
|
549
|
-
<
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
{
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
</HTMLSelect>
|
|
777
|
+
<input
|
|
778
|
+
ref={inputRef}
|
|
779
|
+
type="text"
|
|
780
|
+
placeholder="Select a chat"
|
|
781
|
+
value={query}
|
|
782
|
+
onChange={handleInputChange}
|
|
783
|
+
onFocus={handleInputFocus}
|
|
784
|
+
onClick={handleInputClick}
|
|
785
|
+
onKeyDown={handleKeyDown}
|
|
786
|
+
className="jp-chat-search-input"
|
|
787
|
+
/>
|
|
561
788
|
);
|
|
562
789
|
}
|