Skip to content

Commit 7a906eb

Browse files
authored
Multichatpanel followup (#275)
* Refactor multichat panel callbacks and commands - replace openChat and createChat callbacks with createModel callback - do not open chat with the createChat command. A dedicated commands createAndOpen commands has been added - add some docstring * Fix the side panel chat restoration * Update command caption * Fix the example * Avoid duplicate chats in sidepanel, and fix tests * Remove leftover commented code
1 parent d87101d commit 7a906eb

File tree

5 files changed

+260
-140
lines changed

5 files changed

+260
-140
lines changed

docs/jupyter-chat-example/src/index.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,25 +155,25 @@ const plugin: JupyterFrontEndPlugin<void> = {
155155
app.commands.execute('docmanager:open', { path: attachment.value });
156156
});
157157

158-
app.commands.addCommand('chat-example:openChat', {
159-
execute: async () => {
160-
const model = new MyChatModel({
161-
activeCellManager,
162-
selectionWatcher,
163-
documentManager: filebrowser?.model.manager,
164-
config
165-
});
166-
model.name = UUID.uuid4();
167-
panel.addChat(model);
168-
}
169-
});
158+
const createModel = async (): Promise<MultiChatPanel.IAddChatArgs> => {
159+
const model = new MyChatModel({
160+
activeCellManager,
161+
selectionWatcher,
162+
documentManager: filebrowser?.model.manager,
163+
config
164+
});
165+
model.name = UUID.uuid4();
166+
return { model };
167+
};
170168

171169
const panel = new MultiChatPanel({
172170
rmRegistry,
173171
themeManager,
174172
attachmentOpenerRegistry,
175173
welcomeMessage,
176-
createChat: () => app.commands.execute('chat-example:openChat'),
174+
createModel: createModel,
175+
// No op, since the chat are transient, but it need to be provided to have the
176+
// button in the toolbar.
177177
renameChat: async (oldName: string, newName: string) => true
178178
});
179179
app.shell.add(panel, 'left');

packages/jupyter-chat/src/widgets/multichat-panel.tsx

Lines changed: 135 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@
88
* Originally adapted from jupyterlab-chat's ChatPanel
99
*/
1010

