Skip to content

Commit 8ec997e

Browse files
brichetdlqqqgithub-actions[bot]
authored
Mention users in messages (using @) (#190)
* Add the chat model to the IChatProvider method's arguments * Add chat command for user mention * Add user avatar in command suggestion * Add mentions in the message model * Display mentions in messages * Add the mention string in the IUser interface, to be able to associate any string to a user mention * Fix when there are several mentions in the same message (on send and on display) * Allow to edit a message and update mentions * Add tests on user mention * Add a chat context (readonly subset of the chat model) to the input model * Remove generic context type since all contexts should implements IChatContext * Update doc string * remove type casts from LabChatContext impl * add users to IChatContext, make ChatContext abstract * remove type casts from MentionCommandProvider, use JSX * fix tests, add mock classes in separate file * Automatic application of license header * Update the lite example --------- Co-authored-by: David L. Qiu <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 60f3edc commit 8ec997e

File tree

20 files changed

+621
-47
lines changed

20 files changed

+621
-47
lines changed

docs/jupyter-chat-example/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
IAttachment,
1212
IChatMessage,
1313
INewMessage,
14-
SelectionWatcher
14+
SelectionWatcher,
15+
IChatContext,
16+
AbstractChatContext
1517
} from '@jupyter/chat';
1618
import {
1719
JupyterFrontEnd,
@@ -24,6 +26,12 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2426
import { ISettingRegistry } from '@jupyterlab/settingregistry';
2527
import { UUID } from '@lumino/coreutils';
2628

29+
class ChatContext extends AbstractChatContext {
30+
get users() {
31+
return [];
32+
}
33+
}
34+
2735
class MyChatModel extends AbstractChatModel {
2836
sendMessage(
2937
newMessage: INewMessage
@@ -39,6 +47,10 @@ class MyChatModel extends AbstractChatModel {
3947
this.messageAdded(message);
4048
this.input.clearAttachments();
4149
}
50+
51+
createChatContext(): IChatContext {
52+
return new ChatContext({ model: this });
53+
}
4254
}
4355

4456
/*
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

packages/jupyter-chat/src/__tests__/model.spec.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,26 @@
77
* Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
88
*/
99

10-
import { AbstractChatModel, IChatModel } from '../model';
10+
import { AbstractChatModel, IChatContext, IChatModel } from '../model';
1111
import { IChatMessage, INewMessage } from '../types';
12-
13-
class MyChatModel extends AbstractChatModel {
14-
sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void {
15-
// No-op
16-
}
17-
}
12+
import { MockChatModel, MockChatContext } from './mocks';
1813

1914
describe('test chat model', () => {
2015
describe('model instantiation', () => {
2116
it('should create an AbstractChatModel', () => {
22-
const model = new MyChatModel();
17+
const model = new MockChatModel();
2318
expect(model).toBeInstanceOf(AbstractChatModel);
2419
});
2520

2621
it('should dispose an AbstractChatModel', () => {
27-
const model = new MyChatModel();
22+
const model = new MockChatModel();
2823
model.dispose();
2924
expect(model.isDisposed).toBeTruthy();
3025
});
3126
});
3227

3328
describe('incoming message', () => {
34-
class TestChat extends AbstractChatModel {
29+
class TestChat extends AbstractChatModel implements IChatModel {
3530
protected formatChatMessage(message: IChatMessage): IChatMessage {
3631
message.body = 'formatted msg';
3732
return message;
@@ -41,6 +36,10 @@ describe('test chat model', () => {
4136
): Promise<boolean | void> | boolean | void {
4237
// No-op
4338
}
39+
40+
createChatContext(): IChatContext {
41+
return new MockChatContext({ model: this });
42+
}
4443
}
4544

4645
let model: IChatModel;
@@ -58,7 +57,7 @@ describe('test chat model', () => {
5857
});
5958

6059
it('should signal incoming message', () => {
61-
model = new MyChatModel();
60+
model = new MockChatModel();
6261
model.messagesUpdated.connect((sender: IChatModel) => {
6362
expect(sender).toBe(model);
6463
messages = model.messages;
@@ -83,12 +82,12 @@ describe('test chat model', () => {
8382

8483
describe('model config', () => {
8584
it('should have empty config', () => {
86-
const model = new MyChatModel();
85+
const model = new MockChatModel();
8786
expect(model.config.sendWithShiftEnter).toBeUndefined();
8887
});
8988

9089
it('should allow config', () => {
91-
const model = new MyChatModel({ config: { sendWithShiftEnter: true } });
90+
const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
9291
expect(model.config.sendWithShiftEnter).toBeTruthy();
9392
});
9493
});

packages/jupyter-chat/src/__tests__/widgets.spec.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,16 @@ import {
1111
IRenderMimeRegistry,
1212
RenderMimeRegistry
1313
} from '@jupyterlab/rendermime';
14-
import { AbstractChatModel, IChatModel } from '../model';
15-
import { INewMessage } from '../types';
14+
import { IChatModel } from '../model';
1615
import { ChatWidget } from '../widgets/chat-widget';
17-
18-
class MyChatModel extends AbstractChatModel {
19-
sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void {
20-
// No-op
21-
}
22-
}
16+
import { MockChatModel } from './mocks';
2317

2418
describe('test chat widget', () => {
2519
let model: IChatModel;
2620
let rmRegistry: IRenderMimeRegistry;
2721

2822
beforeEach(() => {
29-
model = new MyChatModel();
23+
model = new MockChatModel();
3024
rmRegistry = new RenderMimeRegistry();
3125
});
3226

packages/jupyter-chat/src/chat-commands/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type ChatCommand = {
2222
* If set, this will be rendered as the icon for the command in the chat
2323
* commands menu. Jupyter Chat will choose a default if this is unset.
2424
*/
25-
icon?: LabIcon | string;
25+
icon?: LabIcon | JSX.Element | string | null;
2626

2727
/**
2828
* If set, this will be rendered as the description for the command in the

packages/jupyter-chat/src/components/chat-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
useChatCommands
2323
} from './input';
2424
import { IInputModel, InputModel } from '../input-model';
25-
import { IAttachment } from '../types';
2625
import { IChatCommandRegistry } from '../chat-commands';
26+
import { IAttachment } from '../types';
2727

2828
const INPUT_BOX_CLASS = 'jp-chat-input-container';
2929
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
@@ -256,7 +256,7 @@ export namespace ChatInput {
256256
*/
257257
export interface IProps {
258258
/**
259-
* The chat model.
259+
* The input model.
260260
*/
261261
model: IInputModel;
262262
/**

packages/jupyter-chat/src/components/chat-messages.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { IChatCommandRegistry } from '../chat-commands';
2525
import { IInputModel, InputModel } from '../input-model';
2626
import { IChatModel } from '../model';
2727
import { IChatMessage, IUser } from '../types';
28+
import { replaceSpanToMention } from '../utils';
2829

2930
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
3031
const MESSAGE_CLASS = 'jp-chat-message';
@@ -375,19 +376,27 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
375376
// Create an input model only if the message is edited.
376377
useEffect(() => {
377378
if (edit && canEdit) {
378-
setInputModel(
379-
new InputModel({
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(),
380386
onSend: (input: string, model?: IInputModel) =>
381387
updateMessage(message.id, input, model),
382388
onCancel: () => cancelEdition(),
383-
value: message.body,
389+
value: body,
390+
activeCellManager: model.activeCellManager,
391+
selectionWatcher: model.selectionWatcher,
392+
documentManager: model.documentManager,
384393
config: {
385394
sendWithShiftEnter: model.config.sendWithShiftEnter
386395
},
387396
attachments: message.attachments,
388-
documentManager: model.documentManager
389-
})
390-
);
397+
mentions: message.mentions
398+
});
399+
});
391400
} else {
392401
setInputModel(null);
393402
}
@@ -411,6 +420,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
411420
const updatedMessage = { ...message };
412421
updatedMessage.body = input;
413422
updatedMessage.attachments = inputModel.attachments;
423+
updatedMessage.mentions = inputModel.mentions;
414424
model.updateMessage!(id, updatedMessage);
415425
setEdit(false);
416426
};

packages/jupyter-chat/src/components/input/use-chat-commands.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
* Distributed under the terms of the Modified BSD License.
44
*/
55

6-
import React from 'react';
7-
import { useEffect, useState } from 'react';
6+
import { LabIcon } from '@jupyterlab/ui-components';
87
import type {
98
AutocompleteChangeReason,
109
AutocompleteProps as GenericAutocompleteProps
1110
} from '@mui/material';
1211
import { Box } from '@mui/material';
12+
import React, { useEffect, useState } from 'react';
1313

1414
import { ChatCommand, IChatCommandRegistry } from '../../chat-commands';
1515
import { IInputModel } from '../../input-model';
@@ -131,9 +131,11 @@ export function useChatCommands(
131131
___: unknown
132132
) => {
133133
const { key, ...listItemProps } = defaultProps;
134-
const commandIcon: JSX.Element = (
134+
const commandIcon: JSX.Element = React.isValidElement(command.icon) ? (
135+
command.icon
136+
) : (
135137
<span>
136-
{typeof command.icon === 'object' ? (
138+
{command.icon instanceof LabIcon ? (
137139
<command.icon.react />
138140
) : (
139141
command.icon

packages/jupyter-chat/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
export * from './active-cell-manager';
7+
export * from './chat-commands';
78
export * from './components';
89
export * from './icons';
910
export * from './input-model';
@@ -14,4 +15,3 @@ export * from './types';
1415
export * from './widgets/chat-error';
1516
export * from './widgets/chat-sidebar';
1617
export * from './widgets/chat-widget';
17-
export * from './chat-commands';

0 commit comments

Comments
 (0)