Skip to content

Commit f30fe91

Browse files
authored
Shortcut to focus on the chat input (#80)
* Add an function in the chat model to focus on the input * Add a shortcut to focus on the current collaborative chat input * Add a shortcut to focus on the websocket chat input
1 parent 5271c9e commit f30fe91

File tree

8 files changed

+104
-7
lines changed

8 files changed

+104
-7
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import { Send, Cancel } from '@mui/icons-material';
1818
import clsx from 'clsx';
1919
import { IChatModel } from '../model';
2020
import { IAutocompletionRegistry } from '../registry';
21-
import { AutocompleteCommand, IAutocompletionCommandsProps } from '../types';
21+
import {
22+
AutocompleteCommand,
23+
IAutocompletionCommandsProps,
24+
IConfig
25+
} from '../types';
2226

2327
const INPUT_BOX_CLASS = 'jp-chat-input-container';
2428
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
@@ -32,10 +36,26 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
3236
model.config.sendWithShiftEnter ?? false
3337
);
3438

39+
// store reference to the input element to enable focusing it easily
40+
const inputRef = useRef<HTMLInputElement>();
41+
3542
useEffect(() => {
36-
model.configChanged.connect((_, config) => {
43+
const configChanged = (_: IChatModel, config: IConfig) => {
3744
setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
38-
});
45+
};
46+
model.configChanged.connect(configChanged);
47+
48+
const focusInputElement = () => {
49+
if (inputRef.current) {
50+
inputRef.current.focus();
51+
}
52+
};
53+
model.focusInputSignal?.connect(focusInputElement);
54+
55+
return () => {
56+
model.configChanged?.disconnect(configChanged);
57+
model.focusInputSignal?.disconnect(focusInputElement);
58+
};
3959
}, [model]);
4060

