@jupyter/chat 0.20.0 → 0.21.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__/model.spec.js +49 -0
- package/lib/components/attachments.js +3 -3
- package/lib/components/chat.d.ts +5 -0
- package/lib/components/code-blocks/code-toolbar.js +12 -8
- package/lib/components/code-blocks/copy-button.js +9 -7
- package/lib/components/input/buttons/attach-button.js +4 -2
- package/lib/components/input/buttons/cancel-button.js +3 -1
- package/lib/components/input/buttons/save-edit-button.js +3 -1
- package/lib/components/input/buttons/send-button.js +4 -2
- package/lib/components/input/buttons/stop-button.js +3 -1
- package/lib/components/input/chat-input.js +3 -2
- package/lib/components/messages/header.js +7 -3
- package/lib/components/messages/message-renderer.js +17 -17
- package/lib/components/messages/navigation.js +5 -4
- package/lib/components/messages/toolbar.js +4 -2
- package/lib/components/writing-indicator.js +11 -6
- package/lib/context.d.ts +7 -0
- package/lib/context.js +12 -0
- package/lib/message.d.ts +2 -1
- package/lib/message.js +3 -0
- package/lib/model.d.ts +16 -0
- package/lib/model.js +28 -8
- package/lib/types.d.ts +46 -1
- package/lib/widgets/chat-error.d.ts +2 -1
- package/lib/widgets/chat-error.js +6 -3
- package/lib/widgets/chat-selector-popup.d.ts +6 -0
- package/lib/widgets/chat-selector-popup.js +8 -5
- package/lib/widgets/chat-sidebar.js +5 -1
- package/lib/widgets/chat-widget.js +6 -1
- package/lib/widgets/multichat-panel.d.ts +6 -0
- package/lib/widgets/multichat-panel.js +21 -13
- package/package.json +2 -1
- package/src/__tests__/model.spec.ts +58 -0
- package/src/components/attachments.tsx +3 -3
- package/src/components/chat.tsx +5 -0
- package/src/components/code-blocks/code-toolbar.tsx +14 -7
- package/src/components/code-blocks/copy-button.tsx +12 -8
- package/src/components/input/buttons/attach-button.tsx +4 -2
- package/src/components/input/buttons/cancel-button.tsx +4 -1
- package/src/components/input/buttons/save-edit-button.tsx +3 -1
- package/src/components/input/buttons/send-button.tsx +4 -2
- package/src/components/input/buttons/stop-button.tsx +3 -1
- package/src/components/input/chat-input.tsx +3 -2
- package/src/components/messages/header.tsx +9 -3
- package/src/components/messages/message-renderer.tsx +17 -17
- package/src/components/messages/navigation.tsx +5 -4
- package/src/components/messages/toolbar.tsx +6 -4
- package/src/components/writing-indicator.tsx +17 -6
- package/src/context.ts +13 -0
- package/src/message.ts +4 -1
- package/src/model.ts +52 -4
- package/src/types.ts +46 -1
- package/src/widgets/chat-error.tsx +9 -5
- package/src/widgets/chat-selector-popup.tsx +21 -3
- package/src/widgets/chat-sidebar.tsx +5 -1
- package/src/widgets/chat-widget.tsx +7 -1
- package/src/widgets/multichat-panel.tsx +32 -12
package/lib/model.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
2
|
+
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
2
3
|
import { CommandRegistry } from '@lumino/commands';
|
|
3
4
|
import { IDisposable } from '@lumino/disposable';
|
|
4
5
|
import { ISignal } from '@lumino/signaling';
|
|
@@ -78,6 +79,10 @@ export interface IChatModel extends IDisposable {
|
|
|
78
79
|
* A signal emitting when the writers change.
|
|
79
80
|
*/
|
|
80
81
|
readonly writersChanged?: ISignal<IChatModel, IChatModel.IWriter[]>;
|
|
82
|
+
/**
|
|
83
|
+
* A signal emitting when a message has been updated.
|
|
84
|
+
*/
|
|
85
|
+
readonly messageChanged: ISignal<IChatModel, IMessage>;
|
|
81
86
|
/**
|
|
82
87
|
* A signal emitting when the message edition input changed change.
|
|
83
88
|
*/
|
|
@@ -254,6 +259,10 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
254
259
|
* A signal emitting when the writers change.
|
|
255
260
|
*/
|
|
256
261
|
get writersChanged(): ISignal<IChatModel, IChatModel.IWriter[]>;
|
|
262
|
+
/**
|
|
263
|
+
* A signal emitting when a message has been updated.
|
|
264
|
+
*/
|
|
265
|
+
get messageChanged(): ISignal<IChatModel, IMessage>;
|
|
257
266
|
/**
|
|
258
267
|
* A signal emitting when the message edition input changed change.
|
|
259
268
|
*/
|
|
@@ -339,6 +348,7 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
339
348
|
* unread messages count decrease.
|
|
340
349
|
*/
|
|
341
350
|
private _notify;
|
|
351
|
+
private _onMessageChanged;
|
|
342
352
|
private _messages;
|
|
343
353
|
private _unreadMessages;
|
|
344
354
|
private _lastRead;
|
|
@@ -346,6 +356,7 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
346
356
|
private _id;
|
|
347
357
|
private _name;
|
|
348
358
|
private _config;
|
|
359
|
+
protected _trans: TranslationBundle;
|
|
349
360
|
private _readyDelegate;
|
|
350
361
|
private _inputModel;
|
|
351
362
|
private _disposed;
|
|
@@ -358,6 +369,7 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
358
369
|
private _writers;
|
|
359
370
|
private _messageEditions;
|
|
360
371
|
private _messagesUpdated;
|
|
372
|
+
private _messageChanged;
|
|
361
373
|
private _configChanged;
|
|
362
374
|
private _unreadChanged;
|
|
363
375
|
private _viewportChanged;
|
|
@@ -384,6 +396,10 @@ export declare namespace IChatModel {
|
|
|
384
396
|
* Commands registry.
|
|
385
397
|
*/
|
|
386
398
|
commands?: CommandRegistry;
|
|
399
|
+
/**
|
|
400
|
+
* The translator for internationalization.
|
|
401
|
+
*/
|
|
402
|
+
translator?: ITranslator;
|
|
387
403
|
/**
|
|
388
404
|
* Active cell manager.
|
|
389
405
|
*/
|
package/lib/model.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
5
6
|
import { ArrayExt } from '@lumino/algorithm';
|
|
6
7
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
8
|
import { Signal } from '@lumino/signaling';
|
|
9
|
+
import { TRANSLATION_DOMAIN } from './context';
|
|
8
10
|
import { InputModel } from './input-model';
|
|
9
11
|
import { Message } from './message';
|
|
10
12
|
/**
|
|
@@ -18,7 +20,7 @@ export class AbstractChatModel {
|
|
|
18
20
|
* Create a new chat model.
|
|
19
21
|
*/
|
|
20
22
|
constructor(options = {}) {
|
|
21
|
-
var _a, _b, _c, _d;
|
|
23
|
+
var _a, _b, _c, _d, _e;
|
|
22
24
|
this._messages = [];
|
|
23
25
|
this._unreadMessages = [];
|
|
24
26
|
this._lastRead = 0;
|
|
@@ -31,6 +33,7 @@ export class AbstractChatModel {
|
|
|
31
33
|
this._writers = [];
|
|
32
34
|
this._messageEditions = new Map();
|
|
33
35
|
this._messagesUpdated = new Signal(this);
|
|
36
|
+
this._messageChanged = new Signal(this);
|
|
34
37
|
this._configChanged = new Signal(this);
|
|
35
38
|
this._unreadChanged = new Signal(this);
|
|
36
39
|
this._viewportChanged = new Signal(this);
|
|
@@ -46,6 +49,7 @@ export class AbstractChatModel {
|
|
|
46
49
|
sendTypingNotification: true,
|
|
47
50
|
...config
|
|
48
51
|
};
|
|
52
|
+
this._trans = ((_b = options.translator) !== null && _b !== void 0 ? _b : nullTranslator).load(TRANSLATION_DOMAIN);
|
|
49
53
|
this._inputModel = new InputModel({
|
|
50
54
|
activeCellManager: options.activeCellManager,
|
|
51
55
|
selectionWatcher: options.selectionWatcher,
|
|
@@ -56,9 +60,9 @@ export class AbstractChatModel {
|
|
|
56
60
|
onSend: (input) => this.sendMessage({ body: input })
|
|
57
61
|
});
|
|
58
62
|
this._commands = options.commands;
|
|
59
|
-
this._activeCellManager = (
|
|
60
|
-
this._selectionWatcher = (
|
|
61
|
-
this._documentManager = (
|
|
63
|
+
this._activeCellManager = (_c = options.activeCellManager) !== null && _c !== void 0 ? _c : null;
|
|
64
|
+
this._selectionWatcher = (_d = options.selectionWatcher) !== null && _d !== void 0 ? _d : null;
|
|
65
|
+
this._documentManager = (_e = options.documentManager) !== null && _e !== void 0 ? _e : null;
|
|
62
66
|
this._readyDelegate = new PromiseDelegate();
|
|
63
67
|
this.ready.then(() => {
|
|
64
68
|
this._inputModel.chatContext = this.createChatContext();
|
|
@@ -257,6 +261,12 @@ export class AbstractChatModel {
|
|
|
257
261
|
get writersChanged() {
|
|
258
262
|
return this._writersChanged;
|
|
259
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* A signal emitting when a message has been updated.
|
|
266
|
+
*/
|
|
267
|
+
get messageChanged() {
|
|
268
|
+
return this._messageChanged;
|
|
269
|
+
}
|
|
260
270
|
/**
|
|
261
271
|
* A signal emitting when the message edition input changed change.
|
|
262
272
|
*/
|
|
@@ -279,6 +289,7 @@ export class AbstractChatModel {
|
|
|
279
289
|
}
|
|
280
290
|
this._isDisposed = true;
|
|
281
291
|
this._disposed.emit();
|
|
292
|
+
Signal.clearData(this);
|
|
282
293
|
}
|
|
283
294
|
/**
|
|
284
295
|
* Whether the chat handler is disposed.
|
|
@@ -326,7 +337,9 @@ export class AbstractChatModel {
|
|
|
326
337
|
// Format the messages.
|
|
327
338
|
messages.forEach((message, idx) => {
|
|
328
339
|
const formattedMessage = this.formatChatMessage(message);
|
|
329
|
-
|
|
340
|
+
const msg = new Message(formattedMessage);
|
|
341
|
+
msg.changed.connect(this._onMessageChanged, this);
|
|
342
|
+
formattedMessages.push(msg);
|
|
330
343
|
if (message.time > this.lastRead) {
|
|
331
344
|
unreadIndexes.push(index + idx);
|
|
332
345
|
}
|
|
@@ -355,7 +368,9 @@ export class AbstractChatModel {
|
|
|
355
368
|
* @param count - the number of messages to delete.
|
|
356
369
|
*/
|
|
357
370
|
messagesDeleted(index, count) {
|
|
358
|
-
this._messages.splice(index, count)
|
|
371
|
+
this._messages.splice(index, count).forEach(msg => {
|
|
372
|
+
msg.changed.disconnect(this._onMessageChanged, this);
|
|
373
|
+
});
|
|
359
374
|
this._messagesUpdated.emit();
|
|
360
375
|
}
|
|
361
376
|
/**
|
|
@@ -423,14 +438,16 @@ export class AbstractChatModel {
|
|
|
423
438
|
this._commands
|
|
424
439
|
.execute('apputils:update-notification', {
|
|
425
440
|
id: this._notificationId,
|
|
426
|
-
message:
|
|
441
|
+
message: this._name
|
|
442
|
+
? this._trans.__('%1 incoming message(s) in %2', unreadCount, this._name)
|
|
443
|
+
: this._trans.__('%1 incoming message(s)', unreadCount)
|
|
427
444
|
})
|
|
428
445
|
.then(success => {
|
|
429
446
|
// Create a new notification only if messages are added.
|
|
430
447
|
if (!success && canCreate) {
|
|
431
448
|
this._commands.execute('apputils:notify', {
|
|
432
449
|
type: 'info',
|
|
433
|
-
message:
|
|
450
|
+
message: this._trans.__('%1 incoming message(s) in %2', unreadCount, this._name)
|
|
434
451
|
}).then(id => {
|
|
435
452
|
this._notificationId = id;
|
|
436
453
|
});
|
|
@@ -446,6 +463,9 @@ export class AbstractChatModel {
|
|
|
446
463
|
}
|
|
447
464
|
}
|
|
448
465
|
}
|
|
466
|
+
_onMessageChanged(msg) {
|
|
467
|
+
this._messageChanged.emit(msg);
|
|
468
|
+
}
|
|
449
469
|
}
|
|
450
470
|
/**
|
|
451
471
|
* An abstract base class implementing `IChatContext`. This can be extended into
|
package/lib/types.d.ts
CHANGED
|
@@ -75,18 +75,59 @@ export interface IMessageMetadata {
|
|
|
75
75
|
* The chat message description.
|
|
76
76
|
*/
|
|
77
77
|
export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
78
|
+
/**
|
|
79
|
+
* The type of the message, usually 'msg' for a regular message.
|
|
80
|
+
*/
|
|
78
81
|
type: string;
|
|
79
|
-
|
|
82
|
+
/**
|
|
83
|
+
* The body of the message, markdown formatted.
|
|
84
|
+
*/
|
|
85
|
+
body: string;
|
|
86
|
+
/**
|
|
87
|
+
* The message id (should be unique).
|
|
88
|
+
*/
|
|
80
89
|
id: string;
|
|
90
|
+
/**
|
|
91
|
+
* The message timestamp (seconds since epoch).
|
|
92
|
+
*/
|
|
81
93
|
time: number;
|
|
94
|
+
/**
|
|
95
|
+
* The sender of the message, default to IUser type.
|
|
96
|
+
*/
|
|
82
97
|
sender: T;
|
|
98
|
+
/**
|
|
99
|
+
* The attachment list, default to IAttachment type.
|
|
100
|
+
*/
|
|
83
101
|
attachments?: U[];
|
|
102
|
+
/**
|
|
103
|
+
* Optional, list of users mentioned in the message.
|
|
104
|
+
*/
|
|
84
105
|
mentions?: T[];
|
|
106
|
+
/**
|
|
107
|
+
* Optional, whether the message time has been verified.
|
|
108
|
+
*/
|
|
85
109
|
raw_time?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Optional, whether the message has been deleted.
|
|
112
|
+
*/
|
|
86
113
|
deleted?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Optional, whether the message has been edited.
|
|
116
|
+
*/
|
|
87
117
|
edited?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Optional, whether the message should be stacked to the previous one.
|
|
120
|
+
*/
|
|
88
121
|
stacked?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Optional, the metadata of the message.
|
|
124
|
+
*/
|
|
89
125
|
metadata?: IMessageMetadata;
|
|
126
|
+
/**
|
|
127
|
+
* Optional, a mime bundle.
|
|
128
|
+
* If provided, the body won't be displayed in favor of the mime bundle.
|
|
129
|
+
*/
|
|
130
|
+
mime_model?: IMimeModelBody;
|
|
90
131
|
};
|
|
91
132
|
export interface IMessage extends IMessageContent {
|
|
92
133
|
/**
|
|
@@ -173,6 +214,10 @@ export interface INotebookAttachment {
|
|
|
173
214
|
* The local path of the notebook, relative to `ContentsManager.root_dir`.
|
|
174
215
|
*/
|
|
175
216
|
value: string;
|
|
217
|
+
/**
|
|
218
|
+
* (optional) The MIME type of the attachment.
|
|
219
|
+
*/
|
|
220
|
+
mimetype?: string;
|
|
176
221
|
/**
|
|
177
222
|
* (optional) A list of cells in the notebook.
|
|
178
223
|
*/
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { IThemeManager, ReactWidget } from '@jupyterlab/apputils';
|
|
2
|
-
|
|
2
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
3
|
+
export declare function buildErrorWidget(themeManager: IThemeManager | null, translator?: ITranslator): ReactWidget;
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
6
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
6
7
|
import { Alert, Box } from '@mui/material';
|
|
7
8
|
import React from 'react';
|
|
8
9
|
import { JlThemeProvider } from '../components/jl-theme-provider';
|
|
10
|
+
import { TRANSLATION_DOMAIN } from '../context';
|
|
9
11
|
import { chatIcon } from '../icons';
|
|
10
|
-
export function buildErrorWidget(themeManager) {
|
|
12
|
+
export function buildErrorWidget(themeManager, translator) {
|
|
13
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load(TRANSLATION_DOMAIN);
|
|
11
14
|
const ErrorWidget = ReactWidget.create(React.createElement(JlThemeProvider, { themeManager: themeManager },
|
|
12
15
|
React.createElement(Box, { sx: {
|
|
13
16
|
width: '100%',
|
|
@@ -18,9 +21,9 @@ export function buildErrorWidget(themeManager) {
|
|
|
18
21
|
flexDirection: 'column'
|
|
19
22
|
} },
|
|
20
23
|
React.createElement(Box, { sx: { padding: 4 } },
|
|
21
|
-
React.createElement(Alert, { severity: "error" },
|
|
24
|
+
React.createElement(Alert, { severity: "error" }, trans.__('There seems to be a problem with the Chat backend, please look at the JupyterLab server logs or contact your administrator to correct this problem.'))))));
|
|
22
25
|
ErrorWidget.id = 'jupyter-chat::chat';
|
|
23
26
|
ErrorWidget.title.icon = chatIcon;
|
|
24
|
-
ErrorWidget.title.caption = 'Jupyter Chat';
|
|
27
|
+
ErrorWidget.title.caption = trans.__('Jupyter Chat');
|
|
25
28
|
return ErrorWidget;
|
|
26
29
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
2
3
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
4
|
import { Message } from '@lumino/messaging';
|
|
4
5
|
/**
|
|
@@ -76,6 +77,7 @@ export declare class ChatSelectorPopup extends ReactWidget {
|
|
|
76
77
|
private _onClose?;
|
|
77
78
|
private _anchor;
|
|
78
79
|
private _anchorRect;
|
|
80
|
+
private _trans;
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* Namespace for ChatSelectorPopup.
|
|
@@ -98,5 +100,9 @@ export declare namespace ChatSelectorPopup {
|
|
|
98
100
|
* The element to anchor the popup to.
|
|
99
101
|
*/
|
|
100
102
|
anchor?: HTMLElement;
|
|
103
|
+
/**
|
|
104
|
+
* The translator for internationalization.
|
|
105
|
+
*/
|
|
106
|
+
translator?: ITranslator;
|
|
101
107
|
}
|
|
102
108
|
}
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { Button } from '@jupyter/react-components';
|
|
6
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
6
7
|
import { closeIcon, ReactWidget } from '@jupyterlab/ui-components';
|
|
7
8
|
import React, { useEffect, useRef } from 'react';
|
|
9
|
+
import { TRANSLATION_DOMAIN } from '../context';
|
|
8
10
|
const POPUP_CLASS = 'jp-chat-selector-popup';
|
|
9
11
|
const POPUP_LIST_CLASS = 'jp-chat-selector-popup-list';
|
|
10
12
|
const POPUP_ITEM_CLASS = 'jp-chat-selector-popup-item';
|
|
@@ -16,7 +18,7 @@ const POPUP_EMPTY_CLASS = 'jp-chat-selector-popup-empty';
|
|
|
16
18
|
*/
|
|
17
19
|
export class ChatSelectorPopup extends ReactWidget {
|
|
18
20
|
constructor(options) {
|
|
19
|
-
var _a;
|
|
21
|
+
var _a, _b;
|
|
20
22
|
super();
|
|
21
23
|
/**
|
|
22
24
|
* Check if the popup should move (anchor has moved).
|
|
@@ -70,6 +72,7 @@ export class ChatSelectorPopup extends ReactWidget {
|
|
|
70
72
|
this._onSelect = options.onSelect;
|
|
71
73
|
this._onClose = options.onClose;
|
|
72
74
|
this._anchor = (_a = options.anchor) !== null && _a !== void 0 ? _a : null;
|
|
75
|
+
this._trans = ((_b = options.translator) !== null && _b !== void 0 ? _b : nullTranslator).load(TRANSLATION_DOMAIN);
|
|
73
76
|
// Start hidden
|
|
74
77
|
this.hide();
|
|
75
78
|
// Initialize filtered chats
|
|
@@ -196,7 +199,7 @@ export class ChatSelectorPopup extends ReactWidget {
|
|
|
196
199
|
super.hide();
|
|
197
200
|
}
|
|
198
201
|
render() {
|
|
199
|
-
return (React.createElement(ChatSelectorList, { names: this._filteredChats, selectedName: this._selectedName, loadedModels: this._loadedModels, onSelect: this._handleItemClick, onUpdateSelectedName: this._handleUpdateSelectedName, onClose: this._handleClose }));
|
|
202
|
+
return (React.createElement(ChatSelectorList, { names: this._filteredChats, selectedName: this._selectedName, loadedModels: this._loadedModels, onSelect: this._handleItemClick, onUpdateSelectedName: this._handleUpdateSelectedName, onClose: this._handleClose, trans: this._trans }));
|
|
200
203
|
}
|
|
201
204
|
onAfterShow(msg) {
|
|
202
205
|
super.onAfterShow(msg);
|
|
@@ -261,7 +264,7 @@ export class ChatSelectorPopup extends ReactWidget {
|
|
|
261
264
|
/**
|
|
262
265
|
* React component for rendering the chat list.
|
|
263
266
|
*/
|
|
264
|
-
function ChatSelectorList({ names, selectedName, loadedModels, onSelect, onUpdateSelectedName, onClose }) {
|
|
267
|
+
function ChatSelectorList({ names, selectedName, loadedModels, onSelect, onUpdateSelectedName, onClose, trans }) {
|
|
265
268
|
const listRef = useRef(null);
|
|
266
269
|
// Scroll selected item into view
|
|
267
270
|
useEffect(() => {
|
|
@@ -277,7 +280,7 @@ function ChatSelectorList({ names, selectedName, loadedModels, onSelect, onUpdat
|
|
|
277
280
|
onClose(name);
|
|
278
281
|
};
|
|
279
282
|
if (names.length === 0) {
|
|
280
|
-
return React.createElement("div", { className: POPUP_EMPTY_CLASS },
|
|
283
|
+
return React.createElement("div", { className: POPUP_EMPTY_CLASS }, trans.__('No chat found'));
|
|
281
284
|
}
|
|
282
285
|
return (React.createElement("ul", { ref: listRef, className: POPUP_LIST_CLASS }, names.map(name => {
|
|
283
286
|
const isLoaded = loadedModels.has(name);
|
|
@@ -287,7 +290,7 @@ function ChatSelectorList({ names, selectedName, loadedModels, onSelect, onUpdat
|
|
|
287
290
|
React.createElement("div", { className: POPUP_ITEM_LABEL_CLASS },
|
|
288
291
|
React.createElement("span", { className: "jp-chat-selector-popup-item-name", title: name }, name),
|
|
289
292
|
isLoaded && (React.createElement("span", { className: "jp-chat-selector-popup-item-indicator" }, "\u25CF")))),
|
|
290
|
-
isLoaded && (React.createElement(Button, { onClick: e => handleCloseClick(e, name), appearance: "stealth", title:
|
|
293
|
+
isLoaded && (React.createElement(Button, { onClick: e => handleCloseClick(e, name), appearance: "stealth", title: trans.__('Close and dispose this chat'), className: "jp-chat-selector-popup-item-close" },
|
|
291
294
|
React.createElement(closeIcon.react, { tag: null }))))));
|
|
292
295
|
})));
|
|
293
296
|
}
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
6
|
+
import { TRANSLATION_DOMAIN } from '../context';
|
|
5
7
|
import { chatIcon } from '../icons';
|
|
6
8
|
import { ChatWidget } from './chat-widget';
|
|
7
9
|
export function buildChatSidebar(options) {
|
|
10
|
+
var _a;
|
|
8
11
|
const widget = new ChatWidget(options);
|
|
12
|
+
const trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load(TRANSLATION_DOMAIN);
|
|
9
13
|
widget.id = 'jupyter-chat::side-panel';
|
|
10
14
|
widget.title.icon = chatIcon;
|
|
11
|
-
widget.title.caption = 'Jupyter Chat';
|
|
15
|
+
widget.title.caption = trans.__('Jupyter Chat');
|
|
12
16
|
return widget;
|
|
13
17
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
6
6
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
7
7
|
import { isCode, isMarkdown, isRaw } from '@jupyterlab/nbformat';
|
|
8
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
8
9
|
import React from 'react';
|
|
9
10
|
import { Drag } from '@lumino/dragdrop';
|
|
10
11
|
import { Chat, MESSAGE_CLASS } from '../components';
|
|
12
|
+
import { TRANSLATION_DOMAIN } from '../context';
|
|
11
13
|
import { chatIcon } from '../icons';
|
|
12
14
|
// MIME type constant for file browser drag events
|
|
13
15
|
const FILE_BROWSER_MIME = 'application/x-jupyter-icontentsrich';
|
|
@@ -20,10 +22,12 @@ const INPUT_CONTAINER_CLASS = 'jp-chat-input-container';
|
|
|
20
22
|
const DRAG_HOVER_CLASS = 'jp-chat-drag-hover';
|
|
21
23
|
export class ChatWidget extends ReactWidget {
|
|
22
24
|
constructor(options) {
|
|
25
|
+
var _a;
|
|
23
26
|
super();
|
|
24
27
|
this._dragTarget = null;
|
|
28
|
+
const trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load(TRANSLATION_DOMAIN);
|
|
25
29
|
this.title.icon = chatIcon;
|
|
26
|
-
this.title.caption = 'Jupyter Chat';
|
|
30
|
+
this.title.caption = trans.__('Jupyter Chat');
|
|
27
31
|
this._chatOptions = options;
|
|
28
32
|
this.id = `jupyter-chat::widget::${options.model.name}`;
|
|
29
33
|
this.addClass('jp-chat-widget');
|
|
@@ -271,6 +275,7 @@ export class ChatWidget extends ReactWidget {
|
|
|
271
275
|
const attachment = {
|
|
272
276
|
type: 'notebook',
|
|
273
277
|
value: notebookPath,
|
|
278
|
+
mimetype: 'application/x-ipynb+json',
|
|
274
279
|
cells: validCells
|
|
275
280
|
};
|
|
276
281
|
const inputModel = this._getInputFromEvent(event);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
1
2
|
import { PanelWithToolbar } from '@jupyterlab/ui-components';
|
|
2
3
|
import { Message } from '@lumino/messaging';
|
|
3
4
|
import { ISignal } from '@lumino/signaling';
|
|
@@ -100,6 +101,7 @@ export declare class MultiChatPanel extends PanelWithToolbar {
|
|
|
100
101
|
private _currentWidget?;
|
|
101
102
|
private _chatNames;
|
|
102
103
|
private _visibilityChanged;
|
|
104
|
+
private _trans;
|
|
103
105
|
}
|
|
104
106
|
/**
|
|
105
107
|
* The chat panel namespace.
|
|
@@ -229,6 +231,10 @@ declare namespace SidePanelWidget {
|
|
|
229
231
|
* The callback to rename the chat.
|
|
230
232
|
*/
|
|
231
233
|
renameChat?: boolean | ((oldName: string) => Promise<string | null>);
|
|
234
|
+
/**
|
|
235
|
+
* The translation bundle.
|
|
236
|
+
*/
|
|
237
|
+
trans: TranslationBundle;
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
240
|
export {};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Originally adapted from jupyterlab-chat's ChatPanel
|
|
8
8
|
*/
|
|
9
9
|
import { InputDialog } from '@jupyterlab/apputils';
|
|
10
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
10
11
|
import { addIcon, closeIcon, launchIcon, PanelWithToolbar, ReactWidget, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
|
|
11
12
|
import { ArrayExt } from '@lumino/algorithm';
|
|
12
13
|
import { Debouncer } from '@lumino/polling';
|
|
@@ -15,6 +16,7 @@ import { Widget } from '@lumino/widgets';
|
|
|
15
16
|
import React, { useEffect, useRef, useState } from 'react';
|
|
16
17
|
import { ChatWidget } from './chat-widget';
|
|
17
18
|
import { ChatSelectorPopup } from './chat-selector-popup';
|
|
19
|
+
import { TRANSLATION_DOMAIN } from '../context';
|
|
18
20
|
import { chatIcon, readIcon } from '../icons';
|
|
19
21
|
const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
|
|
20
22
|
const ADD_BUTTON_CLASS = 'jp-chat-add';
|
|
@@ -29,6 +31,7 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
29
31
|
* The constructor of the multichat panel.
|
|
30
32
|
*/
|
|
31
33
|
constructor(options) {
|
|
34
|
+
var _a;
|
|
32
35
|
super(options);
|
|
33
36
|
/**
|
|
34
37
|
* Invoke the update of the list of available chats.
|
|
@@ -93,7 +96,9 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
93
96
|
this._visibilityChanged = new Signal(this);
|
|
94
97
|
this.id = 'jupyter-chat::multi-chat-panel';
|
|
95
98
|
this.title.icon = chatIcon;
|
|
96
|
-
|
|
99
|
+
const translator = (_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator;
|
|
100
|
+
this._trans = translator.load(TRANSLATION_DOMAIN);
|
|
101
|
+
this.title.caption = this._trans.__('Jupyter Chat');
|
|
97
102
|
this.addClass(SIDEPANEL_CLASS);
|
|
98
103
|
this._chatOptions = options;
|
|
99
104
|
this._inputToolbarFactory = options.inputToolbarFactory;
|
|
@@ -109,14 +114,14 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
109
114
|
this.open(addChatArgs);
|
|
110
115
|
},
|
|
111
116
|
icon: addIcon,
|
|
112
|
-
tooltip: 'Create a new chat'
|
|
117
|
+
tooltip: this._trans.__('Create a new chat')
|
|
113
118
|
});
|
|
114
119
|
addChat.addClass(ADD_BUTTON_CLASS);
|
|
115
120
|
this.toolbar.addItem('createChat', addChat);
|
|
116
121
|
}
|
|
117
122
|
if (this._getChatNames && this._createModel) {
|
|
118
123
|
// Chat selector with search input
|
|
119
|
-
this._openChatWidget = ReactWidget.create(React.createElement(ChatSearchInput, { selectChat: this._onSelectChat, getPopup: () => this._chatSelectorPopup, chatOpened: this._chatOpened }));
|
|
124
|
+
this._openChatWidget = ReactWidget.create(React.createElement(ChatSearchInput, { selectChat: this._onSelectChat, getPopup: () => this._chatSelectorPopup, chatOpened: this._chatOpened, trans: this._trans }));
|
|
120
125
|
this._openChatWidget.addClass(OPEN_SELECT_CLASS);
|
|
121
126
|
this.toolbar.addItem('openChat', this._openChatWidget);
|
|
122
127
|
// Create the popup widget (attached to document body)
|
|
@@ -125,7 +130,8 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
125
130
|
onSelect: this._onSelectChat,
|
|
126
131
|
onClose: (name) => {
|
|
127
132
|
this.disposeLoadedModel(name);
|
|
128
|
-
}
|
|
133
|
+
},
|
|
134
|
+
translator
|
|
129
135
|
});
|
|
130
136
|
}
|
|
131
137
|
// Insert the toolbar as first child.
|
|
@@ -246,7 +252,8 @@ export class MultiChatPanel extends PanelWithToolbar {
|
|
|
246
252
|
renameChat: this._renameChat,
|
|
247
253
|
onClose: (name) => {
|
|
248
254
|
this.disposeLoadedModel(name);
|
|
249
|
-
}
|
|
255
|
+
},
|
|
256
|
+
trans: this._trans
|
|
250
257
|
});
|
|
251
258
|
// Add to content panel
|
|
252
259
|
this.addWidget(widget);
|
|
@@ -326,6 +333,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
326
333
|
this._nameChanged = new Signal(this);
|
|
327
334
|
this._chatWidget = options.widget;
|
|
328
335
|
this._displayName = (_a = options.displayName) !== null && _a !== void 0 ? _a : options.widget.model.name;
|
|
336
|
+
const trans = options.trans;
|
|
329
337
|
this.addClass(SIDEPANEL_WIDGET_CLASS);
|
|
330
338
|
this.toolbar.addClass(TOOLBAR_CLASS);
|
|
331
339
|
this._updateTitle();
|
|
@@ -341,7 +349,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
341
349
|
// Add toolbar buttons
|
|
342
350
|
this._markAsRead = new ToolbarButton({
|
|
343
351
|
icon: readIcon,
|
|
344
|
-
iconLabel: 'Mark chat as read',
|
|
352
|
+
iconLabel: trans.__('Mark chat as read'),
|
|
345
353
|
className: 'jp-mod-styled',
|
|
346
354
|
onClick: () => {
|
|
347
355
|
if (this.model) {
|
|
@@ -353,7 +361,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
353
361
|
if (options.renameChat) {
|
|
354
362
|
const renameButton = new ToolbarButton({
|
|
355
363
|
iconClass: 'jp-EditIcon',
|
|
356
|
-
iconLabel: 'Rename chat',
|
|
364
|
+
iconLabel: trans.__('Rename chat'),
|
|
357
365
|
className: 'jp-mod-styled',
|
|
358
366
|
onClick: async () => {
|
|
359
367
|
if (!options.renameChat) {
|
|
@@ -363,9 +371,9 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
363
371
|
if (options.renameChat === true) {
|
|
364
372
|
// If rename chat is true, let's provide a input to select new name.
|
|
365
373
|
const result = await InputDialog.getText({
|
|
366
|
-
title: 'Rename Chat',
|
|
374
|
+
title: trans.__('Rename Chat'),
|
|
367
375
|
text: this.model.name,
|
|
368
|
-
placeholder: 'new-name'
|
|
376
|
+
placeholder: trans.__('new-name')
|
|
369
377
|
});
|
|
370
378
|
if (!result.button.accept && result.value) {
|
|
371
379
|
return;
|
|
@@ -389,7 +397,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
389
397
|
if (options.openInMain) {
|
|
390
398
|
const moveToMain = new ToolbarButton({
|
|
391
399
|
icon: launchIcon,
|
|
392
|
-
iconLabel: 'Move the chat to the main area',
|
|
400
|
+
iconLabel: trans.__('Move the chat to the main area'),
|
|
393
401
|
className: 'jp-mod-styled',
|
|
394
402
|
onClick: async () => {
|
|
395
403
|
var _a;
|
|
@@ -403,7 +411,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
403
411
|
}
|
|
404
412
|
const closeButton = new ToolbarButton({
|
|
405
413
|
icon: closeIcon,
|
|
406
|
-
iconLabel: 'Close the chat',
|
|
414
|
+
iconLabel: trans.__('Close the chat'),
|
|
407
415
|
className: 'jp-mod-styled',
|
|
408
416
|
onClick: () => {
|
|
409
417
|
options.onClose(this._displayName);
|
|
@@ -480,7 +488,7 @@ class SidePanelWidget extends PanelWithToolbar {
|
|
|
480
488
|
/**
|
|
481
489
|
* A search input component for selecting a chat.
|
|
482
490
|
*/
|
|
483
|
-
function ChatSearchInput({ selectChat, getPopup, chatOpened }) {
|
|
491
|
+
function ChatSearchInput({ selectChat, getPopup, chatOpened, trans }) {
|
|
484
492
|
const [query, setQuery] = useState('');
|
|
485
493
|
const inputRef = useRef(null);
|
|
486
494
|
useEffect(() => {
|
|
@@ -553,5 +561,5 @@ function ChatSearchInput({ selectChat, getPopup, chatOpened }) {
|
|
|
553
561
|
break;
|
|
554
562
|
}
|
|
555
563
|
};
|
|
556
|
-
return (React.createElement("input", { ref: inputRef, type: "text", placeholder:
|
|
564
|
+
return (React.createElement("input", { ref: inputRef, type: "text", placeholder: trans.__('Select a chat'), value: query, onChange: handleInputChange, onFocus: handleInputFocus, onClick: handleInputClick, onKeyDown: handleKeyDown, className: "jp-chat-search-input" }));
|
|
557
565
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@jupyterlab/fileeditor": "^4.2.0",
|
|
57
57
|
"@jupyterlab/notebook": "^4.2.0",
|
|
58
58
|
"@jupyterlab/rendermime": "^4.2.0",
|
|
59
|
+
"@jupyterlab/translation": "^4.2.0",
|
|
59
60
|
"@jupyterlab/ui-components": "^4.2.0",
|
|
60
61
|
"@lumino/algorithm": "^2.0.0",
|
|
61
62
|
"@lumino/commands": "^2.0.0",
|
|
@@ -80,6 +80,64 @@ describe('test chat model', () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
describe('messageChanged signal', () => {
|
|
84
|
+
const msg = {
|
|
85
|
+
type: 'msg',
|
|
86
|
+
id: 'message1',
|
|
87
|
+
time: Date.now() / 1000,
|
|
88
|
+
body: 'original body',
|
|
89
|
+
sender: { username: 'user' }
|
|
90
|
+
} as IMessageContent;
|
|
91
|
+
|
|
92
|
+
it('should emit messageChanged when a message is updated', () => {
|
|
93
|
+
const model = new MockChatModel();
|
|
94
|
+
model.messageAdded(msg);
|
|
95
|
+
|
|
96
|
+
let emitCount = 0;
|
|
97
|
+
model.messageChanged.connect(() => {
|
|
98
|
+
emitCount++;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let changedMessage: IMessage | null = null;
|
|
102
|
+
model.messageChanged.connect((sender, message) => {
|
|
103
|
+
changedMessage = message;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
model.messages[0].update({ body: 'updated body' });
|
|
107
|
+
expect(emitCount).toBe(1);
|
|
108
|
+
expect(changedMessage).not.toBeNull();
|
|
109
|
+
expect(changedMessage!.body).toBe('updated body');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should not emit messageChanged after the message is deleted', () => {
|
|
113
|
+
const model = new MockChatModel();
|
|
114
|
+
model.messageAdded(msg);
|
|
115
|
+
|
|
116
|
+
let emitCount = 0;
|
|
117
|
+
model.messageChanged.connect(() => {
|
|
118
|
+
emitCount++;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
model.messagesDeleted(0, 1);
|
|
122
|
+
model.messages[0]?.update({ body: 'updated body' });
|
|
123
|
+
expect(emitCount).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should not emit messageChanged after the model is disposed', () => {
|
|
127
|
+
const model = new MockChatModel();
|
|
128
|
+
model.messageAdded(msg);
|
|
129
|
+
|
|
130
|
+
let emitCount = 0;
|
|
131
|
+
model.messageChanged.connect(() => {
|
|
132
|
+
emitCount++;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
model.dispose();
|
|
136
|
+
model.messages[0]?.update({ body: 'updated body' });
|
|
137
|
+
expect(emitCount).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
83
141
|
describe('model config', () => {
|
|
84
142
|
it('should have empty config', () => {
|
|
85
143
|
const model = new MockChatModel();
|