@jupyter/chat 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/active-cell-manager.js +1 -4
  2. package/lib/chat-commands/index.d.ts +2 -0
  3. package/lib/chat-commands/index.js +6 -0
  4. package/lib/chat-commands/registry.d.ts +28 -0
  5. package/lib/chat-commands/registry.js +29 -0
  6. package/lib/chat-commands/types.d.ts +51 -0
  7. package/lib/chat-commands/types.js +5 -0
  8. package/lib/components/attachments.d.ts +23 -0
  9. package/lib/components/attachments.js +44 -0
  10. package/lib/components/chat-input.d.ts +8 -11
  11. package/lib/components/chat-input.js +70 -95
  12. package/lib/components/chat-messages.d.ts +4 -0
  13. package/lib/components/chat-messages.js +27 -1
  14. package/lib/components/chat.d.ts +11 -5
  15. package/lib/components/chat.js +7 -8
  16. package/lib/components/input/attach-button.d.ts +14 -0
  17. package/lib/components/input/attach-button.js +45 -0
  18. package/lib/components/input/index.d.ts +1 -0
  19. package/lib/components/input/index.js +1 -0
  20. package/lib/components/input/send-button.d.ts +2 -2
  21. package/lib/components/input/use-chat-commands.d.ts +19 -0
  22. package/lib/components/input/use-chat-commands.js +127 -0
  23. package/lib/context.d.ts +3 -0
  24. package/lib/context.js +6 -0
  25. package/lib/index.d.ts +2 -0
  26. package/lib/index.js +2 -0
  27. package/lib/input-model.d.ts +221 -0
  28. package/lib/input-model.js +217 -0
  29. package/lib/model.d.ts +10 -25
  30. package/lib/model.js +15 -17
  31. package/lib/registry.d.ts +11 -64
  32. package/lib/registry.js +4 -72
  33. package/lib/types.d.ts +19 -38
  34. package/lib/widgets/chat-widget.js +2 -1
  35. package/package.json +3 -114
  36. package/src/active-cell-manager.ts +0 -3
  37. package/src/chat-commands/index.ts +7 -0
  38. package/src/chat-commands/registry.ts +60 -0
  39. package/src/chat-commands/types.ts +67 -0
  40. package/src/components/attachments.tsx +91 -0
  41. package/src/components/chat-input.tsx +97 -124
  42. package/src/components/chat-messages.tsx +36 -3
  43. package/src/components/chat.tsx +28 -19
  44. package/src/components/input/attach-button.tsx +68 -0
  45. package/src/components/input/cancel-button.tsx +1 -0
  46. package/src/components/input/index.ts +1 -0
  47. package/src/components/input/send-button.tsx +2 -2
  48. package/src/components/input/use-chat-commands.tsx +186 -0
  49. package/src/context.ts +10 -0
  50. package/src/index.ts +2 -0
  51. package/src/input-model.ts +406 -0
  52. package/src/model.ts +24 -35
  53. package/src/registry.ts +14 -108
  54. package/src/types.ts +19 -39
  55. package/src/widgets/chat-widget.tsx +2 -1
  56. package/style/chat.css +27 -9
  57. package/style/icons/include-selection.svg +3 -1
  58. package/style/icons/read.svg +8 -6
  59. package/style/icons/replace-cell.svg +10 -6
@@ -10,23 +10,22 @@ import React, { useState } from 'react';
10
10
  import { JlThemeProvider } from './jl-theme-provider';
11
11
  import { ChatMessages } from './chat-messages';
12
12
  import { ChatInput } from './chat-input';
13
+ import { AttachmentOpenerContext } from '../context';
13
14
  export function ChatBody(props) {
14
- const { model, rmRegistry: renderMimeRegistry, autocompletionRegistry } = props;
15
- // no need to append to messageGroups imperatively here. all of that is
16
- // handled by the listeners registered in the effect hooks above.
15
+ const { model } = props;
17
16
  const onSend = async (input) => {
18
17
  // send message to backend
19
18
  model.sendMessage({ body: input });
20
19
  };
21
- return (React.createElement(React.Fragment, null,
22
- React.createElement(ChatMessages, { rmRegistry: renderMimeRegistry, model: model }),
20
+ return (React.createElement(AttachmentOpenerContext.Provider, { value: props.attachmentOpenerRegistry },
21
+ React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, documentManager: props.documentManager }),
23
22
  React.createElement(ChatInput, { onSend: onSend, sx: {
24
23
  paddingLeft: 4,
25
24
  paddingRight: 4,
26
- paddingTop: 3.5,
25
+ paddingTop: 1,
27
26
  paddingBottom: 0,
28
27
  borderTop: '1px solid var(--jp-border-color1)'
29
- }, model: model, autocompletionRegistry: autocompletionRegistry })));
28
+ }, model: model.input, documentManager: props.documentManager, chatCommandRegistry: props.chatCommandRegistry })));
30
29
  }
