@jupyter/chat 0.7.1 → 0.8.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 (59) hide show
  1. package/lib/active-cell-manager.js +1 -4
  2. package/lib/chat-commands/index.d.ts +2 -0
  3. package/lib/chat-commands/index.js +6 -0
  4. package/lib/chat-commands/registry.d.ts +28 -0
  5. package/lib/chat-commands/registry.js +29 -0
  6. package/lib/chat-commands/types.d.ts +51 -0
  7. package/lib/chat-commands/types.js +5 -0
  8. package/lib/components/attachments.d.ts +23 -0
  9. package/lib/components/attachments.js +44 -0
  10. package/lib/components/chat-input.d.ts +8 -11
  11. package/lib/components/chat-input.js +70 -95
  12. package/lib/components/chat-messages.d.ts +4 -0
  13. package/lib/components/chat-messages.js +27 -1
  14. package/lib/components/chat.d.ts +11 -5
  15. package/lib/components/chat.js +7 -8
  16. package/lib/components/input/attach-button.d.ts +14 -0
  17. package/lib/components/input/attach-button.js +45 -0
  18. package/lib/components/input/index.d.ts +1 -0
  19. package/lib/components/input/index.js +1 -0
  20. package/lib/components/input/send-button.d.ts +2 -2
  21. package/lib/components/input/use-chat-commands.d.ts +19 -0
  22. package/lib/components/input/use-chat-commands.js +127 -0
  23. package/lib/context.d.ts +3 -0
  24. package/lib/context.js +6 -0
  25. package/lib/index.d.ts +2 -0
  26. package/lib/index.js +2 -0
  27. package/lib/input-model.d.ts +221 -0
  28. package/lib/input-model.js +217 -0
  29. package/lib/model.d.ts +10 -25
  30. package/lib/model.js +15 -17
  31. package/lib/registry.d.ts +11 -64
  32. package/lib/registry.js +4 -72
  33. package/lib/types.d.ts +19 -38
  34. package/lib/widgets/chat-widget.js +2 -1
  35. package/package.json +3 -114
  36. package/src/active-cell-manager.ts +0 -3
  37. package/src/chat-commands/index.ts +7 -0
  38. package/src/chat-commands/registry.ts +60 -0
  39. package/src/chat-commands/types.ts +67 -0
  40. package/src/components/attachments.tsx +91 -0
  41. package/src/components/chat-input.tsx +97 -124
  42. package/src/components/chat-messages.tsx +36 -3
  43. package/src/components/chat.tsx +28 -19
  44. package/src/components/input/attach-button.tsx +68 -0
  45. package/src/components/input/cancel-button.tsx +1 -0
  46. package/src/components/input/index.ts +1 -0
  47. package/src/components/input/send-button.tsx +2 -2
  48. package/src/components/input/use-chat-commands.tsx +186 -0
  49. package/src/context.ts +10 -0
  50. package/src/index.ts +2 -0
  51. package/src/input-model.ts +406 -0
  52. package/src/model.ts +24 -35
  53. package/src/registry.ts +14 -108
  54. package/src/types.ts +19 -39
  55. package/src/widgets/chat-widget.tsx +2 -1
  56. package/style/chat.css +27 -9
  57. package/style/icons/include-selection.svg +3 -1
  58. package/style/icons/read.svg +8 -6
  59. package/style/icons/replace-cell.svg +10 -6
@@ -34,13 +34,10 @@ export class ActiveCellManager {
34
34
  (_a = this._activeCell) === null || _a === void 0 ? void 0 : _a.model.stateChanged.disconnect(this._cellStateChange);
35
35
  this._activeCell = activeCell;
36
36
  activeCell === null || activeCell === void 0 ? void 0 : activeCell.ready.then(() => {
37
- var _a, _b;
37
+ var _a;
38
38
  (_a = this._activeCell) === null || _a === void 0 ? void 0 : _a.model.stateChanged.connect(this._cellStateChange);
39
39
  this._available = !!this._activeCell && this._notebookVisible;
40
40
  this._availabilityChanged.emit(this._available);
41
- (_b = this._activeCell) === null || _b === void 0 ? void 0 : _b.disposed.connect(() => {
42
- this._activeCell = null;
43
- });
44
41
  });
45
42
  }
