@jupyter/chat 0.6.2 → 0.7.1
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-input.js +12 -6
- package/lib/components/chat-messages.d.ts +10 -9
- package/lib/components/chat-messages.js +77 -63
- package/lib/components/chat.js +5 -2
- package/lib/components/code-blocks/index.d.ts +2 -0
- package/lib/components/code-blocks/index.js +6 -0
- package/lib/components/index.d.ts +10 -0
- package/lib/components/index.js +14 -0
- package/lib/components/input/cancel-button.d.ts +0 -1
- package/lib/components/input/cancel-button.js +1 -2
- package/lib/components/input/index.d.ts +2 -0
- package/lib/components/input/index.js +6 -0
- package/lib/components/markdown-renderer.d.ts +37 -0
- package/lib/components/{rendermime-markdown.js → markdown-renderer.js} +10 -8
- package/lib/components/mui-extras/index.d.ts +3 -0
- package/lib/components/mui-extras/index.js +7 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/model.d.ts +4 -0
- package/lib/model.js +3 -0
- package/lib/types.d.ts +0 -4
- package/package.json +2 -1
- package/src/components/chat-input.tsx +14 -11
- package/src/components/chat-messages.tsx +151 -129
- package/src/components/chat.tsx +3 -0
- package/src/components/code-blocks/index.ts +7 -0
- package/src/components/index.ts +15 -0
- package/src/components/input/cancel-button.tsx +0 -3
- package/src/components/input/index.ts +7 -0
- package/src/components/{rendermime-markdown.tsx → markdown-renderer.tsx} +36 -10
- package/src/components/mui-extras/index.ts +8 -0
- package/src/index.ts +1 -0
- package/src/model.ts +9 -0
- package/src/types.ts +0 -4
- package/style/chat.css +14 -6
- package/lib/components/mui-extras/stacking-alert.d.ts +0 -28
- package/lib/components/mui-extras/stacking-alert.js +0 -56
- package/lib/components/rendermime-markdown.d.ts +0 -14
- package/src/components/mui-extras/stacking-alert.tsx +0 -105
|
@@ -49,6 +49,7 @@ export function ChatInput(props) {
|
|
|
49
49
|
const [highlighted, setHighlighted] = useState(false);
|
|
50
50
|
// controls whether the slash command autocomplete is open
|
|
51
51
|
const [open, setOpen] = useState(false);
|
|
52
|
+
const inputExists = !!input.trim();
|
|
52
53
|
/**
|
|
53
54
|
* Effect: fetch the list of available autocomplete commands.
|
|
54
55
|
*/
|
|
@@ -96,14 +97,19 @@ export function ChatInput(props) {
|
|
|
96
97
|
if (event.key !== 'Enter') {
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
|
-
//
|
|
100
|
+
// Do not send the message if the user was selecting a suggested command from the
|
|
100
101
|
// Autocomplete component.
|
|
101
102
|
if (highlighted) {
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
// Do not send empty messages, and avoid adding new line in empty message.
|
|
106
|
+
if (!inputExists) {
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if ((sendWithShiftEnter && event.shiftKey) ||
|
|
112
|
+
(!sendWithShiftEnter && !event.shiftKey)) {
|
|
107
113
|
onSend();
|
|
108
114
|
event.stopPropagation();
|
|
109
115
|
event.preventDefault();
|
|
@@ -167,8 +173,8 @@ ${selection.source}
|
|
|
167
173
|
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, InputProps: {
|
|
168
174
|
...params.InputProps,
|
|
169
175
|
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
|
170
|
-
props.onCancel &&
|
|
171
|
-
React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists:
|
|
176
|
+
props.onCancel && React.createElement(CancelButton, { onCancel: onCancel }),
|
|
177
|
+
React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
|
|
172
178
|
}, FormHelperTextProps: {
|
|
173
179
|
sx: { marginLeft: 'auto', marginRight: 0 }
|
|
174
180
|
}, helperText: input.length > 2 ? helperText : ' ' })), ...(_d = autocompletion.current) === null || _d === void 0 ? void 0 : _d.props, inputValue: input, onInputChange: (_, newValue) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
3
|
import type { SxProps, Theme } from '@mui/material';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
import { IChatModel } from '../model';
|
|
@@ -26,9 +27,9 @@ type ChatMessageHeaderProps = {
|
|
|
26
27
|
*/
|
|
27
28
|
export declare function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element;
|
|
28
29
|
/**
|
|
29
|
-
* The message component
|
|
30
|
+
* The message component body.
|
|
30
31
|
*/
|
|
31
|
-
|
|
32
|
+
export declare const ChatMessage: React.ForwardRefExoticComponent<BaseMessageProps & {
|
|
32
33
|
/**
|
|
33
34
|
* The message to display.
|
|
34
35
|
*/
|
|
@@ -38,14 +39,10 @@ type ChatMessageProps = BaseMessageProps & {
|
|
|
38
39
|
*/
|
|
39
40
|
index: number;
|
|
40
41
|
/**
|
|
41
|
-
* The
|
|
42
|
+
* The promise to resolve when the message is rendered.
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The message component body.
|
|
47
|
-
*/
|
|
48
|
-
export declare function ChatMessage(props: ChatMessageProps): JSX.Element;
|
|
44
|
+
renderedPromise: PromiseDelegate<void>;
|
|
45
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
49
46
|
/**
|
|
50
47
|
* The writers component props.
|
|
51
48
|
*/
|
|
@@ -67,6 +64,10 @@ type NavigationProps = BaseMessageProps & {
|
|
|
67
64
|
* The reference to the messages container.
|
|
68
65
|
*/
|
|
69
66
|
refMsgBox: React.RefObject<HTMLDivElement>;
|
|
67
|
+
/**
|
|
68
|
+
* Whether all the messages has been rendered once on first display.
|
|
69
|
+
*/
|
|
70
|
+
allRendered: boolean;
|
|
70
71
|
};
|
|
71
72
|
/**
|
|
72
73
|
* The navigation component, to navigate to unread messages.
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Button } from '@jupyter/react-components';
|
|
6
6
|
import { LabIcon, caretDownEmptyIcon, classes } from '@jupyterlab/ui-components';
|
|
7
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
8
|
import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
|
|
8
9
|
import clsx from 'clsx';
|
|
9
|
-
import React, { useEffect, useState, useRef } from 'react';
|
|
10
|
+
import React, { useEffect, useState, useRef, forwardRef } from 'react';
|
|
10
11
|
import { ChatInput } from './chat-input';
|
|
11
|
-
import {
|
|
12
|
+
import { MarkdownRenderer } from './markdown-renderer';
|
|
12
13
|
import { ScrollContainer } from './scroll-container';
|
|
13
14
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
14
15
|
const MESSAGE_CLASS = 'jp-chat-message';
|
|
@@ -27,10 +28,11 @@ export function ChatMessages(props) {
|
|
|
27
28
|
const { model } = props;
|
|
28
29
|
const [messages, setMessages] = useState(model.messages);
|
|
29
30
|
const refMsgBox = useRef(null);
|
|
30
|
-
const inViewport = useRef([]);
|
|
31
31
|
const [currentWriters, setCurrentWriters] = useState([]);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const [allRendered, setAllRendered] = useState(false);
|
|
33
|
+
// The list of message DOM and their rendered promises.
|
|
34
|
+
const listRef = useRef([]);
|
|
35
|
+
const renderedPromise = useRef([]);
|
|
34
36
|
/**
|
|
35
37
|
* Effect: fetch history and config on initial render
|
|
36
38
|
*/
|
|
@@ -67,51 +69,76 @@ export function ChatMessages(props) {
|
|
|
67
69
|
};
|
|
68
70
|
}, [model]);
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
72
|
+
* Observe the messages to update the current viewport and the unread messages.
|
|
71
73
|
*/
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
let unreadModified = false;
|
|
75
|
-
entries.forEach(entry => {
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const observer = new IntersectionObserver(entries => {
|
|
76
76
|
var _a;
|
|
77
|
-
|
|
78
|
-
if (!
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Used on first rendering, to ensure all the message as been rendered once.
|
|
78
|
+
if (!allRendered) {
|
|
79
|
+
Promise.all(renderedPromise.current.map(p => p.promise)).then(() => {
|
|
80
|
+
setAllRendered(true);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const unread = [...model.unreadMessages];
|
|
84
|
+
let unreadModified = false;
|
|
85
|
+
const inViewport = [...((_a = model.messagesInViewport) !== null && _a !== void 0 ? _a : [])];
|
|
86
|
+
entries.forEach(entry => {
|
|
87
|
+
var _a;
|
|
88
|
+
const index = parseInt((_a = entry.target.getAttribute('data-index')) !== null && _a !== void 0 ? _a : '');
|
|
89
|
+
if (!isNaN(index)) {
|
|
90
|
+
const viewportIdx = inViewport.indexOf(index);
|
|
91
|
+
if (!entry.isIntersecting && viewportIdx !== -1) {
|
|
92
|
+
inViewport.splice(viewportIdx, 1);
|
|
93
|
+
}
|
|
94
|
+
else if (entry.isIntersecting && viewportIdx === -1) {
|
|
95
|
+
inViewport.push(index);
|
|
96
|
+
}
|
|
97
|
+
if (unread.length) {
|
|
98
|
+
const unreadIdx = unread.indexOf(index);
|
|
99
|
+
if (unreadIdx !== -1 && entry.isIntersecting) {
|
|
100
|
+
unread.splice(unreadIdx, 1);
|
|
101
|
+
unreadModified = true;
|
|
102
|
+
}
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
});
|
|
106
|
+
props.model.messagesInViewport = inViewport;
|
|
107
|
+
// Ensure that all messages are rendered before updating unread messages, otherwise
|
|
108
|
+
// it can lead to wrong assumption , because more message are in the viewport
|
|
109
|
+
// before they are rendered.
|
|
110
|
+
if (allRendered && unreadModified) {
|
|
111
|
+
model.unreadMessages = unread;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* Observe the messages.
|
|
116
|
+
*/
|
|
117
|
+
listRef.current.forEach(item => {
|
|
118
|
+
if (item) {
|
|
119
|
+
observer.observe(item);
|
|
93
120
|
}
|
|
94
121
|
});
|
|
95
|
-
props.model.messagesInViewport = inViewport.current;
|
|
96
|
-
if (unreadModified) {
|
|
97
|
-
props.model.unreadMessages = unread;
|
|
98
|
-
}
|
|
99
122
|
return () => {
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
listRef.current.forEach(item => {
|
|
124
|
+
if (item) {
|
|
125
|
+
observer.unobserve(item);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
102
128
|
};
|
|
103
|
-
}
|
|
129
|
+
}, [messages, allRendered]);
|
|
104
130
|
return (React.createElement(React.Fragment, null,
|
|
105
131
|
React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
|
|
106
132
|
React.createElement(Box, { ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, messages.map((message, i) => {
|
|
133
|
+
renderedPromise.current[i] = new PromiseDelegate();
|
|
107
134
|
return (
|
|
108
135
|
// extra div needed to ensure each bubble is on a new line
|
|
109
136
|
React.createElement(Box, { key: i, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
110
137
|
React.createElement(ChatMessageHeader, { message: message }),
|
|
111
|
-
React.createElement(ChatMessage, { ...props, message: message,
|
|
138
|
+
React.createElement(ChatMessage, { ...props, message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) })));
|
|
112
139
|
})),
|
|
113
140
|
React.createElement(Writers, { writers: currentWriters })),
|
|
114
|
-
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox })));
|
|
141
|
+
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox, allRendered: allRendered })));
|
|
115
142
|
}
|
|
116
143
|
/**
|
|
117
144
|
* The message header component.
|
|
@@ -193,28 +220,12 @@ export function ChatMessageHeader(props) {
|
|
|
193
220
|
/**
|
|
194
221
|
* The message component body.
|
|
195
222
|
*/
|
|
196
|
-
export
|
|
223
|
+
export const ChatMessage = forwardRef((props, ref) => {
|
|
197
224
|
const { message, model, rmRegistry } = props;
|
|
198
|
-
const elementRef = useRef(null);
|
|
199
225
|
const [edit, setEdit] = useState(false);
|
|
200
226
|
const [deleted, setDeleted] = useState(false);
|
|
201
227
|
const [canEdit, setCanEdit] = useState(false);
|
|
202
228
|
const [canDelete, setCanDelete] = useState(false);
|
|
203
|
-
// Add the current message to the observer, to actualize viewport and unread messages.
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
var _a;
|
|
206
|
-
if (elementRef.current === null) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
// If the observer is defined, let's observe the message.
|
|
210
|
-
(_a = props.observer) === null || _a === void 0 ? void 0 : _a.observe(elementRef.current);
|
|
211
|
-
return () => {
|
|
212
|
-
var _a;
|
|
213
|
-
if (elementRef.current !== null) {
|
|
214
|
-
(_a = props.observer) === null || _a === void 0 ? void 0 : _a.unobserve(elementRef.current);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
}, [model]);
|
|
218
229
|
// Look if the message can be deleted or edited.
|
|
219
230
|
useEffect(() => {
|
|
220
231
|
var _a;
|
|
@@ -253,8 +264,8 @@ export function ChatMessage(props) {
|
|
|
253
264
|
model.deleteMessage(id);
|
|
254
265
|
};
|
|
255
266
|
// Empty if the message has been deleted.
|
|
256
|
-
return deleted ? (React.createElement("div", { ref:
|
|
257
|
-
}
|
|
267
|
+
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index }, edit && canEdit ? (React.createElement(ChatInput, { value: message.body, onSend: (input) => updateMessage(message.id, input), onCancel: () => cancelEdition(), model: model, hideIncludeSelection: true })) : (React.createElement(MarkdownRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise }))));
|
|
268
|
+
});
|
|
258
269
|
/**
|
|
259
270
|
* The writers component, displaying the current writers.
|
|
260
271
|
*/
|
|
@@ -282,14 +293,20 @@ export function Navigation(props) {
|
|
|
282
293
|
const [lastInViewport, setLastInViewport] = useState(true);
|
|
283
294
|
const [unreadBefore, setUnreadBefore] = useState(null);
|
|
284
295
|
const [unreadAfter, setUnreadAfter] = useState(null);
|
|
285
|
-
const gotoMessage = (msgIdx) => {
|
|
296
|
+
const gotoMessage = (msgIdx, alignToTop = true) => {
|
|
286
297
|
var _a, _b;
|
|
287
|
-
(_b = (_a = props.refMsgBox.current) === null || _a === void 0 ? void 0 : _a.children.item(msgIdx)) === null || _b === void 0 ? void 0 : _b.scrollIntoView();
|
|
298
|
+
(_b = (_a = props.refMsgBox.current) === null || _a === void 0 ? void 0 : _a.children.item(msgIdx)) === null || _b === void 0 ? void 0 : _b.scrollIntoView(alignToTop);
|
|
288
299
|
};
|
|
289
300
|
// Listen for change in unread messages, and find the first unread message before or
|
|
290
301
|
// after the current viewport, to display navigation buttons.
|
|
291
302
|
useEffect(() => {
|
|
292
303
|
var _a;
|
|
304
|
+
// Do not attempt to display navigation until messages are rendered, it can lead to
|
|
305
|
+
// wrong assumption, because more messages are in the viewport before they are
|
|
306
|
+
// rendered.
|
|
307
|
+
if (!props.allRendered) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
293
310
|
const unreadChanged = (model, unreadIndexes) => {
|
|
294
311
|
const viewport = model.messagesInViewport;
|
|
295
312
|
if (!viewport) {
|
|
@@ -323,18 +340,13 @@ export function Navigation(props) {
|
|
|
323
340
|
};
|
|
324
341
|
(_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.connect(unreadChanged);
|
|
325
342
|
unreadChanged(model, model.unreadMessages);
|
|
326
|
-
// Move to
|
|
327
|
-
|
|
328
|
-
gotoMessage(Math.min(...model.unreadMessages));
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
gotoMessage(model.messages.length - 1);
|
|
332
|
-
}
|
|
343
|
+
// Move to the last the message after all the messages have been first rendered.
|
|
344
|
+
gotoMessage(model.messages.length - 1, false);
|
|
333
345
|
return () => {
|
|
334
346
|
var _a;
|
|
335
347
|
(_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.disconnect(unreadChanged);
|
|
336
348
|
};
|
|
337
|
-
}, [model]);
|
|
349
|
+
}, [model, props.allRendered]);
|
|
338
350
|
// Listen for change in the viewport, to add a navigation button if the last is not
|
|
339
351
|
// in viewport.
|
|
340
352
|
useEffect(() => {
|
|
@@ -352,7 +364,9 @@ export function Navigation(props) {
|
|
|
352
364
|
return (React.createElement(React.Fragment, null,
|
|
353
365
|
unreadBefore !== null && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`, onClick: () => gotoMessage(unreadBefore), title: 'Go to unread messages' },
|
|
354
366
|
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') }))),
|
|
355
|
-
(unreadAfter !== null || !lastInViewport) && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`, onClick:
|
|
367
|
+
(unreadAfter !== null || !lastInViewport) && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`, onClick: unreadAfter === null
|
|
368
|
+
? () => gotoMessage(model.messages.length - 1, false)
|
|
369
|
+
: () => gotoMessage(unreadAfter), title: unreadAfter !== null
|
|
356
370
|
? 'Go to unread messages'
|
|
357
371
|
: 'Go to last message' },
|
|
358
372
|
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') })))));
|
package/lib/components/chat.js
CHANGED
|
@@ -33,9 +33,12 @@ export function Chat(props) {
|
|
|
33
33
|
const [view, setView] = useState(props.chatView || Chat.View.chat);
|
|
34
34
|
return (React.createElement(JlThemeProvider, { themeManager: (_a = props.themeManager) !== null && _a !== void 0 ? _a : null },
|
|
35
35
|
React.createElement(Box
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// Add .jp-ThemedContainer for CSS compatibility in both JL <4.3.0 and >=4.3.0.
|
|
37
|
+
// See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
|
|
38
38
|
, {
|
|
39
|
+
// Add .jp-ThemedContainer for CSS compatibility in both JL <4.3.0 and >=4.3.0.
|
|
40
|
+
// See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
|
|
41
|
+
className: "jp-ThemedContainer",
|
|
39
42
|
// root box should not include padding as it offsets the vertical
|
|
40
43
|
// scrollbar to the left
|
|
41
44
|
sx: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './chat';
|
|
2
|
+
export * from './chat-input';
|
|
3
|
+
export * from './chat-messages';
|
|
4
|
+
export * from './code-blocks';
|
|
5
|
+
export * from './input';
|
|
6
|
+
export * from './jl-theme-provider';
|
|
7
|
+
export * from './markdown-renderer';
|
|
8
|
+
export * from './mui-extras';
|
|
9
|
+
export * from './scroll-container';
|
|
10
|
+
export * from './toolbar';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export * from './chat';
|
|
6
|
+
export * from './chat-input';
|
|
7
|
+
export * from './chat-messages';
|
|
8
|
+
export * from './code-blocks';
|
|
9
|
+
export * from './input';
|
|
10
|
+
export * from './jl-theme-provider';
|
|
11
|
+
export * from './markdown-renderer';
|
|
12
|
+
export * from './mui-extras';
|
|
13
|
+
export * from './scroll-container';
|
|
14
|
+
export * from './toolbar';
|
|
@@ -11,8 +11,7 @@ const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
|
11
11
|
*/
|
|
12
12
|
export function CancelButton(props) {
|
|
13
13
|
const tooltip = 'Cancel edition';
|
|
14
|
-
|
|
15
|
-
return (React.createElement(TooltippedButton, { onClick: props.onCancel, disabled: disabled, tooltip: tooltip, buttonProps: {
|
|
14
|
+
return (React.createElement(TooltippedButton, { onClick: props.onCancel, tooltip: tooltip, buttonProps: {
|
|
16
15
|
size: 'small',
|
|
17
16
|
variant: 'contained',
|
|
18
17
|
title: tooltip,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { IChatModel } from '../model';
|
|
5
|
+
type MarkdownRendererProps = {
|
|
6
|
+
/**
|
|
7
|
+
* The string to render.
|
|
8
|
+
*/
|
|
9
|
+
markdownStr: string;
|
|
10
|
+
/**
|
|
11
|
+
* The rendermime registry.
|
|
12
|
+
*/
|
|
13
|
+
rmRegistry: IRenderMimeRegistry;
|
|
14
|
+
/**
|
|
15
|
+
* The model of the chat.
|
|
16
|
+
*/
|
|
17
|
+
model: IChatModel;
|
|
18
|
+
/**
|
|
19
|
+
* The promise to resolve when the message is rendered.
|
|
20
|
+
*/
|
|
21
|
+
rendered: PromiseDelegate<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether to append the content to the existing content or not.
|
|
24
|
+
*/
|
|
25
|
+
appendContent?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The function to call to edit a message.
|
|
28
|
+
*/
|
|
29
|
+
edit?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* the function to call to delete a message.
|
|
32
|
+
*/
|
|
33
|
+
delete?: () => void;
|
|
34
|
+
};
|
|
35
|
+
declare function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element;
|
|
36
|
+
export declare const MarkdownRenderer: React.MemoExoticComponent<typeof MarkdownRendererBase>;
|
|
37
|
+
export {};
|
|
@@ -7,7 +7,7 @@ import { createPortal } from 'react-dom';
|
|
|
7
7
|
import { CodeToolbar } from './code-blocks/code-toolbar';
|
|
8
8
|
import { MessageToolbar } from './toolbar';
|
|
9
9
|
const MD_MIME_TYPE = 'text/markdown';
|
|
10
|
-
const
|
|
10
|
+
const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
|
|
11
11
|
/**
|
|
12
12
|
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
|
|
13
13
|
* after the initial MarkDown render. For example, this function takes '\(` and
|
|
@@ -18,12 +18,12 @@ const RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
|
|
|
18
18
|
*/
|
|
19
19
|
function escapeLatexDelimiters(text) {
|
|
20
20
|
return text
|
|
21
|
-
.replace(
|
|
22
|
-
.replace(
|
|
23
|
-
.replace(
|
|
24
|
-
.replace(
|
|
21
|
+
.replace(/\\\(/g, '\\\\(')
|
|
22
|
+
.replace(/\\\)/g, '\\\\)')
|
|
23
|
+
.replace(/\\\[/g, '\\\\[')
|
|
24
|
+
.replace(/\\\]/g, '\\\\]');
|
|
25
25
|
}
|
|
26
|
-
function
|
|
26
|
+
function MarkdownRendererBase(props) {
|
|
27
27
|
const appendContent = props.appendContent || false;
|
|
28
28
|
const [renderedContent, setRenderedContent] = useState(null);
|
|
29
29
|
// each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
@@ -59,10 +59,12 @@ function RendermimeMarkdownBase(props) {
|
|
|
59
59
|
});
|
|
60
60
|
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
61
61
|
setRenderedContent(renderer.node);
|
|
62
|
+
// Resolve the rendered promise.
|
|
63
|
+
props.rendered.resolve();
|
|
62
64
|
};
|
|
63
65
|
renderContent();
|
|
64
66
|
}, [props.markdownStr, props.rmRegistry]);
|
|
65
|
-
return (React.createElement("div", { className:
|
|
67
|
+
return (React.createElement("div", { className: MD_RENDERED_CLASS },
|
|
66
68
|
renderedContent &&
|
|
67
69
|
(appendContent ? (React.createElement("div", { ref: node => node && node.appendChild(renderedContent) })) : (React.createElement("div", { ref: node => node && node.replaceChildren(renderedContent) }))),
|
|
68
70
|
React.createElement(MessageToolbar, { edit: props.edit, delete: props.delete }),
|
|
@@ -74,4 +76,4 @@ function RendermimeMarkdownBase(props) {
|
|
|
74
76
|
return createPortal(React.createElement(CodeToolbar, { ...codeToolbarProps }), codeToolbarRoot);
|
|
75
77
|
})));
|
|
76
78
|
}
|
|
77
|
-
export const
|
|
79
|
+
export const MarkdownRenderer = React.memo(MarkdownRendererBase);
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/model.d.ts
CHANGED
package/lib/model.js
CHANGED
|
@@ -26,6 +26,9 @@ export class ChatModel {
|
|
|
26
26
|
this._viewportChanged = new Signal(this);
|
|
27
27
|
this._writersChanged = new Signal(this);
|
|
28
28
|
this._focusInputSignal = new Signal(this);
|
|
29
|
+
if (options.id) {
|
|
30
|
+
this.id = options.id;
|
|
31
|
+
}
|
|
29
32
|
const config = (_a = options.config) !== null && _a !== void 0 ? _a : {};
|
|
30
33
|
// Stack consecutive messages from the same user by default.
|
|
31
34
|
this._config = {
|
package/lib/types.d.ts
CHANGED
|
@@ -17,10 +17,6 @@ export interface IConfig {
|
|
|
17
17
|
* Whether to send a message via Shift-Enter instead of Enter.
|
|
18
18
|
*/
|
|
19
19
|
sendWithShiftEnter?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Last read message (no use yet).
|
|
22
|
-
*/
|
|
23
|
-
lastRead?: number;
|
|
24
20
|
/**
|
|
25
21
|
* Whether to stack consecutive messages from same user.
|
|
26
22
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"@jupyterlab/rendermime": "^4.2.0",
|
|
63
63
|
"@jupyterlab/ui-components": "^4.2.0",
|
|
64
64
|
"@lumino/commands": "^2.0.0",
|
|
65
|
+
"@lumino/coreutils": "^2.0.0",
|
|
65
66
|
"@lumino/disposable": "^2.0.0",
|
|
66
67
|
"@lumino/signaling": "^2.0.0",
|
|
67
68
|
"@mui/icons-material": "^5.11.0",
|
|
@@ -78,6 +78,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
78
78
|
// controls whether the slash command autocomplete is open
|
|
79
79
|
const [open, setOpen] = useState<boolean>(false);
|
|
80
80
|
|
|
81
|
+
const inputExists = !!input.trim();
|
|
82
|
+
|
|
81
83
|
/**
|
|
82
84
|
* Effect: fetch the list of available autocomplete commands.
|
|
83
85
|
*/
|
|
@@ -130,16 +132,22 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
//
|
|
135
|
+
// Do not send the message if the user was selecting a suggested command from the
|
|
134
136
|
// Autocomplete component.
|
|
135
137
|
if (highlighted) {
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
// Do not send empty messages, and avoid adding new line in empty message.
|
|
142
|
+
if (!inputExists) {
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
if (
|
|
140
|
-
event.
|
|
141
|
-
(
|
|
142
|
-
(!sendWithShiftEnter && !event.shiftKey))
|
|
149
|
+
(sendWithShiftEnter && event.shiftKey) ||
|
|
150
|
+
(!sendWithShiftEnter && !event.shiftKey)
|
|
143
151
|
) {
|
|
144
152
|
onSend();
|
|
145
153
|
event.stopPropagation();
|
|
@@ -224,16 +232,11 @@ ${selection.source}
|
|
|
224
232
|
...params.InputProps,
|
|
225
233
|
endAdornment: (
|
|
226
234
|
<InputAdornment position="end">
|
|
227
|
-
{props.onCancel &&
|
|
228
|
-
<CancelButton
|
|
229
|
-
inputExists={input.length > 0}
|
|
230
|
-
onCancel={onCancel}
|
|
231
|
-
/>
|
|
232
|
-
)}
|
|
235
|
+
{props.onCancel && <CancelButton onCancel={onCancel} />}
|
|
233
236
|
<SendButton
|
|
234
237
|
model={model}
|
|
235
238
|
sendWithShiftEnter={sendWithShiftEnter}
|
|
236
|
-
inputExists={
|
|
239
|
+
inputExists={inputExists}
|
|
237
240
|
onSend={onSend}
|
|
238
241
|
hideIncludeSelection={hideIncludeSelection}
|
|
239
242
|
hasButtonOnLeft={!!props.onCancel}
|