@jupyter/chat 0.22.0 → 0.23.0-alpha.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__/multichat-panel.spec.js +82 -1
- package/lib/components/attachments.d.ts +0 -1
- package/lib/components/avatar.d.ts +0 -1
- package/lib/components/chat.d.ts +0 -1
- package/lib/components/chat.js +8 -9
- package/lib/components/code-blocks/code-toolbar.d.ts +0 -1
- package/lib/components/code-blocks/copy-button.d.ts +0 -1
- package/lib/components/input/buttons/attach-button.d.ts +0 -1
- package/lib/components/input/buttons/cancel-button.d.ts +0 -1
- package/lib/components/input/buttons/save-edit-button.d.ts +0 -1
- package/lib/components/input/buttons/save-edit-button.js +2 -2
- package/lib/components/input/buttons/send-button.d.ts +0 -1
- package/lib/components/input/buttons/send-button.js +3 -10
- package/lib/components/input/buttons/stop-button.d.ts +0 -1
- package/lib/components/input/chat-input.d.ts +0 -1
- package/lib/components/input/chat-input.js +2 -3
- package/lib/components/input/submit-message.d.ts +22 -0
- package/lib/components/input/submit-message.js +13 -0
- package/lib/components/messages/chat-body-placeholder.d.ts +0 -1
- package/lib/components/messages/footer.d.ts +0 -1
- package/lib/components/messages/header.d.ts +3 -2
- package/lib/components/messages/header.js +2 -1
- package/lib/components/messages/message.d.ts +3 -6
- package/lib/components/messages/message.js +26 -14
- package/lib/components/messages/messages.d.ts +0 -1
- package/lib/components/messages/messages.js +11 -20
- package/lib/components/messages/preamble.d.ts +0 -1
- package/lib/components/messages/toolbar.d.ts +0 -1
- package/lib/components/messages/welcome.d.ts +0 -1
- package/lib/components/mui-extras/tooltipped-icon-button.d.ts +0 -1
- package/lib/components/writing-indicator.d.ts +0 -1
- package/lib/context.d.ts +0 -1
- package/lib/message.d.ts +6 -0
- package/lib/message.js +23 -0
- package/lib/model.d.ts +8 -1
- package/lib/model.js +3 -0
- package/lib/registers/chat-commands.d.ts +0 -1
- package/lib/registers/footers.d.ts +0 -1
- package/lib/registers/preambles.d.ts +0 -1
- package/lib/tokens.d.ts +0 -1
- package/lib/types.d.ts +5 -0
- package/lib/utils.js +2 -2
- package/lib/widgets/chat-selector-popup.d.ts +0 -1
- package/lib/widgets/multichat-panel.d.ts +21 -5
- package/lib/widgets/multichat-panel.js +50 -10
- package/package.json +22 -22
- package/src/__tests__/multichat-panel.spec.ts +113 -1
- package/src/components/chat.tsx +10 -10
- package/src/components/input/buttons/save-edit-button.tsx +2 -2
- package/src/components/input/buttons/send-button.tsx +3 -13
- package/src/components/input/chat-input.tsx +2 -3
- package/src/components/input/submit-message.ts +38 -0
- package/src/components/messages/header.tsx +5 -1
- package/src/components/messages/message.tsx +129 -122
- package/src/components/messages/messages.tsx +14 -26
- package/src/message.ts +29 -0
- package/src/model.ts +10 -2
- package/src/types.ts +5 -0
- package/src/utils.ts +2 -2
- package/src/widgets/multichat-panel.tsx +62 -11
- package/style/chat.css +8 -4
package/lib/message.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
5
6
|
import { Signal } from '@lumino/signaling';
|
|
6
7
|
/**
|
|
7
8
|
* The message object.
|
|
@@ -14,6 +15,7 @@ export class Message {
|
|
|
14
15
|
*/
|
|
15
16
|
constructor(content) {
|
|
16
17
|
this._changed = new Signal(this);
|
|
18
|
+
this._rendered = new PromiseDelegate();
|
|
17
19
|
this._content = content;
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
@@ -70,11 +72,32 @@ export class Message {
|
|
|
70
72
|
get changed() {
|
|
71
73
|
return this._changed;
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* A promise delegate that resolve when the message is rendered.
|
|
77
|
+
*/
|
|
78
|
+
get renderedDelegate() {
|
|
79
|
+
return this._rendered;
|
|
80
|
+
}
|
|
73
81
|
/**
|
|
74
82
|
* Update one or several fields of the message.
|
|
75
83
|
*/
|
|
76
84
|
update(updated) {
|
|
85
|
+
// Check if the message will be rendered again.
|
|
86
|
+
const triggerRerender = new Set([
|
|
87
|
+
'body',
|
|
88
|
+
'deleted',
|
|
89
|
+
'mentions',
|
|
90
|
+
'mime_model'
|
|
91
|
+
]);
|
|
92
|
+
const updatedAttributes = Object.keys(updated);
|
|
93
|
+
const shouldRerender = updatedAttributes.some(attribute => triggerRerender.has(attribute) &&
|
|
94
|
+
updated[attribute] !== this._content[attribute]);
|
|
95
|
+
// Update the message content.
|
|
77
96
|
this._content = { ...this._content, ...updated };
|
|
97
|
+
// Update the promise delegate if the message will be rendered again.
|
|
98
|
+
if (shouldRerender) {
|
|
99
|
+
this._rendered = new PromiseDelegate();
|
|
100
|
+
}
|
|
78
101
|
this._changed.emit();
|
|
79
102
|
}
|
|
80
103
|
}
|
package/lib/model.d.ts
CHANGED
|
@@ -124,6 +124,10 @@ export interface IChatModel extends IDisposable {
|
|
|
124
124
|
* Whether the chat handler is disposed.
|
|
125
125
|
*/
|
|
126
126
|
isDisposed: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* A signal emitting when disposing of the model.
|
|
129
|
+
*/
|
|
130
|
+
readonly disposed: ISignal<IChatModel, void>;
|
|
127
131
|
/**
|
|
128
132
|
* Function to call when a message is received.
|
|
129
133
|
*
|
|
@@ -186,7 +190,10 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
186
190
|
*/
|
|
187
191
|
get name(): string;
|
|
188
192
|
set name(value: string);
|
|
189
|
-
|
|
193
|
+
/**
|
|
194
|
+
* A signal emitting when disposing of the model.
|
|
195
|
+
*/
|
|
196
|
+
get disposed(): ISignal<IChatModel, void>;
|
|
190
197
|
/**
|
|
191
198
|
* The chat messages list.
|
|
192
199
|
*/
|
package/lib/model.js
CHANGED
package/lib/tokens.d.ts
CHANGED
package/lib/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
1
2
|
import { ISignal } from '@lumino/signaling';
|
|
2
3
|
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
3
4
|
/**
|
|
@@ -146,6 +147,10 @@ export interface IMessage extends IMessageContent {
|
|
|
146
147
|
* A signal emitting when the message has been updated.
|
|
147
148
|
*/
|
|
148
149
|
changed: ISignal<IMessage, void>;
|
|
150
|
+
/**
|
|
151
|
+
* A promise delegate that resolve when the message is rendered.
|
|
152
|
+
*/
|
|
153
|
+
renderedDelegate: PromiseDelegate<void>;
|
|
149
154
|
}
|
|
150
155
|
/**
|
|
151
156
|
* The chat history interface.
|
package/lib/utils.js
CHANGED
|
@@ -48,7 +48,7 @@ export function replaceMentionToSpan(content, user) {
|
|
|
48
48
|
}
|
|
49
49
|
const mention = '@' + user.mention_name;
|
|
50
50
|
const regex = new RegExp(mention, 'g');
|
|
51
|
-
const mentionEl = `<span class="${MENTION_CLASS}"
|
|
51
|
+
const mentionEl = `<span class="${MENTION_CLASS}"> ${mention} </span>`;
|
|
52
52
|
return content.replace(regex, mentionEl);
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -62,7 +62,7 @@ export function replaceSpanToMention(content, user) {
|
|
|
62
62
|
return content;
|
|
63
63
|
}
|
|
64
64
|
const mention = '@' + user.mention_name;
|
|
65
|
-
const mentionEl = `<span class="${MENTION_CLASS}"
|
|
65
|
+
const mentionEl = `<span class="${MENTION_CLASS}"> ${mention} </span>`;
|
|
66
66
|
const regex = new RegExp(mentionEl, 'g');
|
|
67
67
|
return content.replace(regex, mention);
|
|
68
68
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
-
import { PanelWithToolbar } from '@jupyterlab/ui-components';
|
|
2
|
+
import { PanelWithToolbar, ReactiveToolbar } from '@jupyterlab/ui-components';
|
|
3
3
|
import { Message } from '@lumino/messaging';
|
|
4
4
|
import { ISignal } from '@lumino/signaling';
|
|
5
|
+
import { Panel, Widget } from '@lumino/widgets';
|
|
5
6
|
import { ChatWidget } from './chat-widget';
|
|
6
7
|
import { Chat, IInputToolbarRegistryFactory } from '../components';
|
|
7
8
|
import { IChatModel } from '../model';
|
|
8
9
|
import { IChatPlaceholderFactory } from '../tokens';
|
|
10
|
+
/**
|
|
11
|
+
* A panel widget with a reactive toolbar.
|
|
12
|
+
*/
|
|
13
|
+
declare class ReactivePanelWithToolbar extends Panel {
|
|
14
|
+
constructor();
|
|
15
|
+
get toolbar(): ReactiveToolbar;
|
|
16
|
+
private _toolbar;
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
11
20
|
*/
|
|
@@ -41,9 +50,10 @@ export declare class MultiChatPanel extends PanelWithToolbar {
|
|
|
41
50
|
*/
|
|
42
51
|
getLoadedModelNames(): string[];
|
|
43
52
|
/**
|
|
44
|
-
*
|
|
53
|
+
* Removing a model from loaded list.
|
|
54
|
+
* Dispose of the widget and optionally dispose of the model.
|
|
45
55
|
*/
|
|
46
|
-
|
|
56
|
+
unsetLoadedModel(name: string, dispose?: boolean): void;
|
|
47
57
|
/**
|
|
48
58
|
* Emit a signal when the panel visibility changed.
|
|
49
59
|
*/
|
|
@@ -178,8 +188,10 @@ export declare namespace MultiChatPanel {
|
|
|
178
188
|
/**
|
|
179
189
|
* A widget containing the chat and its toolbar.
|
|
180
190
|
*/
|
|
181
|
-
declare class SidePanelWidget extends
|
|
191
|
+
declare class SidePanelWidget extends ReactivePanelWithToolbar {
|
|
182
192
|
constructor(options: SidePanelWidget.IOptions);
|
|
193
|
+
protected onAfterAttach(msg: Message): void;
|
|
194
|
+
protected onResize(msg: Widget.ResizeMessage): void;
|
|
183
195
|
/**
|
|
184
196
|
* The chat widget embedded in the sidepanel widget.
|
|
185
197
|
*/
|
|
@@ -212,6 +224,10 @@ declare class SidePanelWidget extends PanelWithToolbar {
|
|
|
212
224
|
* Enable/disable unread icon.
|
|
213
225
|
*/
|
|
214
226
|
private _unreadChanged;
|
|
227
|
+
/**
|
|
228
|
+
* Trigger reactive toolbar overflow computation from rendered toolbar size.
|
|
229
|
+
*/
|
|
230
|
+
private _updateReactiveToolbar;
|
|
215
231
|
private _chatWidget;
|
|
216
232
|
private _markAsRead;
|
|
217
233
|
private _displayName;
|
|
@@ -233,7 +249,7 @@ declare namespace SidePanelWidget {
|
|
|
233
249
|
/**
|
|
234
250
|
* The callback when closing the chat.
|
|
235
251
|
*/
|
|
236
|
-
onClose: (name: string) => void;
|
|
252
|
+
onClose: (name: string, disposeModel?: boolean) => void;
|
|
237
253
|
/**
|
|
238
254
|
* The displayed name of the chat.
|
|
239
255
|
*/
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { InputDialog } from '@jupyterlab/apputils';
|
|
10
10
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
11
|
-
import { addIcon, closeIcon, launchIcon, PanelWithToolbar, ReactWidget, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
|
|
11
|
+
import { addIcon, closeIcon, launchIcon, PanelWithToolbar, ReactiveToolbar, ReactWidget, Spinner, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
|
|
12
12
|
import { ArrayExt } from '@lumino/algorithm';
|
|
13
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
13
14
|
import { Debouncer } from '@lumino/polling';
|
|
14
15
|
import { Signal } from '@lumino/signaling';
|
|
15
|
-
import { Widget } from '@lumino/widgets';
|
|
16
|
+
import { Panel, Widget } from '@lumino/widgets';
|
|
16
17
|
import React, { useEffect, useRef, useState } from 'react';
|
|
17
18
|
import { ChatSelectorPopup } from './chat-selector-popup';
|
|
18
19
|
import { ChatWidget } from './chat-widget';
|
|
@@ -23,7 +24,21 @@ const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
|
23
24
|
const ADD_BUTTON_CLASS = 'jp-chat-add';
|
|
24
25
|
const OPEN_SELECT_CLASS = 'jp-chat-open';
|
|
25
26
|
const SIDEPANEL_WIDGET_CLASS = 'jp-chat-sidepanel-widget';
|
|
27
|
+
// TODO: Drop this workaround class (and related CSS) once we depend on
|
|
28
|
+
// @jupyterlab/ui-components >= 4.6.0 (refs - jupyterlab/jupyterlab#18824).
|
|
26
29
|
const TOOLBAR_CLASS = 'jp-chat-sidepanel-widget-toolbar';
|
|
30
|
+
/**
|
|
31
|
+
* A panel widget with a reactive toolbar.
|
|
32
|
+
*/
|
|
33
|
+
class ReactivePanelWithToolbar extends Panel {
|
|
34
|
+
constructor() {
|
|
35
|
+
super();
|
|
36
|
+
this._toolbar = new ReactiveToolbar({ noFocusOnClick: true });
|
|
37
|
+
}
|
|
38
|
+
get toolbar() {
|
|
39
|
+
return this._toolbar;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
29
44
|
*/
|
|
@@ -133,7 +148,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
133
148
|
chatNames: [],
|
|
134
149
|
onSelect: this._onSelectChat,
|
|
135
150
|
onClose: (name) => {
|
|
136
|
-
this.
|
|
151
|
+
this.unsetLoadedModel(name);
|
|
137
152
|
},
|
|
138
153
|
translator
|
|
139
154
|
});
|
|
@@ -197,9 +212,10 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
197
212
|
return Array.from(this._loadedModels.keys());
|
|
198
213
|
}
|
|
199
214
|
/**
|
|
200
|
-
*
|
|
215
|
+
* Removing a model from loaded list.
|
|
216
|
+
* Dispose of the widget and optionally dispose of the model.
|
|
201
217
|
*/
|
|
202
|
-
|
|
218
|
+
unsetLoadedModel(name, dispose = true) {
|
|
203
219
|
var _a, _b;
|
|
204
220
|
const model = this._loadedModels.get(name);
|
|
205
221
|
if (model) {
|
|
@@ -214,7 +230,9 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
214
230
|
}
|
|
215
231
|
this._addPlaceholder();
|
|
216
232
|
}
|
|
217
|
-
|
|
233
|
+
if (dispose) {
|
|
234
|
+
model.dispose();
|
|
235
|
+
}
|
|
218
236
|
this._loadedModels.delete(name);
|
|
219
237
|
(_b = this._chatSelectorPopup) === null || _b === void 0 ? void 0 : _b.setLoadedModels(this.getLoadedModelNames());
|
|
220
238
|
}
|
|
@@ -282,8 +300,8 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
282
300
|
displayName: name,
|
|
283
301
|
openInMain: this._openInMain,
|
|
284
302
|
renameChat: this._renameChat,
|
|
285
|
-
onClose: (name) => {
|
|
286
|
-
this.
|
|
303
|
+
onClose: (name, disposeModel = true) => {
|
|
304
|
+
this.unsetLoadedModel(name, disposeModel);
|
|
287
305
|
},
|
|
288
306
|
trans: this._trans
|
|
289
307
|
});
|
|
@@ -352,7 +370,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
352
370
|
/**
|
|
353
371
|
* A widget containing the chat and its toolbar.
|
|
354
372
|
*/
|
|
355
|
-
class SidePanelWidget extends
|
|
373
|
+
class SidePanelWidget extends ReactivePanelWithToolbar {
|
|
356
374
|
constructor(options) {
|
|
357
375
|
var _a, _b, _c, _d;
|
|
358
376
|
super();
|
|
@@ -367,8 +385,11 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
367
385
|
this._displayName = (_a = options.displayName) !== null && _a !== void 0 ? _a : options.widget.model.name;
|
|
368
386
|
const trans = options.trans;
|
|
369
387
|
this.addClass(SIDEPANEL_WIDGET_CLASS);
|
|
388
|
+
// Keep side-panel toolbar baseline sizing from ui-components.
|
|
389
|
+
this.toolbar.addClass('jp-SidePanel-toolbar');
|
|
370
390
|
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
371
391
|
this._updateTitle();
|
|
392
|
+
this.toolbar.addItem('titleSpacer', Toolbar.createSpacerItem());
|
|
372
393
|
this.addWidget(this.toolbar);
|
|
373
394
|
// Add spinner while loading
|
|
374
395
|
const spinner = new Spinner();
|
|
@@ -435,7 +456,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
435
456
|
var _a;
|
|
436
457
|
const name = this.model.name;
|
|
437
458
|
if (await ((_a = options.openInMain) === null || _a === void 0 ? void 0 : _a.call(options, name))) {
|
|
438
|
-
options.onClose(this._displayName);
|
|
459
|
+
options.onClose(this._displayName, false);
|
|
439
460
|
}
|
|
440
461
|
}
|
|
441
462
|
});
|
|
@@ -454,6 +475,14 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
454
475
|
(_b = this.model.unreadChanged) === null || _b === void 0 ? void 0 : _b.connect(this._unreadChanged);
|
|
455
476
|
this._markAsRead.enabled = ((_d = (_c = this.model) === null || _c === void 0 ? void 0 : _c.unreadMessages.length) !== null && _d !== void 0 ? _d : 0) > 0;
|
|
456
477
|
}
|
|
478
|
+
onAfterAttach(msg) {
|
|
479
|
+
super.onAfterAttach(msg);
|
|
480
|
+
requestAnimationFrame(() => this._updateReactiveToolbar());
|
|
481
|
+
}
|
|
482
|
+
onResize(msg) {
|
|
483
|
+
super.onResize(msg);
|
|
484
|
+
this._updateReactiveToolbar();
|
|
485
|
+
}
|
|
457
486
|
/**
|
|
458
487
|
* The chat widget embedded in the sidepanel widget.
|
|
459
488
|
*/
|
|
@@ -516,6 +545,17 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
516
545
|
this._titleWidget = new Widget({ node: titleElement });
|
|
517
546
|
this.toolbar.insertItem(0, 'title', this._titleWidget);
|
|
518
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Trigger reactive toolbar overflow computation from rendered toolbar size.
|
|
550
|
+
*/
|
|
551
|
+
_updateReactiveToolbar() {
|
|
552
|
+
const toolbarWidth = this.toolbar.node.offsetWidth;
|
|
553
|
+
if (toolbarWidth <= 0) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const toolbarHeight = this.toolbar.node.offsetHeight;
|
|
557
|
+
MessageLoop.sendMessage(this.toolbar, new Widget.ResizeMessage(toolbarWidth, toolbarHeight));
|
|
558
|
+
}
|
|
519
559
|
}
|
|
520
560
|
/**
|
|
521
561
|
* A search input component for selecting a chat.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0-alpha.0",
|
|
4
4
|
"description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -46,25 +46,25 @@
|
|
|
46
46
|
"@emotion/react": "^11.10.5",
|
|
47
47
|
"@emotion/styled": "^11.10.5",
|
|
48
48
|
"@jupyter/react-components": "^0.15.2",
|
|
49
|
-
"@jupyterlab/application": "^4.
|
|
50
|
-
"@jupyterlab/apputils": "^4.
|
|
51
|
-
"@jupyterlab/codeeditor": "^4.
|
|
52
|
-
"@jupyterlab/codemirror": "^4.
|
|
53
|
-
"@jupyterlab/docmanager": "^4.
|
|
54
|
-
"@jupyterlab/docregistry": "^4.
|
|
55
|
-
"@jupyterlab/filebrowser": "^4.
|
|
56
|
-
"@jupyterlab/fileeditor": "^4.
|
|
57
|
-
"@jupyterlab/notebook": "^4.
|
|
58
|
-
"@jupyterlab/rendermime": "^4.
|
|
59
|
-
"@jupyterlab/translation": "^4.
|
|
60
|
-
"@jupyterlab/ui-components": "^4.
|
|
61
|
-
"@lumino/algorithm": "^2.0.
|
|
62
|
-
"@lumino/commands": "^2.
|
|
63
|
-
"@lumino/coreutils": "^2.
|
|
64
|
-
"@lumino/disposable": "^2.
|
|
65
|
-
"@lumino/polling": "^2.
|
|
66
|
-
"@lumino/signaling": "^2.
|
|
67
|
-
"@lumino/widgets": "^2.
|
|
49
|
+
"@jupyterlab/application": "^4.6.0",
|
|
50
|
+
"@jupyterlab/apputils": "^4.7.0",
|
|
51
|
+
"@jupyterlab/codeeditor": "^4.6.0",
|
|
52
|
+
"@jupyterlab/codemirror": "^4.6.0",
|
|
53
|
+
"@jupyterlab/docmanager": "^4.6.0",
|
|
54
|
+
"@jupyterlab/docregistry": "^4.6.0",
|
|
55
|
+
"@jupyterlab/filebrowser": "^4.6.0",
|
|
56
|
+
"@jupyterlab/fileeditor": "^4.6.0",
|
|
57
|
+
"@jupyterlab/notebook": "^4.6.0",
|
|
58
|
+
"@jupyterlab/rendermime": "^4.6.0",
|
|
59
|
+
"@jupyterlab/translation": "^4.6.0",
|
|
60
|
+
"@jupyterlab/ui-components": "^4.6.0",
|
|
61
|
+
"@lumino/algorithm": "^2.0.4",
|
|
62
|
+
"@lumino/commands": "^2.3.3",
|
|
63
|
+
"@lumino/coreutils": "^2.2.2",
|
|
64
|
+
"@lumino/disposable": "^2.1.5",
|
|
65
|
+
"@lumino/polling": "^2.1.5",
|
|
66
|
+
"@lumino/signaling": "^2.1.5",
|
|
67
|
+
"@lumino/widgets": "^2.8.0",
|
|
68
68
|
"@mui/icons-material": "^7.3.2",
|
|
69
69
|
"@mui/material": "^7.3.2",
|
|
70
70
|
"clsx": "^2.1.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"react-dom": "^18.2.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@jupyterlab/testing": "^4.
|
|
75
|
+
"@jupyterlab/testing": "^4.6.0",
|
|
76
76
|
"@types/jest": "^29.2.0",
|
|
77
77
|
"@types/json-schema": "^7.0.11",
|
|
78
78
|
"@types/react": "^18.2.0",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"rimraf": "^5.0.1",
|
|
85
85
|
"source-map-loader": "^1.0.2",
|
|
86
86
|
"style-loader": "^3.3.1",
|
|
87
|
-
"typescript": "~5.
|
|
87
|
+
"typescript": "~5.9.3"
|
|
88
88
|
},
|
|
89
89
|
"sideEffects": [
|
|
90
90
|
"style/*.css",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { ToolbarButton } from '@jupyterlab/ui-components';
|
|
6
7
|
import { RenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
8
|
import { Widget } from '@lumino/widgets';
|
|
8
9
|
|
|
@@ -17,6 +18,101 @@ describe('MultiChatPanel', () => {
|
|
|
17
18
|
rmRegistry = new RenderMimeRegistry();
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
describe('unsetLoadedModel', () => {
|
|
22
|
+
it('should dispose the model by default', async () => {
|
|
23
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
24
|
+
const { MockChatModel } = await import('./mocks');
|
|
25
|
+
const model = new MockChatModel();
|
|
26
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
27
|
+
|
|
28
|
+
panel.unsetLoadedModel('test-chat');
|
|
29
|
+
|
|
30
|
+
expect(model.isDisposed).toBe(true);
|
|
31
|
+
panel.dispose();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should not dispose the model when dispose=false', async () => {
|
|
35
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
36
|
+
const { MockChatModel } = await import('./mocks');
|
|
37
|
+
const model = new MockChatModel();
|
|
38
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
39
|
+
|
|
40
|
+
panel.unsetLoadedModel('test-chat', false);
|
|
41
|
+
|
|
42
|
+
expect(model.isDisposed).toBe(false);
|
|
43
|
+
model.dispose();
|
|
44
|
+
panel.dispose();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should dispose the model when clicking the close button', async () => {
|
|
48
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
49
|
+
const { MockChatModel } = await import('./mocks');
|
|
50
|
+
const model = new MockChatModel();
|
|
51
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
52
|
+
|
|
53
|
+
const toolbar = panel.current!.toolbar;
|
|
54
|
+
const names = Array.from(toolbar.names());
|
|
55
|
+
const closeIdx = names.indexOf('close');
|
|
56
|
+
expect(closeIdx).toBeGreaterThan(-1);
|
|
57
|
+
|
|
58
|
+
const closeButton = (toolbar.layout as any).widgets[
|
|
59
|
+
closeIdx
|
|
60
|
+
] as ToolbarButton;
|
|
61
|
+
|
|
62
|
+
closeButton.onClick();
|
|
63
|
+
|
|
64
|
+
expect(model.isDisposed).toBe(true);
|
|
65
|
+
panel.dispose();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should remove the model from loaded models regardless of dispose flag', async () => {
|
|
69
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
70
|
+
const { MockChatModel } = await import('./mocks');
|
|
71
|
+
const model = new MockChatModel();
|
|
72
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
73
|
+
|
|
74
|
+
panel.unsetLoadedModel('test-chat', false);
|
|
75
|
+
|
|
76
|
+
expect(panel.getLoadedModel('test-chat')).toBeUndefined();
|
|
77
|
+
model.dispose();
|
|
78
|
+
panel.dispose();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('openInMain', () => {
|
|
83
|
+
it('should not dispose the model when moving to main area', async () => {
|
|
84
|
+
const { MockChatModel } = await import('./mocks');
|
|
85
|
+
const model = new MockChatModel();
|
|
86
|
+
const openInMain = jest.fn().mockResolvedValue(true);
|
|
87
|
+
|
|
88
|
+
const panel = new MultiChatPanel({ rmRegistry, openInMain });
|
|
89
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
90
|
+
|
|
91
|
+
// Find the 'moveMain' toolbar button by name and invoke its click handler directly.
|
|
92
|
+
// ToolbarButton stores the onClick handler independently of DOM/React rendering,
|
|
93
|
+
// so this works without attaching the widget to the document.
|
|
94
|
+
const toolbar = panel.current!.toolbar;
|
|
95
|
+
const names = Array.from(toolbar.names());
|
|
96
|
+
const moveMainIdx = names.indexOf('moveMain');
|
|
97
|
+
expect(moveMainIdx).toBeGreaterThan(-1);
|
|
98
|
+
|
|
99
|
+
const moveButton = (toolbar.layout as any).widgets[
|
|
100
|
+
moveMainIdx
|
|
101
|
+
] as ToolbarButton;
|
|
102
|
+
|
|
103
|
+
moveButton.onClick();
|
|
104
|
+
// Wait for the async onClick handler: openInMain resolves, then onClose is called.
|
|
105
|
+
await Promise.resolve();
|
|
106
|
+
|
|
107
|
+
expect(openInMain).toHaveBeenCalledWith(model.name);
|
|
108
|
+
expect(model.isDisposed).toBe(false);
|
|
109
|
+
expect(panel.getLoadedModel('test-chat')).toBeUndefined();
|
|
110
|
+
|
|
111
|
+
model.dispose();
|
|
112
|
+
panel.dispose();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
20
116
|
describe('placeholderFactory', () => {
|
|
21
117
|
it('should use defaultPlaceholder when no factory is provided', () => {
|
|
22
118
|
const panel = new MultiChatPanel({ rmRegistry });
|
|
@@ -126,10 +222,26 @@ describe('MultiChatPanel', () => {
|
|
|
126
222
|
const { MockChatModel } = await import('./mocks');
|
|
127
223
|
const model = new MockChatModel();
|
|
128
224
|
panel.open({ model, displayName: 'test-chat' });
|
|
129
|
-
panel.
|
|
225
|
+
panel.unsetLoadedModel('test-chat');
|
|
130
226
|
|
|
131
227
|
expect(factory.create).toHaveBeenCalledTimes(2);
|
|
132
228
|
panel.dispose();
|
|
133
229
|
});
|
|
134
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('side panel toolbar', () => {
|
|
233
|
+
it('should include the responsive toolbar opener for overflow actions', async () => {
|
|
234
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
235
|
+
const { MockChatModel } = await import('./mocks');
|
|
236
|
+
|
|
237
|
+
panel.open({ model: new MockChatModel(), displayName: 'test-chat' });
|
|
238
|
+
|
|
239
|
+
const opener = panel.current?.toolbar.node.querySelector(
|
|
240
|
+
'.jp-Toolbar-responsive-opener'
|
|
241
|
+
);
|
|
242
|
+
expect(opener).not.toBeNull();
|
|
243
|
+
|
|
244
|
+
panel.dispose();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
135
247
|
});
|
package/src/components/chat.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
10
10
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
11
11
|
import { IconButton } from '@mui/material';
|
|
12
12
|
import { Box } from '@mui/system';
|
|
13
|
-
import React, { useEffect, useState } from 'react';
|
|
13
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
ChatInput,
|
|
@@ -34,11 +34,6 @@ import { IChatBodyPlaceholderFactory } from '../tokens';
|
|
|
34
34
|
export function ChatBody(props: Chat.IChatProps): JSX.Element {
|
|
35
35
|
const { model } = props;
|
|
36
36
|
const [writers, setWriters] = useState<IChatModel.IWriter[]>([]);
|
|
37
|
-
let { inputToolbarRegistry } = props;
|
|
38
|
-
if (!inputToolbarRegistry) {
|
|
39
|
-
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
37
|
/**
|
|
43
38
|
* Handle the changes in the writers list.
|
|
44
39
|
*/
|
|
@@ -65,10 +60,15 @@ export function ChatBody(props: Chat.IChatProps): JSX.Element {
|
|
|
65
60
|
|
|
66
61
|
const horizontalPadding = 4;
|
|
67
62
|
|
|
68
|
-
const contextValue
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const contextValue = useMemo(
|
|
64
|
+
() => ({
|
|
65
|
+
...props,
|
|
66
|
+
inputToolbarRegistry:
|
|
67
|
+
props.inputToolbarRegistry ??
|
|
68
|
+
InputToolbarRegistry.defaultToolbarRegistry()
|
|
69
|
+
}),
|
|
70
|
+
[props]
|
|
71
|
+
);
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
74
|
<ChatReactContext.Provider value={contextValue}>
|
|
@@ -7,6 +7,7 @@ import CheckIcon from '@mui/icons-material/Check';
|
|
|
7
7
|
import React, { useEffect, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
|
+
import { submitInputMessage } from '../submit-message';
|
|
10
11
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
12
|
import { useTranslator } from '../../../context';
|
|
12
13
|
|
|
@@ -47,8 +48,7 @@ export function SaveEditButton(
|
|
|
47
48
|
}, [model]);
|
|
48
49
|
|
|
49
50
|
async function save() {
|
|
50
|
-
await
|
|
51
|
-
model.send(model.value);
|
|
51
|
+
await submitInputMessage({ model, chatCommandRegistry });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return (
|
|
@@ -9,6 +9,7 @@ import { Box, Menu, MenuItem, Typography } from '@mui/material';
|
|
|
9
9
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
12
|
+
import { submitInputMessage } from '../submit-message';
|
|
12
13
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
13
14
|
import { useTranslator } from '../../../context';
|
|
14
15
|
import { includeSelectionIcon } from '../../../icons';
|
|
@@ -111,19 +112,10 @@ export function SendButton(
|
|
|
111
112
|
}, [activeCellManager, selectionWatcher, showSendWithSelection]);
|
|
112
113
|
|
|
113
114
|
async function send() {
|
|
114
|
-
|
|
115
|
-
await chatCommandRegistry?.onSubmit(model);
|
|
116
|
-
|
|
117
|
-
const body = model.value;
|
|
118
|
-
|
|
119
|
-
model.value = '';
|
|
120
|
-
model.send(body);
|
|
121
|
-
model.focus();
|
|
115
|
+
await submitInputMessage({ model, chatCommandRegistry });
|
|
122
116
|
}
|
|
123
117
|
|
|
124
118
|
async function sendWithSelection() {
|
|
125
|
-
await chatCommandRegistry?.onSubmit(model);
|
|
126
|
-
|
|
127
119
|
let source = '';
|
|
128
120
|
let language: string | undefined;
|
|
129
121
|
|
|
@@ -141,10 +133,8 @@ export function SendButton(
|
|
|
141
133
|
body += `\n\n\`\`\`${language ?? ''}\n${source}\n\`\`\`\n`;
|
|
142
134
|
}
|
|
143
135
|
|
|
144
|
-
model.value = '';
|
|
145
136
|
closeMenu();
|
|
146
|
-
model
|
|
147
|
-
model.focus();
|
|
137
|
+
await submitInputMessage({ model, chatCommandRegistry, body });
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
return (
|