Skip to content

Commit e182c03

Browse files
authored
Input toolbar registry (#198)
* Input toolbar buttons only take the input model as prop * Add an input toolbar registry that can be provided by extensions * Fix doc example * Update interface name * Fix the custom toolbar sent to the chat component * Add a toolbar registry with a token in jupyterlab-chat, to be able to replace it with an extension * Add a simple test to modify the input toolbar of main area chat * lint * Removal of an unnecessary method in the input toolbar register * Fix the test
1 parent 11aadc0 commit e182c03

File tree

24 files changed

+643
-262
lines changed

24 files changed

+643
-262
lines changed

docs/jupyter-chat-example/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
7777
shell: app.shell
7878
});
7979

80-
const model = new MyChatModel({ activeCellManager, selectionWatcher });
80+
const model = new MyChatModel({
81+
activeCellManager,
82+
selectionWatcher,
83+
documentManager: filebrowser?.model.manager
84+
});
8185

8286
// Update the settings when they change.
8387
function loadSetting(setting: ISettingRegistry.ISettings): void {
@@ -122,7 +126,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
122126
model,
123127
rmRegistry,
124128
themeManager,
125-
documentManager: filebrowser?.model.manager,
126129
attachmentOpenerRegistry
127130
});
128131
app.shell.add(panel, 'left');

packages/jupyter-chat/src/components/chat-input.tsx

Lines changed: 40 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* Distributed under the terms of the Modified BSD License.
44
*/
55

