Skip to content

Commit b73f5a8

Browse files
Send with selection (#82)
* Rename addMessage to sendMessage in the chat model, for concistency * Add the selection watcher object, and modify the send button to allow adding selection * Add the selection watcher to the collaborative chat (work only with the side panel widget) * Add a selection watcher to the websocket chat * Automatic application of license header * Change the state of include button on signal * Hide the 'include selection' menu when editing a message or if the tools are not available * Invert the logic to hide the 'include selection' menu * Adopt the same button style for the cancel button * Automatic application of license header * Handle the selected text if the chat is a main area widget * Add the ability to replace the current selection on visible editor * Fixes ui-tests * lint * Fix the selection watcher for a new notebook file * Add tests * update snapshots --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent ca4e37b commit b73f5a8

File tree

33 files changed

+1097
-112
lines changed

33 files changed

+1097
-112
lines changed

docs/source/developers/developing_extensions/extension-providing-chat.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ A model is provided by the package, and already includes all the required method
4343
interact with the UI part of the chat.
4444

4545
The extension has to provide a class extending the `@jupyter/chat` model, implementing
46-
at least the `addMessage()` method.
46+
at least the `sendMessage()` method.
4747

4848
This method is called when a user sends a message using the input of the chat. It should
4949
contain the code that will dispatch the message through the messaging technology.
@@ -55,7 +55,7 @@ the message list.
5555
import { ChatModel, IChatMessage, INewMessage } from '@jupyter/chat';
5656

5757
class MyModel extends ChatModel {
58-
addMessage(
58+
sendMessage(
5959
newMessage: INewMessage
6060
): Promise<boolean | void> | boolean | void {
6161
console.log(`New Message:\n${newMessage.body}`);
@@ -88,7 +88,7 @@ When a user sends a message, it is logged in the console and added to the messag
8888
```{tip}
8989
In this example, no messages are sent to other potential users.
9090
91-
An exchange system must be included and use the `addMessage()` and `messageAdded()`
91+
An exchange system must be included and use the `sendMessage()` and `messageAdded()`
9292
methods to correctly manage message transmission and reception.
9393
```
9494

@@ -107,7 +107,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
107107
import { UUID } from '@lumino/coreutils';
108108

109109
class MyModel extends ChatModel {
110-
addMessage(
110+
sendMessage(
111111
newMessage: INewMessage
112112
): Promise<boolean | void> | boolean | void {
113113
console.log(`New Message:\n${newMessage.body}`);

packages/jupyter-chat/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@jupyter/react-components": "^0.15.2",
5858
"@jupyterlab/application": "^4.2.0",
5959
"@jupyterlab/apputils": "^4.3.0",
60+
"@jupyterlab/fileeditor": "^4.2.0",
6061
"@jupyterlab/notebook": "^4.2.0",
6162
"@jupyterlab/rendermime": "^4.2.0",
6263
"@jupyterlab/ui-components": "^4.2.0",

packages/jupyter-chat/src/active-cell-manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type CellWithErrorContent = {
2525
};
2626
};
2727

28+
/**
29+
* The active cell interface.
30+
*/
2831
export interface IActiveCellManager {
2932
/**
3033
* Whether the notebook is available and an active cell exists.

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

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ import React, { useEffect, useRef, useState } from 'react';
88
import {
99
Autocomplete,
1010
Box,
11-
IconButton,
1211
InputAdornment,
1312
SxProps,
1413
TextField,
1514
Theme
1615
} from '@mui/material';
17-
import { Send, Cancel } from '@mui/icons-material';
1816
import clsx from 'clsx';
17+
18+
import { CancelButton } from './input/cancel-button';
19+
import { SendButton } from './input/send-button';
1920
import { IChatModel } from '../model';
2021
import { IAutocompletionRegistry } from '../registry';
2122
import {
2223
AutocompleteCommand,
2324
IAutocompletionCommandsProps,
24-
IConfig
25+
IConfig,
26+
Selection
2527
} from '../types';
2628

2729
const INPUT_BOX_CLASS = 'jp-chat-input-container';
28-
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
29-
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
3030

3131
export function ChatInput(props: ChatInput.IProps): JSX.Element {
3232
const { autocompletionName, autocompletionRegistry, model } = props;
@@ -36,6 +36,13 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
3636
model.config.sendWithShiftEnter ?? false
3737
);
3838

39+
// Display the include selection menu if it is not explicitly hidden, and if at least
40+
// one of the tool to check for text or cell selection is enabled.
41+
let hideIncludeSelection = props.hideIncludeSelection ?? false;
42+
if (model.activeCellManager === null && model.selectionWatcher === null) {
43+
hideIncludeSelection = true;
44+
}
45+
3946
// store reference to the input element to enable focusing it easily
4047
const inputRef = useRef<HTMLInputElement>();
4148

@@ -138,10 +145,21 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
138145

139146
/**
140147
* Triggered when sending the message.
148+
*
149+
* Add code block if cell or text is selected.
141150
*/
142-
function onSend() {
151+
function onSend(selection?: Selection) {
152+
let content = input;
153+
if (selection) {
154+
content += `
155+
156+
\`\`\`
157+
${selection.source}
158+
\`\`\`
159+
`;
160+
}
161+
props.onSend(content);
143162
setInput('');
144-
props.onSend(input);
145163
}
146164

147165
/**
@@ -203,30 +221,19 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
203221
endAdornment: (
204222
<InputAdornment position="end">
205223
{props.onCancel && (
206-
<IconButton
207-
size="small"
208-
color="primary"
209-
onClick={onCancel}
210-
title={'Cancel edition'}
211-
className={clsx(CANCEL_BUTTON_CLASS)}
212-
>
213-
<Cancel />
214-
</IconButton>
224+
<CancelButton
225+
inputExists={input.length > 0}
226+
onCancel={onCancel}
227+
/>
215228
)}
216-
<IconButton
217-
size="small"
218-
color="primary"
219-
onClick={onSend}
220-
disabled={
221-
props.onCancel
222-
? input === props.value
223-
: !input.trim().length
224-
}
225-
title={`Send message ${sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
226-
className={clsx(SEND_BUTTON_CLASS)}
227-
>
228-
<Send />
229-
</IconButton>
229+
<SendButton
230+
model={model}
231+
sendWithShiftEnter={sendWithShiftEnter}
232+
inputExists={input.length > 0}
233+
onSend={onSend}
234+
hideIncludeSelection={hideIncludeSelection}
235+
hasButtonOnLeft={!!props.onCancel}
236+
/>
230237
</InputAdornment>
231238
)
232239
}}
@@ -294,6 +301,10 @@ export namespace ChatInput {
294301
* The function to be called to cancel editing.
295302
*/
296303
onCancel?: () => unknown;
304+
/**
305+
* Whether to allow or not including selection.
306+
*/
307+
hideIncludeSelection?: boolean;
297308
/**
298309
* Custom mui/material styles.
299310
*/

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
393393
onSend={(input: string) => updateMessage(message.id, input)}
394394
onCancel={() => cancelEdition()}
395395
model={model}
396+
hideIncludeSelection={true}
396397
/>
397398
) : (
398399
<RendermimeMarkdown

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
2727
// handled by the listeners registered in the effect hooks above.
2828
const onSend = async (input: string) => {
2929
// send message to backend
30-
model.addMessage({ body: input });
30+
model.sendMessage({ body: input });
3131
};
3232

3333
return (

packages/jupyter-chat/src/components/code-blocks/code-toolbar.tsx

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
1212
import { IActiveCellManager } from '../../active-cell-manager';
1313
import { replaceCellIcon } from '../../icons';
1414
import { IChatModel } from '../../model';
15+
import { ISelectionWatcher } from '../../selection-watcher';
1516

1617
const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
1718
const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
@@ -34,25 +35,41 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
3435
);
3536

3637
const activeCellManager = model.activeCellManager;
38+
const selectionWatcher = model.selectionWatcher;
3739

3840
const [toolbarBtnProps, setToolbarBtnProps] = useState<ToolbarButtonProps>({
39-
content: content,
40-
activeCellManager: activeCellManager,
41-
activeCellAvailable: activeCellManager?.available ?? false
41+
content,
42+
activeCellManager,
43+
selectionWatcher,
44+
activeCellAvailable: !!activeCellManager?.available,
45+
selectionExists: !!selectionWatcher?.selection
4246
});
4347

4448
useEffect(() => {
45-
activeCellManager?.availabilityChanged.connect(() => {
49+
const toggleToolbar = () => {
50+
setToolbarEnable(model.config.enableCodeToolbar ?? true);
51+
};
52+
53+
const selectionStatusChange = () => {
4654
setToolbarBtnProps({
4755
content,
48-
activeCellManager: activeCellManager,
49-
activeCellAvailable: activeCellManager.available
56+
activeCellManager,
57+
selectionWatcher,
58+
activeCellAvailable: !!activeCellManager?.available,
59+
selectionExists: !!selectionWatcher?.selection
5060
});
51-
});
52-
53-
model.configChanged.connect((_, config) => {
54-
setToolbarEnable(config.enableCodeToolbar ?? true);
55-
});
61+
};
62+
63+
activeCellManager?.availabilityChanged.connect(selectionStatusChange);
64+
selectionWatcher?.selectionChanged.connect(selectionStatusChange);
65+
model.configChanged.connect(toggleToolbar);
66+
67+
selectionStatusChange();
68+
return () => {
69+
activeCellManager?.availabilityChanged.disconnect(selectionStatusChange);
70+
selectionWatcher?.selectionChanged.disconnect(selectionStatusChange);
71+
model.configChanged.disconnect(toggleToolbar);
72+
};
5673
}, [model]);
5774

5875
return activeCellManager === null || !toolbarEnable ? (
@@ -86,8 +103,10 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
86103

87104
type ToolbarButtonProps = {
88105
content: string;
89-
activeCellAvailable?: boolean;
90106
activeCellManager: IActiveCellManager | null;
107+
activeCellAvailable?: boolean;
108+
selectionWatcher: ISelectionWatcher | null;
109+
selectionExists?: boolean;
91110
className?: string;
92111
};
93112

@@ -126,16 +145,35 @@ function InsertBelowButton(props: ToolbarButtonProps) {
126145
}
127146

128147
function ReplaceButton(props: ToolbarButtonProps) {
129-
const tooltip = props.activeCellAvailable
130-
? 'Replace active cell'
131-
: 'Replace active cell (no active cell)';
148+
const tooltip = props.selectionExists
149+
? `Replace selection (${props.selectionWatcher?.selection?.numLines} line(s))`
150+
: props.activeCellAvailable
151+
? 'Replace selection (active cell)'
152+
: 'Replace selection (no selection)';
153+
154+
const disabled = !props.activeCellAvailable && !props.selectionExists;
155+
156+
const replace = () => {
157+
if (props.selectionExists) {
158+
const selection = props.selectionWatcher?.selection;
159+
if (!selection) {
160+
return;
161+
}
162+
props.selectionWatcher?.replaceSelection({
163+
...selection,
164+
text: props.content
165+
});
166+
} else if (props.activeCellAvailable) {
167+
props.activeCellManager?.replace(props.content);
168+
}
169+
};
132170

133171
return (
134172
<TooltippedIconButton
135173
className={props.className}
136174
tooltip={tooltip}
137-
disabled={!props.activeCellAvailable}
138-
onClick={() => props.activeCellManager?.replace(props.content)}
175+
disabled={disabled}
176+
onClick={replace}
139177
>
140178
<replaceCellIcon.react height="16px" width="16px" />
141179
</TooltippedIconButton>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
import CancelIcon from '@mui/icons-material/Cancel';
7+
import React from 'react';
8+
import { TooltippedButton } from '../mui-extras/tooltipped-button';
9+
10+
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
11+
12+
/**
13+
* The cancel button props.
14+
*/
15+
export type CancelButtonProps = {
16+
inputExists: boolean;
17+
onCancel: () => void;
18+
};
19+
20+
/**
21+
* The cancel button.
22+
*/
23+
export function CancelButton(props: CancelButtonProps): JSX.Element {
24+
const tooltip = 'Cancel edition';
25+
const disabled = !props.inputExists;
26+
return (
27+
<TooltippedButton
28+
onClick={props.onCancel}
29+
disabled={disabled}
30+
tooltip={tooltip}
31+
buttonProps={{
32+
size: 'small',
33+
variant: 'contained',
34+
title: tooltip,
35+
className: CANCEL_BUTTON_CLASS
36+
}}
37+
sx={{
38+
minWidth: 'unset',
39+
padding: '4px',
40+
borderRadius: '2px 0px 0px 2px',
41+
marginRight: '1px'
42+
}}
43+
>
44+
<CancelIcon />
45+
</TooltippedButton>
46+
);
47+
}

0 commit comments

Comments
 (0)