@jupyter/chat 0.21.0 → 0.22.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__/chat-body-placeholder.spec.d.ts +1 -0
- package/lib/__tests__/chat-body-placeholder.spec.js +54 -0
- package/lib/__tests__/multichat-panel.spec.d.ts +1 -0
- package/lib/__tests__/multichat-panel.spec.js +116 -0
- package/lib/components/chat.d.ts +6 -0
- package/lib/components/input/buttons/send-button.d.ts +1 -1
- package/lib/components/input/buttons/send-button.js +108 -8
- package/lib/components/input/chat-input.js +4 -3
- package/lib/components/messages/chat-body-placeholder.d.ts +6 -0
- package/lib/components/messages/chat-body-placeholder.js +19 -0
- package/lib/components/messages/index.d.ts +1 -0
- package/lib/components/messages/index.js +1 -0
- package/lib/components/messages/message-renderer.js +1 -1
- package/lib/components/messages/messages.js +54 -2
- package/lib/components/scroll-container.d.ts +6 -13
- package/lib/components/scroll-container.js +11 -22
- package/lib/input-model.d.ts +4 -0
- package/lib/theme-provider.js +2 -2
- package/lib/tokens.d.ts +50 -1
- package/lib/tokens.js +12 -0
- package/lib/types.d.ts +4 -0
- package/lib/widgets/chat-widget.js +5 -1
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +15 -0
- package/lib/widgets/multichat-panel.js +41 -9
- package/lib/widgets/placeholder.d.ts +48 -0
- package/lib/widgets/placeholder.js +48 -0
- package/package.json +3 -3
- package/src/__tests__/chat-body-placeholder.spec.ts +69 -0
- package/src/__tests__/multichat-panel.spec.ts +135 -0
- package/src/components/chat.tsx +6 -0
- package/src/components/input/buttons/send-button.tsx +158 -14
- package/src/components/input/chat-input.tsx +6 -5
- package/src/components/messages/chat-body-placeholder.tsx +24 -0
- package/src/components/messages/index.ts +1 -0
- package/src/components/messages/message-renderer.tsx +1 -1
- package/src/components/messages/messages.tsx +59 -2
- package/src/components/scroll-container.tsx +27 -34
- package/src/input-model.ts +4 -0
- package/src/theme-provider.ts +2 -2
- package/src/tokens.ts +61 -1
- package/src/types.ts +4 -0
- package/src/widgets/chat-widget.tsx +14 -1
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +56 -10
- package/src/widgets/placeholder.tsx +122 -0
- package/style/base.css +1 -0
- package/style/placeholder.css +29 -0
package/lib/tokens.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
1
2
|
import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
|
|
2
3
|
import { Token } from '@lumino/coreutils';
|
|
3
|
-
import {
|
|
4
|
+
import { Widget } from '@lumino/widgets';
|
|
5
|
+
import { ChatWidget, Placeholder } from './widgets';
|
|
4
6
|
import { IChatModel } from './model';
|
|
5
7
|
/**
|
|
6
8
|
* The main area chat widget type.
|
|
@@ -16,3 +18,50 @@ export type IChatTracker = IWidgetTracker<ChatWidget | MainAreaChat>;
|
|
|
16
18
|
* A chat tracker token.
|
|
17
19
|
*/
|
|
18
20
|
export declare const IChatTracker: Token<IChatTracker>;
|
|
21
|
+
/**
|
|
22
|
+
* The interface for the placeholder factory.
|
|
23
|
+
*/
|
|
24
|
+
export interface IChatPlaceholderFactory {
|
|
25
|
+
/**
|
|
26
|
+
* Create a placeholder widget for the multi-chat panel.
|
|
27
|
+
*
|
|
28
|
+
* @param props - the props passed to the placeholder.
|
|
29
|
+
* @returns a widget to display as placeholder.
|
|
30
|
+
*/
|
|
31
|
+
create(props: Placeholder.IProps): Widget;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The token for the placeholder factory.
|
|
35
|
+
* Not provided by default — extensions can provide it to replace the default
|
|
36
|
+
* placeholder shown when no chat is open in the multi-chat panel.
|
|
37
|
+
*/
|
|
38
|
+
export declare const IChatPlaceholderFactory: Token<IChatPlaceholderFactory>;
|
|
39
|
+
/**
|
|
40
|
+
* The interface for the chat body placeholder factory.
|
|
41
|
+
*/
|
|
42
|
+
export interface IChatBodyPlaceholderFactory {
|
|
43
|
+
/**
|
|
44
|
+
* Create a chat body placeholder component shown when the chat has no messages.
|
|
45
|
+
*
|
|
46
|
+
* @param props - the props passed to the component.
|
|
47
|
+
* @returns a React element to display as chat body placeholder.
|
|
48
|
+
*/
|
|
49
|
+
create(props: IChatBodyPlaceholderFactory.IProps): JSX.Element | null;
|
|
50
|
+
}
|
|
51
|
+
export declare namespace IChatBodyPlaceholderFactory {
|
|
52
|
+
/**
|
|
53
|
+
* The props passed to the chat body placeholder factory.
|
|
54
|
+
*/
|
|
55
|
+
interface IProps {
|
|
56
|
+
/**
|
|
57
|
+
* Callback to send a message.
|
|
58
|
+
*/
|
|
59
|
+
onSend: (body: string) => void;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The token for the chat body placeholder factory.
|
|
64
|
+
* Not provided by default — extensions can provide it to display clickable
|
|
65
|
+
* chat body placeholder when a chat has no messages.
|
|
66
|
+
*/
|
|
67
|
+
export declare const IChatBodyPlaceholderFactory: Token<IChatBodyPlaceholderFactory>;
|
package/lib/tokens.js
CHANGED
|
@@ -7,3 +7,15 @@ import { Token } from '@lumino/coreutils';
|
|
|
7
7
|
* A chat tracker token.
|
|
8
8
|
*/
|
|
9
9
|
export const IChatTracker = new Token('@jupyter/chat:IChatTracker', 'The chat widget tracker');
|
|
10
|
+
/**
|
|
11
|
+
* The token for the placeholder factory.
|
|
12
|
+
* Not provided by default — extensions can provide it to replace the default
|
|
13
|
+
* placeholder shown when no chat is open in the multi-chat panel.
|
|
14
|
+
*/
|
|
15
|
+
export const IChatPlaceholderFactory = new Token('@jupyter/chat:IChatPlaceholderFactory', 'The placeholder factory for the multi-chat panel');
|
|
16
|
+
/**
|
|
17
|
+
* The token for the chat body placeholder factory.
|
|
18
|
+
* Not provided by default — extensions can provide it to display clickable
|
|
19
|
+
* chat body placeholder when a chat has no messages.
|
|
20
|
+
*/
|
|
21
|
+
export const IChatBodyPlaceholderFactory = new Token('@jupyter/chat:IChatBodyPlaceholderFactory', 'The chat body placeholder factory for empty chats');
|
package/lib/types.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface IConfig {
|
|
|
52
52
|
* Whether to display deleted messages.
|
|
53
53
|
*/
|
|
54
54
|
showDeleted?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether to display the 'send with selection' button.
|
|
57
|
+
*/
|
|
58
|
+
sendWithSelection?: boolean;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* Mime model body type, a partial mime bundle model containing at least the data.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import { ReactWidget } from '@jupyterlab/apputils';
|
|
5
|
+
import { Notification, ReactWidget } from '@jupyterlab/apputils';
|
|
6
6
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
7
7
|
import { isCode, isMarkdown, isRaw } from '@jupyterlab/nbformat';
|
|
8
8
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
@@ -236,11 +236,13 @@ export class ChatWidget extends ReactWidget {
|
|
|
236
236
|
// Get path from first cell as all cells come from same notebook as users can only select or drag cells from one notebook at a time
|
|
237
237
|
if (!((_a = cells[0]) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
238
238
|
console.warn('No valid cells to process');
|
|
239
|
+
Notification.error('Failed to attach notebook cells: no valid cell data found.');
|
|
239
240
|
return false;
|
|
240
241
|
}
|
|
241
242
|
const notebookPath = this._findNotebookPath(String(cells[0].id));
|
|
242
243
|
if (!notebookPath) {
|
|
243
244
|
console.warn(`Cannot find notebook for dragged cells from ${cells[0].id}`);
|
|
245
|
+
Notification.error('Failed to attach notebook cells: source notebook could not be located.');
|
|
244
246
|
return false;
|
|
245
247
|
}
|
|
246
248
|
const validCells = [];
|
|
@@ -282,9 +284,11 @@ export class ChatWidget extends ReactWidget {
|
|
|
282
284
|
(_b = inputModel === null || inputModel === void 0 ? void 0 : inputModel.addAttachment) === null || _b === void 0 ? void 0 : _b.call(inputModel, attachment);
|
|
283
285
|
return !!inputModel;
|
|
284
286
|
}
|
|
287
|
+
Notification.error('Failed to attach notebook cells: no supported cells were found.');
|
|
285
288
|
}
|
|
286
289
|
catch (error) {
|
|
287
290
|
console.error('Failed to process cell drop: ', error);
|
|
291
|
+
Notification.error('Failed to attach notebook cells due to an unexpected error.');
|
|
288
292
|
}
|
|
289
293
|
return false;
|
|
290
294
|
}
|
package/lib/widgets/index.d.ts
CHANGED
package/lib/widgets/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ISignal } from '@lumino/signaling';
|
|
|
5
5
|
import { ChatWidget } from './chat-widget';
|
|
6
6
|
import { Chat, IInputToolbarRegistryFactory } from '../components';
|
|
7
7
|
import { IChatModel } from '../model';
|
|
8
|
+
import { IChatPlaceholderFactory } from '../tokens';
|
|
8
9
|
/**
|
|
9
10
|
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
10
11
|
*/
|
|
@@ -48,6 +49,10 @@ export declare class MultiChatPanel extends PanelWithToolbar {
|
|
|
48
49
|
*/
|
|
49
50
|
protected onAfterShow(msg: Message): void;
|
|
50
51
|
protected onBeforeHide(msg: Message): void;
|
|
52
|
+
/**
|
|
53
|
+
* Add a placeholder in the panel.
|
|
54
|
+
*/
|
|
55
|
+
private _addPlaceholder;
|
|
51
56
|
/**
|
|
52
57
|
* Open a specific chat by name, creating a new sidepanel widget.
|
|
53
58
|
*/
|
|
@@ -88,6 +93,7 @@ export declare class MultiChatPanel extends PanelWithToolbar {
|
|
|
88
93
|
*/
|
|
89
94
|
private _onSelectChat;
|
|
90
95
|
private _chatOpened;
|
|
96
|
+
private _chatNamesChanged;
|
|
91
97
|
private _chatOptions;
|
|
92
98
|
private _inputToolbarFactory?;
|
|
93
99
|
private _updateChatListDebouncer;
|
|
@@ -95,6 +101,7 @@ export declare class MultiChatPanel extends PanelWithToolbar {
|
|
|
95
101
|
private _getChatNames?;
|
|
96
102
|
private _openInMain?;
|
|
97
103
|
private _renameChat?;
|
|
104
|
+
private _placeholderFactory?;
|
|
98
105
|
private _openChatWidget?;
|
|
99
106
|
private _chatSelectorPopup?;
|
|
100
107
|
private _loadedModels;
|
|
@@ -144,6 +151,14 @@ export declare namespace MultiChatPanel {
|
|
|
144
151
|
* @returns - a boolean, whether the chat has been renamed or not.
|
|
145
152
|
*/
|
|
146
153
|
renameChat?: boolean | ((oldName: string) => Promise<string | null>);
|
|
154
|
+
/**
|
|
155
|
+
* An optional factory to create a placeholder widget displayed when no chat
|
|
156
|
+
* is opened. Falls back to the default placeholder if not provided.
|
|
157
|
+
*
|
|
158
|
+
* @param props - the props passed to the placeholder.
|
|
159
|
+
* @returns a widget to display as placeholder.
|
|
160
|
+
*/
|
|
161
|
+
placeholderFactory?: IChatPlaceholderFactory;
|
|
147
162
|
}
|
|
148
163
|
/**
|
|
149
164
|
* The options for the add chat method.
|
|
@@ -14,8 +14,9 @@ import { Debouncer } from '@lumino/polling';
|
|
|
14
14
|
import { Signal } from '@lumino/signaling';
|
|
15
15
|
import { Widget } from '@lumino/widgets';
|
|
16
16
|
import React, { useEffect, useRef, useState } from 'react';
|
|
17
|
-
import { ChatWidget } from './chat-widget';
|
|
18
17
|
import { ChatSelectorPopup } from './chat-selector-popup';
|
|
18
|
+
import { ChatWidget } from './chat-widget';
|
|
19
|
+
import { defaultPlaceholder } from './placeholder';
|
|
19
20
|
import { TRANSLATION_DOMAIN } from '../context';
|
|
20
21
|
import { chatIcon, readIcon } from '../icons';
|
|
21
22
|
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
@@ -49,6 +50,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
49
50
|
if (!ArrayExt.shallowEqual(Object.keys(chatNames !== null && chatNames !== void 0 ? chatNames : {}), Object.keys(this._chatNames))) {
|
|
50
51
|
this._chatNames = chatNames !== null && chatNames !== void 0 ? chatNames : {};
|
|
51
52
|
(_b = this._chatSelectorPopup) === null || _b === void 0 ? void 0 : _b.updateChats(Object.keys(this._chatNames));
|
|
53
|
+
this._chatNamesChanged.emit(this._chatNames);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
catch (e) {
|
|
@@ -65,7 +67,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
65
67
|
this._loadedModels.set(change.new, model);
|
|
66
68
|
this._loadedModels.delete(change.old);
|
|
67
69
|
(_a = this._chatSelectorPopup) === null || _a === void 0 ? void 0 : _a.setLoadedModels(this.getLoadedModelNames());
|
|
68
|
-
if (((_b = this.
|
|
70
|
+
if (((_b = this.current) === null || _b === void 0 ? void 0 : _b.model.name) === model.name) {
|
|
69
71
|
(_c = this._chatSelectorPopup) === null || _c === void 0 ? void 0 : _c.setCurrentChat(change.new);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -91,6 +93,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
91
93
|
(_a = this._chatSelectorPopup) === null || _a === void 0 ? void 0 : _a.hide();
|
|
92
94
|
};
|
|
93
95
|
this._chatOpened = new Signal(this);
|
|
96
|
+
this._chatNamesChanged = new Signal(this);
|
|
94
97
|
this._loadedModels = new Map();
|
|
95
98
|
this._chatNames = {};
|
|
96
99
|
this._visibilityChanged = new Signal(this);
|
|
@@ -106,6 +109,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
106
109
|
this._createModel = options.createModel;
|
|
107
110
|
this._openInMain = options.openInMain;
|
|
108
111
|
this._renameChat = options.renameChat;
|
|
112
|
+
this._placeholderFactory = options.placeholderFactory;
|
|
109
113
|
if (this._createModel) {
|
|
110
114
|
// Add chat button calls the createChat callback
|
|
111
115
|
const addChat = new ToolbarButton({
|
|
@@ -137,12 +141,16 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
137
141
|
// Insert the toolbar as first child.
|
|
138
142
|
this.insertWidget(0, this.toolbar);
|
|
139
143
|
this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
|
|
144
|
+
// Add the placeholder by default.
|
|
145
|
+
this._addPlaceholder();
|
|
140
146
|
}
|
|
141
147
|
/**
|
|
142
148
|
* The currently displayed chat widget.
|
|
143
149
|
*/
|
|
144
150
|
get current() {
|
|
145
|
-
return this._currentWidget
|
|
151
|
+
return this._currentWidget instanceof SidePanelWidget
|
|
152
|
+
? this._currentWidget
|
|
153
|
+
: undefined;
|
|
146
154
|
}
|
|
147
155
|
/**
|
|
148
156
|
* A signal emitting when a chat widget is opened in the panel.
|
|
@@ -196,14 +204,15 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
196
204
|
const model = this._loadedModels.get(name);
|
|
197
205
|
if (model) {
|
|
198
206
|
// If this is the currently displayed chat, remove it.
|
|
199
|
-
if (((_a = this.
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
207
|
+
if (((_a = this.current) === null || _a === void 0 ? void 0 : _a.model) === model) {
|
|
208
|
+
this.current.nameChanged.disconnect(this._modelNameChanged);
|
|
209
|
+
this.current.dispose();
|
|
202
210
|
this._currentWidget = undefined;
|
|
203
211
|
// Clear current chat in selector
|
|
204
212
|
if (this._chatSelectorPopup) {
|
|
205
213
|
this._chatSelectorPopup.setCurrentChat(null);
|
|
206
214
|
}
|
|
215
|
+
this._addPlaceholder();
|
|
207
216
|
}
|
|
208
217
|
model.dispose();
|
|
209
218
|
this._loadedModels.delete(name);
|
|
@@ -219,17 +228,40 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
219
228
|
onBeforeHide(msg) {
|
|
220
229
|
this._visibilityChanged.emit(false);
|
|
221
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Add a placeholder in the panel.
|
|
233
|
+
*/
|
|
234
|
+
_addPlaceholder() {
|
|
235
|
+
const props = {
|
|
236
|
+
chatNames: this._chatNames,
|
|
237
|
+
open: this._onSelectChat,
|
|
238
|
+
onCreate: this._createModel
|
|
239
|
+
? async () => {
|
|
240
|
+
const args = await this._createModel();
|
|
241
|
+
this.open(args);
|
|
242
|
+
}
|
|
243
|
+
: undefined,
|
|
244
|
+
chatNamesChanged: this._chatNamesChanged,
|
|
245
|
+
trans: this._trans
|
|
246
|
+
};
|
|
247
|
+
const placeholder = this._placeholderFactory
|
|
248
|
+
? this._placeholderFactory.create(props)
|
|
249
|
+
: new defaultPlaceholder(props);
|
|
250
|
+
this.addWidget(placeholder);
|
|
251
|
+
this._currentWidget = placeholder;
|
|
252
|
+
}
|
|
222
253
|
/**
|
|
223
254
|
* Open a specific chat by name, creating a new sidepanel widget.
|
|
224
255
|
*/
|
|
225
256
|
_open(name) {
|
|
257
|
+
var _a, _b;
|
|
226
258
|
const model = this._loadedModels.get(name);
|
|
227
259
|
if (!model) {
|
|
228
260
|
return;
|
|
229
261
|
}
|
|
230
|
-
// Dispose current chat widget
|
|
262
|
+
// Dispose of the current chat widget or placeholder.
|
|
231
263
|
if (this._currentWidget) {
|
|
232
|
-
this.
|
|
264
|
+
(_a = this.current) === null || _a === void 0 ? void 0 : _a.nameChanged.disconnect(this._modelNameChanged);
|
|
233
265
|
this._currentWidget.dispose();
|
|
234
266
|
}
|
|
235
267
|
// Create the toolbar registry.
|
|
@@ -259,7 +291,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
259
291
|
this.addWidget(widget);
|
|
260
292
|
this.update();
|
|
261
293
|
this._currentWidget = widget;
|
|
262
|
-
this.
|
|
294
|
+
(_b = this.current) === null || _b === void 0 ? void 0 : _b.nameChanged.connect(this._modelNameChanged);
|
|
263
295
|
// Update selector to show current chat
|
|
264
296
|
if (this._chatSelectorPopup) {
|
|
265
297
|
this._chatSelectorPopup.setCurrentChat(name);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { ISignal } from '@lumino/signaling';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
/**
|
|
6
|
+
* The default placeholder widget.
|
|
7
|
+
*/
|
|
8
|
+
export declare class defaultPlaceholder extends ReactWidget {
|
|
9
|
+
constructor(options: Placeholder.IProps);
|
|
10
|
+
render(): React.JSX.Element;
|
|
11
|
+
private _props;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The placeholder namespace.
|
|
15
|
+
*/
|
|
16
|
+
export declare namespace Placeholder {
|
|
17
|
+
/**
|
|
18
|
+
* The options of the placeholder widget.
|
|
19
|
+
*/
|
|
20
|
+
interface IProps {
|
|
21
|
+
/**
|
|
22
|
+
* The initial chat names.
|
|
23
|
+
*/
|
|
24
|
+
chatNames: {
|
|
25
|
+
[name: string]: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A callback to open an existing chat by name.
|
|
29
|
+
*
|
|
30
|
+
* @param name - the display name of the chat to open.
|
|
31
|
+
*/
|
|
32
|
+
open: (name: string) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* A callback to create and open a new chat.
|
|
35
|
+
*/
|
|
36
|
+
onCreate?: () => Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* An optional signal emitting when the chat list changes.
|
|
39
|
+
*/
|
|
40
|
+
chatNamesChanged?: ISignal<any, {
|
|
41
|
+
[name: string]: string;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* An optional translation bundle.
|
|
45
|
+
*/
|
|
46
|
+
trans?: IRenderMime.TranslationBundle;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { addIcon, ReactWidget, ToolbarButtonComponent } from '@jupyterlab/ui-components';
|
|
6
|
+
import React, { useEffect, useState } from 'react';
|
|
7
|
+
import { useTranslator } from '../context';
|
|
8
|
+
/**
|
|
9
|
+
* The default placeholder widget.
|
|
10
|
+
*/
|
|
11
|
+
export class defaultPlaceholder extends ReactWidget {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this._props = options;
|
|
15
|
+
}
|
|
16
|
+
render() {
|
|
17
|
+
return React.createElement(PlaceholderComponent, { ...this._props });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The default placeholder component.
|
|
22
|
+
*/
|
|
23
|
+
const PlaceholderComponent = (props) => {
|
|
24
|
+
var _a;
|
|
25
|
+
const trans = (_a = props.trans) !== null && _a !== void 0 ? _a : useTranslator();
|
|
26
|
+
const [chatNames, setChatNames] = useState(props.chatNames);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!props.chatNamesChanged) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const onChanged = (_, names) => {
|
|
32
|
+
setChatNames(names);
|
|
33
|
+
};
|
|
34
|
+
props.chatNamesChanged.connect(onChanged);
|
|
35
|
+
return () => {
|
|
36
|
+
props.chatNamesChanged.disconnect(onChanged);
|
|
37
|
+
};
|
|
38
|
+
}, [props.chatNamesChanged]);
|
|
39
|
+
const names = Object.keys(chatNames).sort((a, b) => a.localeCompare(b));
|
|
40
|
+
return (React.createElement("div", { className: "jp-chat-placeholder" },
|
|
41
|
+
React.createElement("h3", null, trans.__('No chat opened')),
|
|
42
|
+
props.onCreate && (React.createElement("div", { className: "jp-chat-placeholder-hint" },
|
|
43
|
+
React.createElement(ToolbarButtonComponent, { label: trans.__('New chat'), icon: addIcon, onClick: props.onCreate, tooltip: trans.__('Create a new chat'), className: "jp-chat-add" }))),
|
|
44
|
+
names.length > 0 && (React.createElement("div", { className: "jp-chat-placeholder-list" },
|
|
45
|
+
React.createElement("p", null, trans.__('Open an existing chat:')),
|
|
46
|
+
names.map(name => (React.createElement("div", { key: name },
|
|
47
|
+
React.createElement("button", { className: "jp-chat-placeholder-chat-item", onClick: () => props.open(name) }, name))))))));
|
|
48
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"url": "https://github.com/jupyterlab/jupyter-chat.git"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "jlpm build:lib",
|
|
34
|
-
"build:prod": "jlpm clean && jlpm build:lib:prod",
|
|
33
|
+
"build": "echo 'Building @jupyter/chat' && jlpm build:lib",
|
|
34
|
+
"build:prod": "echo 'Building @jupyter/chat' && jlpm clean && jlpm build:lib:prod",
|
|
35
35
|
"build:lib": "tsc --sourceMap",
|
|
36
36
|
"build:lib:prod": "tsc",
|
|
37
37
|
"clean": "jlpm clean:lib",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
|
|
8
|
+
import { ChatWidget } from '../widgets/chat-widget';
|
|
9
|
+
import { IChatBodyPlaceholderFactory } from '../tokens';
|
|
10
|
+
import { IChatModel } from '../model';
|
|
11
|
+
import { MockChatModel } from './mocks';
|
|
12
|
+
|
|
13
|
+
describe('ChatBodyPlaceholder', () => {
|
|
14
|
+
let model: IChatModel;
|
|
15
|
+
let rmRegistry: RenderMimeRegistry;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
model = new MockChatModel();
|
|
19
|
+
rmRegistry = new RenderMimeRegistry();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create a widget without chatBodyPlaceholderFactory', () => {
|
|
23
|
+
const widget = new ChatWidget({ model, rmRegistry });
|
|
24
|
+
expect(widget).toBeInstanceOf(ChatWidget);
|
|
25
|
+
widget.dispose();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should create a widget with chatBodyPlaceholderFactory', () => {
|
|
29
|
+
const factory: IChatBodyPlaceholderFactory = {
|
|
30
|
+
create: jest.fn().mockReturnValue(null)
|
|
31
|
+
};
|
|
32
|
+
const widget = new ChatWidget({
|
|
33
|
+
model,
|
|
34
|
+
rmRegistry,
|
|
35
|
+
chatBodyPlaceholderFactory: factory
|
|
36
|
+
});
|
|
37
|
+
expect(widget).toBeInstanceOf(ChatWidget);
|
|
38
|
+
widget.dispose();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should call sendMessage when factory onSend is invoked', () => {
|
|
42
|
+
const factory: IChatBodyPlaceholderFactory = {
|
|
43
|
+
create: jest.fn().mockReturnValue(null)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sendSpy = jest.spyOn(model, 'sendMessage');
|
|
47
|
+
|
|
48
|
+
// Simulate what ChatBodyPlaceholder component does:
|
|
49
|
+
factory.create({ onSend: (body: string) => model.sendMessage({ body }) });
|
|
50
|
+
|
|
51
|
+
// Invoke the onSend passed to factory
|
|
52
|
+
const props = (factory.create as jest.Mock).mock
|
|
53
|
+
.calls[0][0] as IChatBodyPlaceholderFactory.IProps;
|
|
54
|
+
props.onSend('Hello world');
|
|
55
|
+
|
|
56
|
+
expect(sendSpy).toHaveBeenCalledWith({ body: 'Hello world' });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not fail when factory returns null', () => {
|
|
60
|
+
const factory: IChatBodyPlaceholderFactory = {
|
|
61
|
+
create: jest.fn().mockReturnValue(null)
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = factory.create({
|
|
65
|
+
onSend: (body: string) => model.sendMessage({ body })
|
|
66
|
+
});
|
|
67
|
+
expect(result).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
import { Widget } from '@lumino/widgets';
|
|
8
|
+
|
|
9
|
+
import { IChatPlaceholderFactory } from '../tokens';
|
|
10
|
+
import { MultiChatPanel } from '../widgets/multichat-panel';
|
|
11
|
+
import { defaultPlaceholder, Placeholder } from '../widgets/placeholder';
|
|
12
|
+
|
|
13
|
+
describe('MultiChatPanel', () => {
|
|
14
|
+
let rmRegistry: RenderMimeRegistry;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
rmRegistry = new RenderMimeRegistry();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('placeholderFactory', () => {
|
|
21
|
+
it('should use defaultPlaceholder when no factory is provided', () => {
|
|
22
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
23
|
+
const placeholder = Array.from(panel.widgets).find(
|
|
24
|
+
w => w instanceof defaultPlaceholder
|
|
25
|
+
);
|
|
26
|
+
expect(placeholder).toBeInstanceOf(defaultPlaceholder);
|
|
27
|
+
panel.dispose();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should call factory.create when a factory is provided', () => {
|
|
31
|
+
const factory: IChatPlaceholderFactory = {
|
|
32
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
33
|
+
};
|
|
34
|
+
const panel = new MultiChatPanel({
|
|
35
|
+
rmRegistry,
|
|
36
|
+
placeholderFactory: factory
|
|
37
|
+
});
|
|
38
|
+
expect(factory.create).toHaveBeenCalledTimes(1);
|
|
39
|
+
panel.dispose();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should add the widget returned by the factory to the panel', () => {
|
|
43
|
+
const customWidget = new Widget();
|
|
44
|
+
const factory: IChatPlaceholderFactory = {
|
|
45
|
+
create: jest.fn().mockReturnValue(customWidget)
|
|
46
|
+
};
|
|
47
|
+
const panel = new MultiChatPanel({
|
|
48
|
+
rmRegistry,
|
|
49
|
+
placeholderFactory: factory
|
|
50
|
+
});
|
|
51
|
+
expect(Array.from(panel.widgets)).toContain(customWidget);
|
|
52
|
+
panel.dispose();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not use defaultPlaceholder when a factory is provided', () => {
|
|
56
|
+
const factory: IChatPlaceholderFactory = {
|
|
57
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
58
|
+
};
|
|
59
|
+
const panel = new MultiChatPanel({
|
|
60
|
+
rmRegistry,
|
|
61
|
+
placeholderFactory: factory
|
|
62
|
+
});
|
|
63
|
+
const placeholder = Array.from(panel.widgets).find(
|
|
64
|
+
w => w instanceof defaultPlaceholder
|
|
65
|
+
);
|
|
66
|
+
expect(placeholder).toBeUndefined();
|
|
67
|
+
panel.dispose();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should pass empty chatNames in props when no chats are loaded', () => {
|
|
71
|
+
const factory: IChatPlaceholderFactory = {
|
|
72
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
73
|
+
};
|
|
74
|
+
const panel = new MultiChatPanel({
|
|
75
|
+
rmRegistry,
|
|
76
|
+
placeholderFactory: factory
|
|
77
|
+
});
|
|
78
|
+
const props = (factory.create as jest.Mock).mock
|
|
79
|
+
.calls[0][0] as Placeholder.IProps;
|
|
80
|
+
expect(props.chatNames).toEqual({});
|
|
81
|
+
panel.dispose();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should pass undefined onCreate when createModel is not provided', () => {
|
|
85
|
+
const factory: IChatPlaceholderFactory = {
|
|
86
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
87
|
+
};
|
|
88
|
+
const panel = new MultiChatPanel({
|
|
89
|
+
rmRegistry,
|
|
90
|
+
placeholderFactory: factory
|
|
91
|
+
});
|
|
92
|
+
const props = (factory.create as jest.Mock).mock
|
|
93
|
+
.calls[0][0] as Placeholder.IProps;
|
|
94
|
+
expect(props.onCreate).toBeUndefined();
|
|
95
|
+
panel.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should pass onCreate when createModel is provided', () => {
|
|
99
|
+
const factory: IChatPlaceholderFactory = {
|
|
100
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
101
|
+
};
|
|
102
|
+
const createModel = jest.fn().mockResolvedValue({});
|
|
103
|
+
const panel = new MultiChatPanel({
|
|
104
|
+
rmRegistry,
|
|
105
|
+
placeholderFactory: factory,
|
|
106
|
+
createModel
|
|
107
|
+
});
|
|
108
|
+
const props = (factory.create as jest.Mock).mock
|
|
109
|
+
.calls[0][0] as Placeholder.IProps;
|
|
110
|
+
expect(props.onCreate).toBeDefined();
|
|
111
|
+
panel.dispose();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should call factory.create again when the placeholder is re-added after closing a chat', async () => {
|
|
115
|
+
const factory: IChatPlaceholderFactory = {
|
|
116
|
+
create: jest.fn().mockReturnValue(new Widget())
|
|
117
|
+
};
|
|
118
|
+
const createModel = jest.fn().mockResolvedValue({});
|
|
119
|
+
const panel = new MultiChatPanel({
|
|
120
|
+
rmRegistry,
|
|
121
|
+
placeholderFactory: factory,
|
|
122
|
+
createModel
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Simulate opening and closing a chat to trigger _addPlaceholder again.
|
|
126
|
+
const { MockChatModel } = await import('./mocks');
|
|
127
|
+
const model = new MockChatModel();
|
|
128
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
129
|
+
panel.disposeLoadedModel('test-chat');
|
|
130
|
+
|
|
131
|
+
expect(factory.create).toHaveBeenCalledTimes(2);
|
|
132
|
+
panel.dispose();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
package/src/components/chat.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
IMessagePreambleRegistry
|
|
30
30
|
} from '../registers';
|
|
31
31
|
import { ChatArea } from '../types';
|
|
32
|
+
import { IChatBodyPlaceholderFactory } from '../tokens';
|
|
32
33
|
|
|
33
34
|
export function ChatBody(props: Chat.IChatProps): JSX.Element {
|
|
34
35
|
const { model } = props;
|
|
@@ -178,6 +179,11 @@ export namespace Chat {
|
|
|
178
179
|
* The welcome message.
|
|
179
180
|
*/
|
|
180
181
|
welcomeMessage?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Optional factory to create chat body placeholder shown when the chat has
|
|
184
|
+
* no messages.
|
|
185
|
+
*/
|
|
186
|
+
chatBodyPlaceholderFactory?: IChatBodyPlaceholderFactory;
|
|
181
187
|
/**
|
|
182
188
|
* The area where the chat is displayed.
|
|
183
189
|
*/
|