@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.
Files changed (45) 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 +57 -19
  6. package/lib/components/chat.js +1 -1
  7. package/lib/components/code-blocks/code-toolbar.js +51 -17
  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/components/mui-extras/tooltipped-icon-button.js +5 -1
  15. package/lib/components/rendermime-markdown.js +15 -6
  16. package/lib/icons.d.ts +1 -0
  17. package/lib/icons.js +5 -0
  18. package/lib/index.d.ts +2 -1
  19. package/lib/index.js +2 -1
  20. package/lib/model.d.ts +51 -8
  21. package/lib/model.js +44 -12
  22. package/lib/selection-watcher.d.ts +62 -0
  23. package/lib/selection-watcher.js +134 -0
  24. package/lib/types.d.ts +22 -0
  25. package/lib/utils.d.ts +11 -0
  26. package/lib/utils.js +37 -0
  27. package/package.json +2 -1
  28. package/src/active-cell-manager.ts +3 -0
  29. package/src/components/chat-input.tsx +48 -30
  30. package/src/components/chat-messages.tsx +112 -32
  31. package/src/components/chat.tsx +1 -1
  32. package/src/components/code-blocks/code-toolbar.tsx +56 -18
  33. package/src/components/input/cancel-button.tsx +47 -0
  34. package/src/components/input/send-button.tsx +210 -0
  35. package/src/components/mui-extras/tooltipped-button.tsx +92 -0
  36. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -1
  37. package/src/components/rendermime-markdown.tsx +16 -5
  38. package/src/icons.ts +6 -0
  39. package/src/index.ts +2 -1
  40. package/src/model.ts +77 -13
  41. package/src/selection-watcher.ts +221 -0
  42. package/src/types.ts +25 -0
  43. package/src/utils.ts +47 -0
  44. package/style/chat.css +13 -0
  45. 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.4.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",
@@ -25,6 +25,9 @@ type CellWithErrorContent = {
25
25
  };
26
26
  };
27
27
 
28
+ /**
29
+ * The active cell interface.
30
+ */
28
31
  export interface IActiveCellManager {
29
32
  /**
30
33
  * Whether the notebook is available and an active cell exists.
@@ -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
- <IconButton
207
- size="small"
208
- color="primary"
209
- onClick={onCancel}
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
- <IconButton
217
- size="small"
218
- color="primary"
219
- onClick={onSend}
220
- disabled={
221
- props.onCancel
222
- ? input === props.value
223
- : !input.trim().length
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 bgcolor = sender.color;
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
+ }
@@ -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.addMessage({ body: input });
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: content,
40
- activeCellManager: activeCellManager,
41
- activeCellAvailable: activeCellManager?.available ?? false
41
+ content,
42
+ activeCellManager,
43
+ selectionWatcher,
44
+ activeCellAvailable: !!activeCellManager?.available,
45
+ selectionExists: !!selectionWatcher?.selection
42
46
  });
43
47
 
44
48
  useEffect(() => {
45
- activeCellManager?.availabilityChanged.connect(() => {
49
+ const toggleToolbar = () => {
50
+ setToolbarEnable(model.config.enableCodeToolbar ?? true);
51
+ };
52
+
53
+ const selectionStatusChange = () => {
46
54
  setToolbarBtnProps({
47
55
  content,
48
- activeCellManager: activeCellManager,
49
- activeCellAvailable: activeCellManager.available
56
+ activeCellManager,
57
+ selectionWatcher,
58
+ activeCellAvailable: !!activeCellManager?.available,
59
+ selectionExists: !!selectionWatcher?.selection
50
60
  });
51
- });
52
-
53
- model.configChanged.connect((_, config) => {
54
- setToolbarEnable(config.enableCodeToolbar ?? true);
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: '6px 2px',
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.activeCellAvailable
130
- ? 'Replace active cell'
131
- : 'Replace active cell (no active cell)';
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={!props.activeCellAvailable}
138
- onClick={() => props.activeCellManager?.replace(props.content)}
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
+ }