31
30
  export function Chat(props) {
32
31
  var _a;
@@ -54,7 +53,7 @@ export function Chat(props) {
54
53
  React.createElement(ArrowBackIcon, null))) : (React.createElement(Box, null)),
55
54
  view !== Chat.View.settings && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.settings) },
56
55
  React.createElement(SettingsIcon, null))) : (React.createElement(Box, null))),
57
- view === Chat.View.chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry, autocompletionRegistry: props.autocompletionRegistry })),
56
+ view === Chat.View.chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry, documentManager: props.documentManager, chatCommandRegistry: props.chatCommandRegistry, attachmentOpenerRegistry: props.attachmentOpenerRegistry })),
58
57
  view === Chat.View.settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
59
58
  }
60
59
  /**
@@ -0,0 +1,14 @@
1
+ /// <reference types="react" />
2
+ import { IDocumentManager } from '@jupyterlab/docmanager';
3
+ import { IAttachment } from '../../types';
4
+ /**
5
+ * The attach button props.
6
+ */
7
+ export type AttachButtonProps = {
8
+ documentManager: IDocumentManager;
9
+ onAttach: (attachment: IAttachment) => void;
10
+ };
11
+ /**
12
+ * The attach button.
13
+ */
14
+ export declare function AttachButton(props: AttachButtonProps): JSX.Element;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { FileDialog } from '@jupyterlab/filebrowser';
6
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
7
+ import React from 'react';
8
+ import { TooltippedButton } from '../mui-extras/tooltipped-button';
9
+ const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
10
+ /**
11
+ * The attach button.
12
+ */
13
+ export function AttachButton(props) {
14
+ const tooltip = 'Add attachment';
15
+ const onclick = async () => {
16
+ try {
17
+ const files = await FileDialog.getOpenFiles({
18
+ title: 'Select files to attach',
19
+ manager: props.documentManager
20
+ });
21
+ if (files.value) {
22
+ files.value.forEach(file => {
23
+ if (file.type !== 'directory') {
24
+ props.onAttach({ type: 'file', value: file.path });
25
+ }
26
+ });
27
+ }
28
+ }
29
+ catch (e) {
30
+ console.warn('Error selecting files to attach', e);
31
+ }
32
+ };
33
+ return (React.createElement(TooltippedButton, { onClick: onclick, tooltip: tooltip, buttonProps: {
34
+ size: 'small',
35
+ variant: 'contained',
36
+ title: tooltip,
37
+ className: ATTACH_BUTTON_CLASS
38
+ }, sx: {
39
+ minWidth: 'unset',
40
+ padding: '4px',
41
+ borderRadius: '2px 0px 0px 2px',
42
+ marginRight: '1px'
43
+ } },
44
+ React.createElement(AttachFileIcon, null)));
45
+ }
@@ -1,2 +1,3 @@
1
+ export * from './attach-button';
1
2
  export * from './cancel-button';
2
3
  export * from './send-button';
@@ -2,5 +2,6 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ export * from './attach-button';
5
6
  export * from './cancel-button';
6
7
  export * from './send-button';
@@ -1,11 +1,11 @@
1
1
  /// <reference types="react" />
2
- import { IChatModel } from '../../model';
2
+ import { IInputModel } from '../../input-model';
3
3
  import { Selection } from '../../types';
4
4
  /**
5
5
  * The send button props.
6
6
  */
