@jupyter/chat 0.8.1 → 0.9.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 (53) hide show
  1. package/lib/components/chat-input.d.ts +3 -11
  2. package/lib/components/chat-input.js +26 -40
  3. package/lib/components/chat-messages.d.ts +17 -4
  4. package/lib/components/chat-messages.js +9 -9
  5. package/lib/components/chat.d.ts +5 -5
  6. package/lib/components/chat.js +9 -8
  7. package/lib/components/code-blocks/copy-button.js +6 -3
  8. package/lib/components/input/buttons/attach-button.d.ts +6 -0
  9. package/lib/components/input/{attach-button.js → buttons/attach-button.js} +11 -8
  10. package/lib/components/input/buttons/cancel-button.d.ts +6 -0
  11. package/lib/components/input/{cancel-button.js → buttons/cancel-button.js} +5 -7
  12. package/lib/components/input/buttons/index.d.ts +3 -0
  13. package/lib/components/input/buttons/index.js +7 -0
  14. package/lib/components/input/buttons/send-button.d.ts +6 -0
  15. package/lib/components/input/{send-button.js → buttons/send-button.js} +52 -42
  16. package/lib/components/input/index.d.ts +3 -3
  17. package/lib/components/input/index.js +3 -3
  18. package/lib/components/input/toolbar-registry.d.ts +98 -0
  19. package/lib/components/input/toolbar-registry.js +85 -0
  20. package/lib/components/input/use-chat-commands.js +3 -2
  21. package/lib/components/mui-extras/tooltipped-button.d.ts +1 -1
  22. package/lib/components/mui-extras/tooltipped-button.js +3 -2
  23. package/lib/components/mui-extras/tooltipped-icon-button.js +4 -2
  24. package/lib/input-model.d.ts +41 -0
  25. package/lib/input-model.js +17 -1
  26. package/lib/model.d.ts +22 -0
  27. package/lib/model.js +18 -2
  28. package/lib/types.d.ts +0 -18
  29. package/lib/widgets/chat-widget.d.ts +5 -1
  30. package/lib/widgets/chat-widget.js +7 -1
  31. package/package.json +1 -1
  32. package/src/components/chat-input.tsx +40 -65
  33. package/src/components/chat-messages.tsx +31 -14
  34. package/src/components/chat.tsx +12 -21
  35. package/src/components/code-blocks/copy-button.tsx +9 -3
  36. package/src/components/input/{attach-button.tsx → buttons/attach-button.tsx} +15 -20
  37. package/src/components/input/{cancel-button.tsx → buttons/cancel-button.tsx} +9 -16
  38. package/src/components/input/buttons/index.ts +8 -0
  39. package/src/components/input/{send-button.tsx → buttons/send-button.tsx} +62 -61
  40. package/src/components/input/index.ts +3 -3
  41. package/src/components/input/toolbar-registry.tsx +162 -0
  42. package/src/components/input/use-chat-commands.tsx +8 -2
  43. package/src/components/mui-extras/tooltipped-button.tsx +4 -2
  44. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -2
  45. package/src/input-model.ts +58 -1
  46. package/src/model.ts +36 -2
  47. package/src/types.ts +0 -21
  48. package/src/widgets/chat-widget.tsx +8 -1
  49. package/style/base.css +1 -0
  50. package/style/input.css +32 -0
  51. package/lib/components/input/attach-button.d.ts +0 -14
  52. package/lib/components/input/cancel-button.d.ts +0 -11
  53. package/lib/components/input/send-button.d.ts +0 -18
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
- import { IDocumentManager } from '@jupyterlab/docmanager';
3
2
  import { SxProps, Theme } from '@mui/material';
3
+ import { IInputToolbarRegistry } from './input';
4
4
  import { IInputModel } from '../input-model';
5
5
  import { IChatCommandRegistry } from '../chat-commands';
6
6
  export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
