@jupyter/chat 0.18.2 → 0.19.0-alpha.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/components/attachments.js +47 -21
- package/lib/components/chat.d.ts +5 -0
- package/lib/components/chat.js +7 -6
- package/lib/components/code-blocks/code-toolbar.js +29 -10
- package/lib/components/code-blocks/copy-button.js +11 -3
- package/lib/components/index.d.ts +0 -1
- package/lib/components/index.js +0 -1
- package/lib/components/input/buttons/attach-button.js +7 -2
- package/lib/components/input/buttons/cancel-button.js +12 -10
- package/lib/components/input/buttons/index.d.ts +2 -0
- package/lib/components/input/buttons/index.js +2 -0
- package/lib/components/input/buttons/save-edit-button.d.ts +6 -0
- package/lib/components/input/buttons/save-edit-button.js +51 -0
- package/lib/components/input/buttons/send-button.d.ts +1 -1
- package/lib/components/input/buttons/send-button.js +35 -122
- package/lib/components/input/buttons/stop-button.d.ts +6 -0
- package/lib/components/input/buttons/stop-button.js +63 -0
- package/lib/components/input/chat-input.d.ts +15 -0
- package/lib/components/input/chat-input.js +109 -46
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/toolbar-registry.d.ts +10 -0
- package/lib/components/input/toolbar-registry.js +10 -1
- package/lib/components/input/use-chat-commands.js +9 -4
- package/lib/components/input/writing-indicator.d.ts +15 -0
- package/lib/components/input/writing-indicator.js +50 -0
- package/lib/components/messages/header.d.ts +4 -0
- package/lib/components/messages/header.js +4 -0
- package/lib/components/messages/index.d.ts +0 -1
- package/lib/components/messages/index.js +0 -1
- package/lib/components/messages/message.js +1 -1
- package/lib/components/messages/messages.d.ts +5 -0
- package/lib/components/messages/messages.js +24 -14
- package/lib/components/messages/toolbar.js +37 -15
- package/lib/input-model.d.ts +14 -0
- package/lib/input-model.js +12 -4
- package/lib/model.d.ts +8 -0
- package/lib/model.js +6 -0
- package/lib/types.d.ts +4 -0
- package/lib/widgets/chat-widget.d.ts +4 -0
- package/lib/widgets/chat-widget.js +36 -11
- package/lib/widgets/multichat-panel.js +2 -1
- package/package.json +1 -1
- package/src/components/attachments.tsx +70 -33
- package/src/components/chat.tsx +13 -4
- package/src/components/code-blocks/code-toolbar.tsx +56 -28
- package/src/components/code-blocks/copy-button.tsx +21 -12
- package/src/components/index.ts +0 -1
- package/src/components/input/buttons/attach-button.tsx +8 -2
- package/src/components/input/buttons/cancel-button.tsx +20 -15
- package/src/components/input/buttons/index.ts +2 -0
- package/src/components/input/buttons/save-edit-button.tsx +75 -0
- package/src/components/input/buttons/send-button.tsx +50 -167
- package/src/components/input/buttons/stop-button.tsx +88 -0
- package/src/components/input/chat-input.tsx +188 -83
- package/src/components/input/index.ts +1 -0
- package/src/components/input/toolbar-registry.tsx +25 -1
- package/src/components/input/use-chat-commands.tsx +25 -5
- package/src/components/input/writing-indicator.tsx +83 -0
- package/src/components/messages/header.tsx +8 -0
- package/src/components/messages/index.ts +0 -1
- package/src/components/messages/message.tsx +1 -0
- package/src/components/messages/messages.tsx +63 -39
- package/src/components/messages/toolbar.tsx +51 -21
- package/src/input-model.ts +21 -0
- package/src/model.ts +12 -0
- package/src/types.ts +5 -0
- package/src/widgets/chat-widget.tsx +43 -12
- package/src/widgets/multichat-panel.tsx +2 -1
- package/style/chat.css +13 -141
- package/style/input.css +0 -58
- package/lib/components/messages/writers.d.ts +0 -16
- package/lib/components/messages/writers.js +0 -39
- package/src/components/messages/writers.tsx +0 -81
|
@@ -3,6 +3,8 @@ import { SxProps, Theme } from '@mui/material';
|
|
|
3
3
|
import { IInputToolbarRegistry } from '.';
|
|
4
4
|
import { IInputModel } from '../../input-model';
|
|
5
5
|
import { IChatCommandRegistry } from '../../registers';
|
|
6
|
+
import { ChatArea } from '../../types';
|
|
7
|
+
import { IChatModel } from '../../model';
|
|
6
8
|
export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
|
|
7
9
|
/**
|
|
8
10
|
* The chat input namespace.
|
|
@@ -32,5 +34,18 @@ export declare namespace ChatInput {
|
|
|
32
34
|
* Chat command registry.
|
|
33
35
|
*/
|
|
34
36
|
chatCommandRegistry?: IChatCommandRegistry;
|
|
37
|
+
/**
|
|
38
|
+
* The area where the chat is displayed.
|
|
39
|
+
*/
|
|
40
|
+
area?: ChatArea;
|
|
41
|
+
/**
|
|
42
|
+
* The chat model.
|
|
43
|
+
*/
|
|
44
|
+
chatModel?: IChatModel;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the input is in edit mode (editing an existing message).
|
|
47
|
+
* Defaults to false (new message mode).
|
|
48
|
+
*/
|
|
49
|
+
edit?: boolean;
|
|
35
50
|
}
|
|
36
51
|
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import { Autocomplete, Box, TextField
|
|
5
|
+
import { Autocomplete, Box, 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
9
|
import { useChatCommands } from '.';
|
|
10
|
+
import { InputWritingIndicator } from './writing-indicator';
|
|
10
11
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
11
12
|
const INPUT_TEXTFIELD_CLASS = 'jp-chat-input-textfield';
|
|
12
|
-
const INPUT_COMPONENT_CLASS = 'jp-chat-input-component';
|
|
13
13
|
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
14
14
|
export function ChatInput(props) {
|
|
15
15
|
var _a;
|
|
@@ -20,6 +20,16 @@ export function ChatInput(props) {
|
|
|
20
20
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
21
21
|
const [attachments, setAttachments] = useState(model.attachments);
|
|
22
22
|
const [toolbarElements, setToolbarElements] = useState([]);
|
|
23
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
24
|
+
const [writers, setWriters] = useState([]);
|
|
25
|
+
/**
|
|
26
|
+
* Auto-focus the input when the component is first mounted.
|
|
27
|
+
*/
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (inputRef.current) {
|
|
30
|
+
inputRef.current.focus();
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
23
33
|
/**
|
|
24
34
|
* Handle the changes on the model that affect the input.
|
|
25
35
|
* - focus requested
|
|
@@ -67,6 +77,27 @@ export function ChatInput(props) {
|
|
|
67
77
|
toolbarRegistry.itemsChanged.disconnect(updateToolbar);
|
|
68
78
|
};
|
|
69
79
|
}, [toolbarRegistry]);
|
|
80
|
+
/**
|
|
81
|
+
* Handle the changes in the writers list.
|
|
82
|
+
*/
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
var _a;
|
|
85
|
+
if (!props.chatModel) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const updateWriters = (_, writers) => {
|
|
89
|
+
// Show all writers for now - AI generating responses will have messageID
|
|
90
|
+
setWriters(writers);
|
|
91
|
+
};
|
|
92
|
+
// Set initial writers state
|
|
93
|
+
const initialWriters = props.chatModel.writers;
|
|
94
|
+
setWriters(initialWriters);
|
|
95
|
+
(_a = props.chatModel.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(updateWriters);
|
|
96
|
+
return () => {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
(_b = (_a = props.chatModel) === null || _a === void 0 ? void 0 : _a.writersChanged) === null || _b === void 0 ? void 0 : _b.disconnect(updateWriters);
|
|
99
|
+
};
|
|
100
|
+
}, [props.chatModel]);
|
|
70
101
|
const inputExists = !!input.trim();
|
|
71
102
|
/**
|
|
72
103
|
* `handleKeyDown()`: callback invoked when the user presses any key in the
|
|
@@ -91,13 +122,14 @@ export function ChatInput(props) {
|
|
|
91
122
|
}
|
|
92
123
|
/**
|
|
93
124
|
* IMPORTANT: This statement ensures that when the chat commands menu is
|
|
94
|
-
* open
|
|
125
|
+
* open, the "Enter" key should select the command (handled by Autocomplete)
|
|
95
126
|
* instead of sending the message.
|
|
96
127
|
*
|
|
97
128
|
* This is done by returning early and letting the event propagate to the
|
|
98
|
-
* `Autocomplete` component
|
|
129
|
+
* `Autocomplete` component, which will select the auto-highlighted option
|
|
130
|
+
* thanks to autoSelect: true.
|
|
99
131
|
*/
|
|
100
|
-
if (chatCommands.menu.
|
|
132
|
+
if (chatCommands.menu.open) {
|
|
101
133
|
return;
|
|
102
134
|
}
|
|
103
135
|
// remainder of this function only handles the "Enter" key pressed while the
|
|
@@ -125,50 +157,81 @@ export function ChatInput(props) {
|
|
|
125
157
|
event.preventDefault();
|
|
126
158
|
}
|
|
127
159
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'& .MuiAutocomplete-option': {
|
|
156
|
-
padding: 2
|
|
160
|
+
const horizontalPadding = props.area === 'sidebar' ? 1.5 : 2;
|
|
161
|
+
return (React.createElement(Box, { sx: props.sx, className: clsx(INPUT_BOX_CLASS), "data-input-id": model.id },
|
|
162
|
+
React.createElement(Box, { sx: {
|
|
163
|
+
border: '1px solid',
|
|
164
|
+
borderColor: isFocused
|
|
165
|
+
? 'var(--jp-brand-color1)'
|
|
166
|
+
: 'var(--jp-border-color1)',
|
|
167
|
+
borderRadius: 2,
|
|
168
|
+
transition: 'border-color 0.2s ease',
|
|
169
|
+
display: 'flex',
|
|
170
|
+
flexDirection: 'column',
|
|
171
|
+
overflow: 'hidden'
|
|
172
|
+
} },
|
|
173
|
+
attachments.length > 0 && (React.createElement(Box, { sx: {
|
|
174
|
+
px: horizontalPadding,
|
|
175
|
+
pt: 1,
|
|
176
|
+
pb: 1
|
|
177
|
+
} },
|
|
178
|
+
React.createElement(AttachmentPreviewList, { attachments: attachments, onRemove: model.removeAttachment }))),
|
|
179
|
+
React.createElement(Autocomplete, { ...chatCommands.autocompleteProps, slotProps: {
|
|
180
|
+
...(chatCommands.autocompleteProps.slotProps || {}),
|
|
181
|
+
popper: {
|
|
182
|
+
placement: 'top-start'
|
|
183
|
+
},
|
|
184
|
+
listbox: {
|
|
185
|
+
sx: {
|
|
186
|
+
padding: 0
|
|
157
187
|
}
|
|
158
188
|
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, onKeyDown: handleKeyDown, placeholder: "Type a chat message, @ to mention...", inputRef: inputRef, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), 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); }, sx: {
|
|
190
|
+
padding: 1.5,
|
|
191
|
+
margin: 0,
|
|
192
|
+
backgroundColor: 'var(--jp-layout-color0)',
|
|
193
|
+
transition: 'background-color 0.2s ease',
|
|
194
|
+
'& .MuiInputBase-root': {
|
|
195
|
+
padding: 0,
|
|
196
|
+
margin: 0,
|
|
197
|
+
'&:before': {
|
|
198
|
+
display: 'none'
|
|
199
|
+
},
|
|
200
|
+
'&:after': {
|
|
201
|
+
display: 'none'
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
'& .MuiInputBase-input': {
|
|
205
|
+
overflowWrap: 'break-word',
|
|
206
|
+
wordBreak: 'break-word'
|
|
207
|
+
}
|
|
208
|
+
}, InputProps: {
|
|
162
209
|
...params.InputProps,
|
|
163
|
-
|
|
210
|
+
disableUnderline: true
|
|
211
|
+
}, FormHelperTextProps: {
|
|
212
|
+
sx: { display: 'none' }
|
|
213
|
+
} })), inputValue: input, onInputChange: (_, newValue, reason) => {
|
|
214
|
+
// Skip value updates when an autocomplete option is selected.
|
|
215
|
+
// The 'onChange' callback handles the replacement via replaceCurrentWord.
|
|
216
|
+
// 'selectOption' - user selected an option (newValue is just the option label)
|
|
217
|
+
// 'reset' - autocomplete is resetting after selection
|
|
218
|
+
// 'blur' - when user blurs the input (newValue is set to empty string)
|
|
219
|
+
if (reason === 'selectOption' ||
|
|
220
|
+
reason === 'reset' ||
|
|
221
|
+
reason === 'blur') {
|
|
222
|
+
return;
|
|
164
223
|
}
|
|
165
|
-
}, label: input.length > 2 ? helperText : ' ' })), inputValue: input, onInputChange: (_, newValue, reason) => {
|
|
166
|
-
// Do not update the value if the reason is 'reset', which should occur only
|
|
167
|
-
// if an autocompletion command has been selected. In this case, the value is
|
|
168
|
-
// set in the 'onChange()' callback of the autocompletion (to avoid conflicts).
|
|
169
|
-
if (reason !== 'reset') {
|
|
170
224
|
model.value = newValue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
} }),
|
|
226
|
+
React.createElement(Box, { className: INPUT_TOOLBAR_CLASS, sx: {
|
|
227
|
+
display: 'flex',
|
|
228
|
+
justifyContent: 'flex-end',
|
|
229
|
+
gap: 2,
|
|
230
|
+
padding: 1.5,
|
|
231
|
+
borderTop: '1px solid',
|
|
232
|
+
borderColor: 'var(--jp-border-color1)',
|
|
233
|
+
backgroundColor: 'var(--jp-layout-color0)',
|
|
234
|
+
transition: 'background-color 0.2s ease'
|
|
235
|
+
} }, toolbarElements.map((item, index) => (React.createElement(item.element, { key: index, model: model, chatCommandRegistry: props.chatCommandRegistry, chatModel: props.chatModel, edit: props.edit }))))),
|
|
236
|
+
React.createElement(InputWritingIndicator, { writers: writers })));
|
|
174
237
|
}
|
|
@@ -3,6 +3,7 @@ import { ISignal } from '@lumino/signaling';
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { IInputModel } from '../../input-model';
|
|
5
5
|
import { IChatCommandRegistry } from '../../registers';
|
|
6
|
+
import { IChatModel } from '../../model';
|
|
6
7
|
/**
|
|
7
8
|
* The toolbar registry interface.
|
|
8
9
|
*/
|
|
@@ -97,6 +98,15 @@ export declare namespace InputToolbarRegistry {
|
|
|
97
98
|
* `onSubmit()` on all command providers before sending the message.
|
|
98
99
|
*/
|
|
99
100
|
chatCommandRegistry?: IChatCommandRegistry;
|
|
101
|
+
/**
|
|
102
|
+
* The chat model. Provides access to messages, writers, and other chat state.
|
|
103
|
+
*/
|
|
104
|
+
chatModel?: IChatModel;
|
|
105
|
+
/**
|
|
106
|
+
* Whether the input is in edit mode (editing an existing message).
|
|
107
|
+
* Defaults to false (new message mode).
|
|
108
|
+
*/
|
|
109
|
+
edit?: boolean;
|
|
100
110
|
}
|
|
101
111
|
/**
|
|
102
112
|
* The default toolbar registry if none is provided.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Token } from '@lumino/coreutils';
|
|
6
6
|
import { Signal } from '@lumino/signaling';
|
|
7
|
-
import { AttachButton, CancelButton, SendButton } from './buttons';
|
|
7
|
+
import { AttachButton, CancelButton, SaveEditButton, SendButton } from './buttons';
|
|
8
8
|
/**
|
|
9
9
|
* The toolbar registry implementation.
|
|
10
10
|
*/
|
|
@@ -72,10 +72,19 @@ export class InputToolbarRegistry {
|
|
|
72
72
|
*/
|
|
73
73
|
function defaultToolbarRegistry() {
|
|
74
74
|
const registry = new InputToolbarRegistry();
|
|
75
|
+
// TODO: Re-enable stop button once logic is fully implemented
|
|
76
|
+
// registry.addItem('stop', {
|
|
77
|
+
// element: StopButton,
|
|
78
|
+
// position: 90
|
|
79
|
+
// });
|
|
75
80
|
registry.addItem('send', {
|
|
76
81
|
element: SendButton,
|
|
77
82
|
position: 100
|
|
78
83
|
});
|
|
84
|
+
registry.addItem('saveEdit', {
|
|
85
|
+
element: SaveEditButton,
|
|
86
|
+
position: 95
|
|
87
|
+
});
|
|
79
88
|
registry.addItem('attach', {
|
|
80
89
|
element: AttachButton,
|
|
81
90
|
position: 20
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
6
|
-
import { Box } from '@mui/material';
|
|
6
|
+
import { Box, Typography } from '@mui/material';
|
|
7
7
|
import React, { useEffect, useState } from 'react';
|
|
8
8
|
/**
|
|
9
9
|
* A hook which automatically returns the list of command options given the
|
|
@@ -103,18 +103,23 @@ export function useChatCommands(inputModel, chatCommandRegistry) {
|
|
|
103
103
|
renderOption: (defaultProps, command, __, ___) => {
|
|
104
104
|
const { key, ...listItemProps } = defaultProps;
|
|
105
105
|
const commandIcon = React.isValidElement(command.icon) ? (command.icon) : (React.createElement("span", null, command.icon instanceof LabIcon ? (React.createElement(command.icon.react, null)) : (command.icon)));
|
|
106
|
-
return (React.createElement(Box, { key: key, component: "li", ...listItemProps
|
|
106
|
+
return (React.createElement(Box, { key: key, component: "li", ...listItemProps, sx: {
|
|
107
|
+
...(listItemProps.sx || {}),
|
|
108
|
+
padding: '4px 8px !important',
|
|
109
|
+
gap: 2
|
|
110
|
+
} },
|
|
107
111
|
commandIcon,
|
|
108
|
-
React.createElement("
|
|
112
|
+
React.createElement(Typography, { variant: "body2", component: "span", className: "jp-chat-command-name" }, command.name),
|
|
109
113
|
command.description && (React.createElement(React.Fragment, null,
|
|
110
114
|
React.createElement("span", null, " - "),
|
|
111
|
-
React.createElement("
|
|
115
|
+
React.createElement(Typography, { variant: "caption", component: "span", color: "text.secondary" }, command.description)))));
|
|
112
116
|
},
|
|
113
117
|
// always show all options, since command providers should exclusively
|
|
114
118
|
// define what commands are added to the menu.
|
|
115
119
|
filterOptions: (commands) => commands,
|
|
116
120
|
value: null,
|
|
117
121
|
autoHighlight: true,
|
|
122
|
+
autoSelect: true,
|
|
118
123
|
freeSolo: true,
|
|
119
124
|
disableClearable: true,
|
|
120
125
|
onChange,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IChatModel } from '../../model';
|
|
3
|
+
/**
|
|
4
|
+
* The input writing indicator component props.
|
|
5
|
+
*/
|
|
6
|
+
export interface IInputWritingIndicatorProps {
|
|
7
|
+
/**
|
|
8
|
+
* The list of users currently writing.
|
|
9
|
+
*/
|
|
10
|
+
writers: IChatModel.IWriter[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The input writing indicator component, displaying typing status in the chat input area.
|
|
14
|
+
*/
|
|
15
|
+
export declare function InputWritingIndicator(props: IInputWritingIndicatorProps): JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Typography } from '@mui/material';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
/**
|
|
8
|
+
* Classname on the root element. Used in E2E tests.
|
|
9
|
+
*/
|
|
10
|
+
const WRITERS_ELEMENT_CLASSNAME = 'jp-chat-writers';
|
|
11
|
+
/**
|
|
12
|
+
* Format the writers list into a readable string.
|
|
13
|
+
* Examples: "Alice is typing...", "Alice and Bob are typing...", "Alice, Bob, and Carol are typing..."
|
|
14
|
+
*/
|
|
15
|
+
function formatWritersText(writers) {
|
|
16
|
+
if (writers.length === 0) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const names = writers.map(w => { var _a, _b, _c; return (_c = (_b = (_a = w.user.display_name) !== null && _a !== void 0 ? _a : w.user.name) !== null && _b !== void 0 ? _b : w.user.username) !== null && _c !== void 0 ? _c : 'Unknown'; });
|
|
20
|
+
if (names.length === 1) {
|
|
21
|
+
return `${names[0]} is typing...`;
|
|
22
|
+
}
|
|
23
|
+
else if (names.length === 2) {
|
|
24
|
+
return `${names[0]} and ${names[1]} are typing...`;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const allButLast = names.slice(0, -1).join(', ');
|
|
28
|
+
const last = names[names.length - 1];
|
|
29
|
+
return `${allButLast}, and ${last} are typing...`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The input writing indicator component, displaying typing status in the chat input area.
|
|
34
|
+
*/
|
|
35
|
+
export function InputWritingIndicator(props) {
|
|
36
|
+
const { writers } = props;
|
|
37
|
+
// Always render the container to reserve space, even if no writers
|
|
38
|
+
const writersText = writers.length > 0 ? formatWritersText(writers) : '';
|
|
39
|
+
return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME, sx: {
|
|
40
|
+
minHeight: '16px'
|
|
41
|
+
} },
|
|
42
|
+
React.createElement(Typography, { variant: "caption", sx: {
|
|
43
|
+
color: 'var(--jp-ui-font-color2)',
|
|
44
|
+
display: 'block',
|
|
45
|
+
fontSize: '10px',
|
|
46
|
+
fontFamily: 'var(--jp-ui-font-family)',
|
|
47
|
+
lineHeight: '16px',
|
|
48
|
+
visibility: writers.length > 0 ? 'visible' : 'hidden'
|
|
49
|
+
} }, writersText || '\u00A0')));
|
|
50
|
+
}
|
|
@@ -12,6 +12,10 @@ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
|
12
12
|
*/
|
|
13
13
|
export function ChatMessageHeader(props) {
|
|
14
14
|
var _a, _b;
|
|
15
|
+
// Don't render header for stacked messages or current user messages
|
|
16
|
+
if (props.message.stacked || props.isCurrentUser) {
|
|
17
|
+
return React.createElement(React.Fragment, null);
|
|
18
|
+
}
|
|
15
19
|
const [datetime, setDatetime] = useState({});
|
|
16
20
|
const message = props.message;
|
|
17
21
|
const sender = message.sender;
|
|
@@ -94,7 +94,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
94
94
|
};
|
|
95
95
|
// Empty if the message has been deleted.
|
|
96
96
|
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
97
|
-
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry })) : (React.createElement(MessageRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
97
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry, edit: true })) : (React.createElement(MessageRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
98
98
|
message.attachments && !edit && (
|
|
99
99
|
// Display the attachments only if message is not edited, otherwise the
|
|
100
100
|
// input component display them.
|
|
@@ -3,6 +3,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
3
3
|
import { IInputToolbarRegistry } from '../input';
|
|
4
4
|
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
|
|
5
5
|
import { IChatModel } from '../../model';
|
|
6
|
+
import { ChatArea } from '../../types';
|
|
6
7
|
export declare const MESSAGE_CLASS = "jp-chat-message";
|
|
7
8
|
/**
|
|
8
9
|
* The base components props.
|
|
@@ -32,6 +33,10 @@ export type BaseMessageProps = {
|
|
|
32
33
|
* The welcome message.
|
|
33
34
|
*/
|
|
34
35
|
welcomeMessage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The area where the chat is displayed.
|
|
38
|
+
*/
|
|
39
|
+
area?: ChatArea;
|
|
35
40
|
};
|
|
36
41
|
/**
|
|
37
42
|
* The messages list component.
|
|
@@ -11,7 +11,6 @@ import { ChatMessageHeader } from './header';
|
|
|
11
11
|
import { ChatMessage } from './message';
|
|
12
12
|
import { Navigation } from './navigation';
|
|
13
13
|
import { WelcomeMessage } from './welcome';
|
|
14
|
-
import { WritingUsersList } from './writers';
|
|
15
14
|
import { ScrollContainer } from '../scroll-container';
|
|
16
15
|
export const MESSAGE_CLASS = 'jp-chat-message';
|
|
17
16
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
@@ -23,7 +22,6 @@ export function ChatMessages(props) {
|
|
|
23
22
|
const { model } = props;
|
|
24
23
|
const [messages, setMessages] = useState(model.messages);
|
|
25
24
|
const refMsgBox = useRef(null);
|
|
26
|
-
const [currentWriters, setCurrentWriters] = useState([]);
|
|
27
25
|
const [allRendered, setAllRendered] = useState(false);
|
|
28
26
|
// The list of message DOM and their rendered promises.
|
|
29
27
|
const listRef = useRef([]);
|
|
@@ -42,25 +40,17 @@ export function ChatMessages(props) {
|
|
|
42
40
|
.catch(e => console.error(e));
|
|
43
41
|
}
|
|
44
42
|
fetchHistory();
|
|
45
|
-
setCurrentWriters([]);
|
|
46
43
|
}, [model]);
|
|
47
44
|
/**
|
|
48
45
|
* Effect: listen to chat messages.
|
|
49
46
|
*/
|
|
50
47
|
useEffect(() => {
|
|
51
|
-
var _a;
|
|
52
48
|
function handleChatEvents() {
|
|
53
49
|
setMessages([...model.messages]);
|
|
54
50
|
}
|
|
55
|
-
function handleWritersChange(_, writers) {
|
|
56
|
-
setCurrentWriters(writers.map(writer => writer.user));
|
|
57
|
-
}
|
|
58
51
|
model.messagesUpdated.connect(handleChatEvents);
|
|
59
|
-
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(handleWritersChange);
|
|
60
52
|
return function cleanup() {
|
|
61
|
-
var _a;
|
|
62
53
|
model.messagesUpdated.disconnect(handleChatEvents);
|
|
63
|
-
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(handleChatEvents);
|
|
64
54
|
};
|
|
65
55
|
}, [model]);
|
|
66
56
|
/**
|
|
@@ -122,18 +112,38 @@ export function ChatMessages(props) {
|
|
|
122
112
|
});
|
|
123
113
|
};
|
|
124
114
|
}, [messages, allRendered]);
|
|
115
|
+
const horizontalPadding = props.area === 'main' ? 8 : 4;
|
|
125
116
|
return (React.createElement(React.Fragment, null,
|
|
126
117
|
React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
|
|
127
118
|
props.welcomeMessage && (React.createElement(WelcomeMessage, { rmRegistry: props.rmRegistry, content: props.welcomeMessage })),
|
|
128
|
-
React.createElement(Box, {
|
|
119
|
+
React.createElement(Box, { sx: {
|
|
120
|
+
paddingLeft: horizontalPadding,
|
|
121
|
+
paddingRight: horizontalPadding,
|
|
122
|
+
paddingTop: 4,
|
|
123
|
+
paddingBottom: 16,
|
|
124
|
+
display: 'flex',
|
|
125
|
+
flexDirection: 'column',
|
|
126
|
+
gap: 4
|
|
127
|
+
}, ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, messages
|
|
128
|
+
.filter(message => !message.deleted)
|
|
129
|
+
.map((message, i) => {
|
|
129
130
|
renderedPromise.current[i] = new PromiseDelegate();
|
|
131
|
+
const isCurrentUser = model.user !== undefined &&
|
|
132
|
+
model.user.username === message.sender.username;
|
|
130
133
|
return (
|
|
131
134
|
// extra div needed to ensure each bubble is on a new line
|
|
132
|
-
React.createElement(Box, { key: i,
|
|
133
|
-
|
|
135
|
+
React.createElement(Box, { key: i, sx: {
|
|
136
|
+
...(isCurrentUser && {
|
|
137
|
+
marginLeft: props.area === 'main' ? '25%' : '10%',
|
|
138
|
+
backgroundColor: 'var(--jp-layout-color2)',
|
|
139
|
+
border: 'none',
|
|
140
|
+
borderRadius: 2,
|
|
141
|
+
padding: 2
|
|
142
|
+
})
|
|
143
|
+
}, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
144
|
+
React.createElement(ChatMessageHeader, { message: message, isCurrentUser: isCurrentUser }),
|
|
134
145
|
React.createElement(ChatMessage, { ...props, message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) }),
|
|
135
146
|
props.messageFooterRegistry && (React.createElement(MessageFooterComponent, { registry: props.messageFooterRegistry, message: message, model: model }))));
|
|
136
147
|
}))),
|
|
137
|
-
React.createElement(WritingUsersList, { writers: currentWriters }),
|
|
138
148
|
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox, allRendered: allRendered })));
|
|
139
149
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
// import EditIcon from '@mui/icons-material/Edit';
|
|
6
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
7
|
+
import { Box, IconButton, Tooltip } from '@mui/material';
|
|
6
8
|
import React from 'react';
|
|
7
9
|
const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
8
10
|
/**
|
|
@@ -10,21 +12,41 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
|
10
12
|
*/
|
|
11
13
|
export function MessageToolbar(props) {
|
|
12
14
|
const buttons = [];
|
|
13
|
-
if (props.edit !== undefined) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// if (props.edit !== undefined) {
|
|
16
|
+
// const editButton = (
|
|
17
|
+
// <Tooltip key="edit" title="Edit" placement="top" arrow>
|
|
18
|
+
// <span>
|
|
19
|
+
// <IconButton
|
|
20
|
+
// onClick={props.edit}
|
|
21
|
+
// aria-label="Edit"
|
|
22
|
+
// sx={{
|
|
23
|
+
// width: '24px',
|
|
24
|
+
// height: '24px',
|
|
25
|
+
// padding: 0,
|
|
26
|
+
// lineHeight: 0
|
|
27
|
+
// }}
|
|
28
|
+
// >
|
|
29
|
+
// <EditIcon sx={{ fontSize: '16px' }} />
|
|
30
|
+
// </IconButton>
|
|
31
|
+
// </span>
|
|
32
|
+
// </Tooltip>
|
|
33
|
+
// );
|
|
34
|
+
// buttons.push(editButton);
|
|
35
|
+
// }
|
|
21
36
|
if (props.delete !== undefined) {
|
|
22
|
-
const deleteButton =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
const deleteButton = (React.createElement(Tooltip, { key: "delete", title: "Delete", placement: "top", arrow: true },
|
|
38
|
+
React.createElement("span", null,
|
|
39
|
+
React.createElement(IconButton, { onClick: props.delete, "aria-label": "Delete", sx: {
|
|
40
|
+
width: '24px',
|
|
41
|
+
height: '24px',
|
|
42
|
+
padding: 0,
|
|
43
|
+
lineHeight: 0
|
|
44
|
+
} },
|
|
45
|
+
React.createElement(DeleteIcon, { sx: { fontSize: '16px' } })))));
|
|
27
46
|
buttons.push(deleteButton);
|
|
28
47
|
}
|
|
29
|
-
return (React.createElement(
|
|
48
|
+
return (React.createElement(Box, { className: TOOLBAR_CLASS, sx: {
|
|
49
|
+
display: 'flex',
|
|
50
|
+
gap: 2
|
|
51
|
+
} }, buttons));
|
|
30
52
|
}
|