@jupyter/chat 0.4.0 → 0.5.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 +55 -19
- package/lib/components/chat.js +1 -1
- package/lib/components/code-blocks/code-toolbar.js +50 -16
- 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/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- 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 +106 -32
- package/src/components/chat.tsx +1 -1
- package/src/components/code-blocks/code-toolbar.tsx +55 -17
- 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/icons.ts +6 -0
- package/src/index.ts +1 -0
- 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
|
@@ -27,6 +27,10 @@ export declare namespace ChatInput {
|
|
|
27
27
|
* The function to be called to cancel editing.
|
|
28
28
|
*/
|
|
29
29
|
onCancel?: () => unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to allow or not including selection.
|
|
32
|
+
*/
|
|
33
|
+
hideIncludeSelection?: boolean;
|
|
30
34
|
/**
|
|
31
35
|
* Custom mui/material styles.
|
|
32
36
|
*/
|
|
@@ -3,25 +3,32 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import React, { useEffect, useRef, useState } from 'react';
|
|
6
|
-
import { Autocomplete, Box,
|
|
7
|
-
import { Send, Cancel } from '@mui/icons-material';
|
|
6
|
+
import { Autocomplete, Box, InputAdornment, TextField } from '@mui/material';
|
|
8
7
|
import clsx from 'clsx';
|
|
8
|
+
import { CancelButton } from './input/cancel-button';
|
|
9
|
+
import { SendButton } from './input/send-button';
|
|
9
10
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
10
|
-
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
11
|
-
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
12
11
|
export function ChatInput(props) {
|
|
13
|
-
var _a, _b;
|
|
12
|
+
var _a, _b, _c, _d;
|
|
14
13
|
const { autocompletionName, autocompletionRegistry, model } = props;
|
|
15
14
|
const autocompletion = useRef();
|
|
16
15
|
const [input, setInput] = useState(props.value || '');
|
|
17
16
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
17
|
+
const [typingNotification, setTypingNotification] = useState((_b = model.config.sendTypingNotification) !== null && _b !== void 0 ? _b : false);
|
|
18
|
+
// Display the include selection menu if it is not explicitly hidden, and if at least
|
|
19
|
+
// one of the tool to check for text or cell selection is enabled.
|
|
20
|
+
let hideIncludeSelection = (_c = props.hideIncludeSelection) !== null && _c !== void 0 ? _c : false;
|
|
21
|
+
if (model.activeCellManager === null && model.selectionWatcher === null) {
|
|
22
|
+
hideIncludeSelection = true;
|
|
23
|
+
}
|
|
18
24
|
// store reference to the input element to enable focusing it easily
|
|
19
25
|
const inputRef = useRef();
|
|
20
26
|
useEffect(() => {
|
|
21
27
|
var _a;
|
|
22
28
|
const configChanged = (_, config) => {
|
|
23
|
-
var _a;
|
|
29
|
+
var _a, _b;
|
|
24
30
|
setSendWithShiftEnter((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
31
|
+
setTypingNotification((_b = config.sendTypingNotification) !== null && _b !== void 0 ? _b : false);
|
|
25
32
|
};
|
|
26
33
|
model.configChanged.connect(configChanged);
|
|
27
34
|
const focusInputElement = () => {
|
|
@@ -104,10 +111,21 @@ export function ChatInput(props) {
|
|
|
104
111
|
}
|
|
105
112
|
/**
|
|
106
113
|
* Triggered when sending the message.
|
|
114
|
+
*
|
|
115
|
+
* Add code block if cell or text is selected.
|
|
107
116
|
*/
|
|
108
|
-
function onSend() {
|
|
117
|
+
function onSend(selection) {
|
|
118
|
+
let content = input;
|
|
119
|
+
if (selection) {
|
|
120
|
+
content += `
|
|
121
|
+
|
|
122
|
+
\`\`\`
|
|
123
|
+
${selection.source}
|
|
124
|
+
\`\`\`
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
props.onSend(content);
|
|
109
128
|
setInput('');
|
|
110
|
-
props.onSend(input);
|
|
111
129
|
}
|
|
112
130
|
/**
|
|
113
131
|
* Triggered when cancelling edition.
|
|
@@ -149,16 +167,15 @@ export function ChatInput(props) {
|
|
|
149
167
|
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, InputProps: {
|
|
150
168
|
...params.InputProps,
|
|
151
169
|
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
|
152
|
-
props.onCancel && (React.createElement(
|
|
153
|
-
|
|
154
|
-
React.createElement(IconButton, { size: "small", color: "primary", onClick: onSend, disabled: props.onCancel
|
|
155
|
-
? input === props.value
|
|
156
|
-
: !input.trim().length, title: `Send message ${sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`, className: clsx(SEND_BUTTON_CLASS) },
|
|
157
|
-
React.createElement(Send, null))))
|
|
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 })))
|
|
158
172
|
}, FormHelperTextProps: {
|
|
159
173
|
sx: { marginLeft: 'auto', marginRight: 0 }
|
|
160
|
-
}, helperText: input.length > 2 ? helperText : ' ' })), ...(
|
|
174
|
+
}, helperText: input.length > 2 ? helperText : ' ' })), ...(_d = autocompletion.current) === null || _d === void 0 ? void 0 : _d.props, inputValue: input, onInputChange: (_, newValue) => {
|
|
161
175
|
setInput(newValue);
|
|
176
|
+
if (typingNotification && model.inputChanged) {
|
|
177
|
+
model.inputChanged(newValue);
|
|
178
|
+
}
|
|
162
179
|
}, onHighlightChange:
|
|
163
180
|
/**
|
|
164
181
|
* On highlight change: set `highlighted` to whether an option is
|
|
@@ -2,7 +2,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
2
2
|
import type { SxProps, Theme } from '@mui/material';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { IChatModel } from '../model';
|
|
5
|
-
import { IChatMessage } from '../types';
|
|
5
|
+
import { IChatMessage, IUser } from '../types';
|
|
6
6
|
/**
|
|
7
7
|
* The base components props.
|
|
8
8
|
*/
|
|
@@ -46,6 +46,19 @@ type ChatMessageProps = BaseMessageProps & {
|
|
|
46
46
|
* The message component body.
|
|
47
47
|
*/
|
|
48
48
|
export declare function ChatMessage(props: ChatMessageProps): JSX.Element;
|
|
49
|
+
/**
|
|
50
|
+
* The writers component props.
|
|
51
|
+
*/
|
|
52
|
+
type writersProps = {
|
|
53
|
+
/**
|
|
54
|
+
* The list of users currently writing.
|
|
55
|
+
*/
|
|
56
|
+
writers: IUser[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The writers component, displaying the current writers.
|
|
60
|
+
*/
|
|
61
|
+
export declare function Writers(props: writersProps): JSX.Element | null;
|
|
49
62
|
/**
|
|
50
63
|
* The navigation component props.
|
|
51
64
|
*/
|
|
@@ -59,4 +72,21 @@ type NavigationProps = BaseMessageProps & {
|
|
|
59
72
|
* The navigation component, to navigate to unread messages.
|
|
60
73
|
*/
|
|
61
74
|
export declare function Navigation(props: NavigationProps): JSX.Element;
|
|
75
|
+
/**
|
|
76
|
+
* The avatar props.
|
|
77
|
+
*/
|
|
78
|
+
type AvatarProps = {
|
|
79
|
+
/**
|
|
80
|
+
* The user to display an avatar.
|
|
81
|
+
*/
|
|
82
|
+
user: IUser;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the avatar should be small.
|
|
85
|
+
*/
|
|
86
|
+
small?: boolean;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* the avatar component.
|
|
90
|
+
*/
|
|
91
|
+
export declare function Avatar(props: AvatarProps): JSX.Element | null;
|
|
62
92
|
export {};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Button } from '@jupyter/react-components';
|
|
6
6
|
import { LabIcon, caretDownEmptyIcon, classes } from '@jupyterlab/ui-components';
|
|
7
|
-
import { Avatar, Box, Typography } from '@mui/material';
|
|
7
|
+
import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
|
|
8
8
|
import clsx from 'clsx';
|
|
9
9
|
import React, { useEffect, useState, useRef } from 'react';
|
|
10
10
|
import { ChatInput } from './chat-input';
|
|
@@ -15,6 +15,7 @@ const MESSAGE_CLASS = 'jp-chat-message';
|
|
|
15
15
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
16
16
|
const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
|
|
17
17
|
const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
18
|
+
const WRITERS_CLASS = 'jp-chat-writers';
|
|
18
19
|
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
19
20
|
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
|
|
20
21
|
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
|
|
@@ -27,6 +28,7 @@ export function ChatMessages(props) {
|
|
|
27
28
|
const [messages, setMessages] = useState(model.messages);
|
|
28
29
|
const refMsgBox = useRef(null);
|
|
29
30
|
const inViewport = useRef([]);
|
|
31
|
+
const [currentWriters, setCurrentWriters] = useState([]);
|
|
30
32
|
// The intersection observer that listen to all the message visibility.
|
|
31
33
|
const observerRef = useRef(new IntersectionObserver(viewportChange));
|
|
32
34
|
/**
|
|
@@ -43,17 +45,25 @@ export function ChatMessages(props) {
|
|
|
43
45
|
.catch(e => console.error(e));
|
|
44
46
|
}
|
|
45
47
|
fetchHistory();
|
|
48
|
+
setCurrentWriters([]);
|
|
46
49
|
}, [model]);
|
|
47
50
|
/**
|
|
48
51
|
* Effect: listen to chat messages.
|
|
49
52
|
*/
|
|
50
53
|
useEffect(() => {
|
|
54
|
+
var _a;
|
|
51
55
|
function handleChatEvents() {
|
|
52
56
|
setMessages([...model.messages]);
|
|
53
57
|
}
|
|
58
|
+
function handleWritersChange(_, writers) {
|
|
59
|
+
setCurrentWriters(writers);
|
|
60
|
+
}
|
|
54
61
|
model.messagesUpdated.connect(handleChatEvents);
|
|
62
|
+
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(handleWritersChange);
|
|
55
63
|
return function cleanup() {
|
|
64
|
+
var _a;
|
|
56
65
|
model.messagesUpdated.disconnect(handleChatEvents);
|
|
66
|
+
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(handleChatEvents);
|
|
57
67
|
};
|
|
58
68
|
}, [model]);
|
|
59
69
|
/**
|
|
@@ -99,7 +109,8 @@ export function ChatMessages(props) {
|
|
|
99
109
|
React.createElement(Box, { key: i, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
100
110
|
React.createElement(ChatMessageHeader, { message: message }),
|
|
101
111
|
React.createElement(ChatMessage, { ...props, message: message, observer: observerRef.current, index: i })));
|
|
102
|
-
}))
|
|
112
|
+
})),
|
|
113
|
+
React.createElement(Writers, { writers: currentWriters })),
|
|
103
114
|
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox })));
|
|
104
115
|
}
|
|
105
116
|
/**
|
|
@@ -108,10 +119,6 @@ export function ChatMessages(props) {
|
|
|
108
119
|
export function ChatMessageHeader(props) {
|
|
109
120
|
var _a, _b;
|
|
110
121
|
const [datetime, setDatetime] = useState({});
|
|
111
|
-
const sharedStyles = {
|
|
112
|
-
height: '24px',
|
|
113
|
-
width: '24px'
|
|
114
|
-
};
|
|
115
122
|
const message = props.message;
|
|
116
123
|
const sender = message.sender;
|
|
117
124
|
/**
|
|
@@ -148,18 +155,7 @@ export function ChatMessageHeader(props) {
|
|
|
148
155
|
setDatetime(newDatetime);
|
|
149
156
|
}
|
|
150
157
|
});
|
|
151
|
-
const
|
|
152
|
-
const avatar = message.stacked ? null : sender.avatar_url ? (React.createElement(Avatar, { sx: {
|
|
153
|
-
...sharedStyles,
|
|
154
|
-
...(bgcolor && { bgcolor })
|
|
155
|
-
}, src: sender.avatar_url })) : sender.initials ? (React.createElement(Avatar, { sx: {
|
|
156
|
-
...sharedStyles,
|
|
157
|
-
...(bgcolor && { bgcolor })
|
|
158
|
-
} },
|
|
159
|
-
React.createElement(Typography, { sx: {
|
|
160
|
-
fontSize: 'var(--jp-ui-font-size1)',
|
|
161
|
-
color: 'var(--jp-ui-inverse-font-color1)'
|
|
162
|
-
} }, sender.initials))) : null;
|
|
158
|
+
const avatar = message.stacked ? null : Avatar({ user: sender });
|
|
163
159
|
const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || 'User undefined');
|
|
164
160
|
return (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
|
|
165
161
|
display: 'flex',
|
|
@@ -257,7 +253,26 @@ export function ChatMessage(props) {
|
|
|
257
253
|
model.deleteMessage(id);
|
|
258
254
|
};
|
|
259
255
|
// 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 }))));
|
|
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
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The writers component, displaying the current writers.
|
|
260
|
+
*/
|
|
261
|
+
export function Writers(props) {
|
|
262
|
+
const { writers } = props;
|
|
263
|
+
return writers.length > 0 ? (React.createElement(Box, { className: WRITERS_CLASS },
|
|
264
|
+
writers.map((writer, index) => {
|
|
265
|
+
var _a, _b;
|
|
266
|
+
return (React.createElement("div", null,
|
|
267
|
+
React.createElement(Avatar, { user: writer, small: true }),
|
|
268
|
+
React.createElement("span", null, (_b = (_a = writer.display_name) !== null && _a !== void 0 ? _a : writer.name) !== null && _b !== void 0 ? _b : (writer.username || 'User undefined')),
|
|
269
|
+
React.createElement("span", null, index < writers.length - 1
|
|
270
|
+
? index < writers.length - 2
|
|
271
|
+
? ', '
|
|
272
|
+
: ' and '
|
|
273
|
+
: '')));
|
|
274
|
+
}),
|
|
275
|
+
React.createElement("span", null, (writers.length > 1 ? ' are' : ' is') + ' writing'))) : null;
|
|
261
276
|
}
|
|
262
277
|
/**
|
|
263
278
|
* The navigation component, to navigate to unread messages.
|
|
@@ -342,3 +357,24 @@ export function Navigation(props) {
|
|
|
342
357
|
: 'Go to last message' },
|
|
343
358
|
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') })))));
|
|
344
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* the avatar component.
|
|
362
|
+
*/
|
|
363
|
+
export function Avatar(props) {
|
|
364
|
+
const { user } = props;
|
|
365
|
+
const sharedStyles = {
|
|
366
|
+
height: `${props.small ? '16' : '24'}px`,
|
|
367
|
+
width: `${props.small ? '16' : '24'}px`,
|
|
368
|
+
bgcolor: user.color,
|
|
369
|
+
fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`
|
|
370
|
+
};
|
|
371
|
+
return user.avatar_url ? (React.createElement(MuiAvatar, { sx: {
|
|
372
|
+
...sharedStyles
|
|
373
|
+
}, src: user.avatar_url })) : user.initials ? (React.createElement(MuiAvatar, { sx: {
|
|
374
|
+
...sharedStyles
|
|
375
|
+
} },
|
|
376
|
+
React.createElement(Typography, { sx: {
|
|
377
|
+
fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`,
|
|
378
|
+
color: 'var(--jp-ui-inverse-font-color1)'
|
|
379
|
+
} }, user.initials))) : null;
|
|
380
|
+
}
|
package/lib/components/chat.js
CHANGED
|
@@ -16,7 +16,7 @@ export function ChatBody(props) {
|
|
|
16
16
|
// handled by the listeners registered in the effect hooks above.
|
|
17
17
|
const onSend = async (input) => {
|
|
18
18
|
// send message to backend
|
|
19
|
-
model.
|
|
19
|
+
model.sendMessage({ body: input });
|
|
20
20
|
};
|
|
21
21
|
return (React.createElement(React.Fragment, null,
|
|
22
22
|
React.createElement(ChatMessages, { rmRegistry: renderMimeRegistry, model: model }),
|
|
@@ -11,27 +11,41 @@ import { replaceCellIcon } from '../../icons';
|
|
|
11
11
|
const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
|
|
12
12
|
const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
|
|
13
13
|
export function CodeToolbar(props) {
|
|
14
|
-
var _a
|
|
14
|
+
var _a;
|
|
15
15
|
const { content, model } = props;
|
|
16
16
|
const [toolbarEnable, setToolbarEnable] = useState((_a = model.config.enableCodeToolbar) !== null && _a !== void 0 ? _a : true);
|
|
17
17
|
const activeCellManager = model.activeCellManager;
|
|
18
|
+
const selectionWatcher = model.selectionWatcher;
|
|
18
19
|
const [toolbarBtnProps, setToolbarBtnProps] = useState({
|
|
19
|
-
content
|
|
20
|
-
activeCellManager
|
|
21
|
-
|
|
20
|
+
content,
|
|
21
|
+
activeCellManager,
|
|
22
|
+
selectionWatcher,
|
|
23
|
+
activeCellAvailable: !!(activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available),
|
|
24
|
+
selectionExists: !!(selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection)
|
|
22
25
|
});
|
|
23
26
|
useEffect(() => {
|
|
24
|
-
|
|
27
|
+
const toggleToolbar = () => {
|
|
28
|
+
var _a;
|
|
29
|
+
setToolbarEnable((_a = model.config.enableCodeToolbar) !== null && _a !== void 0 ? _a : true);
|
|
30
|
+
};
|
|
31
|
+
const selectionStatusChange = () => {
|
|
25
32
|
setToolbarBtnProps({
|
|
26
33
|
content,
|
|
27
|
-
activeCellManager
|
|
28
|
-
|
|
34
|
+
activeCellManager,
|
|
35
|
+
selectionWatcher,
|
|
36
|
+
activeCellAvailable: !!(activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available),
|
|
37
|
+
selectionExists: !!(selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection)
|
|
29
38
|
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
};
|
|
40
|
+
activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.connect(selectionStatusChange);
|
|
41
|
+
selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selectionChanged.connect(selectionStatusChange);
|
|
42
|
+
model.configChanged.connect(toggleToolbar);
|
|
43
|
+
selectionStatusChange();
|
|
44
|
+
return () => {
|
|
45
|
+
activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.disconnect(selectionStatusChange);
|
|
46
|
+
selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selectionChanged.disconnect(selectionStatusChange);
|
|
47
|
+
model.configChanged.disconnect(toggleToolbar);
|
|
48
|
+
};
|
|
35
49
|
}, [model]);
|
|
36
50
|
return activeCellManager === null || !toolbarEnable ? (React.createElement(React.Fragment, null)) : (React.createElement(Box, { sx: {
|
|
37
51
|
display: 'flex',
|
|
@@ -62,9 +76,29 @@ function InsertBelowButton(props) {
|
|
|
62
76
|
React.createElement(addBelowIcon.react, { height: "16px", width: "16px" })));
|
|
63
77
|
}
|
|
64
78
|
function ReplaceButton(props) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const tooltip = props.selectionExists
|
|
81
|
+
? `Replace selection (${(_b = (_a = props.selectionWatcher) === null || _a === void 0 ? void 0 : _a.selection) === null || _b === void 0 ? void 0 : _b.numLines} line(s))`
|
|
82
|
+
: props.activeCellAvailable
|
|
83
|
+
? 'Replace selection (active cell)'
|
|
84
|
+
: 'Replace selection (no selection)';
|
|
85
|
+
const disabled = !props.activeCellAvailable && !props.selectionExists;
|
|
86
|
+
const replace = () => {
|
|
87
|
+
var _a, _b, _c;
|
|
88
|
+
if (props.selectionExists) {
|
|
89
|
+
const selection = (_a = props.selectionWatcher) === null || _a === void 0 ? void 0 : _a.selection;
|
|
90
|
+
if (!selection) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
(_b = props.selectionWatcher) === null || _b === void 0 ? void 0 : _b.replaceSelection({
|
|
94
|
+
...selection,
|
|
95
|
+
text: props.content
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if (props.activeCellAvailable) {
|
|
99
|
+
(_c = props.activeCellManager) === null || _c === void 0 ? void 0 : _c.replace(props.content);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, disabled: disabled, onClick: replace },
|
|
69
103
|
React.createElement(replaceCellIcon.react, { height: "16px", width: "16px" })));
|
|
70
104
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* The cancel button props.
|
|
4
|
+
*/
|
|
5
|
+
export type CancelButtonProps = {
|
|
6
|
+
inputExists: boolean;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The cancel button.
|
|
11
|
+
*/
|
|
12
|
+
export declare function CancelButton(props: CancelButtonProps): JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { TooltippedButton } from '../mui-extras/tooltipped-button';
|
|
8
|
+
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
9
|
+
/**
|
|
10
|
+
* The cancel button.
|
|
11
|
+
*/
|
|
12
|
+
export function CancelButton(props) {
|
|
13
|
+
const tooltip = 'Cancel edition';
|
|
14
|
+
const disabled = !props.inputExists;
|
|
15
|
+
return (React.createElement(TooltippedButton, { onClick: props.onCancel, disabled: disabled, tooltip: tooltip, buttonProps: {
|
|
16
|
+
size: 'small',
|
|
17
|
+
variant: 'contained',
|
|
18
|
+
title: tooltip,
|
|
19
|
+
className: CANCEL_BUTTON_CLASS
|
|
20
|
+
}, sx: {
|
|
21
|
+
minWidth: 'unset',
|
|
22
|
+
padding: '4px',
|
|
23
|
+
borderRadius: '2px 0px 0px 2px',
|
|
24
|
+
marginRight: '1px'
|
|
25
|
+
} },
|
|
26
|
+
React.createElement(CancelIcon, null)));
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IChatModel } from '../../model';
|
|
3
|
+
import { Selection } from '../../types';
|
|
4
|
+
/**
|
|
5
|
+
* The send button props.
|
|
6
|
+
*/
|
|
7
|
+
export type SendButtonProps = {
|
|
8
|
+
model: IChatModel;
|
|
9
|
+
sendWithShiftEnter: boolean;
|
|
10
|
+
inputExists: boolean;
|
|
11
|
+
onSend: (selection?: Selection) => unknown;
|
|
12
|
+
hideIncludeSelection?: boolean;
|
|
13
|
+
hasButtonOnLeft?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The send button, with optional 'include selection' menu.
|
|
17
|
+
*/
|
|
18
|
+
export declare function SendButton(props: SendButtonProps): JSX.Element;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
|
|
6
|
+
import SendIcon from '@mui/icons-material/Send';
|
|
7
|
+
import { Box, Menu, MenuItem, Typography } from '@mui/material';
|
|
8
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
9
|
+
import { TooltippedButton } from '../mui-extras/tooltipped-button';
|
|
10
|
+
import { includeSelectionIcon } from '../../icons';
|
|
11
|
+
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
12
|
+
const SEND_INCLUDE_OPENER_CLASS = 'jp-chat-send-include-opener';
|
|
13
|
+
const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
|
|
14
|
+
/**
|
|
15
|
+
* The send button, with optional 'include selection' menu.
|
|
16
|
+
*/
|
|
17
|
+
export function SendButton(props) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const { activeCellManager, selectionWatcher } = props.model;
|
|
20
|
+
const hideIncludeSelection = (_a = props.hideIncludeSelection) !== null && _a !== void 0 ? _a : false;
|
|
21
|
+
const hasButtonOnLeft = (_b = props.hasButtonOnLeft) !== null && _b !== void 0 ? _b : false;
|
|
22
|
+
const [menuAnchorEl, setMenuAnchorEl] = useState(null);
|
|
23
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
24
|
+
const openMenu = useCallback((el) => {
|
|
25
|
+
setMenuAnchorEl(el);
|
|
26
|
+
setMenuOpen(true);
|
|
27
|
+
}, []);
|
|
28
|
+
const closeMenu = useCallback(() => {
|
|
29
|
+
setMenuOpen(false);
|
|
30
|
+
}, []);
|
|
31
|
+
const disabled = !props.inputExists;
|
|
32
|
+
const [selectionTooltip, setSelectionTooltip] = useState('');
|
|
33
|
+
const [disableInclude, setDisableInclude] = useState(true);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
/**
|
|
36
|
+
* Enable or disable the include selection button, and adapt the tooltip.
|
|
37
|
+
*/
|
|
38
|
+
const toggleIncludeState = () => {
|
|
39
|
+
setDisableInclude(!((selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection) || (activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available)));
|
|
40
|
+
const tooltip = (selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection)
|
|
41
|
+
? `${selectionWatcher.selection.numLines} line(s) selected`
|
|
42
|
+
: (activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available)
|
|
43
|
+
? 'Code from 1 active cell'
|
|
44
|
+
: 'No selection or active cell';
|
|
45
|
+
setSelectionTooltip(tooltip);
|
|
46
|
+
};
|
|
47
|
+
if (!hideIncludeSelection) {
|
|
48
|
+
selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selectionChanged.connect(toggleIncludeState);
|
|
49
|
+
activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.connect(toggleIncludeState);
|
|
50
|
+
toggleIncludeState();
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selectionChanged.disconnect(toggleIncludeState);
|
|
54
|
+
activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.disconnect(toggleIncludeState);
|
|
55
|
+
};
|
|
56
|
+
}, [activeCellManager, selectionWatcher, hideIncludeSelection]);
|
|
57
|
+
const defaultTooltip = props.sendWithShiftEnter
|
|
58
|
+
? 'Send message (SHIFT+ENTER)'
|
|
59
|
+
: 'Send message (ENTER)';
|
|
60
|
+
const tooltip = defaultTooltip;
|
|
61
|
+
function sendWithSelection() {
|
|
62
|
+
// Append the selected text if exists.
|
|
63
|
+
if (selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection) {
|
|
64
|
+
props.onSend({
|
|
65
|
+
type: 'text',
|
|
66
|
+
source: selectionWatcher.selection.text
|
|
67
|
+
});
|
|
68
|
+
closeMenu();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Append the active cell content if exists.
|
|
72
|
+
if (activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available) {
|
|
73
|
+
props.onSend({
|
|
74
|
+
type: 'cell',
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
76
|
+
source: activeCellManager.getContent(false).source
|
|
77
|
+
});
|
|
78
|
+
closeMenu();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return (React.createElement(Box, { sx: { display: 'flex', flexWrap: 'nowrap' } },
|
|
83
|
+
React.createElement(TooltippedButton, { onClick: () => props.onSend(), disabled: disabled, tooltip: tooltip, buttonProps: {
|
|
84
|
+
size: 'small',
|
|
85
|
+
title: defaultTooltip,
|
|
86
|
+
variant: 'contained',
|
|
87
|
+
className: SEND_BUTTON_CLASS
|
|
88
|
+
}, sx: {
|
|
89
|
+
minWidth: 'unset',
|
|
90
|
+
borderTopLeftRadius: hasButtonOnLeft ? '0px' : '2px',
|
|
91
|
+
borderTopRightRadius: hideIncludeSelection ? '2px' : '0px',
|
|
92
|
+
borderBottomRightRadius: hideIncludeSelection ? '2px' : '0px',
|
|
93
|
+
borderBottomLeftRadius: hasButtonOnLeft ? '0px' : '2px'
|
|
94
|
+
} },
|
|
95
|
+
React.createElement(SendIcon, null)),
|
|
96
|
+
!hideIncludeSelection && (React.createElement(React.Fragment, null,
|
|
97
|
+
React.createElement(TooltippedButton, { onClick: e => {
|
|
98
|
+
openMenu(e.currentTarget);
|
|
99
|
+
}, disabled: disabled, tooltip: "", buttonProps: {
|
|
100
|
+
variant: 'contained',
|
|
101
|
+
onKeyDown: e => {
|
|
102
|
+
if (e.key !== 'Enter' && e.key !== ' ') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
openMenu(e.currentTarget);
|
|
106
|
+
// stopping propagation of this event prevents the prompt from being
|
|
107
|
+
// sent when the dropdown button is selected and clicked via 'Enter'.
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
},
|
|
110
|
+
className: SEND_INCLUDE_OPENER_CLASS
|
|
111
|
+
}, sx: {
|
|
112
|
+
minWidth: 'unset',
|
|
113
|
+
padding: '4px 0px',
|
|
114
|
+
borderRadius: '0px 2px 2px 0px',
|
|
115
|
+
marginLeft: '1px'
|
|
116
|
+
} },
|
|
117
|
+
React.createElement(KeyboardArrowDown, null)),
|
|
118
|
+
React.createElement(Menu, { open: menuOpen, onClose: closeMenu, anchorEl: menuAnchorEl, anchorOrigin: {
|
|
119
|
+
vertical: 'top',
|
|
120
|
+
horizontal: 'right'
|
|
121
|
+
}, transformOrigin: {
|
|
122
|
+
vertical: 'bottom',
|
|
123
|
+
horizontal: 'right'
|
|
124
|
+
}, sx: {
|
|
125
|
+
'& .MuiMenuItem-root': {
|
|
126
|
+
display: 'flex',
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
gap: '8px'
|
|
129
|
+
},
|
|
130
|
+
'& svg': {
|
|
131
|
+
lineHeight: 0
|
|
132
|
+
}
|
|
133
|
+
} },
|
|
134
|
+
React.createElement(MenuItem, { onClick: e => {
|
|
135
|
+
sendWithSelection();
|
|
136
|
+
// prevent sending second message with no selection
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
}, disabled: disableInclude, className: SEND_INCLUDE_LI_CLASS },
|
|
139
|
+
React.createElement(includeSelectionIcon.react, null),
|
|
140
|
+
React.createElement(Box, null,
|
|
141
|
+
React.createElement(Typography, { display: "block" }, "Send message with selection"),
|
|
142
|
+
React.createElement(Typography, { display: "block", sx: { opacity: 0.618 } }, selectionTooltip))))))));
|
|
143
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ButtonProps, SxProps, TooltipProps } from '@mui/material';
|
|
3
|
+
export type TooltippedButtonProps = {
|
|
4
|
+
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
|
5
|
+
tooltip: string;
|
|
6
|
+
children: JSX.Element;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
placement?: TooltipProps['placement'];
|
|
9
|
+
/**
|
|
10
|
+
* The offset of the tooltip popup.
|
|
11
|
+
*
|
|
12
|
+
* The expected syntax is defined by the Popper library:
|
|
13
|
+
* https://popper.js.org/docs/v2/modifiers/offset/
|
|
14
|
+
*/
|
|
15
|
+
offset?: [number, number];
|
|
16
|
+
'aria-label'?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Props passed directly to the MUI `Button` component.
|
|
19
|
+
*/
|
|
20
|
+
buttonProps?: ButtonProps;
|
|
21
|
+
/**
|
|
22
|
+
* Styles applied to the MUI `Button` component.
|
|
23
|
+
*/
|
|
24
|
+
sx?: SxProps;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A component that renders an MUI `Button` with a high-contrast tooltip
|
|
28
|
+
* provided by `ContrastingTooltip`. This component differs from the MUI
|
|
29
|
+
* defaults in the following ways:
|
|
30
|
+
*
|
|
31
|
+
* - Shows the tooltip on hover even if disabled.
|
|
32
|
+
* - Renders the tooltip above the button by default.
|
|
33
|
+
* - Renders the tooltip closer to the button by default.
|
|
34
|
+
* - Lowers the opacity of the Button when disabled.
|
|
35
|
+
* - Renders the Button with `line-height: 0` to avoid showing extra
|
|
36
|
+
* vertical space in SVG icons.
|
|
37
|
+
*
|
|
38
|
+
* NOTE TO DEVS: Please keep this component's features synchronized with
|
|
39
|
+
* features available to `TooltippedIconButton`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function TooltippedButton(props: TooltippedButtonProps): JSX.Element;
|