@jupyter/chat 0.19.0-alpha.2 → 0.19.0-alpha.3

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 (29) hide show
  1. package/lib/components/input/buttons/attach-button.js +1 -1
  2. package/lib/components/input/buttons/cancel-button.js +1 -1
  3. package/lib/components/input/buttons/save-edit-button.js +1 -1
  4. package/lib/components/input/buttons/send-button.js +3 -1
  5. package/lib/components/input/buttons/stop-button.js +1 -1
  6. package/lib/components/input/chat-input.js +2 -5
  7. package/lib/components/mui-extras/tooltipped-button.d.ts +5 -26
  8. package/lib/components/mui-extras/tooltipped-button.js +4 -33
  9. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +12 -22
  10. package/lib/components/mui-extras/tooltipped-icon-button.js +10 -8
  11. package/lib/theme-provider.d.ts +19 -0
  12. package/lib/theme-provider.js +74 -3
  13. package/lib/widgets/chat-widget.d.ts +4 -0
  14. package/lib/widgets/chat-widget.js +38 -1
  15. package/lib/widgets/multichat-panel.js +1 -1
  16. package/package.json +3 -1
  17. package/src/components/input/buttons/attach-button.tsx +1 -1
  18. package/src/components/input/buttons/cancel-button.tsx +1 -1
  19. package/src/components/input/buttons/save-edit-button.tsx +1 -1
  20. package/src/components/input/buttons/send-button.tsx +4 -1
  21. package/src/components/input/buttons/stop-button.tsx +1 -1
  22. package/src/components/input/chat-input.tsx +1 -6
  23. package/src/components/mui-extras/tooltipped-button.tsx +9 -43
  24. package/src/components/mui-extras/tooltipped-icon-button.tsx +17 -38
  25. package/src/theme-provider.ts +95 -3
  26. package/src/widgets/chat-widget.tsx +44 -1
  27. package/src/widgets/multichat-panel.tsx +5 -1
  28. package/style/chat.css +10 -0
  29. package/style/input.css +4 -0
@@ -38,7 +38,7 @@ export function AttachButton(props) {
38
38
  console.warn('Error selecting files to attach', e);
39
39
  }
40
40
  };
