@jupyter/chat 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/input/chat-input.js +18 -14
- package/lib/components/input/toolbar-registry.d.ts +16 -1
- package/lib/components/input/toolbar-registry.js +10 -1
- package/lib/components/messages/messages.d.ts +1 -0
- package/lib/components/messages/messages.js +1 -1
- package/lib/input-model.d.ts +7 -6
- package/lib/input-model.js +3 -0
- package/lib/model.d.ts +15 -0
- package/lib/model.js +23 -1
- package/lib/widgets/chat-widget.js +17 -2
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +212 -0
- package/lib/widgets/multichat-panel.js +368 -0
- package/package.json +4 -3
- package/src/components/input/chat-input.tsx +27 -26
- package/src/components/input/toolbar-registry.tsx +21 -1
- package/src/components/messages/messages.tsx +1 -1
- package/src/input-model.ts +10 -6
- package/src/model.ts +33 -1
- package/src/widgets/chat-widget.tsx +22 -2
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +575 -0
- package/style/chat.css +1 -6
- package/style/input.css +65 -34
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/*
|
|
6
|
+
* Multi-chat panel for @jupyter/chat
|
|
7
|
+
* Originally adapted from jupyterlab-chat's ChatPanel
|
|
8
|
+
*/
|
|
9
|
+
import { InputDialog } from '@jupyterlab/apputils';
|
|
10
|
+
import { addIcon, closeIcon, HTMLSelect, launchIcon, PanelWithToolbar, ReactWidget, SidePanel, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
|
|
11
|
+
import { Debouncer } from '@lumino/polling';
|
|
12
|
+
import { Signal } from '@lumino/signaling';
|
|
13
|
+
import React, { useState } from 'react';
|
|
14
|
+
import { ChatWidget } from './chat-widget';
|
|
15
|
+
import { chatIcon, readIcon } from '../icons';
|
|
16
|
+
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
17
|
+
const ADD_BUTTON_CLASS = 'jp-chat-add';
|
|
18
|
+
const OPEN_SELECT_CLASS = 'jp-chat-open';
|
|
19
|
+
const SECTION_CLASS = 'jp-chat-section';
|
|
20
|
+
const TOOLBAR_CLASS = 'jp-chat-section-toolbar';
|
|
21
|
+
/**
|
|
22
|
+
* Generic sidepanel widget including multiple chats and the add chat button.
|
|
23
|
+
*/
|
|
24
|
+
export class MultiChatPanel extends SidePanel {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
super(options);
|
|
27
|
+
/**
|
|
28
|
+
* Update the list of available chats.
|
|
29
|
+
*/
|
|
30
|
+
this._updateChatList = async () => {
|
|
31
|
+
var _a;
|
|
32
|
+
try {
|
|
33
|
+
const chatNames = await ((_a = this._getChatNames) === null || _a === void 0 ? void 0 : _a.call(this));
|
|
34
|
+
this._chatNamesChanged.emit(chatNames !== null && chatNames !== void 0 ? chatNames : {});
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error('Error getting chat files', e);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
this._chatNamesChanged = new Signal(this);
|
|
41
|
+
this._sectionAdded = new Signal(this);
|
|
42
|
+
this.id = 'jupyter-chat::multi-chat-panel';
|
|
43
|
+
this.title.icon = chatIcon;
|
|
44
|
+
this.title.caption = 'Jupyter Chat'; // TODO: i18n/
|
|
45
|
+
this.addClass(SIDEPANEL_CLASS);
|
|
46
|
+
this._rmRegistry = options.rmRegistry;
|
|
47
|
+
this._themeManager = options.themeManager;
|
|
48
|
+
this._chatCommandRegistry = options.chatCommandRegistry;
|
|
49
|
+
this._attachmentOpenerRegistry = options.attachmentOpenerRegistry;
|
|
50
|
+
this._inputToolbarFactory = options.inputToolbarFactory;
|
|
51
|
+
this._messageFooterRegistry = options.messageFooterRegistry;
|
|
52
|
+
this._welcomeMessage = options.welcomeMessage;
|
|
53
|
+
this._getChatNames = options.getChatNames;
|
|
54
|
+
this._createModel = options.createModel;
|
|
55
|
+
this._openInMain = options.openInMain;
|
|
56
|
+
this._renameChat = options.renameChat;
|
|
57
|
+
if (this._createModel) {
|
|
58
|
+
// Add chat button calls the createChat callback
|
|
59
|
+
const addChat = new ToolbarButton({
|
|
60
|
+
onClick: async () => {
|
|
61
|
+
const addChatArgs = await this._createModel();
|
|
62
|
+
this.addChat(addChatArgs);
|
|
63
|
+
},
|
|
64
|
+
icon: addIcon,
|
|
65
|
+
label: 'Chat',
|
|
66
|
+
tooltip: 'Add a new chat'
|
|
67
|
+
});
|
|
68
|
+
addChat.addClass(ADD_BUTTON_CLASS);
|
|
69
|
+
this.toolbar.addItem('createChat', addChat);
|
|
70
|
+
}
|
|
71
|
+
if (this._getChatNames && this._createModel) {
|
|
72
|
+
// Chat select dropdown
|
|
73
|
+
this._openChatWidget = ReactWidget.create(React.createElement(ChatSelect, { chatNamesChanged: this._chatNamesChanged, handleChange: this._chatSelected.bind(this) }));
|
|
74
|
+
this._openChatWidget.addClass(OPEN_SELECT_CLASS);
|
|
75
|
+
this.toolbar.addItem('openChat', this._openChatWidget);
|
|
76
|
+
}
|
|
77
|
+
const content = this.content;
|
|
78
|
+
content.expansionToggled.connect(this._onExpansionToggled, this);
|
|
79
|
+
this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The sections of the side panel.
|
|
83
|
+
*/
|
|
84
|
+
get sections() {
|
|
85
|
+
return this.widgets;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A signal emitting when a section is added to the panel.
|
|
89
|
+
*/
|
|
90
|
+
get sectionAdded() {
|
|
91
|
+
return this._sectionAdded;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Add a new widget to the chat panel.
|
|
95
|
+
*
|
|
96
|
+
* @param model - the model of the chat widget
|
|
97
|
+
* @param displayName - the name of the chat.
|
|
98
|
+
*/
|
|
99
|
+
addChat(args) {
|
|
100
|
+
const { model, displayName } = args;
|
|
101
|
+
if (!model) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.openIfExists(model.name)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const content = this.content;
|
|
108
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
109
|
+
content.collapse(i);
|
|
110
|
+
}
|
|
111
|
+
// Create the toolbar registry.
|
|
112
|
+
let inputToolbarRegistry;
|
|
113
|
+
if (this._inputToolbarFactory) {
|
|
114
|
+
inputToolbarRegistry = this._inputToolbarFactory.create();
|
|
115
|
+
}
|
|
116
|
+
// Create a new widget.
|
|
117
|
+
const widget = new ChatWidget({
|
|
118
|
+
model,
|
|
119
|
+
rmRegistry: this._rmRegistry,
|
|
120
|
+
themeManager: this._themeManager,
|
|
121
|
+
chatCommandRegistry: this._chatCommandRegistry,
|
|
122
|
+
attachmentOpenerRegistry: this._attachmentOpenerRegistry,
|
|
123
|
+
inputToolbarRegistry,
|
|
124
|
+
messageFooterRegistry: this._messageFooterRegistry,
|
|
125
|
+
welcomeMessage: this._welcomeMessage
|
|
126
|
+
});
|
|
127
|
+
const section = new ChatSection({
|
|
128
|
+
widget,
|
|
129
|
+
openInMain: this._openInMain,
|
|
130
|
+
renameChat: this._renameChat,
|
|
131
|
+
displayName
|
|
132
|
+
});
|
|
133
|
+
this.addWidget(section);
|
|
134
|
+
content.expand(this.widgets.length - 1);
|
|
135
|
+
this._sectionAdded.emit(section);
|
|
136
|
+
return widget;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Invoke the update of the list of available chats.
|
|
140
|
+
*/
|
|
141
|
+
updateChatList() {
|
|
142
|
+
this._updateChatListDebouncer.invoke();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Open a chat if it exists in the side panel.
|
|
146
|
+
*
|
|
147
|
+
* @param name - the name of the chat.
|
|
148
|
+
* @returns a boolean, whether the chat existed in the side panel or not.
|
|
149
|
+
*/
|
|
150
|
+
openIfExists(name) {
|
|
151
|
+
const index = this._getChatIndex(name);
|
|
152
|
+
if (index > -1) {
|
|
153
|
+
this._expandChat(index);
|
|
154
|
+
}
|
|
155
|
+
return index > -1;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A message handler invoked on an `'after-attach'` message.
|
|
159
|
+
*/
|
|
160
|
+
onAfterAttach() {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
(_b = (_a = this._openChatWidget) === null || _a === void 0 ? void 0 : _a.renderPromise) === null || _b === void 0 ? void 0 : _b.then(() => this.updateChatList());
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Return the index of the chat in the list (-1 if not opened).
|
|
166
|
+
*
|
|
167
|
+
* @param name - the chat name.
|
|
168
|
+
*/
|
|
169
|
+
_getChatIndex(name) {
|
|
170
|
+
return this.sections.findIndex(section => { var _a; return ((_a = section.model) === null || _a === void 0 ? void 0 : _a.name) === name; });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Expand the chat from its index.
|
|
174
|
+
*/
|
|
175
|
+
_expandChat(index) {
|
|
176
|
+
if (!this.widgets[index].isVisible) {
|
|
177
|
+
this.content.expand(index);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Handle `change` events for the HTMLSelect component.
|
|
182
|
+
*/
|
|
183
|
+
async _chatSelected(event) {
|
|
184
|
+
const selection = event.target.value;
|
|
185
|
+
if (selection === '-') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this._createModel) {
|
|
189
|
+
const addChatArgs = await this._createModel(selection);
|
|
190
|
+
this.addChat(addChatArgs);
|
|
191
|
+
}
|
|
192
|
+
event.target.selectedIndex = 0;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Triggered when a section is toggled. If the section is opened, all others
|
|
196
|
+
* sections are closed.
|
|
197
|
+
*/
|
|
198
|
+
_onExpansionToggled(panel, index) {
|
|
199
|
+
if (!this.widgets[index].isVisible) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (let i = 0; i < this.widgets.length; i++) {
|
|
203
|
+
if (i !== index) {
|
|
204
|
+
panel.collapse(i);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The chat section containing a chat widget.
|
|
211
|
+
*/
|
|
212
|
+
export class ChatSection extends PanelWithToolbar {
|
|
213
|
+
/**
|
|
214
|
+
* Constructor of the chat section.
|
|
215
|
+
*/
|
|
216
|
+
constructor(options) {
|
|
217
|
+
var _a, _b, _c, _d, _e;
|
|
218
|
+
super(options);
|
|
219
|
+
/**
|
|
220
|
+
* Change the title when messages are unread.
|
|
221
|
+
*
|
|
222
|
+
* TODO: fix it upstream in @jupyterlab/ui-components.
|
|
223
|
+
* Updating the title create a new Title widget, but does not attach again the
|
|
224
|
+
* toolbar. The toolbar is attached only when the title widget is attached the first
|
|
225
|
+
* time.
|
|
226
|
+
*/
|
|
227
|
+
this._unreadChanged = (_, unread) => {
|
|
228
|
+
this._markAsRead.enabled = unread.length > 0;
|
|
229
|
+
};
|
|
230
|
+
this._spinner = new Spinner();
|
|
231
|
+
this._chatWidget = options.widget;
|
|
232
|
+
this.addWidget(this._chatWidget);
|
|
233
|
+
this.addWidget(this._spinner);
|
|
234
|
+
this.addClass(SECTION_CLASS);
|
|
235
|
+
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
236
|
+
this._displayName =
|
|
237
|
+
(_b = (_a = options.displayName) !== null && _a !== void 0 ? _a : options.widget.model.name) !== null && _b !== void 0 ? _b : 'Chat';
|
|
238
|
+
this._updateTitle();
|
|
239
|
+
this._markAsRead = new ToolbarButton({
|
|
240
|
+
icon: readIcon,
|
|
241
|
+
iconLabel: 'Mark chat as read',
|
|
242
|
+
className: 'jp-mod-styled',
|
|
243
|
+
onClick: () => {
|
|
244
|
+
if (this.model) {
|
|
245
|
+
this.model.unreadMessages = [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
this.toolbar.addItem('markRead', this._markAsRead);
|
|
250
|
+
if (options.renameChat) {
|
|
251
|
+
const renameButton = new ToolbarButton({
|
|
252
|
+
iconClass: 'jp-EditIcon',
|
|
253
|
+
iconLabel: 'Rename chat',
|
|
254
|
+
className: 'jp-mod-styled',
|
|
255
|
+
onClick: async () => {
|
|
256
|
+
var _a, _b;
|
|
257
|
+
const oldName = (_a = this.model.name) !== null && _a !== void 0 ? _a : 'Chat';
|
|
258
|
+
const result = await InputDialog.getText({
|
|
259
|
+
title: 'Rename Chat',
|
|
260
|
+
text: this.model.name,
|
|
261
|
+
placeholder: 'new-name'
|
|
262
|
+
});
|
|
263
|
+
if (!result.button.accept) {
|
|
264
|
+
return; // user cancelled
|
|
265
|
+
}
|
|
266
|
+
const newName = result.value;
|
|
267
|
+
if (this.model && newName && newName !== oldName) {
|
|
268
|
+
if (await ((_b = options.renameChat) === null || _b === void 0 ? void 0 : _b.call(options, oldName, newName))) {
|
|
269
|
+
this.model.name = newName;
|
|
270
|
+
this._displayName = newName;
|
|
271
|
+
this._updateTitle();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
this.toolbar.addItem('rename', renameButton);
|
|
277
|
+
}
|
|
278
|
+
if (options.openInMain) {
|
|
279
|
+
const moveToMain = new ToolbarButton({
|
|
280
|
+
icon: launchIcon,
|
|
281
|
+
iconLabel: 'Move the chat to the main area',
|
|
282
|
+
className: 'jp-mod-styled',
|
|
283
|
+
onClick: () => {
|
|
284
|
+
var _a;
|
|
285
|
+
const name = this.model.name;
|
|
286
|
+
this.model.dispose();
|
|
287
|
+
(_a = options.openInMain) === null || _a === void 0 ? void 0 : _a.call(options, name);
|
|
288
|
+
this.dispose();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
this.toolbar.addItem('moveMain', moveToMain);
|
|
292
|
+
}
|
|
293
|
+
const closeButton = new ToolbarButton({
|
|
294
|
+
icon: closeIcon,
|
|
295
|
+
iconLabel: 'Close the chat',
|
|
296
|
+
className: 'jp-mod-styled',
|
|
297
|
+
onClick: () => {
|
|
298
|
+
this.model.dispose();
|
|
299
|
+
this.dispose();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
this.toolbar.addItem('close', closeButton);
|
|
303
|
+
(_c = this.model.unreadChanged) === null || _c === void 0 ? void 0 : _c.connect(this._unreadChanged);
|
|
304
|
+
this._markAsRead.enabled = ((_e = (_d = this.model) === null || _d === void 0 ? void 0 : _d.unreadMessages.length) !== null && _e !== void 0 ? _e : 0) > 0;
|
|
305
|
+
options.widget.node.style.height = '100%';
|
|
306
|
+
/**
|
|
307
|
+
* Remove the spinner when the chat is ready.
|
|
308
|
+
*/
|
|
309
|
+
this.model.ready.then(() => {
|
|
310
|
+
this._spinner.dispose();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The display name.
|
|
315
|
+
*/
|
|
316
|
+
get displayName() {
|
|
317
|
+
return this._displayName;
|
|
318
|
+
}
|
|
319
|
+
set displayName(value) {
|
|
320
|
+
this._displayName = value;
|
|
321
|
+
this._updateTitle();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The chat widget of the section.
|
|
325
|
+
*/
|
|
326
|
+
get widget() {
|
|
327
|
+
return this._chatWidget;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The model of the widget.
|
|
331
|
+
*/
|
|
332
|
+
get model() {
|
|
333
|
+
return this._chatWidget.model;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Dispose of the resources held by the widget.
|
|
337
|
+
*/
|
|
338
|
+
dispose() {
|
|
339
|
+
var _a;
|
|
340
|
+
const model = this.model;
|
|
341
|
+
if (model) {
|
|
342
|
+
(_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.disconnect(this._unreadChanged);
|
|
343
|
+
}
|
|
344
|
+
super.dispose();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* * Update the section’s title based on the chat name.
|
|
348
|
+
* */
|
|
349
|
+
_updateTitle() {
|
|
350
|
+
this.title.label = this._displayName;
|
|
351
|
+
this.title.caption = this._displayName;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* A component to select a chat from the drive.
|
|
356
|
+
*/
|
|
357
|
+
function ChatSelect({ chatNamesChanged, handleChange }) {
|
|
358
|
+
// An object associating a chat name to its path. Both are purely indicative, the name
|
|
359
|
+
// is the section title and the path is used as caption.
|
|
360
|
+
const [chatNames, setChatNames] = useState({});
|
|
361
|
+
// Update the chat list.
|
|
362
|
+
chatNamesChanged.connect((_, chatNames) => {
|
|
363
|
+
setChatNames(chatNames);
|
|
364
|
+
});
|
|
365
|
+
return (React.createElement(HTMLSelect, { onChange: handleChange, value: "-" },
|
|
366
|
+
React.createElement("option", { value: "-" }, "Open a chat"),
|
|
367
|
+
Object.keys(chatNames).map(name => (React.createElement("option", { value: chatNames[name] }, name)))));
|
|
368
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -60,9 +60,10 @@
|
|
|
60
60
|
"@lumino/commands": "^2.0.0",
|
|
61
61
|
"@lumino/coreutils": "^2.0.0",
|
|
62
62
|
"@lumino/disposable": "^2.0.0",
|
|
63
|
+
"@lumino/polling": "^2.0.0",
|
|
63
64
|
"@lumino/signaling": "^2.0.0",
|
|
64
|
-
"@mui/icons-material": "^
|
|
65
|
-
"@mui/material": "^
|
|
65
|
+
"@mui/icons-material": "^7.3.2",
|
|
66
|
+
"@mui/material": "^7.3.2",
|
|
66
67
|
"clsx": "^2.1.0",
|
|
67
68
|
"react": "^18.2.0",
|
|
68
69
|
"react-dom": "^18.2.0"
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
Autocomplete,
|
|
8
8
|
AutocompleteInputChangeReason,
|
|
9
9
|
Box,
|
|
10
|
-
InputAdornment,
|
|
11
10
|
SxProps,
|
|
12
11
|
TextField,
|
|
13
|
-
Theme
|
|
12
|
+
Theme,
|
|
13
|
+
Toolbar
|
|
14
14
|
} from '@mui/material';
|
|
15
15
|
import clsx from 'clsx';
|
|
16
16
|
import React, { useEffect, useRef, useState } from 'react';
|
|
@@ -26,6 +26,8 @@ import { IChatCommandRegistry } from '../../registers';
|
|
|
26
26
|
import { IAttachment } from '../../types';
|
|
27
27
|
|
|
28
28
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
29
|
+
const INPUT_TEXTFIELD_CLASS = 'jp-chat-input-textfield';
|
|
30
|
+
const INPUT_COMPONENT_CLASS = 'jp-chat-input-component';
|
|
29
31
|
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
30
32
|
|
|
31
33
|
export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
@@ -185,7 +187,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
185
187
|
<Autocomplete
|
|
186
188
|
{...chatCommands.autocompleteProps}
|
|
187
189
|
// ensure the autocomplete popup always renders on top
|
|
188
|
-
|
|
190
|
+
slotProps={{
|
|
189
191
|
popper: {
|
|
190
192
|
placement: 'top'
|
|
191
193
|
},
|
|
@@ -193,12 +195,12 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
193
195
|
sx: {
|
|
194
196
|
border: '1px solid lightgray'
|
|
195
197
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
},
|
|
199
|
+
listbox: {
|
|
200
|
+
sx: {
|
|
201
|
+
'& .MuiAutocomplete-option': {
|
|
202
|
+
padding: 2
|
|
203
|
+
}
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
}}
|
|
@@ -206,7 +208,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
206
208
|
<TextField
|
|
207
209
|
{...params}
|
|
208
210
|
fullWidth
|
|
209
|
-
variant="
|
|
211
|
+
variant="filled"
|
|
212
|
+
className={INPUT_TEXTFIELD_CLASS}
|
|
210
213
|
multiline
|
|
211
214
|
onKeyDown={handleKeyDown}
|
|
212
215
|
placeholder="Start chatting"
|
|
@@ -215,23 +218,13 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
215
218
|
onSelect={() =>
|
|
216
219
|
(model.cursorIndex = inputRef.current?.selectionStart ?? null)
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<item.element
|
|
224
|
-
model={model}
|
|
225
|
-
chatCommandRegistry={props.chatCommandRegistry}
|
|
226
|
-
/>
|
|
227
|
-
))}
|
|
228
|
-
</InputAdornment>
|
|
229
|
-
)
|
|
230
|
-
}}
|
|
231
|
-
FormHelperTextProps={{
|
|
232
|
-
sx: { marginLeft: 'auto', marginRight: 0 }
|
|
221
|
+
slotProps={{
|
|
222
|
+
input: {
|
|
223
|
+
...params.InputProps,
|
|
224
|
+
className: INPUT_COMPONENT_CLASS
|
|
225
|
+
}
|
|
233
226
|
}}
|
|
234
|
-
|
|
227
|
+
label={input.length > 2 ? helperText : ' '}
|
|
235
228
|
/>
|
|
236
229
|
)}
|
|
237
230
|
inputValue={input}
|
|
@@ -248,6 +241,14 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
248
241
|
}
|
|
249
242
|
}}
|
|
250
243
|
/>
|
|
244
|
+
<Toolbar className={INPUT_TOOLBAR_CLASS}>
|
|
245
|
+
{toolbarElements.map(item => (
|
|
246
|
+
<item.element
|
|
247
|
+
model={model}
|
|
248
|
+
chatCommandRegistry={props.chatCommandRegistry}
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
251
|
+
</Toolbar>
|
|
251
252
|
</Box>
|
|
252
253
|
);
|
|
253
254
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
5
7
|
import * as React from 'react';
|
|
6
8
|
|
|
7
9
|
import { AttachButton, CancelButton, SendButton } from './buttons';
|
|
8
10
|
import { IInputModel } from '../../input-model';
|
|
9
|
-
import { ISignal, Signal } from '@lumino/signaling';
|
|
10
11
|
import { IChatCommandRegistry } from '../../registers';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -166,3 +167,22 @@ export namespace InputToolbarRegistry {
|
|
|
166
167
|
return registry;
|
|
167
168
|
}
|
|
168
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A factory interface for creating a new Input Toolbar Registry
|
|
173
|
+
* for each Chat Panel.
|
|
174
|
+
*/
|
|
175
|
+
export interface IInputToolbarRegistryFactory {
|
|
176
|
+
/**
|
|
177
|
+
* Create a new input toolbar registry instance.
|
|
178
|
+
*/
|
|
179
|
+
create: () => IInputToolbarRegistry;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The token of the factory to create an input toolbar registry.
|
|
184
|
+
*/
|
|
185
|
+
export const IInputToolbarRegistryFactory =
|
|
186
|
+
new Token<IInputToolbarRegistryFactory>(
|
|
187
|
+
'@jupyter/chat:IInputToolbarRegistryFactory'
|
|
188
|
+
);
|
|
@@ -21,8 +21,8 @@ import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
|
|
|
21
21
|
import { IChatModel } from '../../model';
|
|
22
22
|
import { IChatMessage, IUser } from '../../types';
|
|
23
23
|
|
|
24
|
+
export const MESSAGE_CLASS = 'jp-chat-message';
|
|
24
25
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
25
|
-
const MESSAGE_CLASS = 'jp-chat-message';
|
|
26
26
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
27
27
|
|
|
28
28
|
/**
|
package/src/input-model.ts
CHANGED
|
@@ -16,9 +16,9 @@ import { IAttachment, INotebookAttachment, IUser } from './types';
|
|
|
16
16
|
*/
|
|
17
17
|
export interface IInputModel extends IDisposable {
|
|
18
18
|
/**
|
|
19
|
-
* The chat context (a
|
|
19
|
+
* The chat context (a subset of the chat model).
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
chatContext?: IChatContext;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Function to send a message.
|
|
@@ -175,10 +175,14 @@ export class InputModel implements IInputModel {
|
|
|
175
175
|
/**
|
|
176
176
|
* The chat context (a readonly subset of the chat model);
|
|
177
177
|
*/
|
|
178
|
-
get chatContext(): IChatContext {
|
|
178
|
+
get chatContext(): IChatContext | undefined {
|
|
179
179
|
return this._chatContext;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
set chatContext(value: IChatContext | undefined) {
|
|
183
|
+
this._chatContext = value;
|
|
184
|
+
}
|
|
185
|
+
|
|
182
186
|
/**
|
|
183
187
|
* Function to send a message.
|
|
184
188
|
*/
|
|
@@ -468,7 +472,7 @@ export class InputModel implements IInputModel {
|
|
|
468
472
|
}
|
|
469
473
|
|
|
470
474
|
private _onSend: (input: string, model?: InputModel) => void;
|
|
471
|
-
private _chatContext
|
|
475
|
+
private _chatContext?: IChatContext;
|
|
472
476
|
private _value: string;
|
|
473
477
|
private _cursorIndex: number | null = null;
|
|
474
478
|
private _currentWord: string | null = null;
|
|
@@ -491,9 +495,9 @@ export class InputModel implements IInputModel {
|
|
|
491
495
|
export namespace InputModel {
|
|
492
496
|
export interface IOptions {
|
|
493
497
|
/**
|
|
494
|
-
* The chat context (a
|
|
498
|
+
* The chat context (a subset of the chat model).
|
|
495
499
|
*/
|
|
496
|
-
chatContext
|
|
500
|
+
chatContext?: IChatContext;
|
|
497
501
|
|
|
498
502
|
/**
|
|
499
503
|
* The function that should send the message.
|
package/src/model.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
IUser
|
|
21
21
|
} from './types';
|
|
22
22
|
import { replaceMentionToSpan } from './utils';
|
|
23
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The chat model interface.
|
|
@@ -40,6 +41,11 @@ export interface IChatModel extends IDisposable {
|
|
|
40
41
|
*/
|
|
41
42
|
unreadMessages: number[];
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The promise resolving when the model is ready.
|
|
46
|
+
*/
|
|
47
|
+
readonly ready: Promise<void>;
|
|
48
|
+
|
|
43
49
|
/**
|
|
44
50
|
* The indexes list of the messages currently in the viewport.
|
|
45
51
|
*/
|
|
@@ -226,7 +232,6 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
226
232
|
};
|
|
227
233
|
|
|
228
234
|
this._inputModel = new InputModel({
|
|
229
|
-
chatContext: this.createChatContext(),
|
|
230
235
|
activeCellManager: options.activeCellManager,
|
|
231
236
|
selectionWatcher: options.selectionWatcher,
|
|
232
237
|
documentManager: options.documentManager,
|
|
@@ -241,6 +246,12 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
241
246
|
this._activeCellManager = options.activeCellManager ?? null;
|
|
242
247
|
this._selectionWatcher = options.selectionWatcher ?? null;
|
|
243
248
|
this._documentManager = options.documentManager ?? null;
|
|
249
|
+
|
|
250
|
+
this._readyDelegate = new PromiseDelegate<void>();
|
|
251
|
+
|
|
252
|
+
this.ready.then(() => {
|
|
253
|
+
this._inputModel.chatContext = this.createChatContext();
|
|
254
|
+
});
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
/**
|
|
@@ -263,6 +274,10 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
263
274
|
this._name = value;
|
|
264
275
|
}
|
|
265
276
|
|
|
277
|
+
get disposed(): ISignal<AbstractChatModel, void> {
|
|
278
|
+
return this._disposed;
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
/**
|
|
267
282
|
* The chat messages list.
|
|
268
283
|
*/
|
|
@@ -328,6 +343,20 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
328
343
|
localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
|
|
329
344
|
}
|
|
330
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Promise that resolves when the model is ready.
|
|
348
|
+
*/
|
|
349
|
+
get ready(): Promise<void> {
|
|
350
|
+
return this._readyDelegate.promise;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Set the model as ready.
|
|
355
|
+
*/
|
|
356
|
+
protected setReady(): void {
|
|
357
|
+
this._readyDelegate.resolve();
|
|
358
|
+
}
|
|
359
|
+
|
|
331
360
|
/**
|
|
332
361
|
* The chat settings.
|
|
333
362
|
*/
|
|
@@ -484,6 +513,7 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
484
513
|
return;
|
|
485
514
|
}
|
|
486
515
|
this._isDisposed = true;
|
|
516
|
+
this._disposed.emit();
|
|
487
517
|
}
|
|
488
518
|
|
|
489
519
|
/**
|
|
@@ -677,7 +707,9 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
677
707
|
private _id: string | undefined;
|
|
678
708
|
private _name: string = '';
|
|
679
709
|
private _config: IConfig;
|
|
710
|
+
private _readyDelegate = new PromiseDelegate<void>();
|
|
680
711
|
private _inputModel: IInputModel;
|
|
712
|
+
private _disposed = new Signal<this, void>(this);
|
|
681
713
|
private _isDisposed = false;
|
|
682
714
|
private _commands?: CommandRegistry;
|
|
683
715
|
private _activeCellManager: IActiveCellManager | null;
|