@jupyter/chat 0.19.0-alpha.3 → 0.20.0-alpha.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 +2 -2
- package/lib/components/chat.js +29 -3
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/input/chat-input.js +1 -25
- package/lib/components/input/index.d.ts +0 -1
- package/lib/components/input/index.js +0 -1
- package/lib/components/messages/footer.d.ts +2 -2
- package/lib/components/messages/header.d.ts +2 -2
- package/lib/components/messages/header.js +13 -6
- package/lib/components/messages/message-renderer.d.ts +3 -6
- package/lib/components/messages/message-renderer.js +104 -29
- package/lib/components/messages/message.d.ts +2 -2
- package/lib/components/messages/message.js +18 -3
- package/lib/components/messages/messages.js +3 -2
- package/lib/components/messages/toolbar.js +6 -14
- package/lib/components/messages/welcome.js +24 -14
- package/lib/components/writing-indicator.d.ts +20 -0
- package/lib/components/{input/writing-indicator.js → writing-indicator.js} +3 -2
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/message.d.ts +42 -0
- package/lib/message.js +74 -0
- package/lib/model.d.ts +13 -13
- package/lib/model.js +9 -12
- package/lib/registers/footers.d.ts +2 -2
- package/lib/theme-provider.js +10 -9
- package/lib/types.d.ts +23 -4
- package/lib/widgets/chat-widget.js +18 -11
- package/package.json +1 -1
- package/src/__tests__/model.spec.ts +7 -7
- package/src/components/chat.tsx +35 -2
- package/src/components/index.ts +1 -0
- package/src/components/input/chat-input.tsx +0 -28
- package/src/components/input/index.ts +0 -1
- package/src/components/messages/footer.tsx +2 -2
- package/src/components/messages/header.tsx +20 -8
- package/src/components/messages/message-renderer.tsx +125 -44
- package/src/components/messages/message.tsx +25 -5
- package/src/components/messages/messages.tsx +9 -4
- package/src/components/messages/toolbar.tsx +14 -14
- package/src/components/messages/welcome.tsx +25 -17
- package/src/components/{input/writing-indicator.tsx → writing-indicator.tsx} +9 -4
- package/src/index.ts +0 -1
- package/src/message.ts +86 -0
- package/src/model.ts +25 -26
- package/src/registers/footers.ts +2 -2
- package/src/theme-provider.ts +10 -9
- package/src/types.ts +28 -4
- package/src/widgets/chat-widget.tsx +22 -14
- package/style/chat.css +14 -9
- package/style/input.css +3 -3
- package/lib/components/input/writing-indicator.d.ts +0 -15
- package/lib/markdown-renderer.d.ts +0 -38
- package/lib/markdown-renderer.js +0 -54
- package/src/markdown-renderer.ts +0 -78
|
@@ -52,7 +52,7 @@ describe('test chat model', () => {
|
|
|
52
52
|
});
|
|
53
53
|
model.messageAdded(msg);
|
|
54
54
|
expect(messages).toHaveLength(1);
|
|
55
|
-
expect(messages[0]).toBe(msg);
|
|
55
|
+
expect(messages[0].content).toBe(msg);
|
|
56
56
|
});
|
|
57
57
|
it('should format message', () => {
|
|
58
58
|
model = new TestChat();
|
|
@@ -62,7 +62,7 @@ describe('test chat model', () => {
|
|
|
62
62
|
});
|
|
63
63
|
model.messageAdded({ ...msg });
|
|
64
64
|
expect(messages).toHaveLength(1);
|
|
65
|
-
expect(messages[0]).not.toBe(msg);
|
|
65
|
+
expect(messages[0].content).not.toBe(msg);
|
|
66
66
|
expect(messages[0].body).toBe('formatted msg');
|
|
67
67
|
});
|
|
68
68
|
});
|
package/lib/components/chat.js
CHANGED
|
@@ -6,18 +6,40 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
6
6
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
7
7
|
import { IconButton } from '@mui/material';
|
|
8
8
|
import { Box } from '@mui/system';
|
|
9
|
-
import React, { useState } from 'react';
|
|
9
|
+
import React, { useEffect, useState } from 'react';
|
|
10
10
|
import { ChatInput, InputToolbarRegistry } from './input';
|
|
11
11
|
import { JlThemeProvider } from './jl-theme-provider';
|
|
12
12
|
import { ChatMessages } from './messages';
|
|
13
|
+
import { WritingIndicator } from './writing-indicator';
|
|
13
14
|
import { ChatReactContext } from '../context';
|
|
14
15
|
export function ChatBody(props) {
|
|
15
16
|
const { model } = props;
|
|
17
|
+
const [writers, setWriters] = useState([]);
|
|
16
18
|
let { inputToolbarRegistry } = props;
|
|
17
19
|
if (!inputToolbarRegistry) {
|
|
18
20
|
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Handle the changes in the writers list.
|
|
24
|
+
*/
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
var _a;
|
|
27
|
+
if (!model) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const updateWriters = (_, writers) => {
|
|
31
|
+
// Show all writers for now - AI generating responses will have messageID
|
|
32
|
+
setWriters(writers);
|
|
33
|
+
};
|
|
34
|
+
// Set initial writers state
|
|
35
|
+
const initialWriters = model.writers;
|
|
36
|
+
setWriters(initialWriters);
|
|
37
|
+
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(updateWriters);
|
|
38
|
+
return () => {
|
|
39
|
+
var _a;
|
|
40
|
+
(_a = model === null || model === void 0 ? void 0 : model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
|
|
41
|
+
};
|
|
42
|
+
}, [model]);
|
|
21
43
|
const horizontalPadding = 4;
|
|
22
44
|
const contextValue = {
|
|
23
45
|
...props,
|
|
@@ -30,7 +52,11 @@ export function ChatBody(props) {
|
|
|
30
52
|
paddingRight: horizontalPadding,
|
|
31
53
|
paddingTop: 0,
|
|
32
54
|
paddingBottom: 0
|
|
33
|
-
}, model: model.input })
|
|
55
|
+
}, model: model.input }),
|
|
56
|
+
React.createElement(WritingIndicator, { sx: {
|
|
57
|
+
paddingLeft: horizontalPadding,
|
|
58
|
+
paddingRight: horizontalPadding
|
|
59
|
+
}, writers: writers })));
|
|
34
60
|
}
|
|
35
61
|
export function Chat(props) {
|
|
36
62
|
var _a;
|
package/lib/components/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
8
8
|
import { useChatCommands } from './use-chat-commands';
|
|
9
9
|
import { AttachmentPreviewList } from '../attachments';
|
|
10
10
|
import { useChatContext } from '../../context';
|
|
11
|
-
import { InputWritingIndicator } from './writing-indicator';
|
|
12
11
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
13
12
|
const INPUT_TEXTFIELD_CLASS = 'jp-chat-input-textfield';
|
|
14
13
|
const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
|
|
@@ -23,7 +22,6 @@ export function ChatInput(props) {
|
|
|
23
22
|
const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
|
|
24
23
|
const [attachments, setAttachments] = useState(model.attachments);
|
|
25
24
|
const [toolbarElements, setToolbarElements] = useState([]);
|
|
26
|
-
const [writers, setWriters] = useState([]);
|
|
27
25
|
/**
|
|
28
26
|
* Auto-focus the input when the component is first mounted.
|
|
29
27
|
*/
|
|
@@ -79,27 +77,6 @@ export function ChatInput(props) {
|
|
|
79
77
|
inputToolbarRegistry === null || inputToolbarRegistry === void 0 ? void 0 : inputToolbarRegistry.itemsChanged.disconnect(updateToolbar);
|
|
80
78
|
};
|
|
81
79
|
}, [inputToolbarRegistry]);
|
|
82
|
-
/**
|
|
83
|
-
* Handle the changes in the writers list.
|
|
84
|
-
*/
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
var _a;
|
|
87
|
-
if (!chatModel) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const updateWriters = (_, writers) => {
|
|
91
|
-
// Show all writers for now - AI generating responses will have messageID
|
|
92
|
-
setWriters(writers);
|
|
93
|
-
};
|
|
94
|
-
// Set initial writers state
|
|
95
|
-
const initialWriters = chatModel.writers;
|
|
96
|
-
setWriters(initialWriters);
|
|
97
|
-
(_a = chatModel.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(updateWriters);
|
|
98
|
-
return () => {
|
|
99
|
-
var _a;
|
|
100
|
-
(_a = chatModel === null || chatModel === void 0 ? void 0 : chatModel.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
|
|
101
|
-
};
|
|
102
|
-
}, [chatModel]);
|
|
103
80
|
const inputExists = !!input.trim();
|
|
104
81
|
/**
|
|
105
82
|
* `handleKeyDown()`: callback invoked when the user presses any key in the
|
|
@@ -232,6 +209,5 @@ export function ChatInput(props) {
|
|
|
232
209
|
borderColor: 'var(--jp-border-color1)',
|
|
233
210
|
backgroundColor: 'var(--jp-layout-color0)',
|
|
234
211
|
transition: 'background-color 0.2s ease'
|
|
235
|
-
} }, toolbarElements.map((item, index) => (React.createElement(item.element, { key: index, model: model, chatCommandRegistry: chatCommandRegistry, chatModel: chatModel, edit: props.edit })))))
|
|
236
|
-
React.createElement(InputWritingIndicator, { writers: writers })));
|
|
212
|
+
} }, toolbarElements.map((item, index) => (React.createElement(item.element, { key: index, model: model, chatCommandRegistry: chatCommandRegistry, chatModel: chatModel, edit: props.edit })))))));
|
|
237
213
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import {
|
|
2
|
+
import { IMessageContent } from '../../types';
|
|
3
3
|
/**
|
|
4
4
|
* The chat footer component properties.
|
|
5
5
|
*/
|
|
@@ -7,7 +7,7 @@ export interface IMessageFootersProps {
|
|
|
7
7
|
/**
|
|
8
8
|
* The chat model.
|
|
9
9
|
*/
|
|
10
|
-
message:
|
|
10
|
+
message: IMessageContent;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* The chat footer component, which displays footer components on a row according to
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import {
|
|
2
|
+
import { IMessage } from '../../types';
|
|
3
3
|
/**
|
|
4
4
|
* The message header props.
|
|
5
5
|
*/
|
|
@@ -7,7 +7,7 @@ type ChatMessageHeaderProps = {
|
|
|
7
7
|
/**
|
|
8
8
|
* The chat message.
|
|
9
9
|
*/
|
|
10
|
-
message:
|
|
10
|
+
message: IMessage;
|
|
11
11
|
/**
|
|
12
12
|
* Whether this message is from the current user.
|
|
13
13
|
*/
|
|
@@ -12,11 +12,7 @@ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
|
12
12
|
*/
|
|
13
13
|
export function ChatMessageHeader(props) {
|
|
14
14
|
var _a, _b;
|
|
15
|
-
const message = props.message;
|
|
16
|
-
// Don't render header for stacked messages not deleted or edited.
|
|
17
|
-
if (message.stacked && !message.deleted && !message.edited) {
|
|
18
|
-
return React.createElement(React.Fragment, null);
|
|
19
|
-
}
|
|
15
|
+
const [message, setMessage] = useState(props.message.content);
|
|
20
16
|
// Flag to display only the deleted or edited information (stacked message).
|
|
21
17
|
const onlyState = message.stacked && (message.deleted || message.edited);
|
|
22
18
|
const [datetime, setDatetime] = useState({});
|
|
@@ -55,9 +51,20 @@ export function ChatMessageHeader(props) {
|
|
|
55
51
|
setDatetime(newDatetime);
|
|
56
52
|
}
|
|
57
53
|
});
|
|
54
|
+
// Listen for changes in the current message.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
function messageChanged() {
|
|
57
|
+
setMessage(props.message.content);
|
|
58
|
+
}
|
|
59
|
+
props.message.changed.connect(messageChanged);
|
|
60
|
+
return () => {
|
|
61
|
+
props.message.changed.disconnect(messageChanged);
|
|
62
|
+
};
|
|
63
|
+
}, [props.message]);
|
|
58
64
|
const avatar = message.stacked ? null : Avatar({ user: sender });
|
|
59
65
|
const name = (_b = (_a = sender.display_name) !== null && _a !== void 0 ? _a : sender.name) !== null && _b !== void 0 ? _b : (sender.username || 'User undefined');
|
|
60
|
-
|
|
66
|
+
// Don't render header for stacked messages not deleted or edited.
|
|
67
|
+
return message.stacked && !message.deleted && !message.edited ? (React.createElement(React.Fragment, null)) : (React.createElement(Box, { className: MESSAGE_HEADER_CLASS, sx: {
|
|
61
68
|
display: 'flex',
|
|
62
69
|
alignItems: 'center',
|
|
63
70
|
'& > :not(:last-child)': {
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { IMessageContent } from '../../types';
|
|
3
4
|
/**
|
|
4
5
|
* The type of the props for the MessageRenderer component.
|
|
5
6
|
*/
|
|
6
7
|
type MessageRendererProps = {
|
|
7
8
|
/**
|
|
8
|
-
* The string to render.
|
|
9
|
+
* The string or rendermime bundle to render.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
message: IMessageContent;
|
|
11
12
|
/**
|
|
12
13
|
* The promise to resolve when the message is rendered.
|
|
13
14
|
*/
|
|
14
15
|
rendered: PromiseDelegate<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Whether to append the content to the existing content or not.
|
|
17
|
-
*/
|
|
18
|
-
appendContent?: boolean;
|
|
19
16
|
/**
|
|
20
17
|
* The function to call to edit a message.
|
|
21
18
|
*/
|
|
@@ -2,51 +2,126 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
6
8
|
import { createPortal } from 'react-dom';
|
|
7
9
|
import { MessageToolbar } from './toolbar';
|
|
8
10
|
import { CodeToolbar } from '../code-blocks/code-toolbar';
|
|
9
11
|
import { useChatContext } from '../../context';
|
|
10
|
-
import {
|
|
12
|
+
import { replaceMentionToSpan } from '../../utils';
|
|
13
|
+
const RENDERED_CLASS = 'jp-chat-rendered-message';
|
|
14
|
+
const DEFAULT_MIME_TYPE = 'text/markdown';
|
|
11
15
|
/**
|
|
12
16
|
* The message renderer base component.
|
|
13
17
|
*/
|
|
14
18
|
function MessageRendererBase(props) {
|
|
15
|
-
const {
|
|
19
|
+
const { message } = props;
|
|
16
20
|
const { model, rmRegistry } = useChatContext();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const containerRef = useRef(null);
|
|
22
|
+
// Allow edition only on text messages.
|
|
23
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
24
|
+
// Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
20
25
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
21
26
|
useEffect(() => {
|
|
27
|
+
let node = null;
|
|
28
|
+
const container = containerRef.current;
|
|
29
|
+
if (!container) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
22
32
|
const renderContent = async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(_a =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
var _a, _b;
|
|
34
|
+
let isMarkdownRenderer = true;
|
|
35
|
+
let renderer;
|
|
36
|
+
let mimeModel;
|
|
37
|
+
// Create the renderer and the mime model.
|
|
38
|
+
if (typeof message.body === 'string') {
|
|
39
|
+
// Allow editing content for text messages.
|
|
40
|
+
setCanEdit(true);
|
|
41
|
+
// Improve users display in markdown content.
|
|
42
|
+
let mdStr = message.body;
|
|
43
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
44
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
45
|
+
});
|
|
46
|
+
// Body is a string, use the markdown renderer.
|
|
47
|
+
renderer = rmRegistry.createRenderer(DEFAULT_MIME_TYPE);
|
|
48
|
+
mimeModel = rmRegistry.createModel({
|
|
49
|
+
data: { [DEFAULT_MIME_TYPE]: mdStr }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
setCanEdit(false);
|
|
54
|
+
// This is a mime bundle.
|
|
55
|
+
let mimeContent = message.body;
|
|
56
|
+
let preferred = rmRegistry.preferredMimeType(mimeContent.data, 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
57
|
+
);
|
|
58
|
+
if (!preferred) {
|
|
59
|
+
preferred = DEFAULT_MIME_TYPE;
|
|
60
|
+
mimeContent = {
|
|
61
|
+
data: {
|
|
62
|
+
[DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
renderer = rmRegistry.createRenderer(preferred);
|
|
67
|
+
// Improve users display in markdown content.
|
|
68
|
+
if (preferred === DEFAULT_MIME_TYPE) {
|
|
69
|
+
let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
|
|
70
|
+
if (mdStr) {
|
|
71
|
+
(_b = message.mentions) === null || _b === void 0 ? void 0 : _b.forEach(user => {
|
|
72
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
73
|
+
});
|
|
74
|
+
mimeContent = {
|
|
75
|
+
...mimeContent,
|
|
76
|
+
data: {
|
|
77
|
+
...mimeContent.data,
|
|
78
|
+
[DEFAULT_MIME_TYPE]: mdStr
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
isMarkdownRenderer = false;
|
|
85
|
+
}
|
|
86
|
+
mimeModel = rmRegistry.createModel(mimeContent);
|
|
87
|
+
}
|
|
88
|
+
await renderer.renderModel(mimeModel);
|
|
89
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
90
|
+
// never been attached, only the node.
|
|
91
|
+
// This is necessary to render latex.
|
|
92
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
93
|
+
// Add code toolbar if markdown has been rendered.
|
|
94
|
+
if (isMarkdownRenderer) {
|
|
95
|
+
const newCodeToolbarDefns = [];
|
|
96
|
+
// Attach CodeToolbar root element to each <pre> block
|
|
97
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
98
|
+
preBlocks.forEach(preBlock => {
|
|
99
|
+
var _a;
|
|
100
|
+
const codeToolbarRoot = document.createElement('div');
|
|
101
|
+
(_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
|
|
102
|
+
newCodeToolbarDefns.push([
|
|
103
|
+
codeToolbarRoot,
|
|
104
|
+
{ model: model, content: preBlock.textContent || '' }
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
108
|
+
}
|
|
109
|
+
// Add the rendered node to the DOM.
|
|
110
|
+
node = renderer.node;
|
|
111
|
+
container.insertBefore(node, container.firstChild);
|
|
41
112
|
// Resolve the rendered promise.
|
|
42
113
|
props.rendered.resolve();
|
|
43
114
|
};
|
|
44
115
|
renderContent();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
return () => {
|
|
117
|
+
if (node && container.contains(node)) {
|
|
118
|
+
container.removeChild(node);
|
|
119
|
+
}
|
|
120
|
+
node = null;
|
|
121
|
+
};
|
|
122
|
+
}, [message.body, message.mentions, rmRegistry]);
|
|
123
|
+
return (React.createElement("div", { className: RENDERED_CLASS, ref: containerRef },
|
|
124
|
+
React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
|
|
50
125
|
// Render a `CodeToolbar` element underneath each code block.
|
|
51
126
|
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
52
127
|
// to use the context in the main React tree.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { IMessage } from '../../types';
|
|
4
4
|
/**
|
|
5
5
|
* The message component props.
|
|
6
6
|
*/
|
|
@@ -8,7 +8,7 @@ type ChatMessageProps = {
|
|
|
8
8
|
/**
|
|
9
9
|
* The message to display.
|
|
10
10
|
*/
|
|
11
|
-
message:
|
|
11
|
+
message: IMessage;
|
|
12
12
|
/**
|
|
13
13
|
* The index of the message in the list.
|
|
14
14
|
*/
|
|
@@ -13,8 +13,8 @@ import { replaceSpanToMention } from '../../utils';
|
|
|
13
13
|
* The message component body.
|
|
14
14
|
*/
|
|
15
15
|
export const ChatMessage = forwardRef((props, ref) => {
|
|
16
|
-
const { message } = props;
|
|
17
16
|
const { model } = useChatContext();
|
|
17
|
+
const [message, setMessage] = useState(props.message.content);
|
|
18
18
|
const [edit, setEdit] = useState(false);
|
|
19
19
|
const [deleted, setDeleted] = useState(false);
|
|
20
20
|
const [canEdit, setCanEdit] = useState(false);
|
|
@@ -39,10 +39,25 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
39
39
|
setCanDelete(false);
|
|
40
40
|
}
|
|
41
41
|
}, [model, message]);
|
|
42
|
+
// Listen for changes in the current message.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
function messageChanged() {
|
|
45
|
+
setMessage(props.message.content);
|
|
46
|
+
}
|
|
47
|
+
props.message.changed.connect(messageChanged);
|
|
48
|
+
// Initialize the message when the message is re-rendered.
|
|
49
|
+
// FIX ? This seems to be required for outofband change, to get the new value,
|
|
50
|
+
// even if when an outofband change occurs, all the messages are deleted and
|
|
51
|
+
// recreated.
|
|
52
|
+
setMessage(props.message.content);
|
|
53
|
+
return () => {
|
|
54
|
+
props.message.changed.disconnect(messageChanged);
|
|
55
|
+
};
|
|
56
|
+
}, [props.message]);
|
|
42
57
|
// Create an input model only if the message is edited.
|
|
43
58
|
const startEdition = () => {
|
|
44
59
|
var _a;
|
|
45
|
-
if (!canEdit) {
|
|
60
|
+
if (!canEdit || !(typeof message.body === 'string')) {
|
|
46
61
|
return;
|
|
47
62
|
}
|
|
48
63
|
let body = message.body;
|
|
@@ -96,7 +111,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
96
111
|
};
|
|
97
112
|
// Empty if the message has been deleted.
|
|
98
113
|
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
99
|
-
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, {
|
|
114
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, { message: message, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
100
115
|
message.attachments && !edit && (
|
|
101
116
|
// Display the attachments only if message is not edited, otherwise the
|
|
102
117
|
// input component display them.
|
|
@@ -13,6 +13,7 @@ import { Navigation } from './navigation';
|
|
|
13
13
|
import { WelcomeMessage } from './welcome';
|
|
14
14
|
import { ScrollContainer } from '../scroll-container';
|
|
15
15
|
import { useChatContext } from '../../context';
|
|
16
|
+
import { Message } from '../../message';
|
|
16
17
|
export const MESSAGE_CLASS = 'jp-chat-message';
|
|
17
18
|
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
18
19
|
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
@@ -39,7 +40,7 @@ export function ChatMessages() {
|
|
|
39
40
|
}
|
|
40
41
|
model
|
|
41
42
|
.getHistory()
|
|
42
|
-
.then(history => setMessages(history.messages))
|
|
43
|
+
.then(history => setMessages(history.messages.map(message => new Message({ ...message }))))
|
|
43
44
|
.catch(e => console.error(e));
|
|
44
45
|
}
|
|
45
46
|
fetchHistory();
|
|
@@ -150,7 +151,7 @@ export function ChatMessages() {
|
|
|
150
151
|
model.user.username === message.sender.username;
|
|
151
152
|
return (
|
|
152
153
|
// extra div needed to ensure each bubble is on a new line
|
|
153
|
-
React.createElement(Box, { key:
|
|
154
|
+
React.createElement(Box, { key: message.id, sx: {
|
|
154
155
|
...(isCurrentUser && {
|
|
155
156
|
marginLeft: area === 'main' ? '25%' : '10%',
|
|
156
157
|
backgroundColor: 'var(--jp-layout-color2)',
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
// import EditIcon from '@mui/icons-material/Edit';
|
|
6
5
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
6
|
+
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';
|
|
@@ -13,19 +13,11 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
|
13
13
|
*/
|
|
14
14
|
export function MessageToolbar(props) {
|
|
15
15
|
const buttons = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// aria-label={'Edit'}
|
|
22
|
-
// inputToolbar={false}
|
|
23
|
-
// >
|
|
24
|
-
// <EditIcon />
|
|
25
|
-
// </TooltippedIconButton>
|
|
26
|
-
// );
|
|
27
|
-
// buttons.push(editButton);
|
|
28
|
-
// }
|
|
16
|
+
if (props.edit !== undefined) {
|
|
17
|
+
const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
|
|
18
|
+
React.createElement(EditIcon, null)));
|
|
19
|
+
buttons.push(editButton);
|
|
20
|
+
}
|
|
29
21
|
if (props.delete !== undefined) {
|
|
30
22
|
const deleteButton = (React.createElement(TooltippedIconButton, { tooltip: 'Delete', onClick: props.delete, "aria-label": 'Delete', inputToolbar: false },
|
|
31
23
|
React.createElement(DeleteIcon, null)));
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
6
7
|
import React, { useEffect, useRef } from 'react';
|
|
7
8
|
import { useChatContext } from '../../context';
|
|
8
|
-
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
|
|
9
9
|
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
|
|
10
|
+
const MD_MIME_TYPE = 'text/markdown';
|
|
10
11
|
/**
|
|
11
12
|
* The welcome message component.
|
|
12
13
|
* This message is displayed on top of the chat messages, and is rendered using a
|
|
@@ -17,26 +18,35 @@ export function WelcomeMessage(props) {
|
|
|
17
18
|
const content = props.content + '\n----\n';
|
|
18
19
|
// ref that tracks the content container to store the rendermime node in
|
|
19
20
|
const renderingContainer = useRef(null);
|
|
20
|
-
// ref that tracks whether the rendermime node has already been inserted
|
|
21
|
-
const renderingInserted = useRef(false);
|
|
22
21
|
/**
|
|
23
22
|
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
|
|
24
23
|
* and insert it into `renderingContainer` if not yet inserted.
|
|
25
24
|
*/
|
|
26
25
|
useEffect(() => {
|
|
26
|
+
let node = null;
|
|
27
27
|
const renderContent = async () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
var _a;
|
|
29
|
+
// Render the welcome message using markdown renderer.
|
|
30
|
+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
31
|
+
const mimeModel = rmRegistry.createModel({
|
|
32
|
+
data: { [MD_MIME_TYPE]: content }
|
|
31
33
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
await renderer.renderModel(mimeModel);
|
|
35
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
36
|
+
// never been attached, only the node.
|
|
37
|
+
// This is necessary to render latex.
|
|
38
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
39
|
+
node = renderer.node;
|
|
40
|
+
(_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.append(node);
|
|
37
41
|
};
|
|
38
42
|
renderContent();
|
|
43
|
+
return () => {
|
|
44
|
+
var _a;
|
|
45
|
+
if (node && ((_a = renderingContainer.current) === null || _a === void 0 ? void 0 : _a.contains(node))) {
|
|
46
|
+
renderingContainer.current.removeChild(node);
|
|
47
|
+
}
|
|
48
|
+
node = null;
|
|
49
|
+
};
|
|
39
50
|
}, [content]);
|
|
40
|
-
return
|
|
41
|
-
React.createElement("div", { ref: renderingContainer })));
|
|
51
|
+
return React.createElement("div", { className: WELCOME_MESSAGE_CLASS, ref: renderingContainer });
|
|
42
52
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { SxProps, Theme } from '@mui/material';
|
|
3
|
+
import { IChatModel } from '../model';
|
|
4
|
+
/**
|
|
5
|
+
* The input writing indicator component props.
|
|
6
|
+
*/
|
|
7
|
+
export interface IInputWritingIndicatorProps {
|
|
8
|
+
/**
|
|
9
|
+
* The list of users currently writing.
|
|
10
|
+
*/
|
|
11
|
+
writers: IChatModel.IWriter[];
|
|
12
|
+
/**
|
|
13
|
+
* Custom mui/material styles.
|
|
14
|
+
*/
|
|
15
|
+
sx?: SxProps<Theme>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The writing indicator component, displaying typing status.
|
|
19
|
+
*/
|
|
20
|
+
export declare function WritingIndicator(props: IInputWritingIndicatorProps): JSX.Element;
|
|
@@ -30,13 +30,14 @@ function formatWritersText(writers) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* The
|
|
33
|
+
* The writing indicator component, displaying typing status.
|
|
34
34
|
*/
|
|
35
|
-
export function
|
|
35
|
+
export function WritingIndicator(props) {
|
|
36
36
|
const { writers } = props;
|
|
37
37
|
// Always render the container to reserve space, even if no writers
|
|
38
38
|
const writersText = writers.length > 0 ? formatWritersText(writers) : '';
|
|
39
39
|
return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME, sx: {
|
|
40
|
+
...props.sx,
|
|
40
41
|
minHeight: '16px'
|
|
41
42
|
} },
|
|
42
43
|
React.createElement(Typography, { variant: "caption", sx: {
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ export * from './active-cell-manager';
|
|
|
6
6
|
export * from './components';
|
|
7
7
|
export * from './icons';
|
|
8
8
|
export * from './input-model';
|
|
9
|
-
export * from './markdown-renderer';
|
|
10
9
|
export * from './model';
|
|
11
10
|
export * from './registers';
|
|
12
11
|
export * from './selection-watcher';
|