@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
|
@@ -3,6 +3,7 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { IInputToolbarRegistry } from './input';
|
|
5
5
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
6
|
+
import { IMessageFooterRegistry } from '../footers';
|
|
6
7
|
import { IChatModel } from '../model';
|
|
7
8
|
import { IChatMessage, IUser } from '../types';
|
|
8
9
|
/**
|
|
@@ -25,6 +26,14 @@ type BaseMessageProps = {
|
|
|
25
26
|
* The input toolbar registry.
|
|
26
27
|
*/
|
|
27
28
|
inputToolbarRegistry: IInputToolbarRegistry;
|
|
29
|
+
/**
|
|
30
|
+
* The footer registry.
|
|
31
|
+
*/
|
|
32
|
+
messageFooterRegistry?: IMessageFooterRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* The welcome message.
|
|
35
|
+
*/
|
|
36
|
+
welcomeMessage?: string;
|
|
28
37
|
};
|
|
29
38
|
/**
|
|
30
39
|
* The messages list component.
|
|
@@ -10,7 +10,9 @@ import clsx from 'clsx';
|
|
|
10
10
|
import React, { useEffect, useState, useRef, forwardRef } from 'react';
|
|
11
11
|
import { AttachmentPreviewList } from './attachments';
|
|
12
12
|
import { ChatInput } from './chat-input';
|
|
13
|
-
import {
|
|
13
|
+
import { MessageFooter } from './messages/footer';
|
|
14
|
+
import { MessageRenderer } from './messages/message-renderer';
|
|
15
|
+
import { WelcomeMessage } from './messages/welcome';
|
|
14
16
|
import { ScrollContainer } from './scroll-container';
|
|
15
17
|
import { InputModel } from '../input-model';
|
|
16
18
|
import { replaceSpanToMention } from '../utils';
|
|
@@ -61,7 +63,7 @@ export function ChatMessages(props) {
|
|
|
61
63
|
setMessages([...model.messages]);
|
|
62
64
|
}
|
|
63
65
|
function handleWritersChange(_, writers) {
|
|
64
|
-
setCurrentWriters(writers);
|
|
66
|
+
setCurrentWriters(writers.map(writer => writer.user));
|
|
65
67
|
}
|
|
66
68
|
model.messagesUpdated.connect(handleChatEvents);
|
|
67
69
|
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(handleWritersChange);
|
|
@@ -132,13 +134,15 @@ export function ChatMessages(props) {
|
|
|
132
134
|
}, [messages, allRendered]);
|
|
133
135
|
return (React.createElement(React.Fragment, null,
|
|
134
136
|
React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
|
|
137
|
+
props.welcomeMessage && (React.createElement(WelcomeMessage, { rmRegistry: props.rmRegistry, content: props.welcomeMessage })),
|
|
135
138
|
React.createElement(Box, { ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, messages.map((message, i) => {
|
|
136
139
|
renderedPromise.current[i] = new PromiseDelegate();
|
|
137
140
|
return (
|
|
138
141
|
// extra div needed to ensure each bubble is on a new line
|
|
139
142
|
React.createElement(Box, { key: i, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
140
143
|
React.createElement(ChatMessageHeader, { message: message }),
|
|
141
|
-
React.createElement(ChatMessage, { ...props, message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) })
|
|
144
|
+
React.createElement(ChatMessage, { ...props, message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) }),
|
|
145
|
+
props.messageFooterRegistry && (React.createElement(MessageFooter, { registry: props.messageFooterRegistry, message: message, model: model }))));
|
|
142
146
|
})),
|
|
143
147
|
React.createElement(Writers, { writers: currentWriters })),
|
|
144
148
|
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox, allRendered: allRendered })));
|
|
@@ -228,15 +232,19 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
228
232
|
const [deleted, setDeleted] = useState(false);
|
|
229
233
|
const [canEdit, setCanEdit] = useState(false);
|
|
230
234
|
const [canDelete, setCanDelete] = useState(false);
|
|
231
|
-
const [inputModel, setInputModel] = useState(null);
|
|
232
235
|
// Look if the message can be deleted or edited.
|
|
233
236
|
useEffect(() => {
|
|
234
237
|
var _a;
|
|
238
|
+
// Init canDelete and canEdit state.
|
|
235
239
|
setDeleted((_a = message.deleted) !== null && _a !== void 0 ? _a : false);
|
|
236
240
|
if (model.user !== undefined && !message.deleted) {
|
|
237
241
|
if (model.user.username === message.sender.username) {
|
|
238
242
|
setCanEdit(model.updateMessage !== undefined);
|
|
239
243
|
setCanDelete(model.deleteMessage !== undefined);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (message.sender.bot) {
|
|
247
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
240
248
|
}
|
|
241
249
|
}
|
|
242
250
|
else {
|
|
@@ -245,40 +253,41 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
245
253
|
}
|
|
246
254
|
}, [model, message]);
|
|
247
255
|
// Create an input model only if the message is edited.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
let body = message.body;
|
|
253
|
-
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
254
|
-
body = replaceSpanToMention(body, user);
|
|
255
|
-
});
|
|
256
|
-
return new InputModel({
|
|
257
|
-
chatContext: model.createChatContext(),
|
|
258
|
-
onSend: (input, model) => updateMessage(message.id, input, model),
|
|
259
|
-
onCancel: () => cancelEdition(),
|
|
260
|
-
value: body,
|
|
261
|
-
activeCellManager: model.activeCellManager,
|
|
262
|
-
selectionWatcher: model.selectionWatcher,
|
|
263
|
-
documentManager: model.documentManager,
|
|
264
|
-
config: {
|
|
265
|
-
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
266
|
-
},
|
|
267
|
-
attachments: message.attachments,
|
|
268
|
-
mentions: message.mentions
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
setInputModel(null);
|
|
256
|
+
const startEdition = () => {
|
|
257
|
+
var _a;
|
|
258
|
+
if (!canEdit) {
|
|
259
|
+
return;
|
|
274
260
|
}
|
|
275
|
-
|
|
261
|
+
let body = message.body;
|
|
262
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
263
|
+
body = replaceSpanToMention(body, user);
|
|
264
|
+
});
|
|
265
|
+
const inputModel = new InputModel({
|
|
266
|
+
chatContext: model.createChatContext(),
|
|
267
|
+
onSend: (input, model) => updateMessage(message.id, input, model),
|
|
268
|
+
onCancel: () => cancelEdition(),
|
|
269
|
+
value: body,
|
|
270
|
+
activeCellManager: model.activeCellManager,
|
|
271
|
+
selectionWatcher: model.selectionWatcher,
|
|
272
|
+
documentManager: model.documentManager,
|
|
273
|
+
config: {
|
|
274
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
275
|
+
},
|
|
276
|
+
attachments: message.attachments,
|
|
277
|
+
mentions: message.mentions
|
|
278
|
+
});
|
|
279
|
+
model.addEditionModel(message.id, inputModel);
|
|
280
|
+
setEdit(true);
|
|
281
|
+
};
|
|
276
282
|
// Cancel the current edition of the message.
|
|
277
283
|
const cancelEdition = () => {
|
|
284
|
+
var _a;
|
|
285
|
+
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
278
286
|
setEdit(false);
|
|
279
287
|
};
|
|
280
288
|
// Update the content of the message.
|
|
281
289
|
const updateMessage = (id, input, inputModel) => {
|
|
290
|
+
var _a;
|
|
282
291
|
if (!canEdit || !inputModel) {
|
|
283
292
|
return;
|
|
284
293
|
}
|
|
@@ -288,6 +297,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
288
297
|
updatedMessage.attachments = inputModel.attachments;
|
|
289
298
|
updatedMessage.mentions = inputModel.mentions;
|
|
290
299
|
model.updateMessage(id, updatedMessage);
|
|
300
|
+
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
291
301
|
setEdit(false);
|
|
292
302
|
};
|
|
293
303
|
// Delete the message.
|
|
@@ -299,7 +309,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
299
309
|
};
|
|
300
310
|
// Empty if the message has been deleted.
|
|
301
311
|
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
302
|
-
edit && canEdit &&
|
|
312
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry })) : (React.createElement(MessageRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
303
313
|
message.attachments && !edit && (
|
|
304
314
|
// Display the attachments only if message is not edited, otherwise the
|
|
305
315
|
// input component display them.
|
|
@@ -391,7 +401,8 @@ export function Navigation(props) {
|
|
|
391
401
|
useEffect(() => {
|
|
392
402
|
var _a, _b;
|
|
393
403
|
const viewportChanged = (model, viewport) => {
|
|
394
|
-
setLastInViewport(
|
|
404
|
+
setLastInViewport(model.messages.length === 0 ||
|
|
405
|
+
viewport.includes(model.messages.length - 1));
|
|
395
406
|
};
|
|
396
407
|
(_a = model.viewportChanged) === null || _a === void 0 ? void 0 : _a.connect(viewportChanged);
|
|
397
408
|
viewportChanged(model, (_b = model.messagesInViewport) !== null && _b !== void 0 ? _b : []);
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { IThemeManager } from '@jupyterlab/apputils';
|
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
5
5
|
import { IInputToolbarRegistry } from './input';
|
|
6
|
+
import { IMessageFooterRegistry } from '../footers';
|
|
6
7
|
import { IChatModel } from '../model';
|
|
7
8
|
import { IAttachmentOpenerRegistry } from '../registry';
|
|
8
9
|
export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
|
|
@@ -35,6 +36,14 @@ export declare namespace Chat {
|
|
|
35
36
|
* The input toolbar registry
|
|
36
37
|
*/
|
|
37
38
|
inputToolbarRegistry?: IInputToolbarRegistry;
|
|
39
|
+
/**
|
|
40
|
+
* The footer registry.
|
|
41
|
+
*/
|
|
42
|
+
messageFooterRegistry?: IMessageFooterRegistry;
|
|
43
|
+
/**
|
|
44
|
+
* The welcome message.
|
|
45
|
+
*/
|
|
46
|
+
welcomeMessage?: string;
|
|
38
47
|
}
|
|
39
48
|
/**
|
|
40
49
|
* The options to build the Chat UI.
|
package/lib/components/chat.js
CHANGED
|
@@ -19,7 +19,7 @@ export function ChatBody(props) {
|
|
|
19
19
|
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
20
20
|
}
|
|
21
21
|
return (React.createElement(AttachmentOpenerContext.Provider, { value: props.attachmentOpenerRegistry },
|
|
22
|
-
React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, inputToolbarRegistry: inputToolbarRegistry }),
|
|
22
|
+
React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, inputToolbarRegistry: inputToolbarRegistry, messageFooterRegistry: props.messageFooterRegistry, welcomeMessage: props.welcomeMessage }),
|
|
23
23
|
React.createElement(ChatInput, { sx: {
|
|
24
24
|
paddingLeft: 4,
|
|
25
25
|
paddingRight: 4,
|
|
@@ -4,7 +4,6 @@ export * from './chat-messages';
|
|
|
4
4
|
export * from './code-blocks';
|
|
5
5
|
export * from './input';
|
|
6
6
|
export * from './jl-theme-provider';
|
|
7
|
-
export * from './markdown-renderer';
|
|
8
7
|
export * from './mui-extras';
|
|
9
8
|
export * from './scroll-container';
|
|
10
9
|
export * from './toolbar';
|
package/lib/components/index.js
CHANGED
|
@@ -8,7 +8,6 @@ export * from './chat-messages';
|
|
|
8
8
|
export * from './code-blocks';
|
|
9
9
|
export * from './input';
|
|
10
10
|
export * from './jl-theme-provider';
|
|
11
|
-
export * from './markdown-renderer';
|
|
12
11
|
export * from './mui-extras';
|
|
13
12
|
export * from './scroll-container';
|
|
14
13
|
export * from './toolbar';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IMessageFooterRegistry, MessageFooterSectionProps } from '../../footers';
|
|
3
|
+
/**
|
|
4
|
+
* The chat footer component properties.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMessageFootersProps extends MessageFooterSectionProps {
|
|
7
|
+
/**
|
|
8
|
+
* The chat footer registry.
|
|
9
|
+
*/
|
|
10
|
+
registry: IMessageFooterRegistry;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The chat footer component, which displays footer components on a row according to
|
|
14
|
+
* their respective positions.
|
|
15
|
+
*/
|
|
16
|
+
export declare function MessageFooter(props: IMessageFootersProps): JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Box } from '@mui/material';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
/**
|
|
8
|
+
* The chat footer component, which displays footer components on a row according to
|
|
9
|
+
* their respective positions.
|
|
10
|
+
*/
|
|
11
|
+
export function MessageFooter(props) {
|
|
12
|
+
var _a, _b, _c;
|
|
13
|
+
const { message, model, registry } = props;
|
|
14
|
+
const footer = registry.getFooter();
|
|
15
|
+
return (React.createElement(Box, { sx: { display: 'flex', justifyContent: 'space-between' } },
|
|
16
|
+
((_a = footer.left) === null || _a === void 0 ? void 0 : _a.component) ? (React.createElement(footer.left.component, { message: message, model: model })) : (React.createElement("div", null)),
|
|
17
|
+
((_b = footer.center) === null || _b === void 0 ? void 0 : _b.component) ? (React.createElement(footer.center.component, { message: message, model: model })) : (React.createElement("div", null)),
|
|
18
|
+
((_c = footer.right) === null || _c === void 0 ? void 0 : _c.component) ? (React.createElement(footer.right.component, { message: message, model: model })) : (React.createElement("div", null))));
|
|
19
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
2
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { IChatModel } from '
|
|
5
|
-
|
|
4
|
+
import { IChatModel } from '../../model';
|
|
5
|
+
/**
|
|
6
|
+
* The type of the props for the MessageRenderer component.
|
|
7
|
+
*/
|
|
8
|
+
type MessageRendererProps = {
|
|
6
9
|
/**
|
|
7
10
|
* The string to render.
|
|
8
11
|
*/
|
|
@@ -32,6 +35,9 @@ type MarkdownRendererProps = {
|
|
|
32
35
|
*/
|
|
33
36
|
delete?: () => void;
|
|
34
37
|
};
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The message renderer base component.
|
|
40
|
+
*/
|
|
41
|
+
declare function MessageRendererBase(props: MessageRendererProps): JSX.Element;
|
|
42
|
+
export declare const MessageRenderer: React.MemoExoticComponent<typeof MessageRendererBase>;
|
|
37
43
|
export {};
|
|
@@ -4,47 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import React, { useState, useEffect } from 'react';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
7
|
-
import { CodeToolbar } from '
|
|
8
|
-
import { MessageToolbar } from '
|
|
9
|
-
|
|
10
|
-
const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
|
|
7
|
+
import { CodeToolbar } from '../code-blocks/code-toolbar';
|
|
8
|
+
import { MessageToolbar } from '../toolbar';
|
|
9
|
+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
13
|
-
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
14
|
-
* returns `\\(`.
|
|
15
|
-
*
|
|
16
|
-
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
|
|
17
|
-
* `ILatexTypesetter`.
|
|
11
|
+
* The message renderer base component.
|
|
18
12
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
.replace(/\\\(/g, '\\\\(')
|
|
22
|
-
.replace(/\\\)/g, '\\\\)')
|
|
23
|
-
.replace(/\\\[/g, '\\\\[')
|
|
24
|
-
.replace(/\\\]/g, '\\\\]');
|
|
25
|
-
}
|
|
26
|
-
function MarkdownRendererBase(props) {
|
|
13
|
+
function MessageRendererBase(props) {
|
|
14
|
+
const { markdownStr, rmRegistry } = props;
|
|
27
15
|
const appendContent = props.appendContent || false;
|
|
28
16
|
const [renderedContent, setRenderedContent] = useState(null);
|
|
29
17
|
// each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
30
18
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
31
19
|
useEffect(() => {
|
|
32
20
|
const renderContent = async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const model = props.rmRegistry.createModel({
|
|
37
|
-
data: { [MD_MIME_TYPE]: mdStr }
|
|
21
|
+
const renderer = await MarkdownRenderer.renderContent({
|
|
22
|
+
content: markdownStr,
|
|
23
|
+
rmRegistry
|
|
38
24
|
});
|
|
39
|
-
const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
40
|
-
// step 1: render markdown
|
|
41
|
-
await renderer.renderModel(model);
|
|
42
|
-
(_a = props.rmRegistry.latexTypesetter) === null || _a === void 0 ? void 0 : _a.typeset(renderer.node);
|
|
43
|
-
if (!renderer.node) {
|
|
44
|
-
throw new Error('Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.');
|
|
45
|
-
}
|
|
46
|
-
// step 2: render LaTeX via MathJax.
|
|
47
|
-
(_b = props.rmRegistry.latexTypesetter) === null || _b === void 0 ? void 0 : _b.typeset(renderer.node);
|
|
48
25
|
const newCodeToolbarDefns = [];
|
|
49
26
|
// Attach CodeToolbar root element to each <pre> block
|
|
50
27
|
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
@@ -63,7 +40,7 @@ function MarkdownRendererBase(props) {
|
|
|
63
40
|
props.rendered.resolve();
|
|
64
41
|
};
|
|
65
42
|
renderContent();
|
|
66
|
-
}, [
|
|
43
|
+
}, [markdownStr, rmRegistry]);
|
|
67
44
|
return (React.createElement("div", { className: MD_RENDERED_CLASS },
|
|
68
45
|
renderedContent &&
|
|
69
46
|
(appendContent ? (React.createElement("div", { ref: node => node && node.appendChild(renderedContent) })) : (React.createElement("div", { ref: node => node && node.replaceChildren(renderedContent) }))),
|
|
@@ -76,4 +53,4 @@ function MarkdownRendererBase(props) {
|
|
|
76
53
|
return createPortal(React.createElement(CodeToolbar, { ...codeToolbarProps }), codeToolbarRoot);
|
|
77
54
|
})));
|
|
78
55
|
}
|
|
79
|
-
export const
|
|
56
|
+
export const MessageRenderer = React.memo(MessageRendererBase);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { MarkdownRenderer } from '../../markdown-renderer';
|
|
3
|
+
/**
|
|
4
|
+
* The welcome message component.
|
|
5
|
+
* This message is displayed on top of the chat messages, and is rendered using a
|
|
6
|
+
* markdown renderer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function WelcomeMessage(props: MarkdownRenderer.IOptions): JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { classes } from '@jupyterlab/ui-components';
|
|
6
|
+
import React, { useEffect, useRef } from 'react';
|
|
7
|
+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
8
|
+
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
|
|
9
|
+
/**
|
|
10
|
+
* The welcome message component.
|
|
11
|
+
* This message is displayed on top of the chat messages, and is rendered using a
|
|
12
|
+
* markdown renderer.
|
|
13
|
+
*/
|
|
14
|
+
export function WelcomeMessage(props) {
|
|
15
|
+
const { rmRegistry } = props;
|
|
16
|
+
const content = props.content + '\n----\n';
|
|
17
|
+
// ref that tracks the content container to store the rendermime node in
|
|
18
|
+
const renderingContainer = useRef(null);
|
|
19
|
+
// ref that tracks whether the rendermime node has already been inserted
|
|
20
|
+
const renderingInserted = useRef(false);
|
|
21
|
+
/**
|
|
22
|
+
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
|
|
23
|
+
* and insert it into `renderingContainer` if not yet inserted.
|
|
24
|
+
*/
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const renderContent = async () => {
|
|
27
|
+
const renderer = await MarkdownRenderer.renderContent({
|
|
28
|
+
content,
|
|
29
|
+
rmRegistry
|
|
30
|
+
});
|
|
31
|
+
// insert the rendering into renderingContainer if not yet inserted
|
|
32
|
+
if (renderingContainer.current !== null && !renderingInserted.current) {
|
|
33
|
+
renderingContainer.current.appendChild(renderer.node);
|
|
34
|
+
renderingInserted.current = true;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
renderContent();
|
|
38
|
+
}, [content]);
|
|
39
|
+
return (React.createElement("div", { className: classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS) },
|
|
40
|
+
React.createElement("div", { ref: renderingContainer })));
|
|
41
|
+
}
|
|
@@ -28,6 +28,6 @@ export function ScrollContainer(props) {
|
|
|
28
28
|
},
|
|
29
29
|
...props.sx
|
|
30
30
|
} },
|
|
31
|
-
React.createElement(Box,
|
|
31
|
+
React.createElement(Box, null, props.children),
|
|
32
32
|
React.createElement(Box, { sx: { overflowAnchor: 'auto', height: '1px' } })));
|
|
33
33
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Token } from '@lumino/coreutils';
|
|
2
|
+
import { MessageFooter, MessageFooterSection } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The interface of a registry to provide chat footer.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMessageFooterRegistry {
|
|
7
|
+
/**
|
|
8
|
+
* Get the message footer.
|
|
9
|
+
*/
|
|
10
|
+
getFooter(): MessageFooter;
|
|
11
|
+
/**
|
|
12
|
+
* Add a message footer section.
|
|
13
|
+
* If multiple labextensions add a section in the same region, only
|
|
14
|
+
* the last one will be displayed.
|
|
15
|
+
*/
|
|
16
|
+
addSection(section: MessageFooterSection): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The default implementation of the message footer registry.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MessageFooterRegistry implements IMessageFooterRegistry {
|
|
22
|
+
/**
|
|
23
|
+
* Get the footer from the registry.
|
|
24
|
+
*/
|
|
25
|
+
getFooter(): MessageFooter;
|
|
26
|
+
/**
|
|
27
|
+
* Add a message footer.
|
|
28
|
+
* If several extension add footers, only the last one will be displayed.
|
|
29
|
+
*/
|
|
30
|
+
addSection(footer: MessageFooterSection): void;
|
|
31
|
+
private _footers;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The token providing the chat footer registry.
|
|
35
|
+
*/
|
|
36
|
+
export declare const IMessageFooterRegistry: Token<IMessageFooterRegistry>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
/**
|
|
7
|
+
* The default implementation of the message footer registry.
|
|
8
|
+
*/
|
|
9
|
+
export class MessageFooterRegistry {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._footers = {};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the footer from the registry.
|
|
15
|
+
*/
|
|
16
|
+
getFooter() {
|
|
17
|
+
return this._footers;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Add a message footer.
|
|
21
|
+
* If several extension add footers, only the last one will be displayed.
|
|
22
|
+
*/
|
|
23
|
+
addSection(footer) {
|
|
24
|
+
this._footers[footer.position] = footer;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The token providing the chat footer registry.
|
|
29
|
+
*/
|
|
30
|
+
export const IMessageFooterRegistry = new Token('@jupyter/chat:ChatFooterRegistry');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IChatModel } from '../model';
|
|
3
|
+
import { IChatMessage } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* The props sent passed to each `MessageFooterSection` React component.
|
|
6
|
+
*/
|
|
7
|
+
export type MessageFooterSectionProps = {
|
|
8
|
+
model: IChatModel;
|
|
9
|
+
message: IChatMessage;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A message footer section which can be added to the footer registry.
|
|
13
|
+
*/
|
|
14
|
+
export type MessageFooterSection = {
|
|
15
|
+
component: React.FC<MessageFooterSectionProps>;
|
|
16
|
+
position: 'left' | 'center' | 'right';
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The message footer returned by the registry, composed of 'left', 'center',
|
|
20
|
+
* and 'right' sections.
|
|
21
|
+
*/
|
|
22
|
+
export type MessageFooter = {
|
|
23
|
+
left?: MessageFooterSection;
|
|
24
|
+
center?: MessageFooterSection;
|
|
25
|
+
right?: MessageFooterSection;
|
|
26
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './active-cell-manager';
|
|
2
2
|
export * from './chat-commands';
|
|
3
3
|
export * from './components';
|
|
4
|
+
export * from './footers';
|
|
4
5
|
export * from './icons';
|
|
5
6
|
export * from './input-model';
|
|
7
|
+
export * from './markdown-renderer';
|
|
6
8
|
export * from './model';
|
|
7
9
|
export * from './registry';
|
|
8
10
|
export * from './selection-watcher';
|
package/lib/index.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
export * from './active-cell-manager';
|
|
6
6
|
export * from './chat-commands';
|
|
7
7
|
export * from './components';
|
|
8
|
+
export * from './footers';
|
|
8
9
|
export * from './icons';
|
|
9
10
|
export * from './input-model';
|
|
11
|
+
export * from './markdown-renderer';
|
|
10
12
|
export * from './model';
|
|
11
13
|
export * from './registry';
|
|
12
14
|
export * from './selection-watcher';
|
package/lib/input-model.d.ts
CHANGED
|
@@ -114,6 +114,10 @@ export interface IInputModel extends IDisposable {
|
|
|
114
114
|
* Clear mentions list.
|
|
115
115
|
*/
|
|
116
116
|
clearMentions(): void;
|
|
117
|
+
/**
|
|
118
|
+
* A signal emitting when disposing of the model.
|
|
119
|
+
*/
|
|
120
|
+
readonly onDisposed: ISignal<InputModel, void>;
|
|
117
121
|
}
|
|
118
122
|
/**
|
|
119
123
|
* The input model.
|
|
@@ -231,6 +235,10 @@ export declare class InputModel implements IInputModel {
|
|
|
231
235
|
* Dispose the input model.
|
|
232
236
|
*/
|
|
233
237
|
dispose(): void;
|
|
238
|
+
/**
|
|
239
|
+
* A signal emitting when disposing of the model.
|
|
240
|
+
*/
|
|
241
|
+
get onDisposed(): ISignal<InputModel, void>;
|
|
234
242
|
/**
|
|
235
243
|
* Whether the input model is disposed.
|
|
236
244
|
*/
|
|
@@ -252,6 +260,7 @@ export declare class InputModel implements IInputModel {
|
|
|
252
260
|
private _configChanged;
|
|
253
261
|
private _focusInputSignal;
|
|
254
262
|
private _attachmentsChanged;
|
|
263
|
+
private _onDisposed;
|
|
255
264
|
private _isDisposed;
|
|
256
265
|
}
|
|
257
266
|
export declare namespace InputModel {
|
package/lib/input-model.js
CHANGED
|
@@ -60,6 +60,7 @@ export class InputModel {
|
|
|
60
60
|
this._configChanged = new Signal(this);
|
|
61
61
|
this._focusInputSignal = new Signal(this);
|
|
62
62
|
this._attachmentsChanged = new Signal(this);
|
|
63
|
+
this._onDisposed = new Signal(this);
|
|
63
64
|
this._isDisposed = false;
|
|
64
65
|
this._onSend = options.onSend;
|
|
65
66
|
this._chatContext = options.chatContext;
|
|
@@ -235,8 +236,15 @@ export class InputModel {
|
|
|
235
236
|
if (this.isDisposed) {
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
239
|
+
this._onDisposed.emit();
|
|
238
240
|
this._isDisposed = true;
|
|
239
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* A signal emitting when disposing of the model.
|
|
244
|
+
*/
|
|
245
|
+
get onDisposed() {
|
|
246
|
+
return this._onDisposed;
|
|
247
|
+
}
|
|
240
248
|
/**
|
|
241
249
|
* Whether the input model is disposed.
|
|
242
250
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
+
export declare const MD_RENDERED_CLASS = "jp-chat-rendered-markdown";
|
|
3
|
+
export declare const MD_MIME_TYPE = "text/markdown";
|
|
4
|
+
/**
|
|
5
|
+
* A namespace for the MarkdownRenderer.
|
|
6
|
+
*/
|
|
7
|
+
export declare namespace MarkdownRenderer {
|
|
8
|
+
/**
|
|
9
|
+
* The options for the MarkdownRenderer.
|
|
10
|
+
*/
|
|
11
|
+
interface IOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The rendermime registry.
|
|
14
|
+
*/
|
|
15
|
+
rmRegistry: IRenderMimeRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* The markdown content.
|
|
18
|
+
*/
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A generic function to render a markdown string into a DOM element.
|
|
23
|
+
*
|
|
24
|
+
* @param content - the markdown content.
|
|
25
|
+
* @param rmRegistry - the rendermime registry.
|
|
26
|
+
* @returns a promise that resolves to the renderer.
|
|
27
|
+
*/
|
|
28
|
+
function renderContent(options: IOptions): Promise<IRenderMime.IRenderer>;
|
|
29
|
+
/**
|
|
30
|
+
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
|
|
31
|
+
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
32
|
+
* returns `\\(`.
|
|
33
|
+
*
|
|
34
|
+
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
|
|
35
|
+
* `ILatexTypesetter`.
|
|
36
|
+
*/
|
|
37
|
+
function escapeLatexDelimiters(text: string): string;
|
|
38
|
+
}
|