@jupyter/chat 0.19.0 → 0.20.0-alpha.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/components/chat.js +0 -1
- package/lib/components/messages/footer.js +3 -0
- package/lib/components/messages/message-renderer.d.ts +3 -6
- package/lib/components/messages/message-renderer.js +91 -26
- package/lib/components/messages/message.js +6 -5
- package/lib/components/messages/toolbar.js +1 -1
- package/lib/components/messages/welcome.js +24 -14
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/message.d.ts +2 -1
- package/lib/model.d.ts +1 -0
- package/lib/model.js +14 -18
- package/lib/theme-provider.js +10 -9
- package/lib/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/chat.tsx +0 -1
- package/src/components/messages/footer.tsx +4 -0
- package/src/components/messages/message-renderer.tsx +116 -40
- package/src/components/messages/message.tsx +10 -4
- package/src/components/messages/toolbar.tsx +1 -1
- package/src/components/messages/welcome.tsx +25 -17
- package/src/index.ts +0 -1
- package/src/message.ts +4 -1
- package/src/model.ts +20 -17
- package/src/theme-provider.ts +10 -9
- package/src/types.ts +5 -1
- package/style/chat.css +16 -11
- package/style/input.css +3 -3
- package/lib/markdown-renderer.d.ts +0 -38
- package/lib/markdown-renderer.js +0 -54
- package/src/markdown-renderer.ts +0 -78
package/lib/components/chat.js
CHANGED
|
@@ -40,7 +40,6 @@ export function ChatBody(props) {
|
|
|
40
40
|
(_a = model === null || model === void 0 ? void 0 : model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(updateWriters);
|
|
41
41
|
};
|
|
42
42
|
}, [model]);
|
|
43
|
-
// const horizontalPadding = props.area === 'main' ? 8 : 4;
|
|
44
43
|
const horizontalPadding = 4;
|
|
45
44
|
const contextValue = {
|
|
46
45
|
...props,
|
|
@@ -17,6 +17,9 @@ export function MessageFooterComponent(props) {
|
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
const footer = messageFooterRegistry.getFooter();
|
|
20
|
+
if (!footer.left && !footer.center && !footer.right) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
20
23
|
return (React.createElement(Box, { sx: { display: 'flex', justifyContent: 'space-between' } },
|
|
21
24
|
((_a = footer.left) === null || _a === void 0 ? void 0 : _a.component) ? (React.createElement(footer.left.component, { message: message, model: model })) : (React.createElement("div", null)),
|
|
22
25
|
((_b = footer.center) === null || _b === void 0 ? void 0 : _b.component) ? (React.createElement(footer.center.component, { message: message, model: model })) : (React.createElement("div", null)),
|
|
@@ -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,116 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
5
7
|
import React, { useState, useEffect } 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
|
-
|
|
21
|
+
// The rendered content, return by the mime renderer.
|
|
18
22
|
const [renderedContent, setRenderedContent] = useState(null);
|
|
19
|
-
//
|
|
23
|
+
// Allow edition only on text messages.
|
|
24
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
25
|
+
// Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
20
26
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
21
27
|
useEffect(() => {
|
|
22
28
|
const renderContent = async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(_a =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
var _a, _b;
|
|
30
|
+
let isMarkdownRenderer = true;
|
|
31
|
+
let renderer;
|
|
32
|
+
let mimeModel;
|
|
33
|
+
// Create the renderer and the mime model.
|
|
34
|
+
if (typeof message.body === 'string') {
|
|
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 {
|
|
49
|
+
setCanEdit(false);
|
|
50
|
+
// This is a mime bundle.
|
|
51
|
+
let mimeContent = message.body;
|
|
52
|
+
let preferred = rmRegistry.preferredMimeType(mimeContent.data, 'ensure' // Should be changed with 'prefer' if we can handle trusted content.
|
|
53
|
+
);
|
|
54
|
+
if (!preferred) {
|
|
55
|
+
preferred = DEFAULT_MIME_TYPE;
|
|
56
|
+
mimeContent = {
|
|
57
|
+
data: {
|
|
58
|
+
[DEFAULT_MIME_TYPE]: `_No renderer found for [**${Object.keys(mimeContent.data).join(', ')}**] mimetype(s)_`
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
renderer = rmRegistry.createRenderer(preferred);
|
|
63
|
+
// Improve users display in markdown content.
|
|
64
|
+
if (preferred === DEFAULT_MIME_TYPE) {
|
|
65
|
+
let mdStr = mimeContent.data[DEFAULT_MIME_TYPE];
|
|
66
|
+
if (mdStr) {
|
|
67
|
+
(_b = message.mentions) === null || _b === void 0 ? void 0 : _b.forEach(user => {
|
|
68
|
+
mdStr = replaceMentionToSpan(mdStr, user);
|
|
69
|
+
});
|
|
70
|
+
mimeContent = {
|
|
71
|
+
...mimeContent,
|
|
72
|
+
data: {
|
|
73
|
+
...mimeContent.data,
|
|
74
|
+
[DEFAULT_MIME_TYPE]: mdStr
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
isMarkdownRenderer = false;
|
|
81
|
+
}
|
|
82
|
+
mimeModel = rmRegistry.createModel(mimeContent);
|
|
83
|
+
}
|
|
84
|
+
await renderer.renderModel(mimeModel);
|
|
85
|
+
// Manually trigger the onAfterAttach of the renderer, because the widget will
|
|
86
|
+
// never been attached, only the node.
|
|
87
|
+
// This is necessary to render latex.
|
|
88
|
+
MessageLoop.sendMessage(renderer, Widget.Msg.AfterAttach);
|
|
89
|
+
// Add code toolbar if markdown has been rendered.
|
|
90
|
+
if (isMarkdownRenderer) {
|
|
91
|
+
const newCodeToolbarDefns = [];
|
|
92
|
+
// Attach CodeToolbar root element to each <pre> block
|
|
93
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
94
|
+
preBlocks.forEach(preBlock => {
|
|
95
|
+
var _a;
|
|
96
|
+
const codeToolbarRoot = document.createElement('div');
|
|
97
|
+
(_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
|
|
98
|
+
newCodeToolbarDefns.push([
|
|
99
|
+
codeToolbarRoot,
|
|
100
|
+
{ model: model, content: preBlock.textContent || '' }
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
104
|
+
}
|
|
105
|
+
// Update the content.
|
|
40
106
|
setRenderedContent(renderer.node);
|
|
41
107
|
// Resolve the rendered promise.
|
|
42
108
|
props.rendered.resolve();
|
|
43
109
|
};
|
|
44
110
|
renderContent();
|
|
45
|
-
}, [
|
|
46
|
-
return (React.createElement(
|
|
47
|
-
renderedContent &&
|
|
48
|
-
|
|
49
|
-
React.createElement(MessageToolbar, { edit: props.edit, delete: props.delete }),
|
|
111
|
+
}, [message.body, message.mentions, rmRegistry]);
|
|
112
|
+
return (React.createElement(React.Fragment, null,
|
|
113
|
+
renderedContent && (React.createElement("div", { className: RENDERED_CLASS, ref: node => node && node.replaceChildren(renderedContent) })),
|
|
114
|
+
React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
|
|
50
115
|
// Render a `CodeToolbar` element underneath each code block.
|
|
51
116
|
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
52
117
|
// to use the context in the main React tree.
|
|
@@ -9,6 +9,7 @@ import { ChatInput } from '../input';
|
|
|
9
9
|
import { useChatContext } from '../../context';
|
|
10
10
|
import { InputModel } from '../../input-model';
|
|
11
11
|
import { replaceSpanToMention } from '../../utils';
|
|
12
|
+
const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
12
13
|
/**
|
|
13
14
|
* The message component body.
|
|
14
15
|
*/
|
|
@@ -56,8 +57,8 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
56
57
|
}, [props.message]);
|
|
57
58
|
// Create an input model only if the message is edited.
|
|
58
59
|
const startEdition = () => {
|
|
59
|
-
var _a;
|
|
60
|
-
if (!canEdit) {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
if (!canEdit || !(typeof message.body === 'string')) {
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
let body = message.body;
|
|
@@ -75,7 +76,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
75
76
|
config: {
|
|
76
77
|
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
77
78
|
},
|
|
78
|
-
attachments: message.attachments,
|
|
79
|
+
attachments: structuredClone((_b = message.attachments) !== null && _b !== void 0 ? _b : []),
|
|
79
80
|
mentions: message.mentions
|
|
80
81
|
});
|
|
81
82
|
model.addEditionModel(message.id, inputModel);
|
|
@@ -110,8 +111,8 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
110
111
|
model.deleteMessage(id);
|
|
111
112
|
};
|
|
112
113
|
// Empty if the message has been deleted.
|
|
113
|
-
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
114
|
-
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, {
|
|
114
|
+
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index, className: MESSAGE_CONTAINER_CLASS },
|
|
115
|
+
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 })),
|
|
115
116
|
message.attachments && !edit && (
|
|
116
117
|
// Display the attachments only if message is not edited, otherwise the
|
|
117
118
|
// input component display them.
|
|
@@ -14,7 +14,7 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
|
14
14
|
export function MessageToolbar(props) {
|
|
15
15
|
const buttons = [];
|
|
16
16
|
if (props.edit !== undefined) {
|
|
17
|
-
const editButton = (React.createElement(TooltippedIconButton, { tooltip: '
|
|
17
|
+
const editButton = (React.createElement(TooltippedIconButton, { tooltip: 'Edit', onClick: props.edit, "aria-label": 'Edit', inputToolbar: false },
|
|
18
18
|
React.createElement(EditIcon, null)));
|
|
19
19
|
buttons.push(editButton);
|
|
20
20
|
}
|
|
@@ -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
|
}
|
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';
|
package/lib/message.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
1
2
|
import { ISignal } from '@lumino/signaling';
|
|
2
3
|
import { IAttachment, IMessageContent, IMessage, IUser } from './types';
|
|
3
4
|
/**
|
|
@@ -18,7 +19,7 @@ export declare class Message implements IMessage {
|
|
|
18
19
|
* Getters for each attribute individually.
|
|
19
20
|
*/
|
|
20
21
|
get type(): string;
|
|
21
|
-
get body(): string;
|
|
22
|
+
get body(): string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
|
|
22
23
|
get id(): string;
|
|
23
24
|
get time(): number;
|
|
24
25
|
get sender(): IUser;
|
package/lib/model.d.ts
CHANGED
package/lib/model.js
CHANGED
|
@@ -7,7 +7,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
7
7
|
import { Signal } from '@lumino/signaling';
|
|
8
8
|
import { InputModel } from './input-model';
|
|
9
9
|
import { Message } from './message';
|
|
10
|
-
import { replaceMentionToSpan } from './utils';
|
|
11
10
|
/**
|
|
12
11
|
* An abstract implementation of IChatModel.
|
|
13
12
|
*
|
|
@@ -22,6 +21,7 @@ export class AbstractChatModel {
|
|
|
22
21
|
var _a, _b, _c, _d;
|
|
23
22
|
this._messages = [];
|
|
24
23
|
this._unreadMessages = [];
|
|
24
|
+
this._lastRead = 0;
|
|
25
25
|
this._messagesInViewport = [];
|
|
26
26
|
this._name = '';
|
|
27
27
|
this._readyDelegate = new PromiseDelegate();
|
|
@@ -72,6 +72,12 @@ export class AbstractChatModel {
|
|
|
72
72
|
}
|
|
73
73
|
set id(value) {
|
|
74
74
|
this._id = value;
|
|
75
|
+
// Update the last read message.
|
|
76
|
+
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
77
|
+
if (typeof storage.lastRead === 'number' &&
|
|
78
|
+
storage.lastRead > this._lastRead) {
|
|
79
|
+
this._lastRead = storage.lastRead;
|
|
80
|
+
}
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
77
83
|
* The chat model name.
|
|
@@ -125,17 +131,14 @@ export class AbstractChatModel {
|
|
|
125
131
|
* Timestamp of the last read message in local storage.
|
|
126
132
|
*/
|
|
127
133
|
get lastRead() {
|
|
128
|
-
|
|
129
|
-
if (this._id === undefined) {
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
133
|
-
return (_a = storage.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
134
|
+
return this._lastRead;
|
|
134
135
|
}
|
|
135
136
|
set lastRead(value) {
|
|
137
|
+
this._lastRead = value;
|
|
136
138
|
if (this._id === undefined) {
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
141
|
+
// Save the last read message to the local storage, for persistence across reload.
|
|
139
142
|
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
140
143
|
storage.lastRead = value;
|
|
141
144
|
localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
|
|
@@ -193,17 +196,16 @@ export class AbstractChatModel {
|
|
|
193
196
|
return this._unreadMessages;
|
|
194
197
|
}
|
|
195
198
|
set unreadMessages(unread) {
|
|
196
|
-
var _a;
|
|
197
199
|
const recentlyRead = this._unreadMessages.filter(elem => !unread.includes(elem));
|
|
198
200
|
const unreadCountDiff = unread.length - this._unreadMessages.length;
|
|
199
201
|
this._unreadMessages = unread;
|
|
200
202
|
this._unreadChanged.emit(this._unreadMessages);
|
|
201
203
|
// Notify the change.
|
|
202
204
|
this._notify(unread.length, unreadCountDiff > 0);
|
|
203
|
-
// Save the last read
|
|
204
|
-
if (
|
|
205
|
+
// Save the last read.
|
|
206
|
+
if (recentlyRead.length) {
|
|
205
207
|
let lastReadChanged = false;
|
|
206
|
-
let lastRead =
|
|
208
|
+
let lastRead = this.lastRead;
|
|
207
209
|
recentlyRead.forEach(index => {
|
|
208
210
|
if (this.messages[index].time > lastRead) {
|
|
209
211
|
lastRead = this.messages[index].time;
|
|
@@ -289,10 +291,6 @@ export class AbstractChatModel {
|
|
|
289
291
|
* Can be useful if some actions are required on the message.
|
|
290
292
|
*/
|
|
291
293
|
formatChatMessage(message) {
|
|
292
|
-
var _a;
|
|
293
|
-
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
294
|
-
message.body = replaceMentionToSpan(message.body, user);
|
|
295
|
-
});
|
|
296
294
|
return message;
|
|
297
295
|
}
|
|
298
296
|
/**
|
|
@@ -323,15 +321,13 @@ export class AbstractChatModel {
|
|
|
323
321
|
* @param messages - the messages list.
|
|
324
322
|
*/
|
|
325
323
|
messagesInserted(index, messages) {
|
|
326
|
-
var _a;
|
|
327
324
|
const formattedMessages = [];
|
|
328
325
|
const unreadIndexes = [];
|
|
329
|
-
const lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
330
326
|
// Format the messages.
|
|
331
327
|
messages.forEach((message, idx) => {
|
|
332
328
|
const formattedMessage = this.formatChatMessage(message);
|
|
333
329
|
formattedMessages.push(new Message(formattedMessage));
|
|
334
|
-
if (message.time > lastRead) {
|
|
330
|
+
if (message.time > this.lastRead) {
|
|
335
331
|
unreadIndexes.push(index + idx);
|
|
336
332
|
}
|
|
337
333
|
});
|
package/lib/theme-provider.js
CHANGED
|
@@ -15,8 +15,9 @@ export async function pollUntilReady() {
|
|
|
15
15
|
}
|
|
16
16
|
export async function getJupyterLabTheme() {
|
|
17
17
|
await pollUntilReady();
|
|
18
|
-
const light = document.body.getAttribute('data-jp-theme-light');
|
|
18
|
+
const light = document.body.getAttribute('data-jp-theme-light') === 'true';
|
|
19
19
|
return createTheme({
|
|
20
|
+
cssVariables: true,
|
|
20
21
|
spacing: 4,
|
|
21
22
|
components: {
|
|
22
23
|
MuiButton: {
|
|
@@ -40,12 +41,12 @@ export async function getJupyterLabTheme() {
|
|
|
40
41
|
// The default style for input toolbar button variant.
|
|
41
42
|
props: { variant: 'input-toolbar' },
|
|
42
43
|
style: {
|
|
43
|
-
backgroundColor:
|
|
44
|
-
color: '
|
|
44
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
45
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
45
46
|
borderRadius: '4px',
|
|
46
47
|
boxShadow: 'none',
|
|
47
48
|
'&:hover': {
|
|
48
|
-
backgroundColor:
|
|
49
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
49
50
|
boxShadow: 'none'
|
|
50
51
|
},
|
|
51
52
|
'&:disabled': {
|
|
@@ -97,12 +98,12 @@ export async function getJupyterLabTheme() {
|
|
|
97
98
|
// The default style for input toolbar button variant.
|
|
98
99
|
props: { variant: 'input-toolbar' },
|
|
99
100
|
style: {
|
|
100
|
-
backgroundColor:
|
|
101
|
-
color: '
|
|
101
|
+
backgroundColor: `var(--jp-brand-color${light ? '1' : '2'})`,
|
|
102
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
102
103
|
borderRadius: '4px',
|
|
103
104
|
boxShadow: 'none',
|
|
104
105
|
'&:hover': {
|
|
105
|
-
backgroundColor:
|
|
106
|
+
backgroundColor: `var(--jp-brand-color${light ? '0' : '1'})`,
|
|
106
107
|
boxShadow: 'none'
|
|
107
108
|
},
|
|
108
109
|
'&:disabled': {
|
|
@@ -162,9 +163,9 @@ export async function getJupyterLabTheme() {
|
|
|
162
163
|
paper: getCSSVariable('--jp-layout-color1'),
|
|
163
164
|
default: getCSSVariable('--jp-layout-color1')
|
|
164
165
|
},
|
|
165
|
-
mode: light
|
|
166
|
+
mode: light ? 'light' : 'dark',
|
|
166
167
|
primary: {
|
|
167
|
-
main: getCSSVariable(
|
|
168
|
+
main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
|
|
168
169
|
light: getCSSVariable('--jp-brand-color2'),
|
|
169
170
|
dark: getCSSVariable('--jp-brand-color0')
|
|
170
171
|
},
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ISignal } from '@lumino/signaling';
|
|
2
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
2
3
|
/**
|
|
3
4
|
* The user description.
|
|
4
5
|
*/
|
|
@@ -57,7 +58,7 @@ export interface IConfig {
|
|
|
57
58
|
*/
|
|
58
59
|
export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
59
60
|
type: string;
|
|
60
|
-
body: string;
|
|
61
|
+
body: string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
|
|
61
62
|
id: string;
|
|
62
63
|
time: number;
|
|
63
64
|
sender: T;
|
package/package.json
CHANGED
package/src/components/chat.tsx
CHANGED
|
@@ -33,6 +33,10 @@ export function MessageFooterComponent(
|
|
|
33
33
|
}
|
|
34
34
|
const footer = messageFooterRegistry.getFooter();
|
|
35
35
|
|
|
36
|
+
if (!footer.left && !footer.center && !footer.right) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
return (
|
|
37
41
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
38
42
|
{footer.left?.component ? (
|