@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.
Files changed (57) hide show
  1. package/lib/__tests__/model.spec.js +49 -0
  2. package/lib/components/attachments.js +3 -3
  3. package/lib/components/chat.d.ts +5 -0
  4. package/lib/components/code-blocks/code-toolbar.js +12 -8
  5. package/lib/components/code-blocks/copy-button.js +9 -7
  6. package/lib/components/input/buttons/attach-button.js +4 -2
  7. package/lib/components/input/buttons/cancel-button.js +3 -1
  8. package/lib/components/input/buttons/save-edit-button.js +3 -1
  9. package/lib/components/input/buttons/send-button.js +4 -2
  10. package/lib/components/input/buttons/stop-button.js +3 -1
  11. package/lib/components/input/chat-input.js +3 -2
  12. package/lib/components/messages/header.js +7 -3
  13. package/lib/components/messages/message-renderer.js +17 -17
  14. package/lib/components/messages/navigation.js +5 -4
  15. package/lib/components/messages/toolbar.js +4 -2
  16. package/lib/components/writing-indicator.js +11 -6
  17. package/lib/context.d.ts +7 -0
  18. package/lib/context.js +12 -0
  19. package/lib/message.d.ts +2 -1
  20. package/lib/message.js +3 -0
  21. package/lib/model.d.ts +16 -0
  22. package/lib/model.js +28 -8
  23. package/lib/types.d.ts +46 -1
  24. package/lib/widgets/chat-error.d.ts +2 -1
  25. package/lib/widgets/chat-error.js +6 -3
  26. package/lib/widgets/chat-selector-popup.d.ts +6 -0
  27. package/lib/widgets/chat-selector-popup.js +8 -5
  28. package/lib/widgets/chat-sidebar.js +5 -1
  29. package/lib/widgets/chat-widget.js +6 -1
  30. package/lib/widgets/multichat-panel.d.ts +6 -0
  31. package/lib/widgets/multichat-panel.js +21 -13
  32. package/package.json +2 -1
  33. package/src/__tests__/model.spec.ts +58 -0
  34. package/src/components/attachments.tsx +3 -3
  35. package/src/components/chat.tsx +5 -0
  36. package/src/components/code-blocks/code-toolbar.tsx +14 -7
  37. package/src/components/code-blocks/copy-button.tsx +12 -8
  38. package/src/components/input/buttons/attach-button.tsx +4 -2
  39. package/src/components/input/buttons/cancel-button.tsx +4 -1
  40. package/src/components/input/buttons/save-edit-button.tsx +3 -1
  41. package/src/components/input/buttons/send-button.tsx +4 -2
  42. package/src/components/input/buttons/stop-button.tsx +3 -1
  43. package/src/components/input/chat-input.tsx +3 -2
  44. package/src/components/messages/header.tsx +9 -3
  45. package/src/components/messages/message-renderer.tsx +17 -17
  46. package/src/components/messages/navigation.tsx +5 -4
  47. package/src/components/messages/toolbar.tsx +6 -4
  48. package/src/components/writing-indicator.tsx +17 -6
  49. package/src/context.ts +13 -0
  50. package/src/message.ts +4 -1
  51. package/src/model.ts +52 -4
  52. package/src/types.ts +46 -1
  53. package/src/widgets/chat-error.tsx +9 -5
  54. package/src/widgets/chat-selector-popup.tsx +21 -3
  55. package/src/widgets/chat-sidebar.tsx +5 -1
  56. package/src/widgets/chat-widget.tsx +7 -1
  57. 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 = (_b = options.activeCellManager) !== null && _b !== void 0 ? _b : null;
60
- this._selectionWatcher = (_c = options.selectionWatcher) !== null && _c !== void 0 ? _c : null;
61
- this._documentManager = (_d = options.documentManager) !== null && _d !== void 0 ? _d : null;
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
- formattedMessages.push(new Message(formattedMessage));
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: `${unreadCount} incoming message(s) ${this._name ? 'in ' + this._name : ''}`
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: `${unreadCount} incoming message(s) in ${this._name}`
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
- body: string | IMimeModelBody;
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
- export declare function buildErrorWidget(themeManager: IThemeManager | null): ReactWidget;
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" }, "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.")))));
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'; // TODO: i18n
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 }, "No chat found");
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: "Close and dispose this chat", className: "jp-chat-selector-popup-item-close" },
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'; // TODO: i18n
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'; // TODO: i18n
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
- this.title.caption = 'Jupyter Chat'; // TODO: i18n/
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: "Select a chat", value: query, onChange: handleInputChange, onFocus: handleInputFocus, onClick: handleInputClick, onKeyDown: handleKeyDown, className: "jp-chat-search-input" }));
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.20.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();