@jupyter/chat 0.21.0 → 0.22.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/__tests__/chat-body-placeholder.spec.d.ts +1 -0
- package/lib/__tests__/chat-body-placeholder.spec.js +54 -0
- package/lib/__tests__/multichat-panel.spec.d.ts +1 -0
- package/lib/__tests__/multichat-panel.spec.js +116 -0
- package/lib/components/chat.d.ts +6 -0
- package/lib/components/input/buttons/send-button.d.ts +1 -1
- package/lib/components/input/buttons/send-button.js +108 -8
- package/lib/components/input/chat-input.js +4 -3
- package/lib/components/messages/chat-body-placeholder.d.ts +6 -0
- package/lib/components/messages/chat-body-placeholder.js +19 -0
- package/lib/components/messages/index.d.ts +1 -0
- package/lib/components/messages/index.js +1 -0
- package/lib/components/messages/message-renderer.js +1 -1
- package/lib/components/messages/messages.js +54 -2
- package/lib/components/scroll-container.d.ts +6 -13
- package/lib/components/scroll-container.js +11 -22
- package/lib/input-model.d.ts +4 -0
- package/lib/theme-provider.js +2 -2
- package/lib/tokens.d.ts +50 -1
- package/lib/tokens.js +12 -0
- package/lib/types.d.ts +4 -0
- package/lib/widgets/chat-widget.js +5 -1
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +15 -0
- package/lib/widgets/multichat-panel.js +41 -9
- package/lib/widgets/placeholder.d.ts +48 -0
- package/lib/widgets/placeholder.js +48 -0
- package/package.json +3 -3
- package/src/__tests__/chat-body-placeholder.spec.ts +69 -0
- package/src/__tests__/multichat-panel.spec.ts +135 -0
- package/src/components/chat.tsx +6 -0
- package/src/components/input/buttons/send-button.tsx +158 -14
- package/src/components/input/chat-input.tsx +6 -5
- package/src/components/messages/chat-body-placeholder.tsx +24 -0
- package/src/components/messages/index.ts +1 -0
- package/src/components/messages/message-renderer.tsx +1 -1
- package/src/components/messages/messages.tsx +59 -2
- package/src/components/scroll-container.tsx +27 -34
- package/src/input-model.ts +4 -0
- package/src/theme-provider.ts +2 -2
- package/src/tokens.ts +61 -1
- package/src/types.ts +4 -0
- package/src/widgets/chat-widget.tsx +14 -1
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +56 -10
- package/src/widgets/placeholder.tsx +122 -0
- package/style/base.css +1 -0
- package/style/placeholder.css +29 -0
|
@@ -4,22 +4,29 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
7
|
-
import
|
|
7
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
8
|
+
import { Box, Menu, MenuItem, Typography } from '@mui/material';
|
|
9
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
8
10
|
|
|
9
11
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
12
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
13
|
import { useTranslator } from '../../../context';
|
|
14
|
+
import { includeSelectionIcon } from '../../../icons';
|
|
12
15
|
import { IInputModel, InputModel } from '../../../input-model';
|
|
13
16
|
|
|
14
17
|
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
18
|
+
const SEND_INCLUDE_OPENER_CLASS = 'jp-chat-send-include-opener';
|
|
19
|
+
const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
* The send button.
|
|
22
|
+
* The send button, with optional 'include selection' menu.
|
|
18
23
|
*/
|
|
19
24
|
export function SendButton(
|
|
20
25
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
21
26
|
): JSX.Element {
|
|
22
27
|
const { model, chatCommandRegistry, edit } = props;
|
|
28
|
+
const { activeCellManager, selectionWatcher } = model;
|
|
29
|
+
const supportSelection = !!activeCellManager || !!selectionWatcher;
|
|
23
30
|
const trans = useTranslator();
|
|
24
31
|
|
|
25
32
|
// Don't show this button when in edit mode
|
|
@@ -27,8 +34,24 @@ export function SendButton(
|
|
|
27
34
|
return <></>;
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
|
|
38
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
30
39
|
const [disabled, setDisabled] = useState(false);
|
|
31
40
|
const [tooltip, setTooltip] = useState<string>('');
|
|
41
|
+
const [selectionTooltip, setSelectionTooltip] = useState<string>('');
|
|
42
|
+
const [disableInclude, setDisableInclude] = useState<boolean>(true);
|
|
43
|
+
const [showSendWithSelection, setShowSendWithSelection] = useState<boolean>(
|
|
44
|
+
supportSelection && (model.config.sendWithSelection ?? true)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const openMenu = useCallback((el: HTMLElement | null) => {
|
|
48
|
+
setMenuAnchorEl(el);
|
|
49
|
+
setMenuOpen(true);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const closeMenu = useCallback(() => {
|
|
53
|
+
setMenuOpen(false);
|
|
54
|
+
}, []);
|
|
32
55
|
|
|
33
56
|
useEffect(() => {
|
|
34
57
|
const inputChanged = () => {
|
|
@@ -47,6 +70,9 @@ export function SendButton(
|
|
|
47
70
|
? trans.__('Send message (SHIFT+ENTER)')
|
|
48
71
|
: trans.__('Send message (ENTER)')
|
|
49
72
|
);
|
|
73
|
+
setShowSendWithSelection(
|
|
74
|
+
supportSelection && (config.sendWithSelection ?? true)
|
|
75
|
+
);
|
|
50
76
|
};
|
|
51
77
|
model.configChanged.connect(configChanged);
|
|
52
78
|
|
|
@@ -60,6 +86,30 @@ export function SendButton(
|
|
|
60
86
|
};
|
|
61
87
|
}, [model]);
|
|
62
88
|
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const toggleIncludeState = () => {
|
|
91
|
+
setDisableInclude(
|
|
92
|
+
!(selectionWatcher?.selection || activeCellManager?.available)
|
|
93
|
+
);
|
|
94
|
+
const tip = selectionWatcher?.selection
|
|
95
|
+
? trans.__('%1 line(s) selected', selectionWatcher.selection.numLines)
|
|
96
|
+
: activeCellManager?.available
|
|
97
|
+
? trans.__('Code from 1 active cell')
|
|
98
|
+
: trans.__('No selection or active cell');
|
|
99
|
+
setSelectionTooltip(tip);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (showSendWithSelection) {
|
|
103
|
+
selectionWatcher?.selectionChanged.connect(toggleIncludeState);
|
|
104
|
+
activeCellManager?.availabilityChanged.connect(toggleIncludeState);
|
|
105
|
+
toggleIncludeState();
|
|
106
|
+
}
|
|
107
|
+
return () => {
|
|
108
|
+
selectionWatcher?.selectionChanged.disconnect(toggleIncludeState);
|
|
109
|
+
activeCellManager?.availabilityChanged.disconnect(toggleIncludeState);
|
|
110
|
+
};
|
|
111
|
+
}, [activeCellManager, selectionWatcher, showSendWithSelection]);
|
|
112
|
+
|
|
63
113
|
async function send() {
|
|
64
114
|
// Run all command providers
|
|
65
115
|
await chatCommandRegistry?.onSubmit(model);
|
|
@@ -71,18 +121,112 @@ export function SendButton(
|
|
|
71
121
|
model.focus();
|
|
72
122
|
}
|
|
73
123
|
|
|
124
|
+
async function sendWithSelection() {
|
|
125
|
+
await chatCommandRegistry?.onSubmit(model);
|
|
126
|
+
|
|
127
|
+
let source = '';
|
|
128
|
+
let language: string | undefined;
|
|
129
|
+
|
|
130
|
+
if (selectionWatcher?.selection) {
|
|
131
|
+
source = selectionWatcher.selection.text;
|
|
132
|
+
language = selectionWatcher.selection.language;
|
|
133
|
+
} else if (activeCellManager?.available) {
|
|
134
|
+
const content = activeCellManager.getContent(false);
|
|
135
|
+
source = content!.source;
|
|
136
|
+
language = content?.language;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let body = model.value;
|
|
140
|
+
if (source) {
|
|
141
|
+
body += `\n\n\`\`\`${language ?? ''}\n${source}\n\`\`\`\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
model.value = '';
|
|
145
|
+
closeMenu();
|
|
146
|
+
model.send(body);
|
|
147
|
+
model.focus();
|
|
148
|
+
}
|
|
149
|
+
|
|
74
150
|
return (
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
151
|
+
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: '1px' }}>
|
|
152
|
+
<TooltippedIconButton
|
|
153
|
+
onClick={send}
|
|
154
|
+
tooltip={tooltip}
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
sx={{
|
|
157
|
+
borderRadius: showSendWithSelection
|
|
158
|
+
? 'var(--jp-border-radius) 0 0 var(--jp-border-radius) !important'
|
|
159
|
+
: 'var(--jp-border-radius)'
|
|
160
|
+
}}
|
|
161
|
+
buttonProps={{
|
|
162
|
+
title: tooltip,
|
|
163
|
+
className: SEND_BUTTON_CLASS
|
|
164
|
+
}}
|
|
165
|
+
aria-label={tooltip}
|
|
166
|
+
>
|
|
167
|
+
<ArrowUpwardIcon />
|
|
168
|
+
</TooltippedIconButton>
|
|
169
|
+
{showSendWithSelection && (
|
|
170
|
+
<>
|
|
171
|
+
<TooltippedIconButton
|
|
172
|
+
onClick={e => openMenu(e.currentTarget)}
|
|
173
|
+
tooltip={trans.__('Send with selection')}
|
|
174
|
+
disabled={disabled}
|
|
175
|
+
sx={{
|
|
176
|
+
borderRadius:
|
|
177
|
+
'0 var(--jp-border-radius) var(--jp-border-radius) 0 !important',
|
|
178
|
+
width: '16px',
|
|
179
|
+
minWidth: '16px'
|
|
180
|
+
}}
|
|
181
|
+
buttonProps={{
|
|
182
|
+
onKeyDown: e => {
|
|
183
|
+
if (e.key !== 'Enter' && e.key !== ' ') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
openMenu(e.currentTarget);
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
},
|
|
189
|
+
className: SEND_INCLUDE_OPENER_CLASS
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<KeyboardArrowDownIcon sx={{ fontSize: '12px' }} />
|
|
193
|
+
</TooltippedIconButton>
|
|
194
|
+
<Menu
|
|
195
|
+
open={menuOpen}
|
|
196
|
+
onClose={closeMenu}
|
|
197
|
+
anchorEl={menuAnchorEl}
|
|
198
|
+
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
199
|
+
transformOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
200
|
+
sx={{
|
|
201
|
+
'& .MuiMenuItem-root': {
|
|
202
|
+
display: 'flex',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
gap: '8px'
|
|
205
|
+
},
|
|
206
|
+
'& svg': { lineHeight: 0 }
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<MenuItem
|
|
210
|
+
onClick={e => {
|
|
211
|
+
sendWithSelection();
|
|
212
|
+
e.stopPropagation();
|
|
213
|
+
}}
|
|
214
|
+
disabled={disableInclude}
|
|
215
|
+
className={SEND_INCLUDE_LI_CLASS}
|
|
216
|
+
>
|
|
217
|
+
<includeSelectionIcon.react />
|
|
218
|
+
<Box>
|
|
219
|
+
<Typography display="block">
|
|
220
|
+
{trans.__('Send message with selection')}
|
|
221
|
+
</Typography>
|
|
222
|
+
<Typography display="block" sx={{ opacity: 0.618 }}>
|
|
223
|
+
{selectionTooltip}
|
|
224
|
+
</Typography>
|
|
225
|
+
</Box>
|
|
226
|
+
</MenuItem>
|
|
227
|
+
</Menu>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</Box>
|
|
87
231
|
);
|
|
88
232
|
}
|
|
@@ -156,18 +156,19 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
156
156
|
*/
|
|
157
157
|
event.stopPropagation();
|
|
158
158
|
|
|
159
|
+
const isSendCombination =
|
|
160
|
+
(sendWithShiftEnter && event.shiftKey) ||
|
|
161
|
+
(!sendWithShiftEnter && !event.shiftKey);
|
|
162
|
+
|
|
159
163
|
// Do not send empty messages, and avoid adding new line in empty message.
|
|
160
|
-
if (!inputExists) {
|
|
164
|
+
if (!inputExists && (!isSendCombination || attachments.length === 0)) {
|
|
161
165
|
event.stopPropagation();
|
|
162
166
|
event.preventDefault();
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
// Finally, send the message when all other conditions are met.
|
|
167
|
-
if (
|
|
168
|
-
(sendWithShiftEnter && event.shiftKey) ||
|
|
169
|
-
(!sendWithShiftEnter && !event.shiftKey)
|
|
170
|
-
) {
|
|
171
|
+
if (isSendCombination) {
|
|
171
172
|
// Run all command providers
|
|
172
173
|
await chatCommandRegistry?.onSubmit(model);
|
|
173
174
|
model.send(model.value);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useChatContext } from '../../context';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Component that renders chat body placeholder using the factory from context.
|
|
10
|
+
* Renders nothing if no factory is provided.
|
|
11
|
+
*/
|
|
12
|
+
export function ChatBodyPlaceholder(): JSX.Element | null {
|
|
13
|
+
const { model, chatBodyPlaceholderFactory } = useChatContext();
|
|
14
|
+
|
|
15
|
+
if (!chatBodyPlaceholderFactory) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const onSend = (body: string) => {
|
|
20
|
+
model.sendMessage({ body });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return chatBodyPlaceholderFactory.create({ onSend });
|
|
24
|
+
}
|
|
@@ -158,7 +158,7 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
renderContent();
|
|
161
|
-
}, [message.body, message.mentions, rmRegistry]);
|
|
161
|
+
}, [message.body, message.mime_model, message.mentions, rmRegistry]);
|
|
162
162
|
|
|
163
163
|
return (
|
|
164
164
|
<>
|
|
@@ -14,6 +14,7 @@ import { ChatMessage } from './message';
|
|
|
14
14
|
import { MessagePreambleComponent } from './preamble';
|
|
15
15
|
import { Navigation } from './navigation';
|
|
16
16
|
import { WelcomeMessage } from './welcome';
|
|
17
|
+
import { ChatBodyPlaceholder } from './chat-body-placeholder';
|
|
17
18
|
import { ScrollContainer } from '../scroll-container';
|
|
18
19
|
import { useChatContext } from '../../context';
|
|
19
20
|
import { Message } from '../../message';
|
|
@@ -68,11 +69,24 @@ export function ChatMessages(): JSX.Element {
|
|
|
68
69
|
fetchHistory();
|
|
69
70
|
}, [model]);
|
|
70
71
|
|
|
72
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
// Starts true so the chat scrolls to bottom on first render.
|
|
74
|
+
const shouldScrollRef = useRef<boolean>(true);
|
|
75
|
+
|
|
71
76
|
/**
|
|
72
|
-
* Effect: listen to chat messages.
|
|
77
|
+
* Effect: listen to chat messages and decide whether to auto-scroll.
|
|
78
|
+
*
|
|
79
|
+
* We check `prevLastIdx` (length - 2) because by the time the signal fires,
|
|
80
|
+
* model.messages already contains the new message. So length - 2 is the
|
|
81
|
+
* message that WAS last before the new one arrived. If that was visible,
|
|
82
|
+
* the user was at the bottom and we should follow.
|
|
73
83
|
*/
|
|
74
84
|
useEffect(() => {
|
|
75
85
|
function handleChatEvents() {
|
|
86
|
+
const viewport = model.messagesInViewport ?? [];
|
|
87
|
+
const prevLastIdx = model.messages.length - 2;
|
|
88
|
+
shouldScrollRef.current =
|
|
89
|
+
prevLastIdx < 0 || viewport.includes(prevLastIdx);
|
|
76
90
|
setMessages([...model.messages]);
|
|
77
91
|
}
|
|
78
92
|
model.messagesUpdated.connect(handleChatEvents);
|
|
@@ -82,6 +96,48 @@ export function ChatMessages(): JSX.Element {
|
|
|
82
96
|
};
|
|
83
97
|
}, [model]);
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Effect: observe DOM mutations in the scroll container to keep the viewport
|
|
101
|
+
* pinned to the bottom as message content renders asynchronously.
|
|
102
|
+
*
|
|
103
|
+
* A single scrollTop assignment on state change isn't enough because message
|
|
104
|
+
* content (markdown, code blocks, images) renders after the initial DOM
|
|
105
|
+
* commit. The MutationObserver fires on every subtree change, keeping us
|
|
106
|
+
* pinned as content streams in or expands.
|
|
107
|
+
*
|
|
108
|
+
* The scroll listener lets a user manually scroll to the bottom during
|
|
109
|
+
* streaming to re-engage auto-scroll, or scroll up to disengage it.
|
|
110
|
+
*/
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const el = scrollContainerRef.current;
|
|
113
|
+
if (!el) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const observer = new MutationObserver(() => {
|
|
118
|
+
if (shouldScrollRef.current) {
|
|
119
|
+
el.scrollTop = el.scrollHeight;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const handleScroll = () => {
|
|
124
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
|
125
|
+
shouldScrollRef.current = atBottom;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
observer.observe(el, {
|
|
129
|
+
childList: true,
|
|
130
|
+
subtree: true,
|
|
131
|
+
characterData: true
|
|
132
|
+
});
|
|
133
|
+
el.addEventListener('scroll', handleScroll);
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
observer.disconnect();
|
|
137
|
+
el.removeEventListener('scroll', handleScroll);
|
|
138
|
+
};
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
85
141
|
/**
|
|
86
142
|
* Effect: Listen to the config change.
|
|
87
143
|
*/
|
|
@@ -163,8 +219,9 @@ export function ChatMessages(): JSX.Element {
|
|
|
163
219
|
const horizontalPadding = area === 'main' ? 8 : 4;
|
|
164
220
|
return (
|
|
165
221
|
<>
|
|
166
|
-
<ScrollContainer sx={{ flexGrow: 1 }}>
|
|
222
|
+
<ScrollContainer ref={scrollContainerRef} sx={{ flexGrow: 1 }}>
|
|
167
223
|
{welcomeMessage && <WelcomeMessage content={welcomeMessage} />}
|
|
224
|
+
{messages.length === 0 && <ChatBodyPlaceholder />}
|
|
168
225
|
<Box
|
|
169
226
|
sx={{
|
|
170
227
|
paddingLeft: horizontalPadding,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useMemo } from 'react';
|
|
6
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
7
7
|
import { Box, SxProps, Theme } from '@mui/material';
|
|
8
8
|
|
|
9
9
|
type ScrollContainerProps = {
|
|
@@ -12,39 +12,32 @@ type ScrollContainerProps = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Scrollable container for chat messages. Scroll position is managed
|
|
16
|
+
* explicitly by the parent (ChatMessages) via a forwarded ref.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - If viewport is in the middle of the overflow container, appending new
|
|
21
|
-
* children leaves the viewport unaffected.
|
|
22
|
-
*
|
|
23
|
-
* Currently only works for Chrome and Firefox due to reliance on
|
|
24
|
-
* `overflow-anchor`.
|
|
25
|
-
*
|
|
26
|
-
* **References**
|
|
27
|
-
* - https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
|
|
18
|
+
* `overflowAnchor: 'none'` disables the browser's built-in scroll anchoring
|
|
19
|
+
* which caused partial viewport shifts on new content in Chrome/Firefox
|
|
20
|
+
* and is unsupported in Safari.
|
|
28
21
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
export const ScrollContainer = forwardRef<HTMLDivElement, ScrollContainerProps>(
|
|
23
|
+
function ScrollContainer(props, ref) {
|
|
24
|
+
const id = useMemo(
|
|
25
|
+
() => 'jupyter-chat-scroll-container-' + Date.now().toString(),
|
|
26
|
+
[]
|
|
27
|
+
);
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
overflowAnchor: 'none'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
29
|
+
return (
|
|
30
|
+
<Box
|
|
31
|
+
ref={ref}
|
|
32
|
+
id={id}
|
|
33
|
+
sx={{
|
|
34
|
+
overflowY: 'scroll',
|
|
35
|
+
overflowAnchor: 'none',
|
|
36
|
+
...props.sx
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{props.children}
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
);
|
package/src/input-model.ts
CHANGED
|
@@ -579,6 +579,10 @@ export namespace InputModel {
|
|
|
579
579
|
* Whether to send a message via Shift-Enter instead of Enter.
|
|
580
580
|
*/
|
|
581
581
|
sendWithShiftEnter?: boolean;
|
|
582
|
+
/**
|
|
583
|
+
* Whether to display the 'send with selection' button.
|
|
584
|
+
*/
|
|
585
|
+
sendWithSelection?: boolean;
|
|
582
586
|
}
|
|
583
587
|
}
|
|
584
588
|
|
package/src/theme-provider.ts
CHANGED
|
@@ -68,7 +68,7 @@ export async function getJupyterLabTheme(): Promise<Theme> {
|
|
|
68
68
|
style: {
|
|
69
69
|
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
70
70
|
color: 'var(--jp-ui-inverse-font-color1)',
|
|
71
|
-
borderRadius: '
|
|
71
|
+
borderRadius: 'var(--jp-border-radius)',
|
|
72
72
|
boxShadow: 'none',
|
|
73
73
|
'&:hover': {
|
|
74
74
|
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
@@ -125,7 +125,7 @@ export async function getJupyterLabTheme(): Promise<Theme> {
|
|
|
125
125
|
style: {
|
|
126
126
|
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
127
127
|
color: 'var(--jp-ui-inverse-font-color1)',
|
|
128
|
-
borderRadius: '
|
|
128
|
+
borderRadius: 'var(--jp-border-radius)',
|
|
129
129
|
boxShadow: 'none',
|
|
130
130
|
'&:hover': {
|
|
131
131
|
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
package/src/tokens.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
|
|
7
7
|
import { Token } from '@lumino/coreutils';
|
|
8
|
+
import { Widget } from '@lumino/widgets';
|
|
8
9
|
|
|
9
|
-
import { ChatWidget } from './widgets';
|
|
10
|
+
import { ChatWidget, Placeholder } from './widgets';
|
|
10
11
|
import { IChatModel } from './model';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -28,3 +29,62 @@ export const IChatTracker = new Token<IChatTracker>(
|
|
|
28
29
|
'@jupyter/chat:IChatTracker',
|
|
29
30
|
'The chat widget tracker'
|
|
30
31
|
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The interface for the placeholder factory.
|
|
35
|
+
*/
|
|
36
|
+
export interface IChatPlaceholderFactory {
|
|
37
|
+
/**
|
|
38
|
+
* Create a placeholder widget for the multi-chat panel.
|
|
39
|
+
*
|
|
40
|
+
* @param props - the props passed to the placeholder.
|
|
41
|
+
* @returns a widget to display as placeholder.
|
|
42
|
+
*/
|
|
43
|
+
create(props: Placeholder.IProps): Widget;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The token for the placeholder factory.
|
|
48
|
+
* Not provided by default — extensions can provide it to replace the default
|
|
49
|
+
* placeholder shown when no chat is open in the multi-chat panel.
|
|
50
|
+
*/
|
|
51
|
+
export const IChatPlaceholderFactory = new Token<IChatPlaceholderFactory>(
|
|
52
|
+
'@jupyter/chat:IChatPlaceholderFactory',
|
|
53
|
+
'The placeholder factory for the multi-chat panel'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The interface for the chat body placeholder factory.
|
|
58
|
+
*/
|
|
59
|
+
export interface IChatBodyPlaceholderFactory {
|
|
60
|
+
/**
|
|
61
|
+
* Create a chat body placeholder component shown when the chat has no messages.
|
|
62
|
+
*
|
|
63
|
+
* @param props - the props passed to the component.
|
|
64
|
+
* @returns a React element to display as chat body placeholder.
|
|
65
|
+
*/
|
|
66
|
+
create(props: IChatBodyPlaceholderFactory.IProps): JSX.Element | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export namespace IChatBodyPlaceholderFactory {
|
|
70
|
+
/**
|
|
71
|
+
* The props passed to the chat body placeholder factory.
|
|
72
|
+
*/
|
|
73
|
+
export interface IProps {
|
|
74
|
+
/**
|
|
75
|
+
* Callback to send a message.
|
|
76
|
+
*/
|
|
77
|
+
onSend: (body: string) => void;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The token for the chat body placeholder factory.
|
|
83
|
+
* Not provided by default — extensions can provide it to display clickable
|
|
84
|
+
* chat body placeholder when a chat has no messages.
|
|
85
|
+
*/
|
|
86
|
+
export const IChatBodyPlaceholderFactory =
|
|
87
|
+
new Token<IChatBodyPlaceholderFactory>(
|
|
88
|
+
'@jupyter/chat:IChatBodyPlaceholderFactory',
|
|
89
|
+
'The chat body placeholder factory for empty chats'
|
|
90
|
+
);
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { ReactWidget } from '@jupyterlab/apputils';
|
|
6
|
+
import { Notification, ReactWidget } from '@jupyterlab/apputils';
|
|
7
7
|
import { Cell } from '@jupyterlab/cells';
|
|
8
8
|
import { DirListing } from '@jupyterlab/filebrowser';
|
|
9
9
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
@@ -295,6 +295,9 @@ export class ChatWidget extends ReactWidget {
|
|
|
295
295
|
// Get path from first cell as all cells come from same notebook as users can only select or drag cells from one notebook at a time
|
|
296
296
|
if (!cells[0]?.id) {
|
|
297
297
|
console.warn('No valid cells to process');
|
|
298
|
+
Notification.error(
|
|
299
|
+
'Failed to attach notebook cells: no valid cell data found.'
|
|
300
|
+
);
|
|
298
301
|
return false;
|
|
299
302
|
}
|
|
300
303
|
|
|
@@ -303,6 +306,9 @@ export class ChatWidget extends ReactWidget {
|
|
|
303
306
|
console.warn(
|
|
304
307
|
`Cannot find notebook for dragged cells from ${cells[0].id}`
|
|
305
308
|
);
|
|
309
|
+
Notification.error(
|
|
310
|
+
'Failed to attach notebook cells: source notebook could not be located.'
|
|
311
|
+
);
|
|
306
312
|
return false;
|
|
307
313
|
}
|
|
308
314
|
|
|
@@ -346,8 +352,15 @@ export class ChatWidget extends ReactWidget {
|
|
|
346
352
|
inputModel?.addAttachment?.(attachment);
|
|
347
353
|
return !!inputModel;
|
|
348
354
|
}
|
|
355
|
+
|
|
356
|
+
Notification.error(
|
|
357
|
+
'Failed to attach notebook cells: no supported cells were found.'
|
|
358
|
+
);
|
|
349
359
|
} catch (error) {
|
|
350
360
|
console.error('Failed to process cell drop: ', error);
|
|
361
|
+
Notification.error(
|
|
362
|
+
'Failed to attach notebook cells due to an unexpected error.'
|
|
363
|
+
);
|
|
351
364
|
}
|
|
352
365
|
return false;
|
|
353
366
|
}
|
package/src/widgets/index.ts
CHANGED