46
43
  };
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './registry';
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './types';
6
+ export * from './registry';
@@ -0,0 +1,28 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ import { ChatCommand, IChatCommandProvider } from './types';
3
+ import { IInputModel } from '../input-model';
4
+ /**
5
+ * Interface of a chat command registry, which tracks a list of chat command
6
+ * providers. Providers provide a list of commands given a user's partial input,
7
+ * and define how commands are handled when accepted in the chat commands menu.
8
+ */
9
+ export interface IChatCommandRegistry {
10
+ addProvider(provider: IChatCommandProvider): void;
11
+ getProviders(): IChatCommandProvider[];
12
+ /**
13
+ * Handles a chat command by calling `handleChatCommand()` on the provider
14
+ * corresponding to this chat command.
15
+ */
16
+ handleChatCommand(command: ChatCommand, inputModel: IInputModel): void;
17
+ }
18
+ /**
19
+ * Default chat command registry implementation.
20
+ */
21
+ export declare class ChatCommandRegistry implements IChatCommandRegistry {
22
+ constructor();
23
+ addProvider(provider: IChatCommandProvider): void;
24
+ getProviders(): IChatCommandProvider[];
25
+ handleChatCommand(command: ChatCommand, inputModel: IInputModel): void;
26
+ private _providers;
27
+ }
28
+ export declare const IChatCommandRegistry: Token<IChatCommandRegistry>;
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { Token } from '@lumino/coreutils';
6
+ /**
7
+ * Default chat command registry implementation.
8
+ */
9
+ export class ChatCommandRegistry {
10
+ constructor() {
11
+ this._providers = new Map();
12
+ }
13
+ addProvider(provider) {
14
+ this._providers.set(provider.id, provider);
15
+ }
16
+ getProviders() {
17
+ return Array.from(this._providers.values());
18
+ }
19
+ handleChatCommand(command, inputModel) {
20
+ const provider = this._providers.get(command.providerId);
21
+ if (!provider) {
22
+ console.error('Error in handling chat command: No command provider has an ID of ' +
23
+ command.providerId);
24
+ return;
25
+ }
26
+ provider.handleChatCommand(command, inputModel);
27
+ }
28
+ }
29
+ export const IChatCommandRegistry = new Token('@jupyter/chat:IChatCommandRegistry');
@@ -0,0 +1,51 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+ import { IInputModel } from '../input-model';
3
+ export type ChatCommand = {
4
+ /**
5
+ * The name of the command. This defines what the user should type in the
6
+ * input to have the command appear in the chat commands menu.
7
+ */
8
+ name: string;
9
+ /**
10
+ * ID of the provider the command originated from.
11
+ */
12
+ providerId: string;
13
+ /**
14
+ * If set, this will be rendered as the icon for the command in the chat
15
+ * commands menu. Jupyter Chat will choose a default if this is unset.
16
+ */
17
+ icon?: LabIcon | string;
18
+ /**
19
+ * If set, this will be rendered as the description for the command in the
20
+ * chat commands menu. Jupyter Chat will choose a default if this is unset.
21
+ */
22
+ description?: string;
23
+ /**
24
+ * If set, Jupyter Chat will replace the current word with this string after
25
+ * the command is run from the chat commands menu.
26
+ *
27
+ * If all commands from a provider have this property set, then
28
+ * `handleChatCommands()` can just return on the first line.
29
+ */
30
+ replaceWith?: string;
31
+ };
32
+ /**
33
+ * Interface of a command provider.
34
+ */
35
+ export interface IChatCommandProvider {
36
+ /**
37
+ * ID of this command provider.
38
+ */
39
+ id: string;
40
+ /**
41
+ * Async function which accepts the input model and returns a list of
42
+ * valid chat commands that match the current word. The current word is
43
+ * space-separated word at the user's cursor.
44
+ */
45
+ getChatCommands(inputModel: IInputModel): Promise<ChatCommand[]>;
46
+ /**
47
+ * Function called when a chat command is run by the user through the chat
48
+ * commands menu.
49
+ */
50
+ handleChatCommand(command: ChatCommand, inputModel: IInputModel): Promise<void>;
51
+ }
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export {};
@@ -0,0 +1,23 @@
1
+ /// <reference types="react" />
2
+ import { IAttachment } from '../types';
3
+ /**
4
+ * The attachments props.
5
+ */
6
+ export type AttachmentsProps = {
7
+ attachments: IAttachment[];
8
+ onRemove?: (attachment: IAttachment) => void;
9
+ };
10
+ /**
11
+ * The Attachments component.
12
+ */
13
+ export declare function AttachmentPreviewList(props: AttachmentsProps): JSX.Element;
14
+ /**
15
+ * The attachment props.
16
+ */
17
+ export type AttachmentProps = AttachmentsProps & {
18
+ attachment: IAttachment;
19
+ };
20
+ /**
21
+ * The Attachment component.
22
+ */
23
+ export declare function AttachmentPreview(props: AttachmentProps): JSX.Element;
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ // import { IDocumentManager } from '@jupyterlab/docmanager';
6
+ import CloseIcon from '@mui/icons-material/Close';
7
+ import { Box } from '@mui/material';
8
+ import React, { useContext } from 'react';
9
+ import { TooltippedButton } from './mui-extras/tooltipped-button';
10
+ import { AttachmentOpenerContext } from '../context';
11
+ const ATTACHMENTS_CLASS = 'jp-chat-attachments';
12
+ const ATTACHMENT_CLASS = 'jp-chat-attachment';
13
+ const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
14
+ const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
15
+ /**
16
+ * The Attachments component.
17
+ */
18
+ export function AttachmentPreviewList(props) {
19
+ return (React.createElement(Box, { className: ATTACHMENTS_CLASS }, props.attachments.map(attachment => (React.createElement(AttachmentPreview, { ...props, attachment: attachment })))));
20
+ }
21
+ /**
22
+ * The Attachment component.
23
+ */
24
+ export function AttachmentPreview(props) {
25
+ const remove_tooltip = 'Remove attachment';
26
+ const attachmentOpenerRegistry = useContext(AttachmentOpenerContext);
27
+ return (React.createElement(Box, { className: ATTACHMENT_CLASS },
28
+ React.createElement("span", { className: (attachmentOpenerRegistry === null || attachmentOpenerRegistry === void 0 ? void 0 : attachmentOpenerRegistry.get(props.attachment.type))
29
+ ? ATTACHMENT_CLICKABLE_CLASS
30
+ : '', onClick: () => {
31
+ var _a;
32
+ return (_a = attachmentOpenerRegistry === null || attachmentOpenerRegistry === void 0 ? void 0 : attachmentOpenerRegistry.get(props.attachment.type)) === null || _a === void 0 ? void 0 : _a(props.attachment);
33
+ } }, props.attachment.value),
34
+ props.onRemove && (React.createElement(TooltippedButton, { onClick: () => props.onRemove(props.attachment), tooltip: remove_tooltip, buttonProps: {
35
+ size: 'small',
36
+ title: remove_tooltip,
37
+ className: REMOVE_BUTTON_CLASS
38
+ }, sx: {
39
+ minWidth: 'unset',
40
+ padding: '0',
41
+ color: 'inherit'
42
+ } },
43
+ React.createElement(CloseIcon, null)))));
44
+ }
@@ -1,7 +1,8 @@
1
1
  /// <reference types="react" />
