@jupyter/chat 0.20.0-alpha.0 → 0.20.0-alpha.2
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__/preamble-registry.spec.d.ts +1 -0
- package/lib/__tests__/preamble-registry.spec.js +37 -0
- package/lib/components/chat.d.ts +5 -1
- package/lib/components/input/buttons/send-button.js +3 -6
- package/lib/components/messages/footer.js +3 -0
- package/lib/components/messages/index.d.ts +1 -0
- package/lib/components/messages/index.js +1 -0
- package/lib/components/messages/message-renderer.js +7 -17
- package/lib/components/messages/message.js +4 -3
- package/lib/components/messages/messages.js +3 -1
- package/lib/components/messages/preamble.d.ts +12 -0
- package/lib/components/messages/preamble.js +31 -0
- package/lib/message.d.ts +2 -1
- package/lib/message.js +3 -0
- package/lib/model.d.ts +1 -0
- package/lib/model.js +14 -13
- package/lib/registers/index.d.ts +1 -0
- package/lib/registers/index.js +1 -0
- package/lib/registers/preambles.d.ts +44 -0
- package/lib/registers/preambles.js +29 -0
- package/lib/types.d.ts +15 -3
- package/package.json +1 -1
- package/src/__tests__/preamble-registry.spec.ts +51 -0
- package/src/components/chat.tsx +6 -1
- package/src/components/input/buttons/send-button.tsx +4 -6
- package/src/components/messages/footer.tsx +4 -0
- package/src/components/messages/index.ts +1 -0
- package/src/components/messages/message-renderer.tsx +15 -20
- package/src/components/messages/message.tsx +8 -2
- package/src/components/messages/messages.tsx +11 -2
- package/src/components/messages/preamble.tsx +55 -0
- package/src/message.ts +10 -1
- package/src/model.ts +20 -13
- package/src/registers/index.ts +1 -0
- package/src/registers/preambles.ts +65 -0
- package/src/types.ts +15 -3
- package/style/chat.css +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { MessagePreambleRegistry } from '../registers/preambles';
|
|
6
|
+
describe('MessagePreambleRegistry', () => {
|
|
7
|
+
let registry;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
registry = new MessagePreambleRegistry();
|
|
10
|
+
});
|
|
11
|
+
it('should start with no components', () => {
|
|
12
|
+
expect(registry.getComponents()).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
it('should add a component', () => {
|
|
15
|
+
const component = () => null;
|
|
16
|
+
registry.addComponent(component);
|
|
17
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
18
|
+
expect(registry.getComponents()[0]).toBe(component);
|
|
19
|
+
});
|
|
20
|
+
it('should preserve insertion order', () => {
|
|
21
|
+
const first = () => null;
|
|
22
|
+
const second = () => null;
|
|
23
|
+
registry.addComponent(first);
|
|
24
|
+
registry.addComponent(second);
|
|
25
|
+
const components = registry.getComponents();
|
|
26
|
+
expect(components).toHaveLength(2);
|
|
27
|
+
expect(components[0]).toBe(first);
|
|
28
|
+
expect(components[1]).toBe(second);
|
|
29
|
+
});
|
|
30
|
+
it('should return a copy from getComponents', () => {
|
|
31
|
+
const component = () => null;
|
|
32
|
+
registry.addComponent(component);
|
|
33
|
+
const result = registry.getComponents();
|
|
34
|
+
result.push(() => null);
|
|
35
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/lib/components/chat.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { IThemeManager } from '@jupyterlab/apputils';
|
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { IInputToolbarRegistry } from './input';
|
|
5
5
|
import { IChatModel } from '../model';
|
|
6
|
-
import { IAttachmentOpenerRegistry, IChatCommandRegistry, IMessageFooterRegistry } from '../registers';
|
|
6
|
+
import { IAttachmentOpenerRegistry, IChatCommandRegistry, IMessageFooterRegistry, IMessagePreambleRegistry } from '../registers';
|
|
7
7
|
import { ChatArea } from '../types';
|
|
8
8
|
export declare function ChatBody(props: Chat.IChatProps): JSX.Element;
|
|
9
9
|
export declare function Chat(props: Chat.IOptions): JSX.Element;
|
|
@@ -39,6 +39,10 @@ export declare namespace Chat {
|
|
|
39
39
|
* The footer registry.
|
|
40
40
|
*/
|
|
41
41
|
messageFooterRegistry?: IMessageFooterRegistry;
|
|
42
|
+
/**
|
|
43
|
+
* The preamble registry for content above message body.
|
|
44
|
+
*/
|
|
45
|
+
messagePreambleRegistry?: IMessagePreambleRegistry;
|
|
42
46
|
/**
|
|
43
47
|
* The welcome message.
|
|
44
48
|
*/
|
|
@@ -10,7 +10,7 @@ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
|
10
10
|
* The send button.
|
|
11
11
|
*/
|
|
12
12
|
export function SendButton(props) {
|
|
13
|
-
const { model,
|
|
13
|
+
const { model, chatCommandRegistry, edit } = props;
|
|
14
14
|
// Don't show this button when in edit mode
|
|
15
15
|
if (edit) {
|
|
16
16
|
return React.createElement(React.Fragment, null);
|
|
@@ -45,12 +45,9 @@ export function SendButton(props) {
|
|
|
45
45
|
async function send() {
|
|
46
46
|
// Run all command providers
|
|
47
47
|
await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
|
|
48
|
-
|
|
49
|
-
await (chatModel === null || chatModel === void 0 ? void 0 : chatModel.sendMessage({
|
|
50
|
-
body: model.value
|
|
51
|
-
}));
|
|
52
|
-
// clear input model value & re-focus
|
|
48
|
+
const body = model.value;
|
|
53
49
|
model.value = '';
|
|
50
|
+
model.send(body);
|
|
54
51
|
model.focus();
|
|
55
52
|
}
|
|
56
53
|
return (React.createElement(TooltippedIconButton, { onClick: send, tooltip: tooltip, disabled: disabled, buttonProps: {
|
|
@@ -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
|
|
@@ -9,6 +9,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
|
|
9
9
|
import { MessageFooterComponent } from './footer';
|
|
10
10
|
import { ChatMessageHeader } from './header';
|
|
11
11
|
import { ChatMessage } from './message';
|
|
12
|
+
import { MessagePreambleComponent } from './preamble';
|
|
12
13
|
import { Navigation } from './navigation';
|
|
13
14
|
import { WelcomeMessage } from './welcome';
|
|
14
15
|
import { ScrollContainer } from '../scroll-container';
|
|
@@ -22,7 +23,7 @@ const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
|
22
23
|
*/
|
|
23
24
|
export function ChatMessages() {
|
|
24
25
|
var _a;
|
|
25
|
-
const { area, messageFooterRegistry, model, welcomeMessage } = useChatContext();
|
|
26
|
+
const { area, messageFooterRegistry, messagePreambleRegistry, model, welcomeMessage } = useChatContext();
|
|
26
27
|
const [messages, setMessages] = useState(model.messages);
|
|
27
28
|
const refMsgBox = useRef(null);
|
|
28
29
|
const [allRendered, setAllRendered] = useState(false);
|
|
@@ -161,6 +162,7 @@ export function ChatMessages() {
|
|
|
161
162
|
})
|
|
162
163
|
}, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
163
164
|
React.createElement(ChatMessageHeader, { message: message, isCurrentUser: isCurrentUser }),
|
|
165
|
+
messagePreambleRegistry && (React.createElement(MessagePreambleComponent, { message: message })),
|
|
164
166
|
React.createElement(ChatMessage, { message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) }),
|
|
165
167
|
messageFooterRegistry && (React.createElement(MessageFooterComponent, { message: message }))));
|
|
166
168
|
}))),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IMessage } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* The preamble component properties.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMessagePreambleProps {
|
|
7
|
+
message: IMessage;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders all registered preamble components vertically above the message body.
|
|
11
|
+
*/
|
|
12
|
+
export declare function MessagePreambleComponent(props: IMessagePreambleProps): JSX.Element | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import { useChatContext } from '../../context';
|
|
7
|
+
/**
|
|
8
|
+
* Renders all registered preamble components vertically above the message body.
|
|
9
|
+
*/
|
|
10
|
+
export function MessagePreambleComponent(props) {
|
|
11
|
+
const [message, setMessage] = useState(props.message.content);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
function messageChanged() {
|
|
14
|
+
setMessage(props.message.content);
|
|
15
|
+
}
|
|
16
|
+
props.message.changed.connect(messageChanged);
|
|
17
|
+
setMessage(props.message.content);
|
|
18
|
+
return () => {
|
|
19
|
+
props.message.changed.disconnect(messageChanged);
|
|
20
|
+
};
|
|
21
|
+
}, [props.message]);
|
|
22
|
+
const { model, messagePreambleRegistry } = useChatContext();
|
|
23
|
+
if (!messagePreambleRegistry) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const components = messagePreambleRegistry.getComponents();
|
|
27
|
+
if (!components.length) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return (React.createElement(React.Fragment, null, components.map((Component, i) => (React.createElement(Component, { key: i, model: model, message: message })))));
|
|
31
|
+
}
|
package/lib/message.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
2
2
|
import { ISignal } from '@lumino/signaling';
|
|
3
|
-
import { IAttachment, IMessageContent, IMessage, IUser } from './types';
|
|
3
|
+
import { IAttachment, IMessageContent, IMessage, IMessageMetadata, IUser } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* The message object.
|
|
6
6
|
*/
|
|
@@ -29,6 +29,7 @@ export declare class Message implements IMessage {
|
|
|
29
29
|
get deleted(): boolean | undefined;
|
|
30
30
|
get edited(): boolean | undefined;
|
|
31
31
|
get stacked(): boolean | undefined;
|
|
32
|
+
get metadata(): IMessageMetadata | undefined;
|
|
32
33
|
/**
|
|
33
34
|
* A signal emitting when the message has been updated.
|
|
34
35
|
*/
|
package/lib/message.js
CHANGED
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/lib/registers/index.d.ts
CHANGED
package/lib/registers/index.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { IChatModel } from '../model';
|
|
4
|
+
import { IMessageContent } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* The token providing the chat preamble registry.
|
|
7
|
+
*/
|
|
8
|
+
export declare const IMessagePreambleRegistry: Token<IMessagePreambleRegistry>;
|
|
9
|
+
/**
|
|
10
|
+
* The props passed to each preamble component.
|
|
11
|
+
*/
|
|
12
|
+
export type MessagePreambleProps = {
|
|
13
|
+
model: IChatModel;
|
|
14
|
+
message: IMessageContent;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The interface of a registry to provide message preamble components.
|
|
18
|
+
* Preamble components render above the message body, after the header.
|
|
19
|
+
*/
|
|
20
|
+
export interface IMessagePreambleRegistry {
|
|
21
|
+
/**
|
|
22
|
+
* Add a preamble component to the registry.
|
|
23
|
+
* Components are rendered in the order they are added.
|
|
24
|
+
*/
|
|
25
|
+
addComponent(component: (props: MessagePreambleProps) => JSX.Element | null): void;
|
|
26
|
+
/**
|
|
27
|
+
* Get all registered preamble components.
|
|
28
|
+
*/
|
|
29
|
+
getComponents(): ((props: MessagePreambleProps) => JSX.Element | null)[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The default implementation of the message preamble registry.
|
|
33
|
+
*/
|
|
34
|
+
export declare class MessagePreambleRegistry implements IMessagePreambleRegistry {
|
|
35
|
+
/**
|
|
36
|
+
* Add a preamble component to the registry.
|
|
37
|
+
*/
|
|
38
|
+
addComponent(component: (props: MessagePreambleProps) => JSX.Element | null): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get all registered preamble components.
|
|
41
|
+
*/
|
|
42
|
+
getComponents(): ((props: MessagePreambleProps) => JSX.Element | null)[];
|
|
43
|
+
private _components;
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Token } from '@lumino/coreutils';
|
|
6
|
+
/**
|
|
7
|
+
* The token providing the chat preamble registry.
|
|
8
|
+
*/
|
|
9
|
+
export const IMessagePreambleRegistry = new Token('@jupyter/chat:ChatPreambleRegistry');
|
|
10
|
+
/**
|
|
11
|
+
* The default implementation of the message preamble registry.
|
|
12
|
+
*/
|
|
13
|
+
export class MessagePreambleRegistry {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._components = [];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Add a preamble component to the registry.
|
|
19
|
+
*/
|
|
20
|
+
addComponent(component) {
|
|
21
|
+
this._components.push(component);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get all registered preamble components.
|
|
25
|
+
*/
|
|
26
|
+
getComponents() {
|
|
27
|
+
return [...this._components];
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -53,6 +53,20 @@ export interface IConfig {
|
|
|
53
53
|
*/
|
|
54
54
|
showDeleted?: boolean;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* An empty interface to describe optional metadata attached to a chat message.
|
|
58
|
+
* Extensions can augment this interface to add custom fields:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* declare module '@jupyter/chat' {
|
|
62
|
+
* interface IMessageMetadata {
|
|
63
|
+
* myField?: MyType;
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export interface IMessageMetadata {
|
|
69
|
+
}
|
|
56
70
|
/**
|
|
57
71
|
* The chat message description.
|
|
58
72
|
*/
|
|
@@ -68,10 +82,8 @@ export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
|
68
82
|
deleted?: boolean;
|
|
69
83
|
edited?: boolean;
|
|
70
84
|
stacked?: boolean;
|
|
85
|
+
metadata?: IMessageMetadata;
|
|
71
86
|
};
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
*/
|
|
75
87
|
export interface IMessage extends IMessageContent {
|
|
76
88
|
/**
|
|
77
89
|
* Update one or several fields of the message.
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MessagePreambleRegistry,
|
|
8
|
+
MessagePreambleProps
|
|
9
|
+
} from '../registers/preambles';
|
|
10
|
+
|
|
11
|
+
describe('MessagePreambleRegistry', () => {
|
|
12
|
+
let registry: MessagePreambleRegistry;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
registry = new MessagePreambleRegistry();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should start with no components', () => {
|
|
19
|
+
expect(registry.getComponents()).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should add a component', () => {
|
|
23
|
+
const component: (props: MessagePreambleProps) => JSX.Element | null = () =>
|
|
24
|
+
null;
|
|
25
|
+
registry.addComponent(component);
|
|
26
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
27
|
+
expect(registry.getComponents()[0]).toBe(component);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should preserve insertion order', () => {
|
|
31
|
+
const first: (props: MessagePreambleProps) => JSX.Element | null = () =>
|
|
32
|
+
null;
|
|
33
|
+
const second: (props: MessagePreambleProps) => JSX.Element | null = () =>
|
|
34
|
+
null;
|
|
35
|
+
registry.addComponent(first);
|
|
36
|
+
registry.addComponent(second);
|
|
37
|
+
const components = registry.getComponents();
|
|
38
|
+
expect(components).toHaveLength(2);
|
|
39
|
+
expect(components[0]).toBe(first);
|
|
40
|
+
expect(components[1]).toBe(second);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return a copy from getComponents', () => {
|
|
44
|
+
const component: (props: MessagePreambleProps) => JSX.Element | null = () =>
|
|
45
|
+
null;
|
|
46
|
+
registry.addComponent(component);
|
|
47
|
+
const result = registry.getComponents();
|
|
48
|
+
result.push(() => null);
|
|
49
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/components/chat.tsx
CHANGED
|
@@ -24,7 +24,8 @@ import { IChatModel } from '../model';
|
|
|
24
24
|
import {
|
|
25
25
|
IAttachmentOpenerRegistry,
|
|
26
26
|
IChatCommandRegistry,
|
|
27
|
-
IMessageFooterRegistry
|
|
27
|
+
IMessageFooterRegistry,
|
|
28
|
+
IMessagePreambleRegistry
|
|
28
29
|
} from '../registers';
|
|
29
30
|
import { ChatArea } from '../types';
|
|
30
31
|
|
|
@@ -168,6 +169,10 @@ export namespace Chat {
|
|
|
168
169
|
* The footer registry.
|
|
169
170
|
*/
|
|
170
171
|
messageFooterRegistry?: IMessageFooterRegistry;
|
|
172
|
+
/**
|
|
173
|
+
* The preamble registry for content above message body.
|
|
174
|
+
*/
|
|
175
|
+
messagePreambleRegistry?: IMessagePreambleRegistry;
|
|
171
176
|
/**
|
|
172
177
|
* The welcome message.
|
|
173
178
|
*/
|
|
@@ -18,7 +18,7 @@ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
|
18
18
|
export function SendButton(
|
|
19
19
|
props: InputToolbarRegistry.IToolbarItemProps
|
|
20
20
|
): JSX.Element {
|
|
21
|
-
const { model,
|
|
21
|
+
const { model, chatCommandRegistry, edit } = props;
|
|
22
22
|
|
|
23
23
|
// Don't show this button when in edit mode
|
|
24
24
|
if (edit) {
|
|
@@ -62,12 +62,10 @@ export function SendButton(
|
|
|
62
62
|
// Run all command providers
|
|
63
63
|
await chatCommandRegistry?.onSubmit(model);
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
body: model.value
|
|
68
|
-
});
|
|
69
|
-
// clear input model value & re-focus
|
|
65
|
+
const body = model.value;
|
|
66
|
+
|
|
70
67
|
model.value = '';
|
|
68
|
+
model.send(body);
|
|
71
69
|
model.focus();
|
|
72
70
|
}
|
|
73
71
|
|
|
@@ -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()}
|
|
@@ -11,6 +11,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
|
|
11
11
|
import { MessageFooterComponent } from './footer';
|
|
12
12
|
import { ChatMessageHeader } from './header';
|
|
13
13
|
import { ChatMessage } from './message';
|
|
14
|
+
import { MessagePreambleComponent } from './preamble';
|
|
14
15
|
import { Navigation } from './navigation';
|
|
15
16
|
import { WelcomeMessage } from './welcome';
|
|
16
17
|
import { ScrollContainer } from '../scroll-container';
|
|
@@ -27,8 +28,13 @@ const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
|
27
28
|
* The messages list component.
|
|
28
29
|
*/
|
|
29
30
|
export function ChatMessages(): JSX.Element {
|
|
30
|
-
const {
|
|
31
|
-
|
|
31
|
+
const {
|
|
32
|
+
area,
|
|
33
|
+
messageFooterRegistry,
|
|
34
|
+
messagePreambleRegistry,
|
|
35
|
+
model,
|
|
36
|
+
welcomeMessage
|
|
37
|
+
} = useChatContext();
|
|
32
38
|
|
|
33
39
|
const [messages, setMessages] = useState<IMessage[]>(model.messages);
|
|
34
40
|
const refMsgBox = useRef<HTMLDivElement>(null);
|
|
@@ -203,6 +209,9 @@ export function ChatMessages(): JSX.Element {
|
|
|
203
209
|
message={message}
|
|
204
210
|
isCurrentUser={isCurrentUser}
|
|
205
211
|
/>
|
|
212
|
+
{messagePreambleRegistry && (
|
|
213
|
+
<MessagePreambleComponent message={message} />
|
|
214
|
+
)}
|
|
206
215
|
<ChatMessage
|
|
207
216
|
message={message}
|
|
208
217
|
index={i}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { useChatContext } from '../../context';
|
|
9
|
+
import { IMessage, IMessageContent } from '../../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The preamble component properties.
|
|
13
|
+
*/
|
|
14
|
+
export interface IMessagePreambleProps {
|
|
15
|
+
message: IMessage;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders all registered preamble components vertically above the message body.
|
|
20
|
+
*/
|
|
21
|
+
export function MessagePreambleComponent(
|
|
22
|
+
props: IMessagePreambleProps
|
|
23
|
+
): JSX.Element | null {
|
|
24
|
+
const [message, setMessage] = useState<IMessageContent>(
|
|
25
|
+
props.message.content
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
function messageChanged() {
|
|
30
|
+
setMessage(props.message.content);
|
|
31
|
+
}
|
|
32
|
+
props.message.changed.connect(messageChanged);
|
|
33
|
+
setMessage(props.message.content);
|
|
34
|
+
return () => {
|
|
35
|
+
props.message.changed.disconnect(messageChanged);
|
|
36
|
+
};
|
|
37
|
+
}, [props.message]);
|
|
38
|
+
|
|
39
|
+
const { model, messagePreambleRegistry } = useChatContext();
|
|
40
|
+
if (!messagePreambleRegistry) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const components = messagePreambleRegistry.getComponents();
|
|
44
|
+
if (!components.length) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{components.map((Component, i) => (
|
|
51
|
+
<Component key={i} model={model} message={message} />
|
|
52
|
+
))}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
package/src/message.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
7
7
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
IAttachment,
|
|
10
|
+
IMessageContent,
|
|
11
|
+
IMessage,
|
|
12
|
+
IMessageMetadata,
|
|
13
|
+
IUser
|
|
14
|
+
} from './types';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* The message object.
|
|
@@ -65,6 +71,9 @@ export class Message implements IMessage {
|
|
|
65
71
|
get stacked(): boolean | undefined {
|
|
66
72
|
return this._content.stacked;
|
|
67
73
|
}
|
|
74
|
+
get metadata(): IMessageMetadata | undefined {
|
|
75
|
+
return this._content.metadata;
|
|
76
|
+
}
|
|
68
77
|
|
|
69
78
|
/**
|
|
70
79
|
* A signal emitting when the message has been updated.
|
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/src/registers/index.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Token } from '@lumino/coreutils';
|
|
7
|
+
import { IChatModel } from '../model';
|
|
8
|
+
import { IMessageContent } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The token providing the chat preamble registry.
|
|
12
|
+
*/
|
|
13
|
+
export const IMessagePreambleRegistry = new Token<IMessagePreambleRegistry>(
|
|
14
|
+
'@jupyter/chat:ChatPreambleRegistry'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The props passed to each preamble component.
|
|
19
|
+
*/
|
|
20
|
+
export type MessagePreambleProps = {
|
|
21
|
+
model: IChatModel;
|
|
22
|
+
message: IMessageContent;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The interface of a registry to provide message preamble components.
|
|
27
|
+
* Preamble components render above the message body, after the header.
|
|
28
|
+
*/
|
|
29
|
+
export interface IMessagePreambleRegistry {
|
|
30
|
+
/**
|
|
31
|
+
* Add a preamble component to the registry.
|
|
32
|
+
* Components are rendered in the order they are added.
|
|
33
|
+
*/
|
|
34
|
+
addComponent(
|
|
35
|
+
component: (props: MessagePreambleProps) => JSX.Element | null
|
|
36
|
+
): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get all registered preamble components.
|
|
39
|
+
*/
|
|
40
|
+
getComponents(): ((props: MessagePreambleProps) => JSX.Element | null)[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The default implementation of the message preamble registry.
|
|
45
|
+
*/
|
|
46
|
+
export class MessagePreambleRegistry implements IMessagePreambleRegistry {
|
|
47
|
+
/**
|
|
48
|
+
* Add a preamble component to the registry.
|
|
49
|
+
*/
|
|
50
|
+
addComponent(
|
|
51
|
+
component: (props: MessagePreambleProps) => JSX.Element | null
|
|
52
|
+
): void {
|
|
53
|
+
this._components.push(component);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all registered preamble components.
|
|
58
|
+
*/
|
|
59
|
+
getComponents(): ((props: MessagePreambleProps) => JSX.Element | null)[] {
|
|
60
|
+
return [...this._components];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _components: ((props: MessagePreambleProps) => JSX.Element | null)[] =
|
|
64
|
+
[];
|
|
65
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -61,6 +61,20 @@ export interface IConfig {
|
|
|
61
61
|
showDeleted?: boolean;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* An empty interface to describe optional metadata attached to a chat message.
|
|
66
|
+
* Extensions can augment this interface to add custom fields:
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* declare module '@jupyter/chat' {
|
|
70
|
+
* interface IMessageMetadata {
|
|
71
|
+
* myField?: MyType;
|
|
72
|
+
* }
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export interface IMessageMetadata {} /* eslint-disable-line @typescript-eslint/no-empty-object-type */
|
|
77
|
+
|
|
64
78
|
/**
|
|
65
79
|
* The chat message description.
|
|
66
80
|
*/
|
|
@@ -79,11 +93,9 @@ export type IMessageContent<T = IUser, U = IAttachment> = {
|
|
|
79
93
|
deleted?: boolean;
|
|
80
94
|
edited?: boolean;
|
|
81
95
|
stacked?: boolean;
|
|
96
|
+
metadata?: IMessageMetadata;
|
|
82
97
|
};
|
|
83
98
|
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*/
|
|
87
99
|
export interface IMessage extends IMessageContent {
|
|
88
100
|
/**
|
|
89
101
|
* Update one or several fields of the message.
|
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
|
|