@jupyter/chat 0.1.0 → 0.3.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 (55) hide show
  1. package/lib/active-cell-manager.d.ts +151 -0
  2. package/lib/active-cell-manager.js +201 -0
  3. package/lib/components/chat-input.d.ts +14 -4
  4. package/lib/components/chat-input.js +118 -10
  5. package/lib/components/chat-messages.d.ts +45 -15
  6. package/lib/components/chat-messages.js +237 -55
  7. package/lib/components/chat.d.ts +21 -6
  8. package/lib/components/chat.js +15 -45
  9. package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
  10. package/lib/components/code-blocks/code-toolbar.js +70 -0
  11. package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
  12. package/lib/components/code-blocks/copy-button.js +43 -0
  13. package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
  14. package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
  15. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
  16. package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
  17. package/lib/components/rendermime-markdown.d.ts +2 -0
  18. package/lib/components/rendermime-markdown.js +29 -15
  19. package/lib/components/scroll-container.js +1 -19
  20. package/lib/icons.d.ts +2 -0
  21. package/lib/icons.js +10 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.js +2 -0
  24. package/lib/model.d.ts +98 -14
  25. package/lib/model.js +197 -6
  26. package/lib/registry.d.ts +78 -0
  27. package/lib/registry.js +83 -0
  28. package/lib/types.d.ts +60 -4
  29. package/lib/widgets/chat-sidebar.d.ts +3 -4
  30. package/lib/widgets/chat-sidebar.js +2 -2
  31. package/lib/widgets/chat-widget.d.ts +2 -8
  32. package/lib/widgets/chat-widget.js +6 -6
  33. package/package.json +204 -200
  34. package/src/active-cell-manager.ts +318 -0
  35. package/src/components/chat-input.tsx +196 -50
  36. package/src/components/chat-messages.tsx +357 -95
  37. package/src/components/chat.tsx +43 -69
  38. package/src/components/code-blocks/code-toolbar.tsx +143 -0
  39. package/src/components/code-blocks/copy-button.tsx +68 -0
  40. package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
  41. package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
  42. package/src/components/rendermime-markdown.tsx +44 -20
  43. package/src/components/scroll-container.tsx +1 -25
  44. package/src/icons.ts +12 -0
  45. package/src/index.ts +2 -0
  46. package/src/model.ts +275 -21
  47. package/src/registry.ts +129 -0
  48. package/src/types.ts +62 -4
  49. package/src/widgets/chat-sidebar.tsx +3 -15
  50. package/src/widgets/chat-widget.tsx +8 -21
  51. package/style/chat.css +40 -0
  52. package/style/icons/read.svg +11 -0
  53. package/style/icons/replace-cell.svg +8 -0
  54. package/lib/components/copy-button.js +0 -35
  55. package/src/components/copy-button.tsx +0 -55
@@ -2,15 +2,109 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ import { Button } from '@jupyter/react-components';
6
+ import { LabIcon, caretDownEmptyIcon, classes } from '@jupyterlab/ui-components';
5
7
  import { Avatar, Box, Typography } from '@mui/material';
6
8
  import clsx from 'clsx';
7
- import React, { useState, useEffect } from 'react';
9
+ import React, { useEffect, useState, useRef } from 'react';
8
10
  import { ChatInput } from './chat-input';
9
11
  import { RendermimeMarkdown } from './rendermime-markdown';
12
+ import { ScrollContainer } from './scroll-container';
10
13
  const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
11
14
  const MESSAGE_CLASS = 'jp-chat-message';
15
+ const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
12
16
  const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
13
17
  const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
