@jupyter/chat 0.9.0 → 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.
- package/lib/__tests__/mocks.d.ts +9 -0
- package/lib/__tests__/mocks.js +18 -0
- package/lib/__tests__/model.spec.js +17 -10
- package/lib/__tests__/widgets.spec.js +4 -4
- package/lib/chat-commands/types.d.ts +2 -1
- package/lib/components/chat-input.d.ts +1 -1
- package/lib/components/chat-messages.js +23 -10
- package/lib/components/input/use-chat-commands.js +3 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/input-model.d.ts +53 -2
- package/lib/input-model.js +38 -0
- package/lib/model.d.ts +54 -9
- package/lib/model.js +26 -12
- package/lib/types.d.ts +5 -0
- package/lib/utils.d.ts +15 -0
- package/lib/utils.js +29 -0
- package/package.json +1 -1
- package/src/__tests__/mocks.ts +31 -0
- package/src/__tests__/model.spec.ts +21 -11
- package/src/__tests__/widgets.spec.ts +5 -4
- package/src/chat-commands/types.ts +1 -1
- package/src/components/chat-input.tsx +2 -2
- package/src/components/chat-messages.tsx +16 -6
- package/src/components/input/use-chat-commands.tsx +6 -4
- package/src/index.ts +1 -1
- package/src/input-model.ts +83 -2
- package/src/model.ts +74 -10
- package/src/types.ts +5 -0
- package/src/utils.ts +34 -0
- package/style/chat.css +6 -0
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
|
+
}
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
16
|
-
const model = new
|
|
17
|
-
expect(model).toBeInstanceOf(
|
|
16
|
+
it('should create an AbstractChatModel', () => {
|
|
17
|
+
const model = new MockChatModel();
|
|
18
|
+
expect(model).toBeInstanceOf(AbstractChatModel);
|
|
18
19
|
});
|
|
19
20
|
|
|
20
|
-
it('should dispose
|
|
21
|
-
const model = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
23
|
+
model = new MockChatModel();
|
|
23
24
|
rmRegistry = new RenderMimeRegistry();
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
describe('model instantiation', () => {
|
|
27
|
-
it('should create
|
|
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
|
|
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
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
useChatCommands
|
|
23
23
|
} from './input';
|
|
24
24
|
import { IInputModel, InputModel } from '../input-model';
|
|
25
|
-
import { IAttachment } from '../types';
|
|
26
25
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
26
|
+
import { IAttachment } from '../types';
|
|
27
27
|
|
|
28
28
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
29
29
|
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
@@ -256,7 +256,7 @@ export namespace ChatInput {
|
|
|
256
256
|
*/
|
|
257
257
|
export interface IProps {
|
|
258
258
|
/**
|
|
259
|
-
* The
|
|
259
|
+
* The input model.
|
|
260
260
|
*/
|
|
261
261
|
model: IInputModel;
|
|
262
262
|
/**
|
|
@@ -25,6 +25,7 @@ import { IChatCommandRegistry } from '../chat-commands';
|
|
|
25
25
|
import { IInputModel, InputModel } from '../input-model';
|
|
26
26
|
import { IChatModel } from '../model';
|
|
27
27
|
import { IChatMessage, IUser } from '../types';
|
|
28
|
+
import { replaceSpanToMention } from '../utils';
|
|
28
29
|
|
|
29
30
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
30
31
|
const MESSAGE_CLASS = 'jp-chat-message';
|
|
@@ -375,19 +376,27 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
375
376
|
// Create an input model only if the message is edited.
|
|
376
377
|
useEffect(() => {
|
|
377
378
|
if (edit && canEdit) {
|
|
378
|
-
setInputModel(
|
|
379
|
-
|
|
379
|
+
setInputModel(() => {
|
|
380
|
+
let body = message.body;
|
|
381
|
+
message.mentions?.forEach(user => {
|
|
382
|
+
body = replaceSpanToMention(body, user);
|
|
383
|
+
});
|
|
384
|
+
return new InputModel({
|
|
385
|
+
chatContext: model.createChatContext(),
|
|
380
386
|
onSend: (input: string, model?: IInputModel) =>
|
|
381
387
|
updateMessage(message.id, input, model),
|
|
382
388
|
onCancel: () => cancelEdition(),
|
|
383
|
-
value:
|
|
389
|
+
value: body,
|
|
390
|
+
activeCellManager: model.activeCellManager,
|
|
391
|
+
selectionWatcher: model.selectionWatcher,
|
|
392
|
+
documentManager: model.documentManager,
|
|
384
393
|
config: {
|
|
385
394
|
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
386
395
|
},
|
|
387
396
|
attachments: message.attachments,
|
|
388
|
-
|
|
389
|
-
})
|
|
390
|
-
);
|
|
397
|
+
mentions: message.mentions
|
|
398
|
+
});
|
|
399
|
+
});
|
|
391
400
|
} else {
|
|
392
401
|
setInputModel(null);
|
|
393
402
|
}
|
|
@@ -411,6 +420,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
411
420
|
const updatedMessage = { ...message };
|
|
412
421
|
updatedMessage.body = input;
|
|
413
422
|
updatedMessage.attachments = inputModel.attachments;
|
|
423
|
+
updatedMessage.mentions = inputModel.mentions;
|
|
414
424
|
model.updateMessage!(id, updatedMessage);
|
|
415
425
|
setEdit(false);
|
|
416
426
|
};
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import { useEffect, useState } from 'react';
|
|
6
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
8
7
|
import type {
|
|
9
8
|
AutocompleteChangeReason,
|
|
10
9
|
AutocompleteProps as GenericAutocompleteProps
|
|
11
10
|
} from '@mui/material';
|
|
12
11
|
import { Box } from '@mui/material';
|
|
12
|
+
import React, { useEffect, useState } from 'react';
|
|
13
13
|
|
|
14
14
|
import { ChatCommand, IChatCommandRegistry } from '../../chat-commands';
|
|
15
15
|
import { IInputModel } from '../../input-model';
|
|
@@ -131,9 +131,11 @@ export function useChatCommands(
|
|
|
131
131
|
___: unknown
|
|
132
132
|
) => {
|
|
133
133
|
const { key, ...listItemProps } = defaultProps;
|
|
134
|
-
const commandIcon: JSX.Element = (
|
|
134
|
+
const commandIcon: JSX.Element = React.isValidElement(command.icon) ? (
|
|
135
|
+
command.icon
|
|
136
|
+
) : (
|
|
135
137
|
<span>
|
|
136
|
-
{
|
|
138
|
+
{command.icon instanceof LabIcon ? (
|
|
137
139
|
<command.icon.react />
|
|
138
140
|
) : (
|
|
139
141
|
command.icon
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from './active-cell-manager';
|
|
7
|
+
export * from './chat-commands';
|
|
7
8
|
export * from './components';
|
|
8
9
|
export * from './icons';
|
|
9
10
|
export * from './input-model';
|
|
@@ -14,4 +15,3 @@ export * from './types';
|
|
|
14
15
|
export * from './widgets/chat-error';
|
|
15
16
|
export * from './widgets/chat-sidebar';
|
|
16
17
|
export * from './widgets/chat-widget';
|
|
17
|
-
export * from './chat-commands';
|
package/src/input-model.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
6
7
|
import { IDisposable } from '@lumino/disposable';
|
|
7
8
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
9
|
import { IActiveCellManager } from './active-cell-manager';
|
|
9
10
|
import { ISelectionWatcher } from './selection-watcher';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { IChatContext } from './model';
|
|
12
|
+
import { IAttachment, IUser } from './types';
|
|
12
13
|
|
|
13
14
|
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
14
15
|
|
|
@@ -16,6 +17,11 @@ const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
|
16
17
|
* The chat input interface.
|
|
17
18
|
*/
|
|
18
19
|
export interface IInputModel extends IDisposable {
|
|
20
|
+
/**
|
|
21
|
+
* The chat context (a readonly subset of the chat model).
|
|
22
|
+
*/
|
|
23
|
+
readonly chatContext: IChatContext;
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* Function to send a message.
|
|
21
27
|
*/
|
|
@@ -121,6 +127,26 @@ export interface IInputModel extends IDisposable {
|
|
|
121
127
|
* Replace the current word in the input with a new one.
|
|
122
128
|
*/
|
|
123
129
|
replaceCurrentWord(newWord: string): void;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The mentioned user list.
|
|
133
|
+
*/
|
|
134
|
+
readonly mentions: IUser[];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add user mention.
|
|
138
|
+
*/
|
|
139
|
+
addMention?(user: IUser): void;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Remove a user mention.
|
|
143
|
+
*/
|
|
144
|
+
removeMention(user: IUser): void;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear mentions list.
|
|
148
|
+
*/
|
|
149
|
+
clearMentions(): void;
|
|
124
150
|
}
|
|
125
151
|
|
|
126
152
|
/**
|
|
@@ -129,8 +155,10 @@ export interface IInputModel extends IDisposable {
|
|
|
129
155
|
export class InputModel implements IInputModel {
|
|
130
156
|
constructor(options: InputModel.IOptions) {
|
|
131
157
|
this._onSend = options.onSend;
|
|
158
|
+
this._chatContext = options.chatContext;
|
|
132
159
|
this._value = options.value || '';
|
|
133
160
|
this._attachments = options.attachments || [];
|
|
161
|
+
this._mentions = options.mentions || [];
|
|
134
162
|
this.cursorIndex = options.cursorIndex || this.value.length;
|
|
135
163
|
this._activeCellManager = options.activeCellManager ?? null;
|
|
136
164
|
this._selectionWatcher = options.selectionWatcher ?? null;
|
|
@@ -141,6 +169,13 @@ export class InputModel implements IInputModel {
|
|
|
141
169
|
this.cancel = options.onCancel;
|
|
142
170
|
}
|
|
143
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The chat context (a readonly subset of the chat model);
|
|
174
|
+
*/
|
|
175
|
+
get chatContext(): IChatContext {
|
|
176
|
+
return this._chatContext;
|
|
177
|
+
}
|
|
178
|
+
|
|
144
179
|
/**
|
|
145
180
|
* Function to send a message.
|
|
146
181
|
*/
|
|
@@ -335,6 +370,40 @@ export class InputModel implements IInputModel {
|
|
|
335
370
|
this.value = this.value.slice(0, start) + newWord + this.value.slice(end);
|
|
336
371
|
}
|
|
337
372
|
|
|
373
|
+
/**
|
|
374
|
+
* The mentioned user list.
|
|
375
|
+
*/
|
|
376
|
+
get mentions(): IUser[] {
|
|
377
|
+
return this._mentions;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Add a user mention.
|
|
382
|
+
*/
|
|
383
|
+
addMention(user: IUser): void {
|
|
384
|
+
const usernames = this._mentions.map(user => user.username);
|
|
385
|
+
if (!usernames.includes(user.username)) {
|
|
386
|
+
this._mentions.push(user);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Remove a user mention.
|
|
392
|
+
*/
|
|
393
|
+
removeMention(user: IUser): void {
|
|
394
|
+
const index = this._mentions.indexOf(user);
|
|
395
|
+
if (index > -1) {
|
|
396
|
+
this._mentions.splice(index, 1);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Clear mentions list.
|
|
402
|
+
*/
|
|
403
|
+
clearMentions = (): void => {
|
|
404
|
+
this._mentions = [];
|
|
405
|
+
};
|
|
406
|
+
|
|
338
407
|
/**
|
|
339
408
|
* Dispose the input model.
|
|
340
409
|
*/
|
|
@@ -353,10 +422,12 @@ export class InputModel implements IInputModel {
|
|
|
353
422
|
}
|
|
354
423
|
|
|
355
424
|
private _onSend: (input: string, model?: InputModel) => void;
|
|
425
|
+
private _chatContext: IChatContext;
|
|
356
426
|
private _value: string;
|
|
357
427
|
private _cursorIndex: number | null = null;
|
|
358
428
|
private _currentWord: string | null = null;
|
|
359
429
|
private _attachments: IAttachment[];
|
|
430
|
+
private _mentions: IUser[];
|
|
360
431
|
private _activeCellManager: IActiveCellManager | null;
|
|
361
432
|
private _selectionWatcher: ISelectionWatcher | null;
|
|
362
433
|
private _documentManager: IDocumentManager | null;
|
|
@@ -372,6 +443,11 @@ export class InputModel implements IInputModel {
|
|
|
372
443
|
|
|
373
444
|
export namespace InputModel {
|
|
374
445
|
export interface IOptions {
|
|
446
|
+
/**
|
|
447
|
+
* The chat context (a readonly subset of the chat model).
|
|
448
|
+
*/
|
|
449
|
+
chatContext: IChatContext;
|
|
450
|
+
|
|
375
451
|
/**
|
|
376
452
|
* The function that should send the message.
|
|
377
453
|
* @param content - the content of the message.
|
|
@@ -394,6 +470,11 @@ export namespace InputModel {
|
|
|
394
470
|
*/
|
|
395
471
|
attachments?: IAttachment[];
|
|
396
472
|
|
|
473
|
+
/**
|
|
474
|
+
* The initial mentions.
|
|
475
|
+
*/
|
|
476
|
+
mentions?: IUser[];
|
|
477
|
+
|
|
397
478
|
/**
|
|
398
479
|
* The current cursor index.
|
|
399
480
|
* This refers to the index of the character in front of the cursor.
|
package/src/model.ts
CHANGED
|
@@ -8,6 +8,9 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
8
8
|
import { IDisposable } from '@lumino/disposable';
|
|
9
9
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
10
10
|
|
|
11
|
+
import { IActiveCellManager } from './active-cell-manager';
|
|
12
|
+
import { IInputModel, InputModel } from './input-model';
|
|
13
|
+
import { ISelectionWatcher } from './selection-watcher';
|
|
11
14
|
import {
|
|
12
15
|
IChatHistory,
|
|
13
16
|
INewMessage,
|
|
@@ -15,9 +18,7 @@ import {
|
|
|
15
18
|
IConfig,
|
|
16
19
|
IUser
|
|
17
20
|
} from './types';
|
|
18
|
-
import {
|
|
19
|
-
import { ISelectionWatcher } from './selection-watcher';
|
|
20
|
-
import { IInputModel, InputModel } from './input-model';
|
|
21
|
+
import { replaceMentionToSpan } from './utils';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* The chat model interface.
|
|
@@ -172,18 +173,24 @@ export interface IChatModel extends IDisposable {
|
|
|
172
173
|
* Update the current writers list.
|
|
173
174
|
*/
|
|
174
175
|
updateWriters(writers: IUser[]): void;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create the chat context that will be passed to the input model.
|
|
179
|
+
*/
|
|
180
|
+
createChatContext(): IChatContext;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
184
|
+
* An abstract implementation of IChatModel.
|
|
185
|
+
*
|
|
186
|
+
* The class inheriting from it must implement at least:
|
|
187
|
+
* - sendMessage(message: INewMessage)
|
|
181
188
|
*/
|
|
182
|
-
export class
|
|
189
|
+
export abstract class AbstractChatModel implements IChatModel {
|
|
183
190
|
/**
|
|
184
191
|
* Create a new chat model.
|
|
185
192
|
*/
|
|
186
|
-
constructor(options:
|
|
193
|
+
constructor(options: IChatModel.IOptions = {}) {
|
|
187
194
|
if (options.id) {
|
|
188
195
|
this.id = options.id;
|
|
189
196
|
}
|
|
@@ -198,6 +205,7 @@ export class ChatModel implements IChatModel {
|
|
|
198
205
|
};
|
|
199
206
|
|
|
200
207
|
this._inputModel = new InputModel({
|
|
208
|
+
chatContext: this.createChatContext(),
|
|
201
209
|
activeCellManager: options.activeCellManager,
|
|
202
210
|
selectionWatcher: options.selectionWatcher,
|
|
203
211
|
documentManager: options.documentManager,
|
|
@@ -421,7 +429,9 @@ export class ChatModel implements IChatModel {
|
|
|
421
429
|
* @param message - the message to send.
|
|
422
430
|
* @returns whether the message has been sent or not.
|
|
423
431
|
*/
|
|
424
|
-
sendMessage(
|
|
432
|
+
abstract sendMessage(
|
|
433
|
+
message: INewMessage
|
|
434
|
+
): Promise<boolean | void> | boolean | void;
|
|
425
435
|
|
|
426
436
|
/**
|
|
427
437
|
* Clear the message list.
|
|
@@ -453,6 +463,9 @@ export class ChatModel implements IChatModel {
|
|
|
453
463
|
* Can be useful if some actions are required on the message.
|
|
454
464
|
*/
|
|
455
465
|
protected formatChatMessage(message: IChatMessage): IChatMessage {
|
|
466
|
+
message.mentions?.forEach(user => {
|
|
467
|
+
message.body = replaceMentionToSpan(message.body, user);
|
|
468
|
+
});
|
|
456
469
|
return message;
|
|
457
470
|
}
|
|
458
471
|
|
|
@@ -537,6 +550,11 @@ export class ChatModel implements IChatModel {
|
|
|
537
550
|
this._writersChanged.emit(writers);
|
|
538
551
|
}
|
|
539
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Create the chat context that will be passed to the input model.
|
|
555
|
+
*/
|
|
556
|
+
abstract createChatContext(): IChatContext;
|
|
557
|
+
|
|
540
558
|
/**
|
|
541
559
|
* Add unread messages to the list.
|
|
542
560
|
* @param indexes - list of new indexes.
|
|
@@ -609,7 +627,7 @@ export class ChatModel implements IChatModel {
|
|
|
609
627
|
/**
|
|
610
628
|
* The chat model namespace.
|
|
611
629
|
*/
|
|
612
|
-
export namespace
|
|
630
|
+
export namespace IChatModel {
|
|
613
631
|
/**
|
|
614
632
|
* The instantiation options for a ChatModel.
|
|
615
633
|
*/
|
|
@@ -645,3 +663,49 @@ export namespace ChatModel {
|
|
|
645
663
|
documentManager?: IDocumentManager | null;
|
|
646
664
|
}
|
|
647
665
|
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Interface of the chat context, a 'subset' of the model with readonly attribute,
|
|
669
|
+
* which can be passed to the input model.
|
|
670
|
+
* This allows third party extensions to get some attribute of the model without
|
|
671
|
+
* exposing the method that can modify it.
|
|
672
|
+
*/
|
|
673
|
+
export interface IChatContext {
|
|
674
|
+
/**
|
|
675
|
+
* The name of the chat.
|
|
676
|
+
*/
|
|
677
|
+
readonly name: string;
|
|
678
|
+
/**
|
|
679
|
+
* A copy of the messages.
|
|
680
|
+
*/
|
|
681
|
+
readonly messages: IChatMessage[];
|
|
682
|
+
/**
|
|
683
|
+
* A list of all users who have connected to this chat.
|
|
684
|
+
*/
|
|
685
|
+
readonly users: IUser[];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* An abstract base class implementing `IChatContext`. This can be extended into
|
|
690
|
+
* a complete implementation, as done in `jupyterlab-chat`.
|
|
691
|
+
*/
|
|
692
|
+
export abstract class AbstractChatContext implements IChatContext {
|
|
693
|
+
constructor(options: { model: IChatModel }) {
|
|
694
|
+
this._model = options.model;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
get name(): string {
|
|
698
|
+
return this._model.name;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
get messages(): IChatMessage[] {
|
|
702
|
+
return [...this._model.messages];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* ABSTRACT: Should return a list of users who have connected to this chat.
|
|
707
|
+
*/
|
|
708
|
+
abstract get users(): IUser[];
|
|
709
|
+
|
|
710
|
+
protected _model: IChatModel;
|
|
711
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface IUser {
|
|
|
13
13
|
initials?: string;
|
|
14
14
|
color?: string;
|
|
15
15
|
avatar_url?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The string to use to mention a user in the chat.
|
|
18
|
+
*/
|
|
19
|
+
mention_name?: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -51,6 +55,7 @@ export interface IChatMessage<T = IUser, U = IAttachment> {
|
|
|
51
55
|
time: number;
|
|
52
56
|
sender: T;
|
|
53
57
|
attachments?: U[];
|
|
58
|
+
mentions?: T[];
|
|
54
59
|
raw_time?: boolean;
|
|
55
60
|
deleted?: boolean;
|
|
56
61
|
edited?: boolean;
|
package/src/utils.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { FileEditor } from '@jupyterlab/fileeditor';
|
|
|
10
10
|
import { Notebook } from '@jupyterlab/notebook';
|
|
11
11
|
import { Widget } from '@lumino/widgets';
|
|
12
12
|
|
|
13
|
+
import { IUser } from './types';
|
|
14
|
+
|
|
15
|
+
const MENTION_CLASS = 'jp-chat-mention';
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Gets the editor instance used by a document widget. Returns `null` if unable.
|
|
15
19
|
*/
|
|
@@ -45,3 +49,33 @@ export function getCellIndex(notebook: Notebook, cellId: string): number {
|
|
|
45
49
|
);
|
|
46
50
|
return idx === undefined ? -1 : idx;
|
|
47
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace a mention to user (@someone) to a span, for markdown renderer.
|
|
55
|
+
*
|
|
56
|
+
* @param content - the content to update.
|
|
57
|
+
* @param user - the user mentioned.
|
|
58
|
+
*/
|
|
59
|
+
export function replaceMentionToSpan(content: string, user: IUser): string {
|
|
60
|
+
if (!user.mention_name) {
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
const regex = new RegExp(user.mention_name, 'g');
|
|
64
|
+
const mention = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
|
|
65
|
+
return content.replace(regex, mention);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Replace a span to a mentioned to user string (@someone).
|
|
70
|
+
*
|
|
71
|
+
* @param content - the content to update.
|
|
72
|
+
* @param user - the user mentioned.
|
|
73
|
+
*/
|
|
74
|
+
export function replaceSpanToMention(content: string, user: IUser): string {
|
|
75
|
+
if (!user.mention_name) {
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
const span = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
|
|
79
|
+
const regex = new RegExp(span, 'g');
|
|
80
|
+
return content.replace(regex, user.mention_name);
|
|
81
|
+
}
|