2
+ import { IDocumentManager } from '@jupyterlab/docmanager';
2
3
  import { SxProps, Theme } from '@mui/material';
3
- import { IChatModel } from '../model';
4
- import { IAutocompletionRegistry } from '../registry';
4
+ import { IInputModel } from '../input-model';
5
+ import { IChatCommandRegistry } from '../chat-commands';
5
6
  export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
6
7
  /**
7
8
  * The chat input namespace.
@@ -14,11 +15,7 @@ export declare namespace ChatInput {
14
15
  /**
15
16
  * The chat model.
16
17
  */
17
- model: IChatModel;
18
- /**
19
- * The initial value of the input (default to '')
20
- */
21
- value?: string;
18
+ model: IInputModel;
22
19
  /**
23
20
  * The function to be called to send the message.
24
21
  */
@@ -36,12 +33,12 @@ export declare namespace ChatInput {
36
33
  */
37
34
  sx?: SxProps<Theme>;
38
35
  /**
39
- * Autocompletion properties.
36
+ * The document manager.
40
37
  */
41
- autocompletionRegistry?: IAutocompletionRegistry;
38
+ documentManager?: IDocumentManager;
42
39
  /**
43
- * Autocompletion name.
40
+ * Chat command registry.
44
41
  */
45
- autocompletionName?: string;
42
+ chatCommandRegistry?: IChatCommandRegistry;
46
43
  }
