@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.9.0",
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
@@ -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 chat model.
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
- 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(),
380
386
  onSend: (input: string, model?: IInputModel) =>
381
387
  updateMessage(message.id, input, model),
382
388
  onCancel: () => cancelEdition(),
383
- value: message.body,
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
- documentManager: model.documentManager
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 React from 'react';
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
- {typeof command.icon === 'object' ? (
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';
@@ -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 { IAttachment } from './types';
11
- import { IDocumentManager } from '@jupyterlab/docmanager';
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 { IActiveCellManager } from './active-cell-manager';
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
- * The default chat model implementation.
179
- * It is not able to send or update a message by itself, since it depends on the
180
- * chosen technology.
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 ChatModel implements IChatModel {
189
+ export abstract class AbstractChatModel implements IChatModel {
183
190
  /**
184
191
  * Create a new chat model.
185
192
  */
186
- constructor(options: ChatModel.IOptions = {}) {
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(message: INewMessage): Promise<boolean | void> | boolean | void {}
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 ChatModel {
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
+ }
package/style/chat.css CHANGED
@@ -140,3 +140,9 @@
140
140
  color: gray;
141
141
  margin: 5px;
142
142
  }
143
+
144
+ .jp-chat-mention {
145
+ border-radius: 10px;
146
+ padding: 0 0.2em;
147
+ background-color: var(--jp-brand-color4);
148
+ }