11-
import {
12-
chatIcon,
13-
ChatWidget,
14-
IAttachmentOpenerRegistry,
15-
IChatCommandRegistry,
16-
IChatModel,
17-
IInputToolbarRegistry,
18-
IMessageFooterRegistry,
19-
readIcon
20-
} from '../index';
2111
import { InputDialog, IThemeManager } from '@jupyterlab/apputils';
2212
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2313
import {
@@ -36,6 +26,20 @@ import { ISignal, Signal } from '@lumino/signaling';
3626
import { AccordionPanel, Panel } from '@lumino/widgets';
3727
import React, { useState } from 'react';
3828

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+
3943
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
4044
const ADD_BUTTON_CLASS = 'jp-chat-add';
4145
const OPEN_SELECT_CLASS = 'jp-chat-open';
@@ -62,17 +66,18 @@ export class MultiChatPanel extends SidePanel {
6266
this._messageFooterRegistry = options.messageFooterRegistry;
6367
this._welcomeMessage = options.welcomeMessage;
6468

65-
// Use the passed callback functions
6669
this._getChatNames = options.getChatNames;
67-
this._openChat = options.openChat;
70+
this._createModel = options.createModel;
6871
this._openInMain = options.openInMain;
69-
this._createChat = options.createChat;
7072
this._renameChat = options.renameChat;
7173

72-
if (this._createChat) {
74+
if (this._createModel) {
7375
// Add chat button calls the createChat callback
7476
const addChat = new ToolbarButton({
75-
onClick: () => this._createChat?.(),
77+
onClick: async () => {
78+
const addChatArgs = await this._createModel!();
79+
this.addChat(addChatArgs);
80+
},
7681
icon: addIcon,
7782
label: 'Chat',
7883
tooltip: 'Add a new chat'
@@ -81,7 +86,7 @@ export class MultiChatPanel extends SidePanel {
8186
this.toolbar.addItem('createChat', addChat);
8287
}
8388

84-
if (this._getChatNames && this._openChat) {
89+
if (this._getChatNames && this._createModel) {
8590
// Chat select dropdown
8691
this._openChatWidget = ReactWidget.create(
8792
<ChatSelect
@@ -106,14 +111,30 @@ export class MultiChatPanel extends SidePanel {
106111
return this.widgets as ChatSection[];
107112
}
108113

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+
109121
/**
110122
* Add a new widget to the chat panel.
111123
*
112124
* @param model - the model of the chat widget
113125
* @param displayName - the name of the chat.
114126
*/
115127

116-
addChat(model: IChatModel, displayName?: string): ChatWidget {
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+
117138
const content = this.content as AccordionPanel;
118139
for (let i = 0; i < this.widgets.length; i++) {
119140
content.collapse(i);
@@ -147,15 +168,16 @@ export class MultiChatPanel extends SidePanel {
147168
this.addWidget(section);
148169
content.expand(this.widgets.length - 1);
149170

171+
this._sectionAdded.emit(section);
150172
return widget;
151173
}
152174

153175
/**
154176
* Invoke the update of the list of available chats.
155177
*/
156-
updateChatList = () => {
178+
updateChatList() {
157179
this._updateChatListDebouncer.invoke();
158-
};
180+
}
159181

160182
/**
161183
* Update the list of available chats.
@@ -172,7 +194,7 @@ export class MultiChatPanel extends SidePanel {
172194
/**
173195
* Open a chat if it exists in the side panel.
174196
*
175-
* @param path - the path of the chat.
197+
* @param name - the name of the chat.
176198
* @returns a boolean, whether the chat existed in the side panel or not.
177199
*/
178200
openIfExists(name: string): boolean {
@@ -196,7 +218,7 @@ export class MultiChatPanel extends SidePanel {
196218
* @param name - the chat name.
197219
*/
198220
private _getChatIndex(name: string) {
199-
return this.widgets.findIndex(w => (w as ChatSection).model?.name === name);
221+
return this.sections.findIndex(section => section.model?.name === name);
200222
}
201223

202224
/**
@@ -211,12 +233,17 @@ export class MultiChatPanel extends SidePanel {
211233
/**
212234
* Handle `change` events for the HTMLSelect component.
213235
*/
214-
private _chatSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
236+
private async _chatSelected(
237+
event: React.ChangeEvent<HTMLSelectElement>
238+
): Promise<void> {
215239
const selection = event.target.value;
216240
if (selection === '-') {
217241
return;
218242
}
219-
this._openChat?.(selection);
243+
if (this._createModel) {
244+
const addChatArgs = await this._createModel(selection);
245+
this.addChat(addChatArgs);
246+
}
220247
event.target.selectedIndex = 0;
221248
}
222249

@@ -238,19 +265,20 @@ export class MultiChatPanel extends SidePanel {
238265
private _chatNamesChanged = new Signal<this, { [name: string]: string }>(
239266
this
240267
);
241-
268+
private _sectionAdded = new Signal<MultiChatPanel, ChatSection>(this);
242269
private _rmRegistry: IRenderMimeRegistry;
243-
private _themeManager: IThemeManager | null;
270+
private _themeManager?: IThemeManager | null;
244271
private _chatCommandRegistry?: IChatCommandRegistry;
245272
private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
246-
private _inputToolbarFactory?: MultiChatPanel.IInputToolbarRegistryFactory;
273+
private _inputToolbarFactory?: IInputToolbarRegistryFactory;
247274
private _messageFooterRegistry?: IMessageFooterRegistry;
248275
private _welcomeMessage?: string;
249276
private _updateChatListDebouncer: Debouncer;
250277

278+
private _createModel?: (
279+
name?: string
280+
) => Promise<MultiChatPanel.IAddChatArgs>;
251281
private _getChatNames?: () => Promise<{ [name: string]: string }>;
252-
private _createChat?: () => void;
253-
private _openChat?: (name: string) => void;
254282
private _openInMain?: (name: string) => void;
255283
private _renameChat?: (oldName: string, newName: string) => Promise<boolean>;
256284

@@ -264,25 +292,54 @@ export namespace MultiChatPanel {
264292
/**
265293
* Options of the constructor of the chat panel.
266294
*/
267-
export interface IOptions extends SidePanel.IOptions {
268-
rmRegistry: IRenderMimeRegistry;
269-
themeManager: IThemeManager | null;
270-
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+
*/
271314
getChatNames?: () => Promise<{ [name: string]: string }>;
272-
openChat?: (name: string) => void;
273-
createChat?: () => void;
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+
*/
274320
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+
*/
275328
renameChat?: (oldName: string, newName: string) => Promise<boolean>;
276-
277-
chatCommandRegistry?: IChatCommandRegistry;
278-
attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
279-
inputToolbarFactory?: IInputToolbarRegistryFactory;
280-
messageFooterRegistry?: IMessageFooterRegistry;
281-
welcomeMessage?: string;
282329
}
283-
284-
export interface IInputToolbarRegistryFactory {
285-
create(): IInputToolbarRegistry;
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;
286343
}
287344
}
288345

@@ -295,7 +352,8 @@ export class ChatSection extends PanelWithToolbar {
295352
*/
296353
constructor(options: ChatSection.IOptions) {
297354
super(options);
298-
this.addWidget(options.widget);
355+
this._chatWidget = options.widget;
356+
this.addWidget(this._chatWidget);
299357
this.addWidget(this._spinner);
300358
this.addClass(SECTION_CLASS);
301359
this.toolbar.addClass(TOOLBAR_CLASS);
@@ -394,11 +452,18 @@ export class ChatSection extends PanelWithToolbar {
394452
this._updateTitle();
395453
}
396454

455+
/**
456+
* The chat widget of the section.
457+
*/
458+
get widget(): ChatWidget {
459+
return this._chatWidget;
460+
}
461+
397462
/**
398463
* The model of the widget.
399464
*/
400465
get model(): IChatModel {
401-
return (this.widgets[0] as ChatWidget).model;
466+
return this._chatWidget.model;
402467
}
403468

404469
/**
@@ -433,6 +498,7 @@ export class ChatSection extends PanelWithToolbar {
433498
this._markAsRead.enabled = unread.length > 0;
434499
};
435500

501+
private _chatWidget: ChatWidget;
436502
private _markAsRead: ToolbarButton;
437503
private _spinner = new Spinner();
438504
private _displayName: string;
@@ -446,15 +512,39 @@ export namespace ChatSection {
446512
* Options to build a chat section.
447513
*/
448514
export interface IOptions extends Panel.IOptions {
515+
/**
516+
* The widget to display in the section.
517+
*/
449518
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+
*/
450524
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+
*/
451532
renameChat?: (oldName: string, newName: string) => Promise<boolean>;
533+
/**
534+
* The name to display in the section title.
535+
*/
452536
displayName?: string;
453537
}
454538
}
455539

456540
type ChatSelectProps = {
541+
/**
542+
* A signal emitting when the list of chat changed.
543+
*/
457544
chatNamesChanged: ISignal<MultiChatPanel, { [name: string]: string }>;
545+
/**
546+
* The callback to call when the selection changed in the select.
547+
*/
458548
handleChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
459549
};
460550

0 commit comments

Comments
 (0)