47
44
  }
@@ -2,33 +2,36 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- import React, { useEffect, useRef, useState } from 'react';
6
5
  import { Autocomplete, Box, InputAdornment, TextField } from '@mui/material';
7
6
  import clsx from 'clsx';
8
- import { CancelButton } from './input/cancel-button';
9
- import { SendButton } from './input/send-button';
7
+ import React, { useEffect, useRef, useState } from 'react';
8
+ import { AttachmentPreviewList } from './attachments';
9
+ import { AttachButton, CancelButton, SendButton } from './input';
10
+ import { useChatCommands } from './input/use-chat-commands';
10
11
  const INPUT_BOX_CLASS = 'jp-chat-input-container';
11
12
  export function ChatInput(props) {
12
- var _a, _b, _c, _d;
13
- const { autocompletionName, autocompletionRegistry, model } = props;
14
- const autocompletion = useRef();
15
- const [input, setInput] = useState(props.value || '');
13
+ var _a, _b;
14
+ const { documentManager, model } = props;
15
+ const [input, setInput] = useState(model.value);
16
+ const inputRef = useRef();
17
+ const chatCommands = useChatCommands(model, props.chatCommandRegistry);
16
18
  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);
19
+ const [attachments, setAttachments] = useState(model.attachments);
18
20
  // Display the include selection menu if it is not explicitly hidden, and if at least
19
21
  // 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;
22
+ let hideIncludeSelection = (_b = props.hideIncludeSelection) !== null && _b !== void 0 ? _b : false;
21
23
  if (model.activeCellManager === null && model.selectionWatcher === null) {
22
24
  hideIncludeSelection = true;
23
25
  }
