@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
@@ -3,10 +3,10 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import React, { useEffect, useRef, useState } from 'react';
7
-
6
+ import { IDocumentManager } from '@jupyterlab/docmanager';
8
7
  import {
9
8
  Autocomplete,
9
+ AutocompleteInputChangeReason,
10
10
  Box,
11
11
  InputAdornment,
12
12
  SxProps,
@@ -14,29 +14,29 @@ import {
14
14
  Theme
15
15
  } from '@mui/material';
16
16
  import clsx from 'clsx';
17
+ import React, { useEffect, useRef, useState } from 'react';
17
18
 
18
- import { CancelButton } from './input/cancel-button';
19
- import { SendButton } from './input/send-button';
20
- import { IChatModel } from '../model';
21
- import { IAutocompletionRegistry } from '../registry';
22
- import {
23
- AutocompleteCommand,
24
- IAutocompletionCommandsProps,
25
- IConfig,
26
- Selection
27
- } from '../types';
19
+ import { AttachmentPreviewList } from './attachments';
20
+ import { AttachButton, CancelButton, SendButton } from './input';
21
+ import { IInputModel, InputModel } from '../input-model';
22
+ import { IAttachment, Selection } from '../types';
23
+ import { useChatCommands } from './input/use-chat-commands';
24
+ import { IChatCommandRegistry } from '../chat-commands';
28
25
 
29
26
  const INPUT_BOX_CLASS = 'jp-chat-input-container';
30
27
 
31
28
  export function ChatInput(props: ChatInput.IProps): JSX.Element {
32
- const { autocompletionName, autocompletionRegistry, model } = props;
33
- const autocompletion = useRef<IAutocompletionCommandsProps>();
34
- const [input, setInput] = useState<string>(props.value || '');
29
+ const { documentManager, model } = props;
30
+ const [input, setInput] = useState<string>(model.value);
31
+ const inputRef = useRef<HTMLInputElement>();
32
+
33
+ const chatCommands = useChatCommands(model, props.chatCommandRegistry);
34
+
35
35
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState<boolean>(
36
36
  model.config.sendWithShiftEnter ?? false
37
37
  );
38
- const [typingNotification, setTypingNotification] = useState<boolean>(
39
- model.config.sendTypingNotification ?? false
38
+ const [attachments, setAttachments] = useState<IAttachment[]>(
39
+ model.attachments
40
40
  );
41
41
 
42
42
  // Display the include selection menu if it is not explicitly hidden, and if at least
@@ -46,13 +46,14 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
46
46
  hideIncludeSelection = true;
47
47
  }
48
48
 
49
- // store reference to the input element to enable focusing it easily
50
- const inputRef = useRef<HTMLInputElement>();
51
-
52
49
  useEffect(() => {
53
- const configChanged = (_: IChatModel, config: IConfig) => {
50
+ const inputChanged = (_: IInputModel, value: string) => {
51
+ setInput(value);
52
+ };
53
+ model.valueChanged.connect(inputChanged);
54
+
55
+ const configChanged = (_: IInputModel, config: InputModel.IConfig) => {
54
56
  setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
55
- setTypingNotification(config.sendTypingNotification ?? false);
56
57
  };
57
58
  model.configChanged.connect(configChanged);
58
59
 
@@ -63,81 +64,66 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
63
64
  };
64
65
  model.focusInputSignal?.connect(focusInputElement);
65
66
 
67
+ const attachmentChanged = (_: IInputModel, attachments: IAttachment[]) => {
68
+ setAttachments([...attachments]);
69
+ };
70
+ model.attachmentsChanged?.connect(attachmentChanged);
71
+
66
72
  return () => {
67
73
  model.configChanged?.disconnect(configChanged);
68
74
  model.focusInputSignal?.disconnect(focusInputElement);
75
+ model.attachmentsChanged?.disconnect(attachmentChanged);
69
76
  };
70
77
  }, [model]);
71
78
 
72
- // The autocomplete commands options.
73
- const [commandOptions, setCommandOptions] = useState<AutocompleteCommand[]>(
74
- []
75
- );
76
- // whether any option is highlighted in the slash command autocomplete
77
- const [highlighted, setHighlighted] = useState<boolean>(false);
78
- // controls whether the slash command autocomplete is open
79
- const [open, setOpen] = useState<boolean>(false);
80
-
81
79
  const inputExists = !!input.trim();
82
80
 
83
81
  /**
84
- * Effect: fetch the list of available autocomplete commands.
85
- */
86
- useEffect(() => {
87
- if (autocompletionRegistry === undefined) {
88
- return;
89
- }
90
- autocompletion.current = autocompletionName
91
- ? autocompletionRegistry.get(autocompletionName)
92
- : autocompletionRegistry.getDefaultCompletion();
93
-
94
- if (autocompletion.current === undefined) {
95
- return;
96
- }
97
-
98
- if (Array.isArray(autocompletion.current.commands)) {
99
- setCommandOptions(autocompletion.current.commands);
100
- } else if (typeof autocompletion.current.commands === 'function') {
101
- autocompletion.current
102
- .commands()
103
- .then((commands: AutocompleteCommand[]) => {
104
- setCommandOptions(commands);
105
- });
106
- }
107
- }, []);
108
-
109
- /**
110
- * Effect: Open the autocomplete when the user types the 'opener' string into an
111
- * empty chat input. Close the autocomplete and reset the last selected value when
112
- * the user clears the chat input.
82
+ * `handleKeyDown()`: callback invoked when the user presses any key in the
83
+ * `TextField` component. This is used to send the message when a user presses
84
+ * "Enter". This also handles many of the edge cases in the MUI Autocomplete
85
+ * component.
113
86
  */
114
- useEffect(() => {
115
- if (!autocompletion.current?.opener) {
116
- return;
117
- }
118
-
119
- if (input === autocompletion.current?.opener) {
120
- setOpen(true);
121
- return;
122
- }
123
-
124
- if (input === '') {
125
- setOpen(false);
87
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
88
+ /**
89
+ * IMPORTANT: This statement ensures that arrow keys can be used to navigate
90
+ * the multiline input when the chat commands menu is closed.
91
+ */
92
+ if (
93
+ ['ArrowDown', 'ArrowUp'].includes(event.key) &&
94
+ !chatCommands.menu.open
95
+ ) {
96
+ event.stopPropagation();
126
97
  return;
127
98
  }
128
- }, [input]);
129
99
 
130
- function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
100
+ // remainder of this function only handles the "Enter" key.
131
101
  if (event.key !== 'Enter') {
132
102
  return;
133
103
  }
134
104
 
135
- // Do not send the message if the user was selecting a suggested command from the
136
- // Autocomplete component.
137
- if (highlighted) {
105
+ /**
106
+ * IMPORTANT: This statement ensures that when the chat commands menu is
107
+ * open with a highlighted command, the "Enter" key should run that command
108
+ * instead of sending the message.
109
+ *
110
+ * This is done by returning early and letting the event propagate to the
111
+ * `Autocomplete` component.
112
+ */
113
+ if (chatCommands.menu.highlighted) {
138
114
  return;
139
115
  }
140
116
 
117
+ // remainder of this function only handles the "Enter" key pressed while the
118
+ // commands menu is closed.
119
+ /**
120
+ * IMPORTANT: This ensures that when the "Enter" key is pressed with the
121
+ * commands menu closed, the event is not propagated up to the
122
+ * `Autocomplete` component. Without this, `Autocomplete.onChange()` gets
123
+ * called with an invalid `string` instead of a `ChatCommand`.
124
+ */
125
+ event.stopPropagation();
126
+
141
127
  // Do not send empty messages, and avoid adding new line in empty message.
142
128
  if (!inputExists) {
143
129
  event.stopPropagation();
@@ -145,6 +131,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
145
131
  return;
146
132
  }
147
133
 
134
+ // Finally, send the message when all other conditions are met.
148
135
  if (
149
136
  (sendWithShiftEnter && event.shiftKey) ||
150
137
  (!sendWithShiftEnter && !event.shiftKey)
@@ -171,15 +158,14 @@ ${selection.source}
171
158
  `;
172
159
  }
173
160
  props.onSend(content);
174
- setInput('');
161
+ model.value = '';
175
162
  }
176
163
 
177
164
  /**
178
165
  * Triggered when cancelling edition.
179
166
  */
180
167
  function onCancel() {
181
- setInput(props.value || '');
182
- props.onCancel!();
168
+ props.onCancel?.();
183
169
  }
184
170
 
185
171
  // Set the helper text based on whether Shift+Enter is used for sending.
@@ -195,12 +181,12 @@ ${selection.source}
195
181
 
196
182
  return (
197
183
  <Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
184
+ <AttachmentPreviewList
185
+ attachments={attachments}
186
+ onRemove={model.removeAttachment}
187
+ />
198
188
  <Autocomplete
199
- options={commandOptions}
200
- value={props.value}
201
- open={open}
202
- autoHighlight
203
- freeSolo
189
+ {...chatCommands.autocompleteProps}
204
190
  // ensure the autocomplete popup always renders on top
205
191
  componentsProps={{
206
192
  popper: {
@@ -228,15 +214,25 @@ ${selection.source}
228
214
  onKeyDown={handleKeyDown}
229
215
  placeholder="Start chatting"
230
216
  inputRef={inputRef}
217
+ sx={{ marginTop: '1px' }}
218
+ onSelect={() =>
219
+ (model.cursorIndex = inputRef.current?.selectionStart ?? null)
220
+ }
231
221
  InputProps={{
232
222
  ...params.InputProps,
233
223
  endAdornment: (
234
224
  <InputAdornment position="end">
225
+ {documentManager && model.addAttachment && (
226
+ <AttachButton
227
+ documentManager={documentManager}
228
+ onAttach={model.addAttachment}
229
+ />
230
+ )}
235
231
  {props.onCancel && <CancelButton onCancel={onCancel} />}
236
232
  <SendButton
237
233
  model={model}
238
234
  sendWithShiftEnter={sendWithShiftEnter}
239
- inputExists={inputExists}
235
+ inputExists={inputExists || attachments.length > 0}
240
236
  onSend={onSend}
241
237
  hideIncludeSelection={hideIncludeSelection}
242
238
  hasButtonOnLeft={!!props.onCancel}
@@ -250,38 +246,19 @@ ${selection.source}
250
246
  helperText={input.length > 2 ? helperText : ' '}
251
247
  />
252
248
  )}
253
- {...autocompletion.current?.props}
254
249
  inputValue={input}
255
- onInputChange={(_, newValue: string) => {
256
- setInput(newValue);
257
- if (typingNotification && model.inputChanged) {
258
- model.inputChanged(newValue);
250
+ onInputChange={(
251
+ _,
252
+ newValue: string,
253
+ reason: AutocompleteInputChangeReason
254
+ ) => {
255
+ // Do not update the value if the reason is 'reset', which should occur only
256
+ // if an autocompletion command has been selected. In this case, the value is
257
+ // set in the 'onChange()' callback of the autocompletion (to avoid conflicts).
258
+ if (reason !== 'reset') {
259
+ model.value = newValue;
259
260
  }
260
261
  }}
261
- onHighlightChange={
262
- /**
263
- * On highlight change: set `highlighted` to whether an option is
264
- * highlighted by the user.
265
- *
266
- * This isn't called when an option is selected for some reason, so we
267
- * need to call `setHighlighted(false)` in `onClose()`.
268
- */
269
- (_, highlightedOption) => {
270
- setHighlighted(!!highlightedOption);
271
- }
272
- }
273
- onClose={
274
- /**
275
- * On close: set `highlighted` to `false` and close the popup by
276
- * setting `open` to `false`.
277
- */
278
- () => {
279
- setHighlighted(false);
280
- setOpen(false);
281
- }
282
- }
283
- // hide default extra right padding in the text field
284
- disableClearable
285
262
  />
286
263
  </Box>
287
264
  );
@@ -298,11 +275,7 @@ export namespace ChatInput {
298
275
  /**
299
276
  * The chat model.
300
277
  */
301
- model: IChatModel;
302
- /**
303
- * The initial value of the input (default to '')
304
- */
305
- value?: string;
278
+ model: IInputModel;
306
279
  /**
307
280
  * The function to be called to send the message.
308
281
  */
@@ -320,12 +293,12 @@ export namespace ChatInput {
320
293
  */
321
294
  sx?: SxProps<Theme>;
322
295
  /**
323
- * Autocompletion properties.
296
+ * The document manager.
324
297
  */
325
- autocompletionRegistry?: IAutocompletionRegistry;
298
+ documentManager?: IDocumentManager;
326
299
  /**
327
- * Autocompletion name.
300
+ * Chat command registry.
328
301
  */
329
- autocompletionName?: string;
302
+ chatCommandRegistry?: IChatCommandRegistry;
330
303
  }
331
304
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Button } from '@jupyter/react-components';
7
+ import { IDocumentManager } from '@jupyterlab/docmanager';
7
8
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
9
  import {
9
10
  LabIcon,
@@ -16,9 +17,12 @@ import type { SxProps, Theme } from '@mui/material';
16
17
  import clsx from 'clsx';
17
18
  import React, { useEffect, useState, useRef, forwardRef } from 'react';
18
19
 
20
+ import { AttachmentPreviewList } from './attachments';
19
21
  import { ChatInput } from './chat-input';
20
22
  import { MarkdownRenderer } from './markdown-renderer';
21
23
  import { ScrollContainer } from './scroll-container';
24
+ import { IChatCommandRegistry } from '../chat-commands';
25
+ import { IInputModel, InputModel } from '../input-model';
22
26
  import { IChatModel } from '../model';
23
27
  import { IChatMessage, IUser } from '../types';
24
28
 
@@ -39,6 +43,8 @@ const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
39
43
  type BaseMessageProps = {
40
44
  rmRegistry: IRenderMimeRegistry;
41
45
  model: IChatModel;
46
+ chatCommandRegistry?: IChatCommandRegistry;
47
+ documentManager?: IDocumentManager;
42
48
  };
43
49
 
44
50
  /**
@@ -337,6 +343,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
337
343
  const [deleted, setDeleted] = useState<boolean>(false);
338
344
  const [canEdit, setCanEdit] = useState<boolean>(false);
339
345
  const [canDelete, setCanDelete] = useState<boolean>(false);
346
+ const [inputModel, setInputModel] = useState<IInputModel | null>(null);
340
347
 
341
348
  // Look if the message can be deleted or edited.
342
349
  useEffect(() => {
@@ -352,6 +359,25 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
352
359
  }
353
360
  }, [model, message]);
354
361
 
362
+ // Create an input model only if the message is edited.
363
+ useEffect(() => {
364
+ if (edit && canEdit) {
365
+ setInputModel(
366
+ new InputModel({
367
+ value: message.body,
368
+ activeCellManager: model.activeCellManager,
369
+ selectionWatcher: model.selectionWatcher,
370
+ config: {
371
+ sendWithShiftEnter: model.config.sendWithShiftEnter
372
+ },
373
+ attachments: message.attachments
374
+ })
375
+ );
376
+ } else {
377
+ setInputModel(null);
378
+ }
379
+ }, [edit]);
380
+
355
381
  // Cancel the current edition of the message.
356
382
  const cancelEdition = (): void => {
357
383
  setEdit(false);
@@ -365,6 +391,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
365
391
  // Update the message
366
392
  const updatedMessage = { ...message };
367
393
  updatedMessage.body = input;
394
+ updatedMessage.attachments = inputModel?.attachments;
368
395
  model.updateMessage!(id, updatedMessage);
369
396
  setEdit(false);
370
397
  };
@@ -382,13 +409,14 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
382
409
  <div ref={ref} data-index={props.index}></div>
383
410
  ) : (
384
411
  <div ref={ref} data-index={props.index}>
385
- {edit && canEdit ? (
412
+ {edit && canEdit && inputModel ? (
386
413
  <ChatInput
387
- value={message.body}
388
414
  onSend={(input: string) => updateMessage(message.id, input)}
389
415
  onCancel={() => cancelEdition()}
390
- model={model}
416
+ model={inputModel}
391
417
  hideIncludeSelection={true}
418
+ chatCommandRegistry={props.chatCommandRegistry}
419
+ documentManager={props.documentManager}
392
420
  />
393
421
  ) : (
394
422
  <MarkdownRenderer
@@ -400,6 +428,11 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
400
428
  rendered={props.renderedPromise}
401
429
  />
402
430
  )}
431
+ {message.attachments && !edit && (
432
+ // Display the attachments only if message is not edited, otherwise the
433
+ // input component display them.
434
+ <AttachmentPreviewList attachments={message.attachments} />
435
+ )}
403
436
  </div>
404
437
  );
405
438
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IThemeManager } from '@jupyterlab/apputils';
7
+ import { IDocumentManager } from '@jupyterlab/docmanager';
7
8
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
9
  import ArrowBackIcon from '@mui/icons-material/ArrowBack';
9
10
  import SettingsIcon from '@mui/icons-material/Settings';
@@ -12,40 +13,42 @@ import { Box } from '@mui/system';
12
13
  import React, { useState } from 'react';
13
14
 
14
15
  import { JlThemeProvider } from './jl-theme-provider';
16
+ import { IChatCommandRegistry } from '../chat-commands';
15
17
  import { ChatMessages } from './chat-messages';
16
18
  import { ChatInput } from './chat-input';
19
+ import { AttachmentOpenerContext } from '../context';
17
20
  import { IChatModel } from '../model';
18
- import { IAutocompletionRegistry } from '../registry';
21
+ import { IAttachmentOpenerRegistry } from '../registry';
19
22
 
20
23
  export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
21
- const {
22
- model,
23
- rmRegistry: renderMimeRegistry,
24
- autocompletionRegistry
25
- } = props;
26
- // no need to append to messageGroups imperatively here. all of that is
27
- // handled by the listeners registered in the effect hooks above.
24
+ const { model } = props;
28
25
  const onSend = async (input: string) => {
29
26
  // send message to backend
30
27
  model.sendMessage({ body: input });
31
28
  };
32
29
 
33
30
  return (
34
- <>
35
- <ChatMessages rmRegistry={renderMimeRegistry} model={model} />
31
+ <AttachmentOpenerContext.Provider value={props.attachmentOpenerRegistry}>
32
+ <ChatMessages
33
+ rmRegistry={props.rmRegistry}
34
+ model={model}
35
+ chatCommandRegistry={props.chatCommandRegistry}
36
+ documentManager={props.documentManager}
37
+ />
36
38
  <ChatInput
37
39
  onSend={onSend}
38
40
  sx={{
39
41
  paddingLeft: 4,
40
42
  paddingRight: 4,
41
- paddingTop: 3.5,
43
+ paddingTop: 1,
42
44
  paddingBottom: 0,
43
45
  borderTop: '1px solid var(--jp-border-color1)'
44
46
  }}
45
- model={model}
46
- autocompletionRegistry={autocompletionRegistry}
47
+ model={model.input}
48
+ documentManager={props.documentManager}
49
+ chatCommandRegistry={props.chatCommandRegistry}
47
50
  />
48
- </>
51
+ </AttachmentOpenerContext.Provider>
49
52
  );
50
53
  }
51
54
 
@@ -90,7 +93,9 @@ export function Chat(props: Chat.IOptions): JSX.Element {
90
93
  <ChatBody
91
94
  model={props.model}
92
95
  rmRegistry={props.rmRegistry}
93
- autocompletionRegistry={props.autocompletionRegistry}
96
+ documentManager={props.documentManager}
97
+ chatCommandRegistry={props.chatCommandRegistry}
98
+ attachmentOpenerRegistry={props.attachmentOpenerRegistry}
94
99
  />
95
100
  )}
96
101
  {view === Chat.View.settings && props.settingsPanel && (
@@ -118,13 +123,17 @@ export namespace Chat {
118
123
  */
119
124
  rmRegistry: IRenderMimeRegistry;
120
125
  /**
121
- * Autocompletion registry.
126
+ * The document manager.
127
+ */
128
+ documentManager?: IDocumentManager;
129
+ /**
130
+ * Chat command registry.
122
131
  */
123
- autocompletionRegistry?: IAutocompletionRegistry;
132
+ chatCommandRegistry?: IChatCommandRegistry;
124
133
  /**
125
- * Autocompletion name.
134
+ * Attachment opener registry.
126
135
  */
127
- autocompletionName?: string;
136
+ attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
128
137
  }
129
138
 
130
139
  /**
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IDocumentManager } from '@jupyterlab/docmanager';
7
+ import { FileDialog } from '@jupyterlab/filebrowser';
8
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
9
+ import React from 'react';
10
+
11
+ import { TooltippedButton } from '../mui-extras/tooltipped-button';
12
+ import { IAttachment } from '../../types';
13
+
14
+ const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
15
+
16
+ /**
17
+ * The attach button props.
18
+ */
19
+ export type AttachButtonProps = {
20
+ documentManager: IDocumentManager;
21
+ onAttach: (attachment: IAttachment) => void;
22
+ };
23
+
24
+ /**
25
+ * The attach button.
26
+ */
27
+ export function AttachButton(props: AttachButtonProps): JSX.Element {
28
+ const tooltip = 'Add attachment';
29
+
30
+ const onclick = async () => {
31
+ try {
32
+ const files = await FileDialog.getOpenFiles({
33
+ title: 'Select files to attach',
34
+ manager: props.documentManager
35
+ });
36
+ if (files.value) {
37
+ files.value.forEach(file => {
38
+ if (file.type !== 'directory') {
39
+ props.onAttach({ type: 'file', value: file.path });
40
+ }
41
+ });
42
+ }
43
+ } catch (e) {
44
+ console.warn('Error selecting files to attach', e);
45
+ }
46
+ };
47
+
48
+ return (
49
+ <TooltippedButton
50
+ onClick={onclick}
51
+ tooltip={tooltip}
52
+ buttonProps={{
53
+ size: 'small',
54
+ variant: 'contained',
55
+ title: tooltip,
56
+ className: ATTACH_BUTTON_CLASS
57
+ }}
58
+ sx={{
59
+ minWidth: 'unset',
60
+ padding: '4px',
61
+ borderRadius: '2px 0px 0px 2px',
62
+ marginRight: '1px'
63
+ }}
64
+ >
65
+ <AttachFileIcon />
66
+ </TooltippedButton>
67
+ );
68
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import CancelIcon from '@mui/icons-material/Cancel';
7
7
  import React from 'react';
8
+
8
9
  import { TooltippedButton } from '../mui-extras/tooltipped-button';
9
10
 
10
11
  const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
@@ -3,5 +3,6 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ export * from './attach-button';
6
7
  export * from './cancel-button';
7
8
  export * from './send-button';
@@ -8,9 +8,9 @@ import SendIcon from '@mui/icons-material/Send';
8
8
  import { Box, Menu, MenuItem, Typography } from '@mui/material';
9
9
  import React, { useCallback, useEffect, useState } from 'react';
10
10
 
11
- import { IChatModel } from '../../model';
12
11
  import { TooltippedButton } from '../mui-extras/tooltipped-button';
13
12
  import { includeSelectionIcon } from '../../icons';
13
+ import { IInputModel } from '../../input-model';
14
14
  import { Selection } from '../../types';
15
15
 
16
16
  const SEND_BUTTON_CLASS = 'jp-chat-send-button';
@@ -21,7 +21,7 @@ const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
21
21
  * The send button props.
22
22
  */
23
23
  export type SendButtonProps = {
24
- model: IChatModel;
24
+ model: IInputModel;
25
25
  sendWithShiftEnter: boolean;
26
26
  inputExists: boolean;
27
27
  onSend: (selection?: Selection) => unknown;