41
- return (React.createElement(TooltippedIconButton, { onClick: onclick, tooltip: tooltip, iconButtonProps: {
41
+ return (React.createElement(TooltippedIconButton, { onClick: onclick, tooltip: tooltip, buttonProps: {
42
42
  title: tooltip,
43
43
  className: ATTACH_BUTTON_CLASS
44
44
  } },
@@ -14,7 +14,7 @@ export function CancelButton(props) {
14
14
  return React.createElement(React.Fragment, null);
15
15
  }
16
16
  const tooltip = 'Cancel editing';
17
- return (React.createElement(TooltippedIconButton, { onClick: props.model.cancel, tooltip: tooltip, iconButtonProps: {
17
+ return (React.createElement(TooltippedIconButton, { onClick: props.model.cancel, tooltip: tooltip, buttonProps: {
18
18
  title: tooltip,
19
19
  className: CANCEL_BUTTON_CLASS
20
20
  } },
@@ -36,7 +36,7 @@ export function SaveEditButton(props) {
36
36
  await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
37
37
  model.send(model.value);
38
38
  }
39
- return (React.createElement(TooltippedIconButton, { onClick: save, tooltip: tooltip, disabled: disabled, iconButtonProps: {
39
+ return (React.createElement(TooltippedIconButton, { onClick: save, tooltip: tooltip, disabled: disabled, buttonProps: {
40
40
  title: tooltip,
41
41
  className: SAVE_EDIT_BUTTON_CLASS
42
42
  }, "aria-label": tooltip },
@@ -33,6 +33,8 @@ export function SendButton(props) {
33
33
  : 'Send message (ENTER)');
34
34
  };
35
35
  model.configChanged.connect(configChanged);
36
+ // Initialize the tooltip.
37
+ configChanged(model, model.config);
36
38
  return () => {
37
39
  var _a, _b;
38
40
  model.valueChanged.disconnect(inputChanged);
@@ -51,7 +53,7 @@ export function SendButton(props) {
51
53
  model.value = '';
52
54
  model.focus();
53
55
  }
54
- return (React.createElement(TooltippedIconButton, { onClick: send, tooltip: tooltip, disabled: disabled, iconButtonProps: {
56
+ return (React.createElement(TooltippedIconButton, { onClick: send, tooltip: tooltip, disabled: disabled, buttonProps: {
55
57
  title: tooltip,
56
58
  className: SEND_BUTTON_CLASS
57
59
  }, "aria-label": tooltip },
@@ -38,7 +38,7 @@ export function StopButton(props) {
38
38
  // This will need to be implemented based on how the chat model handles stopping AI responses
39
39
  console.log('Stop button clicked');
40
40
  }
41
- return (React.createElement(TooltippedIconButton, { onClick: stop, tooltip: tooltip, disabled: disabled, iconButtonProps: {
41
+ return (React.createElement(TooltippedIconButton, { onClick: stop, tooltip: tooltip, disabled: disabled, buttonProps: {
42
42
  title: tooltip,
43
43
  className: STOP_BUTTON_CLASS
44
44
  }, "aria-label": tooltip },
@@ -23,7 +23,6 @@ export function ChatInput(props) {
23
23
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
24
24
  const [attachments, setAttachments] = useState(model.attachments);
25
25
  const [toolbarElements, setToolbarElements] = useState([]);
26
- const [isFocused, setIsFocused] = useState(false);
27
26
  const [writers, setWriters] = useState([]);
28
27
  /**
29
28
  * Auto-focus the input when the component is first mounted.
@@ -163,9 +162,7 @@ export function ChatInput(props) {
163
162
  return (React.createElement(Box, { sx: props.sx, className: clsx(INPUT_BOX_CLASS), "data-input-id": model.id },
164
163
  React.createElement(Box, { sx: {
165
164
  border: '1px solid',
166
- borderColor: isFocused
167
- ? 'var(--jp-brand-color1)'
168
- : 'var(--jp-border-color1)',
165
+ borderColor: 'var(--jp-border-color1)',
169
166
  borderRadius: 2,
170
167
  transition: 'border-color 0.2s ease',
171
168
  display: 'flex',
@@ -188,7 +185,7 @@ export function ChatInput(props) {
188
185
  padding: 0
189
186
  }
190
187
  }
191
- }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, 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: {
188
+ }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, onKeyDown: handleKeyDown, placeholder: "Type a chat message, @ to mention...", inputRef: inputRef, 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: {
192
189
  padding: 1.5,
193
190
  margin: 0,
194
191
  boxSizing: 'border-box',
@@ -1,35 +1,14 @@
1
- import { ButtonOwnProps, ButtonProps, SxProps, TooltipProps } from '@mui/material';
1
+ import { ButtonProps, SxProps, TooltipProps } from '@mui/material';
2
2
  import React from 'react';
3
3
  export declare const TOOLTIPPED_WRAP_CLASS = "jp-chat-tooltipped-wrap";
4
- export declare const DEFAULT_BUTTON_PROPS: Partial<ButtonOwnProps>;
5
- export declare const DEFAULT_BUTTON_SX: {
6
- minWidth: string;
7
- width: string;
8
- height: string;
9
- lineHeight: number;
10
- '&:disabled': {
11
- opacity: number;
12
- };
13
- };
14
- export declare const INPUT_TOOLBAR_BUTTON_SX: {
15
- backgroundColor: string;
16
- color: string;
17
- borderRadius: string;
18
- boxShadow: string;
19
- '&:hover': {
20
- backgroundColor: string;
21
- boxShadow: string;
22
- };
23
- '&:disabled': {
24
- backgroundColor: string;
25
- color: string;
26
- opacity: number;
27
- };
28
- };
4
+ /**
5
+ * The props for the tooltipped button.
6
+ */
29
7
  export type TooltippedButtonProps = {
30
8
  onClick: React.MouseEventHandler<HTMLButtonElement>;
31
9
  tooltip: string;
32
10
  children: JSX.Element;
11
+ className?: string;
33
12
  inputToolbar?: boolean;
34
13
  disabled?: boolean;
35
14
  placement?: TooltipProps['placement'];
@@ -2,38 +2,11 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ import { classes } from '@jupyterlab/ui-components';
5
6
  import { Button } from '@mui/material';
6
7
  import React from 'react';
7
8
  import { ContrastingTooltip } from './contrasting-tooltip';
8
9
  export const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap';
9
- export const DEFAULT_BUTTON_PROPS = {
10
- size: 'small',
11
- variant: 'contained'
12
- };
13
- export const DEFAULT_BUTTON_SX = {
14
- minWidth: '24px',
15
- width: '24px',
16
- height: '24px',
17
- lineHeight: 0,
18
- '&:disabled': {
19
- opacity: 0.5
20
- }
21
- };
22
- export const INPUT_TOOLBAR_BUTTON_SX = {
23
- backgroundColor: 'var(--jp-brand-color1)',
24
- color: 'white',
25
- borderRadius: '4px',
26
- boxShadow: 'none',
27
- '&:hover': {
28
- backgroundColor: 'var(--jp-brand-color0)',
29
- boxShadow: 'none'
30
- },
31
- '&:disabled': {
32
- backgroundColor: 'var(--jp-border-color2)',
33
- color: 'var(--jp-ui-font-color3)',
34
- opacity: 0.5
35
- }
36
- };
37
10
  /**
38
11
  * A component that renders an MUI `Button` with a high-contrast tooltip
39
12
  * provided by `ContrastingTooltip`. This component differs from the MUI
@@ -63,10 +36,8 @@ export function TooltippedButton(props) {
63
36
  ]
64
37
  }
65
38
  } },
66
- React.createElement("span", { style: { cursor: 'default' }, className: TOOLTIPPED_WRAP_CLASS },
67
- React.createElement(Button, { ...DEFAULT_BUTTON_PROPS, ...props.buttonProps, onClick: props.onClick, disabled: props.disabled, sx: {
68
- ...DEFAULT_BUTTON_SX,
69
- ...(((_b = props.inputToolbar) !== null && _b !== void 0 ? _b : true) && INPUT_TOOLBAR_BUTTON_SX),
39
+ React.createElement("span", { className: classes(props.className, TOOLTIPPED_WRAP_CLASS) },
40
+ React.createElement(Button, { ...(((_b = props.inputToolbar) !== null && _b !== void 0 ? _b : true) && { variant: 'input-toolbar' }), ...props.buttonProps, onClick: props.onClick, disabled: props.disabled, "aria-label": (_c = props['aria-label']) !== null && _c !== void 0 ? _c : props.tooltip, sx: {
70
41
  ...props.sx
71
- }, "aria-label": (_c = props['aria-label']) !== null && _c !== void 0 ? _c : props.tooltip }, props.children))));
42
+ } }, props.children))));
72
43
  }
@@ -1,29 +1,14 @@
1
1
  /// <reference types="react" />
2
- import { IconButtonProps, SvgIconOwnProps, TooltipProps } from '@mui/material';
3
- export type TooltippedIconButtonProps = {
4
- onClick: () => unknown;
5
- tooltip: string;
6
- children: JSX.Element;
7
- className?: string;
8
- inputToolbar?: boolean;
9
- disabled?: boolean;
10
- placement?: TooltipProps['placement'];
11
- /**
12
- * The font size of the icon. By default it will be set to 'small'.
13
- */
14
- fontSize?: SvgIconOwnProps['fontSize'];
15
- /**
16
- * The offset of the tooltip popup.
17
- *
18
- * The expected syntax is defined by the Popper library:
19
- * https://popper.js.org/docs/v2/modifiers/offset/
20
- */
21
- offset?: [number, number];
22
- 'aria-label'?: string;
2
+ import { IconButtonProps } from '@mui/material';
3
+ import { TooltippedButtonProps } from './tooltipped-button';
4
+ /**
5
+ * The props for the tooltipped icon button.
6
+ */
7
+ export type TooltippedIconButtonProps = TooltippedButtonProps & {
23
8
  /**
24
9
  * Props passed directly to the MUI `IconButton` component.
25
10
  */
26
- iconButtonProps?: IconButtonProps;
11
+ buttonProps?: IconButtonProps;
27
12
  };
28
13
  /**
29
14
  * A component that renders an MUI `IconButton` with a high-contrast tooltip
@@ -36,5 +21,10 @@ export type TooltippedIconButtonProps = {
36
21
  * - Lowers the opacity of the IconButton when disabled.
37
22
  * - Renders the IconButton with `line-height: 0` to avoid showing extra
38
23
  * vertical space in SVG icons.
24
+ *
25
+ * NOTES:
26
+ * This kind of button doesn't allow regular variants ('outlined', 'contained', 'text').
27
+ * The only one allowed is 'input-toolbar'. If you want to use one of the regular, use
28
+ * the TooltippedButton instead.
39
29
  */
40
30
  export declare function TooltippedIconButton(props: TooltippedIconButtonProps): JSX.Element;
@@ -6,7 +6,7 @@ import { classes } from '@jupyterlab/ui-components';
6
6
  import { IconButton } from '@mui/material';
7
7
  import React from 'react';
8
8
  import { ContrastingTooltip } from './contrasting-tooltip';
9
- import { DEFAULT_BUTTON_PROPS, DEFAULT_BUTTON_SX, INPUT_TOOLBAR_BUTTON_SX, TOOLTIPPED_WRAP_CLASS } from './tooltipped-button';
9
+ import { TOOLTIPPED_WRAP_CLASS } from './tooltipped-button';
10
10
  /**
11
11
  * A component that renders an MUI `IconButton` with a high-contrast tooltip
12
12
  * provided by `ContrastingTooltip`. This component differs from the MUI
@@ -18,12 +18,15 @@ import { DEFAULT_BUTTON_PROPS, DEFAULT_BUTTON_SX, INPUT_TOOLBAR_BUTTON_SX, TOOLT
18
18
  * - Lowers the opacity of the IconButton when disabled.
19
19
  * - Renders the IconButton with `line-height: 0` to avoid showing extra
20
20
  * vertical space in SVG icons.
21
+ *
22
+ * NOTES:
23
+ * This kind of button doesn't allow regular variants ('outlined', 'contained', 'text').
24
+ * The only one allowed is 'input-toolbar'. If you want to use one of the regular, use
25
+ * the TooltippedButton instead.
21
26
  */
22
27
  export function TooltippedIconButton(props) {
23
28
  var _a, _b, _c;
24
- // Override the default icon font size from 'medium' to 'small'
25
- props.children.props.fontSize = (_a = props.fontSize) !== null && _a !== void 0 ? _a : 'small';
26
- return (React.createElement(ContrastingTooltip, { title: props.tooltip, placement: (_b = props.placement) !== null && _b !== void 0 ? _b : 'top', slotProps: {
29
+ return (React.createElement(ContrastingTooltip, { title: props.tooltip, placement: (_a = props.placement) !== null && _a !== void 0 ? _a : 'top', slotProps: {
27
30
  popper: {
28
31
  modifiers: [
29
32
  {
@@ -36,8 +39,7 @@ export function TooltippedIconButton(props) {
36
39
  }
37
40
  } },
38
41
  React.createElement("span", { className: classes(props.className, TOOLTIPPED_WRAP_CLASS) },
39
- React.createElement(IconButton, { ...DEFAULT_BUTTON_PROPS, ...props.iconButtonProps, onClick: props.onClick, disabled: props.disabled, sx: {
40
- ...DEFAULT_BUTTON_SX,
41
- ...(((_c = props.inputToolbar) !== null && _c !== void 0 ? _c : true) && INPUT_TOOLBAR_BUTTON_SX)
42
- }, "aria-label": props['aria-label'] }, props.children))));
42
+ React.createElement(IconButton, { ...(((_b = props.inputToolbar) !== null && _b !== void 0 ? _b : true) && { variant: 'input-toolbar' }), ...props.buttonProps, onClick: props.onClick, disabled: props.disabled, "aria-label": (_c = props['aria-label']) !== null && _c !== void 0 ? _c : props.tooltip, sx: {
43
+ ...props.sx
44
+ } }, props.children))));
43
45
  }
@@ -1,3 +1,22 @@
1
+ import '@mui/material/Button';
2
+ import '@mui/material/IconButton';
1
3
  import { Theme } from '@mui/material/styles';
4
+ /**
5
+ * Allow a new variant type, for the input toolbar.
6
+ */
7
+ declare module '@mui/material/Button' {
8
+ interface ButtonPropsVariantOverrides {
9
+ 'input-toolbar': true;
10
+ }
11
+ }
12
+ /**
13
+ * Allow a new variant property in IconButton, to be able to set the same properties
14
+ * as the input toolbar buttons.
15
+ */
16
+ declare module '@mui/material/IconButton' {
17
+ interface IconButtonOwnProps {
18
+ variant?: 'input-toolbar';
19
+ }
20
+ }
2
21
  export declare function pollUntilReady(): Promise<void>;
3
22
  export declare function getJupyterLabTheme(): Promise<Theme>;
@@ -2,6 +2,8 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ import '@mui/material/Button';
6
+ import '@mui/material/IconButton';
5
7
  import { createTheme } from '@mui/material/styles';
6
8
  function getCSSVariable(name) {
7
9
  return getComputedStyle(document.body).getPropertyValue(name).trim();
@@ -19,8 +21,41 @@ export async function getJupyterLabTheme() {
19
21
  components: {
20
22
  MuiButton: {
21
23
  defaultProps: {
22
- size: 'small'
23
- }
24
+ size: 'small',
25
+ variant: 'contained'
26
+ },
27
+ styleOverrides: {
28
+ root: {
29
+ minWidth: '24px',
30
+ width: '24px',
31
+ height: '24px',
32
+ lineHeight: 0,
33
+ '&:disabled': {
34
+ opacity: 0.5
35
+ }
36
+ }
37
+ },
38
+ variants: [
39
+ {
40
+ // The default style for input toolbar button variant.
41
+ props: { variant: 'input-toolbar' },
42
+ style: {
43
+ backgroundColor: 'var(--jp-brand-color1)',
44
+ color: 'white',
45
+ borderRadius: '4px',
46
+ boxShadow: 'none',
47
+ '&:hover': {
48
+ backgroundColor: 'var(--jp-brand-color0)',
49
+ boxShadow: 'none'
50
+ },
51
+ '&:disabled': {
52
+ backgroundColor: 'var(--jp-border-color2)',
53
+ color: 'var(--jp-ui-font-color3)',
54
+ opacity: 0.5
55
+ }
56
+ }
57
+ }
58
+ ]
24
59
  },
25
60
  MuiFilledInput: {
26
61
  defaultProps: {
@@ -41,7 +76,43 @@ export async function getJupyterLabTheme() {
41
76
  MuiIconButton: {
42
77
  defaultProps: {
43
78
  size: 'small'
44
- }
79
+ },
80
+ styleOverrides: {
81
+ root: {
82
+ minWidth: '24px',
83
+ width: '24px',
84
+ height: '24px',
85
+ lineHeight: 0,
86
+ '&:disabled': {
87
+ opacity: 0.5
88
+ },
89
+ // Set the default size of the svg icon if not set by user.
90
+ '& .MuiSvgIcon-root:not([fontSize])': {
91
+ fontSize: 'medium'
92
+ }
93
+ }
94
+ },
95
+ variants: [
96
+ {
97
+ // The default style for input toolbar button variant.
98
+ props: { variant: 'input-toolbar' },
99
+ style: {
100
+ backgroundColor: 'var(--jp-brand-color1)',
101
+ color: 'white',
102
+ borderRadius: '4px',
103
+ boxShadow: 'none',
104
+ '&:hover': {
105
+ backgroundColor: 'var(--jp-brand-color0)',
106
+ boxShadow: 'none'
107
+ },
108
+ '&:disabled': {
109
+ backgroundColor: 'var(--jp-border-color2)',
110
+ color: 'var(--jp-ui-font-color3)',
111
+ opacity: 0.5
112
+ }
113
+ }
114
+ }
115
+ ]
45
116
  },
46
117
  MuiInputBase: {
47
118
  defaultProps: {
@@ -55,6 +55,10 @@ export declare class ChatWidget extends ReactWidget {
55
55
  * Process dropped cells
56
56
  */
57
57
  private _processCellDrop;
58
+ /**
59
+ * Process dropped tabBar files
60
+ */
61
+ private _processTabDrop;
58
62
  /**
59
63
  * Find the notebook path for a cell by searching through active and open notebooks
60
64
  */
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { ReactWidget } from '@jupyterlab/apputils';
6
+ import { DocumentWidget } from '@jupyterlab/docregistry';
6
7
  import { isCode, isMarkdown, isRaw } from '@jupyterlab/nbformat';
7
8
  import React from 'react';
8
9
  import { Drag } from '@lumino/dragdrop';
@@ -12,6 +13,8 @@ import { chatIcon } from '../icons';
12
13
  const FILE_BROWSER_MIME = 'application/x-jupyter-icontentsrich';
13
14
  // MIME type constant for Notebook cell drag events
14
15
  const NOTEBOOK_CELL_MIME = 'application/vnd.jupyter.cells';
16
+ // MIME type constant for TabBar file drag events
17
+ const TABBAR_FILE_MIME = 'application/vnd.lumino.widget-factory';
15
18
  // CSS class constants
16
19
  const INPUT_CONTAINER_CLASS = 'jp-chat-input-container';
17
20
  const DRAG_HOVER_CLASS = 'jp-chat-drag-hover';
@@ -146,7 +149,9 @@ export class ChatWidget extends ReactWidget {
146
149
  */
147
150
  _canHandleDrop(event) {
148
151
  const types = event.mimeData.types();
149
- return (types.includes(NOTEBOOK_CELL_MIME) || types.includes(FILE_BROWSER_MIME));
152
+ return (types.includes(NOTEBOOK_CELL_MIME) ||
153
+ types.includes(FILE_BROWSER_MIME) ||
154
+ types.includes(TABBAR_FILE_MIME));
150
155
  }
151
156
  /**
152
157
  * Handle drop events
@@ -166,6 +171,9 @@ export class ChatWidget extends ReactWidget {
166
171
  else if (event.mimeData.hasData(FILE_BROWSER_MIME)) {
167
172
  this._processFileDrop(event);
168
173
  }
174
+ else if (event.mimeData.hasData(TABBAR_FILE_MIME)) {
175
+ this._processTabDrop(event);
176
+ }
169
177
  }
170
178
  catch (error) {
171
179
  console.error('Error processing drop:', error);
@@ -268,6 +276,35 @@ export class ChatWidget extends ReactWidget {
268
276
  console.error('Failed to process cell drop: ', error);
269
277
  }
270
278
  }
279
+ /**
280
+ * Process dropped tabBar files
281
+ */
282
+ _processTabDrop(event) {
283
+ var _a, _b;
284
+ const factory = event.mimeData.getData(TABBAR_FILE_MIME);
285
+ if (!factory) {
286
+ console.warn('No factory in drag event');
287
+ return;
288
+ }
289
+ const widget = factory();
290
+ if (!widget || !(widget instanceof DocumentWidget)) {
291
+ console.warn('No file associated to the element');
292
+ return;
293
+ }
294
+ const path = widget.context.path;
295
+ if (!path) {
296
+ console.warn('Widget has no path');
297
+ return;
298
+ }
299
+ const mimetype = ((_a = widget.context.model) === null || _a === void 0 ? void 0 : _a.mimeType) || 'application/octet-stream';
300
+ const attachment = {
301
+ type: 'file',
302
+ value: path,
303
+ mimetype
304
+ };
305
+ const inputModel = this._getInputFromEvent(event);
306
+ (_b = inputModel === null || inputModel === void 0 ? void 0 : inputModel.addAttachment) === null || _b === void 0 ? void 0 : _b.call(inputModel, attachment);
307
+ }
271
308
  /**
272
309
  * Find the notebook path for a cell by searching through active and open notebooks
273
310
  */
@@ -354,7 +354,7 @@ function ChatSelect({ chatNamesChanged, handleChange }) {
354
354
  chatNamesChanged.connect((_, chatNames) => {
355
355
  setChatNames(chatNames);
356
356
  });
357
- return (React.createElement(HTMLSelect, { onChange: handleChange, value: "-" },
357
+ return (React.createElement(HTMLSelect, { key: Object.keys(chatNames).join(), onChange: handleChange, value: "-" },
358
358
  React.createElement("option", { value: "-", disabled: true, hidden: true }, "Open a chat"),
359
359
  Object.keys(chatNames).map(name => (React.createElement("option", { value: chatNames[name] }, name)))));
360
360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.19.0-alpha.2",
3
+ "version": "0.19.0-alpha.3",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -51,6 +51,7 @@
51
51
  "@jupyterlab/codeeditor": "^4.2.0",
52
52
  "@jupyterlab/codemirror": "^4.2.0",
53
53
  "@jupyterlab/docmanager": "^4.2.0",
54
+ "@jupyterlab/docregistry": "^4.2.0",
54
55
  "@jupyterlab/filebrowser": "^4.2.0",
55
56
  "@jupyterlab/fileeditor": "^4.2.0",
56
57
  "@jupyterlab/notebook": "^4.2.0",
@@ -62,6 +63,7 @@
62
63
  "@lumino/disposable": "^2.0.0",
63
64
  "@lumino/polling": "^2.0.0",
64
65
  "@lumino/signaling": "^2.0.0",
66
+ "@lumino/widgets": "^2.0.0",
65
67
  "@mui/icons-material": "^7.3.2",
66
68
  "@mui/material": "^7.3.2",
67
69
  "clsx": "^2.1.0",
@@ -50,7 +50,7 @@ export function AttachButton(
50
50
  <TooltippedIconButton
51
51
  onClick={onclick}
52
52
  tooltip={tooltip}
53
- iconButtonProps={{
53
+ buttonProps={{
54
54
  title: tooltip,
55
55
  className: ATTACH_BUTTON_CLASS
56
56
  }}
@@ -25,7 +25,7 @@ export function CancelButton(
25
25
  <TooltippedIconButton
26
26
  onClick={props.model.cancel}
27
27
  tooltip={tooltip}
28
- iconButtonProps={{
28
+ buttonProps={{
29
29
  title: tooltip,
30
30
  className: CANCEL_BUTTON_CLASS
31
31
  }}
@@ -54,7 +54,7 @@ export function SaveEditButton(
54
54
  onClick={save}
55
55
  tooltip={tooltip}
56
56
  disabled={disabled}
57
- iconButtonProps={{
57
+ buttonProps={{
58
58
  title: tooltip,
59
59
  className: SAVE_EDIT_BUTTON_CLASS
60
60
  }}
@@ -48,6 +48,9 @@ export function SendButton(
48
48
  };
49
49
  model.configChanged.connect(configChanged);
50
50
 
51
+ // Initialize the tooltip.
52
+ configChanged(model, model.config);
53
+
51
54
  return () => {
52
55
  model.valueChanged.disconnect(inputChanged);
53
56
  model.attachmentsChanged?.disconnect(inputChanged);
@@ -73,7 +76,7 @@ export function SendButton(
73
76
  onClick={send}
74
77
  tooltip={tooltip}
75
78
  disabled={disabled}
76
- iconButtonProps={{
79
+ buttonProps={{
77
80
  title: tooltip,
78
81
  className: SEND_BUTTON_CLASS
79
82
  }}
@@ -55,7 +55,7 @@ export function StopButton(
55
55
  onClick={stop}
56
56
  tooltip={tooltip}
57
57
  disabled={disabled}
58
- iconButtonProps={{
58
+ buttonProps={{
59
59
  title: tooltip,
60
60
  className: STOP_BUTTON_CLASS
61
61
  }}
@@ -46,7 +46,6 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
46
46
  const [toolbarElements, setToolbarElements] = useState<
47
47
  InputToolbarRegistry.IToolbarItem[]
48
48
  >([]);
49
- const [isFocused, setIsFocused] = useState<boolean>(false);
50
49
  const [writers, setWriters] = useState<IChatModel.IWriter[]>([]);
51
50
 
52
51
  /**
@@ -214,9 +213,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
214
213
  <Box
215
214
  sx={{
216
215
  border: '1px solid',
217
- borderColor: isFocused
218
- ? 'var(--jp-brand-color1)'
219
- : 'var(--jp-border-color1)',
216
+ borderColor: 'var(--jp-border-color1)',
220
217
  borderRadius: 2,
221
218
  transition: 'border-color 0.2s ease',
222
219
  display: 'flex',
@@ -262,8 +259,6 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
262
259
  onKeyDown={handleKeyDown}
263
260
  placeholder="Type a chat message, @ to mention..."
264
261
  inputRef={inputRef}
265
- onFocus={() => setIsFocused(true)}
266
- onBlur={() => setIsFocused(false)}
267
262
  onSelect={() =>
268
263
  (model.cursorIndex = inputRef.current?.selectionStart ?? null)
269
264
  }
@@ -3,54 +3,22 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import {
7
- Button,
8
- ButtonOwnProps,
9
- ButtonProps,
10
- SxProps,
11
- TooltipProps
12
- } from '@mui/material';
6
+ import { classes } from '@jupyterlab/ui-components';
7
+ import { Button, ButtonProps, SxProps, TooltipProps } from '@mui/material';
13
8
  import React from 'react';
14
9
 
15
10
  import { ContrastingTooltip } from './contrasting-tooltip';
16
11
 
17
12
  export const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap';
18
13
 
19
- export const DEFAULT_BUTTON_PROPS: Partial<ButtonOwnProps> = {
20
- size: 'small',
21
- variant: 'contained'
22
- };
23
-
24
- export const DEFAULT_BUTTON_SX = {
25
- minWidth: '24px',
26
- width: '24px',
27
- height: '24px',
28
- lineHeight: 0,
29
- '&:disabled': {
30
- opacity: 0.5
31
- }
32
- };
33
-
34
- export const INPUT_TOOLBAR_BUTTON_SX = {
35
- backgroundColor: 'var(--jp-brand-color1)',
36
- color: 'white',
37
- borderRadius: '4px',
38
- boxShadow: 'none',
39
- '&:hover': {
40
- backgroundColor: 'var(--jp-brand-color0)',
41
- boxShadow: 'none'
42
- },
43
- '&:disabled': {
44
- backgroundColor: 'var(--jp-border-color2)',
45
- color: 'var(--jp-ui-font-color3)',
46
- opacity: 0.5
47
- }
48
- };
49
-
14
+ /**
15
+ * The props for the tooltipped button.
16
+ */
50
17
  export type TooltippedButtonProps = {
51
18
  onClick: React.MouseEventHandler<HTMLButtonElement>;
52
19
  tooltip: string;
53
20
  children: JSX.Element;
21
+ className?: string;
54
22
  inputToolbar?: boolean;
55
23
  disabled?: boolean;
56
24
  placement?: TooltipProps['placement'];
@@ -112,18 +80,16 @@ export function TooltippedButton(props: TooltippedButtonProps): JSX.Element {
112
80
 
113
81
  See: https://mui.com/material-ui/react-tooltip/#disabled-elements
114
82
  */}
115
- <span style={{ cursor: 'default' }} className={TOOLTIPPED_WRAP_CLASS}>
83
+ <span className={classes(props.className, TOOLTIPPED_WRAP_CLASS)}>
116
84
  <Button
117
- {...DEFAULT_BUTTON_PROPS}
85
+ {...((props.inputToolbar ?? true) && { variant: 'input-toolbar' })}
118
86
  {...props.buttonProps}
119
87
  onClick={props.onClick}
120
88
  disabled={props.disabled}
89
+ aria-label={props['aria-label'] ?? props.tooltip}
121
90
  sx={{
122
- ...DEFAULT_BUTTON_SX,
123
- ...((props.inputToolbar ?? true) && INPUT_TOOLBAR_BUTTON_SX),
124
91
  ...props.sx
125
92
  }}
126
- aria-label={props['aria-label'] ?? props.tooltip}
127
93
  >
128
94
  {props.children}
129
95
  </Button>
@@ -4,46 +4,23 @@
4
4
  */
5
5
 
6
6
  import { classes } from '@jupyterlab/ui-components';
7
- import {
8
- IconButton,
9
- IconButtonProps,
10
- SvgIconOwnProps,
11
- TooltipProps
12
- } from '@mui/material';
7
+ import { IconButton, IconButtonProps } from '@mui/material';
13
8
  import React from 'react';
14
9
 
15
10
  import { ContrastingTooltip } from './contrasting-tooltip';
16
11
  import {
17
- DEFAULT_BUTTON_PROPS,
18
- DEFAULT_BUTTON_SX,
19
- INPUT_TOOLBAR_BUTTON_SX,
20
- TOOLTIPPED_WRAP_CLASS
12
+ TOOLTIPPED_WRAP_CLASS,
13
+ TooltippedButtonProps
21
14
  } from './tooltipped-button';
22
15
 
23
- export type TooltippedIconButtonProps = {
24
- onClick: () => unknown;
25
- tooltip: string;
26
- children: JSX.Element;
27
- className?: string;
28
- inputToolbar?: boolean;
29
- disabled?: boolean;
30
- placement?: TooltipProps['placement'];
31
- /**
32
- * The font size of the icon. By default it will be set to 'small'.
33
- */
34
- fontSize?: SvgIconOwnProps['fontSize'];
35
- /**
36
- * The offset of the tooltip popup.
37
- *
38
- * The expected syntax is defined by the Popper library:
39
- * https://popper.js.org/docs/v2/modifiers/offset/
40
- */
41
- offset?: [number, number];
42
- 'aria-label'?: string;
16
+ /**
17
+ * The props for the tooltipped icon button.
18
+ */
19
+ export type TooltippedIconButtonProps = TooltippedButtonProps & {
43
20
  /**
44
21
  * Props passed directly to the MUI `IconButton` component.
45
22
  */
46
- iconButtonProps?: IconButtonProps;
23
+ buttonProps?: IconButtonProps;
47
24
  };
48
25
 
49
26
  /**
@@ -57,12 +34,15 @@ export type TooltippedIconButtonProps = {
57
34
  * - Lowers the opacity of the IconButton when disabled.
58
35
  * - Renders the IconButton with `line-height: 0` to avoid showing extra
59
36
  * vertical space in SVG icons.
37
+ *
38
+ * NOTES:
39
+ * This kind of button doesn't allow regular variants ('outlined', 'contained', 'text').
40
+ * The only one allowed is 'input-toolbar'. If you want to use one of the regular, use
41
+ * the TooltippedButton instead.
60
42
  */
61
43
  export function TooltippedIconButton(
62
44
  props: TooltippedIconButtonProps
63
45
  ): JSX.Element {
64
- // Override the default icon font size from 'medium' to 'small'
65
- props.children.props.fontSize = props.fontSize ?? 'small';
66
46
  return (
67
47
  <ContrastingTooltip
68
48
  title={props.tooltip}
@@ -89,15 +69,14 @@ export function TooltippedIconButton(
89
69
  */}
90
70
  <span className={classes(props.className, TOOLTIPPED_WRAP_CLASS)}>
91
71
  <IconButton
92
- {...DEFAULT_BUTTON_PROPS}
93
- {...props.iconButtonProps}
72
+ {...((props.inputToolbar ?? true) && { variant: 'input-toolbar' })}
73
+ {...props.buttonProps}
94
74
  onClick={props.onClick}
95
75
  disabled={props.disabled}
76
+ aria-label={props['aria-label'] ?? props.tooltip}
96
77
  sx={{
97
- ...DEFAULT_BUTTON_SX,
98
- ...((props.inputToolbar ?? true) && INPUT_TOOLBAR_BUTTON_SX)
78
+ ...props.sx
99
79
  }}
100
- aria-label={props['aria-label']}
101
80
  >
102
81
  {props.children}
103
82
  </IconButton>
@@ -3,8 +3,31 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import '@mui/material/Button';
7
+ import '@mui/material/IconButton';
6
8
  import { Theme, createTheme } from '@mui/material/styles';
7
9
 
10
+ /**
11
+ * Allow a new variant type, for the input toolbar.
12
+ */
13
+ declare module '@mui/material/Button' {
14
+ // eslint-disable-next-line @typescript-eslint/naming-convention
15
+ interface ButtonPropsVariantOverrides {
16
+ 'input-toolbar': true;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Allow a new variant property in IconButton, to be able to set the same properties
22
+ * as the input toolbar buttons.
23
+ */
24
+ declare module '@mui/material/IconButton' {
25
+ // eslint-disable-next-line @typescript-eslint/naming-convention
26
+ interface IconButtonOwnProps {
27
+ variant?: 'input-toolbar';
28
+ }
29
+ }
30
+
8
31
  function getCSSVariable(name: string): string {
9
32
  return getComputedStyle(document.body).getPropertyValue(name).trim();
10
33
  }
@@ -23,8 +46,41 @@ export async function getJupyterLabTheme(): Promise<Theme> {
23
46
  components: {
24
47
  MuiButton: {
25
48
  defaultProps: {
26
- size: 'small'
27
- }
49
+ size: 'small',
50
+ variant: 'contained'
51
+ },
52
+ styleOverrides: {
53
+ root: {
54
+ minWidth: '24px',
55
+ width: '24px',
56
+ height: '24px',
57
+ lineHeight: 0,
58
+ '&:disabled': {
59
+ opacity: 0.5
60
+ }
61
+ }
62
+ },
63
+ variants: [
64
+ {
65
+ // The default style for input toolbar button variant.
66
+ props: { variant: 'input-toolbar' },
67
+ style: {
68
+ backgroundColor: 'var(--jp-brand-color1)',
69
+ color: 'white',
70
+ borderRadius: '4px',
71
+ boxShadow: 'none',
72
+ '&:hover': {
73
+ backgroundColor: 'var(--jp-brand-color0)',
74
+ boxShadow: 'none'
75
+ },
76
+ '&:disabled': {
77
+ backgroundColor: 'var(--jp-border-color2)',
78
+ color: 'var(--jp-ui-font-color3)',
79
+ opacity: 0.5
80
+ }
81
+ }
82
+ }
83
+ ]
28
84
  },
29
85
  MuiFilledInput: {
30
86
  defaultProps: {
@@ -45,7 +101,43 @@ export async function getJupyterLabTheme(): Promise<Theme> {
45
101
  MuiIconButton: {
46
102
  defaultProps: {
47
103
  size: 'small'
48
- }
104
+ },
105
+ styleOverrides: {
106
+ root: {
107
+ minWidth: '24px',
108
+ width: '24px',
109
+ height: '24px',
110
+ lineHeight: 0,
111
+ '&:disabled': {
112
+ opacity: 0.5
113
+ },
114
+ // Set the default size of the svg icon if not set by user.
115
+ '& .MuiSvgIcon-root:not([fontSize])': {
116
+ fontSize: 'medium'
117
+ }
118
+ }
119
+ },
120
+ variants: [
121
+ {
122
+ // The default style for input toolbar button variant.
123
+ props: { variant: 'input-toolbar' },
124
+ style: {
125
+ backgroundColor: 'var(--jp-brand-color1)',
126
+ color: 'white',
127
+ borderRadius: '4px',
128
+ boxShadow: 'none',
129
+ '&:hover': {
130
+ backgroundColor: 'var(--jp-brand-color0)',
131
+ boxShadow: 'none'
132
+ },
133
+ '&:disabled': {
134
+ backgroundColor: 'var(--jp-border-color2)',
135
+ color: 'var(--jp-ui-font-color3)',
136
+ opacity: 0.5
137
+ }
138
+ }
139
+ }
140
+ ]
49
141
  },
50
142
  MuiInputBase: {
51
143
  defaultProps: {
@@ -6,10 +6,12 @@
6
6
  import { ReactWidget } from '@jupyterlab/apputils';
7
7
  import { Cell } from '@jupyterlab/cells';
8
8
  import { DirListing } from '@jupyterlab/filebrowser';
9
+ import { DocumentWidget } from '@jupyterlab/docregistry';
9
10
  import { ICell, isCode, isMarkdown, isRaw } from '@jupyterlab/nbformat';
10
11
  import React from 'react';
11
12
  import { Message } from '@lumino/messaging';
12
13
  import { Drag } from '@lumino/dragdrop';
14
+ import { Widget } from '@lumino/widgets';
13
15
 
14
16
  import { Chat, IInputToolbarRegistry, MESSAGE_CLASS } from '../components';
15
17
  import { chatIcon } from '../icons';
@@ -29,6 +31,9 @@ const FILE_BROWSER_MIME = 'application/x-jupyter-icontentsrich';
29
31
  // MIME type constant for Notebook cell drag events
30
32
  const NOTEBOOK_CELL_MIME = 'application/vnd.jupyter.cells';
31
33
 
34
+ // MIME type constant for TabBar file drag events
35
+ const TABBAR_FILE_MIME = 'application/vnd.lumino.widget-factory';
36
+
32
37
  // CSS class constants
33
38
  const INPUT_CONTAINER_CLASS = 'jp-chat-input-container';
34
39
  const DRAG_HOVER_CLASS = 'jp-chat-drag-hover';
@@ -187,7 +192,9 @@ export class ChatWidget extends ReactWidget {
187
192
  private _canHandleDrop(event: Drag.Event): boolean {
188
193
  const types = event.mimeData.types();
189
194
  return (
190
- types.includes(NOTEBOOK_CELL_MIME) || types.includes(FILE_BROWSER_MIME)
195
+ types.includes(NOTEBOOK_CELL_MIME) ||
196
+ types.includes(FILE_BROWSER_MIME) ||
197
+ types.includes(TABBAR_FILE_MIME)
191
198
  );
192
199
  }
193
200
 
@@ -210,6 +217,8 @@ export class ChatWidget extends ReactWidget {
210
217
  this._processCellDrop(event);
211
218
  } else if (event.mimeData.hasData(FILE_BROWSER_MIME)) {
212
219
  this._processFileDrop(event);
220
+ } else if (event.mimeData.hasData(TABBAR_FILE_MIME)) {
221
+ this._processTabDrop(event);
213
222
  }
214
223
  } catch (error) {
215
224
  console.error('Error processing drop:', error);
@@ -329,6 +338,40 @@ export class ChatWidget extends ReactWidget {
329
338
  }
330
339
  }
331
340
 
341
+ /**
342
+ * Process dropped tabBar files
343
+ */
344
+ private _processTabDrop(event: Drag.Event) {
345
+ const factory = event.mimeData.getData(TABBAR_FILE_MIME) as () => Widget;
346
+ if (!factory) {
347
+ console.warn('No factory in drag event');
348
+ return;
349
+ }
350
+
351
+ const widget = factory();
352
+ if (!widget || !(widget instanceof DocumentWidget)) {
353
+ console.warn('No file associated to the element');
354
+ return;
355
+ }
356
+
357
+ const path = widget.context.path;
358
+ if (!path) {
359
+ console.warn('Widget has no path');
360
+ return;
361
+ }
362
+
363
+ const mimetype =
364
+ widget.context.model?.mimeType || 'application/octet-stream';
365
+ const attachment: IFileAttachment = {
366
+ type: 'file',
367
+ value: path,
368
+ mimetype
369
+ };
370
+
371
+ const inputModel = this._getInputFromEvent(event);
372
+ inputModel?.addAttachment?.(attachment);
373
+ }
374
+
332
375
  /**
333
376
  * Find the notebook path for a cell by searching through active and open notebooks
334
377
  */
@@ -546,7 +546,11 @@ function ChatSelect({
546
546
  });
547
547
 
548
548
  return (
549
- <HTMLSelect onChange={handleChange} value="-">
549
+ <HTMLSelect
550
+ key={Object.keys(chatNames).join()}
551
+ onChange={handleChange}
552
+ value="-"
553
+ >
550
554
  <option value="-" disabled hidden>
551
555
  Open a chat
552
556
  </option>
package/style/chat.css CHANGED
@@ -106,3 +106,13 @@
106
106
  padding: 2px 0;
107
107
  font-weight: bold;
108
108
  }
109
+
110
+ .jp-chat-sidepanel .jp-chat-open {
111
+ flex: 1 1 0%;
112
+ min-width: 0;
113
+ max-width: fit-content;
114
+ }
115
+
116
+ .jp-AccordionPanel-title .jp-chat-section-toolbar {
117
+ flex: 0 0 auto;
118
+ }
package/style/input.css CHANGED
@@ -45,3 +45,7 @@
45
45
  .jp-chat-input-toolbar .jp-chat-send-include-opener {
46
46
  padding: 4px 0;
47
47
  }
48
+
49
+ .jp-chat-input-container:focus-within > div:first-of-type {
50
+ border-color: var(--jp-brand-color1);
51
+ }