@jupyter/chat 0.19.0 → 0.20.0-alpha.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.js +0 -1
- package/lib/components/messages/message-renderer.d.ts +3 -6
- package/lib/components/messages/message-renderer.js +104 -29
- package/lib/components/messages/message.js +2 -2
- package/lib/components/messages/toolbar.js +1 -1
- package/lib/components/messages/welcome.js +24 -14
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/message.d.ts +2 -1
- package/lib/model.js +0 -5
- package/lib/theme-provider.js +10 -9
- package/lib/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/chat.tsx +0 -1
- package/src/components/messages/message-renderer.tsx +125 -44
- package/src/components/messages/message.tsx +2 -2
- package/src/components/messages/toolbar.tsx +1 -1
- package/src/components/messages/welcome.tsx +25 -17
- package/src/index.ts +0 -1
- package/src/message.ts +4 -1
- package/src/model.ts +0 -4
- package/src/theme-provider.ts +10 -9
- package/src/types.ts +5 -1
- package/style/chat.css +14 -9
- package/style/input.css +3 -3
- package/lib/markdown-renderer.d.ts +0 -38
- package/lib/markdown-renderer.js +0 -54
- package/src/markdown-renderer.ts +0 -78
package/lib/components/chat.js
CHANGED
|
@@ -40,7 +40,6 @@ export function ChatBody(props) {
|
|
|
40
40
|
(_a = model === null || model === void 0 ? void 0 : model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
|
|
41
41
|
};
|
|
42
42
|
}, [model]);
|
|
43
|
-
// const horizontalPadding = props.area === 'main' ? 8 : 4;
|
|
44
43
|
const horizontalPadding = 4;
|
|
45
44
|
const contextValue = {
|
|
46
45
|
...props,
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { IMessageContent } from '../../types';
|
|
3
4
|
/**
|
|
4
5
|
* The type of the props for the MessageRenderer component.
|
|
5
6
|
*/
|
|
6
7
|
type MessageRendererProps = {
|
|
7
8
|
/**
|
|
8
|
-
* The string to render.
|
|
9
|
+
* The string or rendermime bundle to render.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
message: IMessageContent;
|
|
11
12
|
/**
|
|
12
13
|
* The promise to resolve when the message is rendered.
|
|
13
14
|
*/
|
|
14
15
|
rendered: PromiseDelegate<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Whether to append the content to the existing content or not.
|
|
17
|
-
*/
|
|
18
|
-
appendContent?: boolean;
|
|
19
16
|
/**
|
|
20
17
|
* The function to call to edit a message.
|
|
21
18
|
*/
|
|
@@ -2,51 +2,126 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
6
8
|
import { createPortal } from 'react-dom';
|
|
7
9
|
import { MessageToolbar } from './toolbar';
|
|
8
10
|
import { CodeToolbar } from '../code-blocks/code-toolbar';
|
|
9
11
|
import { useChatContext } from '../../context';
|
|
10
|
-
import {
|
|
12
|
+
import { replaceMentionToSpan } from '../../utils';
|
|
13
|
+
const RENDERED_CLASS = 'jp-chat-rendered-message';
|
|
14
|
+
const DEFAULT_MIME_TYPE = 'text/markdown';
|
|
11
15
|
/**
|
|
12
16
|
* The message renderer base component.
|
|
13
17
|
*/
|
|
14
18
|
function MessageRendererBase(props) {
|
|
15
|
-
const {
|
|
19
|
+
const { message } = props;
|
|
16
20
|
const { model, rmRegistry } = useChatContext();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const containerRef = useRef(null);
|
|
22
|
+
// Allow edition only on text messages.
|
|
23
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
24
|
+
// Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
20
25
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
21
26
|
useEffect(() => {
|
|
27
|
+
let node = null;
|
|
28
|
+
const container = containerRef.current;
|
|
29
|
+
if (!container) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
22
32
|
const renderContent = async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(_a =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
var _a, _b;
|
|
34
|
+
let isMarkdownRenderer = true;
|
|
35
|
+
let renderer;
|
|
36
|
+
let mimeModel;
|
|
37
|
+
// Create the renderer and the mime model.
|
|
38
|
+
if (typeof message.body === 'string') {
|
|
39
|
+
// Allow editing content for text messages.
|
|
40
|
+
setCanEdit(true);
|
|
41
|
+
// Improve users display in markdown content.
|
|
42
|
+
let mdStr = message.body;
|
|
43
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
44
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
45
|
+
});
|
|
46
|
+
// Body is a string, use the markdown renderer.
|
|
47
|
+
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
48
|
+
mimeModel = rmRegistry.createModel({
|
|
49
|
+
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
setCanEdit(false);
|
|
54
|
+
// This is a mime bundle.
|
|
55
|
+
let mimeContent = message.body;
|
|
56
|
+
let preferred = rmRegistry.preferredMimeType(mimeContent.data, 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
57
|
+
);
|
|
58
|
+
if (!preferred) {
|
|
59
|
+
preferred = DEFAULT_MIME_TYPE;
|
|
60
|
+
mimeContent = {
|
|
61
|
+
data: {
|
|
62
|
+
[DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
renderer = rmRegistry.createRenderer(preferred);
|
|
67
|
+
// Improve users display in markdown content.
|
|
68
|
+
if (preferred === DEFAULT_MIME_TYPE) {
|
|
69
|
+
let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
|
|
70
|
+
if (mdStr) {
|
|
71
|
+
(_b = message.mentions) === null || _b === void 0 ? void 0 : _b.forEach(user => {
|
|
72
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
73
|
+
});
|
|
74
|
+
mimeContent = {
|
|
75
|
+
...mimeContent,
|
|
76
|
+
data: {
|
|
77
|
+
...mimeContent.data,
|
|
78
|
+
[DEFAULT_MIME_TYPE]: mdStr
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
isMarkdownRenderer = false;
|
|
85
|
+
}
|
|
86
|
+
mimeModel = rmRegistry.createModel(mimeContent);
|
|
87
|
+
}
|
|
88
|
+
await renderer.renderModel(mimeModel);
|
|
89
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
90
|
+
// never been attached, only the node.
|
|
91
|
+
// This is necessary to render latex.
|
|
92
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
93
|
+
// Add code toolbar if markdown has been rendered.
|
|
94
|
+
if (isMarkdownRenderer) {
|
|
95
|
+
const newCodeToolbarDefns = [];
|
|
96
|
+
// Attach CodeToolbar root element to each <pre> block
|
|
97
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
98
|
+
preBlocks.forEach(preBlock => {
|
|
99
|
+
var _a;
|
|
100
|
+
const codeToolbarRoot = document.createElement('div');
|
|
101
|
+
(_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
|
|
102
|
+
newCodeToolbarDefns.push([
|
|
103
|
+
codeToolbarRoot,
|
|
104
|
+
{ model: model, content: preBlock.textContent || '' }
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
108
|
+
}
|
|
109
|
+
// Add the rendered node to the DOM.
|
|
110
|
+
node = renderer.node;
|
|
111
|
+
container.insertBefore(node, container.firstChild);
|
|
41
112
|
// Resolve the rendered promise.
|
|
42
113
|
props.rendered.resolve();
|
|
43
114
|
};
|
|
44
115
|
renderContent();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
return () => {
|
|
117
|
+
if (node && container.contains(node)) {
|
|
118
|
+
container.removeChild(node);
|
|
119
|
+
}
|
|
120
|
+
node = null;
|
|
121
|
+
};
|
|
122
|
+
}, [message.body, message.mentions, rmRegistry]);
|
|
123
|
+
return (React.createElement("div", { className: RENDERED_CLASS, ref: containerRef },
|
|
124
|
+
React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
|
|
50
125
|
// Render a `CodeToolbar` element underneath each code block.
|
|
51
126
|
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
52
127
|
// to use the context in the main React tree.
|
|
@@ -57,7 +57,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
57
57
|
// Create an input model only if the message is edited.
|
|
58
58
|
const startEdition = () => {
|
|
59
59
|
var _a;
|
|
60
|
-
if (!canEdit) {
|
|
60
|
+
if (!canEdit || !(typeof message.body === 'string')) {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
let body = message.body;
|
|
@@ -111,7 +111,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
111
111
|
};
|
|
112
112
|
// Empty if the message has been deleted.
|
|
113
113
|
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
114
|
-
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, {
|
|
114
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, { message: message, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
115
115
|
message.attachments && !edit && (
|
|
116
116
|
// Display the attachments only if message is not edited, otherwise the
|
|
117
117
|
// input component display them.
|
|
@@ -14,7 +14,7 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
|
14
14
|
export function MessageToolbar(props) {
|
|
15
15
|
const buttons = [];
|
|
16
16
|
if (props.edit !== undefined) {
|
|
17
|
-
const editButton = (React.createElement(TooltippedIconButton, { tooltip: '
|
|
17
|
+
const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
|
|
18
18
|
React.createElement(EditIcon, null)));
|
|
19
19
|
buttons.push(editButton);
|
|
20
20
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
6
7
|
import React, { useEffect, useRef } from 'react';
|
|
7
8
|
import { useChatContext } from '../../context';
|
|
8
|
-
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
9
9
|
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
|
|
10
|
+
const MD_MIME_TYPE = 'text/markdown';
|
|
10
11
|
/**
|
|
11
12
|
* The welcome message component.
|
|
12
13
|
* This message is displayed on top of the chat messages, and is rendered using a
|
|
@@ -17,26 +18,35 @@ export function WelcomeMessage(props) {
|
|
|
17
18
|
const content = props.content + '\n----\n';
|
|
18
19
|
// ref that tracks the content container to store the rendermime node in
|
|
19
20
|
const renderingContainer = useRef(null);
|
|
20
|
-
// ref that tracks whether the rendermime node has already been inserted
|
|
21
|
-
const renderingInserted = useRef(false);
|
|
22
21
|
/**
|
|
23
22
|
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
|
|
24
23
|
* and insert it into `renderingContainer` if not yet inserted.
|
|
25
24
|
*/
|
|
26
25
|
useEffect(() => {
|
|
26
|
+
let node = null;
|
|
27
27
|
const renderContent = async () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
var _a;
|
|
29
|
+
// Render the welcome message using markdown renderer.
|
|
30
|
+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
31
|
+
const mimeModel = rmRegistry.createModel({
|
|
32
|
+
data: { [MD_MIME_TYPE]: content }
|
|
31
33
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
await renderer.renderModel(mimeModel);
|
|
35
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
36
|
+
// never been attached, only the node.
|
|
37
|
+
// This is necessary to render latex.
|
|
38
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
39
|
+
node = renderer.node;
|
|
40
|
+
(_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.append(node);
|
|
37
41
|
};
|
|
38
42
|
renderContent();
|
|
43
|
+
return () => {
|
|
44
|
+
var _a;
|
|
45
|
+
if (node && ((_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.contains(node))) {
|
|
46
|
+
renderingContainer.current.removeChild(node);
|
|
47
|
+
}
|
|
48
|
+
node = null;
|
|
49
|
+
};
|
|
39
50
|
}, [content]);
|
|
40
|
-
return
|
|
41
|
-
React.createElement("div", { ref: renderingContainer })));
|
|
51
|
+
return React.createElement("div", { className: WELCOME_MESSAGE_CLASS, ref: renderingContainer });
|
|
42
52
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ export * from './active-cell-manager';
|
|
|
6
6
|
export * from './components';
|
|
7
7
|
export * from './icons';
|
|
8
8
|
export * from './input-model';
|
|
9
|
-
export * from './markdown-renderer';
|
|
10
9
|
export * from './model';
|
|
11
10
|
export * from './registers';
|
|
12
11
|
export * from './selection-watcher';
|
package/lib/message.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
1
2
|
import { ISignal } from '@lumino/signaling';
|
|
2
3
|
import { IAttachment, IMessageContent, IMessage, IUser } from './types';
|
|
3
4
|
/**
|
|
@@ -18,7 +19,7 @@ export declare class Message implements IMessage {
|
|
|
18
19
|
* Getters for each attribute individually.
|
|
19
20
|
*/
|
|
20
21
|
get type(): string;
|
|
21
|
-
get body(): string;
|
|
22
|
+
get body(): string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
|
|
22
23
|
get id(): string;
|
|
23
24
|
get time(): number;
|
|
24
25
|
get sender(): IUser;
|
package/lib/model.js
CHANGED
|
@@ -7,7 +7,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
7
7
|
import { Signal } from '@lumino/signaling';
|
|
8
8
|
import { InputModel } from './input-model';
|
|
9
9
|
import { Message } from './message';
|
|
10
|
-
import { replaceMentionToSpan } from './utils';
|
|
11
10
|
/**
|
|
12
11
|
* An abstract implementation of IChatModel.
|
|
13
12
|
*
|
|
@@ -289,10 +288,6 @@ export class AbstractChatModel {
|
|
|
289
288
|
* Can be useful if some actions are required on the message.
|
|
290
289
|
*/
|
|
291
290
|
formatChatMessage(message) {
|
|
292
|
-
var _a;
|
|
293
|
-
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
294
|
-
message.body = replaceMentionToSpan(message.body, user);
|
|
295
|
-
});
|
|
296
291
|
return message;
|
|
297
292
|
}
|
|
298
293
|
/**
|
package/lib/theme-provider.js
CHANGED
|
@@ -15,8 +15,9 @@ export async function pollUntilReady() {
|
|
|
15
15
|
}
|
|
16
16
|
export async function getJupyterLabTheme() {
|
|
17
17
|
await pollUntilReady();
|
|
18
|
-
const light = document.body.getAttribute('data-jp-theme-light');
|
|
18
|
+
const light = document.body.getAttribute('data-jp-theme-light') === 'true';
|
|
19
19
|
return createTheme({
|
|
20
|
+
cssVariables: true,
|
|
20
21
|
spacing: 4,
|
|
21
22
|
components: {
|
|
22
23
|
MuiButton: {
|
|
@@ -40,12 +41,12 @@ export async function getJupyterLabTheme() {
|
|
|
40
41
|
// The default style for input toolbar button variant.
|
|
41
42
|
props: { variant: 'input-toolbar' },
|
|
42
43
|
style: {
|
|
43
|
-
backgroundColor:
|
|
44
|
-
color: '
|
|
44
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
45
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
45
46
|
borderRadius: '4px',
|
|
46
47
|
boxShadow: 'none',
|
|
47
48
|
'&:hover': {
|
|
48
|
-
backgroundColor:
|
|
49
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
49
50
|
boxShadow: 'none'
|
|
50
51
|
},
|
|
51
52
|
'&:disabled': {
|
|
@@ -97,12 +98,12 @@ export async function getJupyterLabTheme() {
|
|
|
97
98
|
// The default style for input toolbar button variant.
|
|
98
99
|
props: { variant: 'input-toolbar' },
|
|
99
100
|
style: {
|
|
100
|
-
backgroundColor:
|
|
101
|
-
color: '
|
|
101
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
102
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
102
103
|
borderRadius: '4px',
|
|
103
104
|
boxShadow: 'none',
|
|
104
105
|
'&:hover': {
|
|
105
|
-
backgroundColor:
|
|
106
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
106
107
|
boxShadow: 'none'
|
|
107
108
|
},
|
|
108
109
|
'&:disabled': {
|
|
@@ -162,9 +163,9 @@ export async function getJupyterLabTheme() {
|
|
|
162
163
|
paper: getCSSVariable('--jp-layout-color1'),
|
|
163
164
|
default: getCSSVariable('--jp-layout-color1')
|
|
164
165
|
},
|
|
165
|
-
mode: light
|
|
166
|
+
mode: light ? 'light' : 'dark',
|
|
166
167
|
primary: {
|
|
167
|
-
main: getCSSVariable(
|
|
168
|
+
main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
|
|
168
169
|
light: getCSSVariable('--jp-brand-color2'),
|
|
169
170
|
dark: getCSSVariable('--jp-brand-color0')
|
|
170
171
|
},
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ISignal } from '@lumino/signaling';
|
|
2
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
2
3
|
/**
|
|
3
4
|
* The user description.
|
|
4
5
|
*/
|
|
@@ -57,7 +58,7 @@ export interface IConfig {
|
|
|
57
58
|
*/
|
|
58
59
|
export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
59
60
|
type: string;
|
|
60
|
-
body: string;
|
|
61
|
+
body: string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
|
|
61
62
|
id: string;
|
|
62
63
|
time: number;
|
|
63
64
|
sender: T;
|
package/package.json
CHANGED
package/src/components/chat.tsx
CHANGED
|
@@ -3,31 +3,34 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
6
7
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
|
-
import
|
|
8
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
9
|
+
import { Widget } from '@lumino/widgets';
|
|
10
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
8
11
|
import { createPortal } from 'react-dom';
|
|
9
12
|
|
|
10
13
|
import { MessageToolbar } from './toolbar';
|
|
11
14
|
import { CodeToolbar, CodeToolbarProps } from '../code-blocks/code-toolbar';
|
|
12
15
|
import { useChatContext } from '../../context';
|
|
13
|
-
import {
|
|
16
|
+
import { IMessageContent } from '../../types';
|
|
17
|
+
import { replaceMentionToSpan } from '../../utils';
|
|
18
|
+
|
|
19
|
+
const RENDERED_CLASS = 'jp-chat-rendered-message';
|
|
20
|
+
const DEFAULT_MIME_TYPE = 'text/markdown';
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* The type of the props for the MessageRenderer component.
|
|
17
24
|
*/
|
|
18
25
|
type MessageRendererProps = {
|
|
19
26
|
/**
|
|
20
|
-
* The string to render.
|
|
27
|
+
* The string or rendermime bundle to render.
|
|
21
28
|
*/
|
|
22
|
-
|
|
29
|
+
message: IMessageContent;
|
|
23
30
|
/**
|
|
24
31
|
* The promise to resolve when the message is rendered.
|
|
25
32
|
*/
|
|
26
33
|
rendered: PromiseDelegate<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Whether to append the content to the existing content or not.
|
|
29
|
-
*/
|
|
30
|
-
appendContent?: boolean;
|
|
31
34
|
/**
|
|
32
35
|
* The function to call to edit a message.
|
|
33
36
|
*/
|
|
@@ -42,60 +45,138 @@ type MessageRendererProps = {
|
|
|
42
45
|
* The message renderer base component.
|
|
43
46
|
*/
|
|
44
47
|
function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
45
|
-
const {
|
|
48
|
+
const { message } = props;
|
|
46
49
|
const { model, rmRegistry } = useChatContext();
|
|
47
|
-
const appendContent = props.appendContent || false;
|
|
48
|
-
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
|
|
49
|
-
null
|
|
50
|
-
);
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
|
|
53
|
+
// Allow edition only on text messages.
|
|
54
|
+
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
55
|
+
|
|
56
|
+
// Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
53
57
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState<
|
|
54
58
|
Array<[HTMLDivElement, CodeToolbarProps]>
|
|
55
59
|
>([]);
|
|
56
60
|
|
|
57
61
|
useEffect(() => {
|
|
62
|
+
let node: HTMLElement | null = null;
|
|
63
|
+
const container = containerRef.current;
|
|
64
|
+
if (!container) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
const renderContent = async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
let isMarkdownRenderer = true;
|
|
70
|
+
let renderer: IRenderMime.IRenderer;
|
|
71
|
+
let mimeModel: IRenderMime.IMimeModel;
|
|
72
|
+
|
|
73
|
+
// Create the renderer and the mime model.
|
|
74
|
+
if (typeof message.body === 'string') {
|
|
75
|
+
// Allow editing content for text messages.
|
|
76
|
+
setCanEdit(true);
|
|
77
|
+
|
|
78
|
+
// Improve users display in markdown content.
|
|
79
|
+
let mdStr = message.body;
|
|
80
|
+
message.mentions?.forEach(user => {
|
|
81
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Body is a string, use the markdown renderer.
|
|
85
|
+
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
86
|
+
mimeModel = rmRegistry.createModel({
|
|
87
|
+
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
setCanEdit(false);
|
|
91
|
+
// This is a mime bundle.
|
|
92
|
+
let mimeContent = message.body;
|
|
93
|
+
let preferred = rmRegistry.preferredMimeType(
|
|
94
|
+
mimeContent.data,
|
|
95
|
+
'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
73
96
|
);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
if (!preferred) {
|
|
98
|
+
preferred = DEFAULT_MIME_TYPE;
|
|
99
|
+
mimeContent = {
|
|
100
|
+
data: {
|
|
101
|
+
[DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
renderer = rmRegistry.createRenderer(preferred);
|
|
106
|
+
|
|
107
|
+
// Improve users display in markdown content.
|
|
108
|
+
if (preferred === DEFAULT_MIME_TYPE) {
|
|
109
|
+
let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
|
|
110
|
+
if (mdStr) {
|
|
111
|
+
message.mentions?.forEach(user => {
|
|
112
|
+
mdStr = replaceMentionToSpan(mdStr as string, user);
|
|
113
|
+
});
|
|
114
|
+
mimeContent = {
|
|
115
|
+
...mimeContent,
|
|
116
|
+
data: {
|
|
117
|
+
...mimeContent.data,
|
|
118
|
+
[DEFAULT_MIME_TYPE]: mdStr
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
isMarkdownRenderer = false;
|
|
124
|
+
}
|
|
79
125
|
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
mimeModel = rmRegistry.createModel(mimeContent);
|
|
127
|
+
}
|
|
128
|
+
await renderer.renderModel(mimeModel);
|
|
129
|
+
|
|
130
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
131
|
+
// never been attached, only the node.
|
|
132
|
+
// This is necessary to render latex.
|
|
133
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
134
|
+
|
|
135
|
+
// Add code toolbar if markdown has been rendered.
|
|
136
|
+
if (isMarkdownRenderer) {
|
|
137
|
+
const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
|
|
138
|
+
|
|
139
|
+
// Attach CodeToolbar root element to each <pre> block
|
|
140
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
141
|
+
preBlocks.forEach(preBlock => {
|
|
142
|
+
const codeToolbarRoot = document.createElement('div');
|
|
143
|
+
preBlock.parentNode?.insertBefore(
|
|
144
|
+
codeToolbarRoot,
|
|
145
|
+
preBlock.nextSibling
|
|
146
|
+
);
|
|
147
|
+
newCodeToolbarDefns.push([
|
|
148
|
+
codeToolbarRoot,
|
|
149
|
+
{ model: model, content: preBlock.textContent || '' }
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add the rendered node to the DOM.
|
|
157
|
+
node = renderer.node;
|
|
158
|
+
container.insertBefore(node, container.firstChild);
|
|
82
159
|
|
|
83
160
|
// Resolve the rendered promise.
|
|
84
161
|
props.rendered.resolve();
|
|
85
162
|
};
|
|
86
163
|
|
|
87
164
|
renderContent();
|
|
88
|
-
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
if (node && container.contains(node)) {
|
|
168
|
+
container.removeChild(node);
|
|
169
|
+
}
|
|
170
|
+
node = null;
|
|
171
|
+
};
|
|
172
|
+
}, [message.body, message.mentions, rmRegistry]);
|
|
89
173
|
|
|
90
174
|
return (
|
|
91
|
-
<div className={
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<div ref={node => node && node.replaceChildren(renderedContent)} />
|
|
97
|
-
))}
|
|
98
|
-
<MessageToolbar edit={props.edit} delete={props.delete} />
|
|
175
|
+
<div className={RENDERED_CLASS} ref={containerRef}>
|
|
176
|
+
<MessageToolbar
|
|
177
|
+
edit={canEdit ? props.edit : undefined}
|
|
178
|
+
delete={props.delete}
|
|
179
|
+
/>
|
|
99
180
|
{
|
|
100
181
|
// Render a `CodeToolbar` element underneath each code block.
|
|
101
182
|
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
@@ -85,7 +85,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
85
85
|
|
|
86
86
|
// Create an input model only if the message is edited.
|
|
87
87
|
const startEdition = (): void => {
|
|
88
|
-
if (!canEdit) {
|
|
88
|
+
if (!canEdit || !(typeof message.body === 'string')) {
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
let body = message.body;
|
|
@@ -157,7 +157,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
157
157
|
/>
|
|
158
158
|
) : (
|
|
159
159
|
<MessageRenderer
|
|
160
|
-
|
|
160
|
+
message={message}
|
|
161
161
|
edit={canEdit ? startEdition : undefined}
|
|
162
162
|
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
163
163
|
rendered={props.renderedPromise}
|
|
@@ -21,7 +21,7 @@ export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
|
|
|
21
21
|
if (props.edit !== undefined) {
|
|
22
22
|
const editButton = (
|
|
23
23
|
<TooltippedIconButton
|
|
24
|
-
tooltip={'
|
|
24
|
+
tooltip={'Edit'}
|
|
25
25
|
onClick={props.edit}
|
|
26
26
|
aria-label={'Edit'}
|
|
27
27
|
inputToolbar={false}
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
7
|
+
import { Widget } from '@lumino/widgets';
|
|
7
8
|
import React, { useEffect, useRef } from 'react';
|
|
8
9
|
|
|
9
10
|
import { useChatContext } from '../../context';
|
|
10
|
-
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
11
11
|
|
|
12
12
|
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
|
|
13
|
+
const MD_MIME_TYPE = 'text/markdown';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* The component props.
|
|
@@ -32,33 +33,40 @@ export function WelcomeMessage(props: IWelcomeMessageProps): JSX.Element {
|
|
|
32
33
|
|
|
33
34
|
// ref that tracks the content container to store the rendermime node in
|
|
34
35
|
const renderingContainer = useRef<HTMLDivElement | null>(null);
|
|
35
|
-
// ref that tracks whether the rendermime node has already been inserted
|
|
36
|
-
const renderingInserted = useRef<boolean>(false);
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
|
|
40
39
|
* and insert it into `renderingContainer` if not yet inserted.
|
|
41
40
|
*/
|
|
42
41
|
useEffect(() => {
|
|
42
|
+
let node: HTMLElement | null = null;
|
|
43
|
+
|
|
43
44
|
const renderContent = async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Render the welcome message using markdown renderer.
|
|
46
|
+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
47
|
+
const mimeModel = rmRegistry.createModel({
|
|
48
|
+
data: { [MD_MIME_TYPE]: content }
|
|
47
49
|
});
|
|
50
|
+
await renderer.renderModel(mimeModel);
|
|
48
51
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
53
|
+
// never been attached, only the node.
|
|
54
|
+
// This is necessary to render latex.
|
|
55
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
56
|
+
|
|
57
|
+
node = renderer.node;
|
|
58
|
+
renderingContainer.current?.append(node);
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
renderContent();
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
if (node && renderingContainer.current?.contains(node)) {
|
|
65
|
+
renderingContainer.current.removeChild(node);
|
|
66
|
+
}
|
|
67
|
+
node = null;
|
|
68
|
+
};
|
|
57
69
|
}, [content]);
|
|
58
70
|
|
|
59
|
-
return
|
|
60
|
-
<div className={classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS)}>
|
|
61
|
-
<div ref={renderingContainer} />
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
71
|
+
return <div className={WELCOME_MESSAGE_CLASS} ref={renderingContainer}></div>;
|
|
64
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ export * from './active-cell-manager';
|
|
|
7
7
|
export * from './components';
|
|
8
8
|
export * from './icons';
|
|
9
9
|
export * from './input-model';
|
|
10
|
-
export * from './markdown-renderer';
|
|
11
10
|
export * from './model';
|
|
12
11
|
export * from './registers';
|
|
13
12
|
export * from './selection-watcher';
|
package/src/message.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
6
7
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
7
8
|
import { IAttachment, IMessageContent, IMessage, IUser } from './types';
|
|
8
9
|
|
|
@@ -32,7 +33,9 @@ export class Message implements IMessage {
|
|
|
32
33
|
get type(): string {
|
|
33
34
|
return this._content.type;
|
|
34
35
|
}
|
|
35
|
-
get body():
|
|
36
|
+
get body():
|
|
37
|
+
| string
|
|
38
|
+
| (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>) {
|
|
36
39
|
return this._content.body;
|
|
37
40
|
}
|
|
38
41
|
get id(): string {
|
package/src/model.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
INewMessage,
|
|
23
23
|
IUser
|
|
24
24
|
} from './types';
|
|
25
|
-
import { replaceMentionToSpan } from './utils';
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* The chat model interface.
|
|
@@ -535,9 +534,6 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
535
534
|
* Can be useful if some actions are required on the message.
|
|
536
535
|
*/
|
|
537
536
|
protected formatChatMessage(message: IMessageContent): IMessageContent {
|
|
538
|
-
message.mentions?.forEach(user => {
|
|
539
|
-
message.body = replaceMentionToSpan(message.body, user);
|
|
540
|
-
});
|
|
541
537
|
return message;
|
|
542
538
|
}
|
|
543
539
|
|
package/src/theme-provider.ts
CHANGED
|
@@ -40,8 +40,9 @@ export async function pollUntilReady(): Promise<void> {
|
|
|
40
40
|
|
|
41
41
|
export async function getJupyterLabTheme(): Promise<Theme> {
|
|
42
42
|
await pollUntilReady();
|
|
43
|
-
const light = document.body.getAttribute('data-jp-theme-light');
|
|
43
|
+
const light = document.body.getAttribute('data-jp-theme-light') === 'true';
|
|
44
44
|
return createTheme({
|
|
45
|
+
cssVariables: true,
|
|
45
46
|
spacing: 4,
|
|
46
47
|
components: {
|
|
47
48
|
MuiButton: {
|
|
@@ -65,12 +66,12 @@ export async function getJupyterLabTheme(): Promise<Theme> {
|
|
|
65
66
|
// The default style for input toolbar button variant.
|
|
66
67
|
props: { variant: 'input-toolbar' },
|
|
67
68
|
style: {
|
|
68
|
-
backgroundColor:
|
|
69
|
-
color: '
|
|
69
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
70
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
70
71
|
borderRadius: '4px',
|
|
71
72
|
boxShadow: 'none',
|
|
72
73
|
'&:hover': {
|
|
73
|
-
backgroundColor:
|
|
74
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
74
75
|
boxShadow: 'none'
|
|
75
76
|
},
|
|
76
77
|
'&:disabled': {
|
|
@@ -122,12 +123,12 @@ export async function getJupyterLabTheme(): Promise<Theme> {
|
|
|
122
123
|
// The default style for input toolbar button variant.
|
|
123
124
|
props: { variant: 'input-toolbar' },
|
|
124
125
|
style: {
|
|
125
|
-
backgroundColor:
|
|
126
|
-
color: '
|
|
126
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
127
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
127
128
|
borderRadius: '4px',
|
|
128
129
|
boxShadow: 'none',
|
|
129
130
|
'&:hover': {
|
|
130
|
-
backgroundColor:
|
|
131
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
131
132
|
boxShadow: 'none'
|
|
132
133
|
},
|
|
133
134
|
'&:disabled': {
|
|
@@ -187,9 +188,9 @@ export async function getJupyterLabTheme(): Promise<Theme> {
|
|
|
187
188
|
paper: getCSSVariable('--jp-layout-color1'),
|
|
188
189
|
default: getCSSVariable('--jp-layout-color1')
|
|
189
190
|
},
|
|
190
|
-
mode: light
|
|
191
|
+
mode: light ? 'light' : 'dark',
|
|
191
192
|
primary: {
|
|
192
|
-
main: getCSSVariable(
|
|
193
|
+
main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
|
|
193
194
|
light: getCSSVariable('--jp-brand-color2'),
|
|
194
195
|
dark: getCSSVariable('--jp-brand-color0')
|
|
195
196
|
},
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ISignal } from '@lumino/signaling';
|
|
7
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The user description.
|
|
@@ -65,7 +66,10 @@ export interface IConfig {
|
|
|
65
66
|
*/
|
|
66
67
|
export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
67
68
|
type: string;
|
|
68
|
-
body:
|
|
69
|
+
body:
|
|
70
|
+
| string
|
|
71
|
+
// Should contain at least the data of the mime model.
|
|
72
|
+
| (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
|
|
69
73
|
id: string;
|
|
70
74
|
time: number;
|
|
71
75
|
sender: T;
|
package/style/chat.css
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
.jp-chat-rendered-
|
|
6
|
+
.jp-chat-rendered-message {
|
|
7
7
|
position: relative;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.jp-chat-rendered-
|
|
10
|
+
.jp-chat-rendered-message hr {
|
|
11
11
|
color: #00000026;
|
|
12
12
|
background-color: transparent;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.jp-chat-
|
|
15
|
+
.jp-chat-message .jp-RenderedHTMLCommon > :last-child {
|
|
16
16
|
margin-bottom: 0;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -23,15 +23,20 @@
|
|
|
23
23
|
* See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
|
|
24
24
|
* See also: https://github.com/jupyterlab/jupyter-ai/issues/1090
|
|
25
25
|
*/
|
|
26
|
-
.jp-ThemedContainer .jp-chat-
|
|
26
|
+
.jp-ThemedContainer .jp-chat-welcome-message {
|
|
27
27
|
padding: 0 1em;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.jp-ThemedContainer .jp-chat-rendered-
|
|
30
|
+
.jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedHTMLCommon {
|
|
31
31
|
padding-right: 0;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
.jp-ThemedContainer .jp-chat-rendered-
|
|
34
|
+
.jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedJSON {
|
|
35
|
+
padding-left: 5px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedMarkdown pre,
|
|
39
|
+
.jp-ThemedContainer .jp-chat-welcome-message .jp-RenderedMarkdown pre {
|
|
35
40
|
background-color: var(--jp-cell-editor-background);
|
|
36
41
|
overflow-x: auto;
|
|
37
42
|
white-space: pre;
|
|
@@ -40,13 +45,13 @@
|
|
|
40
45
|
border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
.jp-ThemedContainer .jp-
|
|
48
|
+
.jp-ThemedContainer .jp-RenderedMarkdown pre > code {
|
|
44
49
|
background-color: inherit;
|
|
45
50
|
overflow-x: inherit;
|
|
46
51
|
white-space: inherit;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
.jp-ThemedContainer .jp-
|
|
54
|
+
.jp-ThemedContainer .jp-RenderedHTMLCommon mjx-container {
|
|
50
55
|
font-size: 119%;
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -64,7 +69,7 @@
|
|
|
64
69
|
color: var(--jp-ui-font-color2);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
.jp-chat-rendered-
|
|
72
|
+
.jp-chat-rendered-message:hover .jp-chat-toolbar {
|
|
68
73
|
visibility: visible;
|
|
69
74
|
}
|
|
70
75
|
|
package/style/input.css
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
position: absolute;
|
|
21
21
|
inset: 0;
|
|
22
22
|
background: rgb(33 150 243 / 10%);
|
|
23
|
-
border: 2px dashed var(--
|
|
23
|
+
border: 2px dashed var(--mui-palette-primary-main);
|
|
24
24
|
border-radius: 4px;
|
|
25
25
|
pointer-events: none;
|
|
26
26
|
z-index: 1;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
top: -24px;
|
|
33
33
|
left: 50%;
|
|
34
34
|
transform: translateX(-50%);
|
|
35
|
-
color: var(--
|
|
35
|
+
color: var(--mui-palette-primary-main);
|
|
36
36
|
font-size: 12px;
|
|
37
37
|
font-weight: 500;
|
|
38
38
|
pointer-events: none;
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.jp-chat-input-container:focus-within > div:first-of-type {
|
|
50
|
-
border-color: var(--
|
|
50
|
+
border-color: var(--mui-palette-primary-main);
|
|
51
51
|
}
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/markdown-renderer.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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/src/markdown-renderer.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|