@jupyter/chat 0.8.1 → 0.10.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 (69) hide show
  1. package/lib/__tests__/mocks.d.ts +9 -0
  2. package/lib/__tests__/mocks.js +18 -0
  3. package/lib/__tests__/model.spec.js +17 -10
  4. package/lib/__tests__/widgets.spec.js +4 -4
  5. package/lib/chat-commands/types.d.ts +2 -1
  6. package/lib/components/chat-input.d.ts +4 -12
  7. package/lib/components/chat-input.js +26 -40
  8. package/lib/components/chat-messages.d.ts +17 -4
  9. package/lib/components/chat-messages.js +28 -15
  10. package/lib/components/chat.d.ts +5 -5
  11. package/lib/components/chat.js +9 -8
  12. package/lib/components/code-blocks/copy-button.js +6 -3
  13. package/lib/components/input/buttons/attach-button.d.ts +6 -0
  14. package/lib/components/input/{attach-button.js → buttons/attach-button.js} +11 -8
  15. package/lib/components/input/buttons/cancel-button.d.ts +6 -0
  16. package/lib/components/input/{cancel-button.js → buttons/cancel-button.js} +5 -7
  17. package/lib/components/input/buttons/index.d.ts +3 -0
  18. package/lib/components/input/buttons/index.js +7 -0
  19. package/lib/components/input/buttons/send-button.d.ts +6 -0
  20. package/lib/components/input/{send-button.js → buttons/send-button.js} +52 -42
  21. package/lib/components/input/index.d.ts +3 -3
  22. package/lib/components/input/index.js +3 -3
  23. package/lib/components/input/toolbar-registry.d.ts +98 -0
  24. package/lib/components/input/toolbar-registry.js +85 -0
  25. package/lib/components/input/use-chat-commands.js +6 -5
  26. package/lib/components/mui-extras/tooltipped-button.d.ts +1 -1
  27. package/lib/components/mui-extras/tooltipped-button.js +3 -2
  28. package/lib/components/mui-extras/tooltipped-icon-button.js +4 -2
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/input-model.d.ts +93 -1
  32. package/lib/input-model.js +55 -1
  33. package/lib/model.d.ts +76 -9
  34. package/lib/model.js +42 -12
  35. package/lib/types.d.ts +5 -18
  36. package/lib/utils.d.ts +15 -0
  37. package/lib/utils.js +29 -0
  38. package/lib/widgets/chat-widget.d.ts +5 -1
  39. package/lib/widgets/chat-widget.js +7 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/mocks.ts +31 -0
  42. package/src/__tests__/model.spec.ts +21 -11
  43. package/src/__tests__/widgets.spec.ts +5 -4
  44. package/src/chat-commands/types.ts +1 -1
  45. package/src/components/chat-input.tsx +41 -66
  46. package/src/components/chat-messages.tsx +44 -17
  47. package/src/components/chat.tsx +12 -21
  48. package/src/components/code-blocks/copy-button.tsx +9 -3
  49. package/src/components/input/{attach-button.tsx → buttons/attach-button.tsx} +15 -20
  50. package/src/components/input/{cancel-button.tsx → buttons/cancel-button.tsx} +9 -16
  51. package/src/components/input/buttons/index.ts +8 -0
  52. package/src/components/input/{send-button.tsx → buttons/send-button.tsx} +62 -61
  53. package/src/components/input/index.ts +3 -3
  54. package/src/components/input/toolbar-registry.tsx +162 -0
  55. package/src/components/input/use-chat-commands.tsx +14 -6
  56. package/src/components/mui-extras/tooltipped-button.tsx +4 -2
  57. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -2
  58. package/src/index.ts +1 -1
  59. package/src/input-model.ts +140 -2
  60. package/src/model.ts +110 -12
  61. package/src/types.ts +5 -21
  62. package/src/utils.ts +34 -0
  63. package/src/widgets/chat-widget.tsx +8 -1
  64. package/style/base.css +1 -0
  65. package/style/chat.css +6 -0
  66. package/style/input.css +32 -0
  67. package/lib/components/input/attach-button.d.ts +0 -14
  68. package/lib/components/input/cancel-button.d.ts +0 -11
  69. package/lib/components/input/send-button.d.ts +0 -18