6-
import { IDocumentManager } from '@jupyterlab/docmanager';
76
import {
87
Autocomplete,
98
AutocompleteInputChangeReason,
@@ -17,16 +16,20 @@ import clsx from 'clsx';
1716
import React, { useEffect, useRef, useState } from 'react';
1817

1918
import { AttachmentPreviewList } from './attachments';
20-
import { AttachButton, CancelButton, SendButton } from './input';
19+
import {
20+
IInputToolbarRegistry,
21+
InputToolbarRegistry,
22+
useChatCommands
23+
} from './input';
2124
import { IInputModel, InputModel } from '../input-model';
22-
import { IAttachment, Selection } from '../types';
23-
import { useChatCommands } from './input/use-chat-commands';
25+
import { IAttachment } from '../types';
2426
import { IChatCommandRegistry } from '../chat-commands';
2527

2628
const INPUT_BOX_CLASS = 'jp-chat-input-container';
29+
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
2730

2831
export function ChatInput(props: ChatInput.IProps): JSX.Element {
29-
const { documentManager, model } = props;
32+
const { model, toolbarRegistry } = props;
3033
const [input, setInput] = useState<string>(model.value);
3134
const inputRef = useRef<HTMLInputElement>();
3235

@@ -38,14 +41,16 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
3841
const [attachments, setAttachments] = useState<IAttachment[]>(
3942
model.attachments
4043
);
44+
const [toolbarElements, setToolbarElements] = useState<
45+
InputToolbarRegistry.IToolbarItem[]
46+
>([]);
4147

42-
// Display the include selection menu if it is not explicitly hidden, and if at least
43-
// one of the tool to check for text or cell selection is enabled.
44-
let hideIncludeSelection = props.hideIncludeSelection ?? false;
45-
if (model.activeCellManager === null && model.selectionWatcher === null) {
46-
hideIncludeSelection = true;
47-
}
48-
48+
/**
49+
* Handle the changes on the model that affect the input.
50+
* - focus requested
51+
* - config changed
52+
* - attachments changed
53+
*/
4954
useEffect(() => {
5055
const inputChanged = (_: IInputModel, value: string) => {
5156
setInput(value);
@@ -76,6 +81,22 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
7681
};
7782
}, [model]);
7883

84+
/**
85+
* Handle the changes in the toolbar items.
86+
*/
87+
useEffect(() => {
88+
const updateToolbar = () => {
89+
setToolbarElements(toolbarRegistry.getItems());
90+
};
91+
92+
toolbarRegistry.itemsChanged.connect(updateToolbar);
93+
updateToolbar();
94+
95+
return () => {
96+
toolbarRegistry.itemsChanged.disconnect(updateToolbar);
97+
};
98+
}, [toolbarRegistry]);
99+
79100
const inputExists = !!input.trim();
80101

81102
/**
@@ -136,38 +157,12 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
136157
(sendWithShiftEnter && event.shiftKey) ||
137158
(!sendWithShiftEnter && !event.shiftKey)
138159
) {
139-
onSend();
160+
model.send(input);
140161
event.stopPropagation();
141162
event.preventDefault();
142163
}
143164
}
144165

145-
/**
146-
* Triggered when sending the message.
147-
*
148-
* Add code block if cell or text is selected.
149-
*/
150-
function onSend(selection?: Selection) {
151-
let content = input;
152-
if (selection) {
153-
content += `
154-
155-
\`\`\`
156-
${selection.source}
157-
\`\`\`
158-
`;
159-
}
160-
props.onSend(content);
161-
model.value = '';
162-
}
163-
164-
/**
165-
* Triggered when cancelling edition.
166-
*/
167-
function onCancel() {
168-
props.onCancel?.();
169-
}
170-
171166
// Set the helper text based on whether Shift+Enter is used for sending.
172167
const helperText = sendWithShiftEnter ? (
173168
<span>
@@ -221,22 +216,10 @@ ${selection.source}
221216
InputProps={{
222217
...params.InputProps,
223218
endAdornment: (
224-
<InputAdornment position="end">
225-
{documentManager && model.addAttachment && (
226-
<AttachButton
227-
documentManager={documentManager}
228-
onAttach={model.addAttachment}
229-
/>
230-
)}
231-
{props.onCancel && <CancelButton onCancel={onCancel} />}
232-
<SendButton
233-
model={model}
234-
sendWithShiftEnter={sendWithShiftEnter}
235-
inputExists={inputExists || attachments.length > 0}
236-
onSend={onSend}
237-
hideIncludeSelection={hideIncludeSelection}
238-
hasButtonOnLeft={!!props.onCancel}
239-
/>
219+
<InputAdornment position="end" className={INPUT_TOOLBAR_CLASS}>
220+
{toolbarElements.map(item => (
221+
<item.element model={model} />
222+
))}
240223
</InputAdornment>
241224
)
242225
}}
@@ -277,25 +260,17 @@ export namespace ChatInput {
277260
*/
278261
model: IInputModel;
279262
/**
280-
* The function to be called to send the message.
263+
* The toolbar registry.
281264
*/
282-
onSend: (input: string) => unknown;
265+
toolbarRegistry: IInputToolbarRegistry;
283266
/**
284267
* The function to be called to cancel editing.
285268
*/
286269
onCancel?: () => unknown;
287-
/**
288-
* Whether to allow or not including selection.
289-
*/
290-
hideIncludeSelection?: boolean;
291270
/**
292271
* Custom mui/material styles.
293272
*/
294273
sx?: SxProps<Theme>;
295-
/**
296-
* The document manager.
297-
*/
298-
documentManager?: IDocumentManager;
299274
/**
300275
* Chat command registry.
301276
*/

packages/jupyter-chat/src/components/chat-messages.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

66
import { Button } from '@jupyter/react-components';
7-
import { IDocumentManager } from '@jupyterlab/docmanager';
87
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
98
import {
109
LabIcon,
@@ -19,6 +18,7 @@ import React, { useEffect, useState, useRef, forwardRef } from 'react';
1918

2019
import { AttachmentPreviewList } from './attachments';
2120
import { ChatInput } from './chat-input';
21+
import { IInputToolbarRegistry } from './input';
2222
import { MarkdownRenderer } from './markdown-renderer';
2323
import { ScrollContainer } from './scroll-container';
2424
import { IChatCommandRegistry } from '../chat-commands';
@@ -41,10 +41,22 @@ const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
4141
* The base components props.
4242
*/
4343
type BaseMessageProps = {
44+
/**
45+
* The mime renderer registry.
46+
*/
4447
rmRegistry: IRenderMimeRegistry;
48+
/**
49+
* The chat model.
50+
*/
4551
model: IChatModel;
52+
/**
53+
* The chat commands registry.
54+
*/
4655
chatCommandRegistry?: IChatCommandRegistry;
47-
documentManager?: IDocumentManager;
56+
/**
57+
* The input toolbar registry.
58+
*/
59+
inputToolbarRegistry: IInputToolbarRegistry;
4860
};
4961

5062
/**
@@ -200,8 +212,10 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
200212
* The message header props.
201213
*/
202214
type ChatMessageHeaderProps = {
215+
/**
216+
* The chat message.
217+
*/
203218
message: IChatMessage;
204-
sx?: SxProps<Theme>;
205219
};
206220

207221
/**
@@ -262,8 +276,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
262276
'& > :not(:last-child)': {
263277
marginRight: 3
264278
},
265-
marginBottom: message.stacked ? '0px' : '12px',
266-
...props.sx
279+
marginBottom: message.stacked ? '0px' : '12px'
267280
}}
268281
>
269282
{avatar}
@@ -364,13 +377,15 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
364377
if (edit && canEdit) {
365378
setInputModel(
366379
new InputModel({
380+
onSend: (input: string, model?: IInputModel) =>
381+
updateMessage(message.id, input, model),
382+
onCancel: () => cancelEdition(),
367383
value: message.body,
368-
activeCellManager: model.activeCellManager,
369-
selectionWatcher: model.selectionWatcher,
370384
config: {
371385
sendWithShiftEnter: model.config.sendWithShiftEnter
372386
},
373-
attachments: message.attachments
387+
attachments: message.attachments,
388+
documentManager: model.documentManager
374389
})
375390
);
376391
} else {
@@ -384,14 +399,18 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
384399
};
385400

386401
// Update the content of the message.
387-
const updateMessage = (id: string, input: string): void => {
388-
if (!canEdit) {
402+
const updateMessage = (
403+
id: string,
404+
input: string,
405+
inputModel?: IInputModel
406+
): void => {
407+
if (!canEdit || !inputModel) {
389408
return;
390409
}
391410
// Update the message
392411
const updatedMessage = { ...message };
393412
updatedMessage.body = input;
394-
updatedMessage.attachments = inputModel?.attachments;
413+
updatedMessage.attachments = inputModel.attachments;
395414
model.updateMessage!(id, updatedMessage);
396415
setEdit(false);
397416
};
@@ -411,12 +430,10 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
411430
<div ref={ref} data-index={props.index}>
412431
{edit && canEdit && inputModel ? (
413432
<ChatInput
414-
onSend={(input: string) => updateMessage(message.id, input)}
415433
onCancel={() => cancelEdition()}
416434
model={inputModel}
417-
hideIncludeSelection={true}
418435
chatCommandRegistry={props.chatCommandRegistry}
419-
documentManager={props.documentManager}
436+
toolbarRegistry={props.inputToolbarRegistry}
420437
/>
421438
) : (
422439
<MarkdownRenderer

0 commit comments

Comments
 (0)