@jupyter/chat 0.20.0 → 0.21.1
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__/model.spec.js +49 -0
- package/lib/components/attachments.js +3 -3
- package/lib/components/chat.d.ts +5 -0
- package/lib/components/code-blocks/code-toolbar.js +12 -8
- package/lib/components/code-blocks/copy-button.js +9 -7
- package/lib/components/input/buttons/attach-button.js +4 -2
- package/lib/components/input/buttons/cancel-button.js +3 -1
- package/lib/components/input/buttons/save-edit-button.js +3 -1
- package/lib/components/input/buttons/send-button.js +4 -2
- package/lib/components/input/buttons/stop-button.js +3 -1
- package/lib/components/input/chat-input.js +3 -2
- package/lib/components/messages/header.js +7 -3
- package/lib/components/messages/message-renderer.js +18 -18
- package/lib/components/messages/navigation.js +5 -4
- package/lib/components/messages/toolbar.js +4 -2
- package/lib/components/writing-indicator.js +11 -6
- package/lib/context.d.ts +7 -0
- package/lib/context.js +12 -0
- package/lib/message.d.ts +2 -1
- package/lib/message.js +3 -0
- package/lib/model.d.ts +16 -0
- package/lib/model.js +28 -8
- package/lib/types.d.ts +46 -1
- package/lib/widgets/chat-error.d.ts +2 -1
- package/lib/widgets/chat-error.js +6 -3
- package/lib/widgets/chat-selector-popup.d.ts +6 -0
- package/lib/widgets/chat-selector-popup.js +8 -5
- package/lib/widgets/chat-sidebar.js +5 -1
- package/lib/widgets/chat-widget.js +6 -1
- package/lib/widgets/multichat-panel.d.ts +6 -0
- package/lib/widgets/multichat-panel.js +21 -13
- package/package.json +2 -1
- package/src/__tests__/model.spec.ts +58 -0
- package/src/components/attachments.tsx +3 -3
- package/src/components/chat.tsx +5 -0
- package/src/components/code-blocks/code-toolbar.tsx +14 -7
- package/src/components/code-blocks/copy-button.tsx +12 -8
- package/src/components/input/buttons/attach-button.tsx +4 -2
- package/src/components/input/buttons/cancel-button.tsx +4 -1
- package/src/components/input/buttons/save-edit-button.tsx +3 -1
- package/src/components/input/buttons/send-button.tsx +4 -2
- package/src/components/input/buttons/stop-button.tsx +3 -1
- package/src/components/input/chat-input.tsx +3 -2
- package/src/components/messages/header.tsx +9 -3
- package/src/components/messages/message-renderer.tsx +18 -18
- package/src/components/messages/navigation.tsx +5 -4
- package/src/components/messages/toolbar.tsx +6 -4
- package/src/components/writing-indicator.tsx +17 -6
- package/src/context.ts +13 -0
- package/src/message.ts +4 -1
- package/src/model.ts +52 -4
- package/src/types.ts +46 -1
- package/src/widgets/chat-error.tsx +9 -5
- package/src/widgets/chat-selector-popup.tsx +21 -3
- package/src/widgets/chat-sidebar.tsx +5 -1
- package/src/widgets/chat-widget.tsx +7 -1
- package/src/widgets/multichat-panel.tsx +32 -12
|
@@ -66,6 +66,55 @@ describe('test chat model', () => {
|
|
|
66
66
|
expect(messages[0].body).toBe('formatted msg');
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
+
describe('messageChanged signal', () => {
|
|
70
|
+
const msg = {
|
|
71
|
+
type: 'msg',
|
|
72
|
+
id: 'message1',
|
|
73
|
+
time: Date.now() / 1000,
|
|
74
|
+
body: 'original body',
|
|
75
|
+
sender: { username: 'user' }
|
|
76
|
+
};
|
|
77
|
+
it('should emit messageChanged when a message is updated', () => {
|
|
78
|
+
const model = new MockChatModel();
|
|
79
|
+
model.messageAdded(msg);
|
|
80
|
+
let emitCount = 0;
|
|
81
|
+
model.messageChanged.connect(() => {
|
|
82
|
+
emitCount++;
|
|
83
|
+
});
|
|
84
|
+
let changedMessage = null;
|
|
85
|
+
model.messageChanged.connect((sender, message) => {
|
|
86
|
+
changedMessage = message;
|
|
87
|
+
});
|
|
88
|
+
model.messages[0].update({ body: 'updated body' });
|
|
89
|
+
expect(emitCount).toBe(1);
|
|
90
|
+
expect(changedMessage).not.toBeNull();
|
|
91
|
+
expect(changedMessage.body).toBe('updated body');
|
|
92
|
+
});
|
|
93
|
+
it('should not emit messageChanged after the message is deleted', () => {
|
|
94
|
+
var _a;
|
|
95
|
+
const model = new MockChatModel();
|
|
96
|
+
model.messageAdded(msg);
|
|
97
|
+
let emitCount = 0;
|
|
98
|
+
model.messageChanged.connect(() => {
|
|
99
|
+
emitCount++;
|
|
100
|
+
});
|
|
101
|
+
model.messagesDeleted(0, 1);
|
|
102
|
+
(_a = model.messages[0]) === null || _a === void 0 ? void 0 : _a.update({ body: 'updated body' });
|
|
103
|
+
expect(emitCount).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
it('should not emit messageChanged after the model is disposed', () => {
|
|
106
|
+
var _a;
|
|
107
|
+
const model = new MockChatModel();
|
|
108
|
+
model.messageAdded(msg);
|
|
109
|
+
let emitCount = 0;
|
|
110
|
+
model.messageChanged.connect(() => {
|
|
111
|
+
emitCount++;
|
|
112
|
+
});
|
|
113
|
+
model.dispose();
|
|
114
|
+
(_a = model.messages[0]) === null || _a === void 0 ? void 0 : _a.update({ body: 'updated body' });
|
|
115
|
+
expect(emitCount).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
69
118
|
describe('model config', () => {
|
|
70
119
|
it('should have empty config', () => {
|
|
71
120
|
const model = new MockChatModel();
|
|
@@ -8,7 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
9
9
|
import { UUID } from '@lumino/coreutils';
|
|
10
10
|
import { TooltippedIconButton } from './mui-extras';
|
|
11
|
-
import { useChatContext } from '../context';
|
|
11
|
+
import { useChatContext, useTranslator } from '../context';
|
|
12
12
|
const ATTACHMENT_CLASS = 'jp-chat-attachment';
|
|
13
13
|
const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
|
|
14
14
|
const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
|
|
@@ -52,7 +52,7 @@ export function AttachmentPreviewList(props) {
|
|
|
52
52
|
* The Attachment component.
|
|
53
53
|
*/
|
|
54
54
|
export function AttachmentPreview(props) {
|
|
55
|
-
const
|
|
55
|
+
const trans = useTranslator();
|
|
56
56
|
const { attachmentOpenerRegistry } = useChatContext();
|
|
57
57
|
const isClickable = !!(attachmentOpenerRegistry === null || attachmentOpenerRegistry === void 0 ? void 0 : attachmentOpenerRegistry.get(props.attachment.type));
|
|
58
58
|
return (React.createElement(Box, { className: ATTACHMENT_CLASS, sx: {
|
|
@@ -80,7 +80,7 @@ export function AttachmentPreview(props) {
|
|
|
80
80
|
}
|
|
81
81
|
: {}
|
|
82
82
|
} }, getAttachmentDisplayName(props.attachment))),
|
|
83
|
-
props.onRemove && (React.createElement(TooltippedIconButton, { tooltip:
|
|
83
|
+
props.onRemove && (React.createElement(TooltippedIconButton, { tooltip: trans.__('Remove attachment'), onClick: () => props.onRemove(props.attachment), className: REMOVE_BUTTON_CLASS, inputToolbar: false, sx: {
|
|
84
84
|
width: 'unset',
|
|
85
85
|
minWidth: 'unset',
|
|
86
86
|
height: 'unset',
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
4
5
|
import { IInputToolbarRegistry } from './input';
|
|
5
6
|
import { IChatModel } from '../model';
|
|
6
7
|
import { IAttachmentOpenerRegistry, IChatCommandRegistry, IMessageFooterRegistry, IMessagePreambleRegistry } from '../registers';
|
|
@@ -51,6 +52,10 @@ export declare namespace Chat {
|
|
|
51
52
|
* The area where the chat is displayed.
|
|
52
53
|
*/
|
|
53
54
|
area?: ChatArea;
|
|
55
|
+
/**
|
|
56
|
+
* The translator for internationalization.
|
|
57
|
+
*/
|
|
58
|
+
translator?: ITranslator;
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
56
61
|
* The options to build the Chat UI.
|
|
@@ -7,6 +7,7 @@ import { Box } from '@mui/material';
|
|
|
7
7
|
import React, { useEffect, useState } from 'react';
|
|
8
8
|
import { CopyButton } from './copy-button';
|
|
9
9
|
import { TooltippedIconButton } from '../mui-extras';
|
|
10
|
+
import { useTranslator } from '../../context';
|
|
10
11
|
import { replaceCellIcon } from '../../icons';
|
|
11
12
|
const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
|
|
12
13
|
const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
|
|
@@ -61,26 +62,29 @@ export function CodeToolbar(props) {
|
|
|
61
62
|
React.createElement(CopyButton, { value: content, className: CODE_TOOLBAR_ITEM_CLASS })));
|
|
62
63
|
}
|
|
63
64
|
function InsertAboveButton(props) {
|
|
65
|
+
const trans = useTranslator();
|
|
64
66
|
const tooltip = props.activeCellAvailable
|
|
65
|
-
? 'Insert above active cell'
|
|
66
|
-
: 'Insert above active cell (no active cell)';
|
|
67
|
+
? trans.__('Insert above active cell')
|
|
68
|
+
: trans.__('Insert above active cell (no active cell)');
|
|
67
69
|
return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, onClick: () => { var _a; return (_a = props.activeCellManager) === null || _a === void 0 ? void 0 : _a.insertAbove(props.content); }, disabled: !props.activeCellAvailable, inputToolbar: false },
|
|
68
70
|
React.createElement(addAboveIcon.react, { height: "16px", width: "16px" })));
|
|
69
71
|
}
|
|
70
72
|
function InsertBelowButton(props) {
|
|
73
|
+
const trans = useTranslator();
|
|
71
74
|
const tooltip = props.activeCellAvailable
|
|
72
|
-
? 'Insert below active cell'
|
|
73
|
-
: 'Insert below active cell (no active cell)';
|
|
75
|
+
? trans.__('Insert below active cell')
|
|
76
|
+
: trans.__('Insert below active cell (no active cell)');
|
|
74
77
|
return (React.createElement(TooltippedIconButton, { className: props.className, tooltip: tooltip, disabled: !props.activeCellAvailable, onClick: () => { var _a; return (_a = props.activeCellManager) === null || _a === void 0 ? void 0 : _a.insertBelow(props.content); }, inputToolbar: false },
|
|
75
78
|
React.createElement(addBelowIcon.react, { height: "16px", width: "16px" })));
|
|
76
79
|
}
|
|
77
80
|
function ReplaceButton(props) {
|
|
78
|
-
var _a, _b;
|
|
81
|
+
var _a, _b, _c;
|
|
82
|
+
const trans = useTranslator();
|
|
79
83
|
const tooltip = props.selectionExists
|
|
80
|
-
?
|
|
84
|
+
? trans.__('Replace selection (%1 line(s))', (_c = (_b = (_a = props.selectionWatcher) === null || _a === void 0 ? void 0 : _a.selection) === null || _b === void 0 ? void 0 : _b.numLines) !== null && _c !== void 0 ? _c : 0)
|
|
81
85
|
: props.activeCellAvailable
|
|
82
|
-
? 'Replace selection (active cell)'
|
|
83
|
-
: 'Replace selection (no selection)';
|
|
86
|
+
? trans.__('Replace selection (active cell)')
|
|
87
|
+
: trans.__('Replace selection (no selection)');
|
|
84
88
|
const disabled = !props.activeCellAvailable && !props.selectionExists;
|
|
85
89
|
const replace = () => {
|
|
86
90
|
var _a, _b, _c;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { copyIcon } from '@jupyterlab/ui-components';
|
|
6
6
|
import React, { useState, useCallback, useRef } from 'react';
|
|
7
7
|
import { TooltippedIconButton } from '../mui-extras';
|
|
8
|
+
import { useTranslator } from '../../context';
|
|
8
9
|
var CopyStatus;
|
|
9
10
|
(function (CopyStatus) {
|
|
10
11
|
CopyStatus[CopyStatus["None"] = 0] = "None";
|
|
@@ -12,13 +13,8 @@ var CopyStatus;
|
|
|
12
13
|
CopyStatus[CopyStatus["Copied"] = 2] = "Copied";
|
|
13
14
|
CopyStatus[CopyStatus["Disabled"] = 3] = "Disabled";
|
|
14
15
|
})(CopyStatus || (CopyStatus = {}));
|
|
15
|
-
const COPYBTN_TEXT_BY_STATUS = {
|
|
16
|
-
[CopyStatus.None]: 'Copy to clipboard',
|
|
17
|
-
[CopyStatus.Copying]: 'Copying…',
|
|
18
|
-
[CopyStatus.Copied]: 'Copied!',
|
|
19
|
-
[CopyStatus.Disabled]: 'Copy to clipboard disabled in insecure context'
|
|
20
|
-
};
|
|
21
16
|
export function CopyButton(props) {
|
|
17
|
+
const trans = useTranslator();
|
|
22
18
|
const isCopyDisabled = navigator.clipboard === undefined;
|
|
23
19
|
const [copyStatus, setCopyStatus] = useState(isCopyDisabled ? CopyStatus.Disabled : CopyStatus.None);
|
|
24
20
|
const timeoutId = useRef(null);
|
|
@@ -41,7 +37,13 @@ export function CopyButton(props) {
|
|
|
41
37
|
}
|
|
42
38
|
timeoutId.current = window.setTimeout(() => setCopyStatus(CopyStatus.None), 1000);
|
|
43
39
|
}, [copyStatus, props.value]);
|
|
40
|
+
const COPYBTN_TEXT_BY_STATUS = {
|
|
41
|
+
[CopyStatus.None]: trans.__('Copy to clipboard'),
|
|
42
|
+
[CopyStatus.Copying]: trans.__('Copying…'),
|
|
43
|
+
[CopyStatus.Copied]: trans.__('Copied!'),
|
|
44
|
+
[CopyStatus.Disabled]: trans.__('Copy to clipboard disabled in insecure context')
|
|
45
|
+
};
|
|
44
46
|
const tooltip = COPYBTN_TEXT_BY_STATUS[copyStatus];
|
|
45
|
-
return (React.createElement(TooltippedIconButton, { disabled: isCopyDisabled, className: props.className, tooltip: tooltip, placement: "top", onClick: copy, "aria-label":
|
|
47
|
+
return (React.createElement(TooltippedIconButton, { disabled: isCopyDisabled, className: props.className, tooltip: tooltip, placement: "top", onClick: copy, "aria-label": trans.__('Copy to clipboard'), inputToolbar: false },
|
|
46
48
|
React.createElement(copyIcon.react, { height: "16px", width: "16px" })));
|
|
47
49
|
}
|
|
@@ -6,13 +6,15 @@ import { FileDialog } from '@jupyterlab/filebrowser';
|
|
|
6
6
|
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
9
|
+
import { useTranslator } from '../../../context';
|
|
9
10
|
const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
|
|
10
11
|
/**
|
|
11
12
|
* The attach button.
|
|
12
13
|
*/
|
|
13
14
|
export function AttachButton(props) {
|
|
14
15
|
const { model } = props;
|
|
15
|
-
const
|
|
16
|
+
const trans = useTranslator();
|
|
17
|
+
const tooltip = trans.__('Add attachment');
|
|
16
18
|
if (!model.documentManager || !model.addAttachment) {
|
|
17
19
|
return React.createElement(React.Fragment, null);
|
|
18
20
|
}
|
|
@@ -22,7 +24,7 @@ export function AttachButton(props) {
|
|
|
22
24
|
}
|
|
23
25
|
try {
|
|
24
26
|
const files = await FileDialog.getOpenFiles({
|
|
25
|
-
title: 'Select files to attach',
|
|
27
|
+
title: trans.__('Select files to attach'),
|
|
26
28
|
manager: model.documentManager
|
|
27
29
|
});
|
|
28
30
|
if (files.value) {
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
8
|
+
import { useTranslator } from '../../../context';
|
|
8
9
|
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
9
10
|
/**
|
|
10
11
|
* The cancel button.
|
|
11
12
|
*/
|
|
12
13
|
export function CancelButton(props) {
|
|
14
|
+
const trans = useTranslator();
|
|
13
15
|
if (!props.model.cancel) {
|
|
14
16
|
return React.createElement(React.Fragment, null);
|
|
15
17
|
}
|
|
16
|
-
const tooltip = 'Cancel editing';
|
|
18
|
+
const tooltip = trans.__('Cancel editing');
|
|
17
19
|
return (React.createElement(TooltippedIconButton, { onClick: props.model.cancel, tooltip: tooltip, buttonProps: {
|
|
18
20
|
title: tooltip,
|
|
19
21
|
className: CANCEL_BUTTON_CLASS
|
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
import CheckIcon from '@mui/icons-material/Check';
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
7
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
8
|
+
import { useTranslator } from '../../../context';
|
|
8
9
|
const SAVE_EDIT_BUTTON_CLASS = 'jp-chat-save-edit-button';
|
|
9
10
|
/**
|
|
10
11
|
* The save edit button.
|
|
11
12
|
*/
|
|
12
13
|
export function SaveEditButton(props) {
|
|
13
14
|
const { model, chatCommandRegistry, edit } = props;
|
|
15
|
+
const trans = useTranslator();
|
|
14
16
|
// Don't show this button when not in edit mode
|
|
15
17
|
if (!edit) {
|
|
16
18
|
return React.createElement(React.Fragment, null);
|
|
17
19
|
}
|
|
18
20
|
const [disabled, setDisabled] = useState(false);
|
|
19
|
-
const tooltip = 'Save edits';
|
|
21
|
+
const tooltip = trans.__('Save edits');
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
var _a;
|
|
22
24
|
const inputChanged = () => {
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
7
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
8
|
+
import { useTranslator } from '../../../context';
|
|
8
9
|
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
9
10
|
/**
|
|
10
11
|
* The send button.
|
|
11
12
|
*/
|
|
12
13
|
export function SendButton(props) {
|
|
13
14
|
const { model, chatCommandRegistry, edit } = props;
|
|
15
|
+
const trans = useTranslator();
|
|
14
16
|
// Don't show this button when in edit mode
|
|
15
17
|
if (edit) {
|
|
16
18
|
return React.createElement(React.Fragment, null);
|
|
@@ -29,8 +31,8 @@ export function SendButton(props) {
|
|
|
29
31
|
const configChanged = (_, config) => {
|
|
30
32
|
var _a;
|
|
31
33
|
setTooltip(((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false)
|
|
32
|
-
? 'Send message (SHIFT+ENTER)'
|
|
33
|
-
: 'Send message (ENTER)');
|
|
34
|
+
? trans.__('Send message (SHIFT+ENTER)')
|
|
35
|
+
: trans.__('Send message (ENTER)'));
|
|
34
36
|
};
|
|
35
37
|
model.configChanged.connect(configChanged);
|
|
36
38
|
// Initialize the tooltip.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import StopIcon from '@mui/icons-material/Stop';
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
7
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
8
|
+
import { useTranslator } from '../../../context';
|
|
8
9
|
const STOP_BUTTON_CLASS = 'jp-chat-stop-button';
|
|
9
10
|
/**
|
|
10
11
|
* The stop button.
|
|
@@ -12,7 +13,8 @@ const STOP_BUTTON_CLASS = 'jp-chat-stop-button';
|
|
|
12
13
|
export function StopButton(props) {
|
|
13
14
|
const { chatModel } = props;
|
|
14
15
|
const [disabled, setDisabled] = useState(true);
|
|
15
|
-
const
|
|
16
|
+
const trans = useTranslator();
|
|
17
|
+
const tooltip = trans.__('Stop generating');
|
|
16
18
|
useEffect(() => {
|
|
17
19
|
var _a;
|
|
18
20
|
if (!chatModel) {
|
|
@@ -7,13 +7,14 @@ import clsx from 'clsx';
|
|
|
7
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { useChatCommands } from './use-chat-commands';
|
|
9
9
|
import { AttachmentPreviewList } from '../attachments';
|
|
10
|
-
import { useChatContext } from '../../context';
|
|
10
|
+
import { useChatContext, useTranslator } from '../../context';
|
|
11
11
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
12
12
|
const INPUT_TEXTFIELD_CLASS = 'jp-chat-input-textfield';
|
|
13
13
|
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
14
14
|
export function ChatInput(props) {
|
|
15
15
|
var _a;
|
|
16
16
|
const { model } = props;
|
|
17
|
+
const trans = useTranslator();
|
|
17
18
|
const { area, chatCommandRegistry, inputToolbarRegistry } = useChatContext();
|
|
18
19
|
const chatModel = useChatContext().model;
|
|
19
20
|
const [input, setInput] = useState(model.value);
|
|
@@ -162,7 +163,7 @@ export function ChatInput(props) {
|
|
|
162
163
|
padding: 0
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
|
-
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, onKeyDown: handleKeyDown, placeholder:
|
|
166
|
+
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, onKeyDown: handleKeyDown, placeholder: trans.__('Type a chat message, @ to mention...'), inputRef: inputRef, 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); }, sx: {
|
|
166
167
|
padding: 1.5,
|
|
167
168
|
margin: 0,
|
|
168
169
|
boxSizing: 'border-box',
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Box, Typography } from '@mui/material';
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
7
|
import { Avatar } from '../avatar';
|
|
8
|
+
import { useTranslator } from '../../context';
|
|
8
9
|
const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
|
|
9
10
|
const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
10
11
|
/**
|
|
@@ -12,6 +13,7 @@ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
|
12
13
|
*/
|
|
13
14
|
export function ChatMessageHeader(props) {
|
|
14
15
|
var _a, _b;
|
|
16
|
+
const trans = useTranslator();
|
|
15
17
|
const [message, setMessage] = useState(props.message.content);
|
|
16
18
|
// Flag to display only the deleted or edited information (stacked message).
|
|
17
19
|
const onlyState = message.stacked && (message.deleted || message.edited);
|
|
@@ -62,7 +64,7 @@ export function ChatMessageHeader(props) {
|
|
|
62
64
|
};
|
|
63
65
|
}, [props.message]);
|
|
64
66
|
const avatar = message.stacked ? null : Avatar({ user: sender });
|
|
65
|
-
const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || 'User undefined');
|
|
67
|
+
const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || trans.__('User undefined'));
|
|
66
68
|
// Don't render header for stacked messages not deleted or edited.
|
|
67
69
|
return message.stacked && !message.deleted && !message.edited ? (React.createElement(React.Fragment, null)) : (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
|
|
68
70
|
display: 'flex',
|
|
@@ -89,10 +91,12 @@ export function ChatMessageHeader(props) {
|
|
|
89
91
|
(message.deleted || message.edited) && (React.createElement(Typography, { sx: {
|
|
90
92
|
fontStyle: 'italic',
|
|
91
93
|
fontSize: 'var(--jp-content-font-size0)'
|
|
92
|
-
} }, message.deleted
|
|
94
|
+
} }, message.deleted
|
|
95
|
+
? trans.__('(message deleted)')
|
|
96
|
+
: trans.__('(edited)')))),
|
|
93
97
|
!onlyState && (React.createElement(Typography, { className: MESSAGE_TIME_CLASS, sx: {
|
|
94
98
|
fontSize: '0.8em',
|
|
95
99
|
color: 'var(--jp-ui-font-color2)',
|
|
96
100
|
fontWeight: 300
|
|
97
|
-
}, title: message.raw_time ? 'Unverified time' : '' }, `${datetime[message.time]}${message.raw_time ? '*' : ''}`)))));
|
|
101
|
+
}, title: message.raw_time ? trans.__('Unverified time') : '' }, `${datetime[message.time]}${message.raw_time ? '*' : ''}`)))));
|
|
98
102
|
}
|
|
@@ -31,24 +31,10 @@ function MessageRendererBase(props) {
|
|
|
31
31
|
let renderer;
|
|
32
32
|
let mimeModel;
|
|
33
33
|
// Create the renderer and the mime model.
|
|
34
|
-
if (
|
|
35
|
-
// Allow editing content for text messages.
|
|
36
|
-
setCanEdit(true);
|
|
37
|
-
// Improve users display in markdown content.
|
|
38
|
-
let mdStr = message.body;
|
|
39
|
-
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
40
|
-
mdStr = replaceMentionToSpan(mdStr, user);
|
|
41
|
-
});
|
|
42
|
-
// Body is a string, use the markdown renderer.
|
|
43
|
-
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
44
|
-
mimeModel = rmRegistry.createModel({
|
|
45
|
-
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
34
|
+
if (message.mime_model) {
|
|
49
35
|
setCanEdit(false);
|
|
50
36
|
// This is a mime bundle.
|
|
51
|
-
let mimeContent = message.
|
|
37
|
+
let mimeContent = message.mime_model;
|
|
52
38
|
let preferred = rmRegistry.preferredMimeType(mimeContent.data, 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
53
39
|
);
|
|
54
40
|
if (!preferred) {
|
|
@@ -64,7 +50,7 @@ function MessageRendererBase(props) {
|
|
|
64
50
|
if (preferred === DEFAULT_MIME_TYPE) {
|
|
65
51
|
let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
|
|
66
52
|
if (mdStr) {
|
|
67
|
-
(
|
|
53
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
68
54
|
mdStr = replaceMentionToSpan(mdStr, user);
|
|
69
55
|
});
|
|
70
56
|
mimeContent = {
|
|
@@ -81,6 +67,20 @@ function MessageRendererBase(props) {
|
|
|
81
67
|
}
|
|
82
68
|
mimeModel = rmRegistry.createModel(mimeContent);
|
|
83
69
|
}
|
|
70
|
+
else {
|
|
71
|
+
// Allow editing content for text messages.
|
|
72
|
+
setCanEdit(true);
|
|
73
|
+
// Improve users display in markdown content.
|
|
74
|
+
let mdStr = message.body;
|
|
75
|
+
(_b = message.mentions) === null || _b === void 0 ? void 0 : _b.forEach(user => {
|
|
76
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
77
|
+
});
|
|
78
|
+
// Body is a string, use the markdown renderer.
|
|
79
|
+
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
80
|
+
mimeModel = rmRegistry.createModel({
|
|
81
|
+
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
84
|
await renderer.renderModel(mimeModel);
|
|
85
85
|
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
86
86
|
// never been attached, only the node.
|
|
@@ -108,7 +108,7 @@ function MessageRendererBase(props) {
|
|
|
108
108
|
props.rendered.resolve();
|
|
109
109
|
};
|
|
110
110
|
renderContent();
|
|
111
|
-
}, [message.body, message.mentions, rmRegistry]);
|
|
111
|
+
}, [message.body, message.mime_model, message.mentions, rmRegistry]);
|
|
112
112
|
return (React.createElement(React.Fragment, null,
|
|
113
113
|
renderedContent && (React.createElement("div", { className: RENDERED_CLASS, ref: node => node && node.replaceChildren(renderedContent) })),
|
|
114
114
|
React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Button } from '@jupyter/react-components';
|
|
6
6
|
import { LabIcon, caretDownEmptyIcon, classes } from '@jupyterlab/ui-components';
|
|
7
7
|
import React, { useEffect, useState } from 'react';
|
|
8
|
-
import { useChatContext } from '../../context';
|
|
8
|
+
import { useChatContext, useTranslator } from '../../context';
|
|
9
9
|
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
10
10
|
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
|
|
11
11
|
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
|
|
@@ -15,6 +15,7 @@ const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
|
|
|
15
15
|
*/
|
|
16
16
|
export function Navigation(props) {
|
|
17
17
|
const { model } = useChatContext();
|
|
18
|
+
const trans = useTranslator();
|
|
18
19
|
const [lastInViewport, setLastInViewport] = useState(true);
|
|
19
20
|
const [unreadBefore, setUnreadBefore] = useState(null);
|
|
20
21
|
const [unreadAfter, setUnreadAfter] = useState(null);
|
|
@@ -88,12 +89,12 @@ export function Navigation(props) {
|
|
|
88
89
|
};
|
|
89
90
|
}, [model]);
|
|
90
91
|
return (React.createElement(React.Fragment, null,
|
|
91
|
-
unreadBefore !== null && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`, onClick: () => gotoMessage(unreadBefore), title: 'Go to unread messages' },
|
|
92
|
+
unreadBefore !== null && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`, onClick: () => gotoMessage(unreadBefore), title: trans.__('Go to unread messages') },
|
|
92
93
|
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') }))),
|
|
93
94
|
(unreadAfter !== null || !lastInViewport) && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`, onClick: unreadAfter === null
|
|
94
95
|
? () => gotoMessage(model.messages.length - 1, false)
|
|
95
96
|
: () => gotoMessage(unreadAfter), title: unreadAfter !== null
|
|
96
|
-
? 'Go to unread messages'
|
|
97
|
-
: 'Go to last message' },
|
|
97
|
+
? trans.__('Go to unread messages')
|
|
98
|
+
: trans.__('Go to last message') },
|
|
98
99
|
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') })))));
|
|
99
100
|
}
|
|
@@ -7,19 +7,21 @@ import EditIcon from '@mui/icons-material/Edit';
|
|
|
7
7
|
import { Box } from '@mui/material';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { TooltippedIconButton } from '../mui-extras';
|
|
10
|
+
import { useTranslator } from '../../context';
|
|
10
11
|
const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
11
12
|
/**
|
|
12
13
|
* The toolbar attached to a message.
|
|
13
14
|
*/
|
|
14
15
|
export function MessageToolbar(props) {
|
|
16
|
+
const trans = useTranslator();
|
|
15
17
|
const buttons = [];
|
|
16
18
|
if (props.edit !== undefined) {
|
|
17
|
-
const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
|
|
19
|
+
const editButton = (React.createElement(TooltippedIconButton, { tooltip: trans.__('Edit'), onClick: props.edit, "aria-label": trans.__('Edit'), inputToolbar: false },
|
|
18
20
|
React.createElement(EditIcon, null)));
|
|
19
21
|
buttons.push(editButton);
|
|
20
22
|
}
|
|
21
23
|
if (props.delete !== undefined) {
|
|
22
|
-
const deleteButton = (React.createElement(TooltippedIconButton, { tooltip: 'Delete', onClick: props.delete, "aria-label": 'Delete', inputToolbar: false },
|
|
24
|
+
const deleteButton = (React.createElement(TooltippedIconButton, { tooltip: trans.__('Delete'), onClick: props.delete, "aria-label": trans.__('Delete'), inputToolbar: false },
|
|
23
25
|
React.createElement(DeleteIcon, null)));
|
|
24
26
|
buttons.push(deleteButton);
|
|
25
27
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Box, Typography } from '@mui/material';
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import { useTranslator } from '../context';
|
|
7
8
|
/**
|
|
8
9
|
* Classname on the root element. Used in E2E tests.
|
|
9
10
|
*/
|
|
@@ -12,21 +13,24 @@ const WRITERS_ELEMENT_CLASSNAME = 'jp-chat-writers';
|
|
|
12
13
|
* Format the writers list into a readable string.
|
|
13
14
|
* Examples: "Alice is typing...", "Alice and Bob are typing...", "Alice, Bob, and Carol are typing..."
|
|
14
15
|
*/
|
|
15
|
-
function formatWritersText(writers) {
|
|
16
|
+
function formatWritersText(writers, trans) {
|
|
16
17
|
if (writers.length === 0) {
|
|
17
18
|
return '';
|
|
18
19
|
}
|
|
19
|
-
const names = writers.map(w => {
|
|
20
|
+
const names = writers.map(w => {
|
|
21
|
+
var _a, _b, _c;
|
|
22
|
+
return (_c = (_b = (_a = w.user.display_name) !== null && _a !== void 0 ? _a : w.user.name) !== null && _b !== void 0 ? _b : w.user.username) !== null && _c !== void 0 ? _c : trans.__('Unknown');
|
|
23
|
+
});
|
|
20
24
|
if (names.length === 1) {
|
|
21
|
-
return
|
|
25
|
+
return trans.__('%1 is typing...', names[0]);
|
|
22
26
|
}
|
|
23
27
|
else if (names.length === 2) {
|
|
24
|
-
return
|
|
28
|
+
return trans.__('%1 and %2 are typing...', names[0], names[1]);
|
|
25
29
|
}
|
|
26
30
|
else {
|
|
27
31
|
const allButLast = names.slice(0, -1).join(', ');
|
|
28
32
|
const last = names[names.length - 1];
|
|
29
|
-
return
|
|
33
|
+
return trans.__('%1, and %2 are typing...', allButLast, last);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
@@ -34,8 +38,9 @@ function formatWritersText(writers) {
|
|
|
34
38
|
*/
|
|
35
39
|
export function WritingIndicator(props) {
|
|
36
40
|
const { writers } = props;
|
|
41
|
+
const trans = useTranslator();
|
|
37
42
|
// Always render the container to reserve space, even if no writers
|
|
38
|
-
const writersText = writers.length > 0 ? formatWritersText(writers) : '';
|
|
43
|
+
const writersText = writers.length > 0 ? formatWritersText(writers, trans) : '';
|
|
39
44
|
return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME, sx: {
|
|
40
45
|
...props.sx,
|
|
41
46
|
minHeight: '16px'
|
package/lib/context.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
3
|
import { Chat } from './components';
|
|
4
|
+
export declare const TRANSLATION_DOMAIN = "jupyter-chat";
|
|
3
5
|
export declare const ChatReactContext: import("react").Context<Chat.IChatProps | undefined>;
|
|
4
6
|
export declare function useChatContext(): Chat.IChatProps;
|
|
7
|
+
/**
|
|
8
|
+
* Hook to get the translation bundle for the chat.
|
|
9
|
+
* Must be used within a ChatReactContext.Provider.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useTranslator(): TranslationBundle;
|
package/lib/context.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
5
6
|
import { createContext, useContext } from 'react';
|
|
7
|
+
export const TRANSLATION_DOMAIN = 'jupyter-chat';
|
|
6
8
|
export const ChatReactContext = createContext(undefined);
|
|
7
9
|
export function useChatContext() {
|
|
8
10
|
const context = useContext(ChatReactContext);
|
|
@@ -11,3 +13,13 @@ export function useChatContext() {
|
|
|
11
13
|
}
|
|
12
14
|
return context;
|
|
13
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Hook to get the translation bundle for the chat.
|
|
18
|
+
* Must be used within a ChatReactContext.Provider.
|
|
19
|
+
*/
|
|
20
|
+
export function useTranslator() {
|
|
21
|
+
var _a;
|
|
22
|
+
const context = useContext(ChatReactContext);
|
|
23
|
+
const translator = (_a = context === null || context === void 0 ? void 0 : context.translator) !== null && _a !== void 0 ? _a : nullTranslator;
|
|
24
|
+
return translator.load(TRANSLATION_DOMAIN);
|
|
25
|
+
}
|
package/lib/message.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare class Message implements IMessage {
|
|
|
18
18
|
* Getters for each attribute individually.
|
|
19
19
|
*/
|
|
20
20
|
get type(): string;
|
|
21
|
-
get body(): string
|
|
21
|
+
get body(): string;
|
|
22
22
|
get id(): string;
|
|
23
23
|
get time(): number;
|
|
24
24
|
get sender(): IUser;
|
|
@@ -29,6 +29,7 @@ export declare class Message implements IMessage {
|
|
|
29
29
|
get edited(): boolean | undefined;
|
|
30
30
|
get stacked(): boolean | undefined;
|
|
31
31
|
get metadata(): IMessageMetadata | undefined;
|
|
32
|
+
get mime_model(): IMimeModelBody | undefined;
|
|
32
33
|
/**
|
|
33
34
|
* A signal emitting when the message has been updated.
|
|
34
35
|
*/
|