@jupyter/chat 0.7.1 → 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.
- package/lib/active-cell-manager.js +1 -4
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +6 -0
- package/lib/chat-commands/registry.d.ts +28 -0
- package/lib/chat-commands/registry.js +29 -0
- package/lib/chat-commands/types.d.ts +51 -0
- package/lib/chat-commands/types.js +5 -0
- package/lib/components/attachments.d.ts +23 -0
- package/lib/components/attachments.js +44 -0
- package/lib/components/chat-input.d.ts +8 -11
- package/lib/components/chat-input.js +70 -95
- package/lib/components/chat-messages.d.ts +4 -0
- package/lib/components/chat-messages.js +27 -1
- package/lib/components/chat.d.ts +11 -5
- package/lib/components/chat.js +7 -8
- package/lib/components/input/attach-button.d.ts +14 -0
- package/lib/components/input/attach-button.js +45 -0
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/send-button.d.ts +2 -2
- package/lib/components/input/use-chat-commands.d.ts +19 -0
- package/lib/components/input/use-chat-commands.js +127 -0
- package/lib/context.d.ts +3 -0
- package/lib/context.js +6 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/input-model.d.ts +221 -0
- package/lib/input-model.js +217 -0
- package/lib/model.d.ts +10 -25
- package/lib/model.js +15 -17
- package/lib/registry.d.ts +11 -64
- package/lib/registry.js +4 -72
- package/lib/types.d.ts +19 -38
- package/lib/widgets/chat-widget.js +2 -1
- package/package.json +3 -114
- package/src/active-cell-manager.ts +0 -3
- package/src/chat-commands/index.ts +7 -0
- package/src/chat-commands/registry.ts +60 -0
- package/src/chat-commands/types.ts +67 -0
- package/src/components/attachments.tsx +91 -0
- package/src/components/chat-input.tsx +97 -124
- package/src/components/chat-messages.tsx +36 -3
- package/src/components/chat.tsx +28 -19
- package/src/components/input/attach-button.tsx +68 -0
- package/src/components/input/cancel-button.tsx +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/send-button.tsx +2 -2
- package/src/components/input/use-chat-commands.tsx +186 -0
- package/src/context.ts +10 -0
- package/src/index.ts +2 -0
- package/src/input-model.ts +406 -0
- package/src/model.ts +24 -35
- package/src/registry.ts +14 -108
- package/src/types.ts +19 -39
- package/src/widgets/chat-widget.tsx +2 -1
- package/style/chat.css +27 -9
- package/style/icons/include-selection.svg +3 -1
- package/style/icons/read.svg +8 -6
- package/style/icons/replace-cell.svg +10 -6
|
@@ -34,13 +34,10 @@ export class ActiveCellManager {
|
|
|
34
34
|
(_a = this._activeCell) === null || _a === void 0 ? void 0 : _a.model.stateChanged.disconnect(this._cellStateChange);
|
|
35
35
|
this._activeCell = activeCell;
|
|
36
36
|
activeCell === null || activeCell === void 0 ? void 0 : activeCell.ready.then(() => {
|
|
37
|
-
var _a
|
|
37
|
+
var _a;
|
|
38
38
|
(_a = this._activeCell) === null || _a === void 0 ? void 0 : _a.model.stateChanged.connect(this._cellStateChange);
|
|
39
39
|
this._available = !!this._activeCell && this._notebookVisible;
|
|
40
40
|
this._availabilityChanged.emit(this._available);
|
|
41
|
-
(_b = this._activeCell) === null || _b === void 0 ? void 0 : _b.disposed.connect(() => {
|
|
42
|
-
this._activeCell = null;
|
|
43
|
-
});
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
43
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Token } from '@lumino/coreutils';
|
|
2
|
+
import { ChatCommand, IChatCommandProvider } from './types';
|
|
3
|
+
import { IInputModel } from '../input-model';
|
|
4
|
+
/**
|
|
5
|
+
* Interface of a chat command registry, which tracks a list of chat command
|
|
6
|
+
* providers. Providers provide a list of commands given a user's partial input,
|
|
7
|
+
* and define how commands are handled when accepted in the chat commands menu.
|
|
8
|
+
*/
|
|
9
|
+
export interface IChatCommandRegistry {
|
|
10
|
+
addProvider(provider: IChatCommandProvider): void;
|
|
11
|
+
getProviders(): IChatCommandProvider[];
|
|
12
|
+
/**
|
|
13
|
+
* Handles a chat command by calling `handleChatCommand()` on the provider
|
|
14
|
+
* corresponding to this chat command.
|
|
15
|
+
*/
|
|
16
|
+
handleChatCommand(command: ChatCommand, inputModel: IInputModel): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default chat command registry implementation.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ChatCommandRegistry implements IChatCommandRegistry {
|
|
22
|
+
constructor();
|
|
23
|
+
addProvider(provider: IChatCommandProvider): void;
|
|
24
|
+
getProviders(): IChatCommandProvider[];
|
|
25
|
+
handleChatCommand(command: ChatCommand, inputModel: IInputModel): void;
|
|
26
|
+
private _providers;
|
|
27
|
+
}
|
|
28
|
+
export declare const IChatCommandRegistry: Token<IChatCommandRegistry>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
/**
|
|
7
|
+
* Default chat command registry implementation.
|
|
8
|
+
*/
|
|
9
|
+
export class ChatCommandRegistry {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._providers = new Map();
|
|
12
|
+
}
|
|
13
|
+
addProvider(provider) {
|
|
14
|
+
this._providers.set(provider.id, provider);
|
|
15
|
+
}
|
|
16
|
+
getProviders() {
|
|
17
|
+
return Array.from(this._providers.values());
|
|
18
|
+
}
|
|
19
|
+
handleChatCommand(command, inputModel) {
|
|
20
|
+
const provider = this._providers.get(command.providerId);
|
|
21
|
+
if (!provider) {
|
|
22
|
+
console.error('Error in handling chat command: No command provider has an ID of ' +
|
|
23
|
+
command.providerId);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
provider.handleChatCommand(command, inputModel);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const IChatCommandRegistry = new Token('@jupyter/chat:IChatCommandRegistry');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
import { IInputModel } from '../input-model';
|
|
3
|
+
export type ChatCommand = {
|
|
4
|
+
/**
|
|
5
|
+
* The name of the command. This defines what the user should type in the
|
|
6
|
+
* input to have the command appear in the chat commands menu.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* ID of the provider the command originated from.
|
|
11
|
+
*/
|
|
12
|
+
providerId: string;
|
|
13
|
+
/**
|
|
14
|
+
* If set, this will be rendered as the icon for the command in the chat
|
|
15
|
+
* commands menu. Jupyter Chat will choose a default if this is unset.
|
|
16
|
+
*/
|
|
17
|
+
icon?: LabIcon | string;
|
|
18
|
+
/**
|
|
19
|
+
* If set, this will be rendered as the description for the command in the
|
|
20
|
+
* chat commands menu. Jupyter Chat will choose a default if this is unset.
|
|
21
|
+
*/
|
|
22
|
+
description?: string;
|
|
23
|
+
/**
|
|
24
|
+
* If set, Jupyter Chat will replace the current word with this string after
|
|
25
|
+
* the command is run from the chat commands menu.
|
|
26
|
+
*
|
|
27
|
+
* If all commands from a provider have this property set, then
|
|
28
|
+
* `handleChatCommands()` can just return on the first line.
|
|
29
|
+
*/
|
|
30
|
+
replaceWith?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Interface of a command provider.
|
|
34
|
+
*/
|
|
35
|
+
export interface IChatCommandProvider {
|
|
36
|
+
/**
|
|
37
|
+
* ID of this command provider.
|
|
38
|
+
*/
|
|
39
|
+
id: string;
|
|
40
|
+
/**
|
|
41
|
+
* Async function which accepts the input model and returns a list of
|
|
42
|
+
* valid chat commands that match the current word. The current word is
|
|
43
|
+
* space-separated word at the user's cursor.
|
|
44
|
+
*/
|
|
45
|
+
getChatCommands(inputModel: IInputModel): Promise<ChatCommand[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Function called when a chat command is run by the user through the chat
|
|
48
|
+
* commands menu.
|
|
49
|
+
*/
|
|
50
|
+
handleChatCommand(command: ChatCommand, inputModel: IInputModel): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IAttachment } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* The attachments props.
|
|
5
|
+
*/
|
|
6
|
+
export type AttachmentsProps = {
|
|
7
|
+
attachments: IAttachment[];
|
|
8
|
+
onRemove?: (attachment: IAttachment) => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The Attachments component.
|
|
12
|
+
*/
|
|
13
|
+
export declare function AttachmentPreviewList(props: AttachmentsProps): JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* The attachment props.
|
|
16
|
+
*/
|
|
17
|
+
export type AttachmentProps = AttachmentsProps & {
|
|
18
|
+
attachment: IAttachment;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The Attachment component.
|
|
22
|
+
*/
|
|
23
|
+
export declare function AttachmentPreview(props: AttachmentProps): JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
// import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
6
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
7
|
+
import { Box } from '@mui/material';
|
|
8
|
+
import React, { useContext } from 'react';
|
|
9
|
+
import { TooltippedButton } from './mui-extras/tooltipped-button';
|
|
10
|
+
import { AttachmentOpenerContext } from '../context';
|
|
11
|
+
const ATTACHMENTS_CLASS = 'jp-chat-attachments';
|
|
12
|
+
const ATTACHMENT_CLASS = 'jp-chat-attachment';
|
|
13
|
+
const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
|
|
14
|
+
const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
|
|
15
|
+
/**
|
|
16
|
+
* The Attachments component.
|
|
17
|
+
*/
|
|
18
|
+
export function AttachmentPreviewList(props) {
|
|
19
|
+
return (React.createElement(Box, { className: ATTACHMENTS_CLASS }, props.attachments.map(attachment => (React.createElement(AttachmentPreview, { ...props, attachment: attachment })))));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The Attachment component.
|
|
23
|
+
*/
|
|
24
|
+
export function AttachmentPreview(props) {
|
|
25
|
+
const remove_tooltip = 'Remove attachment';
|
|
26
|
+
const attachmentOpenerRegistry = useContext(AttachmentOpenerContext);
|
|
27
|
+
return (React.createElement(Box, { className: ATTACHMENT_CLASS },
|
|
28
|
+
React.createElement("span", { className: (attachmentOpenerRegistry === null || attachmentOpenerRegistry === void 0 ? void 0 : attachmentOpenerRegistry.get(props.attachment.type))
|
|
29
|
+
? ATTACHMENT_CLICKABLE_CLASS
|
|
30
|
+
: '', onClick: () => {
|
|
31
|
+
var _a;
|
|
32
|
+
return (_a = attachmentOpenerRegistry === null || attachmentOpenerRegistry === void 0 ? void 0 : attachmentOpenerRegistry.get(props.attachment.type)) === null || _a === void 0 ? void 0 : _a(props.attachment);
|
|
33
|
+
} }, props.attachment.value),
|
|
34
|
+
props.onRemove && (React.createElement(TooltippedButton, { onClick: () => props.onRemove(props.attachment), tooltip: remove_tooltip, buttonProps: {
|
|
35
|
+
size: 'small',
|
|
36
|
+
title: remove_tooltip,
|
|
37
|
+
className: REMOVE_BUTTON_CLASS
|
|
38
|
+
}, sx: {
|
|
39
|
+
minWidth: 'unset',
|
|
40
|
+
padding: '0',
|
|
41
|
+
color: 'inherit'
|
|
42
|
+
} },
|
|
43
|
+
React.createElement(CloseIcon, null)))));
|
|
44
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
2
3
|
import { SxProps, Theme } from '@mui/material';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { IInputModel } from '../input-model';
|
|
5
|
+
import { IChatCommandRegistry } from '../chat-commands';
|
|
5
6
|
export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
|
|
6
7
|
/**
|
|
7
8
|
* The chat input namespace.
|
|
@@ -14,11 +15,7 @@ export declare namespace ChatInput {
|
|
|
14
15
|
/**
|
|
15
16
|
* The chat model.
|
|
16
17
|
*/
|
|
17
|
-
model:
|
|
18
|
-
/**
|
|
19
|
-
* The initial value of the input (default to '')
|
|
20
|
-
*/
|
|
21
|
-
value?: string;
|
|
18
|
+
model: IInputModel;
|
|
22
19
|
/**
|
|
23
20
|
* The function to be called to send the message.
|
|
24
21
|
*/
|
|
@@ -36,12 +33,12 @@ export declare namespace ChatInput {
|
|
|
36
33
|
*/
|
|
37
34
|
sx?: SxProps<Theme>;
|
|
38
35
|
/**
|
|
39
|
-
*
|
|
36
|
+
* The document manager.
|
|
40
37
|
*/
|
|
41
|
-
|
|
38
|
+
documentManager?: IDocumentManager;
|
|
42
39
|
/**
|
|
43
|
-
*
|
|
40
|
+
* Chat command registry.
|
|
44
41
|
*/
|
|
45
|
-
|
|
42
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
46
43
|
}
|
|
47
44
|
}
|
|
@@ -2,33 +2,36 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
6
5
|
import { Autocomplete, Box, InputAdornment, TextField } from '@mui/material';
|
|
7
6
|
import clsx from 'clsx';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { AttachmentPreviewList } from './attachments';
|
|
9
|
+
import { AttachButton, CancelButton, SendButton } from './input';
|
|
10
|
+
import { useChatCommands } from './input/use-chat-commands';
|
|
10
11
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
11
12
|
export function ChatInput(props) {
|
|
12
|
-
var _a, _b
|
|
13
|
-
const {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
var _a, _b;
|
|
14
|
+
const { documentManager, model } = props;
|
|
15
|
+
const [input, setInput] = useState(model.value);
|
|
16
|
+
const inputRef = useRef();
|
|
17
|
+
const chatCommands = useChatCommands(model, props.chatCommandRegistry);
|
|
16
18
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
17
|
-
const [
|
|
19
|
+
const [attachments, setAttachments] = useState(model.attachments);
|
|
18
20
|
// Display the include selection menu if it is not explicitly hidden, and if at least
|
|
19
21
|
// one of the tool to check for text or cell selection is enabled.
|
|
20
|
-
let hideIncludeSelection = (
|
|
22
|
+
let hideIncludeSelection = (_b = props.hideIncludeSelection) !== null && _b !== void 0 ? _b : false;
|
|
21
23
|
if (model.activeCellManager === null && model.selectionWatcher === null) {
|
|
22
24
|
hideIncludeSelection = true;
|
|
23
25
|
}
|
|
24
|
-
// store reference to the input element to enable focusing it easily
|
|
25
|
-
const inputRef = useRef();
|
|
26
26
|
useEffect(() => {
|
|
27
|
-
var _a;
|
|
27
|
+
var _a, _b;
|
|
28
|
+
const inputChanged = (_, value) => {
|
|
29
|
+
setInput(value);
|
|
30
|
+
};
|
|
31
|
+
model.valueChanged.connect(inputChanged);
|
|
28
32
|
const configChanged = (_, config) => {
|
|
29
|
-
var _a
|
|
33
|
+
var _a;
|
|
30
34
|
setSendWithShiftEnter((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
31
|
-
setTypingNotification((_b = config.sendTypingNotification) !== null && _b !== void 0 ? _b : false);
|
|
32
35
|
};
|
|
33
36
|
model.configChanged.connect(configChanged);
|
|
34
37
|
const focusInputElement = () => {
|
|
@@ -37,77 +40,65 @@ export function ChatInput(props) {
|
|
|
37
40
|
}
|
|
38
41
|
};
|
|
39
42
|
(_a = model.focusInputSignal) === null || _a === void 0 ? void 0 : _a.connect(focusInputElement);
|
|
43
|
+
const attachmentChanged = (_, attachments) => {
|
|
44
|
+
setAttachments([...attachments]);
|
|
45
|
+
};
|
|
46
|
+
(_b = model.attachmentsChanged) === null || _b === void 0 ? void 0 : _b.connect(attachmentChanged);
|
|
40
47
|
return () => {
|
|
41
|
-
var _a, _b;
|
|
48
|
+
var _a, _b, _c;
|
|
42
49
|
(_a = model.configChanged) === null || _a === void 0 ? void 0 : _a.disconnect(configChanged);
|
|
43
50
|
(_b = model.focusInputSignal) === null || _b === void 0 ? void 0 : _b.disconnect(focusInputElement);
|
|
51
|
+
(_c = model.attachmentsChanged) === null || _c === void 0 ? void 0 : _c.disconnect(attachmentChanged);
|
|
44
52
|
};
|
|
45
53
|
}, [model]);
|
|
46
|
-
// The autocomplete commands options.
|
|
47
|
-
const [commandOptions, setCommandOptions] = useState([]);
|
|
48
|
-
// whether any option is highlighted in the slash command autocomplete
|
|
49
|
-
const [highlighted, setHighlighted] = useState(false);
|
|
50
|
-
// controls whether the slash command autocomplete is open
|
|
51
|
-
const [open, setOpen] = useState(false);
|
|
52
54
|
const inputExists = !!input.trim();
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
56
|
+
* `handleKeyDown()`: callback invoked when the user presses any key in the
|
|
57
|
+
* `TextField` component. This is used to send the message when a user presses
|
|
58
|
+
* "Enter". This also handles many of the edge cases in the MUI Autocomplete
|
|
59
|
+
* component.
|
|
55
60
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (Array.isArray(autocompletion.current.commands)) {
|
|
67
|
-
setCommandOptions(autocompletion.current.commands);
|
|
68
|
-
}
|
|
69
|
-
else if (typeof autocompletion.current.commands === 'function') {
|
|
70
|
-
autocompletion.current
|
|
71
|
-
.commands()
|
|
72
|
-
.then((commands) => {
|
|
73
|
-
setCommandOptions(commands);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}, []);
|
|
77
|
-
/**
|
|
78
|
-
* Effect: Open the autocomplete when the user types the 'opener' string into an
|
|
79
|
-
* empty chat input. Close the autocomplete and reset the last selected value when
|
|
80
|
-
* the user clears the chat input.
|
|
81
|
-
*/
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
var _a, _b;
|
|
84
|
-
if (!((_a = autocompletion.current) === null || _a === void 0 ? void 0 : _a.opener)) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (input === ((_b = autocompletion.current) === null || _b === void 0 ? void 0 : _b.opener)) {
|
|
88
|
-
setOpen(true);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (input === '') {
|
|
92
|
-
setOpen(false);
|
|
61
|
+
function handleKeyDown(event) {
|
|
62
|
+
/**
|
|
63
|
+
* IMPORTANT: This statement ensures that arrow keys can be used to navigate
|
|
64
|
+
* the multiline input when the chat commands menu is closed.
|
|
65
|
+
*/
|
|
66
|
+
if (['ArrowDown', 'ArrowUp'].includes(event.key) &&
|
|
67
|
+
!chatCommands.menu.open) {
|
|
68
|
+
event.stopPropagation();
|
|
93
69
|
return;
|
|
94
70
|
}
|
|
95
|
-
|
|
96
|
-
function handleKeyDown(event) {
|
|
71
|
+
// remainder of this function only handles the "Enter" key.
|
|
97
72
|
if (event.key !== 'Enter') {
|
|
98
73
|
return;
|
|
99
74
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
75
|
+
/**
|
|
76
|
+
* IMPORTANT: This statement ensures that when the chat commands menu is
|
|
77
|
+
* open with a highlighted command, the "Enter" key should run that command
|
|
78
|
+
* instead of sending the message.
|
|
79
|
+
*
|
|
80
|
+
* This is done by returning early and letting the event propagate to the
|
|
81
|
+
* `Autocomplete` component.
|
|
82
|
+
*/
|
|
83
|
+
if (chatCommands.menu.highlighted) {
|
|
103
84
|
return;
|
|
104
85
|
}
|
|
86
|
+
// remainder of this function only handles the "Enter" key pressed while the
|
|
87
|
+
// commands menu is closed.
|
|
88
|
+
/**
|
|
89
|
+
* IMPORTANT: This ensures that when the "Enter" key is pressed with the
|
|
90
|
+
* commands menu closed, the event is not propagated up to the
|
|
91
|
+
* `Autocomplete` component. Without this, `Autocomplete.onChange()` gets
|
|
92
|
+
* called with an invalid `string` instead of a `ChatCommand`.
|
|
93
|
+
*/
|
|
94
|
+
event.stopPropagation();
|
|
105
95
|
// Do not send empty messages, and avoid adding new line in empty message.
|
|
106
96
|
if (!inputExists) {
|
|
107
97
|
event.stopPropagation();
|
|
108
98
|
event.preventDefault();
|
|
109
99
|
return;
|
|
110
100
|
}
|
|
101
|
+
// Finally, send the message when all other conditions are met.
|
|
111
102
|
if ((sendWithShiftEnter && event.shiftKey) ||
|
|
112
103
|
(!sendWithShiftEnter && !event.shiftKey)) {
|
|
113
104
|
onSend();
|
|
@@ -131,14 +122,14 @@ ${selection.source}
|
|
|
131
122
|
`;
|
|
132
123
|
}
|
|
133
124
|
props.onSend(content);
|
|
134
|
-
|
|
125
|
+
model.value = '';
|
|
135
126
|
}
|
|
136
127
|
/**
|
|
137
128
|
* Triggered when cancelling edition.
|
|
138
129
|
*/
|
|
139
130
|
function onCancel() {
|
|
140
|
-
|
|
141
|
-
props.onCancel();
|
|
131
|
+
var _a;
|
|
132
|
+
(_a = props.onCancel) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
142
133
|
}
|
|
143
134
|
// Set the helper text based on whether Shift+Enter is used for sending.
|
|
144
135
|
const helperText = sendWithShiftEnter ? (React.createElement("span", null,
|
|
@@ -153,7 +144,8 @@ ${selection.source}
|
|
|
153
144
|
React.createElement("b", null, "Enter"),
|
|
154
145
|
" to add a new line"));
|
|
155
146
|
return (React.createElement(Box, { sx: props.sx, className: clsx(INPUT_BOX_CLASS) },
|
|
156
|
-
React.createElement(
|
|
147
|
+
React.createElement(AttachmentPreviewList, { attachments: attachments, onRemove: model.removeAttachment }),
|
|
148
|
+
React.createElement(Autocomplete, { ...chatCommands.autocompleteProps,
|
|
157
149
|
// ensure the autocomplete popup always renders on top
|
|
158
150
|
componentsProps: {
|
|
159
151
|
popper: {
|
|
@@ -170,37 +162,20 @@ ${selection.source}
|
|
|
170
162
|
padding: 2
|
|
171
163
|
}
|
|
172
164
|
}
|
|
173
|
-
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, InputProps: {
|
|
165
|
+
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "outlined", multiline: true, onKeyDown: handleKeyDown, placeholder: "Start chatting", inputRef: inputRef, sx: { marginTop: '1px' }, onSelect: () => { var _a, _b; return (model.cursorIndex = (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : null); }, InputProps: {
|
|
174
166
|
...params.InputProps,
|
|
175
167
|
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
|
168
|
+
documentManager && model.addAttachment && (React.createElement(AttachButton, { documentManager: documentManager, onAttach: model.addAttachment })),
|
|
176
169
|
props.onCancel && React.createElement(CancelButton, { onCancel: onCancel }),
|
|
177
|
-
React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
|
|
170
|
+
React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists || attachments.length > 0, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
|
|
178
171
|
}, FormHelperTextProps: {
|
|
179
172
|
sx: { marginLeft: 'auto', marginRight: 0 }
|
|
180
|
-
}, helperText: input.length > 2 ? helperText : ' ' })),
|
|
181
|
-
|
|
182
|
-
if
|
|
183
|
-
|
|
173
|
+
}, helperText: input.length > 2 ? helperText : ' ' })), inputValue: input, onInputChange: (_, newValue, reason) => {
|
|
174
|
+
// Do not update the value if the reason is 'reset', which should occur only
|
|
175
|
+
// if an autocompletion command has been selected. In this case, the value is
|
|
176
|
+
// set in the 'onChange()' callback of the autocompletion (to avoid conflicts).
|
|
177
|
+
if (reason !== 'reset') {
|
|
178
|
+
model.value = newValue;
|
|
184
179
|
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* On highlight change: set `highlighted` to whether an option is
|
|
188
|
-
* highlighted by the user.
|
|
189
|
-
*
|
|
190
|
-
* This isn't called when an option is selected for some reason, so we
|
|
191
|
-
* need to call `setHighlighted(false)` in `onClose()`.
|
|
192
|
-
*/
|
|
193
|
-
(_, highlightedOption) => {
|
|
194
|
-
setHighlighted(!!highlightedOption);
|
|
195
|
-
}, onClose:
|
|
196
|
-
/**
|
|
197
|
-
* On close: set `highlighted` to `false` and close the popup by
|
|
198
|
-
* setting `open` to `false`.
|
|
199
|
-
*/
|
|
200
|
-
() => {
|
|
201
|
-
setHighlighted(false);
|
|
202
|
-
setOpen(false);
|
|
203
|
-
},
|
|
204
|
-
// hide default extra right padding in the text field
|
|
205
|
-
disableClearable: true })));
|
|
180
|
+
} })));
|
|
206
181
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
1
2
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
3
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
3
4
|
import type { SxProps, Theme } from '@mui/material';
|
|
4
5
|
import React from 'react';
|
|
6
|
+
import { IChatCommandRegistry } from '../chat-commands';
|
|
5
7
|
import { IChatModel } from '../model';
|
|
6
8
|
import { IChatMessage, IUser } from '../types';
|
|
7
9
|
/**
|
|
@@ -10,6 +12,8 @@ import { IChatMessage, IUser } from '../types';
|
|
|
10
12
|
type BaseMessageProps = {
|
|
11
13
|
rmRegistry: IRenderMimeRegistry;
|
|
12
14
|
model: IChatModel;
|
|
15
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
16
|
+
documentManager?: IDocumentManager;
|
|
13
17
|
};
|
|
14
18
|
/**
|
|
15
19
|
* The messages list component.
|
|
@@ -8,9 +8,11 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
8
8
|
import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
|
|
9
9
|
import clsx from 'clsx';
|
|
10
10
|
import React, { useEffect, useState, useRef, forwardRef } from 'react';
|
|
11
|
+
import { AttachmentPreviewList } from './attachments';
|
|
11
12
|
import { ChatInput } from './chat-input';
|
|
12
13
|
import { MarkdownRenderer } from './markdown-renderer';
|
|
13
14
|
import { ScrollContainer } from './scroll-container';
|
|
15
|
+
import { InputModel } from '../input-model';
|
|
14
16
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
15
17
|
const MESSAGE_CLASS = 'jp-chat-message';
|
|
16
18
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
@@ -226,6 +228,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
226
228
|
const [deleted, setDeleted] = useState(false);
|
|
227
229
|
const [canEdit, setCanEdit] = useState(false);
|
|
228
230
|
const [canDelete, setCanDelete] = useState(false);
|
|
231
|
+
const [inputModel, setInputModel] = useState(null);
|
|
229
232
|
// Look if the message can be deleted or edited.
|
|
230
233
|
useEffect(() => {
|
|
231
234
|
var _a;
|
|
@@ -241,6 +244,23 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
241
244
|
setCanDelete(false);
|
|
242
245
|
}
|
|
243
246
|
}, [model, message]);
|
|
247
|
+
// Create an input model only if the message is edited.
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (edit && canEdit) {
|
|
250
|
+
setInputModel(new InputModel({
|
|
251
|
+
value: message.body,
|
|
252
|
+
activeCellManager: model.activeCellManager,
|
|
253
|
+
selectionWatcher: model.selectionWatcher,
|
|
254
|
+
config: {
|
|
255
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
256
|
+
},
|
|
257
|
+
attachments: message.attachments
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
setInputModel(null);
|
|
262
|
+
}
|
|
263
|
+
}, [edit]);
|
|
244
264
|
// Cancel the current edition of the message.
|
|
245
265
|
const cancelEdition = () => {
|
|
246
266
|
setEdit(false);
|
|
@@ -253,6 +273,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
253
273
|
// Update the message
|
|
254
274
|
const updatedMessage = { ...message };
|
|
255
275
|
updatedMessage.body = input;
|
|
276
|
+
updatedMessage.attachments = inputModel === null || inputModel === void 0 ? void 0 : inputModel.attachments;
|
|
256
277
|
model.updateMessage(id, updatedMessage);
|
|
257
278
|
setEdit(false);
|
|
258
279
|
};
|
|
@@ -264,7 +285,12 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
264
285
|
model.deleteMessage(id);
|
|
265
286
|
};
|
|
266
287
|
// Empty if the message has been deleted.
|
|
267
|
-
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
288
|
+
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
289
|
+
edit && canEdit && inputModel ? (React.createElement(ChatInput, { onSend: (input) => updateMessage(message.id, input), onCancel: () => cancelEdition(), model: inputModel, hideIncludeSelection: true, chatCommandRegistry: props.chatCommandRegistry, documentManager: props.documentManager })) : (React.createElement(MarkdownRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? () => setEdit(true) : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
290
|
+
message.attachments && !edit && (
|
|
291
|
+
// Display the attachments only if message is not edited, otherwise the
|
|
292
|
+
// input component display them.
|
|
293
|
+
React.createElement(AttachmentPreviewList, { attachments: message.attachments }))));
|
|
268
294
|
});
|
|
269
295
|
/**
|
|
270
296
|
* The writers component, displaying the current writers.
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
3
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
3
4
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
5
|
+
import { IChatCommandRegistry } from '../chat-commands';
|
|
4
6
|
import { IChatModel } from '../model';
|
|
5
|
-
import {
|
|
7
|
+
import { IAttachmentOpenerRegistry } from '../registry';
|
|
6
8
|
export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
|
|
7
9
|
export declare function Chat(props: Chat.IOptions): JSX.Element;
|
|
8
10
|
/**
|
|
@@ -22,13 +24,17 @@ export declare namespace Chat {
|
|
|
22
24
|
*/
|
|
23
25
|
rmRegistry: IRenderMimeRegistry;
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
27
|
+
* The document manager.
|
|
26
28
|
*/
|
|
27
|
-
|
|
29
|
+
documentManager?: IDocumentManager;
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
31
|
+
* Chat command registry.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* Attachment opener registry.
|
|
36
|
+
*/
|
|
37
|
+
attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* The options to build the Chat UI.
|