18
+ const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
19
+ const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
20
+ const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
21
+ const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
22
+ /**
23
+ * The messages list component.
24
+ */
25
+ export function ChatMessages(props) {
26
+ const { model } = props;
27
+ const [messages, setMessages] = useState(model.messages);
28
+ const refMsgBox = useRef(null);
29
+ const inViewport = useRef([]);
30
+ // The intersection observer that listen to all the message visibility.
31
+ const observerRef = useRef(new IntersectionObserver(viewportChange));
32
+ /**
33
+ * Effect: fetch history and config on initial render
34
+ */
35
+ useEffect(() => {
36
+ async function fetchHistory() {
37
+ if (!model.getHistory) {
38
+ return;
39
+ }
40
+ model
41
+ .getHistory()
42
+ .then(history => setMessages(history.messages))
43
+ .catch(e => console.error(e));
44
+ }
45
+ fetchHistory();
46
+ }, [model]);
47
+ /**
48
+ * Effect: listen to chat messages.
49
+ */
50
+ useEffect(() => {
51
+ function handleChatEvents() {
52
+ setMessages([...model.messages]);
53
+ }
54
+ model.messagesUpdated.connect(handleChatEvents);
55
+ return function cleanup() {
56
+ model.messagesUpdated.disconnect(handleChatEvents);
57
+ };
58
+ }, [model]);
59
+ /**
60
+ * Function called when a message enter or leave the viewport.
61
+ */
62
+ function viewportChange(entries) {
63
+ const unread = [...model.unreadMessages];
64
+ let unreadModified = false;
65
+ entries.forEach(entry => {
66
+ var _a;
67
+ const index = parseInt((_a = entry.target.getAttribute('data-index')) !== null && _a !== void 0 ? _a : '');
68
+ if (!isNaN(index)) {
69
+ if (unread.length) {
70
+ const unreadIdx = unread.indexOf(index);
71
+ if (unreadIdx !== -1 && entry.isIntersecting) {
72
+ unread.splice(unreadIdx, 1);
73
+ unreadModified = true;
74
+ }
75
+ }
76
+ const viewportIdx = inViewport.current.indexOf(index);
77
+ if (!entry.isIntersecting && viewportIdx !== -1) {
78
+ inViewport.current.splice(viewportIdx, 1);
79
+ }
80
+ else if (entry.isIntersecting && viewportIdx === -1) {
81
+ inViewport.current.push(index);
82
+ }
83
+ }
84
+ });
85
+ props.model.messagesInViewport = inViewport.current;
86
+ if (unreadModified) {
87
+ props.model.unreadMessages = unread;
88
+ }
89
+ return () => {
90
+ var _a;
91
+ (_a = observerRef.current) === null || _a === void 0 ? void 0 : _a.disconnect();
92
+ };
93
+ }
94
+ return (React.createElement(React.Fragment, null,
95
+ React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
96
+ React.createElement(Box, { ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, messages.map((message, i) => {
97
+ return (
98
+ // extra div needed to ensure each bubble is on a new line
99
+ React.createElement(Box, { key: i, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
100
+ React.createElement(ChatMessageHeader, { message: message }),
101
+ React.createElement(ChatMessage, { ...props, message: message, observer: observerRef.current, index: i })));
102
+ }))),
103
+ React.createElement(Navigation, { ...props, refMsgBox: refMsgBox })));
104
+ }
105
+ /**
106
+ * The message header component.
107
+ */
14
108
  export function ChatMessageHeader(props) {
15
109
  var _a, _b;
16
110
  const [datetime, setDatetime] = useState({});
@@ -18,18 +112,20 @@ export function ChatMessageHeader(props) {
18
112
  height: '24px',
19
113
  width: '24px'
20
114
  };
115
+ const message = props.message;
116
+ const sender = message.sender;
21
117
  /**
22
118
  * Effect: update cached datetime strings upon receiving a new message.
23
119
  */
24
120
  useEffect(() => {
25
- if (!datetime[props.timestamp]) {
121
+ if (!datetime[message.time]) {
26
122
  const newDatetime = {};
27
123
  let datetime;
28
124
  const currentDate = new Date();
29
125
  const sameDay = (date) => date.getFullYear() === currentDate.getFullYear() &&
30
126
  date.getMonth() === currentDate.getMonth() &&
31
127
  date.getDate() === currentDate.getDate();
32
- const msgDate = new Date(props.timestamp * 1000); // Convert message time to milliseconds
128
+ const msgDate = new Date(message.time * 1000); // Convert message time to milliseconds
33
129
  // Display only the time if the day of the message is the current one.
34
130
  if (sameDay(msgDate)) {
35
131
  // Use the browser's default locale
@@ -48,29 +144,30 @@ export function ChatMessageHeader(props) {
48
144
  minute: '2-digit'
49
145
  });
50
146
  }
51
- newDatetime[props.timestamp] = datetime;
147
+ newDatetime[message.time] = datetime;
52
148
  setDatetime(newDatetime);
53
149
  }
54
150
  });
55
- const bgcolor = props.color;
56
- const avatar = props.avatar_url ? (React.createElement(Avatar, { sx: {
151
+ const bgcolor = sender.color;
152
+ const avatar = message.stacked ? null : sender.avatar_url ? (React.createElement(Avatar, { sx: {
57
153
  ...sharedStyles,
58
154
  ...(bgcolor && { bgcolor })
59
- }, src: props.avatar_url })) : props.initials ? (React.createElement(Avatar, { sx: {
155
+ }, src: sender.avatar_url })) : sender.initials ? (React.createElement(Avatar, { sx: {
60
156
  ...sharedStyles,
61
157
  ...(bgcolor && { bgcolor })
62
158
  } },
63
159
  React.createElement(Typography, { sx: {
64
160
  fontSize: 'var(--jp-ui-font-size1)',
65
161
  color: 'var(--jp-ui-inverse-font-color1)'
66
- } }, props.initials))) : null;
67
- const name = (_b = (_a = props.display_name) !== null && _a !== void 0 ? _a : props.name) !== null && _b !== void 0 ? _b : (props.username || 'User undefined');
162
+ } }, sender.initials))) : null;
163
+ const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || 'User undefined');
68
164
  return (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
69
165
  display: 'flex',
70
166
  alignItems: 'center',
71
167
  '& > :not(:last-child)': {
72
168
  marginRight: 3
73
169
  },
170
+ marginBottom: message.stacked ? '0px' : '12px',
74
171
  ...props.sx
75
172
  } },
