@jupyter/chat 0.8.1 → 0.10.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 (69) hide show
  1. package/lib/__tests__/mocks.d.ts +9 -0
  2. package/lib/__tests__/mocks.js +18 -0
  3. package/lib/__tests__/model.spec.js +17 -10
  4. package/lib/__tests__/widgets.spec.js +4 -4
  5. package/lib/chat-commands/types.d.ts +2 -1
  6. package/lib/components/chat-input.d.ts +4 -12
  7. package/lib/components/chat-input.js +26 -40
  8. package/lib/components/chat-messages.d.ts +17 -4
  9. package/lib/components/chat-messages.js +28 -15
  10. package/lib/components/chat.d.ts +5 -5
  11. package/lib/components/chat.js +9 -8
  12. package/lib/components/code-blocks/copy-button.js +6 -3
  13. package/lib/components/input/buttons/attach-button.d.ts +6 -0
  14. package/lib/components/input/{attach-button.js → buttons/attach-button.js} +11 -8
  15. package/lib/components/input/buttons/cancel-button.d.ts +6 -0
  16. package/lib/components/input/{cancel-button.js → buttons/cancel-button.js} +5 -7
  17. package/lib/components/input/buttons/index.d.ts +3 -0
  18. package/lib/components/input/buttons/index.js +7 -0
  19. package/lib/components/input/buttons/send-button.d.ts +6 -0
  20. package/lib/components/input/{send-button.js → buttons/send-button.js} +52 -42
  21. package/lib/components/input/index.d.ts +3 -3
  22. package/lib/components/input/index.js +3 -3
  23. package/lib/components/input/toolbar-registry.d.ts +98 -0
  24. package/lib/components/input/toolbar-registry.js +85 -0
  25. package/lib/components/input/use-chat-commands.js +6 -5
  26. package/lib/components/mui-extras/tooltipped-button.d.ts +1 -1
  27. package/lib/components/mui-extras/tooltipped-button.js +3 -2
  28. package/lib/components/mui-extras/tooltipped-icon-button.js +4 -2
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/input-model.d.ts +93 -1
  32. package/lib/input-model.js +55 -1
  33. package/lib/model.d.ts +76 -9
  34. package/lib/model.js +42 -12
  35. package/lib/types.d.ts +5 -18
  36. package/lib/utils.d.ts +15 -0
  37. package/lib/utils.js +29 -0
  38. package/lib/widgets/chat-widget.d.ts +5 -1
  39. package/lib/widgets/chat-widget.js +7 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/mocks.ts +31 -0
  42. package/src/__tests__/model.spec.ts +21 -11
  43. package/src/__tests__/widgets.spec.ts +5 -4
  44. package/src/chat-commands/types.ts +1 -1
  45. package/src/components/chat-input.tsx +41 -66
  46. package/src/components/chat-messages.tsx +44 -17
  47. package/src/components/chat.tsx +12 -21
  48. package/src/components/code-blocks/copy-button.tsx +9 -3
  49. package/src/components/input/{attach-button.tsx → buttons/attach-button.tsx} +15 -20
  50. package/src/components/input/{cancel-button.tsx → buttons/cancel-button.tsx} +9 -16
  51. package/src/components/input/buttons/index.ts +8 -0
  52. package/src/components/input/{send-button.tsx → buttons/send-button.tsx} +62 -61
  53. package/src/components/input/index.ts +3 -3
  54. package/src/components/input/toolbar-registry.tsx +162 -0
  55. package/src/components/input/use-chat-commands.tsx +14 -6
  56. package/src/components/mui-extras/tooltipped-button.tsx +4 -2
  57. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -2
  58. package/src/index.ts +1 -1
  59. package/src/input-model.ts +140 -2
  60. package/src/model.ts +110 -12
  61. package/src/types.ts +5 -21
  62. package/src/utils.ts +34 -0
  63. package/src/widgets/chat-widget.tsx +8 -1
  64. package/style/base.css +1 -0
  65. package/style/chat.css +6 -0
  66. package/style/input.css +32 -0
  67. package/lib/components/input/attach-button.d.ts +0 -14
  68. package/lib/components/input/cancel-button.d.ts +0 -11
  69. package/lib/components/input/send-button.d.ts +0 -18
