@jupyter/chat 0.10.1 → 0.12.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 +9 -0
- package/lib/components/chat-messages.js +44 -33
- package/lib/components/chat.d.ts +9 -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/messages/footer.d.ts +16 -0
- package/lib/components/messages/footer.js +19 -0
- 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/footers/index.d.ts +2 -0
- package/lib/footers/index.js +6 -0
- package/lib/footers/registry.d.ts +36 -0
- package/lib/footers/registry.js +30 -0
- package/lib/footers/types.d.ts +26 -0
- package/lib/footers/types.js +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -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/lib/types.d.ts +4 -0
- package/package.json +2 -1
- package/src/components/chat-messages.tsx +67 -35
- package/src/components/chat.tsx +11 -0
- package/src/components/index.ts +0 -1
- package/src/components/messages/footer.tsx +50 -0
- 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/footers/index.ts +7 -0
- package/src/footers/registry.ts +52 -0
- package/src/footers/types.ts +33 -0
- package/src/index.ts +2 -0
- package/src/input-model.ts +14 -0
- package/src/markdown-renderer.ts +78 -0
- package/src/model.ts +108 -7
- package/src/types.ts +4 -0
- package/style/chat.css +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
|
|
6
|
+
export const MD_MIME_TYPE = 'text/markdown';
|
|
7
|
+
/**
|
|
8
|
+
* A namespace for the MarkdownRenderer.
|
|
9
|
+
*/
|
|
10
|
+
export var MarkdownRenderer;
|
|
11
|
+
(function (MarkdownRenderer) {
|
|
12
|
+
/**
|
|
13
|
+
* A generic function to render a markdown string into a DOM element.
|
|
14
|
+
*
|
|
15
|
+
* @param content - the markdown content.
|
|
16
|
+
* @param rmRegistry - the rendermime registry.
|
|
17
|
+
* @returns a promise that resolves to the renderer.
|
|
18
|
+
*/
|
|
19
|
+
async function renderContent(options) {
|
|
20
|
+
var _a;
|
|
21
|
+
const { rmRegistry, content } = options;
|
|
22
|
+
// initialize mime model
|
|
23
|
+
const mdStr = escapeLatexDelimiters(content);
|
|
24
|
+
const model = rmRegistry.createModel({
|
|
25
|
+
data: { [MD_MIME_TYPE]: mdStr }
|
|
26
|
+
});
|
|
27
|
+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
28
|
+
// step 1: render markdown
|
|
29
|
+
await renderer.renderModel(model);
|
|
30
|
+
if (!renderer.node) {
|
|
31
|
+
throw new Error('Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.');
|
|
32
|
+
}
|
|
33
|
+
// step 2: render LaTeX via MathJax.
|
|
34
|
+
(_a = rmRegistry.latexTypesetter) === null || _a === void 0 ? void 0 : _a.typeset(renderer.node);
|
|
35
|
+
return renderer;
|
|
36
|
+
}
|
|
37
|
+
MarkdownRenderer.renderContent = renderContent;
|
|
38
|
+
/**
|
|
39
|
+
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
|
|
40
|
+
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
41
|
+
* returns `\\(`.
|
|
42
|
+
*
|
|
43
|
+
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
|
|
44
|
+
* `ILatexTypesetter`.
|
|
45
|
+
*/
|
|
46
|
+
function escapeLatexDelimiters(text) {
|
|
47
|
+
return text
|
|
48
|
+
.replace(/\\\(/g, '\\\\(')
|
|
49
|
+
.replace(/\\\)/g, '\\\\)')
|
|
50
|
+
.replace(/\\\[/g, '\\\\[')
|
|
51
|
+
.replace(/\\\]/g, '\\\\]');
|
|
52
|
+
}
|
|
53
|
+
MarkdownRenderer.escapeLatexDelimiters = escapeLatexDelimiters;
|
|
54
|
+
})(MarkdownRenderer || (MarkdownRenderer = {}));
|
package/lib/model.d.ts
CHANGED
|
@@ -38,6 +38,10 @@ export interface IChatModel extends IDisposable {
|
|
|
38
38
|
* The input model.
|
|
39
39
|
*/
|
|
40
40
|
readonly input: IInputModel;
|
|
41
|
+
/**
|
|
42
|
+
* The current writer list.
|
|
43
|
+
*/
|
|
44
|
+
readonly writers: IChatModel.IWriter[];
|
|
41
45
|
/**
|
|
42
46
|
* Get the active cell manager.
|
|
43
47
|
*/
|
|
@@ -57,7 +61,7 @@ export interface IChatModel extends IDisposable {
|
|
|
57
61
|
/**
|
|
58
62
|
* A signal emitting when the messages list is updated.
|
|
59
63
|
*/
|
|
60
|
-
|
|
64
|
+
readonly configChanged: ISignal<IChatModel, IConfig>;
|
|
61
65
|
/**
|
|
62
66
|
* A signal emitting when unread messages change.
|
|
63
67
|
*/
|
|
@@ -69,7 +73,11 @@ export interface IChatModel extends IDisposable {
|
|
|
69
73
|
/**
|
|
70
74
|
* A signal emitting when the writers change.
|
|
71
75
|
*/
|
|
72
|
-
readonly writersChanged?: ISignal<IChatModel,
|
|
76
|
+
readonly writersChanged?: ISignal<IChatModel, IChatModel.IWriter[]>;
|
|
77
|
+
/**
|
|
78
|
+
* A signal emitting when the message edition input changed change.
|
|
79
|
+
*/
|
|
80
|
+
readonly messageEditionAdded: ISignal<IChatModel, IChatModel.IMessageEdition>;
|
|
73
81
|
/**
|
|
74
82
|
* Send a message, to be defined depending on the chosen technology.
|
|
75
83
|
* Default to no-op.
|
|
@@ -130,11 +138,19 @@ export interface IChatModel extends IDisposable {
|
|
|
130
138
|
/**
|
|
131
139
|
* Update the current writers list.
|
|
132
140
|
*/
|
|
133
|
-
updateWriters(writers:
|
|
141
|
+
updateWriters(writers: IChatModel.IWriter[]): void;
|
|
134
142
|
/**
|
|
135
143
|
* Create the chat context that will be passed to the input model.
|
|
136
144
|
*/
|
|
137
145
|
createChatContext(): IChatContext;
|
|
146
|
+
/**
|
|
147
|
+
* Get the input model of the edited message, given its id.
|
|
148
|
+
*/
|
|
149
|
+
getEditionModel(messageID: string): IInputModel | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Add an input model of the edited message.
|
|
152
|
+
*/
|
|
153
|
+
addEditionModel(messageID: string, inputModel: IInputModel): void;
|
|
138
154
|
}
|
|
139
155
|
/**
|
|
140
156
|
* An abstract implementation of IChatModel.
|
|
@@ -165,6 +181,10 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
165
181
|
* The input model.
|
|
166
182
|
*/
|
|
167
183
|
get input(): IInputModel;
|
|
184
|
+
/**
|
|
185
|
+
* The current writer list.
|
|
186
|
+
*/
|
|
187
|
+
get writers(): IChatModel.IWriter[];
|
|
168
188
|
/**
|
|
169
189
|
* Get the active cell manager.
|
|
170
190
|
*/
|
|
@@ -216,7 +236,11 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
216
236
|
/**
|
|
217
237
|
* A signal emitting when the writers change.
|
|
218
238
|
*/
|
|
219
|
-
get writersChanged(): ISignal<IChatModel,
|
|
239
|
+
get writersChanged(): ISignal<IChatModel, IChatModel.IWriter[]>;
|
|
240
|
+
/**
|
|
241
|
+
* A signal emitting when the message edition input changed change.
|
|
242
|
+
*/
|
|
243
|
+
get messageEditionAdded(): ISignal<IChatModel, IChatModel.IMessageEdition>;
|
|
220
244
|
/**
|
|
221
245
|
* Send a message, to be defined depending on the chosen technology.
|
|
222
246
|
* Default to no-op.
|
|
@@ -266,11 +290,19 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
266
290
|
* Update the current writers list.
|
|
267
291
|
* This implementation only propagate the list via a signal.
|
|
268
292
|
*/
|
|
269
|
-
updateWriters(writers:
|
|
293
|
+
updateWriters(writers: IChatModel.IWriter[]): void;
|
|
270
294
|
/**
|
|
271
295
|
* Create the chat context that will be passed to the input model.
|
|
272
296
|
*/
|
|
273
297
|
abstract createChatContext(): IChatContext;
|
|
298
|
+
/**
|
|
299
|
+
* Get the input model of the edited message, given its id.
|
|
300
|
+
*/
|
|
301
|
+
getEditionModel(messageID: string): IInputModel | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Add an input model of the edited message.
|
|
304
|
+
*/
|
|
305
|
+
addEditionModel(messageID: string, inputModel: IInputModel): void;
|
|
274
306
|
/**
|
|
275
307
|
* Add unread messages to the list.
|
|
276
308
|
* @param indexes - list of new indexes.
|
|
@@ -299,11 +331,14 @@ export declare abstract class AbstractChatModel implements IChatModel {
|
|
|
299
331
|
private _selectionWatcher;
|
|
300
332
|
private _documentManager;
|
|
301
333
|
private _notificationId;
|
|
334
|
+
private _writers;
|
|
335
|
+
private _messageEditions;
|
|
302
336
|
private _messagesUpdated;
|
|
303
337
|
private _configChanged;
|
|
304
338
|
private _unreadChanged;
|
|
305
339
|
private _viewportChanged;
|
|
306
340
|
private _writersChanged;
|
|
341
|
+
private _messageEditionAdded;
|
|
307
342
|
}
|
|
308
343
|
/**
|
|
309
344
|
* The chat model namespace.
|
|
@@ -338,6 +373,32 @@ export declare namespace IChatModel {
|
|
|
338
373
|
*/
|
|
339
374
|
documentManager?: IDocumentManager | null;
|
|
340
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Representation of a message edition.
|
|
378
|
+
*/
|
|
379
|
+
interface IMessageEdition {
|
|
380
|
+
/**
|
|
381
|
+
* The id of the edited message.
|
|
382
|
+
*/
|
|
383
|
+
id: string;
|
|
384
|
+
/**
|
|
385
|
+
* The model of the input editing the message.
|
|
386
|
+
*/
|
|
387
|
+
model: IInputModel;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Writer interface, including the message ID if the writer is editing a message.
|
|
391
|
+
*/
|
|
392
|
+
interface IWriter {
|
|
393
|
+
/**
|
|
394
|
+
* The user currently writing.
|
|
395
|
+
*/
|
|
396
|
+
user: IUser;
|
|
397
|
+
/**
|
|
398
|
+
* The message ID (optional)
|
|
399
|
+
*/
|
|
400
|
+
messageID?: string;
|
|
401
|
+
}
|
|
341
402
|
}
|
|
342
403
|
/**
|
|
343
404
|
* Interface of the chat context, a 'subset' of the model with readonly attribute,
|
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/lib/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,9 +19,12 @@ 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 {
|
|
22
|
+
import { MessageFooter } from './messages/footer';
|
|
23
|
+
import { MessageRenderer } from './messages/message-renderer';
|
|
24
|
+
import { WelcomeMessage } from './messages/welcome';
|
|
23
25
|
import { ScrollContainer } from './scroll-container';
|
|
24
26
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
27
|
+
import { IMessageFooterRegistry } from '../footers';
|
|
25
28
|
import { IInputModel, InputModel } from '../input-model';
|
|
26
29
|
import { IChatModel } from '../model';
|
|
27
30
|
import { IChatMessage, IUser } from '../types';
|
|
@@ -58,6 +61,14 @@ type BaseMessageProps = {
|
|
|
58
61
|
* The input toolbar registry.
|
|
59
62
|
*/
|
|
60
63
|
inputToolbarRegistry: IInputToolbarRegistry;
|
|
64
|
+
/**
|
|
65
|
+
* The footer registry.
|
|
66
|
+
*/
|
|
67
|
+
messageFooterRegistry?: IMessageFooterRegistry;
|
|
68
|
+
/**
|
|
69
|
+
* The welcome message.
|
|
70
|
+
*/
|
|
71
|
+
welcomeMessage?: string;
|
|
61
72
|
};
|
|
62
73
|
|
|
63
74
|
/**
|
|
@@ -100,8 +111,8 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
100
111
|
setMessages([...model.messages]);
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
function handleWritersChange(_: IChatModel, writers:
|
|
104
|
-
setCurrentWriters(writers);
|
|
114
|
+
function handleWritersChange(_: IChatModel, writers: IChatModel.IWriter[]) {
|
|
115
|
+
setCurrentWriters(writers.map(writer => writer.user));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
model.messagesUpdated.connect(handleChatEvents);
|
|
@@ -178,6 +189,12 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
178
189
|
return (
|
|
179
190
|
<>
|
|
180
191
|
<ScrollContainer sx={{ flexGrow: 1 }}>
|
|
192
|
+
{props.welcomeMessage && (
|
|
193
|
+
<WelcomeMessage
|
|
194
|
+
rmRegistry={props.rmRegistry}
|
|
195
|
+
content={props.welcomeMessage}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
181
198
|
<Box ref={refMsgBox} className={clsx(MESSAGES_BOX_CLASS)}>
|
|
182
199
|
{messages.map((message, i) => {
|
|
183
200
|
renderedPromise.current[i] = new PromiseDelegate();
|
|
@@ -198,6 +215,13 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
198
215
|
renderedPromise={renderedPromise.current[i]}
|
|
199
216
|
ref={el => (listRef.current[i] = el)}
|
|
200
217
|
/>
|
|
218
|
+
{props.messageFooterRegistry && (
|
|
219
|
+
<MessageFooter
|
|
220
|
+
registry={props.messageFooterRegistry}
|
|
221
|
+
message={message}
|
|
222
|
+
model={model}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
201
225
|
</Box>
|
|
202
226
|
);
|
|
203
227
|
})}
|
|
@@ -357,15 +381,19 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
357
381
|
const [deleted, setDeleted] = useState<boolean>(false);
|
|
358
382
|
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
359
383
|
const [canDelete, setCanDelete] = useState<boolean>(false);
|
|
360
|
-
const [inputModel, setInputModel] = useState<IInputModel | null>(null);
|
|
361
384
|
|
|
362
385
|
// Look if the message can be deleted or edited.
|
|
363
386
|
useEffect(() => {
|
|
387
|
+
// Init canDelete and canEdit state.
|
|
364
388
|
setDeleted(message.deleted ?? false);
|
|
365
389
|
if (model.user !== undefined && !message.deleted) {
|
|
366
390
|
if (model.user.username === message.sender.username) {
|
|
367
391
|
setCanEdit(model.updateMessage !== undefined);
|
|
368
392
|
setCanDelete(model.deleteMessage !== undefined);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (message.sender.bot) {
|
|
396
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
369
397
|
}
|
|
370
398
|
} else {
|
|
371
399
|
setCanEdit(false);
|
|
@@ -374,36 +402,36 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
374
402
|
}, [model, message]);
|
|
375
403
|
|
|
376
404
|
// Create an input model only if the message is edited.
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
let body = message.body;
|
|
381
|
-
message.mentions?.forEach(user => {
|
|
382
|
-
body = replaceSpanToMention(body, user);
|
|
383
|
-
});
|
|
384
|
-
return new InputModel({
|
|
385
|
-
chatContext: model.createChatContext(),
|
|
386
|
-
onSend: (input: string, model?: IInputModel) =>
|
|
387
|
-
updateMessage(message.id, input, model),
|
|
388
|
-
onCancel: () => cancelEdition(),
|
|
389
|
-
value: body,
|
|
390
|
-
activeCellManager: model.activeCellManager,
|
|
391
|
-
selectionWatcher: model.selectionWatcher,
|
|
392
|
-
documentManager: model.documentManager,
|
|
393
|
-
config: {
|
|
394
|
-
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
395
|
-
},
|
|
396
|
-
attachments: message.attachments,
|
|
397
|
-
mentions: message.mentions
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
} else {
|
|
401
|
-
setInputModel(null);
|
|
405
|
+
const startEdition = (): void => {
|
|
406
|
+
if (!canEdit) {
|
|
407
|
+
return;
|
|
402
408
|
}
|
|
403
|
-
|
|
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
|
+
};
|
|
404
431
|
|
|
405
432
|
// Cancel the current edition of the message.
|
|
406
433
|
const cancelEdition = (): void => {
|
|
434
|
+
model.getEditionModel(message.id)?.dispose();
|
|
407
435
|
setEdit(false);
|
|
408
436
|
};
|
|
409
437
|
|
|
@@ -422,6 +450,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
422
450
|
updatedMessage.attachments = inputModel.attachments;
|
|
423
451
|
updatedMessage.mentions = inputModel.mentions;
|
|
424
452
|
model.updateMessage!(id, updatedMessage);
|
|
453
|
+
model.getEditionModel(message.id)?.dispose();
|
|
425
454
|
setEdit(false);
|
|
426
455
|
};
|
|
427
456
|
|
|
@@ -438,19 +467,19 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
438
467
|
<div ref={ref} data-index={props.index}></div>
|
|
439
468
|
) : (
|
|
440
469
|
<div ref={ref} data-index={props.index}>
|
|
441
|
-
{edit && canEdit &&
|
|
470
|
+
{edit && canEdit && model.getEditionModel(message.id) ? (
|
|
442
471
|
<ChatInput
|
|
443
472
|
onCancel={() => cancelEdition()}
|
|
444
|
-
model={
|
|
473
|
+
model={model.getEditionModel(message.id)!}
|
|
445
474
|
chatCommandRegistry={props.chatCommandRegistry}
|
|
446
475
|
toolbarRegistry={props.inputToolbarRegistry}
|
|
447
476
|
/>
|
|
448
477
|
) : (
|
|
449
|
-
<
|
|
478
|
+
<MessageRenderer
|
|
450
479
|
rmRegistry={rmRegistry}
|
|
451
480
|
markdownStr={message.body}
|
|
452
481
|
model={model}
|
|
453
|
-
edit={canEdit ?
|
|
482
|
+
edit={canEdit ? startEdition : undefined}
|
|
454
483
|
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
455
484
|
rendered={props.renderedPromise}
|
|
456
485
|
/>
|
|
@@ -596,7 +625,10 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
596
625
|
// in viewport.
|
|
597
626
|
useEffect(() => {
|
|
598
627
|
const viewportChanged = (model: IChatModel, viewport: number[]) => {
|
|
599
|
-
setLastInViewport(
|
|
628
|
+
setLastInViewport(
|
|
629
|
+
model.messages.length === 0 ||
|
|
630
|
+
viewport.includes(model.messages.length - 1)
|
|
631
|
+
);
|
|
600
632
|
};
|
|
601
633
|
|
|
602
634
|
model.viewportChanged?.connect(viewportChanged);
|
package/src/components/chat.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { ChatMessages } from './chat-messages';
|
|
|
17
17
|
import { ChatInput } from './chat-input';
|
|
18
18
|
import { IInputToolbarRegistry, InputToolbarRegistry } from './input';
|
|
19
19
|
import { AttachmentOpenerContext } from '../context';
|
|
20
|
+
import { IMessageFooterRegistry } from '../footers';
|
|
20
21
|
import { IChatModel } from '../model';
|
|
21
22
|
import { IAttachmentOpenerRegistry } from '../registry';
|
|
22
23
|
|
|
@@ -34,6 +35,8 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
|
|
|
34
35
|
model={model}
|
|
35
36
|
chatCommandRegistry={props.chatCommandRegistry}
|
|
36
37
|
inputToolbarRegistry={inputToolbarRegistry}
|
|
38
|
+
messageFooterRegistry={props.messageFooterRegistry}
|
|
39
|
+
welcomeMessage={props.welcomeMessage}
|
|
37
40
|
/>
|
|
38
41
|
<ChatInput
|
|
39
42
|
sx={{
|
|
@@ -125,6 +128,14 @@ export namespace Chat {
|
|
|
125
128
|
* The input toolbar registry
|
|
126
129
|
*/
|
|
127
130
|
inputToolbarRegistry?: IInputToolbarRegistry;
|
|
131
|
+
/**
|
|
132
|
+
* The footer registry.
|
|
133
|
+
*/
|
|
134
|
+
messageFooterRegistry?: IMessageFooterRegistry;
|
|
135
|
+
/**
|
|
136
|
+
* The welcome message.
|
|
137
|
+
*/
|
|
138
|
+
welcomeMessage?: string;
|
|
128
139
|
}
|
|
129
140
|
|
|
130
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';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Box } from '@mui/material';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {
|
|
9
|
+
IMessageFooterRegistry,
|
|
10
|
+
MessageFooterSectionProps
|
|
11
|
+
} from '../../footers';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The chat footer component properties.
|
|
15
|
+
*/
|
|
16
|
+
export interface IMessageFootersProps extends MessageFooterSectionProps {
|
|
17
|
+
/**
|
|
18
|
+
* The chat footer registry.
|
|
19
|
+
*/
|
|
20
|
+
registry: IMessageFooterRegistry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The chat footer component, which displays footer components on a row according to
|
|
25
|
+
* their respective positions.
|
|
26
|
+
*/
|
|
27
|
+
export function MessageFooter(props: IMessageFootersProps): JSX.Element {
|
|
28
|
+
const { message, model, registry } = props;
|
|
29
|
+
const footer = registry.getFooter();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
33
|
+
{footer.left?.component ? (
|
|
34
|
+
<footer.left.component message={message} model={model} />
|
|
35
|
+
) : (
|
|
36
|
+
<div />
|
|
37
|
+
)}
|
|
38
|
+
{footer.center?.component ? (
|
|
39
|
+
<footer.center.component message={message} model={model} />
|
|
40
|
+
) : (
|
|
41
|
+
<div />
|
|
42
|
+
)}
|
|
43
|
+
{footer.right?.component ? (
|
|
44
|
+
<footer.right.component message={message} model={model} />
|
|
45
|
+
) : (
|
|
46
|
+
<div />
|
|
47
|
+
)}
|
|
48
|
+
</Box>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -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);
|