@jupyter/chat 0.4.0 → 0.6.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 +3 -0
- package/lib/components/chat-input.d.ts +4 -0
- package/lib/components/chat-input.js +32 -15
- package/lib/components/chat-messages.d.ts +31 -1
- package/lib/components/chat-messages.js +57 -19
- package/lib/components/chat.js +1 -1
- package/lib/components/code-blocks/code-toolbar.js +51 -17
- package/lib/components/input/cancel-button.d.ts +12 -0
- package/lib/components/input/cancel-button.js +27 -0
- package/lib/components/input/send-button.d.ts +18 -0
- package/lib/components/input/send-button.js +143 -0
- package/lib/components/mui-extras/tooltipped-button.d.ts +41 -0
- package/lib/components/mui-extras/tooltipped-button.js +43 -0
- package/lib/components/mui-extras/tooltipped-icon-button.js +5 -1
- package/lib/components/rendermime-markdown.js +15 -6
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/model.d.ts +51 -8
- package/lib/model.js +44 -12
- package/lib/selection-watcher.d.ts +62 -0
- package/lib/selection-watcher.js +134 -0
- package/lib/types.d.ts +22 -0
- package/lib/utils.d.ts +11 -0
- package/lib/utils.js +37 -0
- package/package.json +2 -1
- package/src/active-cell-manager.ts +3 -0
- package/src/components/chat-input.tsx +48 -30
- package/src/components/chat-messages.tsx +112 -32
- package/src/components/chat.tsx +1 -1
- package/src/components/code-blocks/code-toolbar.tsx +56 -18
- package/src/components/input/cancel-button.tsx +47 -0
- package/src/components/input/send-button.tsx +210 -0
- package/src/components/mui-extras/tooltipped-button.tsx +92 -0
- package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -1
- package/src/components/rendermime-markdown.tsx +16 -5
- package/src/icons.ts +6 -0
- package/src/index.ts +2 -1
- package/src/model.ts +77 -13
- package/src/selection-watcher.ts +221 -0
- package/src/types.ts +25 -0
- package/src/utils.ts +47 -0
- package/style/chat.css +13 -0
- package/style/icons/include-selection.svg +5 -0
package/lib/utils.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
|
|
6
|
+
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
7
|
+
import { FileEditor } from '@jupyterlab/fileeditor';
|
|
8
|
+
import { Notebook } from '@jupyterlab/notebook';
|
|
9
|
+
/**
|
|
10
|
+
* Gets the editor instance used by a document widget. Returns `null` if unable.
|
|
11
|
+
*/
|
|
12
|
+
export function getEditor(widget) {
|
|
13
|
+
var _a;
|
|
14
|
+
if (!(widget instanceof DocumentWidget)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
let editor;
|
|
18
|
+
const { content } = widget;
|
|
19
|
+
if (content instanceof FileEditor) {
|
|
20
|
+
editor = content.editor;
|
|
21
|
+
}
|
|
22
|
+
else if (content instanceof Notebook) {
|
|
23
|
+
editor = (_a = content.activeCell) === null || _a === void 0 ? void 0 : _a.editor;
|
|
24
|
+
}
|
|
25
|
+
if (!(editor instanceof CodeMirrorEditor)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return editor;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Gets the index of the cell associated with `cellId`.
|
|
32
|
+
*/
|
|
33
|
+
export function getCellIndex(notebook, cellId) {
|
|
34
|
+
var _a;
|
|
35
|
+
const idx = (_a = notebook.model) === null || _a === void 0 ? void 0 : _a.sharedModel.cells.findIndex(cell => cell.getId() === cellId);
|
|
36
|
+
return idx === undefined ? -1 : idx;
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@jupyter/react-components": "^0.15.2",
|
|
58
58
|
"@jupyterlab/application": "^4.2.0",
|
|
59
59
|
"@jupyterlab/apputils": "^4.3.0",
|
|
60
|
+
"@jupyterlab/fileeditor": "^4.2.0",
|
|
60
61
|
"@jupyterlab/notebook": "^4.2.0",
|
|
61
62
|
"@jupyterlab/rendermime": "^4.2.0",
|
|
62
63
|
"@jupyterlab/ui-components": "^4.2.0",
|
|
@@ -8,25 +8,25 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
8
8
|
import {
|
|
9
9
|
Autocomplete,
|
|
10
10
|
Box,
|
|
11
|
-
IconButton,
|
|
12
11
|
InputAdornment,
|
|
13
12
|
SxProps,
|
|
14
13
|
TextField,
|
|
15
14
|
Theme
|
|
16
15
|
} from '@mui/material';
|
|
17
|
-
import { Send, Cancel } from '@mui/icons-material';
|
|
18
16
|
import clsx from 'clsx';
|
|
17
|
+
|
|
18
|
+
import { CancelButton } from './input/cancel-button';
|
|
19
|
+
import { SendButton } from './input/send-button';
|
|
19
20
|
import { IChatModel } from '../model';
|
|
20
21
|
import { IAutocompletionRegistry } from '../registry';
|
|
21
22
|
import {
|
|
22
23
|
AutocompleteCommand,
|
|
23
24
|
IAutocompletionCommandsProps,
|
|
24
|
-
IConfig
|
|
25
|
+
IConfig,
|
|
26
|
+
Selection
|
|
25
27
|
} from '../types';
|
|
26
28
|
|
|
27
29
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
28
|
-
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
29
|
-
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
30
30
|
|
|
31
31
|
export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
32
32
|
const { autocompletionName, autocompletionRegistry, model } = props;
|
|
@@ -35,6 +35,16 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
35
35
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState<boolean>(
|
|
36
36
|
model.config.sendWithShiftEnter ?? false
|
|
37
37
|
);
|
|
38
|
+
const [typingNotification, setTypingNotification] = useState<boolean>(
|
|
39
|
+
model.config.sendTypingNotification ?? false
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Display the include selection menu if it is not explicitly hidden, and if at least
|
|
43
|
+
// one of the tool to check for text or cell selection is enabled.
|
|
44
|
+
let hideIncludeSelection = props.hideIncludeSelection ?? false;
|
|
45
|
+
if (model.activeCellManager === null && model.selectionWatcher === null) {
|
|
46
|
+
hideIncludeSelection = true;
|
|
47
|
+
}
|
|
38
48
|
|
|
39
49
|
// store reference to the input element to enable focusing it easily
|
|
40
50
|
const inputRef = useRef<HTMLInputElement>();
|
|
@@ -42,6 +52,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
42
52
|
useEffect(() => {
|
|
43
53
|
const configChanged = (_: IChatModel, config: IConfig) => {
|
|
44
54
|
setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
|
|
55
|
+
setTypingNotification(config.sendTypingNotification ?? false);
|
|
45
56
|
};
|
|
46
57
|
model.configChanged.connect(configChanged);
|
|
47
58
|
|
|
@@ -138,10 +149,21 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
138
149
|
|
|
139
150
|
/**
|
|
140
151
|
* Triggered when sending the message.
|
|
152
|
+
*
|
|
153
|
+
* Add code block if cell or text is selected.
|
|
141
154
|
*/
|
|
142
|
-
function onSend() {
|
|
155
|
+
function onSend(selection?: Selection) {
|
|
156
|
+
let content = input;
|
|
157
|
+
if (selection) {
|
|
158
|
+
content += `
|
|
159
|
+
|
|
160
|
+
\`\`\`
|
|
161
|
+
${selection.source}
|
|
162
|
+
\`\`\`
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
props.onSend(content);
|
|
143
166
|
setInput('');
|
|
144
|
-
props.onSend(input);
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
/**
|
|
@@ -203,30 +225,19 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
203
225
|
endAdornment: (
|
|
204
226
|
<InputAdornment position="end">
|
|
205
227
|
{props.onCancel && (
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
title={'Cancel edition'}
|
|
211
|
-
className={clsx(CANCEL_BUTTON_CLASS)}
|
|
212
|
-
>
|
|
213
|
-
<Cancel />
|
|
214
|
-
</IconButton>
|
|
228
|
+
<CancelButton
|
|
229
|
+
inputExists={input.length > 0}
|
|
230
|
+
onCancel={onCancel}
|
|
231
|
+
/>
|
|
215
232
|
)}
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
title={`Send message ${sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
|
|
226
|
-
className={clsx(SEND_BUTTON_CLASS)}
|
|
227
|
-
>
|
|
228
|
-
<Send />
|
|
229
|
-
</IconButton>
|
|
233
|
+
<SendButton
|
|
234
|
+
model={model}
|
|
235
|
+
sendWithShiftEnter={sendWithShiftEnter}
|
|
236
|
+
inputExists={input.length > 0}
|
|
237
|
+
onSend={onSend}
|
|
238
|
+
hideIncludeSelection={hideIncludeSelection}
|
|
239
|
+
hasButtonOnLeft={!!props.onCancel}
|
|
240
|
+
/>
|
|
230
241
|
</InputAdornment>
|
|
231
242
|
)
|
|
232
243
|
}}
|
|
@@ -240,6 +251,9 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
240
251
|
inputValue={input}
|
|
241
252
|
onInputChange={(_, newValue: string) => {
|
|
242
253
|
setInput(newValue);
|
|
254
|
+
if (typingNotification && model.inputChanged) {
|
|
255
|
+
model.inputChanged(newValue);
|
|
256
|
+
}
|
|
243
257
|
}}
|
|
244
258
|
onHighlightChange={
|
|
245
259
|
/**
|
|
@@ -294,6 +308,10 @@ export namespace ChatInput {
|
|
|
294
308
|
* The function to be called to cancel editing.
|
|
295
309
|
*/
|
|
296
310
|
onCancel?: () => unknown;
|
|
311
|
+
/**
|
|
312
|
+
* Whether to allow or not including selection.
|
|
313
|
+
*/
|
|
314
|
+
hideIncludeSelection?: boolean;
|
|
297
315
|
/**
|
|
298
316
|
* Custom mui/material styles.
|
|
299
317
|
*/
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
caretDownEmptyIcon,
|
|
11
11
|
classes
|
|
12
12
|
} from '@jupyterlab/ui-components';
|
|
13
|
-
import { Avatar, Box, Typography } from '@mui/material';
|
|
13
|
+
import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
|
|
14
14
|
import type { SxProps, Theme } from '@mui/material';
|
|
15
15
|
import clsx from 'clsx';
|
|
16
16
|
import React, { useEffect, useState, useRef } from 'react';
|
|
@@ -19,13 +19,14 @@ import { ChatInput } from './chat-input';
|
|
|
19
19
|
import { RendermimeMarkdown } from './rendermime-markdown';
|
|
20
20
|
import { ScrollContainer } from './scroll-container';
|
|
21
21
|
import { IChatModel } from '../model';
|
|
22
|
-
import { IChatMessage } from '../types';
|
|
22
|
+
import { IChatMessage, IUser } from '../types';
|
|
23
23
|
|
|
24
24
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
25
25
|
const MESSAGE_CLASS = 'jp-chat-message';
|
|
26
26
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
27
27
|
const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
|
|
28
28
|
const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
29
|
+
const WRITERS_CLASS = 'jp-chat-writers';
|
|
29
30
|
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
30
31
|
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
|
|
31
32
|
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
|
|
@@ -47,6 +48,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
47
48
|
const [messages, setMessages] = useState<IChatMessage[]>(model.messages);
|
|
48
49
|
const refMsgBox = useRef<HTMLDivElement>(null);
|
|
49
50
|
const inViewport = useRef<number[]>([]);
|
|
51
|
+
const [currentWriters, setCurrentWriters] = useState<IUser[]>([]);
|
|
50
52
|
|
|
51
53
|
// The intersection observer that listen to all the message visibility.
|
|
52
54
|
const observerRef = useRef<IntersectionObserver>(
|
|
@@ -68,6 +70,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
fetchHistory();
|
|
73
|
+
setCurrentWriters([]);
|
|
71
74
|
}, [model]);
|
|
72
75
|
|
|
73
76
|
/**
|
|
@@ -78,9 +81,16 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
78
81
|
setMessages([...model.messages]);
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function handleWritersChange(_: IChatModel, writers: IUser[]) {
|
|
85
|
+
setCurrentWriters(writers);
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
model.messagesUpdated.connect(handleChatEvents);
|
|
89
|
+
model.writersChanged?.connect(handleWritersChange);
|
|
90
|
+
|
|
82
91
|
return function cleanup() {
|
|
83
92
|
model.messagesUpdated.disconnect(handleChatEvents);
|
|
93
|
+
model.writersChanged?.disconnect(handleChatEvents);
|
|
84
94
|
};
|
|
85
95
|
}, [model]);
|
|
86
96
|
|
|
@@ -144,6 +154,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
144
154
|
);
|
|
145
155
|
})}
|
|
146
156
|
</Box>
|
|
157
|
+
<Writers writers={currentWriters}></Writers>
|
|
147
158
|
</ScrollContainer>
|
|
148
159
|
<Navigation {...props} refMsgBox={refMsgBox} />
|
|
149
160
|
</>
|
|
@@ -163,10 +174,6 @@ type ChatMessageHeaderProps = {
|
|
|
163
174
|
*/
|
|
164
175
|
export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
165
176
|
const [datetime, setDatetime] = useState<Record<number, string>>({});
|
|
166
|
-
const sharedStyles: SxProps<Theme> = {
|
|
167
|
-
height: '24px',
|
|
168
|
-
width: '24px'
|
|
169
|
-
};
|
|
170
177
|
const message = props.message;
|
|
171
178
|
const sender = message.sender;
|
|
172
179
|
/**
|
|
@@ -206,32 +213,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
|
206
213
|
}
|
|
207
214
|
});
|
|
208
215
|
|
|
209
|
-
const
|
|
210
|
-
const avatar = message.stacked ? null : sender.avatar_url ? (
|
|
211
|
-
<Avatar
|
|
212
|
-
sx={{
|
|
213
|
-
...sharedStyles,
|
|
214
|
-
...(bgcolor && { bgcolor })
|
|
215
|
-
}}
|
|
216
|
-
src={sender.avatar_url}
|
|
217
|
-
></Avatar>
|
|
218
|
-
) : sender.initials ? (
|
|
219
|
-
<Avatar
|
|
220
|
-
sx={{
|
|
221
|
-
...sharedStyles,
|
|
222
|
-
...(bgcolor && { bgcolor })
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
<Typography
|
|
226
|
-
sx={{
|
|
227
|
-
fontSize: 'var(--jp-ui-font-size1)',
|
|
228
|
-
color: 'var(--jp-ui-inverse-font-color1)'
|
|
229
|
-
}}
|
|
230
|
-
>
|
|
231
|
-
{sender.initials}
|
|
232
|
-
</Typography>
|
|
233
|
-
</Avatar>
|
|
234
|
-
) : null;
|
|
216
|
+
const avatar = message.stacked ? null : Avatar({ user: sender });
|
|
235
217
|
|
|
236
218
|
const name =
|
|
237
219
|
sender.display_name ?? sender.name ?? (sender.username || 'User undefined');
|
|
@@ -393,6 +375,7 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
|
|
|
393
375
|
onSend={(input: string) => updateMessage(message.id, input)}
|
|
394
376
|
onCancel={() => cancelEdition()}
|
|
395
377
|
model={model}
|
|
378
|
+
hideIncludeSelection={true}
|
|
396
379
|
/>
|
|
397
380
|
) : (
|
|
398
381
|
<RendermimeMarkdown
|
|
@@ -407,6 +390,45 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
|
|
|
407
390
|
);
|
|
408
391
|
}
|
|
409
392
|
|
|
393
|
+
/**
|
|
394
|
+
* The writers component props.
|
|
395
|
+
*/
|
|
396
|
+
type writersProps = {
|
|
397
|
+
/**
|
|
398
|
+
* The list of users currently writing.
|
|
399
|
+
*/
|
|
400
|
+
writers: IUser[];
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The writers component, displaying the current writers.
|
|
405
|
+
*/
|
|
406
|
+
export function Writers(props: writersProps): JSX.Element | null {
|
|
407
|
+
const { writers } = props;
|
|
408
|
+
return writers.length > 0 ? (
|
|
409
|
+
<Box className={WRITERS_CLASS}>
|
|
410
|
+
{writers.map((writer, index) => (
|
|
411
|
+
<div>
|
|
412
|
+
<Avatar user={writer} small />
|
|
413
|
+
<span>
|
|
414
|
+
{writer.display_name ??
|
|
415
|
+
writer.name ??
|
|
416
|
+
(writer.username || 'User undefined')}
|
|
417
|
+
</span>
|
|
418
|
+
<span>
|
|
419
|
+
{index < writers.length - 1
|
|
420
|
+
? index < writers.length - 2
|
|
421
|
+
? ', '
|
|
422
|
+
: ' and '
|
|
423
|
+
: ''}
|
|
424
|
+
</span>
|
|
425
|
+
</div>
|
|
426
|
+
))}
|
|
427
|
+
<span>{(writers.length > 1 ? ' are' : ' is') + ' writing'}</span>
|
|
428
|
+
</Box>
|
|
429
|
+
) : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
410
432
|
/**
|
|
411
433
|
* The navigation component props.
|
|
412
434
|
*/
|
|
@@ -543,3 +565,61 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
543
565
|
</>
|
|
544
566
|
);
|
|
545
567
|
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The avatar props.
|
|
571
|
+
*/
|
|
572
|
+
type AvatarProps = {
|
|
573
|
+
/**
|
|
574
|
+
* The user to display an avatar.
|
|
575
|
+
*/
|
|
576
|
+
user: IUser;
|
|
577
|
+
/**
|
|
578
|
+
* Whether the avatar should be small.
|
|
579
|
+
*/
|
|
580
|
+
small?: boolean;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The avatar component.
|
|
585
|
+
*/
|
|
586
|
+
export function Avatar(props: AvatarProps): JSX.Element | null {
|
|
587
|
+
const { user } = props;
|
|
588
|
+
|
|
589
|
+
const sharedStyles: SxProps<Theme> = {
|
|
590
|
+
height: `${props.small ? '16' : '24'}px`,
|
|
591
|
+
width: `${props.small ? '16' : '24'}px`,
|
|
592
|
+
bgcolor: user.color,
|
|
593
|
+
fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const name =
|
|
597
|
+
user.display_name ?? user.name ?? (user.username || 'User undefined');
|
|
598
|
+
return user.avatar_url ? (
|
|
599
|
+
<MuiAvatar
|
|
600
|
+
sx={{
|
|
601
|
+
...sharedStyles
|
|
602
|
+
}}
|
|
603
|
+
src={user.avatar_url}
|
|
604
|
+
alt={name}
|
|
605
|
+
title={name}
|
|
606
|
+
></MuiAvatar>
|
|
607
|
+
) : user.initials ? (
|
|
608
|
+
<MuiAvatar
|
|
609
|
+
sx={{
|
|
610
|
+
...sharedStyles
|
|
611
|
+
}}
|
|
612
|
+
alt={name}
|
|
613
|
+
title={name}
|
|
614
|
+
>
|
|
615
|
+
<Typography
|
|
616
|
+
sx={{
|
|
617
|
+
fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`,
|
|
618
|
+
color: 'var(--jp-ui-inverse-font-color1)'
|
|
619
|
+
}}
|
|
620
|
+
>
|
|
621
|
+
{user.initials}
|
|
622
|
+
</Typography>
|
|
623
|
+
</MuiAvatar>
|
|
624
|
+
) : null;
|
|
625
|
+
}
|
package/src/components/chat.tsx
CHANGED
|
@@ -27,7 +27,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
|
|
|
27
27
|
// handled by the listeners registered in the effect hooks above.
|
|
28
28
|
const onSend = async (input: string) => {
|
|
29
29
|
// send message to backend
|
|
30
|
-
model.
|
|
30
|
+
model.sendMessage({ body: input });
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
return (
|
|
@@ -12,6 +12,7 @@ import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
|
|
|
12
12
|
import { IActiveCellManager } from '../../active-cell-manager';
|
|
13
13
|
import { replaceCellIcon } from '../../icons';
|
|
14
14
|
import { IChatModel } from '../../model';
|
|
15
|
+
import { ISelectionWatcher } from '../../selection-watcher';
|
|
15
16
|
|
|
16
17
|
const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
|
|
17
18
|
const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
|
|
@@ -34,25 +35,41 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
|
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
const activeCellManager = model.activeCellManager;
|
|
38
|
+
const selectionWatcher = model.selectionWatcher;
|
|
37
39
|
|
|
38
40
|
const [toolbarBtnProps, setToolbarBtnProps] = useState<ToolbarButtonProps>({
|
|
39
|
-
content
|
|
40
|
-
activeCellManager
|
|
41
|
-
|
|
41
|
+
content,
|
|
42
|
+
activeCellManager,
|
|
43
|
+
selectionWatcher,
|
|
44
|
+
activeCellAvailable: !!activeCellManager?.available,
|
|
45
|
+
selectionExists: !!selectionWatcher?.selection
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
useEffect(() => {
|
|
45
|
-
|
|
49
|
+
const toggleToolbar = () => {
|
|
50
|
+
setToolbarEnable(model.config.enableCodeToolbar ?? true);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const selectionStatusChange = () => {
|
|
46
54
|
setToolbarBtnProps({
|
|
47
55
|
content,
|
|
48
|
-
activeCellManager
|
|
49
|
-
|
|
56
|
+
activeCellManager,
|
|
57
|
+
selectionWatcher,
|
|
58
|
+
activeCellAvailable: !!activeCellManager?.available,
|
|
59
|
+
selectionExists: !!selectionWatcher?.selection
|
|
50
60
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
activeCellManager?.availabilityChanged.connect(selectionStatusChange);
|
|
64
|
+
selectionWatcher?.selectionChanged.connect(selectionStatusChange);
|
|
65
|
+
model.configChanged.connect(toggleToolbar);
|
|
66
|
+
|
|
67
|
+
selectionStatusChange();
|
|
68
|
+
return () => {
|
|
69
|
+
activeCellManager?.availabilityChanged.disconnect(selectionStatusChange);
|
|
70
|
+
selectionWatcher?.selectionChanged.disconnect(selectionStatusChange);
|
|
71
|
+
model.configChanged.disconnect(toggleToolbar);
|
|
72
|
+
};
|
|
56
73
|
}, [model]);
|
|
57
74
|
|
|
58
75
|
return activeCellManager === null || !toolbarEnable ? (
|
|
@@ -63,7 +80,7 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
|
|
|
63
80
|
display: 'flex',
|
|
64
81
|
justifyContent: 'flex-end',
|
|
65
82
|
alignItems: 'center',
|
|
66
|
-
padding: '
|
|
83
|
+
padding: '2px 2px',
|
|
67
84
|
marginBottom: '1em',
|
|
68
85
|
border: '1px solid var(--jp-cell-editor-border-color)',
|
|
69
86
|
borderTop: 'none'
|
|
@@ -86,8 +103,10 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
|
|
|
86
103
|
|
|
87
104
|
type ToolbarButtonProps = {
|
|
88
105
|
content: string;
|
|
89
|
-
activeCellAvailable?: boolean;
|
|
90
106
|
activeCellManager: IActiveCellManager | null;
|
|
107
|
+
activeCellAvailable?: boolean;
|
|
108
|
+
selectionWatcher: ISelectionWatcher | null;
|
|
109
|
+
selectionExists?: boolean;
|
|
91
110
|
className?: string;
|
|
92
111
|
};
|
|
93
112
|
|
|
@@ -126,16 +145,35 @@ function InsertBelowButton(props: ToolbarButtonProps) {
|
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
function ReplaceButton(props: ToolbarButtonProps) {
|
|
129
|
-
const tooltip = props.
|
|
130
|
-
?
|
|
131
|
-
:
|
|
148
|
+
const tooltip = props.selectionExists
|
|
149
|
+
? `Replace selection (${props.selectionWatcher?.selection?.numLines} line(s))`
|
|
150
|
+
: props.activeCellAvailable
|
|
151
|
+
? 'Replace selection (active cell)'
|
|
152
|
+
: 'Replace selection (no selection)';
|
|
153
|
+
|
|
154
|
+
const disabled = !props.activeCellAvailable && !props.selectionExists;
|
|
155
|
+
|
|
156
|
+
const replace = () => {
|
|
157
|
+
if (props.selectionExists) {
|
|
158
|
+
const selection = props.selectionWatcher?.selection;
|
|
159
|
+
if (!selection) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
props.selectionWatcher?.replaceSelection({
|
|
163
|
+
...selection,
|
|
164
|
+
text: props.content
|
|
165
|
+
});
|
|
166
|
+
} else if (props.activeCellAvailable) {
|
|
167
|
+
props.activeCellManager?.replace(props.content);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
132
170
|
|
|
133
171
|
return (
|
|
134
172
|
<TooltippedIconButton
|
|
135
173
|
className={props.className}
|
|
136
174
|
tooltip={tooltip}
|
|
137
|
-
disabled={
|
|
138
|
-
onClick={
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
onClick={replace}
|
|
139
177
|
>
|
|
140
178
|
<replaceCellIcon.react height="16px" width="16px" />
|
|
141
179
|
</TooltippedIconButton>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { TooltippedButton } from '../mui-extras/tooltipped-button';
|
|
9
|
+
|
|
10
|
+
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The cancel button props.
|
|
14
|
+
*/
|
|
15
|
+
export type CancelButtonProps = {
|
|
16
|
+
inputExists: boolean;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The cancel button.
|
|
22
|
+
*/
|
|
23
|
+
export function CancelButton(props: CancelButtonProps): JSX.Element {
|
|
24
|
+
const tooltip = 'Cancel edition';
|
|
25
|
+
const disabled = !props.inputExists;
|
|
26
|
+
return (
|
|
27
|
+
<TooltippedButton
|
|
28
|
+
onClick={props.onCancel}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
tooltip={tooltip}
|
|
31
|
+
buttonProps={{
|
|
32
|
+
size: 'small',
|
|
33
|
+
variant: 'contained',
|
|
34
|
+
title: tooltip,
|
|
35
|
+
className: CANCEL_BUTTON_CLASS
|
|
36
|
+
}}
|
|
37
|
+
sx={{
|
|
38
|
+
minWidth: 'unset',
|
|
39
|
+
padding: '4px',
|
|
40
|
+
borderRadius: '2px 0px 0px 2px',
|
|
41
|
+
marginRight: '1px'
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<CancelIcon />
|
|
45
|
+
</TooltippedButton>
|
|
46
|
+
);
|
|
47
|
+
}
|