@@ -0,0 +1,9 @@
1
+ import { AbstractChatContext, AbstractChatModel, IChatModel, IChatContext } from '../model';
2
+ import { INewMessage } from '../types';
3
+ export declare class MockChatContext extends AbstractChatContext implements IChatContext {
4
+ get users(): never[];
5
+ }
6
+ export declare class MockChatModel extends AbstractChatModel implements IChatModel {
7
+ sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
8
+ createChatContext(): IChatContext;
9
+ }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { AbstractChatContext, AbstractChatModel } from '../model';
6
+ export class MockChatContext extends AbstractChatContext {
7
+ get users() {
8
+ return [];
9
+ }
10
+ }
11
+ export class MockChatModel extends AbstractChatModel {
12
+ sendMessage(message) {
13
+ // No-op
14
+ }
15
+ createChatContext() {
16
+ return new MockChatContext({ model: this });
17
+ }
18
+ }
@@ -5,25 +5,32 @@
5
5
  /**
6
6
  * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
7
7
  */
8
- import { ChatModel } from '../model';
8
+ import { AbstractChatModel } from '../model';
9
+ import { MockChatModel, MockChatContext } from './mocks';
9
10
  describe('test chat model', () => {
10
11
  describe('model instantiation', () => {
11
- it('should create a ChatModel', () => {
12
- const model = new ChatModel();
13
- expect(model).toBeInstanceOf(ChatModel);
12
+ it('should create an AbstractChatModel', () => {
13
+ const model = new MockChatModel();
14
+ expect(model).toBeInstanceOf(AbstractChatModel);
14
15
  });
15
- it('should dispose a ChatModel', () => {
16
- const model = new ChatModel();
16
+ it('should dispose an AbstractChatModel', () => {
17
+ const model = new MockChatModel();
17
18
  model.dispose();
18
19
  expect(model.isDisposed).toBeTruthy();
19
20
  });
20
21
  });
21
22
  describe('incoming message', () => {
22
- class TestChat extends ChatModel {
23
+ class TestChat extends AbstractChatModel {
23
24
  formatChatMessage(message) {
24
25
  message.body = 'formatted msg';
25
26
  return message;
26
27
  }
28
+ sendMessage(message) {
29
+ // No-op
30
+ }
31
+ createChatContext() {
32
+ return new MockChatContext({ model: this });
33
+ }
27
34
  }
28
35
  let model;
29
36
  let messages;
@@ -38,7 +45,7 @@ describe('test chat model', () => {
38
45
  messages = [];
39
46
  });
40
47
  it('should signal incoming message', () => {
41
- model = new ChatModel();
48
+ model = new MockChatModel();
42
49
  model.messagesUpdated.connect((sender) => {
43
50
  expect(sender).toBe(model);
44
51
  messages = model.messages;
@@ -61,11 +68,11 @@ describe('test chat model', () => {
61
68
  });
62
69
  describe('model config', () => {
63
70
  it('should have empty config', () => {
64
- const model = new ChatModel();
71
+ const model = new MockChatModel();
65
72
  expect(model.config.sendWithShiftEnter).toBeUndefined();
66
73
  });
67
74
  it('should allow config', () => {
68
- const model = new ChatModel({ config: { sendWithShiftEnter: true } });
75
+ const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
69
76
  expect(model.config.sendWithShiftEnter).toBeTruthy();
70
77
  });
71
78
  });
@@ -6,21 +6,21 @@
6
6
  * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
7
7
  */
8
8
  import { RenderMimeRegistry } from '@jupyterlab/rendermime';
9
- import { ChatModel } from '../model';
10
9
  import { ChatWidget } from '../widgets/chat-widget';
10
+ import { MockChatModel } from './mocks';
11
11
  describe('test chat widget', () => {
12
12
  let model;
13
13
  let rmRegistry;
14
14
  beforeEach(() => {
15
- model = new ChatModel();
15
+ model = new MockChatModel();
16
16
  rmRegistry = new RenderMimeRegistry();
17
17
  });
18
18
  describe('model instantiation', () => {
19
- it('should create a ChatModel', () => {
19
+ it('should create an AbstractChatModel', () => {
20
20
  const widget = new ChatWidget({ model, rmRegistry });
21
21
  expect(widget).toBeInstanceOf(ChatWidget);
22
22
  });
23
- it('should dispose a ChatModel', () => {
23
+ it('should dispose an AbstractChatModel', () => {
24
24
  const widget = new ChatWidget({ model, rmRegistry });
25
25
  widget.dispose();
26
26
  expect(widget.isDisposed).toBeTruthy();
@@ -1,3 +1,4 @@
1
+ /// <reference types="react" />
1
2
  import { LabIcon } from '@jupyterlab/ui-components';
2
3
  import { IInputModel } from '../input-model';
3
4
  export type ChatCommand = {
@@ -14,7 +15,7 @@ export type ChatCommand = {
14
15
  * If set, this will be rendered as the icon for the command in the chat
15
16
  * commands menu. Jupyter Chat will choose a default if this is unset.
16
17
  */
17
- icon?: LabIcon | string;
18
+ icon?: LabIcon | JSX.Element | string | null;
18
19
  /**
19
20
  * If set, this will be rendered as the description for the command in the
20
21
  * chat commands menu. Jupyter Chat will choose a default if this is unset.
@@ -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;
@@ -13,29 +13,21 @@ export declare namespace ChatInput {
13
13
  */
14
14
  interface IProps {
15
15
  /**
16
- * The chat model.
16
+ * The input model.
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.
@@ -13,6 +13,7 @@ import { ChatInput } from './chat-input';
13
13
  import { MarkdownRenderer } from './markdown-renderer';
14
14
  import { ScrollContainer } from './scroll-container';
15
15
  import { InputModel } from '../input-model';
16
+ import { replaceSpanToMention } from '../utils';
16
17
  const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
17
18
  const MESSAGE_CLASS = 'jp-chat-message';
18
19
  const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
@@ -192,8 +193,7 @@ export function ChatMessageHeader(props) {
192
193
  '& > :not(:last-child)': {
193
194
  marginRight: 3
194
195
  },
195
- marginBottom: message.stacked ? '0px' : '12px',
196
- ...props.sx
196
+ marginBottom: message.stacked ? '0px' : '12px'
197
197
  } },
198
198
  avatar,
199
199
  React.createElement(Box, { sx: {
@@ -247,15 +247,27 @@ export const ChatMessage = forwardRef((props, ref) => {
247
247
  // Create an input model only if the message is edited.
248
248
  useEffect(() => {
249
249
  if (edit && canEdit) {
250
- setInputModel(new InputModel({
251
- value: message.body,
252
- activeCellManager: model.activeCellManager,
253
- selectionWatcher: model.selectionWatcher,
254
- config: {
255
- sendWithShiftEnter: model.config.sendWithShiftEnter
256
- },
257
- attachments: message.attachments
258
- }));
250
+ setInputModel(() => {
251
+ var _a;
252
+ let body = message.body;
253
+ (_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
254
+ body = replaceSpanToMention(body, user);
255
+ });
256
+ return new InputModel({
257
+ chatContext: model.createChatContext(),
258
+ onSend: (input, model) => updateMessage(message.id, input, model),
259
+ onCancel: () => cancelEdition(),
260
+ value: body,
261
+ activeCellManager: model.activeCellManager,
262
+ selectionWatcher: model.selectionWatcher,
263
+ documentManager: model.documentManager,
264
+ config: {
265
+ sendWithShiftEnter: model.config.sendWithShiftEnter
266
+ },
267
+ attachments: message.attachments,
268
+ mentions: message.mentions
269
+ });
270
+ });
259
271
  }
260
272
  else {
261
273
  setInputModel(null);
@@ -266,14 +278,15 @@ export const ChatMessage = forwardRef((props, ref) => {
266
278
  setEdit(false);
267
279
  };
268
280
  // Update the content of the message.
269
- const updateMessage = (id, input) => {
270
- if (!canEdit) {
281
+ const updateMessage = (id, input, inputModel) => {
282
+ if (!canEdit || !inputModel) {
271
283
  return;
272
284
  }
273
285
  // Update the message
274
286
  const updatedMessage = { ...message };
275
287
  updatedMessage.body = input;
276
- updatedMessage.attachments = inputModel === null || inputModel === void 0 ? void 0 : inputModel.attachments;
288
+ updatedMessage.attachments = inputModel.attachments;
289
+ updatedMessage.mentions = inputModel.mentions;
277
290
  model.updateMessage(id, updatedMessage);
278
291
  setEdit(false);
279
292
  };
@@ -286,7 +299,7 @@ export const ChatMessage = forwardRef((props, ref) => {
286
299
  };
287
300
  // Empty if the message has been deleted.
288
301
  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 })),
302
+ 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
303
  message.attachments && !edit && (
291
304
  // Display the attachments only if message is not edited, otherwise the
292
305
  // 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;