76
173
  avatar,
@@ -82,64 +179,66 @@ export function ChatMessageHeader(props) {
82
179
  alignItems: 'center'
83
180
  } },
84
181
  React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
85
- React.createElement(Typography, { sx: { fontWeight: 700, color: 'var(--jp-ui-font-color1)' } }, name),
86
- (props.deleted || props.edited) && (React.createElement(Typography, { sx: {
182
+ !message.stacked && (React.createElement(Typography, { sx: {
183
+ fontWeight: 700,
184
+ color: 'var(--jp-ui-font-color1)',
185
+ paddingRight: '0.5em'
186
+ } }, name)),
187
+ (message.deleted || message.edited) && (React.createElement(Typography, { sx: {
87
188
  fontStyle: 'italic',
88
- fontSize: 'var(--jp-content-font-size0)',
89
- paddingLeft: '0.5em'
90
- } }, props.deleted ? '(message deleted)' : '(edited)'))),
189
+ fontSize: 'var(--jp-content-font-size0)'
190
+ } }, message.deleted ? '(message deleted)' : '(edited)'))),
91
191
  React.createElement(Typography, { className: MESSAGE_TIME_CLASS, sx: {
92
192
  fontSize: '0.8em',
93
193
  color: 'var(--jp-ui-font-color2)',
94
194
  fontWeight: 300
95
- }, title: props.rawTime ? 'Unverified time' : '' }, `${datetime[props.timestamp]}${props.rawTime ? '*' : ''}`))));
195
+ }, title: message.raw_time ? 'Unverified time' : '' }, `${datetime[message.time]}${message.raw_time ? '*' : ''}`))));
96
196
  }
