@jupyter/chat 0.1.0 → 0.3.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 (55) hide show
  1. package/lib/active-cell-manager.d.ts +151 -0
  2. package/lib/active-cell-manager.js +201 -0
  3. package/lib/components/chat-input.d.ts +14 -4
  4. package/lib/components/chat-input.js +118 -10
  5. package/lib/components/chat-messages.d.ts +45 -15
  6. package/lib/components/chat-messages.js +237 -55
  7. package/lib/components/chat.d.ts +21 -6
  8. package/lib/components/chat.js +15 -45
  9. package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
  10. package/lib/components/code-blocks/code-toolbar.js +70 -0
  11. package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
  12. package/lib/components/code-blocks/copy-button.js +43 -0
  13. package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
  14. package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
  15. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
  16. package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
  17. package/lib/components/rendermime-markdown.d.ts +2 -0
  18. package/lib/components/rendermime-markdown.js +29 -15
  19. package/lib/components/scroll-container.js +1 -19
  20. package/lib/icons.d.ts +2 -0
  21. package/lib/icons.js +10 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.js +2 -0
  24. package/lib/model.d.ts +98 -14
  25. package/lib/model.js +197 -6
  26. package/lib/registry.d.ts +78 -0
  27. package/lib/registry.js +83 -0
  28. package/lib/types.d.ts +60 -4
  29. package/lib/widgets/chat-sidebar.d.ts +3 -4
  30. package/lib/widgets/chat-sidebar.js +2 -2
  31. package/lib/widgets/chat-widget.d.ts +2 -8
  32. package/lib/widgets/chat-widget.js +6 -6
  33. package/package.json +204 -200
  34. package/src/active-cell-manager.ts +318 -0
  35. package/src/components/chat-input.tsx +196 -50
  36. package/src/components/chat-messages.tsx +357 -95
  37. package/src/components/chat.tsx +43 -69
  38. package/src/components/code-blocks/code-toolbar.tsx +143 -0
  39. package/src/components/code-blocks/copy-button.tsx +68 -0
  40. package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
  41. package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
  42. package/src/components/rendermime-markdown.tsx +44 -20
  43. package/src/components/scroll-container.tsx +1 -25
  44. package/src/icons.ts +12 -0
  45. package/src/index.ts +2 -0
  46. package/src/model.ts +275 -21
  47. package/src/registry.ts +129 -0
  48. package/src/types.ts +62 -4
  49. package/src/widgets/chat-sidebar.tsx +3 -15
  50. package/src/widgets/chat-widget.tsx +8 -21
  51. package/style/chat.css +40 -0
  52. package/style/icons/read.svg +11 -0
  53. package/style/icons/replace-cell.svg +8 -0
  54. package/lib/components/copy-button.js +0 -35
  55. package/src/components/copy-button.tsx +0 -55