@@ -9,7 +9,14 @@ const WHITESPACE = new Set([' ', '\n', '\t']);
9
9
  */
10
10
  export class InputModel {
11
11
  constructor(options) {
12
- var _a, _b;
12
+ var _a, _b, _c;
13
+ /**
14
+ * Function to send a message.
15
+ */
16
+ this.send = (input) => {
17
+ this._onSend(input, this);
18
+ this.value = '';
19
+ };
13
20
  /**
14
21
  * Add attachment to send with next message.
15
22
  */
@@ -39,6 +46,12 @@ export class InputModel {
39
46
  this._attachments = [];
40
47
  this._attachmentsChanged.emit([]);
41
48
  };
49
+ /**
50
+ * Clear mentions list.
51
+ */
52
+ this.clearMentions = () => {
53
+ this._mentions = [];
54
+ };
42
55
  this._cursorIndex = null;
43
56
  this._currentWord = null;
44
57
  this._valueChanged = new Signal(this);
@@ -48,14 +61,25 @@ export class InputModel {
48
61
  this._focusInputSignal = new Signal(this);
49
62
  this._attachmentsChanged = new Signal(this);
50
63
  this._isDisposed = false;
64
+ this._onSend = options.onSend;
65
+ this._chatContext = options.chatContext;
51
66
  this._value = options.value || '';
52
67
  this._attachments = options.attachments || [];
68
+ this._mentions = options.mentions || [];
53
69
  this.cursorIndex = options.cursorIndex || this.value.length;
54
70
  this._activeCellManager = (_a = options.activeCellManager) !== null && _a !== void 0 ? _a : null;
55
71
  this._selectionWatcher = (_b = options.selectionWatcher) !== null && _b !== void 0 ? _b : null;
72
+ this._documentManager = (_c = options.documentManager) !== null && _c !== void 0 ? _c : null;
56
73
  this._config = {
57
74
  ...options.config
58
75
  };
76
+ this.cancel = options.onCancel;
77
+ }
78
+ /**
79
+ * The chat context (a readonly subset of the chat model);
80
+ */
81
+ get chatContext() {
82
+ return this._chatContext;
59
83
  }
60
84
  /**
61
85
  * The entire input value.
@@ -124,6 +148,12 @@ export class InputModel {
124
148
  get selectionWatcher() {
125
149
  return this._selectionWatcher;
126
150
  }
151
+ /**
152
+ * Get the document manager.
153
+ */
154
+ get documentManager() {
155
+ return this._documentManager;
156
+ }
127
157
  /**
128
158
  * The input configuration.
129
159
  */
@@ -174,6 +204,30 @@ export class InputModel {
174
204
  const [start, end] = Private.getCurrentWordBoundaries(this.value, this.cursorIndex);
175
205
  this.value = this.value.slice(0, start) + newWord + this.value.slice(end);
176
206
  }
207
+ /**
208
+ * The mentioned user list.
209
+ */
210
+ get mentions() {
211
+ return this._mentions;
212
+ }
213
+ /**
214
+ * Add a user mention.
215
+ */
216
+ addMention(user) {
217
+ const usernames = this._mentions.map(user => user.username);
218
+ if (!usernames.includes(user.username)) {
219
+ this._mentions.push(user);
220
+ }
221
+ }
222
+ /**
223
+ * Remove a user mention.
224
+ */
225
+ removeMention(user) {
226
+ const index = this._mentions.indexOf(user);
227
+ if (index > -1) {
228
+ this._mentions.splice(index, 1);
229
+ }
230
+ }
177
231
  /**
178
232
  * Dispose the input model.
179
233
  */
package/lib/model.d.ts CHANGED
@@ -1,10 +1,11 @@
1
+ import { IDocumentManager } from '@jupyterlab/docmanager';
1
2
  import { CommandRegistry } from '@lumino/commands';
2
3
  import { IDisposable } from '@lumino/disposable';
3
4
  import { ISignal } from '@lumino/signaling';
4
- import { IChatHistory, INewMessage, IChatMessage, IConfig, IUser } from './types';
5
5
  import { IActiveCellManager } from './active-cell-manager';
6
- import { ISelectionWatcher } from './selection-watcher';
7
6
  import { IInputModel } from './input-model';
7
+ import { ISelectionWatcher } from './selection-watcher';
8
+ import { IChatHistory, INewMessage, IChatMessage, IConfig, IUser } from './types';
8
9
  /**
9
10
  * The chat model interface.
10
11
  */
@@ -45,6 +46,10 @@ export interface IChatModel extends IDisposable {
45
46
  * Get the selection watcher.
46
47
  */
47
48
  readonly selectionWatcher: ISelectionWatcher | null;
49
+ /**
50
+ * Get the selection watcher.
51
+ */
52
+ readonly documentManager: IDocumentManager | null;
48
53
  /**
49
54
  * A signal emitting when the messages list is updated.
50
55
  */
@@ -73,6 +78,10 @@ export interface IChatModel extends IDisposable {
73
78
  * @returns whether the message has been sent or not, or nothing if not needed.
74
79
  */
75
80
  sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
81
+ /**
82
+ * Clear the message list.
83
+ */
84
+ clearMessages(): void;
76
85
  /**
77
86
  * Optional, to update a message from the chat panel.
78
87
  *
@@ -122,17 +131,22 @@ export interface IChatModel extends IDisposable {
122
131
  * Update the current writers list.
123
132
  */
124
133
  updateWriters(writers: IUser[]): void;
134
+ /**
135
+ * Create the chat context that will be passed to the input model.
136
+ */
137
+ createChatContext(): IChatContext;
125
138
  }
126
139
  /**
127
- * The default chat model implementation.
128
- * It is not able to send or update a message by itself, since it depends on the
129
- * chosen technology.
140
+ * An abstract implementation of IChatModel.
141
+ *
142
+ * The class inheriting from it must implement at least:
143
+ * - sendMessage(message: INewMessage)
130
144
  */
131
- export declare class ChatModel implements IChatModel {
145
+ export declare abstract class AbstractChatModel implements IChatModel {
132
146
  /**
133
147
  * Create a new chat model.
134
148
  */
135
- constructor(options?: ChatModel.IOptions);
149
+ constructor(options?: IChatModel.IOptions);
136
150
  /**
137
151
  * The chat model id.
138
152
  */
@@ -159,6 +173,10 @@ export declare class ChatModel implements IChatModel {
159
173
  * Get the selection watcher.
160
174
  */
161
175
  get selectionWatcher(): ISelectionWatcher | null;
176
+ /**
177
+ * Get the document manager.
178
+ */
179
+ get documentManager(): IDocumentManager | null;
162
180
  /**
163
181
  * Timestamp of the last read message in local storage.
164
182
  */
@@ -206,7 +224,11 @@ export declare class ChatModel implements IChatModel {
206
224
  * @param message - the message to send.
207
225
  * @returns whether the message has been sent or not.
208
226
  */
209
- sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
227
+ abstract sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
228
+ /**
229
+ * Clear the message list.
230
+ */
231
+ clearMessages(): void;
210
232
  /**
211
233
  * Dispose the chat model.
212
234
  */
@@ -245,6 +267,10 @@ export declare class ChatModel implements IChatModel {
245
267
  * This implementation only propagate the list via a signal.
246
268
  */
247
269
  updateWriters(writers: IUser[]): void;
270
+ /**
271
+ * Create the chat context that will be passed to the input model.
272
+ */
273
+ abstract createChatContext(): IChatContext;
248
274
  /**
249
275
  * Add unread messages to the list.
250
276
  * @param indexes - list of new indexes.
@@ -271,6 +297,7 @@ export declare class ChatModel implements IChatModel {
271
297
  private _commands?;
272
298
  private _activeCellManager;
273
299
  private _selectionWatcher;
300
+ private _documentManager;
274
301
  private _notificationId;
275
302
  private _messagesUpdated;
276
303
  private _configChanged;
@@ -281,7 +308,7 @@ export declare class ChatModel implements IChatModel {
281
308
  /**
282
309
  * The chat model namespace.
283
310
  */
284
- export declare namespace ChatModel {
311
+ export declare namespace IChatModel {
285
312
  /**
286
313
  * The instantiation options for a ChatModel.
287
314
  */
@@ -306,5 +333,45 @@ export declare namespace ChatModel {
306
333
  * Selection watcher.
307
334
  */
308
335
  selectionWatcher?: ISelectionWatcher | null;
336
+ /**
337
+ * Document manager.
338
+ */
339
+ documentManager?: IDocumentManager | null;
309
340
  }
310
341
  }
342
+ /**
343
+ * Interface of the chat context, a 'subset' of the model with readonly attribute,
344
+ * which can be passed to the input model.
345
+ * This allows third party extensions to get some attribute of the model without
346
+ * exposing the method that can modify it.
347
+ */
348
+ export interface IChatContext {
349
+ /**
350
+ * The name of the chat.
351
+ */
352
+ readonly name: string;
353
+ /**
354
+ * A copy of the messages.
355
+ */
356
+ readonly messages: IChatMessage[];
357
+ /**
358
+ * A list of all users who have connected to this chat.
359
+ */
360
+ readonly users: IUser[];
361
+ }
362
+ /**
363
+ * An abstract base class implementing `IChatContext`. This can be extended into
364
+ * a complete implementation, as done in `jupyterlab-chat`.
365
+ */
366
+ export declare abstract class AbstractChatContext implements IChatContext {
367
+ constructor(options: {
368
+ model: IChatModel;
369
+ });
370
+ get name(): string;
371
+ get messages(): IChatMessage[];
372
+ /**
373
+ * ABSTRACT: Should return a list of users who have connected to this chat.
374
+ */
375
+ abstract get users(): IUser[];
376
+ protected _model: IChatModel;
377
+ }
package/lib/model.js CHANGED
@@ -4,17 +4,19 @@
4
4
  */
5
5
  import { Signal } from '@lumino/signaling';
6
6
  import { InputModel } from './input-model';
7
+ import { replaceMentionToSpan } from './utils';
7
8
  /**
8
- * The default chat model implementation.
9
- * It is not able to send or update a message by itself, since it depends on the
10
- * chosen technology.
9
+ * An abstract implementation of IChatModel.
10
+ *
11
+ * The class inheriting from it must implement at least:
12
+ * - sendMessage(message: INewMessage)
11
13
  */
12
- export class ChatModel {
14
+ export class AbstractChatModel {
13
15
  /**
14
16
  * Create a new chat model.
15
17
  */
16
18
  constructor(options = {}) {
17
- var _a, _b, _c;
19
+ var _a, _b, _c, _d;
18
20
  this._messages = [];
19
21
  this._unreadMessages = [];
20
22
  this._messagesInViewport = [];
@@ -37,15 +39,19 @@ export class ChatModel {
37
39
  ...config
38
40
  };
39
41
  this._inputModel = new InputModel({
42
+ chatContext: this.createChatContext(),
40
43
  activeCellManager: options.activeCellManager,
41
44
  selectionWatcher: options.selectionWatcher,
45
+ documentManager: options.documentManager,
42
46
  config: {
43
47
  sendWithShiftEnter: config.sendWithShiftEnter
44
- }
48
+ },
49
+ onSend: (input) => this.sendMessage({ body: input })
45
50
  });
46
51
  this._commands = options.commands;
47
52
  this._activeCellManager = (_b = options.activeCellManager) !== null && _b !== void 0 ? _b : null;
48
53
  this._selectionWatcher = (_c = options.selectionWatcher) !== null && _c !== void 0 ? _c : null;
54
+ this._documentManager = (_d = options.documentManager) !== null && _d !== void 0 ? _d : null;
49
55
  }
50
56
  /**
51
57
  * The chat model id.
@@ -89,6 +95,12 @@ export class ChatModel {
89
95
  get selectionWatcher() {
90
96
  return this._selectionWatcher;
91
97
  }
98
+ /**
99
+ * Get the document manager.
100
+ */
101
+ get documentManager() {
102
+ return this._documentManager;
103
+ }
92
104
  /**
93
105
  * Timestamp of the last read message in local storage.
94
106
  */
@@ -212,13 +224,12 @@ export class ChatModel {
212
224
  return this._writersChanged;
213
225
  }
214
226
  /**
215
- * Send a message, to be defined depending on the chosen technology.
216
- * Default to no-op.
217
- *
218
- * @param message - the message to send.
219
- * @returns whether the message has been sent or not.
227
+ * Clear the message list.
220
228
  */
221
- sendMessage(message) { }
229
+ clearMessages() {
230
+ this._messages = [];
231
+ this._messagesUpdated.emit();
232
+ }
222
233
  /**
223
234
  * Dispose the chat model.
224
235
  */
@@ -239,6 +250,10 @@ export class ChatModel {
239
250
  * Can be useful if some actions are required on the message.
240
251
  */
241
252
  formatChatMessage(message) {
253
+ var _a;
254
+ (_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
255
+ message.body = replaceMentionToSpan(message.body, user);
256
+ });
242
257
  return message;
243
258
  }
244
259
  /**
@@ -363,3 +378,18 @@ export class ChatModel {
363
378
  }
364
379
  }
365
380
  }
381
+ /**
382
+ * An abstract base class implementing `IChatContext`. This can be extended into
383
+ * a complete implementation, as done in `jupyterlab-chat`.
384
+ */
385
+ export class AbstractChatContext {
386
+ constructor(options) {
387
+ this._model = options.model;
388
+ }
389
+ get name() {
390
+ return this._model.name;
391
+ }
392
+ get messages() {
393
+ return [...this._model.messages];
394
+ }
395
+ }
package/lib/types.d.ts CHANGED
@@ -8,6 +8,10 @@ export interface IUser {
8
8
  initials?: string;
9
9
  color?: string;
10
10
  avatar_url?: string;
11
+ /**
12
+ * The string to use to mention a user in the chat.
13
+ */
14
+ mention_name?: string;
11
15
  }
12
16
  /**
13
17
  * The configuration interface.
@@ -44,6 +48,7 @@ export interface IChatMessage<T = IUser, U = IAttachment> {
44
48
  time: number;
45
49
  sender: T;
46
50
  attachments?: U[];
51
+ mentions?: T[];
47
52
  raw_time?: boolean;
48
53
  deleted?: boolean;
49
54
  edited?: boolean;
@@ -84,21 +89,3 @@ export interface IAttachment {
84
89
  */
85
90
  export interface ISettings {
86
91
  }
87
- /**
88
- * Representation of a selected text.
89
- */
90
- export type TextSelection = {
91
- type: 'text';
92
- source: string;
93
- };
94
- /**
95
- * Representation of a selected cell.
96
- */
97
- export type CellSelection = {
98
- type: 'cell';
99
- source: string;
100
- };
101
- /**
102
- * Selection object (text or cell).
103
- */
104
- export type Selection = TextSelection | CellSelection;
package/lib/utils.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CodeMirrorEditor } from '@jupyterlab/codemirror';
2
2
  import { Notebook } from '@jupyterlab/notebook';
3
3
  import { Widget } from '@lumino/widgets';
4
+ import { IUser } from './types';
4
5
  /**
5
6
  * Gets the editor instance used by a document widget. Returns `null` if unable.
6
7
  */
@@ -9,3 +10,17 @@ export declare function getEditor(widget: Widget | null): CodeMirrorEditor | nul
9
10
  * Gets the index of the cell associated with `cellId`.
10
11
  */
11
12
  export declare function getCellIndex(notebook: Notebook, cellId: string): number;
13
+ /**
14
+ * Replace a mention to user (@someone) to a span, for markdown renderer.
15
+ *
16
+ * @param content - the content to update.
17
+ * @param user - the user mentioned.
18
+ */
19
+ export declare function replaceMentionToSpan(content: string, user: IUser): string;
20
+ /**
21
+ * Replace a span to a mentioned to user string (@someone).
22
+ *
23
+ * @param content - the content to update.
24
+ * @param user - the user mentioned.
25
+ */
26
+ export declare function replaceSpanToMention(content: string, user: IUser): string;
package/lib/utils.js CHANGED
@@ -6,6 +6,7 @@ import { CodeMirrorEditor } from '@jupyterlab/codemirror';
6
6
  import { DocumentWidget } from '@jupyterlab/docregistry';
7
7
  import { FileEditor } from '@jupyterlab/fileeditor';
8
8
  import { Notebook } from '@jupyterlab/notebook';
9
+ const MENTION_CLASS = 'jp-chat-mention';
9
10
  /**
10
11
  * Gets the editor instance used by a document widget. Returns `null` if unable.
11
12
  */
@@ -35,3 +36,31 @@ export function getCellIndex(notebook, cellId) {
35
36
  const idx = (_a = notebook.model) === null || _a === void 0 ? void 0 : _a.sharedModel.cells.findIndex(cell => cell.getId() === cellId);
36
37
  return idx === undefined ? -1 : idx;
37
38
  }
39
+ /**
40
+ * Replace a mention to user (@someone) to a span, for markdown renderer.
41
+ *
42
+ * @param content - the content to update.
43
+ * @param user - the user mentioned.
44
+ */
45
+ export function replaceMentionToSpan(content, user) {
46
+ if (!user.mention_name) {
47
+ return content;
48
+ }
49
+ const regex = new RegExp(user.mention_name, 'g');
50
+ const mention = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
51
+ return content.replace(regex, mention);
52
+ }
53
+ /**
54
+ * Replace a span to a mentioned to user string (@someone).
55
+ *
56
+ * @param content - the content to update.
57
+ * @param user - the user mentioned.
58
+ */
59
+ export function replaceSpanToMention(content, user) {
60
+ if (!user.mention_name) {
61
+ return content;
62
+ }
63
+ const span = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
64
+ const regex = new RegExp(span, 'g');
65
+ return content.replace(regex, user.mention_name);
66
+ }
@@ -1,6 +1,6 @@
1
1
  import { ReactWidget } from '@jupyterlab/apputils';
2
2
  import React from 'react';
3
- import { Chat } from '../components/chat';
3
+ import { Chat, IInputToolbarRegistry } from '../components';
4
4
  import { IChatModel } from '../model';
5
5
  export declare class ChatWidget extends ReactWidget {
6
6
  constructor(options: Chat.IOptions);
@@ -8,6 +8,10 @@ export declare class ChatWidget extends ReactWidget {
8
8
  * Get the model of the widget.
9
9
  */
10
10
  get model(): IChatModel;
11
+ /**
12
+ * Get the input toolbar registry (if it has been provided when creating the widget).
13
+ */
14
+ get inputToolbarRegistry(): IInputToolbarRegistry | undefined;
11
15
  render(): React.JSX.Element;
12
16
  private _chatOptions;
13
17
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { ReactWidget } from '@jupyterlab/apputils';
6
6
  import React from 'react';
7
- import { Chat } from '../components/chat';
7
+ import { Chat } from '../components';
8
8
  import { chatIcon } from '../icons';
9
9
  export class ChatWidget extends ReactWidget {
10
10
  constructor(options) {
@@ -21,6 +21,12 @@ export class ChatWidget extends ReactWidget {
21
21
  get model() {
22
22
  return this._chatOptions.model;
23
23
  }
24
+ /**
25
+ * Get the input toolbar registry (if it has been provided when creating the widget).
26
+ */
27
+ get inputToolbarRegistry() {
28
+ return this._chatOptions.inputToolbarRegistry;
29
+ }
24
30
  render() {
25
31
  // The model need to be passed, otherwise it is undefined in the widget in
26
32
  // the case of collaborative document.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.8.1",
3
+ "version": "0.10.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",
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import {
7
+ AbstractChatContext,
8
+ AbstractChatModel,
9
+ IChatModel,
10
+ IChatContext
11
+ } from '../model';
12
+ import { INewMessage } from '../types';
13
+
14
+ export class MockChatContext
15
+ extends AbstractChatContext
16
+ implements IChatContext
17
+ {
18
+ get users() {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ export class MockChatModel extends AbstractChatModel implements IChatModel {
24
+ sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void {
25
+ // No-op
26
+ }
27
+
28
+ createChatContext(): IChatContext {
29
+ return new MockChatContext({ model: this });
30
+ }
31
+ }
@@ -7,29 +7,39 @@
7
7
  * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
8
8
  */
9
9
 
10
- import { ChatModel, IChatModel } from '../model';
11
- import { IChatMessage } from '../types';
10
+ import { AbstractChatModel, IChatContext, IChatModel } from '../model';
11
+ import { IChatMessage, INewMessage } from '../types';
12
+ import { MockChatModel, MockChatContext } from './mocks';
12
13
 
13
14
  describe('test chat model', () => {
14
15
  describe('model instantiation', () => {
15
- it('should create a ChatModel', () => {
16
- const model = new ChatModel();
17
- expect(model).toBeInstanceOf(ChatModel);
16
+ it('should create an AbstractChatModel', () => {
17
+ const model = new MockChatModel();
18
+ expect(model).toBeInstanceOf(AbstractChatModel);
18
19
  });
19
20
 
20
- it('should dispose a ChatModel', () => {
21
- const model = new ChatModel();
21
+ it('should dispose an AbstractChatModel', () => {
22
+ const model = new MockChatModel();
22
23
  model.dispose();
23
24
  expect(model.isDisposed).toBeTruthy();
24
25
  });
25
26
  });
26
27
 
27
28
  describe('incoming message', () => {
28
- class TestChat extends ChatModel {
29
+ class TestChat extends AbstractChatModel implements IChatModel {
29
30
  protected formatChatMessage(message: IChatMessage): IChatMessage {
30
31
  message.body = 'formatted msg';
31
32
  return message;
32
33
  }
34
+ sendMessage(
35
+ message: INewMessage
36
+ ): Promise<boolean | void> | boolean | void {
37
+ // No-op
38
+ }
39
+
40
+ createChatContext(): IChatContext {
41
+ return new MockChatContext({ model: this });
42
+ }
33
43
  }
34
44
 
35
45
  let model: IChatModel;
@@ -47,7 +57,7 @@ describe('test chat model', () => {
47
57
  });
48
58
 
49
59
  it('should signal incoming message', () => {
50
- model = new ChatModel();
60
+ model = new MockChatModel();
51
61
  model.messagesUpdated.connect((sender: IChatModel) => {
52
62
  expect(sender).toBe(model);
53
63
  messages = model.messages;
@@ -72,12 +82,12 @@ describe('test chat model', () => {
72
82
 
73
83
  describe('model config', () => {
74
84
  it('should have empty config', () => {
75
- const model = new ChatModel();
85
+ const model = new MockChatModel();
76
86
  expect(model.config.sendWithShiftEnter).toBeUndefined();
77
87
  });
78
88
 
79
89
  it('should allow config', () => {
80
- const model = new ChatModel({ config: { sendWithShiftEnter: true } });
90
+ const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
81
91
  expect(model.config.sendWithShiftEnter).toBeTruthy();
82
92
  });
83
93
  });
@@ -11,25 +11,26 @@ import {
11
11
  IRenderMimeRegistry,
12
12
  RenderMimeRegistry
13
13
  } from '@jupyterlab/rendermime';
14
- import { ChatModel, IChatModel } from '../model';
14
+ import { IChatModel } from '../model';
15
15
  import { ChatWidget } from '../widgets/chat-widget';
16
+ import { MockChatModel } from './mocks';
16
17
 
17
18
  describe('test chat widget', () => {
18
19
  let model: IChatModel;
19
20
  let rmRegistry: IRenderMimeRegistry;
20
21
 
21
22
  beforeEach(() => {
22
- model = new ChatModel();
23
+ model = new MockChatModel();
23
24
  rmRegistry = new RenderMimeRegistry();
24
25
  });
25
26
 
26
27
  describe('model instantiation', () => {
27
- it('should create a ChatModel', () => {
28
+ it('should create an AbstractChatModel', () => {
28
29
  const widget = new ChatWidget({ model, rmRegistry });
29
30
  expect(widget).toBeInstanceOf(ChatWidget);
30
31
  });
31
32
 
32
- it('should dispose a ChatModel', () => {
33
+ it('should dispose an AbstractChatModel', () => {
33
34
  const widget = new ChatWidget({ model, rmRegistry });
34
35
  widget.dispose();
35
36
  expect(widget.isDisposed).toBeTruthy();
@@ -22,7 +22,7 @@ export type ChatCommand = {
22
22
  * If set, this will be rendered as the icon for the command in the chat
23
23
  * commands menu. Jupyter Chat will choose a default if this is unset.
24
24
  */
25
- icon?: LabIcon | string;
25
+ icon?: LabIcon | JSX.Element | string | null;
26
26
 
27
27
  /**
28
28
  * If set, this will be rendered as the description for the command in the