@jupyter/chat 0.8.1 → 0.10.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/__tests__/mocks.d.ts +9 -0
- package/lib/__tests__/mocks.js +18 -0
- package/lib/__tests__/model.spec.js +17 -10
- package/lib/__tests__/widgets.spec.js +4 -4
- package/lib/chat-commands/types.d.ts +2 -1
- package/lib/components/chat-input.d.ts +4 -12
- package/lib/components/chat-input.js +26 -40
- package/lib/components/chat-messages.d.ts +17 -4
- package/lib/components/chat-messages.js +28 -15
- package/lib/components/chat.d.ts +5 -5
- package/lib/components/chat.js +9 -8
- package/lib/components/code-blocks/copy-button.js +6 -3
- package/lib/components/input/buttons/attach-button.d.ts +6 -0
- package/lib/components/input/{attach-button.js → buttons/attach-button.js} +11 -8
- package/lib/components/input/buttons/cancel-button.d.ts +6 -0
- package/lib/components/input/{cancel-button.js → buttons/cancel-button.js} +5 -7
- package/lib/components/input/buttons/index.d.ts +3 -0
- package/lib/components/input/buttons/index.js +7 -0
- package/lib/components/input/buttons/send-button.d.ts +6 -0
- package/lib/components/input/{send-button.js → buttons/send-button.js} +52 -42
- package/lib/components/input/index.d.ts +3 -3
- package/lib/components/input/index.js +3 -3
- package/lib/components/input/toolbar-registry.d.ts +98 -0
- package/lib/components/input/toolbar-registry.js +85 -0
- package/lib/components/input/use-chat-commands.js +6 -5
- package/lib/components/mui-extras/tooltipped-button.d.ts +1 -1
- package/lib/components/mui-extras/tooltipped-button.js +3 -2
- package/lib/components/mui-extras/tooltipped-icon-button.js +4 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/input-model.d.ts +93 -1
- package/lib/input-model.js +55 -1
- package/lib/model.d.ts +76 -9
- package/lib/model.js +42 -12
- package/lib/types.d.ts +5 -18
- package/lib/utils.d.ts +15 -0
- package/lib/utils.js +29 -0
- package/lib/widgets/chat-widget.d.ts +5 -1
- package/lib/widgets/chat-widget.js +7 -1
- package/package.json +1 -1
- package/src/__tests__/mocks.ts +31 -0
- package/src/__tests__/model.spec.ts +21 -11
- package/src/__tests__/widgets.spec.ts +5 -4
- package/src/chat-commands/types.ts +1 -1
- package/src/components/chat-input.tsx +41 -66
- package/src/components/chat-messages.tsx +44 -17
- package/src/components/chat.tsx +12 -21
- package/src/components/code-blocks/copy-button.tsx +9 -3
- package/src/components/input/{attach-button.tsx → buttons/attach-button.tsx} +15 -20
- package/src/components/input/{cancel-button.tsx → buttons/cancel-button.tsx} +9 -16
- package/src/components/input/buttons/index.ts +8 -0
- package/src/components/input/{send-button.tsx → buttons/send-button.tsx} +62 -61
- package/src/components/input/index.ts +3 -3
- package/src/components/input/toolbar-registry.tsx +162 -0
- package/src/components/input/use-chat-commands.tsx +14 -6
- package/src/components/mui-extras/tooltipped-button.tsx +4 -2
- package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -2
- package/src/index.ts +1 -1
- package/src/input-model.ts +140 -2
- package/src/model.ts +110 -12
- package/src/types.ts +5 -21
- package/src/utils.ts +34 -0
- package/src/widgets/chat-widget.tsx +8 -1
- package/style/base.css +1 -0
- package/style/chat.css +6 -0
- package/style/input.css +32 -0
- package/lib/components/input/attach-button.d.ts +0 -14
- package/lib/components/input/cancel-button.d.ts +0 -11
- package/lib/components/input/send-button.d.ts +0 -18
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AbstractChatContext, AbstractChatModel, IChatModel, IChatContext } from '../model';
|
|
2
|
+
import { INewMessage } from '../types';
|
|
3
|
+
export declare class MockChatContext extends AbstractChatContext implements IChatContext {
|
|
4
|
+
get users(): never[];
|
|
5
|
+
}
|
|
6
|
+
export declare class MockChatModel extends AbstractChatModel implements IChatModel {
|
|
7
|
+
sendMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
|
|
8
|
+
createChatContext(): IChatContext;
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { AbstractChatContext, AbstractChatModel } from '../model';
|
|
6
|
+
export class MockChatContext extends AbstractChatContext {
|
|
7
|
+
get users() {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class MockChatModel extends AbstractChatModel {
|
|
12
|
+
sendMessage(message) {
|
|
13
|
+
// No-op
|
|
14
|
+
}
|
|
15
|
+
createChatContext() {
|
|
16
|
+
return new MockChatContext({ model: this });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -5,25 +5,32 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { AbstractChatModel } from '../model';
|
|
9
|
+
import { MockChatModel, MockChatContext } from './mocks';
|
|
9
10
|
describe('test chat model', () => {
|
|
10
11
|
describe('model instantiation', () => {
|
|
11
|
-
it('should create
|
|
12
|
-
const model = new
|
|
13
|
-
expect(model).toBeInstanceOf(
|
|
12
|
+
it('should create an AbstractChatModel', () => {
|
|
13
|
+
const model = new MockChatModel();
|
|
14
|
+
expect(model).toBeInstanceOf(AbstractChatModel);
|
|
14
15
|
});
|
|
15
|
-
it('should dispose
|
|
16
|
-
const model = new
|
|
16
|
+
it('should dispose an AbstractChatModel', () => {
|
|
17
|
+
const model = new MockChatModel();
|
|
17
18
|
model.dispose();
|
|
18
19
|
expect(model.isDisposed).toBeTruthy();
|
|
19
20
|
});
|
|
20
21
|
});
|
|
21
22
|
describe('incoming message', () => {
|
|
22
|
-
class TestChat extends
|
|
23
|
+
class TestChat extends AbstractChatModel {
|
|
23
24
|
formatChatMessage(message) {
|
|
24
25
|
message.body = 'formatted msg';
|
|
25
26
|
return message;
|
|
26
27
|
}
|
|
28
|
+
sendMessage(message) {
|
|
29
|
+
// No-op
|
|
30
|
+
}
|
|
31
|
+
createChatContext() {
|
|
32
|
+
return new MockChatContext({ model: this });
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
let model;
|
|
29
36
|
let messages;
|
|
@@ -38,7 +45,7 @@ describe('test chat model', () => {
|
|
|
38
45
|
messages = [];
|
|
39
46
|
});
|
|
40
47
|
it('should signal incoming message', () => {
|
|
41
|
-
model = new
|
|
48
|
+
model = new MockChatModel();
|
|
42
49
|
model.messagesUpdated.connect((sender) => {
|
|
43
50
|
expect(sender).toBe(model);
|
|
44
51
|
messages = model.messages;
|
|
@@ -61,11 +68,11 @@ describe('test chat model', () => {
|
|
|
61
68
|
});
|
|
62
69
|
describe('model config', () => {
|
|
63
70
|
it('should have empty config', () => {
|
|
64
|
-
const model = new
|
|
71
|
+
const model = new MockChatModel();
|
|
65
72
|
expect(model.config.sendWithShiftEnter).toBeUndefined();
|
|
66
73
|
});
|
|
67
74
|
it('should allow config', () => {
|
|
68
|
-
const model = new
|
|
75
|
+
const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
|
|
69
76
|
expect(model.config.sendWithShiftEnter).toBeTruthy();
|
|
70
77
|
});
|
|
71
78
|
});
|
|
@@ -6,21 +6,21 @@
|
|
|
6
6
|
* Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
|
|
7
7
|
*/
|
|
8
8
|
import { RenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
9
|
-
import { ChatModel } from '../model';
|
|
10
9
|
import { ChatWidget } from '../widgets/chat-widget';
|
|
10
|
+
import { MockChatModel } from './mocks';
|
|
11
11
|
describe('test chat widget', () => {
|
|
12
12
|
let model;
|
|
13
13
|
let rmRegistry;
|
|
14
14
|
beforeEach(() => {
|
|
15
|
-
model = new
|
|
15
|
+
model = new MockChatModel();
|
|
16
16
|
rmRegistry = new RenderMimeRegistry();
|
|
17
17
|
});
|
|
18
18
|
describe('model instantiation', () => {
|
|
19
|
-
it('should create
|
|
19
|
+
it('should create an AbstractChatModel', () => {
|
|
20
20
|
const widget = new ChatWidget({ model, rmRegistry });
|
|
21
21
|
expect(widget).toBeInstanceOf(ChatWidget);
|
|
22
22
|
});
|
|
23
|
-
it('should dispose
|
|
23
|
+
it('should dispose an AbstractChatModel', () => {
|
|
24
24
|
const widget = new ChatWidget({ model, rmRegistry });
|
|
25
25
|
widget.dispose();
|
|
26
26
|
expect(widget.isDisposed).toBeTruthy();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
1
2
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
3
|
import { IInputModel } from '../input-model';
|
|
3
4
|
export type ChatCommand = {
|
|
@@ -14,7 +15,7 @@ export type ChatCommand = {
|
|
|
14
15
|
* If set, this will be rendered as the icon for the command in the chat
|
|
15
16
|
* commands menu. Jupyter Chat will choose a default if this is unset.
|
|
16
17
|
*/
|
|
17
|
-
icon?: LabIcon | string;
|
|
18
|
+
icon?: LabIcon | JSX.Element | string | null;
|
|
18
19
|
/**
|
|
19
20
|
* If set, this will be rendered as the description for the command in the
|
|
20
21
|
* chat commands menu. Jupyter Chat will choose a default if this is unset.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
3
2
|
import { SxProps, Theme } from '@mui/material';
|
|
3
|
+
import { IInputToolbarRegistry } from './input';
|
|
4
4
|
import { IInputModel } from '../input-model';
|
|
5
5
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
6
6
|
export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
|
|
@@ -13,29 +13,21 @@ export declare namespace ChatInput {
|
|
|
13
13
|
*/
|
|
14
14
|
interface IProps {
|
|
15
15
|
/**
|
|
16
|
-
* The
|
|
16
|
+
* The input model.
|
|
17
17
|
*/
|
|
18
18
|
model: IInputModel;
|
|
19
19
|
/**
|
|
20
|
-
* The
|
|
20
|
+
* The toolbar registry.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
toolbarRegistry: IInputToolbarRegistry;
|
|
23
23
|
/**
|
|
24
24
|
* The function to be called to cancel editing.
|
|
25
25
|
*/
|
|
26
26
|
onCancel?: () => unknown;
|
|
27
|
-
/**
|
|
28
|
-
* Whether to allow or not including selection.
|
|
29
|
-
*/
|
|
30
|
-
hideIncludeSelection?: boolean;
|
|
31
27
|
/**
|
|
32
28
|
* Custom mui/material styles.
|
|
33
29
|
*/
|
|
34
30
|
sx?: SxProps<Theme>;
|
|
35
|
-
/**
|
|
36
|
-
* The document manager.
|
|
37
|
-
*/
|
|
38
|
-
documentManager?: IDocumentManager;
|
|
39
31
|
/**
|
|
40
32
|
* Chat command registry.
|
|
41
33
|
*/
|
|
@@ -6,23 +6,24 @@ import { Autocomplete, Box, InputAdornment, TextField } from '@mui/material';
|
|
|
6
6
|
import clsx from 'clsx';
|
|
7
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { AttachmentPreviewList } from './attachments';
|
|
9
|
-
import {
|
|
10
|
-
import { useChatCommands } from './input/use-chat-commands';
|
|
9
|
+
import { useChatCommands } from './input';
|
|
11
10
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
11
|
+
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
12
12
|
export function ChatInput(props) {
|
|
13
|
-
var _a
|
|
14
|
-
const {
|
|
13
|
+
var _a;
|
|
14
|
+
const { model, toolbarRegistry } = props;
|
|
15
15
|
const [input, setInput] = useState(model.value);
|
|
16
16
|
const inputRef = useRef();
|
|
17
17
|
const chatCommands = useChatCommands(model, props.chatCommandRegistry);
|
|
18
18
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
19
19
|
const [attachments, setAttachments] = useState(model.attachments);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
const [toolbarElements, setToolbarElements] = useState([]);
|
|
21
|
+
/**
|
|
22
|
+
* Handle the changes on the model that affect the input.
|
|
23
|
+
* - focus requested
|
|
24
|
+
* - config changed
|
|
25
|
+
* - attachments changed
|
|
26
|
+
*/
|
|
26
27
|
useEffect(() => {
|
|
27
28
|
var _a, _b;
|
|
28
29
|
const inputChanged = (_, value) => {
|
|
@@ -51,6 +52,19 @@ export function ChatInput(props) {
|
|
|
51
52
|
(_c = model.attachmentsChanged) === null || _c === void 0 ? void 0 : _c.disconnect(attachmentChanged);
|
|
52
53
|
};
|
|
53
54
|
}, [model]);
|
|
55
|
+
/**
|
|
56
|
+
* Handle the changes in the toolbar items.
|
|
57
|
+
*/
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const updateToolbar = () => {
|
|
60
|
+
setToolbarElements(toolbarRegistry.getItems());
|
|
61
|
+
};
|
|
62
|
+
toolbarRegistry.itemsChanged.connect(updateToolbar);
|
|
63
|
+
updateToolbar();
|
|
64
|
+
return () => {
|
|
65
|
+
toolbarRegistry.itemsChanged.disconnect(updateToolbar);
|
|
66
|
+
};
|
|
67
|
+
}, [toolbarRegistry]);
|
|
54
68
|
const inputExists = !!input.trim();
|
|
55
69
|
/**
|
|
56
70
|
* `handleKeyDown()`: callback invoked when the user presses any key in the
|
|
@@ -101,36 +115,11 @@ export function ChatInput(props) {
|
|
|
101
115
|
// Finally, send the message when all other conditions are met.
|
|
102
116
|
if ((sendWithShiftEnter && event.shiftKey) ||
|
|
103
117
|
(!sendWithShiftEnter && !event.shiftKey)) {
|
|
104
|
-
|
|
118
|
+
model.send(input);
|
|
105
119
|
event.stopPropagation();
|
|
106
120
|
event.preventDefault();
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Triggered when sending the message.
|
|
111
|
-
*
|
|
112
|
-
* Add code block if cell or text is selected.
|
|
113
|
-
*/
|
|
114
|
-
function onSend(selection) {
|
|
115
|
-
let content = input;
|
|
116
|
-
if (selection) {
|
|
117
|
-
content += `
|
|
118
|
-
|
|
119
|
-
\`\`\`
|
|
120
|
-
${selection.source}
|
|
121
|
-
\`\`\`
|
|
122
|
-
`;
|
|
123
|
-
}
|
|
124
|
-
props.onSend(content);
|
|
125
|
-
model.value = '';
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Triggered when cancelling edition.
|
|
129
|
-
*/
|
|
130
|
-
function onCancel() {
|
|
131
|
-
var _a;
|
|
132
|
-
(_a = props.onCancel) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
133
|
-
}
|
|
134
123
|
// Set the helper text based on whether Shift+Enter is used for sending.
|
|
135
124
|
const helperText = sendWithShiftEnter ? (React.createElement("span", null,
|
|
136
125
|
"Press ",
|
|
@@ -164,10 +153,7 @@ ${selection.source}
|
|
|
164
153
|
}
|
|
165
154
|
}, 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: {
|
|
166
155
|
...params.InputProps,
|
|
167
|
-
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
|
168
|
-
documentManager && model.addAttachment && (React.createElement(AttachButton, { documentManager: documentManager, onAttach: model.addAttachment })),
|
|
169
|
-
props.onCancel && React.createElement(CancelButton, { onCancel: onCancel }),
|
|
170
|
-
React.createElement(SendButton, { model: model, sendWithShiftEnter: sendWithShiftEnter, inputExists: inputExists || attachments.length > 0, onSend: onSend, hideIncludeSelection: hideIncludeSelection, hasButtonOnLeft: !!props.onCancel })))
|
|
156
|
+
endAdornment: (React.createElement(InputAdornment, { position: "end", className: INPUT_TOOLBAR_CLASS }, toolbarElements.map(item => (React.createElement(item.element, { model: model })))))
|
|
171
157
|
}, FormHelperTextProps: {
|
|
172
158
|
sx: { marginLeft: 'auto', marginRight: 0 }
|
|
173
159
|
}, helperText: input.length > 2 ? helperText : ' ' })), inputValue: input, onInputChange: (_, newValue, reason) => {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
2
1
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
3
2
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
4
|
-
import type { SxProps, Theme } from '@mui/material';
|
|
5
3
|
import React from 'react';
|
|
4
|
+
import { IInputToolbarRegistry } from './input';
|
|
6
5
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
7
6
|
import { IChatModel } from '../model';
|
|
8
7
|
import { IChatMessage, IUser } from '../types';
|
|
@@ -10,10 +9,22 @@ import { IChatMessage, IUser } from '../types';
|
|
|
10
9
|
* The base components props.
|
|
11
10
|
*/
|
|
12
11
|
type BaseMessageProps = {
|
|
12
|
+
/**
|
|
13
|
+
* The mime renderer registry.
|
|
14
|
+
*/
|
|
13
15
|
rmRegistry: IRenderMimeRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* The chat model.
|
|
18
|
+
*/
|
|
14
19
|
model: IChatModel;
|
|
20
|
+
/**
|
|
21
|
+
* The chat commands registry.
|
|
22
|
+
*/
|
|
15
23
|
chatCommandRegistry?: IChatCommandRegistry;
|
|
16
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The input toolbar registry.
|
|
26
|
+
*/
|
|
27
|
+
inputToolbarRegistry: IInputToolbarRegistry;
|
|
17
28
|
};
|
|
18
29
|
/**
|
|
19
30
|
* The messages list component.
|
|
@@ -23,8 +34,10 @@ export declare function ChatMessages(props: BaseMessageProps): JSX.Element;
|
|
|
23
34
|
* The message header props.
|
|
24
35
|
*/
|
|
25
36
|
type ChatMessageHeaderProps = {
|
|
37
|
+
/**
|
|
38
|
+
* The chat message.
|
|
39
|
+
*/
|
|
26
40
|
message: IChatMessage;
|
|
27
|
-
sx?: SxProps<Theme>;
|
|
28
41
|
};
|
|
29
42
|
/**
|
|
30
43
|
* The message header component.
|
|
@@ -13,6 +13,7 @@ import { ChatInput } from './chat-input';
|
|
|
13
13
|
import { MarkdownRenderer } from './markdown-renderer';
|
|
14
14
|
import { ScrollContainer } from './scroll-container';
|
|
15
15
|
import { InputModel } from '../input-model';
|
|
16
|
+
import { replaceSpanToMention } from '../utils';
|
|
16
17
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
17
18
|
const MESSAGE_CLASS = 'jp-chat-message';
|
|
18
19
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
@@ -192,8 +193,7 @@ export function ChatMessageHeader(props) {
|
|
|
192
193
|
'& > :not(:last-child)': {
|
|
193
194
|
marginRight: 3
|
|
194
195
|
},
|
|
195
|
-
marginBottom: message.stacked ? '0px' : '12px'
|
|
196
|
-
...props.sx
|
|
196
|
+
marginBottom: message.stacked ? '0px' : '12px'
|
|
197
197
|
} },
|
|
198
198
|
avatar,
|
|
199
199
|
React.createElement(Box, { sx: {
|
|
@@ -247,15 +247,27 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
247
247
|
// Create an input model only if the message is edited.
|
|
248
248
|
useEffect(() => {
|
|
249
249
|
if (edit && canEdit) {
|
|
250
|
-
setInputModel(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
setInputModel(() => {
|
|
251
|
+
var _a;
|
|
252
|
+
let body = message.body;
|
|
253
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
254
|
+
body = replaceSpanToMention(body, user);
|
|
255
|
+
});
|
|
256
|
+
return new InputModel({
|
|
257
|
+
chatContext: model.createChatContext(),
|
|
258
|
+
onSend: (input, model) => updateMessage(message.id, input, model),
|
|
259
|
+
onCancel: () => cancelEdition(),
|
|
260
|
+
value: body,
|
|
261
|
+
activeCellManager: model.activeCellManager,
|
|
262
|
+
selectionWatcher: model.selectionWatcher,
|
|
263
|
+
documentManager: model.documentManager,
|
|
264
|
+
config: {
|
|
265
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
266
|
+
},
|
|
267
|
+
attachments: message.attachments,
|
|
268
|
+
mentions: message.mentions
|
|
269
|
+
});
|
|
270
|
+
});
|
|
259
271
|
}
|
|
260
272
|
else {
|
|
261
273
|
setInputModel(null);
|
|
@@ -266,14 +278,15 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
266
278
|
setEdit(false);
|
|
267
279
|
};
|
|
268
280
|
// Update the content of the message.
|
|
269
|
-
const updateMessage = (id, input) => {
|
|
270
|
-
if (!canEdit) {
|
|
281
|
+
const updateMessage = (id, input, inputModel) => {
|
|
282
|
+
if (!canEdit || !inputModel) {
|
|
271
283
|
return;
|
|
272
284
|
}
|
|
273
285
|
// Update the message
|
|
274
286
|
const updatedMessage = { ...message };
|
|
275
287
|
updatedMessage.body = input;
|
|
276
|
-
updatedMessage.attachments = inputModel
|
|
288
|
+
updatedMessage.attachments = inputModel.attachments;
|
|
289
|
+
updatedMessage.mentions = inputModel.mentions;
|
|
277
290
|
model.updateMessage(id, updatedMessage);
|
|
278
291
|
setEdit(false);
|
|
279
292
|
};
|
|
@@ -286,7 +299,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
286
299
|
};
|
|
287
300
|
// Empty if the message has been deleted.
|
|
288
301
|
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, {
|
|
302
|
+
edit && canEdit && inputModel ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: inputModel, chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry })) : (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
303
|
message.attachments && !edit && (
|
|
291
304
|
// Display the attachments only if message is not edited, otherwise the
|
|
292
305
|
// input component display them.
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
3
|
-
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
4
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
5
4
|
import { IChatCommandRegistry } from '../chat-commands';
|
|
5
|
+
import { IInputToolbarRegistry } from './input';
|
|
6
6
|
import { IChatModel } from '../model';
|
|
7
7
|
import { IAttachmentOpenerRegistry } from '../registry';
|
|
8
8
|
export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
|
|
@@ -23,10 +23,6 @@ export declare namespace Chat {
|
|
|
23
23
|
* The rendermime registry.
|
|
24
24
|
*/
|
|
25
25
|
rmRegistry: IRenderMimeRegistry;
|
|
26
|
-
/**
|
|
27
|
-
* The document manager.
|
|
28
|
-
*/
|
|
29
|
-
documentManager?: IDocumentManager;
|
|
30
26
|
/**
|
|
31
27
|
* Chat command registry.
|
|
32
28
|
*/
|
|
@@ -35,6 +31,10 @@ export declare namespace Chat {
|
|
|
35
31
|
* Attachment opener registry.
|
|
36
32
|
*/
|
|
37
33
|
attachmentOpenerRegistry?: IAttachmentOpenerRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* The input toolbar registry
|
|
36
|
+
*/
|
|
37
|
+
inputToolbarRegistry?: IInputToolbarRegistry;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* The options to build the Chat UI.
|
package/lib/components/chat.js
CHANGED
|
@@ -10,22 +10,23 @@ import React, { useState } from 'react';
|
|
|
10
10
|
import { JlThemeProvider } from './jl-theme-provider';
|
|
11
11
|
import { ChatMessages } from './chat-messages';
|
|
12
12
|
import { ChatInput } from './chat-input';
|
|
13
|
+
import { InputToolbarRegistry } from './input';
|
|
13
14
|
import { AttachmentOpenerContext } from '../context';
|
|
14
15
|
export function ChatBody(props) {
|
|
15
16
|
const { model } = props;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
17
|
+
let { inputToolbarRegistry } = props;
|
|
18
|
+
if (!inputToolbarRegistry) {
|
|
19
|
+
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
20
|
+
}
|
|
20
21
|
return (React.createElement(AttachmentOpenerContext.Provider, { value: props.attachmentOpenerRegistry },
|
|
21
|
-
React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry,
|
|
22
|
-
React.createElement(ChatInput, {
|
|
22
|
+
React.createElement(ChatMessages, { rmRegistry: props.rmRegistry, model: model, chatCommandRegistry: props.chatCommandRegistry, inputToolbarRegistry: inputToolbarRegistry }),
|
|
23
|
+
React.createElement(ChatInput, { sx: {
|
|
23
24
|
paddingLeft: 4,
|
|
24
25
|
paddingRight: 4,
|
|
25
26
|
paddingTop: 1,
|
|
26
27
|
paddingBottom: 0,
|
|
27
28
|
borderTop: '1px solid var(--jp-border-color1)'
|
|
28
|
-
}, model: model.input,
|
|
29
|
+
}, model: model.input, chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: inputToolbarRegistry })));
|
|
29
30
|
}
|
|
30
31
|
export function Chat(props) {
|
|
31
32
|
var _a;
|
|
@@ -53,7 +54,7 @@ export function Chat(props) {
|
|
|
53
54
|
React.createElement(ArrowBackIcon, null))) : (React.createElement(Box, null)),
|
|
54
55
|
view !== Chat.View.settings && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.settings) },
|
|
55
56
|
React.createElement(SettingsIcon, null))) : (React.createElement(Box, null))),
|
|
56
|
-
view === Chat.View.chat &&
|
|
57
|
+
view === Chat.View.chat && React.createElement(ChatBody, { ...props }),
|
|
57
58
|
view === Chat.View.settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
@@ -10,14 +10,17 @@ var CopyStatus;
|
|
|
10
10
|
CopyStatus[CopyStatus["None"] = 0] = "None";
|
|
11
11
|
CopyStatus[CopyStatus["Copying"] = 1] = "Copying";
|
|
12
12
|
CopyStatus[CopyStatus["Copied"] = 2] = "Copied";
|
|
13
|
+
CopyStatus[CopyStatus["Disabled"] = 3] = "Disabled";
|
|
13
14
|
})(CopyStatus || (CopyStatus = {}));
|
|
14
15
|
const COPYBTN_TEXT_BY_STATUS = {
|
|
15
16
|
[CopyStatus.None]: 'Copy to clipboard',
|
|
16
17
|
[CopyStatus.Copying]: 'Copying…',
|
|
17
|
-
[CopyStatus.Copied]: 'Copied!'
|
|
18
|
+
[CopyStatus.Copied]: 'Copied!',
|
|
19
|
+
[CopyStatus.Disabled]: 'Copy to clipboard disabled in insecure context'
|
|
18
20
|
};
|
|
19
21
|
export function CopyButton(props) {
|
|
20
|
-
const
|
|
22
|
+
const isCopyDisabled = navigator.clipboard === undefined;
|
|
23
|
+
const [copyStatus, setCopyStatus] = useState(isCopyDisabled ? CopyStatus.Disabled : CopyStatus.None);
|
|
21
24
|
const timeoutId = useRef(null);
|
|
22
25
|
const copy = useCallback(async () => {
|
|
23
26
|
// ignore if we are already copying
|
|
@@ -38,6 +41,6 @@ export function CopyButton(props) {
|
|
|
38
41
|
}
|
|
39
42
|
timeoutId.current = window.setTimeout(() => setCopyStatus(CopyStatus.None), 1000);
|
|
40
43
|
}, [copyStatus, props.value]);
|
|
41
|
-
return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: COPYBTN_TEXT_BY_STATUS[copyStatus], placement: "top", onClick: copy, "aria-label": "Copy to clipboard" },
|
|
44
|
+
return (React.createElement(TooltippedIconButton, { disabled: isCopyDisabled, className: props.className, tooltip: COPYBTN_TEXT_BY_STATUS[copyStatus], placement: "top", onClick: copy, "aria-label": "Copy to clipboard" },
|
|
42
45
|
React.createElement(copyIcon.react, { height: "16px", width: "16px" })));
|
|
43
46
|
}
|
|
@@ -5,23 +5,31 @@
|
|
|
5
5
|
import { FileDialog } from '@jupyterlab/filebrowser';
|
|
6
6
|
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
|
7
7
|
import React from 'react';
|
|
8
|
-
import { TooltippedButton } from '
|
|
8
|
+
import { TooltippedButton } from '../../mui-extras/tooltipped-button';
|
|
9
9
|
const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
|
|
10
10
|
/**
|
|
11
11
|
* The attach button.
|
|
12
12
|
*/
|
|
13
13
|
export function AttachButton(props) {
|
|
14
|
+
const { model } = props;
|
|
14
15
|
const tooltip = 'Add attachment';
|
|
16
|
+
if (!model.documentManager || !model.addAttachment) {
|
|
17
|
+
return React.createElement(React.Fragment, null);
|
|
18
|
+
}
|
|
15
19
|
const onclick = async () => {
|
|
20
|
+
if (!model.documentManager || !model.addAttachment) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
16
23
|
try {
|
|
17
24
|
const files = await FileDialog.getOpenFiles({
|
|
18
25
|
title: 'Select files to attach',
|
|
19
|
-
manager:
|
|
26
|
+
manager: model.documentManager
|
|
20
27
|
});
|
|
21
28
|
if (files.value) {
|
|
22
29
|
files.value.forEach(file => {
|
|
30
|
+
var _a;
|
|
23
31
|
if (file.type !== 'directory') {
|
|
24
|
-
|
|
32
|
+
(_a = model.addAttachment) === null || _a === void 0 ? void 0 : _a.call(model, { type: 'file', value: file.path });
|
|
25
33
|
}
|
|
26
34
|
});
|
|
27
35
|
}
|
|
@@ -35,11 +43,6 @@ export function AttachButton(props) {
|
|
|
35
43
|
variant: 'contained',
|
|
36
44
|
title: tooltip,
|
|
37
45
|
className: ATTACH_BUTTON_CLASS
|
|
38
|
-
}, sx: {
|
|
39
|
-
minWidth: 'unset',
|
|
40
|
-
padding: '4px',
|
|
41
|
-
borderRadius: '2px 0px 0px 2px',
|
|
42
|
-
marginRight: '1px'
|
|
43
46
|
} },
|
|
44
47
|
React.createElement(AttachFileIcon, null)));
|
|
45
48
|
}
|
|
@@ -4,23 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
6
6
|
import React from 'react';
|
|
7
|
-
import { TooltippedButton } from '
|
|
7
|
+
import { TooltippedButton } from '../../mui-extras/tooltipped-button';
|
|
8
8
|
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
9
9
|
/**
|
|
10
10
|
* The cancel button.
|
|
11
11
|
*/
|
|
12
12
|
export function CancelButton(props) {
|
|
13
|
+
if (!props.model.cancel) {
|
|
14
|
+
return React.createElement(React.Fragment, null);
|
|
15
|
+
}
|
|
13
16
|
const tooltip = 'Cancel edition';
|
|
14
|
-
return (React.createElement(TooltippedButton, { onClick: props.
|
|
17
|
+
return (React.createElement(TooltippedButton, { onClick: props.model.cancel, tooltip: tooltip, buttonProps: {
|
|
15
18
|
size: 'small',
|
|
16
19
|
variant: 'contained',
|
|
17
20
|
title: tooltip,
|
|
18
21
|
className: CANCEL_BUTTON_CLASS
|
|
19
|
-
}, sx: {
|
|
20
|
-
minWidth: 'unset',
|
|
21
|
-
padding: '4px',
|
|
22
|
-
borderRadius: '2px 0px 0px 2px',
|
|
23
|
-
marginRight: '1px'
|
|
24
22
|
} },
|
|
25
23
|
React.createElement(CancelIcon, null)));
|
|
26
24
|
}
|