24
- // store reference to the input element to enable focusing it easily
25
- const inputRef = useRef();
26
26
  useEffect(() => {
27
- var _a;
27
+ var _a, _b;
28
+ const inputChanged = (_, value) => {
29
+ setInput(value);
30
+ };
31
+ model.valueChanged.connect(inputChanged);
28
32
  const configChanged = (_, config) => {
29
- var _a, _b;
33
+ var _a;
30
34
  setSendWithShiftEnter((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
31
- setTypingNotification((_b = config.sendTypingNotification) !== null && _b !== void 0 ? _b : false);
32
35
  };
33
36
  model.configChanged.connect(configChanged);
34
37
  const focusInputElement = () => {
@@ -37,77 +40,65 @@ export function ChatInput(props) {
37
40
  }
38
41
  };
39
42
  (_a = model.focusInputSignal) === null || _a === void 0 ? void 0 : _a.connect(focusInputElement);
43
+ const attachmentChanged = (_, attachments) => {
44
+ setAttachments([...attachments]);
45
+ };
46
+ (_b = model.attachmentsChanged) === null || _b === void 0 ? void 0 : _b.connect(attachmentChanged);
40
47
  return () => {
41
- var _a, _b;
48
+ var _a, _b, _c;
42
49
  (_a = model.configChanged) === null || _a === void 0 ? void 0 : _a.disconnect(configChanged);
43
50
  (_b = model.focusInputSignal) === null || _b === void 0 ? void 0 : _b.disconnect(focusInputElement);
51
+ (_c = model.attachmentsChanged) === null || _c === void 0 ? void 0 : _c.disconnect(attachmentChanged);
44
52
  };
45
53
  }, [model]);
46
- // The autocomplete commands options.
47
- const [commandOptions, setCommandOptions] = useState([]);
48
- // whether any option is highlighted in the slash command autocomplete
49
- const [highlighted, setHighlighted] = useState(false);
50
- // controls whether the slash command autocomplete is open
51
- const [open, setOpen] = useState(false);
52
54
  const inputExists = !!input.trim();
53
55
  /**
54
- * Effect: fetch the list of available autocomplete commands.
56
+ * `handleKeyDown()`: callback invoked when the user presses any key in the
57
+ * `TextField` component. This is used to send the message when a user presses
58
+ * "Enter". This also handles many of the edge cases in the MUI Autocomplete
59
+ * component.
55
60
  */
56
- useEffect(() => {
57
- if (autocompletionRegistry === undefined) {
58
- return;
59
- }
60
- autocompletion.current = autocompletionName
61
- ? autocompletionRegistry.get(autocompletionName)
62
- : autocompletionRegistry.getDefaultCompletion();
63
- if (autocompletion.current === undefined) {
64
- return;
65
- }
66
- if (Array.isArray(autocompletion.current.commands)) {
67
- setCommandOptions(autocompletion.current.commands);
68
- }
69
- else if (typeof autocompletion.current.commands === 'function') {
70
- autocompletion.current
71
- .commands()
72
- .then((commands) => {
73
- setCommandOptions(commands);
74
- });
75
- }
76
- }, []);
77
- /**
78
- * Effect: Open the autocomplete when the user types the 'opener' string into an
79
- * empty chat input. Close the autocomplete and reset the last selected value when
80
- * the user clears the chat input.
81
- */
82
- useEffect(() => {
83
- var _a, _b;
84
- if (!((_a = autocompletion.current) === null || _a === void 0 ? void 0 : _a.opener)) {
85
- return;
86
- }
87
- if (input === ((_b = autocompletion.current) === null || _b === void 0 ? void 0 : _b.opener)) {
88
- setOpen(true);
89
- return;
90
- }
91
- if (input === '') {
92
- setOpen(false);
61
+ function handleKeyDown(event) {
62
+ /**
63
+ * IMPORTANT: This statement ensures that arrow keys can be used to navigate
64
+ * the multiline input when the chat commands menu is closed.
65
+ */
66
+ if (['ArrowDown', 'ArrowUp'].includes(event.key) &&
67
+ !chatCommands.menu.open) {
68
+ event.stopPropagation();
93
69
  return;
94
70
  }
95
- }, [input]);
96
- function handleKeyDown(event) {
71
+ // remainder of this function only handles the "Enter" key.
97
72
  if (event.key !== 'Enter') {
98
73
  return;
99
74
  }
100
- // Do not send the message if the user was selecting a suggested command from the
101
- // Autocomplete component.
102
- if (highlighted) {
75
+ /**
76
+ * IMPORTANT: This statement ensures that when the chat commands menu is
77
+ * open with a highlighted command, the "Enter" key should run that command
78
+ * instead of sending the message.
79
+ *
80
+ * This is done by returning early and letting the event propagate to the
81
+ * `Autocomplete` component.
82
+ */
83
+ if (chatCommands.menu.highlighted) {
103
84
  return;
104
85
  }
86
+ // remainder of this function only handles the "Enter" key pressed while the
87
+ // commands menu is closed.
88
+ /**
89
+ * IMPORTANT: This ensures that when the "Enter" key is pressed with the
90
+ * commands menu closed, the event is not propagated up to the
91
+ * `Autocomplete` component. Without this, `Autocomplete.onChange()` gets
92
+ * called with an invalid `string` instead of a `ChatCommand`.
93
+ */
94
+ event.stopPropagation();
105
95
  // Do not send empty messages, and avoid adding new line in empty message.
106
96
  if (!inputExists) {
107
97
  event.stopPropagation();
108
98
  event.preventDefault();
109
99
  return;
110
100
  }
101
+ // Finally, send the message when all other conditions are met.
111
102
  if ((sendWithShiftEnter && event.shiftKey) ||
112
103
  (!sendWithShiftEnter && !event.shiftKey)) {
113
104
  onSend();
@@ -131,14 +122,14 @@ ${selection.source}
131
122
  `;
132
123
  }
133
124
  props.onSend(content);
134
- setInput('');
125
+ model.value = '';
135
126
  }
136
127
  /**
137
128
  * Triggered when cancelling edition.
138
129
  */
139
130
  function onCancel() {
140
- setInput(props.value || '');
141
- props.onCancel();
131
+ var _a;
132
+ (_a = props.onCancel) === null || _a === void 0 ? void 0 : _a.call(props);
142
133
  }
143
134
  // Set the helper text based on whether Shift+Enter is used for sending.
144
135
  const helperText = sendWithShiftEnter ? (React.createElement("span", null,
@@ -153,7 +144,8 @@ ${selection.source}
153
144
  React.createElement("b", null, "Enter"),
154
145
  " to add a new line"));
155
146
  return (React.createElement(Box, { sx: props.sx, className: clsx(INPUT_BOX_CLASS) },
156
- React.createElement(Autocomplete, { options: commandOptions, value: props.value, open: open, autoHighlight: true, freeSolo: true,
147
+ React.createElement(AttachmentPreviewList, { attachments: attachments, onRemove: model.removeAttachment }),
148
+ React.createElement(Autocomplete, { ...chatCommands.autocompleteProps,
157
149
  // ensure the autocomplete popup always renders on top
158
150
  componentsProps: {
159
151
  popper: {
@@ -170,37 +162,20 @@ ${selection.source}
170
162
  padding: 2
171
163
  }
172
164
  }
173
- }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, InputProps: {
165
+ }, 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: {
174
166
  ...params.InputProps,
175
167
  endAdornment: (React.createElement(InputAdornment, { position: "end" },
168
+ documentManager && model.addAttachment && (React.createElement(AttachButton, { documentManager: documentManager, onAttach: model.addAttachment })),
176
169
  props.onCancel && React.createElement(CancelButton, { onCancel: onCancel }),
177
- React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
170
+ React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists || attachments.length > 0, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
178
171
  }, FormHelperTextProps: {
179
172
  sx: { marginLeft: 'auto', marginRight: 0 }
180
- }, helperText: input.length > 2 ? helperText : ' ' })), ...(_d = autocompletion.current) === null || _d === void 0 ? void 0 : _d.props, inputValue: input, onInputChange: (_, newValue) => {
181
- setInput(newValue);
182
- if (typingNotification && model.inputChanged) {
183
- model.inputChanged(newValue);
173
+ }, helperText: input.length > 2 ? helperText : ' ' })), inputValue: input, onInputChange: (_, newValue, reason) => {
174
+ // Do not update the value if the reason is 'reset', which should occur only
175
+ // if an autocompletion command has been selected. In this case, the value is
176
+ // set in the 'onChange()' callback of the autocompletion (to avoid conflicts).
177
+ if (reason !== 'reset') {
178
+ model.value = newValue;
184
179
  }
185
- }, onHighlightChange:
186
- /**
187
- * On highlight change: set `highlighted` to whether an option is
188
- * highlighted by the user.
189
- *
190
- * This isn't called when an option is selected for some reason, so we
191
- * need to call `setHighlighted(false)` in `onClose()`.
192
- */
193
- (_, highlightedOption) => {
194
- setHighlighted(!!highlightedOption);
195
- }, onClose:
196
- /**
197
- * On close: set `highlighted` to `false` and close the popup by
198
- * setting `open` to `false`.
199
- */
200
- () => {
201
- setHighlighted(false);
202
- setOpen(false);
203
- },
204
- // hide default extra right padding in the text field
205
- disableClearable: true })));
180
+ } })));
206
181
  }
@@ -1,7 +1,9 @@
1
+ import { IDocumentManager } from '@jupyterlab/docmanager';
1
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
3
  import { PromiseDelegate } from '@lumino/coreutils';
3
4
  import type { SxProps, Theme } from '@mui/material';
4
5
  import React from 'react';
6
+ import { IChatCommandRegistry } from '../chat-commands';
5
7
  import { IChatModel } from '../model';
6
8
  import { IChatMessage, IUser } from '../types';
7
9
  /**
@@ -10,6 +12,8 @@ import { IChatMessage, IUser } from '../types';
10
12
  type BaseMessageProps = {
11
13
  rmRegistry: IRenderMimeRegistry;
12
14
  model: IChatModel;
15
+ chatCommandRegistry?: IChatCommandRegistry;
16
+ documentManager?: IDocumentManager;
13
17
  };
14
18
  /**
15
19
  * The messages list component.
@@ -8,9 +8,11 @@ import { PromiseDelegate } from '@lumino/coreutils';
8
8
  import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
9
9
  import clsx from 'clsx';
10
10
  import React, { useEffect, useState, useRef, forwardRef } from 'react';
11
+ import { AttachmentPreviewList } from './attachments';
11
12
  import { ChatInput } from './chat-input';
12
13
  import { MarkdownRenderer } from './markdown-renderer';
13
14
  import { ScrollContainer } from './scroll-container';
15
+ import { InputModel } from '../input-model';
14
16
  const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
15
17
  const MESSAGE_CLASS = 'jp-chat-message';
16
18
  const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
@@ -226,6 +228,7 @@ export const ChatMessage = forwardRef((props, ref) => {
226
228
  const [deleted, setDeleted] = useState(false);
227
229
  const [canEdit, setCanEdit] = useState(false);
228
230
  const [canDelete, setCanDelete] = useState(false);
231
+ const [inputModel, setInputModel] = useState(null);
229
232
  // Look if the message can be deleted or edited.
230
233
  useEffect(() => {
231
234
  var _a;
@@ -241,6 +244,23 @@ export const ChatMessage = forwardRef((props, ref) => {
241
244
  setCanDelete(false);
242
245
  }
243
246
  }, [model, message]);
247
+ // Create an input model only if the message is edited.
248
+ useEffect(() => {
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
+ }));
259
+ }
260
+ else {
261
+ setInputModel(null);
262
+ }
263
+ }, [edit]);
244
264
  // Cancel the current edition of the message.
245
265
  const cancelEdition = () => {
246
266
  setEdit(false);
@@ -253,6 +273,7 @@ export const ChatMessage = forwardRef((props, ref) => {
253
273
  // Update the message
254
274
  const updatedMessage = { ...message };
255
275
  updatedMessage.body = input;
276
+ updatedMessage.attachments = inputModel === null || inputModel === void 0 ? void 0 : inputModel.attachments;
256
277
  model.updateMessage(id, updatedMessage);
257
278
  setEdit(false);
258
279
  };
@@ -264,7 +285,12 @@ export const ChatMessage = forwardRef((props, ref) => {
264
285
  model.deleteMessage(id);
265
286
  };
266
287
  // Empty if the message has been deleted.
267
- return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "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(MarkdownRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise }))));
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 })),
290
+ message.attachments && !edit && (
291
+ // Display the attachments only if message is not edited, otherwise the
292
+ // input component display them.
293
+ React.createElement(AttachmentPreviewList, { attachments: message.attachments }))));
268
294
  });
269
295
  /**
270
296
  * The writers component, displaying the current writers.
@@ -1,8 +1,10 @@
1
1
  /// <reference types="react" />
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
+ import { IDocumentManager } from '@jupyterlab/docmanager';
3
4
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
+ import { IChatCommandRegistry } from '../chat-commands';
4
6
  import { IChatModel } from '../model';
5
- import { IAutocompletionRegistry } from '../registry';
7
+ import { IAttachmentOpenerRegistry } from '../registry';
6
8
  export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
7
9
  export declare function Chat(props: Chat.IOptions): JSX.Element;
8
10
  /**
@@ -22,13 +24,17 @@ export declare namespace Chat {
22
24
  */
23
25
  rmRegistry: IRenderMimeRegistry;
24
26
  /**
25
- * Autocompletion registry.
27
+ * The document manager.
26
28
  */
27
- autocompletionRegistry?: IAutocompletionRegistry;
29
+ documentManager?: IDocumentManager;
28
30
  /**
29
- * Autocompletion name.
31
+ * Chat command registry.
30
32
  */
31
- autocompletionName?: string;
33
+ chatCommandRegistry?: IChatCommandRegistry;
34
+ /**
35
+ * Attachment opener registry.
36
+ */
37
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
32
38
  }
33
39
  /**
34
40
  * The options to build the Chat UI.