@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.
Files changed (41) hide show
  1. package/lib/active-cell-manager.d.ts +3 -0
  2. package/lib/components/chat-input.d.ts +4 -0
  3. package/lib/components/chat-input.js +32 -15
  4. package/lib/components/chat-messages.d.ts +31 -1
  5. package/lib/components/chat-messages.js +55 -19
  6. package/lib/components/chat.js +1 -1
  7. package/lib/components/code-blocks/code-toolbar.js +50 -16
  8. package/lib/components/input/cancel-button.d.ts +12 -0
  9. package/lib/components/input/cancel-button.js +27 -0
  10. package/lib/components/input/send-button.d.ts +18 -0
  11. package/lib/components/input/send-button.js +143 -0
  12. package/lib/components/mui-extras/tooltipped-button.d.ts +41 -0
  13. package/lib/components/mui-extras/tooltipped-button.js +43 -0
  14. package/lib/icons.d.ts +1 -0
  15. package/lib/icons.js +5 -0
  16. package/lib/index.d.ts +1 -0
  17. package/lib/index.js +1 -0
  18. package/lib/model.d.ts +51 -8
  19. package/lib/model.js +44 -12
  20. package/lib/selection-watcher.d.ts +62 -0
  21. package/lib/selection-watcher.js +134 -0
  22. package/lib/types.d.ts +22 -0
  23. package/lib/utils.d.ts +11 -0
  24. package/lib/utils.js +37 -0
  25. package/package.json +2 -1
  26. package/src/active-cell-manager.ts +3 -0
  27. package/src/components/chat-input.tsx +48 -30
  28. package/src/components/chat-messages.tsx +106 -32
  29. package/src/components/chat.tsx +1 -1
  30. package/src/components/code-blocks/code-toolbar.tsx +55 -17
  31. package/src/components/input/cancel-button.tsx +47 -0
  32. package/src/components/input/send-button.tsx +210 -0
  33. package/src/components/mui-extras/tooltipped-button.tsx +92 -0
  34. package/src/icons.ts +6 -0
  35. package/src/index.ts +1 -0
  36. package/src/model.ts +77 -13
  37. package/src/selection-watcher.ts +221 -0
  38. package/src/types.ts +25 -0
  39. package/src/utils.ts +47 -0
  40. package/style/chat.css +13 -0
  41. package/style/icons/include-selection.svg +5 -0
@@ -15,6 +15,9 @@ type CellWithErrorContent = {
15
15
  traceback: string[];
16
16
  };
17
17
  };
18
+ /**
19
+ * The active cell interface.
20
+ */
18
21
  export interface IActiveCellManager {
19
22
  /**
20
23
  * Whether the notebook is available and an active cell exists.
@@ -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, IconButton, InputAdornment, TextField } from '@mui/material';
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(IconButton, { size: "small", color: "primary", onClick: onCancel, title: 'Cancel edition', className: clsx(CANCEL_BUTTON_CLASS) },
153
- React.createElement(Cancel, null))),
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 : ' ' })), ...(_b = autocompletion.current) === null || _b === void 0 ? void 0 : _b.props, inputValue: input, onInputChange: (_, newValue) => {
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 bgcolor = sender.color;
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
+ }
@@ -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.addMessage({ body: input });
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, _b;
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: content,
20
- activeCellManager: activeCellManager,
21
- activeCellAvailable: (_b = activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available) !== null && _b !== void 0 ? _b : false
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
- activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.connect(() => {
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: activeCellManager,
28
- activeCellAvailable: activeCellManager.available
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
- model.configChanged.connect((_, config) => {
32
- var _a;
33
- setToolbarEnable((_a = config.enableCodeToolbar) !== null && _a !== void 0 ? _a : true);
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
- 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); } },
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;