@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.
- package/lib/active-cell-manager.js +1 -4
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +6 -0
- package/lib/chat-commands/registry.d.ts +28 -0
- package/lib/chat-commands/registry.js +29 -0
- package/lib/chat-commands/types.d.ts +51 -0
- package/lib/chat-commands/types.js +5 -0
- package/lib/components/attachments.d.ts +23 -0
- package/lib/components/attachments.js +44 -0
- package/lib/components/chat-input.d.ts +8 -11
- package/lib/components/chat-input.js +70 -95
- package/lib/components/chat-messages.d.ts +4 -0
- package/lib/components/chat-messages.js +27 -1
- package/lib/components/chat.d.ts +11 -5
- package/lib/components/chat.js +7 -8
- package/lib/components/input/attach-button.d.ts +14 -0
- package/lib/components/input/attach-button.js +45 -0
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/send-button.d.ts +2 -2
- package/lib/components/input/use-chat-commands.d.ts +19 -0
- package/lib/components/input/use-chat-commands.js +127 -0
- package/lib/context.d.ts +3 -0
- package/lib/context.js +6 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/input-model.d.ts +221 -0
- package/lib/input-model.js +217 -0
- package/lib/model.d.ts +10 -25
- package/lib/model.js +15 -17
- package/lib/registry.d.ts +11 -64
- package/lib/registry.js +4 -72
- package/lib/types.d.ts +19 -38
- package/lib/widgets/chat-widget.js +2 -1
- package/package.json +3 -114
- package/src/active-cell-manager.ts +0 -3
- package/src/chat-commands/index.ts +7 -0
- package/src/chat-commands/registry.ts +60 -0
- package/src/chat-commands/types.ts +67 -0
- package/src/components/attachments.tsx +91 -0
- package/src/components/chat-input.tsx +97 -124
- package/src/components/chat-messages.tsx +36 -3
- package/src/components/chat.tsx +28 -19
- package/src/components/input/attach-button.tsx +68 -0
- package/src/components/input/cancel-button.tsx +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/send-button.tsx +2 -2
- package/src/components/input/use-chat-commands.tsx +186 -0
- package/src/context.ts +10 -0
- package/src/index.ts +2 -0
- package/src/input-model.ts +406 -0
- package/src/model.ts +24 -35
- package/src/registry.ts +14 -108
- package/src/types.ts +19 -39
- package/src/widgets/chat-widget.tsx +2 -1
- package/style/chat.css +27 -9
- package/style/icons/include-selection.svg +3 -1
- package/style/icons/read.svg +8 -6
- 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
|
|
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 {
|
|
19
|
-
import { SendButton } from './input
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
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 {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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 [
|
|
39
|
-
model.
|
|
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
|
|
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
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
100
|
+
// remainder of this function only handles the "Enter" key.
|
|
131
101
|
if (event.key !== 'Enter') {
|
|
132
102
|
return;
|
|
133
103
|
}
|
|
134
104
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
161
|
+
model.value = '';
|
|
175
162
|
}
|
|
176
163
|
|
|
177
164
|
/**
|
|
178
165
|
* Triggered when cancelling edition.
|
|
179
166
|
*/
|
|
180
167
|
function onCancel() {
|
|
181
|
-
|
|
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
|
-
|
|
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={(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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:
|
|
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
|
-
*
|
|
296
|
+
* The document manager.
|
|
324
297
|
*/
|
|
325
|
-
|
|
298
|
+
documentManager?: IDocumentManager;
|
|
326
299
|
/**
|
|
327
|
-
*
|
|
300
|
+
* Chat command registry.
|
|
328
301
|
*/
|
|
329
|
-
|
|
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={
|
|
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
|
}
|
package/src/components/chat.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
43
|
+
paddingTop: 1,
|
|
42
44
|
paddingBottom: 0,
|
|
43
45
|
borderTop: '1px solid var(--jp-border-color1)'
|
|
44
46
|
}}
|
|
45
|
-
model={model}
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
126
|
+
* The document manager.
|
|
127
|
+
*/
|
|
128
|
+
documentManager?: IDocumentManager;
|
|
129
|
+
/**
|
|
130
|
+
* Chat command registry.
|
|
122
131
|
*/
|
|
123
|
-
|
|
132
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
124
133
|
/**
|
|
125
|
-
*
|
|
134
|
+
* Attachment opener registry.
|
|
126
135
|
*/
|
|
127
|
-
|
|
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
|
+
}
|
|
@@ -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:
|
|
24
|
+
model: IInputModel;
|
|
25
25
|
sendWithShiftEnter: boolean;
|
|
26
26
|
inputExists: boolean;
|
|
27
27
|
onSend: (selection?: Selection) => unknown;
|