package/lib/model.js CHANGED
@@ -13,12 +13,22 @@ export class ChatModel {
13
13
  * Create a new chat model.
14
14
  */
15
15
  constructor(options = {}) {
16
- var _a;
16
+ var _a, _b;
17
17
  this._messages = [];
18
- this._id = '';
18
+ this._unreadMessages = [];
19
+ this._messagesInViewport = [];
20
+ this._name = '';
19
21
  this._isDisposed = false;
22
+ this._notificationId = null;
20
23
  this._messagesUpdated = new Signal(this);
21
- this._config = (_a = options.config) !== null && _a !== void 0 ? _a : {};
24
+ this._configChanged = new Signal(this);
25
+ this._unreadChanged = new Signal(this);
26
+ this._viewportChanged = new Signal(this);
27
+ const config = (_a = options.config) !== null && _a !== void 0 ? _a : {};
28
+ // Stack consecutive messages from the same user by default.
29
+ this._config = { stackMessages: true, ...config };
30
+ this._commands = options.commands;
31
+ this._activeCellManager = (_b = options.activeCellManager) !== null && _b !== void 0 ? _b : null;
22
32
  }
23
33
  /**
24
34
  * The chat messages list.
@@ -26,8 +36,11 @@ export class ChatModel {
26
36
  get messages() {
27
37
  return this._messages;
28
38
  }
39
+ get activeCellManager() {
40
+ return this._activeCellManager;
41
+ }
29
42
  /**
30
- * The chat model ID.
43
+ * The chat model id.
31
44
  */
32
45
  get id() {
33
46
  return this._id;
@@ -35,6 +48,34 @@ export class ChatModel {
35
48
  set id(value) {
36
49
  this._id = value;
37
50
  }
51
+ /**
52
+ * The chat model name.
53
+ */
54
+ get name() {
55
+ return this._name;
56
+ }
57
+ set name(value) {
58
+ this._name = value;
59
+ }
60
+ /**
61
+ * Timestamp of the last read message in local storage.
62
+ */
63
+ get lastRead() {
64
+ var _a;
65
+ if (this._id === undefined) {
66
+ return 0;
67
+ }
68
+ const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
69
+ return (_a = storage.lastRead) !== null && _a !== void 0 ? _a : 0;
70
+ }
71
+ set lastRead(value) {
72
+ if (this._id === undefined) {
73
+ return;
74
+ }
75
+ const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
76
+ storage.lastRead = value;
77
+ localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
78
+ }
38
79
  /**
39
80
  * The chat settings.
40
81
  */
@@ -42,14 +83,95 @@ export class ChatModel {
42
83
  return this._config;
43
84
  }
44
85
  set config(value) {
86
+ const stackMessagesChanged = 'stackMessages' in value &&
87
+ this._config.stackMessages !== value.stackMessages;
88
+ const unreadNotificationsChanged = 'unreadNotifications' in value &&
89
+ this._config.unreadNotifications !== value.unreadNotifications;
45
90
  this._config = { ...this._config, ...value };
91
+ this._configChanged.emit(this._config);
92
+ // Update the stacked status of the messages and the view.
93
+ if (stackMessagesChanged) {
94
+ if (this._config.stackMessages) {
95
+ this._messages.slice(1).forEach((message, idx) => {
96
+ const previousUser = this._messages[idx].sender.username;
97
+ message.stacked = previousUser === message.sender.username;
98
+ });
99
+ }
100
+ else {
101
+ this._messages.forEach(message => {
102
+ delete message.stacked;
103
+ });
104
+ }
105
+ this._messagesUpdated.emit();
106
+ }
107
+ // remove existing notifications if they are not required anymore.
108
+ if (unreadNotificationsChanged && !this._config.unreadNotifications) {
109
+ this._notify(0);
110
+ }
46
111
  }
47
112
  /**
48
- * The signal emitted when the messages list is updated.
113
+ * The indexes list of the unread messages.
114
+ */
115
+ get unreadMessages() {
116
+ return this._unreadMessages;
117
+ }
118
+ set unreadMessages(unread) {
119
+ var _a;
120
+ const recentlyRead = this._unreadMessages.filter(elem => !unread.includes(elem));
121
+ const unreadCountDiff = unread.length - this._unreadMessages.length;
122
+ this._unreadMessages = unread;
123
+ this._unreadChanged.emit(this._unreadMessages);
124
+ // Notify the change.
125
+ this._notify(unread.length, unreadCountDiff > 0);
126
+ // Save the last read to the local storage.
127
+ if (this._id !== undefined && recentlyRead.length) {
128
+ let lastReadChanged = false;
129
+ let lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : this.messages[recentlyRead[0]].time;
130
+ recentlyRead.forEach(index => {
131
+ if (this.messages[index].time > lastRead) {
132
+ lastRead = this.messages[index].time;
133
+ lastReadChanged = true;
134
+ }
135
+ });
136
+ if (lastReadChanged) {
137
+ this.lastRead = lastRead;
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * The indexes list of the messages currently in the viewport.
143
+ */
144
+ get messagesInViewport() {
145
+ return this._messagesInViewport;
146
+ }
147
+ set messagesInViewport(values) {
148
+ this._messagesInViewport = values;
149
+ this._viewportChanged.emit(values);
150
+ }
151
+ /**
152
+ * A signal emitting when the messages list is updated.
49
153
  */
50
154
  get messagesUpdated() {
51
155
  return this._messagesUpdated;
52
156
  }
157
+ /**
158
+ * A signal emitting when the messages list is updated.
159
+ */
160
+ get configChanged() {
161
+ return this._configChanged;
162
+ }
163
+ /**
164
+ * A signal emitting when unread messages change.
165
+ */
166
+ get unreadChanged() {
167
+ return this._unreadChanged;
168
+ }
169
+ /**
170
+ * A signal emitting when the viewport change.
171
+ */
172
+ get viewportChanged() {
173
+ return this._viewportChanged;
174
+ }
53
175
  /**
54
176
  * Send a message, to be defined depending on the chosen technology.
55
177
  * Default to no-op.
@@ -108,11 +230,32 @@ export class ChatModel {
108
230
  * @param messages - the messages list.
109
231
  */
110
232
  messagesInserted(index, messages) {
233
+ var _a;
111
234
  const formattedMessages = [];
112
- messages.forEach(message => {
235
+ const unreadIndexes = [];
236
+ const lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : 0;
237
+ // Format the messages.
238
+ messages.forEach((message, idx) => {
113
239
  formattedMessages.push(this.formatChatMessage(message));
240
+ if (message.time > lastRead) {
241
+ unreadIndexes.push(index + idx);
242
+ }
114
243
  });
244
+ // Insert the messages.
115
245
  this._messages.splice(index, 0, ...formattedMessages);
246
+ if (this._config.stackMessages) {
247
+ // Check if some messages should be stacked by comparing each message' sender
248
+ // with the previous one.
249
+ const lastIdx = index + formattedMessages.length - 1;
250
+ const start = index === 0 ? 1 : index;
251
+ const end = this._messages.length > lastIdx + 1 ? lastIdx + 1 : lastIdx;
252
+ for (let idx = start; idx <= end; idx++) {
253
+ const message = this._messages[idx];
254
+ const previousUser = this._messages[idx - 1].sender.username;
255
+ message.stacked = previousUser === message.sender.username;
256
+ }
257
+ }
258
+ this._addUnreadMessages(unreadIndexes);
116
259
  this._messagesUpdated.emit();
117
260
  }
118
261
  /**
@@ -125,4 +268,52 @@ export class ChatModel {
125
268
  this._messages.splice(index, count);
126
269
  this._messagesUpdated.emit();
127
270
  }
271
+ /**
272
+ * Add unread messages to the list.
273
+ * @param indexes - list of new indexes.
274
+ */
275
+ _addUnreadMessages(indexes) {
276
+ const unread = new Set(this._unreadMessages);
277
+ indexes.forEach(index => unread.add(index));
278
+ this.unreadMessages = Array.from(unread.values());
279
+ }
280
+ /**
281
+ * Notifications on unread messages.
282
+ *
283
+ * @param unreadCount - number of unread messages.
284
+ * If the value is 0, existing notification will be deleted.
285
+ * @param canCreate - whether to create a notification if it does not exist.
286
+ * Usually it is used when there are new unread messages, and not when the
287
+ * unread messages count decrease.
288
+ */
289
+ _notify(unreadCount, canCreate = false) {
290
+ if (this._commands) {
291
+ if (unreadCount && this._config.unreadNotifications) {
292
+ // Update the notification if exist.
293
+ this._commands
294
+ .execute('apputils:update-notification', {
295
+ id: this._notificationId,
296
+ message: `${unreadCount} incoming message(s) ${this._name ? 'in ' + this._name : ''}`
297
+ })
298
+ .then(success => {
299
+ // Create a new notification only if messages are added.
300
+ if (!success && canCreate) {
301
+ this._commands.execute('apputils:notify', {
302
+ type: 'info',
303
+ message: `${unreadCount} incoming message(s) in ${this._name}`
304
+ }).then(id => {
305
+ this._notificationId = id;
306
+ });
307
+ }
308
+ });
309
+ }
310
+ else if (this._notificationId) {
311
+ // Delete notification if there is no more unread messages.
312
+ this._commands.execute('apputils:dismiss-notification', {
313
+ id: this._notificationId
314
+ });
315
+ this._notificationId = null;
316
+ }
317
+ }
318
+ }
128
319
  }
@@ -0,0 +1,78 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ import { IAutocompletionCommandsProps } from './types';
3
+ /**
4
+ * The token for the autocomplete registry, which can be provided by an extension
5
+ * using @jupyter/chat package.
6
+ */
7
+ export declare const IAutocompletionRegistry: Token<IAutocompletionRegistry>;
8
+ /**
9
+ * The interface of a registry to provide autocompleters.
10
+ */
11
+ export interface IAutocompletionRegistry {
12
+ /**
13
+ * The default autocompletion name.
14
+ */
15
+ default: string | null;
16
+ /**
17
+ * Get the default autocompletion.
18
+ */
19
+ getDefaultCompletion(): IAutocompletionCommandsProps | undefined;
20
+ /**
21
+ * Return a registered autocomplete props.
22
+ *
23
+ * @param name - the name of the registered autocomplete props.
24
+ */
25
+ get(name: string): IAutocompletionCommandsProps | undefined;
26
+ /**
27
+ * Register autocomplete props.
28
+ *
29
+ * @param name - the name for the registration.
30
+ * @param autocompletion - the autocomplete props.
31
+ */
32
+ add(name: string, autocompletion: IAutocompletionCommandsProps): boolean;
33
+ /**
34
+ * Remove a registered autocomplete props.
35
+ *
36
+ * @param name - the name of the autocomplete props.
37
+ */
38
+ remove(name: string): boolean;
39
+ }
40
+ /**
41
+ * A registry to provide autocompleters.
42
+ */
43
+ export declare class AutocompletionRegistry implements IAutocompletionRegistry {
44
+ /**
45
+ * Getter and setter for the default autocompletion name.
46
+ */
47
+ get default(): string | null;
48
+ set default(name: string | null);
49
+ /**
50
+ * Get the default autocompletion.
51
+ */
52
+ getDefaultCompletion(): IAutocompletionCommandsProps | undefined;
53
+ /**
54
+ * Return a registered autocomplete props.
55
+ *
56
+ * @param name - the name of the registered autocomplete props.
57
+ */
58
+ get(name: string): IAutocompletionCommandsProps | undefined;
59
+ /**
60
+ * Register autocomplete props.
61
+ *
62
+ * @param name - the name for the registration.
63
+ * @param autocompletion - the autocomplete props.
64
+ */
65
+ add(name: string, autocompletion: IAutocompletionCommandsProps, isDefault?: boolean): boolean;
66
+ /**
67
+ * Remove a registered autocomplete props.
68
+ *
69
+ * @param name - the name of the autocomplete props.
70
+ */
71
+ remove(name: string): boolean;
72
+ /**
73
+ * Remove all registered autocompletions.
74
+ */
75
+ removeAll(): void;
76
+ private _default;
77
+ private _autocompletions;
78
+ }
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { Token } from '@lumino/coreutils';
6
+ /**
7
+ * The token for the autocomplete registry, which can be provided by an extension
8
+ * using @jupyter/chat package.
9
+ */
10
+ export const IAutocompletionRegistry = new Token('@jupyter/chat:IAutocompleteRegistry');
11
+ /**
12
+ * A registry to provide autocompleters.
13
+ */
14
+ export class AutocompletionRegistry {
15
+ constructor() {
16
+ this._default = null;
17
+ this._autocompletions = new Map();
18
+ }
19
+ /**
20
+ * Getter and setter for the default autocompletion name.
21
+ */
22
+ get default() {
23
+ return this._default;
24
+ }
25
+ set default(name) {
26
+ if (name === null || this._autocompletions.has(name)) {
27
+ this._default = name;
28
+ }
29
+ else {
30
+ console.warn(`There is no registered completer with the name '${name}'`);
31
+ }
32
+ }
33
+ /**
34
+ * Get the default autocompletion.
35
+ */
36
+ getDefaultCompletion() {
37
+ if (this._default === null) {
38
+ return undefined;
39
+ }
40
+ return this._autocompletions.get(this._default);
41
+ }
42
+ /**
43
+ * Return a registered autocomplete props.
44
+ *
45
+ * @param name - the name of the registered autocomplete props.
46
+ */
47
+ get(name) {
48
+ return this._autocompletions.get(name);
49
+ }
50
+ /**
51
+ * Register autocomplete props.
52
+ *
53
+ * @param name - the name for the registration.
54
+ * @param autocompletion - the autocomplete props.
55
+ */
56
+ add(name, autocompletion, isDefault = false) {
57
+ if (!this._autocompletions.has(name)) {
58
+ this._autocompletions.set(name, autocompletion);
59
+ if (this._autocompletions.size === 1 || isDefault) {
60
+ this.default = name;
61
+ }
62
+ return true;
63
+ }
64
+ else {
65
+ console.warn(`A completer with the name '${name}' is already registered`);
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Remove a registered autocomplete props.
71
+ *
72
+ * @param name - the name of the autocomplete props.
73
+ */
74
+ remove(name) {
75
+ return this._autocompletions.delete(name);
76
+ }
77
+ /**
78
+ * Remove all registered autocompletions.
79
+ */
80
+ removeAll() {
81
+ this._autocompletions.clear();
82
+ }
83
+ }
package/lib/types.d.ts CHANGED
@@ -13,21 +13,40 @@ export interface IUser {
13
13
  * The configuration interface.
14
14
  */
15
15
  export interface IConfig {
16
+ /**
17
+ * Whether to send a message via Shift-Enter instead of Enter.
18
+ */
16
19
  sendWithShiftEnter?: boolean;
20
+ /**
21
+ * Last read message (no use yet).
22
+ */
17
23
  lastRead?: number;
24
+ /**
25
+ * Whether to stack consecutive messages from same user.
26
+ */
27
+ stackMessages?: boolean;
28
+ /**
29
+ * Whether to enable or not the notifications on unread messages.
30
+ */
31
+ unreadNotifications?: boolean;
32
+ /**
33
+ * Whether to enable or not the code toolbar.
34
+ */
35
+ enableCodeToolbar?: boolean;
18
36
  }
19
37
  /**
20
- * The chat message decription.
38
+ * The chat message description.
21
39
  */
22
- export interface IChatMessage {
40
+ export interface IChatMessage<T = IUser> {
23
41
  type: 'msg';
24
42
  body: string;
25
43
  id: string;
26
44
  time: number;
27
- sender: IUser | string;
45
+ sender: T;
28
46
  raw_time?: boolean;
29
47
  deleted?: boolean;
30
48
  edited?: boolean;
49
+ stacked?: boolean;
31
50
  }
32
51
  /**
33
52
  * The chat history interface.
@@ -43,7 +62,44 @@ export interface INewMessage {
43
62
  id?: string;
44
63
  }
45
64
  /**
46
- * An empty interface to describe optional settings taht could be fetched from server.
65
+ * An empty interface to describe optional settings that could be fetched from server.
47
66
  */
48
67
  export interface ISettings {
49
68
  }
69
+ /**
70
+ * The autocomplete command type.
71
+ */
72
+ export type AutocompleteCommand = {
73
+ label: string;
74
+ };
75
+ /**
76
+ * The properties of the autocompletion.
77
+ *
78
+ * The autocompletion component will open if the 'opener' string is typed at the
79
+ * beginning of the input field.
80
+ */
81
+ export interface IAutocompletionCommandsProps {
82
+ /**
83
+ * The string that open the completer.
84
+ */
85
+ opener: string;
86
+ /**
87
+ * The list of available commands.
88
+ */
89
+ commands?: AutocompleteCommand[] | (() => Promise<AutocompleteCommand[]>);
90
+ /**
91
+ * The props for the Autocomplete component.
92
+ *
93
+ * Must be compatible with https://mui.com/material-ui/api/autocomplete/#props.
94
+ *
95
+ * ## NOTES:
96
+ * - providing `options` will overwrite the commands argument.
97
+ * - providing `renderInput` will overwrite the input component.
98
+ * - providing `renderOptions` allows to customize the rendering of the component.
99
+ * - some arguments should not be provided and would be overwritten:
100
+ * - inputValue
101
+ * - onInputChange
102
+ * - onHighlightChange
103
+ */
104
+ props?: any;
105
+ }
@@ -1,4 +1,3 @@
1
- import { IThemeManager, ReactWidget } from '@jupyterlab/apputils';
2
- import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
- import { IChatModel } from '../model';
4
- export declare function buildChatSidebar(chatModel: IChatModel, themeManager: IThemeManager | null, rmRegistry: IRenderMimeRegistry): ReactWidget;
1
+ import { ReactWidget } from '@jupyterlab/apputils';
2
+ import { Chat } from '../components/chat';
3
+ export declare function buildChatSidebar(options: Chat.IOptions): ReactWidget;
@@ -6,8 +6,8 @@ import { ReactWidget } from '@jupyterlab/apputils';
6
6
  import React from 'react';
7
7
  import { Chat } from '../components/chat';
8
8
  import { chatIcon } from '../icons';
9
- export function buildChatSidebar(chatModel, themeManager, rmRegistry) {
10
- const ChatWidget = ReactWidget.create(React.createElement(Chat, { model: chatModel, themeManager: themeManager, rmRegistry: rmRegistry }));
9
+ export function buildChatSidebar(options) {
10
+ const ChatWidget = ReactWidget.create(React.createElement(Chat, { ...options }));
11
11
  ChatWidget.id = 'jupyter-chat::side-panel';
12
12
  ChatWidget.title.icon = chatIcon;
13
13
  ChatWidget.title.caption = 'Jupyter Chat'; // TODO: i18n
@@ -5,15 +5,9 @@ import { IChatModel } from '../model';
5
5
  export declare class ChatWidget extends ReactWidget {
6
6
  constructor(options: Chat.IOptions);
7
7
  /**
8
- * Gte the model of the widget.
8
+ * Get the model of the widget.
9
9
  */
10
10
  get model(): IChatModel;
11
11
  render(): React.JSX.Element;
12
- private readonly _model;
13
- private _themeManager;
14
- private _rmRegistry;
15
- }
16
- export declare namespace ChatWidget {
17
- interface IOptions extends Chat.IOptions {
18
- }
12
+ private _chatOptions;
19
13
  }
@@ -12,17 +12,17 @@ export class ChatWidget extends ReactWidget {
12
12
  this.id = 'jupyter-chat::widget';
13
13
  this.title.icon = chatIcon;
14
14
  this.title.caption = 'Jupyter Chat'; // TODO: i18n
15
- this._model = options.model;
16
- this._themeManager = (options === null || options === void 0 ? void 0 : options.themeManager) || null;
17
- this._rmRegistry = options.rmRegistry;
15
+ this._chatOptions = options;
18
16
  }
19
17
  /**
20
- * Gte the model of the widget.
18
+ * Get the model of the widget.
21
19
  */
22
20
  get model() {
23
- return this._model;
21
+ return this._chatOptions.model;
24
22
  }
25
23
  render() {
26
- return (React.createElement(Chat, { model: this._model, themeManager: this._themeManager, rmRegistry: this._rmRegistry }));
24
+ // The model need to be passed, otherwise it is undefined in the widget in
25
+ // the case of collaborative document.
26
+ return React.createElement(Chat, { ...this._chatOptions, model: this._chatOptions.model });
27
27
  }
28
28
  }