Skip to content

Commit

Permalink
Shortcut to focus on the chat input (#80)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
brichet authored Sep 12, 2024
1 parent 5271c9e commit f30fe91
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 7 deletions.
27 changes: 24 additions & 3 deletions packages/jupyter-chat/src/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { Send, Cancel } from '@mui/icons-material';
import clsx from 'clsx';
import { IChatModel } from '../model';
import { IAutocompletionRegistry } from '../registry';
import { AutocompleteCommand, IAutocompletionCommandsProps } from '../types';
import {
AutocompleteCommand,
IAutocompletionCommandsProps,
IConfig
} from '../types';

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

// store reference to the input element to enable focusing it easily
const inputRef = useRef<HTMLInputElement>();

useEffect(() => {
model.configChanged.connect((_, config) => {
const configChanged = (_: IChatModel, config: IConfig) => {
setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
});
};
model.configChanged.connect(configChanged);

const focusInputElement = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
model.focusInputSignal?.connect(focusInputElement);

return () => {
model.configChanged?.disconnect(configChanged);
model.focusInputSignal?.disconnect(focusInputElement);
};
}, [model]);

// The autocomplete commands options.
Expand Down Expand Up @@ -177,6 +197,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
multiline
onKeyDown={handleKeyDown}
placeholder="Start chatting"
inputRef={inputRef}
InputProps={{
...params.InputProps,
endAdornment: (
Expand Down
25 changes: 25 additions & 0 deletions packages/jupyter-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface IChatModel extends IDisposable {
*/
readonly viewportChanged?: ISignal<IChatModel, number[]>;

/**
* A signal emitting when the focus is requested on the input.
*/
readonly focusInputSignal?: ISignal<IChatModel, void>;

/**
* Send a message, to be defined depending on the chosen technology.
* Default to no-op.
Expand Down Expand Up @@ -139,6 +144,11 @@ export interface IChatModel extends IDisposable {
* @param count - the number of messages to delete.
*/
messagesDeleted(index: number, count: number): void;

/**
* Function to request the focus on the input of the chat.
*/
focusInput(): void;
}

/**
Expand Down Expand Up @@ -328,6 +338,13 @@ export class ChatModel implements IChatModel {
return this._viewportChanged;
}

/**
* A signal emitting when the focus is requested on the input.
*/
get focusInputSignal(): ISignal<IChatModel, void> {
return this._focusInputSignal;
}

/**
* Send a message, to be defined depending on the chosen technology.
* Default to no-op.
Expand Down Expand Up @@ -435,6 +452,13 @@ export class ChatModel implements IChatModel {
this._messagesUpdated.emit();
}

/**
* Function to request the focus on the input of the chat.
*/
focusInput(): void {
this._focusInputSignal.emit();
}

/**
* Add unread messages to the list.
* @param indexes - list of new indexes.
Expand Down Expand Up @@ -498,6 +522,7 @@ export class ChatModel implements IChatModel {
private _configChanged = new Signal<IChatModel, IConfig>(this);
private _unreadChanged = new Signal<IChatModel, number[]>(this);
private _viewportChanged = new Signal<IChatModel, number[]>(this);
private _focusInputSignal = new Signal<ChatModel, void>(this);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/jupyterlab-collaborative-chat/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,17 @@ export const CommandIDs = {
*/
openChat: 'collaborative-chat:open',
/**
* Move a main widget to the side panel
* Move a main widget to the side panel.
*/
moveToSide: 'collaborative-chat:moveToSide',
/**
* Mark as read.
*/
markAsRead: 'collaborative-chat:markAsRead'
markAsRead: 'collaborative-chat:markAsRead',
/**
* Focus the input of the current chat.
*/
focusInput: 'collaborative-chat:focusInput'
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/jupyterlab-collaborative-chat/src/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class CollaborativeChatPanel extends DocumentWidget<
* The model for the widget.
*/
get model(): CollaborativeChatModel {
return this.content.model as CollaborativeChatModel;
return this.context.model;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions python/jupyterlab-collaborative-chat/schema/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
}
]
},
"jupyter.lab.shortcuts": [
{
"command": "collaborative-chat:focusInput",
"keys": ["Accel Shift 1"],
"selector": "body",
"preventDefault": false
}
],
"properties": {},
"additionalProperties": false
}
19 changes: 19 additions & 0 deletions python/jupyterlab-collaborative-chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,25 @@ const chatCommands: JupyterFrontEndPlugin<void> = {
.catch(e =>
console.error('The command to open a chat is not initialized\n', e)
);

// The command to focus the input of the current chat widget.
commands.addCommand(CommandIDs.focusInput, {
caption: 'Focus the input of the current chat widget',
isEnabled: () => tracker.currentWidget !== null,
execute: async () => {
const widget = tracker.currentWidget;
// Ensure widget is a CollaborativeChatPanel and is in main area
if (
!widget ||
!(widget instanceof CollaborativeChatPanel) ||
!Array.from(app.shell.widgets('main')).includes(widget)
) {
return;
}
app.shell.activateById(widget.id);
widget.model.focusInput();
}
});
}
};

Expand Down
8 changes: 8 additions & 0 deletions python/jupyterlab-ws-chat/schema/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
"title": "Chat configuration",
"description": "Configuration for the chat panel",
"type": "object",
"jupyter.lab.shortcuts": [
{
"command": "websocket-chat:focusInput",
"keys": ["Accel Shift 1"],
"selector": "body",
"preventDefault": false
}
],
"properties": {
"sendWithShiftEnter": {
"description": "Whether to send a message via Shift-Enter instead of Enter.",
Expand Down
14 changes: 13 additions & 1 deletion python/jupyterlab-ws-chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const chat: JupyterFrontEndPlugin<void> = {
settingsRegistry: ISettingRegistry | null,
themeManager: IThemeManager | null
) => {
const { commands } = app;

// Create an active cell manager for code toolbar.
const activeCellManager = new ActiveCellManager({
tracker: notebookTracker,
Expand Down Expand Up @@ -146,7 +148,17 @@ const chat: JupyterFrontEndPlugin<void> = {
restorer.add(chatWidget as ReactWidget, 'jupyter-chat');
}

console.log('Chat extension initialized');
// The command to focus the input of the chat widget.
commands.addCommand('websocket-chat:focusInput', {
caption: 'Focus the input of the chat widget',
isEnabled: () => chatWidget !== null,
execute: async () => {
if (chatWidget !== null) {
app.shell.activateById(chatWidget.id);
chatHandler.focusInput();
}
}
});
}
};

Expand Down

0 comments on commit f30fe91

Please sign in to comment.