@jupyter/chat 0.6.1 → 0.7.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.
Files changed (39) hide show
  1. package/lib/components/chat-input.js +12 -6
  2. package/lib/components/chat-messages.d.ts +10 -9
  3. package/lib/components/chat-messages.js +77 -63
  4. package/lib/components/chat.js +5 -2
  5. package/lib/components/code-blocks/index.d.ts +2 -0
  6. package/lib/components/code-blocks/index.js +6 -0
  7. package/lib/components/index.d.ts +10 -0
  8. package/lib/components/index.js +14 -0
  9. package/lib/components/input/cancel-button.d.ts +0 -1
  10. package/lib/components/input/cancel-button.js +1 -2
  11. package/lib/components/input/index.d.ts +2 -0
  12. package/lib/components/input/index.js +6 -0
  13. package/lib/components/markdown-renderer.d.ts +37 -0
  14. package/lib/components/{rendermime-markdown.js → markdown-renderer.js} +10 -8
  15. package/lib/components/mui-extras/index.d.ts +3 -0
  16. package/lib/components/mui-extras/index.js +7 -0
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.js +1 -0
  19. package/lib/model.d.ts +4 -0
  20. package/lib/model.js +3 -0
  21. package/lib/types.d.ts +0 -4
  22. package/package.json +2 -1
  23. package/src/components/chat-input.tsx +14 -11
  24. package/src/components/chat-messages.tsx +151 -129
  25. package/src/components/chat.tsx +3 -0
  26. package/src/components/code-blocks/index.ts +7 -0
  27. package/src/components/index.ts +15 -0
  28. package/src/components/input/cancel-button.tsx +0 -3
  29. package/src/components/input/index.ts +7 -0
  30. package/src/components/{rendermime-markdown.tsx → markdown-renderer.tsx} +36 -10
  31. package/src/components/mui-extras/index.ts +8 -0
  32. package/src/index.ts +1 -0
  33. package/src/model.ts +9 -0
  34. package/src/types.ts +0 -4
  35. package/style/chat.css +14 -6
  36. package/lib/components/mui-extras/stacking-alert.d.ts +0 -28
  37. package/lib/components/mui-extras/stacking-alert.js +0 -56
  38. package/lib/components/rendermime-markdown.d.ts +0 -14
  39. 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
- // do not send the message if the user was selecting a suggested command from the
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
- if (event.key === 'Enter' &&
105
- ((sendWithShiftEnter && event.shiftKey) ||
106
- (!sendWithShiftEnter && !event.shiftKey))) {
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 && (React.createElement(CancelButton, { inputExists: input.length > 0, onCancel: onCancel })),
171
- React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: input.length > 0, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
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 props.
30
+ * The message component body.
30
31
  */
31
- type ChatMessageProps = BaseMessageProps & {
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 intersection observer for all the messages.
42
+ * The promise to resolve when the message is rendered.
42
43
  */
43
- observer: IntersectionObserver | null;
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 { RendermimeMarkdown } from './rendermime-markdown';
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
- // The intersection observer that listen to all the message visibility.
33
- const observerRef = useRef(new IntersectionObserver(viewportChange));
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
- * Function called when a message enter or leave the viewport.
72
+ * Observe the messages to update the current viewport and the unread messages.
71
73
  */
72
- function viewportChange(entries) {
73
- const unread = [...model.unreadMessages];
74
- let unreadModified = false;
75
- entries.forEach(entry => {
74
+ useEffect(() => {
75
+ const observer = new IntersectionObserver(entries => {
76
76
  var _a;
77
- const index = parseInt((_a = entry.target.getAttribute('data-index')) !== null && _a !== void 0 ? _a : '');
78
- if (!isNaN(index)) {
79
- if (unread.length) {
80
- const unreadIdx = unread.indexOf(index);
81
- if (unreadIdx !== -1 && entry.isIntersecting) {
82
- unread.splice(unreadIdx, 1);
83
- unreadModified = true;
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
- const viewportIdx = inViewport.current.indexOf(index);
87
- if (!entry.isIntersecting && viewportIdx !== -1) {
88
- inViewport.current.splice(viewportIdx, 1);
89
- }
90
- else if (entry.isIntersecting && viewportIdx === -1) {
91
- inViewport.current.push(index);
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
- var _a;
101
- (_a = observerRef.current) === null || _a === void 0 ? void 0 : _a.disconnect();
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, observer: observerRef.current, index: i })));
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 function ChatMessage(props) {
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: elementRef, "data-index": props.index })) : (React.createElement("div", { ref: elementRef, "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(RendermimeMarkdown, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined }))));
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 first the unread message or to last message on first rendering.
327
- if (model.unreadMessages.length) {
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: () => gotoMessage(unreadAfter !== null ? unreadAfter : model.messages.length - 1), title: unreadAfter !== null
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') })))));
@@ -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
- // root box should not include padding as it offsets the vertical
37
- // scrollbar to the left
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,2 @@
1
+ export * from './code-toolbar';
2
+ export * from './copy-button';
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './code-toolbar';
6
+ export * from './copy-button';
@@ -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';
@@ -3,7 +3,6 @@
3
3
  * The cancel button props.
4
4
  */
5
5
  export type CancelButtonProps = {
6
- inputExists: boolean;
7
6
  onCancel: () => void;
8
7
  };
9
8
  /**
@@ -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
- const disabled = !props.inputExists;
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,2 @@
1
+ export * from './cancel-button';
2
+ export * from './send-button';
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './cancel-button';
6
+ export * from './send-button';
@@ -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 RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
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('\\(/g', '\\\\(')
22
- .replace('\\)/g', '\\\\)')
23
- .replace('\\[/g', '\\\\[')
24
- .replace('\\]/g', '\\\\]');
21
+ .replace(/\\\(/g, '\\\\(')
22
+ .replace(/\\\)/g, '\\\\)')
23
+ .replace(/\\\[/g, '\\\\[')
24
+ .replace(/\\\]/g, '\\\\]');
25
25
  }
26
- function RendermimeMarkdownBase(props) {
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: RENDERMIME_MD_CLASS },
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 RendermimeMarkdown = React.memo(RendermimeMarkdownBase);
79
+ export const MarkdownRenderer = React.memo(MarkdownRendererBase);
@@ -0,0 +1,3 @@
1
+ export * from './contrasting-tooltip';
2
+ export * from './tooltipped-button';
3
+ export * from './tooltipped-icon-button';
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './contrasting-tooltip';
6
+ export * from './tooltipped-button';
7
+ export * from './tooltipped-icon-button';
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './active-cell-manager';
2
+ export * from './components';
2
3
  export * from './icons';
3
4
  export * from './model';
4
5
  export * from './registry';
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  export * from './active-cell-manager';
6
+ export * from './components';
6
7
  export * from './icons';
7
8
  export * from './model';
8
9
  export * from './registry';
package/lib/model.d.ts CHANGED
@@ -301,6 +301,10 @@ export declare namespace ChatModel {
301
301
  * The instantiation options for a ChatModel.
302
302
  */
303
303
  interface IOptions {
304
+ /**
305
+ * The id of the chat.
306
+ */
307
+ id?: string;
304
308
  /**
305
309
  * Initial config for the chat widget.
306
310
  */
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.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -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
- // do not send the message if the user was selecting a suggested command from the
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.key === 'Enter' &&
141
- ((sendWithShiftEnter && event.shiftKey) ||
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={input.length > 0}
239
+ inputExists={inputExists}
237
240
  onSend={onSend}
238
241
  hideIncludeSelection={hideIncludeSelection}
239
242
  hasButtonOnLeft={!!props.onCancel}