4161
// The autocomplete commands options.
@@ -177,6 +197,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
177197
multiline
178198
onKeyDown={handleKeyDown}
179199
placeholder="Start chatting"
200+
inputRef={inputRef}
180201
InputProps={{
181202
...params.InputProps,
182203
endAdornment: (

packages/jupyter-chat/src/model.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export interface IChatModel extends IDisposable {
7575
*/
7676
readonly viewportChanged?: ISignal<IChatModel, number[]>;
7777

78+
/**
79+
* A signal emitting when the focus is requested on the input.
80+
*/
81+
readonly focusInputSignal?: ISignal<IChatModel, void>;
82+
7883
/**
7984
* Send a message, to be defined depending on the chosen technology.
8085
* Default to no-op.
@@ -139,6 +144,11 @@ export interface IChatModel extends IDisposable {
139144
* @param count - the number of messages to delete.
140145
*/
141146
messagesDeleted(index: number, count: number): void;
147+
148+
/**
149+
* Function to request the focus on the input of the chat.
150+
*/
151+
focusInput(): void;
142152
}
143153

144154
/**
@@ -328,6 +338,13 @@ export class ChatModel implements IChatModel {
328338
return this._viewportChanged;
329339
}
330340

341+
/**
342+
* A signal emitting when the focus is requested on the input.
343+
*/
344+
get focusInputSignal(): ISignal<IChatModel, void> {
345+
return this._focusInputSignal;
346+
}
347+
331348
/**
332349
* Send a message, to be defined depending on the chosen technology.
333350
* Default to no-op.
@@ -435,6 +452,13 @@ export class ChatModel implements IChatModel {
435452
this._messagesUpdated.emit();
436453
}
437454

455+
/**
456+
* Function to request the focus on the input of the chat.
457+
*/
458+
focusInput(): void {
459+
this._focusInputSignal.emit();
460+
}
461+
438462
/**
439463
* Add unread messages to the list.
440464
* @param indexes - list of new indexes.
@@ -498,6 +522,7 @@ export class ChatModel implements IChatModel {
498522
private _configChanged = new Signal<IChatModel, IConfig>(this);
499523
private _unreadChanged = new Signal<IChatModel, number[]>(this);
500524
private _viewportChanged = new Signal<IChatModel, number[]>(this);
525+
private _focusInputSignal = new Signal<ChatModel, void>(this);
501526
}
502527

503528
/**

packages/jupyterlab-collaborative-chat/src/token.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,17 @@ export const CommandIDs = {
7878
*/
7979
openChat: 'collaborative-chat:open',
8080
/**
81-
* Move a main widget to the side panel
81+
* Move a main widget to the side panel.
8282
*/
8383
moveToSide: 'collaborative-chat:moveToSide',
8484
/**
8585
* Mark as read.
8686
*/
87-
markAsRead: 'collaborative-chat:markAsRead'
87+
markAsRead: 'collaborative-chat:markAsRead',
88+
/**
89+
* Focus the input of the current chat.
90+
*/
91+
focusInput: 'collaborative-chat:focusInput'
8892
};
8993

9094
/**

packages/jupyterlab-collaborative-chat/src/widget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class CollaborativeChatPanel extends DocumentWidget<
7373
* The model for the widget.
7474
*/
7575
get model(): CollaborativeChatModel {
76-
return this.content.model as CollaborativeChatModel;
76+
return this.context.model;
7777
}
7878

7979
/**

python/jupyterlab-collaborative-chat/schema/commands.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
}
2424
]
2525
},
26+
"jupyter.lab.shortcuts": [
27+
{
28+
"command": "collaborative-chat:focusInput",
29+
"keys": ["Accel Shift 1"],
30+
"selector": "body",
31+
"preventDefault": false
32+
}
33+
],
2634
"properties": {},
2735
"additionalProperties": false
2836
}

python/jupyterlab-collaborative-chat/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,25 @@ const chatCommands: JupyterFrontEndPlugin<void> = {
512512
.catch(e =>
513513
console.error('The command to open a chat is not initialized\n', e)
514514
);
515+
516+
// The command to focus the input of the current chat widget.
517+
commands.addCommand(CommandIDs.focusInput, {
518+
caption: 'Focus the input of the current chat widget',
519+
isEnabled: () => tracker.currentWidget !== null,
520+
execute: async () => {
521+
const widget = tracker.currentWidget;
522+
// Ensure widget is a CollaborativeChatPanel and is in main area
523+
if (
524+
!widget ||
525+
!(widget instanceof CollaborativeChatPanel) ||
526+
!Array.from(app.shell.widgets('main')).includes(widget)
527+
) {
528+
return;
529+
}
530+
app.shell.activateById(widget.id);
531+
widget.model.focusInput();
532+
}
533+
});
515534
}
516535
};
517536

python/jupyterlab-ws-chat/schema/chat.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"title": "Chat configuration",
33
"description": "Configuration for the chat panel",
44
"type": "object",
5+
"jupyter.lab.shortcuts": [
6+
{
7+
"command": "websocket-chat:focusInput",
8+
"keys": ["Accel Shift 1"],
9+
"selector": "body",
10+
"preventDefault": false
11+
}
12+
],
513
"properties": {
614
"sendWithShiftEnter": {
715
"description": "Whether to send a message via Shift-Enter instead of Enter.",

python/jupyterlab-ws-chat/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ const chat: JupyterFrontEndPlugin<void> = {
6464
settingsRegistry: ISettingRegistry | null,
6565
themeManager: IThemeManager | null
6666
) => {
67+
const { commands } = app;
68+
6769
// Create an active cell manager for code toolbar.
6870
const activeCellManager = new ActiveCellManager({
6971
tracker: notebookTracker,
@@ -146,7 +148,17 @@ const chat: JupyterFrontEndPlugin<void> = {
146148
restorer.add(chatWidget as ReactWidget, 'jupyter-chat');
147149
}
148150

149-
console.log('Chat extension initialized');
151+
// The command to focus the input of the chat widget.
152+
commands.addCommand('websocket-chat:focusInput', {
153+
caption: 'Focus the input of the chat widget',
154+
isEnabled: () => chatWidget !== null,
155+
execute: async () => {
156+
if (chatWidget !== null) {
157+
app.shell.activateById(chatWidget.id);
158+
chatHandler.focusInput();
159+
}
160+
}
161+
});
150162
}
151163
};
152164

0 commit comments

Comments
 (0)