97
197
  /**
98
- * The messages list UI.
99
- */
100
- export function ChatMessages(props) {
101
- return (React.createElement(Box, { sx: {
102
- '& > :not(:last-child)': {
103
- borderBottom: '1px solid var(--jp-border-color2)'
104
- }
105
- }, className: clsx(MESSAGES_BOX_CLASS) }, props.messages.map((message, i) => {
106
- let sender;
107
- if (typeof message.sender === 'string') {
108
- sender = { username: message.sender };
109
- }
110
- else {
111
- sender = message.sender;
112
- }
113
- return (
114
- // extra div needed to ensure each bubble is on a new line
115
- React.createElement(Box, { key: i, sx: { padding: '1em 1em 0 1em' }, className: clsx(MESSAGE_CLASS) },
116
- React.createElement(ChatMessageHeader, { ...sender, timestamp: message.time, rawTime: message.raw_time, deleted: message.deleted, edited: message.edited, sx: { marginBottom: 3 } }),
117
- React.createElement(ChatMessage, { ...props, message: message })));
118
- })));
119
- }
120
- /**
121
- * the message UI.
198
+ * The message component body.
122
199
  */
123
200
  export function ChatMessage(props) {
124
- var _a;
125
201
  const { message, model, rmRegistry } = props;
126
- let canEdit = false;
127
- let canDelete = false;
128
- if (model.user !== undefined && !message.deleted) {
129
- const username = typeof message.sender === 'string'
130
- ? message.sender
131
- : message.sender.username;
132
- if (model.user.username === username && model.updateMessage !== undefined) {
133
- canEdit = true;
202
+ const elementRef = useRef(null);
203
+ const [edit, setEdit] = useState(false);
204
+ const [deleted, setDeleted] = useState(false);
205
+ const [canEdit, setCanEdit] = useState(false);
206
+ const [canDelete, setCanDelete] = useState(false);
207
+ // Add the current message to the observer, to actualize viewport and unread messages.
208
+ useEffect(() => {
209
+ var _a;
210
+ if (elementRef.current === null) {
211
+ return;
134
212
  }
135
- if (model.user.username === username && model.deleteMessage !== undefined) {
136
- canDelete = true;
213
+ // If the observer is defined, let's observe the message.
214
+ (_a = props.observer) === null || _a === void 0 ? void 0 : _a.observe(elementRef.current);
215
+ return () => {
216
+ var _a;
217
+ if (elementRef.current !== null) {
218
+ (_a = props.observer) === null || _a === void 0 ? void 0 : _a.unobserve(elementRef.current);
219
+ }
220
+ };
221
+ }, [model]);
222
+ // Look if the message can be deleted or edited.
223
+ useEffect(() => {
224
+ var _a;
225
+ setDeleted((_a = message.deleted) !== null && _a !== void 0 ? _a : false);
226
+ if (model.user !== undefined && !message.deleted) {
227
+ if (model.user.username === message.sender.username) {
228
+ setCanEdit(model.updateMessage !== undefined);
229
+ setCanDelete(model.deleteMessage !== undefined);
230
+ }
137
231
  }
138
- }
139
- const [edit, setEdit] = useState(false);
232
+ else {
233
+ setCanEdit(false);
234
+ setCanDelete(false);
235
+ }
236
+ }, [model, message]);
237
+ // Cancel the current edition of the message.
140
238
  const cancelEdition = () => {
141
239
  setEdit(false);
142
240
  };
241
+ // Update the content of the message.
143
242
  const updateMessage = (id, input) => {
144
243
  if (!canEdit) {
145
244
  return;
@@ -150,13 +249,96 @@ export function ChatMessage(props) {
150
249
  model.updateMessage(id, updatedMessage);
151
250
  setEdit(false);
152
251
  };
252
+ // Delete the message.
153
253
  const deleteMessage = (id) => {
154
254
  if (!canDelete) {
155
255
  return;
156
256
  }
157
- // Delete the message
158
257
  model.deleteMessage(id);
159
258
  };
160
- // Empty if the message has been deleted
161
- return message.deleted ? (React.createElement(React.Fragment, null)) : (React.createElement("div", null, edit && canEdit ? (React.createElement(ChatInput, { value: message.body, onSend: (input) => updateMessage(message.id, input), onCancel: () => cancelEdition(), sendWithShiftEnter: (_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false })) : (React.createElement(RendermimeMarkdown, { rmRegistry: rmRegistry, markdownStr: message.body, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined }))));
259
+ // Empty if the message has been deleted.
260
+ 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 })) : (React.createElement(RendermimeMarkdown, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined }))));
261
+ }
262
+ /**
263
+ * The navigation component, to navigate to unread messages.
264
+ */
265
+ export function Navigation(props) {
266
+ const { model } = props;
267
+ const [lastInViewport, setLastInViewport] = useState(true);
268
+ const [unreadBefore, setUnreadBefore] = useState(null);
269
+ const [unreadAfter, setUnreadAfter] = useState(null);
270
+ const gotoMessage = (msgIdx) => {
271
+ var _a, _b;
272
+ (_b = (_a = props.refMsgBox.current) === null || _a === void 0 ? void 0 : _a.children.item(msgIdx)) === null || _b === void 0 ? void 0 : _b.scrollIntoView();
273
+ };
274
+ // Listen for change in unread messages, and find the first unread message before or
275
+ // after the current viewport, to display navigation buttons.
276
+ useEffect(() => {
277
+ var _a;
278
+ const unreadChanged = (model, unreadIndexes) => {
279
+ const viewport = model.messagesInViewport;
280
+ if (!viewport) {
281
+ return;
282
+ }
283
+ // Initialize the next values with the current values if there still relevant.
284
+ let before = unreadBefore !== null &&
285
+ unreadIndexes.includes(unreadBefore) &&
286
+ unreadBefore < Math.min(...viewport)
287
+ ? unreadBefore
288
+ : null;
289
+ let after = unreadAfter !== null &&
290
+ unreadIndexes.includes(unreadAfter) &&
291
+ unreadAfter > Math.max(...viewport)
292
+ ? unreadAfter
293
+ : null;
294
+ unreadIndexes.forEach(unread => {
295
+ if (viewport === null || viewport === void 0 ? void 0 : viewport.includes(unread)) {
296
+ return;
297
+ }
298
+ if (unread < (before !== null && before !== void 0 ? before : Math.min(...viewport))) {
299
+ before = unread;
300
+ }
301
+ else if (unread > Math.max(...viewport) &&
302
+ unread < (after !== null && after !== void 0 ? after : model.messages.length)) {
303
+ after = unread;
304
+ }
305
+ });
306
+ setUnreadBefore(before);
307
+ setUnreadAfter(after);
308
+ };
309
+ (_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.connect(unreadChanged);
310
+ unreadChanged(model, model.unreadMessages);
311
+ // Move to first the unread message or to last message on first rendering.
312
+ if (model.unreadMessages.length) {
313
+ gotoMessage(Math.min(...model.unreadMessages));
314
+ }
315
+ else {
316
+ gotoMessage(model.messages.length - 1);
317
+ }
318
+ return () => {
319
+ var _a;
320
+ (_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.disconnect(unreadChanged);
321
+ };
322
+ }, [model]);
323
+ // Listen for change in the viewport, to add a navigation button if the last is not
324
+ // in viewport.
325
+ useEffect(() => {
326
+ var _a, _b;
327
+ const viewportChanged = (model, viewport) => {
328
+ setLastInViewport(viewport.includes(model.messages.length - 1));
329
+ };
330
+ (_a = model.viewportChanged) === null || _a === void 0 ? void 0 : _a.connect(viewportChanged);
331
+ viewportChanged(model, (_b = model.messagesInViewport) !== null && _b !== void 0 ? _b : []);
332
+ return () => {
333
+ var _a;
334
+ (_a = model.viewportChanged) === null || _a === void 0 ? void 0 : _a.disconnect(viewportChanged);
335
+ };
336
+ }, [model]);
337
+ return (React.createElement(React.Fragment, null,
338
+ unreadBefore !== null && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`, onClick: () => gotoMessage(unreadBefore), title: 'Go to unread messages' },
339
+ React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') }))),
340
+ (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
341
+ ? 'Go to unread messages'
342
+ : 'Go to last message' },
343
+ React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') })))));
162
344
  }
@@ -2,15 +2,17 @@
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { IChatModel } from '../model';
5
+ import { IAutocompletionRegistry } from '../registry';
6
+ export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
5
7
  export declare function Chat(props: Chat.IOptions): JSX.Element;
6
8
  /**
7
9
  * The chat UI namespace
8
10
  */
9
11
  export declare namespace Chat {
10
12
  /**
11
- * The options to build the Chat UI.
13
+ * The props for the chat body component.
12
14
  */
13
- interface IOptions {
15
+ interface IChatBodyProps {
14
16
  /**
15
17
  * The chat model.
16
18
  */
@@ -19,6 +21,19 @@ export declare namespace Chat {
19
21
  * The rendermime registry.
20
22
  */
21
23
  rmRegistry: IRenderMimeRegistry;
24
+ /**
25
+ * Autocompletion registry.
26
+ */
27
+ autocompletionRegistry?: IAutocompletionRegistry;
28
+ /**
29
+ * Autocompletion name.
30
+ */
31
+ autocompletionName?: string;
32
+ }
33
+ /**
34
+ * The options to build the Chat UI.
35
+ */
36
+ interface IOptions extends IChatBodyProps {
22
37
  /**
23
38
  * The theme manager.
24
39
  */
@@ -26,7 +41,7 @@ export declare namespace Chat {
26
41
  /**
27
42
  * The view to render.
28
43
  */
29
- chatView?: ChatView;
44
+ chatView?: View;
30
45
  /**
31
46
  * A settings panel that can be used for dedicated settings (e.g. jupyter-ai)
32
47
  */
@@ -36,8 +51,8 @@ export declare namespace Chat {
36
51
  * The view to render.
37
52
  * The settings view is available only if the settings panel is provided in options.
38
53
  */
39
- enum ChatView {
40
- Chat = 0,
41
- Settings = 1
54
+ enum View {
55
+ chat = 0,
56
+ settings = 1
42
57
  }
43
58
  }
@@ -6,41 +6,12 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
6
6
  import SettingsIcon from '@mui/icons-material/Settings';
7
7
  import { IconButton } from '@mui/material';
8
8
  import { Box } from '@mui/system';
9
- import React, { useState, useEffect } from 'react';
9
+ import React, { useState } from 'react';
10
10
  import { JlThemeProvider } from './jl-theme-provider';
11
11
  import { ChatMessages } from './chat-messages';
12
12
  import { ChatInput } from './chat-input';
13
- import { ScrollContainer } from './scroll-container';
14
- function ChatBody({ model, rmRegistry: renderMimeRegistry }) {
15
- var _a;
16
- const [messages, setMessages] = useState([]);
17
- /**
18
- * Effect: fetch history and config on initial render
19
- */
20
- useEffect(() => {
21
- async function fetchHistory() {
22
- if (!model.getHistory) {
23
- return;
24
- }
25
- model
26
- .getHistory()
27
- .then(history => setMessages(history.messages))
28
- .catch(e => console.error(e));
29
- }
30
- fetchHistory();
31
- }, [model]);
32
- /**
33
- * Effect: listen to chat messages
34
- */
35
- useEffect(() => {
36
- function handleChatEvents(_) {
37
- setMessages([...model.messages]);
38
- }
39
- model.messagesUpdated.connect(handleChatEvents);
40
- return function cleanup() {
41
- model.messagesUpdated.disconnect(handleChatEvents);
42
- };
43
- }, [model]);
13
+ export function ChatBody(props) {
14
+ const { model, rmRegistry: renderMimeRegistry, autocompletionRegistry } = props;
44
15
  // no need to append to messageGroups imperatively here. all of that is
45
16
  // handled by the listeners registered in the effect hooks above.
46
17
  const onSend = async (input) => {
@@ -48,19 +19,18 @@ function ChatBody({ model, rmRegistry: renderMimeRegistry }) {
48
19
  model.addMessage({ body: input });
49
20
  };
50
21
  return (React.createElement(React.Fragment, null,
51
- React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
52
- React.createElement(ChatMessages, { messages: messages, rmRegistry: renderMimeRegistry, model: model })),
22
+ React.createElement(ChatMessages, { rmRegistry: renderMimeRegistry, model: model }),
53
23
  React.createElement(ChatInput, { onSend: onSend, sx: {
54
24
  paddingLeft: 4,
55
25
  paddingRight: 4,
56
26
  paddingTop: 3.5,
57
27
  paddingBottom: 0,
58
28
  borderTop: '1px solid var(--jp-border-color1)'
59
- }, sendWithShiftEnter: (_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false })));
29
+ }, model: model, autocompletionRegistry: autocompletionRegistry })));
60
30
  }
61
31
  export function Chat(props) {
62
32
  var _a;
63
- const [view, setView] = useState(props.chatView || Chat.ChatView.Chat);
33
+ const [view, setView] = useState(props.chatView || Chat.View.chat);
64
34
  return (React.createElement(JlThemeProvider, { themeManager: (_a = props.themeManager) !== null && _a !== void 0 ? _a : null },
65
35
  React.createElement(Box
66
36
  // root box should not include padding as it offsets the vertical
@@ -77,12 +47,12 @@ export function Chat(props) {
77
47
  flexDirection: 'column'
78
48
  } },
79
49
  React.createElement(Box, { sx: { display: 'flex', justifyContent: 'space-between' } },
80
- view !== Chat.ChatView.Chat ? (React.createElement(IconButton, { onClick: () => setView(Chat.ChatView.Chat) },
50
+ view !== Chat.View.chat ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.chat) },
81
51
  React.createElement(ArrowBackIcon, null))) : (React.createElement(Box, null)),
82
- view === Chat.ChatView.Chat && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.ChatView.Settings) },
52
+ view !== Chat.View.settings && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.settings) },
83
53
  React.createElement(SettingsIcon, null))) : (React.createElement(Box, null))),
84
- view === Chat.ChatView.Chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry })),
85
- view === Chat.ChatView.Settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
54
+ view === Chat.View.chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry, autocompletionRegistry: props.autocompletionRegistry })),
55
+ view === Chat.View.settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
86
56
  }
87
57
  /**
88
58
  * The chat UI namespace
@@ -92,9 +62,9 @@ export function Chat(props) {
92
62
  * The view to render.
93
63
  * The settings view is available only if the settings panel is provided in options.
94
64
  */
95
- let ChatView;
96
- (function (ChatView) {
97
- ChatView[ChatView["Chat"] = 0] = "Chat";
98
- ChatView[ChatView["Settings"] = 1] = "Settings";
99
- })(ChatView = Chat.ChatView || (Chat.ChatView = {}));
65
+ let View;
66
+ (function (View) {
67
+ View[View["chat"] = 0] = "chat";
68
+ View[View["settings"] = 1] = "settings";
69
+ })(View = Chat.View || (Chat.View = {}));
100
70
  })(Chat || (Chat = {}));
@@ -0,0 +1,13 @@
1
+ /// <reference types="react" />
2
+ import { IChatModel } from '../../model';
3
+ export type CodeToolbarProps = {
4
+ /**
5
+ * The chat model.
6
+ */
7
+ model: IChatModel;
8
+ /**
9
+ * The content of the Markdown code block this component is attached to.
10
+ */
11
+ content: string;
12
+ };
13
+ export declare function CodeToolbar(props: CodeToolbarProps): JSX.Element;
@@ -0,0 +1,70 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { addAboveIcon, addBelowIcon } from '@jupyterlab/ui-components';
6
+ import { Box } from '@mui/material';
7
+ import React, { useEffect, useState } from 'react';
8
+ import { CopyButton } from './copy-button';
9
+ import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
10
+ import { replaceCellIcon } from '../../icons';
11
+ const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
12
+ const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
13
+ export function CodeToolbar(props) {
14
+ var _a, _b;
15
+ const { content, model } = props;
16
+ const [toolbarEnable, setToolbarEnable] = useState((_a = model.config.enableCodeToolbar) !== null && _a !== void 0 ? _a : true);
17
+ const activeCellManager = model.activeCellManager;
18
+ const [toolbarBtnProps, setToolbarBtnProps] = useState({
19
+ content: content,
20
+ activeCellManager: activeCellManager,
21
+ activeCellAvailable: (_b = activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available) !== null && _b !== void 0 ? _b : false
22
+ });
23
+ useEffect(() => {
24
+ activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.connect(() => {
25
+ setToolbarBtnProps({
26
+ content,
27
+ activeCellManager: activeCellManager,
28
+ activeCellAvailable: activeCellManager.available
29
+ });
30
+ });
31
+ model.configChanged.connect((_, config) => {
32
+ var _a;
33
+ setToolbarEnable((_a = config.enableCodeToolbar) !== null && _a !== void 0 ? _a : true);
34
+ });
35
+ }, [model]);
36
+ return activeCellManager === null || !toolbarEnable ? (React.createElement(React.Fragment, null)) : (React.createElement(Box, { sx: {
37
+ display: 'flex',
38
+ justifyContent: 'flex-end',
39
+ alignItems: 'center',
40
+ padding: '6px 2px',
41
+ marginBottom: '1em',
42
+ border: '1px solid var(--jp-cell-editor-border-color)',
43
+ borderTop: 'none'
44
+ }, className: CODE_TOOLBAR_CLASS },
45
+ React.createElement(InsertAboveButton, { ...toolbarBtnProps, className: CODE_TOOLBAR_ITEM_CLASS }),
46
+ React.createElement(InsertBelowButton, { ...toolbarBtnProps, className: CODE_TOOLBAR_ITEM_CLASS }),
47
+ React.createElement(ReplaceButton, { ...toolbarBtnProps, className: CODE_TOOLBAR_ITEM_CLASS }),
48
+ React.createElement(CopyButton, { value: content, className: CODE_TOOLBAR_ITEM_CLASS })));
49
+ }
50
+ function InsertAboveButton(props) {
51
+ const tooltip = props.activeCellAvailable
52
+ ? 'Insert above active cell'
53
+ : 'Insert above active cell (no active cell)';
54
+ return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, onClick: () => { var _a; return (_a = props.activeCellManager) === null || _a === void 0 ? void 0 : _a.insertAbove(props.content); }, disabled: !props.activeCellAvailable },
55
+ React.createElement(addAboveIcon.react, { height: "16px", width: "16px" })));
56
+ }
57
+ function InsertBelowButton(props) {
58
+ const tooltip = props.activeCellAvailable
59
+ ? 'Insert below active cell'
60
+ : 'Insert below active cell (no active cell)';
61
+ return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, disabled: !props.activeCellAvailable, onClick: () => { var _a; return (_a = props.activeCellManager) === null || _a === void 0 ? void 0 : _a.insertBelow(props.content); } },
62
+ React.createElement(addBelowIcon.react, { height: "16px", width: "16px" })));
63
+ }
64
+ function ReplaceButton(props) {
65
+ const tooltip = props.activeCellAvailable
66
+ ? 'Replace active cell'
67
+ : 'Replace active cell (no active cell)';
68
+ return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, disabled: !props.activeCellAvailable, onClick: () => { var _a; return (_a = props.activeCellManager) === null || _a === void 0 ? void 0 : _a.replace(props.content); } },
69
+ React.createElement(replaceCellIcon.react, { height: "16px", width: "16px" })));
70
+ }
@@ -1,6 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  type CopyButtonProps = {
3
3
  value: string;
4
+ className?: string;
4
5
  };
5
6
  export declare function CopyButton(props: CopyButtonProps): JSX.Element;
6
7
  export {};