@@ -17,25 +17,17 @@ export declare namespace ChatInput {
17
17
  */
18
18
  model: IInputModel;
19
19
  /**
20
- * The function to be called to send the message.
20
+ * The toolbar registry.
21
21
  */
22
- onSend: (input: string) => unknown;
22
+ toolbarRegistry: IInputToolbarRegistry;
23
23
  /**
24
24
  * The function to be called to cancel editing.
25
25
  */
26
26
  onCancel?: () => unknown;
27
- /**
28
- * Whether to allow or not including selection.
29
- */
30
- hideIncludeSelection?: boolean;
31
27
  /**
32
28
  * Custom mui/material styles.
33
29
  */
34
30
  sx?: SxProps<Theme>;
35
- /**
36
- * The document manager.
37
- */
38
- documentManager?: IDocumentManager;
39
31
  /**
40
32
  * Chat command registry.
41
33
  */
@@ -6,23 +6,24 @@ import { Autocomplete, Box, InputAdornment, TextField } from '@mui/material';
6
6
  import clsx from 'clsx';
7
7
  import React, { useEffect, useRef, useState } from 'react';
8
8
  import { AttachmentPreviewList } from './attachments';
9
- import { AttachButton, CancelButton, SendButton } from './input';
10
- import { useChatCommands } from './input/use-chat-commands';
9
+ import { useChatCommands } from './input';
11
10
  const INPUT_BOX_CLASS = 'jp-chat-input-container';
11
+ const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
12
12
  export function ChatInput(props) {
13
- var _a, _b;
14
- const { documentManager, model } = props;
13
+ var _a;
14
+ const { model, toolbarRegistry } = props;
15
15
  const [input, setInput] = useState(model.value);
16
16
  const inputRef = useRef();
17
17
  const chatCommands = useChatCommands(model, props.chatCommandRegistry);
18
18
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
19
19
  const [attachments, setAttachments] = useState(model.attachments);
20
- // Display the include selection menu if it is not explicitly hidden, and if at least
21
- // one of the tool to check for text or cell selection is enabled.
22
- let hideIncludeSelection = (_b = props.hideIncludeSelection) !== null && _b !== void 0 ? _b : false;
23
- if (model.activeCellManager === null && model.selectionWatcher === null) {
24
- hideIncludeSelection = true;
25
- }
20
+ const [toolbarElements, setToolbarElements] = useState([]);
21
+ /**
22
+ * Handle the changes on the model that affect the input.
23
+ * - focus requested
24
+ * - config changed
25
+ * - attachments changed
26
+ */
26
27
  useEffect(() => {
27
28
  var _a, _b;
28
29
  const inputChanged = (_, value) => {
@@ -51,6 +52,19 @@ export function ChatInput(props) {
51
52
  (_c = model.attachmentsChanged) === null || _c === void 0 ? void 0 : _c.disconnect(attachmentChanged);
52
53
  };
53
54
  }, [model]);
55
+ /**
56
+ * Handle the changes in the toolbar items.
57
+ */
58
+ useEffect(() => {
59
+ const updateToolbar = () => {
60
+ setToolbarElements(toolbarRegistry.getItems());
61
+ };
62
+ toolbarRegistry.itemsChanged.connect(updateToolbar);
63
+ updateToolbar();
64
+ return () => {
65
+ toolbarRegistry.itemsChanged.disconnect(updateToolbar);
66
+ };
67
+ }, [toolbarRegistry]);
54
68
  const inputExists = !!input.trim();
55
69
  /**
56
70
  * `handleKeyDown()`: callback invoked when the user presses any key in the
@@ -101,36 +115,11 @@ export function ChatInput(props) {
101
115
  // Finally, send the message when all other conditions are met.
102
116
  if ((sendWithShiftEnter && event.shiftKey) ||
103
117
  (!sendWithShiftEnter && !event.shiftKey)) {
104
- onSend();
118
+ model.send(input);
105
119
  event.stopPropagation();
106
120
  event.preventDefault();
107
121
  }
108
122
  }
109
- /**
110
- * Triggered when sending the message.
111
- *
112
- * Add code block if cell or text is selected.
113
- */
114
- function onSend(selection) {
115
- let content = input;
116
- if (selection) {
117
- content += `
118
-
119
- \`\`\`
120
- ${selection.source}
121
- \`\`\`
122
- `;
123
- }
124
- props.onSend(content);
125
- model.value = '';
126
- }
127
- /**
128
- * Triggered when cancelling edition.
129
- */
130
- function onCancel() {
131
- var _a;
132
- (_a = props.onCancel) === null || _a === void 0 ? void 0 : _a.call(props);
133
- }
134
123
  // Set the helper text based on whether Shift+Enter is used for sending.
135
124
  const helperText = sendWithShiftEnter ? (React.createElement("span", null,
136
125
  "Press ",
@@ -164,10 +153,7 @@ ${selection.source}
164
153
  }
165
154
  }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, sx: { marginTop: '1px' }, onSelect: () => { var _a, _b; return (model.cursorIndex = (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : null); }, InputProps: {
166
155
  ...params.InputProps,
167
- endAdornment: (React.createElement(InputAdornment, { position: "end" },
168
- documentManager && model.addAttachment && (React.createElement(AttachButton, { documentManager: documentManager, onAttach: model.addAttachment })),
169
- props.onCancel && React.createElement(CancelButton, { onCancel: onCancel }),
170
- React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists || attachments.length > 0, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
156
+ endAdornment: (React.createElement(InputAdornment, { position: "end", className: INPUT_TOOLBAR_CLASS }, toolbarElements.map(item => (React.createElement(item.element, { model: model })))))
171
157
  }, FormHelperTextProps: {
172
158
  sx: { marginLeft: 'auto', marginRight: 0 }
173
159
  }, helperText: input.length > 2 ? helperText : ' ' })), inputValue: input, onInputChange: (_, newValue, reason) => {
@@ -1,8 +1,7 @@
1
- import { IDocumentManager } from '@jupyterlab/docmanager';
2
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
2
  import { PromiseDelegate } from '@lumino/coreutils';
4
- import type { SxProps, Theme } from '@mui/material';
5
3
  import React from 'react';
4
+ import { IInputToolbarRegistry } from './input';
6
5
  import { IChatCommandRegistry } from '../chat-commands';
7
6
  import { IChatModel } from '../model';
8
7
  import { IChatMessage, IUser } from '../types';
@@ -10,10 +9,22 @@ import { IChatMessage, IUser } from '../types';
10
9
  * The base components props.
11
10
  */
12
11
  type BaseMessageProps = {
12
+ /**
13
+ * The mime renderer registry.
14
+ */
13
15
  rmRegistry: IRenderMimeRegistry;
16
+ /**
17
+ * The chat model.
18
+ */
14
19
  model: IChatModel;
20
+ /**
21
+ * The chat commands registry.
22
+ */
15
23
  chatCommandRegistry?: IChatCommandRegistry;
16
- documentManager?: IDocumentManager;
24
+ /**
25
+ * The input toolbar registry.
26
+ */
27
+ inputToolbarRegistry: IInputToolbarRegistry;
17
28
  };
18
29
  /**
19
30
  * The messages list component.
@@ -23,8 +34,10 @@ export declare function ChatMessages(props: BaseMessageProps): JSX.Element;
23
34
  * The message header props.
24
35
  */
25
36
  type ChatMessageHeaderProps = {
37
+ /**
38
+ * The chat message.
39
+ */
26
40
  message: IChatMessage;
27
- sx?: SxProps<Theme>;
28
41
  };
29
42
  /**
30
43
  * The message header component.
@@ -192,8 +192,7 @@ export function ChatMessageHeader(props) {
192
192
  '& > :not(:last-child)': {
193
193
  marginRight: 3
194
194
  },
195
- marginBottom: message.stacked ? '0px' : '12px',
196
- ...props.sx
195
+ marginBottom: message.stacked ? '0px' : '12px'
197
196
  } },
198
197
  avatar,
199
198
  React.createElement(Box, { sx: {
@@ -248,13 +247,14 @@ export const ChatMessage = forwardRef((props, ref) => {
248
247
  useEffect(() => {
249
248
  if (edit && canEdit) {
250
249
  setInputModel(new InputModel({
250
+ onSend: (input, model) => updateMessage(message.id, input, model),
251
+ onCancel: () => cancelEdition(),
251
252
  value: message.body,
252
- activeCellManager: model.activeCellManager,
253
- selectionWatcher: model.selectionWatcher,
254
253
  config: {
255
254
  sendWithShiftEnter: model.config.sendWithShiftEnter
256
255
  },
257
- attachments: message.attachments
256
+ attachments: message.attachments,
257
+ documentManager: model.documentManager
258
258
  }));
259
259
  }
260
260
  else {
@@ -266,14 +266,14 @@ export const ChatMessage = forwardRef((props, ref) => {
266
266
  setEdit(false);
267
267
  };
268
268
  // Update the content of the message.
269
- const updateMessage = (id, input) => {
270
- if (!canEdit) {
269
+ const updateMessage = (id, input, inputModel) => {
270
+ if (!canEdit || !inputModel) {
271
271
  return;
272
272
  }
273
273
  // Update the message
274
274
  const updatedMessage = { ...message };
275
275
  updatedMessage.body = input;
276
- updatedMessage.attachments = inputModel === null || inputModel === void 0 ? void 0 : inputModel.attachments;
276
+ updatedMessage.attachments = inputModel.attachments;
277
277
  model.updateMessage(id, updatedMessage);
278
278
  setEdit(false);
279
279
  };
@@ -286,7 +286,7 @@ export const ChatMessage = forwardRef((props, ref) => {
286
286
  };
287
287
  // Empty if the message has been deleted.
288
288
  return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
289
- edit && canEdit && inputModel ? (React.createElement(ChatInput, { onSend: (input) => updateMessage(message.id, input), onCancel: () => cancelEdition(), model: inputModel, hideIncludeSelection: true, chatCommandRegistry: props.chatCommandRegistry, documentManager: props.documentManager })) : (React.createElement(MarkdownRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
289
+ edit && canEdit && inputModel ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: inputModel, chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry })) : (React.createElement(MarkdownRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
290
290
  message.attachments && !edit && (
291
291
  // Display the attachments only if message is not edited, otherwise the
292
292
  // input component display them.
@@ -1,8 +1,8 @@
1
1
  /// <reference types="react" />
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
- import { IDocumentManager } from '@jupyterlab/docmanager';
4
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
4
  import { IChatCommandRegistry } from '../chat-commands';
5
+ import { IInputToolbarRegistry } from './input';
6
6
  import { IChatModel } from '../model';
7
7
  import { IAttachmentOpenerRegistry } from '../registry';
8
8
  export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
@@ -23,10 +23,6 @@ export declare namespace Chat {
23
23
  * The rendermime registry.
24
24
  */
25
25
  rmRegistry: IRenderMimeRegistry;
26
- /**
27
- * The document manager.
28
- */
29
- documentManager?: IDocumentManager;
30
26
  /**
31
27
  * Chat command registry.
32
28
  */
@@ -35,6 +31,10 @@ export declare namespace Chat {
35
31
  * Attachment opener registry.
36
32
  */
37
33
  attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
34
+ /**
35
+ * The input toolbar registry
36
+ */
37
+ inputToolbarRegistry?: IInputToolbarRegistry;
38
38
  }
39
39
  /**
40
40
  * The options to build the Chat UI.
@@ -10,22 +10,23 @@ import React, { useState } from 'react';
10
10
  import { JlThemeProvider } from './jl-theme-provider';
11
11
  import { ChatMessages } from './chat-messages';
12
12
  import { ChatInput } from './chat-input';
13
+ import { InputToolbarRegistry } from './input';
13
14
  import { AttachmentOpenerContext } from '../context';
14
15
  export function ChatBody(props) {
15
16
  const { model } = props;
16
- const onSend = async (input) => {
17
- // send message to backend
18
- model.sendMessage({ body: input });
19
- };
17
+ let { inputToolbarRegistry } = props;
18
+ if (!inputToolbarRegistry) {
19
+ inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
20
+ }
20
21
  return (React.createElement(AttachmentOpenerContext.Provider, { value: props.attachmentOpenerRegistry },
21
- React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, documentManager: props.documentManager }),
22
- React.createElement(ChatInput, { onSend: onSend, sx: {
22
+ React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, inputToolbarRegistry: inputToolbarRegistry }),
23
+ React.createElement(ChatInput, { sx: {
23
24
  paddingLeft: 4,
24
25
  paddingRight: 4,
25
26
  paddingTop: 1,
26
27
  paddingBottom: 0,
27
28
  borderTop: '1px solid var(--jp-border-color1)'
28
- }, model: model.input, documentManager: props.documentManager, chatCommandRegistry: props.chatCommandRegistry })));
29
+ }, model: model.input, chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: inputToolbarRegistry })));
29
30
  }
30
31
  export function Chat(props) {
31
32
  var _a;
@@ -53,7 +54,7 @@ export function Chat(props) {
53
54
  React.createElement(ArrowBackIcon, null))) : (React.createElement(Box, null)),
54
55
  view !== Chat.View.settings && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.settings) },
55
56
  React.createElement(SettingsIcon, null))) : (React.createElement(Box, null))),
56
- view === Chat.View.chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry, documentManager: props.documentManager, chatCommandRegistry: props.chatCommandRegistry, attachmentOpenerRegistry: props.attachmentOpenerRegistry })),
57
+ view === Chat.View.chat && React.createElement(ChatBody, { ...props }),
57
58
  view === Chat.View.settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
58
59
  }
59
60
  /**
@@ -10,14 +10,17 @@ var CopyStatus;
10
10
  CopyStatus[CopyStatus["None"] = 0] = "None";
11
11
  CopyStatus[CopyStatus["Copying"] = 1] = "Copying";
12
12
  CopyStatus[CopyStatus["Copied"] = 2] = "Copied";
13
+ CopyStatus[CopyStatus["Disabled"] = 3] = "Disabled";
13
14
  })(CopyStatus || (CopyStatus = {}));
14
15
  const COPYBTN_TEXT_BY_STATUS = {
15
16
  [CopyStatus.None]: 'Copy to clipboard',
16
17
  [CopyStatus.Copying]: 'Copying…',
17
- [CopyStatus.Copied]: 'Copied!'
18
+ [CopyStatus.Copied]: 'Copied!',
19
+ [CopyStatus.Disabled]: 'Copy to clipboard disabled in insecure context'
18
20
  };
19
21
  export function CopyButton(props) {
20
- const [copyStatus, setCopyStatus] = useState(CopyStatus.None);
22
+ const isCopyDisabled = navigator.clipboard === undefined;
23
+ const [copyStatus, setCopyStatus] = useState(isCopyDisabled ? CopyStatus.Disabled : CopyStatus.None);
21
24
  const timeoutId = useRef(null);
22
25
  const copy = useCallback(async () => {
23
26
  // ignore if we are already copying
@@ -38,6 +41,6 @@ export function CopyButton(props) {
38
41
  }
39
42
  timeoutId.current = window.setTimeout(() => setCopyStatus(CopyStatus.None), 1000);
40
43
  }, [copyStatus, props.value]);
41
- return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: COPYBTN_TEXT_BY_STATUS[copyStatus], placement: "top", onClick: copy, "aria-label": "Copy to clipboard" },
44
+ return (React.createElement(TooltippedIconButton, { disabled: isCopyDisabled, className: props.className, tooltip: COPYBTN_TEXT_BY_STATUS[copyStatus], placement: "top", onClick: copy, "aria-label": "Copy to clipboard" },
42
45
  React.createElement(copyIcon.react, { height: "16px", width: "16px" })));
43
46
  }
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import { InputToolbarRegistry } from '../toolbar-registry';
3
+ /**
4
+ * The attach button.
5
+ */
6
+ export declare function AttachButton(props: InputToolbarRegistry.IToolbarItemProps): JSX.Element;
@@ -5,23 +5,31 @@
5
5
  import { FileDialog } from '@jupyterlab/filebrowser';
6
6
  import AttachFileIcon from '@mui/icons-material/AttachFile';
7
7
  import React from 'react';
8
- import { TooltippedButton } from '../mui-extras/tooltipped-button';
8
+ import { TooltippedButton } from '../../mui-extras/tooltipped-button';
9
9
  const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
10
10
  /**
11
11
  * The attach button.
12
12
  */
13
13
  export function AttachButton(props) {
14
+ const { model } = props;
14
15
  const tooltip = 'Add attachment';
16
+ if (!model.documentManager || !model.addAttachment) {
17
+ return React.createElement(React.Fragment, null);
18
+ }
15
19
  const onclick = async () => {
20
+ if (!model.documentManager || !model.addAttachment) {
21
+ return;
22
+ }
16
23
  try {
17
24
  const files = await FileDialog.getOpenFiles({
18
25
  title: 'Select files to attach',
19
- manager: props.documentManager
26
+ manager: model.documentManager
20
27
  });
21
28
  if (files.value) {
22
29
  files.value.forEach(file => {
30
+ var _a;
23
31
  if (file.type !== 'directory') {
24
- props.onAttach({ type: 'file', value: file.path });
32
+ (_a = model.addAttachment) === null || _a === void 0 ? void 0 : _a.call(model, { type: 'file', value: file.path });
25
33
  }
26
34
  });
27
35
  }
@@ -35,11 +43,6 @@ export function AttachButton(props) {
35
43
  variant: 'contained',
36
44
  title: tooltip,
37
45
  className: ATTACH_BUTTON_CLASS
38
- }, sx: {
39
- minWidth: 'unset',
40
- padding: '4px',
41
- borderRadius: '2px 0px 0px 2px',
42
- marginRight: '1px'
43
46
  } },
44
47
  React.createElement(AttachFileIcon, null)));
45
48
  }
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import { InputToolbarRegistry } from '../toolbar-registry';
3
+ /**
4
+ * The cancel button.
5
+ */
6
+ export declare function CancelButton(props: InputToolbarRegistry.IToolbarItemProps): JSX.Element;
@@ -4,23 +4,21 @@
4
4
  */
5
5
  import CancelIcon from '@mui/icons-material/Cancel';
6
6
  import React from 'react';
7
- import { TooltippedButton } from '../mui-extras/tooltipped-button';
7
+ import { TooltippedButton } from '../../mui-extras/tooltipped-button';
8
8
  const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
9
9
  /**
10
10
  * The cancel button.
11
11
  */
12
12
  export function CancelButton(props) {
13
+ if (!props.model.cancel) {
14
+ return React.createElement(React.Fragment, null);
15
+ }
13
16
  const tooltip = 'Cancel edition';
14
- return (React.createElement(TooltippedButton, { onClick: props.onCancel, tooltip: tooltip, buttonProps: {
17
+ return (React.createElement(TooltippedButton, { onClick: props.model.cancel, tooltip: tooltip, buttonProps: {
15
18
  size: 'small',
16
19
  variant: 'contained',
17
20
  title: tooltip,
18
21
  className: CANCEL_BUTTON_CLASS
19
- }, sx: {
20
- minWidth: 'unset',
21
- padding: '4px',
22
- borderRadius: '2px 0px 0px 2px',
23
- marginRight: '1px'
24
22
  } },
25
23
  React.createElement(CancelIcon, null)));
26
24
  }
@@ -0,0 +1,3 @@
1
+ export { AttachButton } from './attach-button';
2
+ export { CancelButton } from './cancel-button';
3
+ export { SendButton } from './send-button';
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export { AttachButton } from './attach-button';
6
+ export { CancelButton } from './cancel-button';
7
+ export { SendButton } from './send-button';
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import { InputToolbarRegistry } from '../toolbar-registry';
3
+ /**
4
+ * The send button, with optional 'include selection' menu.
5
+ */
6
+ export declare function SendButton(props: InputToolbarRegistry.IToolbarItemProps): JSX.Element;
@@ -6,8 +6,8 @@ import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
6
6
  import SendIcon from '@mui/icons-material/Send';
7
7
  import { Box, Menu, MenuItem, Typography } from '@mui/material';
8
8
  import React, { useCallback, useEffect, useState } from 'react';
9
- import { TooltippedButton } from '../mui-extras/tooltipped-button';
10
- import { includeSelectionIcon } from '../../icons';
9
+ import { TooltippedButton } from '../../mui-extras/tooltipped-button';
10
+ import { includeSelectionIcon } from '../../../icons';
11
11
  const SEND_BUTTON_CLASS = 'jp-chat-send-button';
12
12
  const SEND_INCLUDE_OPENER_CLASS = 'jp-chat-send-include-opener';
13
13
  const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
@@ -15,12 +15,13 @@ const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
15
15
  * The send button, with optional 'include selection' menu.
16
16
  */
17
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;
18
+ const { model } = props;
19
+ const { activeCellManager, selectionWatcher } = model;
20
+ const hideIncludeSelection = !activeCellManager || !selectionWatcher;
22
21
  const [menuAnchorEl, setMenuAnchorEl] = useState(null);
23
22
  const [menuOpen, setMenuOpen] = useState(false);
23
+ const [disabled, setDisabled] = useState(false);
24
+ const [tooltip, setTooltip] = useState('');
24
25
  const openMenu = useCallback((el) => {
25
26
  setMenuAnchorEl(el);
26
27
  setMenuOpen(true);
@@ -28,9 +29,31 @@ export function SendButton(props) {
28
29
  const closeMenu = useCallback(() => {
29
30
  setMenuOpen(false);
30
31
  }, []);
31
- const disabled = !props.inputExists;
32
32
  const [selectionTooltip, setSelectionTooltip] = useState('');
33
33
  const [disableInclude, setDisableInclude] = useState(true);
34
+ useEffect(() => {
35
+ var _a;
36
+ const inputChanged = () => {
37
+ const inputExist = !!model.value.trim() || model.attachments.length;
38
+ setDisabled(!inputExist);
39
+ };
40
+ model.valueChanged.connect(inputChanged);
41
+ (_a = model.attachmentsChanged) === null || _a === void 0 ? void 0 : _a.connect(inputChanged);
42
+ inputChanged();
43
+ const configChanged = (_, config) => {
44
+ var _a;
45
+ setTooltip(((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false)
46
+ ? 'Send message (SHIFT+ENTER)'
47
+ : 'Send message (ENTER)');
48
+ };
49
+ model.configChanged.connect(configChanged);
50
+ return () => {
51
+ var _a, _b;
52
+ model.valueChanged.disconnect(inputChanged);
53
+ (_a = model.attachmentsChanged) === null || _a === void 0 ? void 0 : _a.disconnect(inputChanged);
54
+ (_b = model.configChanged) === null || _b === void 0 ? void 0 : _b.disconnect(configChanged);
55
+ };
56
+ }, [model]);
34
57
  useEffect(() => {
35
58
  /**
36
59
  * Enable or disable the include selection button, and adapt the tooltip.
@@ -53,44 +76,36 @@ export function SendButton(props) {
53
76
  selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selectionChanged.disconnect(toggleIncludeState);
54
77
  activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.availabilityChanged.disconnect(toggleIncludeState);
55
78
  };
56
- }, [activeCellManager, selectionWatcher, hideIncludeSelection]);
57
- const defaultTooltip = props.sendWithShiftEnter
58
- ? 'Send message (SHIFT+ENTER)'
59
- : 'Send message (ENTER)';
60
- const tooltip = defaultTooltip;
79
+ }, [activeCellManager, selectionWatcher]);
61
80
  function sendWithSelection() {
62
- // Append the selected text if exists.
81
+ let source = '';
63
82
  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;
83
+ // Append the selected text if exists.
84
+ source = selectionWatcher.selection.text;
85
+ }
86
+ else if (activeCellManager === null || activeCellManager === void 0 ? void 0 : activeCellManager.available) {
87
+ // Append the active cell content if exists.
88
+ source = activeCellManager.getContent(false).source;
70
89
  }
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;
90
+ let content = model.value;
91
+ if (source) {
92
+ content += `
93
+
94
+ \`\`\`
95
+ ${source}
96
+ \`\`\`
97
+ `;
80
98
  }
99
+ model.send(content);
100
+ closeMenu();
101
+ model.value = '';
81
102
  }
82
- return (React.createElement(Box, { sx: { display: 'flex', flexWrap: 'nowrap' } },
83
- React.createElement(TooltippedButton, { onClick: () => props.onSend(), disabled: disabled, tooltip: tooltip, buttonProps: {
103
+ return (React.createElement(React.Fragment, null,
104
+ React.createElement(TooltippedButton, { onClick: () => model.send(model.value), disabled: disabled, tooltip: tooltip, buttonProps: {
84
105
  size: 'small',
85
- title: defaultTooltip,
106
+ title: tooltip,
86
107
  variant: 'contained',
87
108
  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
109
  } },
95
110
  React.createElement(SendIcon, null)),
96
111
  !hideIncludeSelection && (React.createElement(React.Fragment, null,
@@ -108,11 +123,6 @@ export function SendButton(props) {
108
123
  e.stopPropagation();
109
124
  },
110
125
  className: SEND_INCLUDE_OPENER_CLASS
111
- }, sx: {
112
- minWidth: 'unset',
113
- padding: '4px 0px',
114
- borderRadius: '0px 2px 2px 0px',
115
- marginLeft: '1px'
116
126
  } },
117
127
  React.createElement(KeyboardArrowDown, null)),
118
128
  React.createElement(Menu, { open: menuOpen, onClose: closeMenu, anchorEl: menuAnchorEl, anchorOrigin: {
@@ -1,3 +1,3 @@
1
- export * from './attach-button';
2
- export * from './cancel-button';
3
- export * from './send-button';
1
+ export * from './buttons';
2
+ export * from './toolbar-registry';
3
+ export * from './use-chat-commands';
@@ -2,6 +2,6 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- export * from './attach-button';
6
- export * from './cancel-button';
7
- export * from './send-button';
5
+ export * from './buttons';
6
+ export * from './toolbar-registry';
7
+ export * from './use-chat-commands';