@jupyter/chat 0.20.0 → 0.21.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__/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 +17 -17
- 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 +17 -17
- 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
|
@@ -10,7 +10,7 @@ import { PathExt } from '@jupyterlab/coreutils';
|
|
|
10
10
|
import { UUID } from '@lumino/coreutils';
|
|
11
11
|
|
|
12
12
|
import { TooltippedIconButton } from './mui-extras';
|
|
13
|
-
import { useChatContext } from '../context';
|
|
13
|
+
import { useChatContext, useTranslator } from '../context';
|
|
14
14
|
import { IAttachment } from '../types';
|
|
15
15
|
|
|
16
16
|
const ATTACHMENT_CLASS = 'jp-chat-attachment';
|
|
@@ -87,7 +87,7 @@ export type AttachmentProps = AttachmentsProps & {
|
|
|
87
87
|
* The Attachment component.
|
|
88
88
|
*/
|
|
89
89
|
export function AttachmentPreview(props: AttachmentProps): JSX.Element {
|
|
90
|
-
const
|
|
90
|
+
const trans = useTranslator();
|
|
91
91
|
const { attachmentOpenerRegistry } = useChatContext();
|
|
92
92
|
const isClickable = !!attachmentOpenerRegistry?.get(props.attachment.type);
|
|
93
93
|
|
|
@@ -133,7 +133,7 @@ export function AttachmentPreview(props: AttachmentProps): JSX.Element {
|
|
|
133
133
|
</Tooltip>
|
|
134
134
|
{props.onRemove && (
|
|
135
135
|
<TooltippedIconButton
|
|
136
|
-
tooltip={
|
|
136
|
+
tooltip={trans.__('Remove attachment')}
|
|
137
137
|
onClick={() => props.onRemove!(props.attachment)}
|
|
138
138
|
className={REMOVE_BUTTON_CLASS}
|
|
139
139
|
inputToolbar={false}
|
package/src/components/chat.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
7
7
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
8
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
8
9
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
9
10
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
10
11
|
import { IconButton } from '@mui/material';
|
|
@@ -181,6 +182,10 @@ export namespace Chat {
|
|
|
181
182
|
* The area where the chat is displayed.
|
|
182
183
|
*/
|
|
183
184
|
area?: ChatArea;
|
|
185
|
+
/**
|
|
186
|
+
* The translator for internationalization.
|
|
187
|
+
*/
|
|
188
|
+
translator?: ITranslator;
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
/**
|
|
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
9
9
|
|
|
10
10
|
import { CopyButton } from './copy-button';
|
|
11
11
|
import { TooltippedIconButton } from '../mui-extras';
|
|
12
|
+
import { useTranslator } from '../../context';
|
|
12
13
|
import { IActiveCellManager } from '../../active-cell-manager';
|
|
13
14
|
import { replaceCellIcon } from '../../icons';
|
|
14
15
|
import { IChatModel } from '../../model';
|
|
@@ -110,9 +111,10 @@ type ToolbarButtonProps = {
|
|
|
110
111
|
};
|
|
111
112
|
|
|
112
113
|
function InsertAboveButton(props: ToolbarButtonProps) {
|
|
114
|
+
const trans = useTranslator();
|
|
113
115
|
const tooltip = props.activeCellAvailable
|
|
114
|
-
? 'Insert above active cell'
|
|
115
|
-
: 'Insert above active cell (no active cell)';
|
|
116
|
+
? trans.__('Insert above active cell')
|
|
117
|
+
: trans.__('Insert above active cell (no active cell)');
|
|
116
118
|
|
|
117
119
|
return (
|
|
118
120
|
<TooltippedIconButton
|
|
@@ -128,9 +130,10 @@ function InsertAboveButton(props: ToolbarButtonProps) {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
function InsertBelowButton(props: ToolbarButtonProps) {
|
|
133
|
+
const trans = useTranslator();
|
|
131
134
|
const tooltip = props.activeCellAvailable
|
|
132
|
-
? 'Insert below active cell'
|
|
133
|
-
: 'Insert below active cell (no active cell)';
|
|
135
|
+
? trans.__('Insert below active cell')
|
|
136
|
+
: trans.__('Insert below active cell (no active cell)');
|
|
134
137
|
|
|
135
138
|
return (
|
|
136
139
|
<TooltippedIconButton
|
|
@@ -146,11 +149,15 @@ function InsertBelowButton(props: ToolbarButtonProps) {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
function ReplaceButton(props: ToolbarButtonProps) {
|
|
152
|
+
const trans = useTranslator();
|
|
149
153
|
const tooltip = props.selectionExists
|
|
150
|
-
?
|
|
154
|
+
? trans.__(
|
|
155
|
+
'Replace selection (%1 line(s))',
|
|
156
|
+
props.selectionWatcher?.selection?.numLines ?? 0
|
|
157
|
+
)
|
|
151
158
|
: props.activeCellAvailable
|
|
152
|
-
? 'Replace selection (active cell)'
|
|
153
|
-
: 'Replace selection (no selection)';
|
|
159
|
+
? trans.__('Replace selection (active cell)')
|
|
160
|
+
: trans.__('Replace selection (no selection)');
|
|
154
161
|
|
|
155
162
|
const disabled = !props.activeCellAvailable && !props.selectionExists;
|
|
156
163
|
|
|
@@ -7,6 +7,7 @@ import { copyIcon } from '@jupyterlab/ui-components';
|
|
|
7
7
|
import React, { useState, useCallback, useRef } from 'react';
|
|
8
8
|
|
|
9
9
|
import { TooltippedIconButton } from '../mui-extras';
|
|
10
|
+
import { useTranslator } from '../../context';
|
|
10
11
|
|
|
11
12
|
enum CopyStatus {
|
|
12
13
|
None,
|
|
@@ -15,19 +16,13 @@ enum CopyStatus {
|
|
|
15
16
|
Disabled
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const COPYBTN_TEXT_BY_STATUS: Record<CopyStatus, string> = {
|
|
19
|
-
[CopyStatus.None]: 'Copy to clipboard',
|
|
20
|
-
[CopyStatus.Copying]: 'Copying…',
|
|
21
|
-
[CopyStatus.Copied]: 'Copied!',
|
|
22
|
-
[CopyStatus.Disabled]: 'Copy to clipboard disabled in insecure context'
|
|
23
|
-
};
|
|
24
|
-
|
|
25
19
|
type CopyButtonProps = {
|
|
26
20
|
value: string;
|
|
27
21
|
className?: string;
|
|
28
22
|
};
|
|
29
23
|
|
|
30
24
|
export function CopyButton(props: CopyButtonProps): JSX.Element {
|
|
25
|
+
const trans = useTranslator();
|
|
31
26
|
const isCopyDisabled = navigator.clipboard === undefined;
|
|
32
27
|
const [copyStatus, setCopyStatus] = useState<CopyStatus>(
|
|
33
28
|
isCopyDisabled ? CopyStatus.Disabled : CopyStatus.None
|
|
@@ -58,6 +53,15 @@ export function CopyButton(props: CopyButtonProps): JSX.Element {
|
|
|
58
53
|
);
|
|
59
54
|
}, [copyStatus, props.value]);
|
|
60
55
|
|
|
56
|
+
const COPYBTN_TEXT_BY_STATUS: Record<CopyStatus, string> = {
|
|
57
|
+
[CopyStatus.None]: trans.__('Copy to clipboard'),
|
|
58
|
+
[CopyStatus.Copying]: trans.__('Copying…'),
|
|
59
|
+
[CopyStatus.Copied]: trans.__('Copied!'),
|
|
60
|
+
[CopyStatus.Disabled]: trans.__(
|
|
61
|
+
'Copy to clipboard disabled in insecure context'
|
|
62
|
+
)
|
|
63
|
+
};
|
|
64
|
+
|
|
61
65
|
const tooltip = COPYBTN_TEXT_BY_STATUS[copyStatus];
|
|
62
66
|
|
|
63
67
|
return (
|
|
@@ -67,7 +71,7 @@ export function CopyButton(props: CopyButtonProps): JSX.Element {
|
|
|
67
71
|
tooltip={tooltip}
|
|
68
72
|
placement="top"
|
|
69
73
|
onClick={copy}
|
|
70
|
-
aria-label=
|
|
74
|
+
aria-label={trans.__('Copy to clipboard')}
|
|
71
75
|
inputToolbar={false}
|
|
72
76
|
>
|
|
73
77
|
<copyIcon.react height="16px" width="16px" />
|
|
@@ -9,6 +9,7 @@ import React from 'react';
|
|
|
9
9
|
|
|
10
10
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
11
11
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
12
|
+
import { useTranslator } from '../../../context';
|
|
12
13
|
|
|
13
14
|
const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button';
|
|
14
15
|
|
|
@@ -19,7 +20,8 @@ export function AttachButton(
|
|
|
19
20
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
20
21
|
): JSX.Element {
|
|
21
22
|
const { model } = props;
|
|
22
|
-
const
|
|
23
|
+
const trans = useTranslator();
|
|
24
|
+
const tooltip = trans.__('Add attachment');
|
|
23
25
|
|
|
24
26
|
if (!model.documentManager || !model.addAttachment) {
|
|
25
27
|
return <></>;
|
|
@@ -31,7 +33,7 @@ export function AttachButton(
|
|
|
31
33
|
}
|
|
32
34
|
try {
|
|
33
35
|
const files = await FileDialog.getOpenFiles({
|
|
34
|
-
title: 'Select files to attach',
|
|
36
|
+
title: trans.__('Select files to attach'),
|
|
35
37
|
manager: model.documentManager
|
|
36
38
|
});
|
|
37
39
|
if (files.value) {
|
|
@@ -8,6 +8,7 @@ import React from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
10
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
|
+
import { useTranslator } from '../../../context';
|
|
11
12
|
|
|
12
13
|
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
13
14
|
|
|
@@ -17,10 +18,12 @@ const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
|
17
18
|
export function CancelButton(
|
|
18
19
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
19
20
|
): JSX.Element {
|
|
21
|
+
const trans = useTranslator();
|
|
22
|
+
|
|
20
23
|
if (!props.model.cancel) {
|
|
21
24
|
return <></>;
|
|
22
25
|
}
|
|
23
|
-
const tooltip = 'Cancel editing';
|
|
26
|
+
const tooltip = trans.__('Cancel editing');
|
|
24
27
|
return (
|
|
25
28
|
<TooltippedIconButton
|
|
26
29
|
onClick={props.model.cancel}
|
|
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
10
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
|
+
import { useTranslator } from '../../../context';
|
|
11
12
|
|
|
12
13
|
const SAVE_EDIT_BUTTON_CLASS = 'jp-chat-save-edit-button';
|
|
13
14
|
|
|
@@ -18,6 +19,7 @@ export function SaveEditButton(
|
|
|
18
19
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
19
20
|
): JSX.Element {
|
|
20
21
|
const { model, chatCommandRegistry, edit } = props;
|
|
22
|
+
const trans = useTranslator();
|
|
21
23
|
|
|
22
24
|
// Don't show this button when not in edit mode
|
|
23
25
|
if (!edit) {
|
|
@@ -25,7 +27,7 @@ export function SaveEditButton(
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const [disabled, setDisabled] = useState(false);
|
|
28
|
-
const tooltip = 'Save edits';
|
|
30
|
+
const tooltip = trans.__('Save edits');
|
|
29
31
|
|
|
30
32
|
useEffect(() => {
|
|
31
33
|
const inputChanged = () => {
|
|
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
10
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
|
+
import { useTranslator } from '../../../context';
|
|
11
12
|
import { IInputModel, InputModel } from '../../../input-model';
|
|
12
13
|
|
|
13
14
|
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
@@ -19,6 +20,7 @@ export function SendButton(
|
|
|
19
20
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
20
21
|
): JSX.Element {
|
|
21
22
|
const { model, chatCommandRegistry, edit } = props;
|
|
23
|
+
const trans = useTranslator();
|
|
22
24
|
|
|
23
25
|
// Don't show this button when in edit mode
|
|
24
26
|
if (edit) {
|
|
@@ -42,8 +44,8 @@ export function SendButton(
|
|
|
42
44
|
const configChanged = (_: IInputModel, config: InputModel.IConfig) => {
|
|
43
45
|
setTooltip(
|
|
44
46
|
(config.sendWithShiftEnter ?? false)
|
|
45
|
-
? 'Send message (SHIFT+ENTER)'
|
|
46
|
-
: 'Send message (ENTER)'
|
|
47
|
+
? trans.__('Send message (SHIFT+ENTER)')
|
|
48
|
+
: trans.__('Send message (ENTER)')
|
|
47
49
|
);
|
|
48
50
|
};
|
|
49
51
|
model.configChanged.connect(configChanged);
|
|
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { InputToolbarRegistry } from '../toolbar-registry';
|
|
10
10
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
11
|
+
import { useTranslator } from '../../../context';
|
|
11
12
|
|
|
12
13
|
const STOP_BUTTON_CLASS = 'jp-chat-stop-button';
|
|
13
14
|
|
|
@@ -19,7 +20,8 @@ export function StopButton(
|
|
|
19
20
|
): JSX.Element {
|
|
20
21
|
const { chatModel } = props;
|
|
21
22
|
const [disabled, setDisabled] = useState(true);
|
|
22
|
-
const
|
|
23
|
+
const trans = useTranslator();
|
|
24
|
+
const tooltip = trans.__('Stop generating');
|
|
23
25
|
|
|
24
26
|
useEffect(() => {
|
|
25
27
|
if (!chatModel) {
|
|
@@ -17,7 +17,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
17
17
|
import { InputToolbarRegistry } from './toolbar-registry';
|
|
18
18
|
import { useChatCommands } from './use-chat-commands';
|
|
19
19
|
import { AttachmentPreviewList } from '../attachments';
|
|
20
|
-
import { useChatContext } from '../../context';
|
|
20
|
+
import { useChatContext, useTranslator } from '../../context';
|
|
21
21
|
import { IInputModel, InputModel } from '../../input-model';
|
|
22
22
|
import { IAttachment } from '../../types';
|
|
23
23
|
|
|
@@ -27,6 +27,7 @@ const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
|
27
27
|
|
|
28
28
|
export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
29
29
|
const { model } = props;
|
|
30
|
+
const trans = useTranslator();
|
|
30
31
|
const { area, chatCommandRegistry, inputToolbarRegistry } = useChatContext();
|
|
31
32
|
const chatModel = useChatContext().model;
|
|
32
33
|
|
|
@@ -230,7 +231,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
230
231
|
multiline
|
|
231
232
|
maxRows={10}
|
|
232
233
|
onKeyDown={handleKeyDown}
|
|
233
|
-
placeholder=
|
|
234
|
+
placeholder={trans.__('Type a chat message, @ to mention...')}
|
|
234
235
|
inputRef={inputRef}
|
|
235
236
|
onSelect={() =>
|
|
236
237
|
(model.cursorIndex = inputRef.current?.selectionStart ?? null)
|
|
@@ -7,6 +7,7 @@ import { Box, Typography } from '@mui/material';
|
|
|
7
7
|
import React, { useEffect, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Avatar } from '../avatar';
|
|
10
|
+
import { useTranslator } from '../../context';
|
|
10
11
|
import { IMessageContent, IMessage } from '../../types';
|
|
11
12
|
|
|
12
13
|
const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
|
|
@@ -30,6 +31,7 @@ type ChatMessageHeaderProps = {
|
|
|
30
31
|
* The message header component.
|
|
31
32
|
*/
|
|
32
33
|
export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
34
|
+
const trans = useTranslator();
|
|
33
35
|
const [message, setMessage] = useState<IMessageContent>(
|
|
34
36
|
props.message.content
|
|
35
37
|
);
|
|
@@ -90,7 +92,9 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
|
90
92
|
const avatar = message.stacked ? null : Avatar({ user: sender });
|
|
91
93
|
|
|
92
94
|
const name =
|
|
93
|
-
sender.display_name ??
|
|
95
|
+
sender.display_name ??
|
|
96
|
+
sender.name ??
|
|
97
|
+
(sender.username || trans.__('User undefined'));
|
|
94
98
|
|
|
95
99
|
// Don't render header for stacked messages not deleted or edited.
|
|
96
100
|
return message.stacked && !message.deleted && !message.edited ? (
|
|
@@ -136,7 +140,9 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
|
136
140
|
fontSize: 'var(--jp-content-font-size0)'
|
|
137
141
|
}}
|
|
138
142
|
>
|
|
139
|
-
{message.deleted
|
|
143
|
+
{message.deleted
|
|
144
|
+
? trans.__('(message deleted)')
|
|
145
|
+
: trans.__('(edited)')}
|
|
140
146
|
</Typography>
|
|
141
147
|
)}
|
|
142
148
|
</Box>
|
|
@@ -148,7 +154,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
|
148
154
|
color: 'var(--jp-ui-font-color2)',
|
|
149
155
|
fontWeight: 300
|
|
150
156
|
}}
|
|
151
|
-
title={message.raw_time ? 'Unverified time' : ''}
|
|
157
|
+
title={message.raw_time ? trans.__('Unverified time') : ''}
|
|
152
158
|
>
|
|
153
159
|
{`${datetime[message.time]}${message.raw_time ? '*' : ''}`}
|
|
154
160
|
</Typography>
|
|
@@ -68,25 +68,10 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
68
68
|
let mimeModel: IRenderMime.IMimeModel;
|
|
69
69
|
|
|
70
70
|
// Create the renderer and the mime model.
|
|
71
|
-
if (
|
|
72
|
-
// Allow editing content for text messages.
|
|
73
|
-
setCanEdit(true);
|
|
74
|
-
|
|
75
|
-
// Improve users display in markdown content.
|
|
76
|
-
let mdStr = message.body;
|
|
77
|
-
message.mentions?.forEach(user => {
|
|
78
|
-
mdStr = replaceMentionToSpan(mdStr, user);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Body is a string, use the markdown renderer.
|
|
82
|
-
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
83
|
-
mimeModel = rmRegistry.createModel({
|
|
84
|
-
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
85
|
-
});
|
|
86
|
-
} else {
|
|
71
|
+
if (message.mime_model) {
|
|
87
72
|
setCanEdit(false);
|
|
88
73
|
// This is a mime bundle.
|
|
89
|
-
let mimeContent = message.
|
|
74
|
+
let mimeContent = message.mime_model;
|
|
90
75
|
let preferred = rmRegistry.preferredMimeType(
|
|
91
76
|
mimeContent.data,
|
|
92
77
|
'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
@@ -121,6 +106,21 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
121
106
|
}
|
|
122
107
|
|
|
123
108
|
mimeModel = rmRegistry.createModel(mimeContent);
|
|
109
|
+
} else {
|
|
110
|
+
// Allow editing content for text messages.
|
|
111
|
+
setCanEdit(true);
|
|
112
|
+
|
|
113
|
+
// Improve users display in markdown content.
|
|
114
|
+
let mdStr = message.body;
|
|
115
|
+
message.mentions?.forEach(user => {
|
|
116
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Body is a string, use the markdown renderer.
|
|
120
|
+
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
121
|
+
mimeModel = rmRegistry.createModel({
|
|
122
|
+
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
123
|
+
});
|
|
124
124
|
}
|
|
125
125
|
await renderer.renderModel(mimeModel);
|
|
126
126
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from '@jupyterlab/ui-components';
|
|
12
12
|
import React, { useEffect, useState } from 'react';
|
|
13
13
|
|
|
14
|
-
import { useChatContext } from '../../context';
|
|
14
|
+
import { useChatContext, useTranslator } from '../../context';
|
|
15
15
|
import { IChatModel } from '../../model';
|
|
16
16
|
|
|
17
17
|
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
@@ -38,6 +38,7 @@ type NavigationProps = {
|
|
|
38
38
|
*/
|
|
39
39
|
export function Navigation(props: NavigationProps): JSX.Element {
|
|
40
40
|
const { model } = useChatContext();
|
|
41
|
+
const trans = useTranslator();
|
|
41
42
|
const [lastInViewport, setLastInViewport] = useState<boolean>(true);
|
|
42
43
|
const [unreadBefore, setUnreadBefore] = useState<number | null>(null);
|
|
43
44
|
const [unreadAfter, setUnreadAfter] = useState<number | null>(null);
|
|
@@ -132,7 +133,7 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
132
133
|
<Button
|
|
133
134
|
className={`${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`}
|
|
134
135
|
onClick={() => gotoMessage!(unreadBefore)}
|
|
135
|
-
title={'Go to unread messages'}
|
|
136
|
+
title={trans.__('Go to unread messages')}
|
|
136
137
|
>
|
|
137
138
|
<LabIcon.resolveReact
|
|
138
139
|
display={'flex'}
|
|
@@ -151,8 +152,8 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
151
152
|
}
|
|
152
153
|
title={
|
|
153
154
|
unreadAfter !== null
|
|
154
|
-
? 'Go to unread messages'
|
|
155
|
-
: 'Go to last message'
|
|
155
|
+
? trans.__('Go to unread messages')
|
|
156
|
+
: trans.__('Go to last message')
|
|
156
157
|
}
|
|
157
158
|
>
|
|
158
159
|
<LabIcon.resolveReact
|
|
@@ -9,6 +9,7 @@ import { Box } from '@mui/material';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
|
|
11
11
|
import { TooltippedIconButton } from '../mui-extras';
|
|
12
|
+
import { useTranslator } from '../../context';
|
|
12
13
|
|
|
13
14
|
const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
14
15
|
|
|
@@ -16,14 +17,15 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
|
16
17
|
* The toolbar attached to a message.
|
|
17
18
|
*/
|
|
18
19
|
export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
|
|
20
|
+
const trans = useTranslator();
|
|
19
21
|
const buttons: JSX.Element[] = [];
|
|
20
22
|
|
|
21
23
|
if (props.edit !== undefined) {
|
|
22
24
|
const editButton = (
|
|
23
25
|
<TooltippedIconButton
|
|
24
|
-
tooltip={'Edit'}
|
|
26
|
+
tooltip={trans.__('Edit')}
|
|
25
27
|
onClick={props.edit}
|
|
26
|
-
aria-label={'Edit'}
|
|
28
|
+
aria-label={trans.__('Edit')}
|
|
27
29
|
inputToolbar={false}
|
|
28
30
|
>
|
|
29
31
|
<EditIcon />
|
|
@@ -34,9 +36,9 @@ export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
|
|
|
34
36
|
if (props.delete !== undefined) {
|
|
35
37
|
const deleteButton = (
|
|
36
38
|
<TooltippedIconButton
|
|
37
|
-
tooltip={'Delete'}
|
|
39
|
+
tooltip={trans.__('Delete')}
|
|
38
40
|
onClick={props.delete}
|
|
39
|
-
aria-label={'Delete'}
|
|
41
|
+
aria-label={trans.__('Delete')}
|
|
40
42
|
inputToolbar={false}
|
|
41
43
|
>
|
|
42
44
|
<DeleteIcon />
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
6
7
|
import { Box, SxProps, Theme, Typography } from '@mui/material';
|
|
7
8
|
import React from 'react';
|
|
8
9
|
|
|
10
|
+
import { useTranslator } from '../context';
|
|
9
11
|
import { IChatModel } from '../model';
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -31,23 +33,30 @@ export interface IInputWritingIndicatorProps {
|
|
|
31
33
|
* Format the writers list into a readable string.
|
|
32
34
|
* Examples: "Alice is typing...", "Alice and Bob are typing...", "Alice, Bob, and Carol are typing..."
|
|
33
35
|
*/
|
|
34
|
-
function formatWritersText(
|
|
36
|
+
function formatWritersText(
|
|
37
|
+
writers: IChatModel.IWriter[],
|
|
38
|
+
trans: TranslationBundle
|
|
39
|
+
): string {
|
|
35
40
|
if (writers.length === 0) {
|
|
36
41
|
return '';
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const names = writers.map(
|
|
40
|
-
w =>
|
|
45
|
+
w =>
|
|
46
|
+
w.user.display_name ??
|
|
47
|
+
w.user.name ??
|
|
48
|
+
w.user.username ??
|
|
49
|
+
trans.__('Unknown')
|
|
41
50
|
);
|
|
42
51
|
|
|
43
52
|
if (names.length === 1) {
|
|
44
|
-
return
|
|
53
|
+
return trans.__('%1 is typing...', names[0]);
|
|
45
54
|
} else if (names.length === 2) {
|
|
46
|
-
return
|
|
55
|
+
return trans.__('%1 and %2 are typing...', names[0], names[1]);
|
|
47
56
|
} else {
|
|
48
57
|
const allButLast = names.slice(0, -1).join(', ');
|
|
49
58
|
const last = names[names.length - 1];
|
|
50
|
-
return
|
|
59
|
+
return trans.__('%1, and %2 are typing...', allButLast, last);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -58,9 +67,11 @@ export function WritingIndicator(
|
|
|
58
67
|
props: IInputWritingIndicatorProps
|
|
59
68
|
): JSX.Element {
|
|
60
69
|
const { writers } = props;
|
|
70
|
+
const trans = useTranslator();
|
|
61
71
|
|
|
62
72
|
// Always render the container to reserve space, even if no writers
|
|
63
|
-
const writersText =
|
|
73
|
+
const writersText =
|
|
74
|
+
writers.length > 0 ? formatWritersText(writers, trans) : '';
|
|
64
75
|
|
|
65
76
|
return (
|
|
66
77
|
<Box
|
package/src/context.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
6
7
|
import { createContext, useContext } from 'react';
|
|
7
8
|
|
|
8
9
|
import { Chat } from './components';
|
|
9
10
|
|
|
11
|
+
export const TRANSLATION_DOMAIN = 'jupyter-chat';
|
|
12
|
+
|
|
10
13
|
export const ChatReactContext = createContext<Chat.IChatProps | undefined>(
|
|
11
14
|
undefined
|
|
12
15
|
);
|
|
@@ -18,3 +21,13 @@ export function useChatContext(): Chat.IChatProps {
|
|
|
18
21
|
}
|
|
19
22
|
return context;
|
|
20
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to get the translation bundle for the chat.
|
|
27
|
+
* Must be used within a ChatReactContext.Provider.
|
|
28
|
+
*/
|
|
29
|
+
export function useTranslator(): TranslationBundle {
|
|
30
|
+
const context = useContext(ChatReactContext);
|
|
31
|
+
const translator = context?.translator ?? nullTranslator;
|
|
32
|
+
return translator.load(TRANSLATION_DOMAIN);
|
|
33
|
+
}
|
package/src/message.ts
CHANGED
|
@@ -39,7 +39,7 @@ export class Message implements IMessage {
|
|
|
39
39
|
get type(): string {
|
|
40
40
|
return this._content.type;
|
|
41
41
|
}
|
|
42
|
-
get body(): string
|
|
42
|
+
get body(): string {
|
|
43
43
|
return this._content.body;
|
|
44
44
|
}
|
|
45
45
|
get id(): string {
|
|
@@ -72,6 +72,9 @@ export class Message implements IMessage {
|
|
|
72
72
|
get metadata(): IMessageMetadata | undefined {
|
|
73
73
|
return this._content.metadata;
|
|
74
74
|
}
|
|
75
|
+
get mime_model(): IMimeModelBody | undefined {
|
|
76
|
+
return this._content.mime_model;
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
80
|
* A signal emitting when the message has been updated.
|