7
7
  export type SendButtonProps = {
8
- model: IChatModel;
8
+ model: IInputModel;
9
9
  sendWithShiftEnter: boolean;
10
10
  inputExists: boolean;
11
11
  onSend: (selection?: Selection) => unknown;
@@ -0,0 +1,19 @@
1
+ import type { AutocompleteProps as GenericAutocompleteProps } from '@mui/material';
2
+ import { IChatCommandRegistry } from '../../chat-commands';
3
+ import { IInputModel } from '../../input-model';
4
+ type AutocompleteProps = GenericAutocompleteProps<any, any, any, any>;
5
+ type UseChatCommandsReturn = {
6
+ autocompleteProps: Omit<AutocompleteProps, 'renderInput'>;
7
+ menu: {
8
+ open: boolean;
9
+ highlighted: boolean;
10
+ };
11
+ };
12
+ /**
13
+ * A hook which automatically returns the list of command options given the
14
+ * current input and chat command registry.
15
+ *
16
+ * Intended usage: `const chatCommands = useChatCommands(...)`.
17
+ */
18
+ export declare function useChatCommands(inputModel: IInputModel, chatCommandRegistry?: IChatCommandRegistry): UseChatCommandsReturn;
19
+ export {};
@@ -0,0 +1,127 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import React from 'react';
6
+ import { useEffect, useState } from 'react';
7
+ import { Box } from '@mui/material';
8
+ /**
9
+ * A hook which automatically returns the list of command options given the
10
+ * current input and chat command registry.
11
+ *
12
+ * Intended usage: `const chatCommands = useChatCommands(...)`.
13
+ */
14
+ export function useChatCommands(inputModel, chatCommandRegistry) {
15
+ // whether an option is highlighted in the chat commands menu
16
+ const [highlighted, setHighlighted] = useState(false);
17
+ // whether the chat commands menu is open
18
+ const [open, setOpen] = useState(false);
19
+ // current list of chat commands matched by the current word.
20
+ // the current word is the space-separated word at the user's cursor.
21
+ const [commands, setCommands] = useState([]);
22
+ useEffect(() => {
23
+ async function getCommands(_, currentWord) {
24
+ const providers = chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.getProviders();
25
+ if (!providers) {
26
+ return;
27
+ }
28
+ if (!(currentWord === null || currentWord === void 0 ? void 0 : currentWord.length)) {
29
+ setCommands([]);
30
+ setOpen(false);
31
+ setHighlighted(false);
32
+ return;
33
+ }
34
+ let newCommands = [];
35
+ for (const provider of providers) {
36
+ // TODO: optimize performance when this method is truly async
37
+ try {
38
+ newCommands = newCommands.concat(await provider.getChatCommands(inputModel));
39
+ }
40
+ catch (e) {
41
+ console.error(`Error when getting chat commands from command provider '${provider.id}': `, e);
42
+ }
43
+ }
44
+ if (newCommands) {
45
+ setOpen(true);
46
+ }
47
+ setCommands(newCommands);
48
+ }
49
+ inputModel.currentWordChanged.connect(getCommands);
50
+ return () => {
51
+ inputModel.currentWordChanged.disconnect(getCommands);
52
+ };
53
+ }, [inputModel]);
54
+ /**
55
+ * onChange(): the callback invoked when a command is selected from the chat
56
+ * commands menu by the user.
57
+ */
58
+ const onChange = (e, command, reason) => {
59
+ if (reason !== 'selectOption') {
60
+ // only call this callback when a command is selected by the user. this
61
+ // requires `reason === 'selectOption'`.
62
+ return;
63
+ }
64
+ if (!chatCommandRegistry) {
65
+ return;
66
+ }
67
+ const currentWord = inputModel.currentWord;
68
+ if (!currentWord) {
69
+ return;
70
+ }
71
+ // if replaceWith is set, handle the command immediately
72
+ if (command.replaceWith) {
73
+ inputModel.replaceCurrentWord(command.replaceWith);
74
+ return;
75
+ }
76
+ // otherwise, defer handling to the command provider
77
+ chatCommandRegistry.handleChatCommand(command, inputModel);
78
+ };
79
+ return {
80
+ autocompleteProps: {
81
+ open,
82
+ options: commands,
83
+ getOptionLabel: (command) => command.name,
84
+ renderOption: (defaultProps, command, __, ___) => {
85
+ const { key, ...listItemProps } = defaultProps;
86
+ const commandIcon = (React.createElement("span", null, typeof command.icon === 'object' ? (React.createElement(command.icon.react, null)) : (command.icon)));
87
+ return (React.createElement(Box, { key: key, component: "li", ...listItemProps },
88
+ commandIcon,
89
+ React.createElement("p", { className: "jp-chat-command-name" }, command.name),
90
+ React.createElement("span", null, " - "),
91
+ React.createElement("p", { className: "jp-chat-command-description" }, command.description)));
92
+ },
93
+ // always show all options, since command providers should exclusively
94
+ // define what commands are added to the menu.
95
+ filterOptions: (commands) => commands,
96
+ value: null,
97
+ autoHighlight: true,
98
+ freeSolo: true,
99
+ disableClearable: true,
100
+ onChange,
101
+ onHighlightChange:
102
+ /**
103
+ * On highlight change: set `highlighted` to whether an option is
104
+ * highlighted by the user.
105
+ *
106
+ * This isn't called when an option is selected for some reason, so we
107
+ * need to call `setHighlighted(false)` in `onClose()`.
108
+ */
109
+ (_, highlightedOption) => {
110
+ setHighlighted(!!highlightedOption);
111
+ },
112
+ onClose:
113
+ /**
114
+ * On close: set `highlighted` to `false` and close the popup by
115
+ * setting `open` to `false`.
116
+ */
117
+ () => {
118
+ setHighlighted(false);
119
+ setOpen(false);
120
+ }
121
+ },
122
+ menu: {
123
+ open,
124
+ highlighted
125
+ }
126
+ };
127
+ }
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ import { IAttachmentOpenerRegistry } from './registry';
3
+ export declare const AttachmentOpenerContext: import("react").Context<IAttachmentOpenerRegistry | undefined>;
package/lib/context.js ADDED
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { createContext } from 'react';
6
+ export const AttachmentOpenerContext = createContext(undefined);
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './active-cell-manager';
2
2
  export * from './components';
3
3
  export * from './icons';
4
+ export * from './input-model';
4
5
  export * from './model';
5
6
  export * from './registry';
6
7
  export * from './selection-watcher';
@@ -8,3 +9,4 @@ export * from './types';
8
9
  export * from './widgets/chat-error';
9
10
  export * from './widgets/chat-sidebar';
10
11
  export * from './widgets/chat-widget';
12
+ export * from './chat-commands';
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  export * from './active-cell-manager';
6
6
  export * from './components';
7
7
  export * from './icons';
8
+ export * from './input-model';
8
9
  export * from './model';
9
10
  export * from './registry';
10
11
  export * from './selection-watcher';
@@ -12,3 +13,4 @@ export * from './types';
12
13
  export * from './widgets/chat-error';
13
14
  export * from './widgets/chat-sidebar';
14
15
  export * from './widgets/chat-widget';
16
+ export * from './chat-commands';
@@ -0,0 +1,221 @@
1
+ import { IDisposable } from '@lumino/disposable';
2
+ import { ISignal } from '@lumino/signaling';
3
+ import { IActiveCellManager } from './active-cell-manager';
4
+ import { ISelectionWatcher } from './selection-watcher';
5
+ import { IAttachment } from './types';
6
+ /**
7
+ * The chat input interface.
8
+ */
9
+ export interface IInputModel extends IDisposable {
10
+ /**
11
+ * The entire input value.
12
+ */
13
+ value: string;
14
+ /**
15
+ * A signal emitting when the value has changed.
16
+ */
17
+ readonly valueChanged: ISignal<IInputModel, string>;
18
+ /**
19
+ * The current cursor index.
20
+ * This refers to the index of the character in front of the cursor.
21
+ */
22
+ cursorIndex: number | null;
23
+ /**
24
+ * A signal emitting when the cursor position has changed.
25
+ */
26
+ readonly cursorIndexChanged: ISignal<IInputModel, number | null>;
27
+ /**
28
+ * The current word behind the user's cursor, space-separated.
29
+ */
30
+ readonly currentWord: string | null;
31
+ /**
32
+ * A signal emitting when the current word has changed.
33
+ */
34
+ readonly currentWordChanged: ISignal<IInputModel, string | null>;
35
+ /**
36
+ * Get the active cell manager.
37
+ */
38
+ readonly activeCellManager: IActiveCellManager | null;
39
+ /**
40
+ * Get the selection watcher.
41
+ */
42
+ readonly selectionWatcher: ISelectionWatcher | null;
43
+ /**
44
+ * The input configuration.
45
+ */
46
+ config: InputModel.IConfig;
47
+ /**
48
+ * A signal emitting when the messages list is updated.
49
+ */
50
+ readonly configChanged: ISignal<IInputModel, InputModel.IConfig>;
51
+ /**
52
+ * Function to request the focus on the input of the chat.
53
+ */
54
+ focus(): void;
55
+ /**
56
+ * A signal emitting when the focus is requested on the input.
57
+ */
58
+ readonly focusInputSignal?: ISignal<IInputModel, void>;
59
+ /**
60
+ * The attachments list.
61
+ */
62
+ readonly attachments: IAttachment[];
63
+ /**
64
+ * Add attachment to the next message to send.
65
+ */
66
+ addAttachment?(attachment: IAttachment): void;
67
+ /**
68
+ * Remove attachment to the next message to send.
69
+ */
70
+ removeAttachment?(attachment: IAttachment): void;
71
+ /**
72
+ * Clear the attachment list.
73
+ */
74
+ clearAttachments(): void;
75
+ /**
76
+ * A signal emitting when the attachment list has changed.
77
+ */
78
+ readonly attachmentsChanged?: ISignal<IInputModel, IAttachment[]>;
79
+ /**
80
+ * Replace the current word in the input with a new one.
81
+ */
82
+ replaceCurrentWord(newWord: string): void;
83
+ }
84
+ /**
85
+ * The input model.
86
+ */
87
+ export declare class InputModel implements IInputModel {
88
+ constructor(options: InputModel.IOptions);
89
+ /**
90
+ * The entire input value.
91
+ */
92
+ get value(): string;
93
+ set value(newInput: string);
94
+ /**
95
+ * A signal emitting when the value has changed.
96
+ */
97
+ get valueChanged(): ISignal<IInputModel, string>;
98
+ /**
99
+ * The cursor position in the input.
100
+ */
101
+ get cursorIndex(): number | null;
102
+ set cursorIndex(newIndex: number | null);
103
+ /**
104
+ * A signal emitting when the cursor position has changed.
105
+ */
106
+ get cursorIndexChanged(): ISignal<IInputModel, number | null>;
107
+ /**
108
+ * The current word behind the user's cursor, space-separated.
109
+ */
110
+ get currentWord(): string | null;
111
+ /**
112
+ * A signal emitting when the current word has changed.
113
+ */
114
+ get currentWordChanged(): ISignal<IInputModel, string | null>;
115
+ /**
116
+ * Get the active cell manager.
117
+ */
118
+ get activeCellManager(): IActiveCellManager | null;
119
+ /**
120
+ * Get the selection watcher.
121
+ */
122
+ get selectionWatcher(): ISelectionWatcher | null;
123
+ /**
124
+ * The input configuration.
125
+ */
126
+ get config(): InputModel.IConfig;
127
+ set config(value: Partial<InputModel.IConfig>);
128
+ /**
129
+ * A signal emitting when the configuration is updated.
130
+ */
131
+ get configChanged(): ISignal<IInputModel, InputModel.IConfig>;
132
+ /**
133
+ * Function to request the focus on the input of the chat.
134
+ */
135
+ focus(): void;
136
+ /**
137
+ * A signal emitting when the focus is requested on the input.
138
+ */
139
+ get focusInputSignal(): ISignal<IInputModel, void>;
140
+ /**
141
+ * The attachments list.
142
+ */
143
+ get attachments(): IAttachment[];
144
+ /**
145
+ * Add attachment to send with next message.
146
+ */
147
+ addAttachment: (attachment: IAttachment) => void;
148
+ /**
149
+ * Remove attachment to be sent.
150
+ */
151
+ removeAttachment: (attachment: IAttachment) => void;
152
+ /**
153
+ * Update attachments.
154
+ */
155
+ clearAttachments: () => void;
156
+ /**
157
+ * A signal emitting when the input attachments changed.
158
+ */
159
+ get attachmentsChanged(): ISignal<IInputModel, IAttachment[]>;
160
+ /**
161
+ * Replace the current word in the input with a new one.
162
+ */
163
+ replaceCurrentWord(newWord: string): void;
164
+ /**
165
+ * Dispose the input model.
166
+ */
167
+ dispose(): void;
168
+ /**
169
+ * Whether the input model is disposed.
170
+ */
171
+ get isDisposed(): boolean;
172
+ private _value;
173
+ private _cursorIndex;
174
+ private _currentWord;
175
+ private _attachments;
176
+ private _activeCellManager;
177
+ private _selectionWatcher;
178
+ private _config;
179
+ private _valueChanged;
180
+ private _cursorIndexChanged;
181
+ private _currentWordChanged;
182
+ private _configChanged;
183
+ private _focusInputSignal;
184
+ private _attachmentsChanged;
185
+ private _isDisposed;
186
+ }
187
+ export declare namespace InputModel {
188
+ interface IOptions {
189
+ /**
190
+ * The initial value of the input.
191
+ */
192
+ value?: string;
193
+ /**
194
+ * The initial attachments.
195
+ */
196
+ attachments?: IAttachment[];
197
+ /**
198
+ * The current cursor index.
199
+ * This refers to the index of the character in front of the cursor.
200
+ */
201
+ cursorIndex?: number;
202
+ /**
203
+ * The configuration for the input component.
204
+ */
205
+ config?: IConfig;
206
+ /**
207
+ * Active cell manager.
208
+ */
209
+ activeCellManager?: IActiveCellManager | null;
210
+ /**
211
+ * Selection watcher.
212
+ */
213
+ selectionWatcher?: ISelectionWatcher | null;
214
+ }
215
+ interface IConfig {
216
+ /**
217
+ * Whether to send a message via Shift-Enter instead of Enter.
218
+ */
219
+ sendWithShiftEnter?: boolean;
220
+ }
221
+ }