@jupyter/chat 0.20.0-alpha.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/messages/footer.js +3 -0
- package/lib/components/messages/message-renderer.js +7 -17
- package/lib/components/messages/message.js +4 -3
- package/lib/model.d.ts +1 -0
- package/lib/model.js +14 -13
- package/package.json +1 -1
- package/src/components/messages/footer.tsx +4 -0
- package/src/components/messages/message-renderer.tsx +15 -20
- package/src/components/messages/message.tsx +8 -2
- package/src/model.ts +20 -13
- package/style/chat.css +4 -4
|
@@ -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)),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { MessageLoop } from '@lumino/messaging';
|
|
6
6
|
import { Widget } from '@lumino/widgets';
|
|
7
|
-
import React, { useState, useEffect
|
|
7
|
+
import React, { useState, useEffect } from 'react';
|
|
8
8
|
import { createPortal } from 'react-dom';
|
|
9
9
|
import { MessageToolbar } from './toolbar';
|
|
10
10
|
import { CodeToolbar } from '../code-blocks/code-toolbar';
|
|
@@ -18,17 +18,13 @@ const DEFAULT_MIME_TYPE = 'text/markdown';
|
|
|
18
18
|
function MessageRendererBase(props) {
|
|
19
19
|
const { message } = props;
|
|
20
20
|
const { model, rmRegistry } = useChatContext();
|
|
21
|
-
|
|
21
|
+
// The rendered content, return by the mime renderer.
|
|
22
|
+
const [renderedContent, setRenderedContent] = useState(null);
|
|
22
23
|
// Allow edition only on text messages.
|
|
23
24
|
const [canEdit, setCanEdit] = useState(false);
|
|
24
25
|
// Each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
25
26
|
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
26
27
|
useEffect(() => {
|
|
27
|
-
let node = null;
|
|
28
|
-
const container = containerRef.current;
|
|
29
|
-
if (!container) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
28
|
const renderContent = async () => {
|
|
33
29
|
var _a, _b;
|
|
34
30
|
let isMarkdownRenderer = true;
|
|
@@ -106,21 +102,15 @@ function MessageRendererBase(props) {
|
|
|
106
102
|
});
|
|
107
103
|
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
108
104
|
}
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
container.insertBefore(node, container.firstChild);
|
|
105
|
+
// Update the content.
|
|
106
|
+
setRenderedContent(renderer.node);
|
|
112
107
|
// Resolve the rendered promise.
|
|
113
108
|
props.rendered.resolve();
|
|
114
109
|
};
|
|
115
110
|
renderContent();
|
|
116
|
-
return () => {
|
|
117
|
-
if (node && container.contains(node)) {
|
|
118
|
-
container.removeChild(node);
|
|
119
|
-
}
|
|
120
|
-
node = null;
|
|
121
|
-
};
|
|
122
111
|
}, [message.body, message.mentions, rmRegistry]);
|
|
123
|
-
return (React.createElement(
|
|
112
|
+
return (React.createElement(React.Fragment, null,
|
|
113
|
+
renderedContent && (React.createElement("div", { className: RENDERED_CLASS, ref: node => node && node.replaceChildren(renderedContent) })),
|
|
124
114
|
React.createElement(MessageToolbar, { edit: canEdit ? props.edit : undefined, delete: props.delete }),
|
|
125
115
|
// Render a `CodeToolbar` element underneath each code block.
|
|
126
116
|
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
@@ -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,7 +57,7 @@ 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
|
+
var _a, _b;
|
|
60
61
|
if (!canEdit || !(typeof message.body === 'string')) {
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
@@ -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,7 +111,7 @@ 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
|
+
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index, className: MESSAGE_CONTAINER_CLASS },
|
|
114
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
|
package/lib/model.d.ts
CHANGED
package/lib/model.js
CHANGED
|
@@ -21,6 +21,7 @@ export class AbstractChatModel {
|
|
|
21
21
|
var _a, _b, _c, _d;
|
|
22
22
|
this._messages = [];
|
|
23
23
|
this._unreadMessages = [];
|
|
24
|
+
this._lastRead = 0;
|
|
24
25
|
this._messagesInViewport = [];
|
|
25
26
|
this._name = '';
|
|
26
27
|
this._readyDelegate = new PromiseDelegate();
|
|
@@ -71,6 +72,12 @@ export class AbstractChatModel {
|
|
|
71
72
|
}
|
|
72
73
|
set id(value) {
|
|
73
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
|
+
}
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
76
83
|
* The chat model name.
|
|
@@ -124,17 +131,14 @@ export class AbstractChatModel {
|
|
|
124
131
|
* Timestamp of the last read message in local storage.
|
|
125
132
|
*/
|
|
126
133
|
get lastRead() {
|
|
127
|
-
|
|
128
|
-
if (this._id === undefined) {
|
|
129
|
-
return 0;
|
|
130
|
-
}
|
|
131
|
-
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
132
|
-
return (_a = storage.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
134
|
+
return this._lastRead;
|
|
133
135
|
}
|
|
134
136
|
set lastRead(value) {
|
|
137
|
+
this._lastRead = value;
|
|
135
138
|
if (this._id === undefined) {
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
141
|
+
// Save the last read message to the local storage, for persistence across reload.
|
|
138
142
|
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
139
143
|
storage.lastRead = value;
|
|
140
144
|
localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
|
|
@@ -192,17 +196,16 @@ export class AbstractChatModel {
|
|
|
192
196
|
return this._unreadMessages;
|
|
193
197
|
}
|
|
194
198
|
set unreadMessages(unread) {
|
|
195
|
-
var _a;
|
|
196
199
|
const recentlyRead = this._unreadMessages.filter(elem => !unread.includes(elem));
|
|
197
200
|
const unreadCountDiff = unread.length - this._unreadMessages.length;
|
|
198
201
|
this._unreadMessages = unread;
|
|
199
202
|
this._unreadChanged.emit(this._unreadMessages);
|
|
200
203
|
// Notify the change.
|
|
201
204
|
this._notify(unread.length, unreadCountDiff > 0);
|
|
202
|
-
// Save the last read
|
|
203
|
-
if (
|
|
205
|
+
// Save the last read.
|
|
206
|
+
if (recentlyRead.length) {
|
|
204
207
|
let lastReadChanged = false;
|
|
205
|
-
let lastRead =
|
|
208
|
+
let lastRead = this.lastRead;
|
|
206
209
|
recentlyRead.forEach(index => {
|
|
207
210
|
if (this.messages[index].time > lastRead) {
|
|
208
211
|
lastRead = this.messages[index].time;
|
|
@@ -318,15 +321,13 @@ export class AbstractChatModel {
|
|
|
318
321
|
* @param messages - the messages list.
|
|
319
322
|
*/
|
|
320
323
|
messagesInserted(index, messages) {
|
|
321
|
-
var _a;
|
|
322
324
|
const formattedMessages = [];
|
|
323
325
|
const unreadIndexes = [];
|
|
324
|
-
const lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
325
326
|
// Format the messages.
|
|
326
327
|
messages.forEach((message, idx) => {
|
|
327
328
|
const formattedMessage = this.formatChatMessage(message);
|
|
328
329
|
formattedMessages.push(new Message(formattedMessage));
|
|
329
|
-
if (message.time > lastRead) {
|
|
330
|
+
if (message.time > this.lastRead) {
|
|
330
331
|
unreadIndexes.push(index + idx);
|
|
331
332
|
}
|
|
332
333
|
});
|
package/package.json
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 ? (
|
|
@@ -7,7 +7,7 @@ import { IRenderMime } from '@jupyterlab/rendermime';
|
|
|
7
7
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
8
8
|
import { MessageLoop } from '@lumino/messaging';
|
|
9
9
|
import { Widget } from '@lumino/widgets';
|
|
10
|
-
import React, { useState, useEffect
|
|
10
|
+
import React, { useState, useEffect } from 'react';
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
12
|
|
|
13
13
|
import { MessageToolbar } from './toolbar';
|
|
@@ -48,7 +48,10 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
48
48
|
const { message } = props;
|
|
49
49
|
const { model, rmRegistry } = useChatContext();
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// The rendered content, return by the mime renderer.
|
|
52
|
+
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
|
|
53
|
+
null
|
|
54
|
+
);
|
|
52
55
|
|
|
53
56
|
// Allow edition only on text messages.
|
|
54
57
|
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
@@ -59,12 +62,6 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
59
62
|
>([]);
|
|
60
63
|
|
|
61
64
|
useEffect(() => {
|
|
62
|
-
let node: HTMLElement | null = null;
|
|
63
|
-
const container = containerRef.current;
|
|
64
|
-
if (!container) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
65
|
const renderContent = async () => {
|
|
69
66
|
let isMarkdownRenderer = true;
|
|
70
67
|
let renderer: IRenderMime.IRenderer;
|
|
@@ -153,26 +150,24 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
153
150
|
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
container.insertBefore(node, container.firstChild);
|
|
153
|
+
// Update the content.
|
|
154
|
+
setRenderedContent(renderer.node);
|
|
159
155
|
|
|
160
156
|
// Resolve the rendered promise.
|
|
161
157
|
props.rendered.resolve();
|
|
162
158
|
};
|
|
163
159
|
|
|
164
160
|
renderContent();
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
if (node && container.contains(node)) {
|
|
168
|
-
container.removeChild(node);
|
|
169
|
-
}
|
|
170
|
-
node = null;
|
|
171
|
-
};
|
|
172
161
|
}, [message.body, message.mentions, rmRegistry]);
|
|
173
162
|
|
|
174
163
|
return (
|
|
175
|
-
|
|
164
|
+
<>
|
|
165
|
+
{renderedContent && (
|
|
166
|
+
<div
|
|
167
|
+
className={RENDERED_CLASS}
|
|
168
|
+
ref={node => node && node.replaceChildren(renderedContent)}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
176
171
|
<MessageToolbar
|
|
177
172
|
edit={canEdit ? props.edit : undefined}
|
|
178
173
|
delete={props.delete}
|
|
@@ -189,7 +184,7 @@ function MessageRendererBase(props: MessageRendererProps): JSX.Element {
|
|
|
189
184
|
);
|
|
190
185
|
})
|
|
191
186
|
}
|
|
192
|
-
|
|
187
|
+
</>
|
|
193
188
|
);
|
|
194
189
|
}
|
|
195
190
|
|
|
@@ -14,6 +14,8 @@ import { IInputModel, InputModel } from '../../input-model';
|
|
|
14
14
|
import { IMessageContent, IMessage } from '../../types';
|
|
15
15
|
import { replaceSpanToMention } from '../../utils';
|
|
16
16
|
|
|
17
|
+
const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* The message component props.
|
|
19
21
|
*/
|
|
@@ -104,7 +106,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
104
106
|
config: {
|
|
105
107
|
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
106
108
|
},
|
|
107
|
-
attachments: message.attachments,
|
|
109
|
+
attachments: structuredClone(message.attachments ?? []),
|
|
108
110
|
mentions: message.mentions
|
|
109
111
|
});
|
|
110
112
|
model.addEditionModel(message.id, inputModel);
|
|
@@ -148,7 +150,11 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
148
150
|
return deleted ? (
|
|
149
151
|
<div ref={ref} data-index={props.index}></div>
|
|
150
152
|
) : (
|
|
151
|
-
<div
|
|
153
|
+
<div
|
|
154
|
+
ref={ref}
|
|
155
|
+
data-index={props.index}
|
|
156
|
+
className={MESSAGE_CONTAINER_CLASS}
|
|
157
|
+
>
|
|
152
158
|
{edit && canEdit && model.getEditionModel(message.id) ? (
|
|
153
159
|
<ChatInput
|
|
154
160
|
onCancel={() => cancelEdition()}
|
package/src/model.ts
CHANGED
|
@@ -268,6 +268,17 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
268
268
|
}
|
|
269
269
|
set id(value: string | undefined) {
|
|
270
270
|
this._id = value;
|
|
271
|
+
|
|
272
|
+
// Update the last read message.
|
|
273
|
+
const storage = JSON.parse(
|
|
274
|
+
localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}'
|
|
275
|
+
);
|
|
276
|
+
if (
|
|
277
|
+
typeof storage.lastRead === 'number' &&
|
|
278
|
+
storage.lastRead > this._lastRead
|
|
279
|
+
) {
|
|
280
|
+
this._lastRead = storage.lastRead;
|
|
281
|
+
}
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
/**
|
|
@@ -330,18 +341,15 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
330
341
|
* Timestamp of the last read message in local storage.
|
|
331
342
|
*/
|
|
332
343
|
get lastRead(): number {
|
|
333
|
-
|
|
334
|
-
return 0;
|
|
335
|
-
}
|
|
336
|
-
const storage = JSON.parse(
|
|
337
|
-
localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}'
|
|
338
|
-
);
|
|
339
|
-
return storage.lastRead ?? 0;
|
|
344
|
+
return this._lastRead;
|
|
340
345
|
}
|
|
341
346
|
set lastRead(value: number) {
|
|
347
|
+
this._lastRead = value;
|
|
348
|
+
|
|
342
349
|
if (this._id === undefined) {
|
|
343
350
|
return;
|
|
344
351
|
}
|
|
352
|
+
// Save the last read message to the local storage, for persistence across reload.
|
|
345
353
|
const storage = JSON.parse(
|
|
346
354
|
localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}'
|
|
347
355
|
);
|
|
@@ -422,10 +430,10 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
422
430
|
// Notify the change.
|
|
423
431
|
this._notify(unread.length, unreadCountDiff > 0);
|
|
424
432
|
|
|
425
|
-
// Save the last read
|
|
426
|
-
if (
|
|
433
|
+
// Save the last read.
|
|
434
|
+
if (recentlyRead.length) {
|
|
427
435
|
let lastReadChanged = false;
|
|
428
|
-
let lastRead = this.lastRead
|
|
436
|
+
let lastRead = this.lastRead;
|
|
429
437
|
recentlyRead.forEach(index => {
|
|
430
438
|
if (this.messages[index].time > lastRead) {
|
|
431
439
|
lastRead = this.messages[index].time;
|
|
@@ -569,13 +577,11 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
569
577
|
const formattedMessages: IMessage[] = [];
|
|
570
578
|
const unreadIndexes: number[] = [];
|
|
571
579
|
|
|
572
|
-
const lastRead = this.lastRead ?? 0;
|
|
573
|
-
|
|
574
580
|
// Format the messages.
|
|
575
581
|
messages.forEach((message, idx) => {
|
|
576
582
|
const formattedMessage = this.formatChatMessage(message);
|
|
577
583
|
formattedMessages.push(new Message(formattedMessage));
|
|
578
|
-
if (message.time > lastRead) {
|
|
584
|
+
if (message.time > this.lastRead) {
|
|
579
585
|
unreadIndexes.push(index + idx);
|
|
580
586
|
}
|
|
581
587
|
});
|
|
@@ -714,6 +720,7 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
714
720
|
|
|
715
721
|
private _messages: IMessage[] = [];
|
|
716
722
|
private _unreadMessages: number[] = [];
|
|
723
|
+
private _lastRead: number = 0;
|
|
717
724
|
private _messagesInViewport: number[] = [];
|
|
718
725
|
private _id: string | undefined;
|
|
719
726
|
private _name: string = '';
|
package/style/chat.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
.jp-chat-
|
|
6
|
+
.jp-chat-message-container {
|
|
7
7
|
position: relative;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
.jp-chat-toolbar {
|
|
59
59
|
visibility: hidden;
|
|
60
60
|
position: absolute;
|
|
61
|
-
right:
|
|
62
|
-
top:
|
|
61
|
+
right: 0;
|
|
62
|
+
top: 0;
|
|
63
63
|
font-size: var(--jp-ui-font-size0);
|
|
64
64
|
color: var(--jp-ui-font-color3);
|
|
65
65
|
}
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
color: var(--jp-ui-font-color2);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.jp-chat-
|
|
72
|
+
.jp-chat-message-container:hover .jp-chat-toolbar {
|
|
73
73
|
visibility: visible;
|
|
74
74
|
}
|
|
75
75
|
|