@jupyter/chat 0.11.0 → 0.13.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/components/chat-messages.d.ts +4 -0
- package/lib/components/chat-messages.js +37 -32
- package/lib/components/chat.d.ts +4 -0
- package/lib/components/chat.js +1 -1
- package/lib/components/index.d.ts +0 -1
- package/lib/components/index.js +0 -1
- package/lib/components/{markdown-renderer.d.ts → messages/message-renderer.d.ts} +10 -4
- package/lib/components/{markdown-renderer.js → messages/message-renderer.js} +11 -34
- package/lib/components/messages/welcome.d.ts +8 -0
- package/lib/components/messages/welcome.js +41 -0
- package/lib/components/scroll-container.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/input-model.d.ts +9 -0
- package/lib/input-model.js +8 -0
- package/lib/markdown-renderer.d.ts +38 -0
- package/lib/markdown-renderer.js +54 -0
- package/lib/model.d.ts +66 -5
- package/lib/model.js +44 -1
- package/package.json +2 -1
- package/src/components/chat-messages.tsx +50 -35
- package/src/components/chat.tsx +5 -0
- package/src/components/index.ts +0 -1
- package/src/components/{markdown-renderer.tsx → messages/message-renderer.tsx} +16 -42
- package/src/components/messages/welcome.tsx +52 -0
- package/src/components/scroll-container.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/input-model.ts +14 -0
- package/src/markdown-renderer.ts +78 -0
- package/src/model.ts +108 -7
- package/style/chat.css +4 -0
package/lib/model.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { ArrayExt } from '@lumino/algorithm';
|
|
5
6
|
import { Signal } from '@lumino/signaling';
|
|
6
7
|
import { InputModel } from './input-model';
|
|
7
8
|
import { replaceMentionToSpan } from './utils';
|
|
@@ -23,11 +24,14 @@ export class AbstractChatModel {
|
|
|
23
24
|
this._name = '';
|
|
24
25
|
this._isDisposed = false;
|
|
25
26
|
this._notificationId = null;
|
|
27
|
+
this._writers = [];
|
|
28
|
+
this._messageEditions = new Map();
|
|
26
29
|
this._messagesUpdated = new Signal(this);
|
|
27
30
|
this._configChanged = new Signal(this);
|
|
28
31
|
this._unreadChanged = new Signal(this);
|
|
29
32
|
this._viewportChanged = new Signal(this);
|
|
30
33
|
this._writersChanged = new Signal(this);
|
|
34
|
+
this._messageEditionAdded = new Signal(this);
|
|
31
35
|
if (options.id) {
|
|
32
36
|
this.id = options.id;
|
|
33
37
|
}
|
|
@@ -83,6 +87,12 @@ export class AbstractChatModel {
|
|
|
83
87
|
get input() {
|
|
84
88
|
return this._inputModel;
|
|
85
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The current writer list.
|
|
92
|
+
*/
|
|
93
|
+
get writers() {
|
|
94
|
+
return this._writers;
|
|
95
|
+
}
|
|
86
96
|
/**
|
|
87
97
|
* Get the active cell manager.
|
|
88
98
|
*/
|
|
@@ -223,6 +233,12 @@ export class AbstractChatModel {
|
|
|
223
233
|
get writersChanged() {
|
|
224
234
|
return this._writersChanged;
|
|
225
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* A signal emitting when the message edition input changed change.
|
|
238
|
+
*/
|
|
239
|
+
get messageEditionAdded() {
|
|
240
|
+
return this._messageEditionAdded;
|
|
241
|
+
}
|
|
226
242
|
/**
|
|
227
243
|
* Clear the message list.
|
|
228
244
|
*/
|
|
@@ -327,7 +343,34 @@ export class AbstractChatModel {
|
|
|
327
343
|
* This implementation only propagate the list via a signal.
|
|
328
344
|
*/
|
|
329
345
|
updateWriters(writers) {
|
|
330
|
-
|
|
346
|
+
const compareWriters = (a, b) => {
|
|
347
|
+
return (a.user.username === b.user.username &&
|
|
348
|
+
a.user.display_name === b.user.display_name &&
|
|
349
|
+
a.messageID === b.messageID);
|
|
350
|
+
};
|
|
351
|
+
if (!ArrayExt.shallowEqual(this._writers, writers, compareWriters)) {
|
|
352
|
+
this._writers = writers;
|
|
353
|
+
this._writersChanged.emit(writers);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get the input model of the edited message, given its id.
|
|
358
|
+
*/
|
|
359
|
+
getEditionModel(messageID) {
|
|
360
|
+
return this._messageEditions.get(messageID);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Add an input model of the edited message.
|
|
364
|
+
*/
|
|
365
|
+
addEditionModel(messageID, inputModel) {
|
|
366
|
+
var _a;
|
|
367
|
+
// Dispose of an hypothetic previous model for this message.
|
|
368
|
+
(_a = this.getEditionModel(messageID)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
369
|
+
this._messageEditions.set(messageID, inputModel);
|
|
370
|
+
this._messageEditionAdded.emit({ id: messageID, model: inputModel });
|
|
371
|
+
inputModel.onDisposed.connect(() => {
|
|
372
|
+
this._messageEditions.delete(messageID);
|
|
373
|
+
});
|
|
331
374
|
}
|
|
332
375
|
/**
|
|
333
376
|
* Add unread messages to the list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@jupyterlab/notebook": "^4.2.0",
|
|
55
55
|
"@jupyterlab/rendermime": "^4.2.0",
|
|
56
56
|
"@jupyterlab/ui-components": "^4.2.0",
|
|
57
|
+
"@lumino/algorithm": "^2.0.0",
|
|
57
58
|
"@lumino/commands": "^2.0.0",
|
|
58
59
|
"@lumino/coreutils": "^2.0.0",
|
|
59
60
|
"@lumino/disposable": "^2.0.0",
|
|
@@ -19,8 +19,9 @@ import React, { useEffect, useState, useRef, forwardRef } from 'react';
|
|
|
19
19
|
import { AttachmentPreviewList } from './attachments';
|
|
20
20
|
import { ChatInput } from './chat-input';
|
|
21
21
|
import { IInputToolbarRegistry } from './input';
|
|
22
|
-
import { MarkdownRenderer } from './markdown-renderer';
|
|
23
22
|
import { MessageFooter } from './messages/footer';
|
|
23
|
+
import { MessageRenderer } from './messages/message-renderer';
|
|
24
|
+
import { WelcomeMessage } from './messages/welcome';
|
|
24
25
|
import { ScrollContainer } from './scroll-container';
|
|
25
26
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
26
27
|
import { IMessageFooterRegistry } from '../footers';
|
|
@@ -64,6 +65,10 @@ type BaseMessageProps = {
|
|
|
64
65
|
* The footer registry.
|
|
65
66
|
*/
|
|
66
67
|
messageFooterRegistry?: IMessageFooterRegistry;
|
|
68
|
+
/**
|
|
69
|
+
* The welcome message.
|
|
70
|
+
*/
|
|
71
|
+
welcomeMessage?: string;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
/**
|
|
@@ -106,8 +111,8 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
106
111
|
setMessages([...model.messages]);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
function handleWritersChange(_: IChatModel, writers:
|
|
110
|
-
setCurrentWriters(writers);
|
|
114
|
+
function handleWritersChange(_: IChatModel, writers: IChatModel.IWriter[]) {
|
|
115
|
+
setCurrentWriters(writers.map(writer => writer.user));
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
model.messagesUpdated.connect(handleChatEvents);
|
|
@@ -184,6 +189,12 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
184
189
|
return (
|
|
185
190
|
<>
|
|
186
191
|
<ScrollContainer sx={{ flexGrow: 1 }}>
|
|
192
|
+
{props.welcomeMessage && (
|
|
193
|
+
<WelcomeMessage
|
|
194
|
+
rmRegistry={props.rmRegistry}
|
|
195
|
+
content={props.welcomeMessage}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
187
198
|
<Box ref={refMsgBox} className={clsx(MESSAGES_BOX_CLASS)}>
|
|
188
199
|
{messages.map((message, i) => {
|
|
189
200
|
renderedPromise.current[i] = new PromiseDelegate();
|
|
@@ -370,10 +381,10 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
370
381
|
const [deleted, setDeleted] = useState<boolean>(false);
|
|
371
382
|
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
372
383
|
const [canDelete, setCanDelete] = useState<boolean>(false);
|
|
373
|
-
const [inputModel, setInputModel] = useState<IInputModel | null>(null);
|
|
374
384
|
|
|
375
385
|
// Look if the message can be deleted or edited.
|
|
376
386
|
useEffect(() => {
|
|
387
|
+
// Init canDelete and canEdit state.
|
|
377
388
|
setDeleted(message.deleted ?? false);
|
|
378
389
|
if (model.user !== undefined && !message.deleted) {
|
|
379
390
|
if (model.user.username === message.sender.username) {
|
|
@@ -391,36 +402,36 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
391
402
|
}, [model, message]);
|
|
392
403
|
|
|
393
404
|
// Create an input model only if the message is edited.
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
let body = message.body;
|
|
398
|
-
message.mentions?.forEach(user => {
|
|
399
|
-
body = replaceSpanToMention(body, user);
|
|
400
|
-
});
|
|
401
|
-
return new InputModel({
|
|
402
|
-
chatContext: model.createChatContext(),
|
|
403
|
-
onSend: (input: string, model?: IInputModel) =>
|
|
404
|
-
updateMessage(message.id, input, model),
|
|
405
|
-
onCancel: () => cancelEdition(),
|
|
406
|
-
value: body,
|
|
407
|
-
activeCellManager: model.activeCellManager,
|
|
408
|
-
selectionWatcher: model.selectionWatcher,
|
|
409
|
-
documentManager: model.documentManager,
|
|
410
|
-
config: {
|
|
411
|
-
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
412
|
-
},
|
|
413
|
-
attachments: message.attachments,
|
|
414
|
-
mentions: message.mentions
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
} else {
|
|
418
|
-
setInputModel(null);
|
|
405
|
+
const startEdition = (): void => {
|
|
406
|
+
if (!canEdit) {
|
|
407
|
+
return;
|
|
419
408
|
}
|
|
420
|
-
|
|
409
|
+
let body = message.body;
|
|
410
|
+
message.mentions?.forEach(user => {
|
|
411
|
+
body = replaceSpanToMention(body, user);
|
|
412
|
+
});
|
|
413
|
+
const inputModel = new InputModel({
|
|
414
|
+
chatContext: model.createChatContext(),
|
|
415
|
+
onSend: (input: string, model?: IInputModel) =>
|
|
416
|
+
updateMessage(message.id, input, model),
|
|
417
|
+
onCancel: () => cancelEdition(),
|
|
418
|
+
value: body,
|
|
419
|
+
activeCellManager: model.activeCellManager,
|
|
420
|
+
selectionWatcher: model.selectionWatcher,
|
|
421
|
+
documentManager: model.documentManager,
|
|
422
|
+
config: {
|
|
423
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
424
|
+
},
|
|
425
|
+
attachments: message.attachments,
|
|
426
|
+
mentions: message.mentions
|
|
427
|
+
});
|
|
428
|
+
model.addEditionModel(message.id, inputModel);
|
|
429
|
+
setEdit(true);
|
|
430
|
+
};
|
|
421
431
|
|
|
422
432
|
// Cancel the current edition of the message.
|
|
423
433
|
const cancelEdition = (): void => {
|
|
434
|
+
model.getEditionModel(message.id)?.dispose();
|
|
424
435
|
setEdit(false);
|
|
425
436
|
};
|
|
426
437
|
|
|
@@ -439,6 +450,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
439
450
|
updatedMessage.attachments = inputModel.attachments;
|
|
440
451
|
updatedMessage.mentions = inputModel.mentions;
|
|
441
452
|
model.updateMessage!(id, updatedMessage);
|
|
453
|
+
model.getEditionModel(message.id)?.dispose();
|
|
442
454
|
setEdit(false);
|
|
443
455
|
};
|
|
444
456
|
|
|
@@ -455,19 +467,19 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
455
467
|
<div ref={ref} data-index={props.index}></div>
|
|
456
468
|
) : (
|
|
457
469
|
<div ref={ref} data-index={props.index}>
|
|
458
|
-
{edit && canEdit &&
|
|
470
|
+
{edit && canEdit && model.getEditionModel(message.id) ? (
|
|
459
471
|
<ChatInput
|
|
460
472
|
onCancel={() => cancelEdition()}
|
|
461
|
-
model={
|
|
473
|
+
model={model.getEditionModel(message.id)!}
|
|
462
474
|
chatCommandRegistry={props.chatCommandRegistry}
|
|
463
475
|
toolbarRegistry={props.inputToolbarRegistry}
|
|
464
476
|
/>
|
|
465
477
|
) : (
|
|
466
|
-
<
|
|
478
|
+
<MessageRenderer
|
|
467
479
|
rmRegistry={rmRegistry}
|
|
468
480
|
markdownStr={message.body}
|
|
469
481
|
model={model}
|
|
470
|
-
edit={canEdit ?
|
|
482
|
+
edit={canEdit ? startEdition : undefined}
|
|
471
483
|
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
472
484
|
rendered={props.renderedPromise}
|
|
473
485
|
/>
|
|
@@ -613,7 +625,10 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
613
625
|
// in viewport.
|
|
614
626
|
useEffect(() => {
|
|
615
627
|
const viewportChanged = (model: IChatModel, viewport: number[]) => {
|
|
616
|
-
setLastInViewport(
|
|
628
|
+
setLastInViewport(
|
|
629
|
+
model.messages.length === 0 ||
|
|
630
|
+
viewport.includes(model.messages.length - 1)
|
|
631
|
+
);
|
|
617
632
|
};
|
|
618
633
|
|
|
619
634
|
model.viewportChanged?.connect(viewportChanged);
|
package/src/components/chat.tsx
CHANGED
|
@@ -36,6 +36,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
|
|
|
36
36
|
chatCommandRegistry={props.chatCommandRegistry}
|
|
37
37
|
inputToolbarRegistry={inputToolbarRegistry}
|
|
38
38
|
messageFooterRegistry={props.messageFooterRegistry}
|
|
39
|
+
welcomeMessage={props.welcomeMessage}
|
|
39
40
|
/>
|
|
40
41
|
<ChatInput
|
|
41
42
|
sx={{
|
|
@@ -131,6 +132,10 @@ export namespace Chat {
|
|
|
131
132
|
* The footer registry.
|
|
132
133
|
*/
|
|
133
134
|
messageFooterRegistry?: IMessageFooterRegistry;
|
|
135
|
+
/**
|
|
136
|
+
* The welcome message.
|
|
137
|
+
*/
|
|
138
|
+
welcomeMessage?: string;
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/**
|
package/src/components/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ export * from './chat-messages';
|
|
|
9
9
|
export * from './code-blocks';
|
|
10
10
|
export * from './input';
|
|
11
11
|
export * from './jl-theme-provider';
|
|
12
|
-
export * from './markdown-renderer';
|
|
13
12
|
export * from './mui-extras';
|
|
14
13
|
export * from './scroll-container';
|
|
15
14
|
export * from './toolbar';
|
|
@@ -8,14 +8,15 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
8
8
|
import React, { useState, useEffect } from 'react';
|
|
9
9
|
import { createPortal } from 'react-dom';
|
|
10
10
|
|
|
11
|
-
import { CodeToolbar, CodeToolbarProps } from '
|
|
12
|
-
import { MessageToolbar } from '
|
|
13
|
-
import {
|
|
11
|
+
import { CodeToolbar, CodeToolbarProps } from '../code-blocks/code-toolbar';
|
|
12
|
+
import { MessageToolbar } from '../toolbar';
|
|
13
|
+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
14
|
+
import { IChatModel } from '../../model';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
16
|
+
/**
|
|
17
|
+
* The type of the props for the MessageRenderer component.
|
|
18
|
+
*/
|
|
19
|
+
type MessageRendererProps = {
|
|
19
20
|
/**
|
|
20
21
|
* The string to render.
|
|
21
22
|
*/
|
|
@@ -47,22 +48,10 @@ type MarkdownRendererProps = {
|
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
52
|
-
* returns `\\(`.
|
|
53
|
-
*
|
|
54
|
-
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
|
|
55
|
-
* `ILatexTypesetter`.
|
|
51
|
+
* The message renderer base component.
|
|
56
52
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
.replace(/\\\(/g, '\\\\(')
|
|
60
|
-
.replace(/\\\)/g, '\\\\)')
|
|
61
|
-
.replace(/\\\[/g, '\\\\[')
|
|
62
|
-
.replace(/\\\]/g, '\\\\]');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
|
|
53
|
+
function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
54
|
+
const { markdownStr, rmRegistry } = props;
|
|
66
55
|
const appendContent = props.appendContent || false;
|
|
67
56
|
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
|
|
68
57
|
null
|
|
@@ -75,26 +64,11 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
|
|
|
75
64
|
|
|
76
65
|
useEffect(() => {
|
|
77
66
|
const renderContent = async () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
data: { [MD_MIME_TYPE]: mdStr }
|
|
67
|
+
const renderer = await MarkdownRenderer.renderContent({
|
|
68
|
+
content: markdownStr,
|
|
69
|
+
rmRegistry
|
|
82
70
|
});
|
|
83
71
|
|
|
84
|
-
const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
85
|
-
|
|
86
|
-
// step 1: render markdown
|
|
87
|
-
await renderer.renderModel(model);
|
|
88
|
-
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
|
|
89
|
-
if (!renderer.node) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// step 2: render LaTeX via MathJax.
|
|
96
|
-
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
|
|
97
|
-
|
|
98
72
|
const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
|
|
99
73
|
|
|
100
74
|
// Attach CodeToolbar root element to each <pre> block
|
|
@@ -119,7 +93,7 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
|
|
|
119
93
|
};
|
|
120
94
|
|
|
121
95
|
renderContent();
|
|
122
|
-
}, [
|
|
96
|
+
}, [markdownStr, rmRegistry]);
|
|
123
97
|
|
|
124
98
|
return (
|
|
125
99
|
<div className={MD_RENDERED_CLASS}>
|
|
@@ -146,4 +120,4 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
|
|
|
146
120
|
);
|
|
147
121
|
}
|
|
148
122
|
|
|
149
|
-
export const
|
|
123
|
+
export const MessageRenderer = React.memo(MessageRendererBase);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { classes } from '@jupyterlab/ui-components';
|
|
7
|
+
import React, { useEffect, useRef } from 'react';
|
|
8
|
+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
9
|
+
|
|
10
|
+
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The welcome message component.
|
|
14
|
+
* This message is displayed on top of the chat messages, and is rendered using a
|
|
15
|
+
* markdown renderer.
|
|
16
|
+
*/
|
|
17
|
+
export function WelcomeMessage(props: MarkdownRenderer.IOptions): JSX.Element {
|
|
18
|
+
const { rmRegistry } = props;
|
|
19
|
+
const content = props.content + '\n----\n';
|
|
20
|
+
|
|
21
|
+
// ref that tracks the content container to store the rendermime node in
|
|
22
|
+
const renderingContainer = useRef<HTMLDivElement | null>(null);
|
|
23
|
+
// ref that tracks whether the rendermime node has already been inserted
|
|
24
|
+
const renderingInserted = useRef<boolean>(false);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
|
|
28
|
+
* and insert it into `renderingContainer` if not yet inserted.
|
|
29
|
+
*/
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const renderContent = async () => {
|
|
32
|
+
const renderer = await MarkdownRenderer.renderContent({
|
|
33
|
+
content,
|
|
34
|
+
rmRegistry
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// insert the rendering into renderingContainer if not yet inserted
|
|
38
|
+
if (renderingContainer.current !== null && !renderingInserted.current) {
|
|
39
|
+
renderingContainer.current.appendChild(renderer.node);
|
|
40
|
+
renderingInserted.current = true;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
renderContent();
|
|
45
|
+
}, [content]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS)}>
|
|
49
|
+
<div ref={renderingContainer} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -43,7 +43,7 @@ export function ScrollContainer(props: ScrollContainerProps): JSX.Element {
|
|
|
43
43
|
...props.sx
|
|
44
44
|
}}
|
|
45
45
|
>
|
|
46
|
-
<Box
|
|
46
|
+
<Box>{props.children}</Box>
|
|
47
47
|
<Box sx={{ overflowAnchor: 'auto', height: '1px' }} />
|
|
48
48
|
</Box>
|
|
49
49
|
);
|
package/src/index.ts
CHANGED
package/src/input-model.ts
CHANGED
|
@@ -147,6 +147,11 @@ export interface IInputModel extends IDisposable {
|
|
|
147
147
|
* Clear mentions list.
|
|
148
148
|
*/
|
|
149
149
|
clearMentions(): void;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A signal emitting when disposing of the model.
|
|
153
|
+
*/
|
|
154
|
+
readonly onDisposed: ISignal<InputModel, void>;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/**
|
|
@@ -411,9 +416,17 @@ export class InputModel implements IInputModel {
|
|
|
411
416
|
if (this.isDisposed) {
|
|
412
417
|
return;
|
|
413
418
|
}
|
|
419
|
+
this._onDisposed.emit();
|
|
414
420
|
this._isDisposed = true;
|
|
415
421
|
}
|
|
416
422
|
|
|
423
|
+
/**
|
|
424
|
+
* A signal emitting when disposing of the model.
|
|
425
|
+
*/
|
|
426
|
+
get onDisposed(): ISignal<InputModel, void> {
|
|
427
|
+
return this._onDisposed;
|
|
428
|
+
}
|
|
429
|
+
|
|
417
430
|
/**
|
|
418
431
|
* Whether the input model is disposed.
|
|
419
432
|
*/
|
|
@@ -438,6 +451,7 @@ export class InputModel implements IInputModel {
|
|
|
438
451
|
private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
|
|
439
452
|
private _focusInputSignal = new Signal<InputModel, void>(this);
|
|
440
453
|
private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
|
|
454
|
+
private _onDisposed = new Signal<InputModel, void>(this);
|
|
441
455
|
private _isDisposed = false;
|
|
442
456
|
}
|
|
443
457
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
|
|
8
|
+
export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
|
|
9
|
+
export const MD_MIME_TYPE = 'text/markdown';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A namespace for the MarkdownRenderer.
|
|
13
|
+
*/
|
|
14
|
+
export namespace MarkdownRenderer {
|
|
15
|
+
/**
|
|
16
|
+
* The options for the MarkdownRenderer.
|
|
17
|
+
*/
|
|
18
|
+
export interface IOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The rendermime registry.
|
|
21
|
+
*/
|
|
22
|
+
rmRegistry: IRenderMimeRegistry;
|
|
23
|
+
/**
|
|
24
|
+
* The markdown content.
|
|
25
|
+
*/
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A generic function to render a markdown string into a DOM element.
|
|
31
|
+
*
|
|
32
|
+
* @param content - the markdown content.
|
|
33
|
+
* @param rmRegistry - the rendermime registry.
|
|
34
|
+
* @returns a promise that resolves to the renderer.
|
|
35
|
+
*/
|
|
36
|
+
export async function renderContent(
|
|
37
|
+
options: IOptions
|
|
38
|
+
): Promise<IRenderMime.IRenderer> {
|
|
39
|
+
const { rmRegistry, content } = options;
|
|
40
|
+
|
|
41
|
+
// initialize mime model
|
|
42
|
+
const mdStr = escapeLatexDelimiters(content);
|
|
43
|
+
const model = rmRegistry.createModel({
|
|
44
|
+
data: { [MD_MIME_TYPE]: mdStr }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
48
|
+
|
|
49
|
+
// step 1: render markdown
|
|
50
|
+
await renderer.renderModel(model);
|
|
51
|
+
if (!renderer.node) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// step 2: render LaTeX via MathJax.
|
|
58
|
+
rmRegistry.latexTypesetter?.typeset(renderer.node);
|
|
59
|
+
|
|
60
|
+
return renderer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
|
|
65
|
+
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
66
|
+
* returns `\\(`.
|
|
67
|
+
*
|
|
68
|
+
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
|
|
69
|
+
* `ILatexTypesetter`.
|
|
70
|
+
*/
|
|
71
|
+
export function escapeLatexDelimiters(text: string) {
|
|
72
|
+
return text
|
|
73
|
+
.replace(/\\\(/g, '\\\\(')
|
|
74
|
+
.replace(/\\\)/g, '\\\\)')
|
|
75
|
+
.replace(/\\\[/g, '\\\\[')
|
|
76
|
+
.replace(/\\\]/g, '\\\\]');
|
|
77
|
+
}
|
|
78
|
+
}
|