@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.
- package/lib/active-cell-manager.d.ts +151 -0
- package/lib/active-cell-manager.js +201 -0
- package/lib/components/chat-input.d.ts +14 -4
- package/lib/components/chat-input.js +118 -10
- package/lib/components/chat-messages.d.ts +45 -15
- package/lib/components/chat-messages.js +237 -55
- package/lib/components/chat.d.ts +21 -6
- package/lib/components/chat.js +15 -45
- package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
- package/lib/components/code-blocks/code-toolbar.js +70 -0
- package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
- package/lib/components/code-blocks/copy-button.js +43 -0
- package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
- package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
- package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
- package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
- package/lib/components/rendermime-markdown.d.ts +2 -0
- package/lib/components/rendermime-markdown.js +29 -15
- package/lib/components/scroll-container.js +1 -19
- package/lib/icons.d.ts +2 -0
- package/lib/icons.js +10 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/model.d.ts +98 -14
- package/lib/model.js +197 -6
- package/lib/registry.d.ts +78 -0
- package/lib/registry.js +83 -0
- package/lib/types.d.ts +60 -4
- package/lib/widgets/chat-sidebar.d.ts +3 -4
- package/lib/widgets/chat-sidebar.js +2 -2
- package/lib/widgets/chat-widget.d.ts +2 -8
- package/lib/widgets/chat-widget.js +6 -6
- package/package.json +204 -200
- package/src/active-cell-manager.ts +318 -0
- package/src/components/chat-input.tsx +196 -50
- package/src/components/chat-messages.tsx +357 -95
- package/src/components/chat.tsx +43 -69
- package/src/components/code-blocks/code-toolbar.tsx +143 -0
- package/src/components/code-blocks/copy-button.tsx +68 -0
- package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
- package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
- package/src/components/rendermime-markdown.tsx +44 -20
- package/src/components/scroll-container.tsx +1 -25
- package/src/icons.ts +12 -0
- package/src/index.ts +2 -0
- package/src/model.ts +275 -21
- package/src/registry.ts +129 -0
- package/src/types.ts +62 -4
- package/src/widgets/chat-sidebar.tsx +3 -15
- package/src/widgets/chat-widget.tsx +8 -21
- package/style/chat.css +40 -0
- package/style/icons/read.svg +11 -0
- package/style/icons/replace-cell.svg +8 -0
- package/lib/components/copy-button.js +0 -35
- 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,
|
|
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[
|
|
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(
|
|
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[
|
|
147
|
+
newDatetime[message.time] = datetime;
|
|
52
148
|
setDatetime(newDatetime);
|
|
53
149
|
}
|
|
54
150
|
});
|
|
55
|
-
const bgcolor =
|
|
56
|
-
const avatar =
|
|
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:
|
|
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
|
-
} },
|
|
67
|
-
const name = (_b = (_a =
|
|
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: {
|
|
86
|
-
|
|
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
|
-
|
|
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:
|
|
195
|
+
}, title: message.raw_time ? 'Unverified time' : '' }, `${datetime[message.time]}${message.raw_time ? '*' : ''}`))));
|
|
96
196
|
}
|
|
97
197
|
/**
|
|
98
|
-
* The
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -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
|
|
13
|
+
* The props for the chat body component.
|
|
12
14
|
*/
|
|
13
|
-
interface
|
|
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?:
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
enum View {
|
|
55
|
+
chat = 0,
|
|
56
|
+
settings = 1
|
|
42
57
|
}
|
|
43
58
|
}
|
package/lib/components/chat.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
-
},
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
85
|
-
view === Chat.
|
|
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
|
|
96
|
-
(function (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})(
|
|
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
|
+
}
|