@jupyter/chat 0.21.1 → 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.
Files changed (47) hide show
  1. package/lib/__tests__/chat-body-placeholder.spec.d.ts +1 -0
  2. package/lib/__tests__/chat-body-placeholder.spec.js +54 -0
  3. package/lib/__tests__/multichat-panel.spec.d.ts +1 -0
  4. package/lib/__tests__/multichat-panel.spec.js +116 -0
  5. package/lib/components/chat.d.ts +6 -0
  6. package/lib/components/input/buttons/send-button.d.ts +1 -1
  7. package/lib/components/input/buttons/send-button.js +108 -8
  8. package/lib/components/input/chat-input.js +4 -3
  9. package/lib/components/messages/chat-body-placeholder.d.ts +6 -0
  10. package/lib/components/messages/chat-body-placeholder.js +19 -0
  11. package/lib/components/messages/index.d.ts +1 -0
  12. package/lib/components/messages/index.js +1 -0
  13. package/lib/components/messages/messages.js +54 -2
  14. package/lib/components/scroll-container.d.ts +6 -13
  15. package/lib/components/scroll-container.js +11 -22
  16. package/lib/input-model.d.ts +4 -0
  17. package/lib/theme-provider.js +2 -2
  18. package/lib/tokens.d.ts +50 -1
  19. package/lib/tokens.js +12 -0
  20. package/lib/types.d.ts +4 -0
  21. package/lib/widgets/chat-widget.js +5 -1
  22. package/lib/widgets/index.d.ts +1 -0
  23. package/lib/widgets/index.js +1 -0
  24. package/lib/widgets/multichat-panel.d.ts +15 -0
  25. package/lib/widgets/multichat-panel.js +41 -9
  26. package/lib/widgets/placeholder.d.ts +48 -0
  27. package/lib/widgets/placeholder.js +48 -0
  28. package/package.json +3 -3
  29. package/src/__tests__/chat-body-placeholder.spec.ts +69 -0
  30. package/src/__tests__/multichat-panel.spec.ts +135 -0
  31. package/src/components/chat.tsx +6 -0
  32. package/src/components/input/buttons/send-button.tsx +158 -14
  33. package/src/components/input/chat-input.tsx +6 -5
  34. package/src/components/messages/chat-body-placeholder.tsx +24 -0
  35. package/src/components/messages/index.ts +1 -0
  36. package/src/components/messages/messages.tsx +59 -2
  37. package/src/components/scroll-container.tsx +27 -34
  38. package/src/input-model.ts +4 -0
  39. package/src/theme-provider.ts +2 -2
  40. package/src/tokens.ts +61 -1
  41. package/src/types.ts +4 -0
  42. package/src/widgets/chat-widget.tsx +14 -1
  43. package/src/widgets/index.ts +1 -0
  44. package/src/widgets/multichat-panel.tsx +56 -10
  45. package/src/widgets/placeholder.tsx +122 -0
  46. package/style/base.css +1 -0
  47. 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 React, { useEffect, useState } from 'react';
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
- <TooltippedIconButton
76
- onClick={send}
77
- tooltip={tooltip}
78
- disabled={disabled}
79
- buttonProps={{
80
- title: tooltip,
81
- className: SEND_BUTTON_CLASS
82
- }}
83
- aria-label={tooltip}
84
- >
85
- <ArrowUpwardIcon />
86
- </TooltippedIconButton>
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
+ }
@@ -12,3 +12,4 @@ export * from './navigation';
12
12
  export * from './preamble';
13
13
  export * from './toolbar';
14
14
  export * from './welcome';
15
+ export * from './chat-body-placeholder';
@@ -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
- * Component that handles intelligent scrolling.
15
+ * Scrollable container for chat messages. Scroll position is managed
16
+ * explicitly by the parent (ChatMessages) via a forwarded ref.
16
17
  *
17
- * - If viewport is at the bottom of the overflow container, appending new
18
- * children keeps the viewport on the bottom of the overflow container.
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 function ScrollContainer(props: ScrollContainerProps): JSX.Element {
30
- const id = useMemo(
31
- () => 'jupyter-chat-scroll-container-' + Date.now().toString(),
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
- return (
36
- <Box
37
- id={id}
38
- sx={{
39
- overflowY: 'scroll',
40
- '& *': {
41
- overflowAnchor: 'none'
42
- },
43
- ...props.sx
44
- }}
45
- >
46
- <Box>{props.children}</Box>
47
- <Box sx={{ overflowAnchor: 'auto', height: '1px' }} />
48
- </Box>
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
+ );
@@ -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
 
@@ -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: '4px',
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: '4px',
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
@@ -59,6 +59,10 @@ export interface IConfig {
59
59
  * Whether to display deleted messages.
60
60
  */
61
61
  showDeleted?: boolean;
62
+ /**
63
+ * Whether to display the 'send with selection' button.
64
+ */
65
+ sendWithSelection?: boolean;
62
66
  }
63
67
 
64
68
  /**
@@ -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
  }
@@ -8,3 +8,4 @@ export * from './chat-selector-popup';
8
8
  export * from './chat-sidebar';
9
9
  export * from './chat-widget';
10
10
  export * from './multichat-panel';
11
+ export * from './placeholder';