@jupyter/chat 0.13.0 → 0.15.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/active-cell-manager.d.ts +2 -0
- package/lib/active-cell-manager.js +7 -2
- package/lib/components/avatar.d.ts +20 -0
- package/lib/components/avatar.js +29 -0
- package/lib/components/chat.d.ts +1 -3
- package/lib/components/chat.js +2 -3
- package/lib/components/index.d.ts +2 -3
- package/lib/components/index.js +2 -3
- package/lib/components/input/buttons/send-button.js +15 -5
- package/lib/components/{chat-input.d.ts → input/chat-input.d.ts} +3 -3
- package/lib/components/{chat-input.js → input/chat-input.js} +8 -5
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/toolbar-registry.d.ts +6 -0
- package/lib/components/input/use-chat-commands.d.ts +1 -1
- package/lib/components/input/use-chat-commands.js +32 -13
- package/lib/components/messages/footer.d.ts +2 -2
- package/lib/components/messages/footer.js +1 -1
- package/lib/components/messages/header.d.ts +16 -0
- package/lib/components/messages/header.js +85 -0
- package/lib/components/messages/index.d.ts +9 -0
- package/lib/components/messages/index.js +13 -0
- package/lib/components/messages/message-renderer.js +1 -1
- package/lib/components/messages/message.d.ts +21 -0
- package/lib/components/messages/message.js +102 -0
- package/lib/components/messages/messages.d.ts +38 -0
- package/lib/components/messages/messages.js +139 -0
- package/lib/components/messages/navigation.d.ts +20 -0
- package/lib/components/messages/navigation.js +98 -0
- package/lib/components/messages/writers.d.ts +16 -0
- package/lib/components/messages/writers.js +39 -0
- package/lib/context.d.ts +1 -1
- package/lib/index.d.ts +2 -6
- package/lib/index.js +2 -6
- package/lib/input-model.js +30 -3
- package/lib/{registry.d.ts → registers/attachment-openers.d.ts} +1 -1
- package/lib/registers/chat-commands.d.ts +108 -0
- package/lib/{chat-commands/registry.js → registers/chat-commands.js} +8 -8
- package/lib/{footers/registry.d.ts → registers/footers.d.ts} +30 -5
- package/lib/registers/index.d.ts +3 -0
- package/lib/{footers → registers}/index.js +3 -2
- package/lib/selection-watcher.d.ts +11 -1
- package/lib/selection-watcher.js +10 -4
- package/lib/types.d.ts +7 -2
- package/lib/utils.js +8 -6
- package/lib/widgets/index.d.ts +3 -0
- package/lib/{chat-commands → widgets}/index.js +3 -2
- package/package.json +3 -1
- package/src/active-cell-manager.ts +10 -1
- package/src/components/avatar.tsx +68 -0
- package/src/components/chat.tsx +11 -6
- package/src/components/index.ts +2 -3
- package/src/components/input/buttons/send-button.tsx +17 -5
- package/src/components/{chat-input.tsx → input/chat-input.tsx} +13 -8
- package/src/components/input/index.ts +1 -0
- package/src/components/input/toolbar-registry.tsx +6 -0
- package/src/components/input/use-chat-commands.tsx +39 -16
- package/src/components/messages/footer.tsx +5 -2
- package/src/components/messages/header.tsx +133 -0
- package/src/components/messages/index.ts +14 -0
- package/src/components/messages/message-renderer.tsx +1 -1
- package/src/components/messages/message.tsx +156 -0
- package/src/components/messages/messages.tsx +218 -0
- package/src/components/messages/navigation.tsx +167 -0
- package/src/components/messages/welcome.tsx +1 -0
- package/src/components/messages/writers.tsx +81 -0
- package/src/context.ts +1 -1
- package/src/index.ts +2 -6
- package/src/input-model.ts +33 -4
- package/src/{registry.ts → registers/attachment-openers.ts} +2 -1
- package/src/registers/chat-commands.ts +142 -0
- package/src/{footers/registry.ts → registers/footers.ts} +35 -8
- package/src/{footers → registers}/index.ts +3 -2
- package/src/selection-watcher.ts +28 -5
- package/src/types.ts +7 -2
- package/src/utils.ts +8 -6
- package/src/{chat-commands → widgets}/index.ts +3 -2
- package/style/chat.css +82 -0
- package/lib/chat-commands/index.d.ts +0 -2
- package/lib/chat-commands/registry.d.ts +0 -28
- package/lib/chat-commands/types.d.ts +0 -52
- package/lib/chat-commands/types.js +0 -5
- package/lib/components/chat-messages.d.ts +0 -119
- package/lib/components/chat-messages.js +0 -446
- package/lib/footers/index.d.ts +0 -2
- package/lib/footers/types.d.ts +0 -26
- package/lib/footers/types.js +0 -5
- package/src/chat-commands/registry.ts +0 -60
- package/src/chat-commands/types.ts +0 -67
- package/src/components/chat-messages.tsx +0 -739
- package/src/footers/types.ts +0 -33
- package/lib/components/{toolbar.d.ts → messages/toolbar.d.ts} +0 -0
- package/lib/components/{toolbar.js → messages/toolbar.js} +0 -0
- package/lib/{registry.js → registers/attachment-openers.js} +0 -0
- package/lib/{footers/registry.js → registers/footers.js} +4 -4
- /package/src/components/{toolbar.tsx → messages/toolbar.tsx} +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import React, { forwardRef, useEffect, useState } from 'react';
|
|
6
|
+
import { MessageRenderer } from './message-renderer';
|
|
7
|
+
import { AttachmentPreviewList } from '../attachments';
|
|
8
|
+
import { ChatInput } from '../input';
|
|
9
|
+
import { InputModel } from '../../input-model';
|
|
10
|
+
import { replaceSpanToMention } from '../../utils';
|
|
11
|
+
/**
|
|
12
|
+
* The message component body.
|
|
13
|
+
*/
|
|
14
|
+
export const ChatMessage = forwardRef((props, ref) => {
|
|
15
|
+
const { message, model, rmRegistry } = props;
|
|
16
|
+
const [edit, setEdit] = useState(false);
|
|
17
|
+
const [deleted, setDeleted] = useState(false);
|
|
18
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
19
|
+
const [canDelete, setCanDelete] = useState(false);
|
|
20
|
+
// Look if the message can be deleted or edited.
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
var _a;
|
|
23
|
+
// Init canDelete and canEdit state.
|
|
24
|
+
setDeleted((_a = message.deleted) !== null && _a !== void 0 ? _a : false);
|
|
25
|
+
if (model.user !== undefined && !message.deleted) {
|
|
26
|
+
if (model.user.username === message.sender.username) {
|
|
27
|
+
setCanEdit(model.updateMessage !== undefined);
|
|
28
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (message.sender.bot) {
|
|
32
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
setCanEdit(false);
|
|
37
|
+
setCanDelete(false);
|
|
38
|
+
}
|
|
39
|
+
}, [model, message]);
|
|
40
|
+
// Create an input model only if the message is edited.
|
|
41
|
+
const startEdition = () => {
|
|
42
|
+
var _a;
|
|
43
|
+
if (!canEdit) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let body = message.body;
|
|
47
|
+
(_a = message.mentions) === null || _a === void 0 ? void 0 : _a.forEach(user => {
|
|
48
|
+
body = replaceSpanToMention(body, user);
|
|
49
|
+
});
|
|
50
|
+
const inputModel = new InputModel({
|
|
51
|
+
chatContext: model.createChatContext(),
|
|
52
|
+
onSend: (input, model) => updateMessage(message.id, input, model),
|
|
53
|
+
onCancel: () => cancelEdition(),
|
|
54
|
+
value: body,
|
|
55
|
+
activeCellManager: model.activeCellManager,
|
|
56
|
+
selectionWatcher: model.selectionWatcher,
|
|
57
|
+
documentManager: model.documentManager,
|
|
58
|
+
config: {
|
|
59
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
60
|
+
},
|
|
61
|
+
attachments: message.attachments,
|
|
62
|
+
mentions: message.mentions
|
|
63
|
+
});
|
|
64
|
+
model.addEditionModel(message.id, inputModel);
|
|
65
|
+
setEdit(true);
|
|
66
|
+
};
|
|
67
|
+
// Cancel the current edition of the message.
|
|
68
|
+
const cancelEdition = () => {
|
|
69
|
+
var _a;
|
|
70
|
+
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
71
|
+
setEdit(false);
|
|
72
|
+
};
|
|
73
|
+
// Update the content of the message.
|
|
74
|
+
const updateMessage = (id, input, inputModel) => {
|
|
75
|
+
var _a;
|
|
76
|
+
if (!canEdit || !inputModel) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Update the message
|
|
80
|
+
const updatedMessage = { ...message };
|
|
81
|
+
updatedMessage.body = input;
|
|
82
|
+
updatedMessage.attachments = inputModel.attachments;
|
|
83
|
+
updatedMessage.mentions = inputModel.mentions;
|
|
84
|
+
model.updateMessage(id, updatedMessage);
|
|
85
|
+
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
86
|
+
setEdit(false);
|
|
87
|
+
};
|
|
88
|
+
// Delete the message.
|
|
89
|
+
const deleteMessage = (id) => {
|
|
90
|
+
if (!canDelete) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
model.deleteMessage(id);
|
|
94
|
+
};
|
|
95
|
+
// Empty if the message has been deleted.
|
|
96
|
+
return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index },
|
|
97
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), chatCommandRegistry: props.chatCommandRegistry, toolbarRegistry: props.inputToolbarRegistry })) : (React.createElement(MessageRenderer, { rmRegistry: rmRegistry, markdownStr: message.body, model: model, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: props.renderedPromise })),
|
|
98
|
+
message.attachments && !edit && (
|
|
99
|
+
// Display the attachments only if message is not edited, otherwise the
|
|
100
|
+
// input component display them.
|
|
101
|
+
React.createElement(AttachmentPreviewList, { attachments: message.attachments }))));
|
|
102
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
3
|
+
import { IInputToolbarRegistry } from '../input';
|
|
4
|
+
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
|
|
5
|
+
import { IChatModel } from '../../model';
|
|
6
|
+
/**
|
|
7
|
+
* The base components props.
|
|
8
|
+
*/
|
|
9
|
+
export type BaseMessageProps = {
|
|
10
|
+
/**
|
|
11
|
+
* The mime renderer registry.
|
|
12
|
+
*/
|
|
13
|
+
rmRegistry: IRenderMimeRegistry;
|
|
14
|
+
/**
|
|
15
|
+
* The chat model.
|
|
16
|
+
*/
|
|
17
|
+
model: IChatModel;
|
|
18
|
+
/**
|
|
19
|
+
* The chat commands registry.
|
|
20
|
+
*/
|
|
21
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
22
|
+
/**
|
|
23
|
+
* The input toolbar registry.
|
|
24
|
+
*/
|
|
25
|
+
inputToolbarRegistry: IInputToolbarRegistry;
|
|
26
|
+
/**
|
|
27
|
+
* The footer registry.
|
|
28
|
+
*/
|
|
29
|
+
messageFooterRegistry?: IMessageFooterRegistry;
|
|
30
|
+
/**
|
|
31
|
+
* The welcome message.
|
|
32
|
+
*/
|
|
33
|
+
welcomeMessage?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The messages list component.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ChatMessages(props: BaseMessageProps): JSX.Element;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
6
|
+
import { Box } from '@mui/material';
|
|
7
|
+
import clsx from 'clsx';
|
|
8
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
9
|
+
import { MessageFooterComponent } from './footer';
|
|
10
|
+
import { ChatMessageHeader } from './header';
|
|
11
|
+
import { ChatMessage } from './message';
|
|
12
|
+
import { Navigation } from './navigation';
|
|
13
|
+
import { WelcomeMessage } from './welcome';
|
|
14
|
+
import { WritingUsersList } from './writers';
|
|
15
|
+
import { ScrollContainer } from '../scroll-container';
|
|
16
|
+
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
|
|
17
|
+
const MESSAGE_CLASS = 'jp-chat-message';
|
|
18
|
+
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked';
|
|
19
|
+
/**
|
|
20
|
+
* The messages list component.
|
|
21
|
+
*/
|
|
22
|
+
export function ChatMessages(props) {
|
|
23
|
+
const { model } = props;
|
|
24
|
+
const [messages, setMessages] = useState(model.messages);
|
|
25
|
+
const refMsgBox = useRef(null);
|
|
26
|
+
const [currentWriters, setCurrentWriters] = useState([]);
|
|
27
|
+
const [allRendered, setAllRendered] = useState(false);
|
|
28
|
+
// The list of message DOM and their rendered promises.
|
|
29
|
+
const listRef = useRef([]);
|
|
30
|
+
const renderedPromise = useRef([]);
|
|
31
|
+
/**
|
|
32
|
+
* Effect: fetch history and config on initial render
|
|
33
|
+
*/
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
async function fetchHistory() {
|
|
36
|
+
if (!model.getHistory) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
model
|
|
40
|
+
.getHistory()
|
|
41
|
+
.then(history => setMessages(history.messages))
|
|
42
|
+
.catch(e => console.error(e));
|
|
43
|
+
}
|
|
44
|
+
fetchHistory();
|
|
45
|
+
setCurrentWriters([]);
|
|
46
|
+
}, [model]);
|
|
47
|
+
/**
|
|
48
|
+
* Effect: listen to chat messages.
|
|
49
|
+
*/
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
var _a;
|
|
52
|
+
function handleChatEvents() {
|
|
53
|
+
setMessages([...model.messages]);
|
|
54
|
+
}
|
|
55
|
+
function handleWritersChange(_, writers) {
|
|
56
|
+
setCurrentWriters(writers.map(writer => writer.user));
|
|
57
|
+
}
|
|
58
|
+
model.messagesUpdated.connect(handleChatEvents);
|
|
59
|
+
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.connect(handleWritersChange);
|
|
60
|
+
return function cleanup() {
|
|
61
|
+
var _a;
|
|
62
|
+
model.messagesUpdated.disconnect(handleChatEvents);
|
|
63
|
+
(_a = model.writersChanged) === null || _a === void 0 ? void 0 : _a.disconnect(handleChatEvents);
|
|
64
|
+
};
|
|
65
|
+
}, [model]);
|
|
66
|
+
/**
|
|
67
|
+
* Observe the messages to update the current viewport and the unread messages.
|
|
68
|
+
*/
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const observer = new IntersectionObserver(entries => {
|
|
71
|
+
var _a;
|
|
72
|
+
// Used on first rendering, to ensure all the message as been rendered once.
|
|
73
|
+
if (!allRendered) {
|
|
74
|
+
Promise.all(renderedPromise.current.map(p => p.promise)).then(() => {
|
|
75
|
+
setAllRendered(true);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const unread = [...model.unreadMessages];
|
|
79
|
+
let unreadModified = false;
|
|
80
|
+
const inViewport = [...((_a = model.messagesInViewport) !== null && _a !== void 0 ? _a : [])];
|
|
81
|
+
entries.forEach(entry => {
|
|
82
|
+
var _a;
|
|
83
|
+
const index = parseInt((_a = entry.target.getAttribute('data-index')) !== null && _a !== void 0 ? _a : '');
|
|
84
|
+
if (!isNaN(index)) {
|
|
85
|
+
const viewportIdx = inViewport.indexOf(index);
|
|
86
|
+
if (!entry.isIntersecting && viewportIdx !== -1) {
|
|
87
|
+
inViewport.splice(viewportIdx, 1);
|
|
88
|
+
}
|
|
89
|
+
else if (entry.isIntersecting && viewportIdx === -1) {
|
|
90
|
+
inViewport.push(index);
|
|
91
|
+
}
|
|
92
|
+
if (unread.length) {
|
|
93
|
+
const unreadIdx = unread.indexOf(index);
|
|
94
|
+
if (unreadIdx !== -1 && entry.isIntersecting) {
|
|
95
|
+
unread.splice(unreadIdx, 1);
|
|
96
|
+
unreadModified = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
props.model.messagesInViewport = inViewport;
|
|
102
|
+
// Ensure that all messages are rendered before updating unread messages, otherwise
|
|
103
|
+
// it can lead to wrong assumption , because more message are in the viewport
|
|
104
|
+
// before they are rendered.
|
|
105
|
+
if (allRendered && unreadModified) {
|
|
106
|
+
model.unreadMessages = unread;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Observe the messages.
|
|
111
|
+
*/
|
|
112
|
+
listRef.current.forEach(item => {
|
|
113
|
+
if (item) {
|
|
114
|
+
observer.observe(item);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
listRef.current.forEach(item => {
|
|
119
|
+
if (item) {
|
|
120
|
+
observer.unobserve(item);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}, [messages, allRendered]);
|
|
125
|
+
return (React.createElement(React.Fragment, null,
|
|
126
|
+
React.createElement(ScrollContainer, { sx: { flexGrow: 1 } },
|
|
127
|
+
props.welcomeMessage && (React.createElement(WelcomeMessage, { rmRegistry: props.rmRegistry, content: props.welcomeMessage })),
|
|
128
|
+
React.createElement(Box, { ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, messages.map((message, i) => {
|
|
129
|
+
renderedPromise.current[i] = new PromiseDelegate();
|
|
130
|
+
return (
|
|
131
|
+
// extra div needed to ensure each bubble is on a new line
|
|
132
|
+
React.createElement(Box, { key: i, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
133
|
+
React.createElement(ChatMessageHeader, { message: message }),
|
|
134
|
+
React.createElement(ChatMessage, { ...props, message: message, index: i, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) }),
|
|
135
|
+
props.messageFooterRegistry && (React.createElement(MessageFooterComponent, { registry: props.messageFooterRegistry, message: message, model: model }))));
|
|
136
|
+
}))),
|
|
137
|
+
React.createElement(WritingUsersList, { writers: currentWriters }),
|
|
138
|
+
React.createElement(Navigation, { ...props, refMsgBox: refMsgBox, allRendered: allRendered })));
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BaseMessageProps } from './messages';
|
|
3
|
+
/**
|
|
4
|
+
* The navigation component props.
|
|
5
|
+
*/
|
|
6
|
+
type NavigationProps = BaseMessageProps & {
|
|
7
|
+
/**
|
|
8
|
+
* The reference to the messages container.
|
|
9
|
+
*/
|
|
10
|
+
refMsgBox: React.RefObject<HTMLDivElement>;
|
|
11
|
+
/**
|
|
12
|
+
* Whether all the messages has been rendered once on first display.
|
|
13
|
+
*/
|
|
14
|
+
allRendered: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The navigation component, to navigate to unread messages.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Navigation(props: NavigationProps): JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Button } from '@jupyter/react-components';
|
|
6
|
+
import { LabIcon, caretDownEmptyIcon, classes } from '@jupyterlab/ui-components';
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
9
|
+
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
|
|
10
|
+
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
|
|
11
|
+
const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
|
|
12
|
+
/**
|
|
13
|
+
* The navigation component, to navigate to unread messages.
|
|
14
|
+
*/
|
|
15
|
+
export function Navigation(props) {
|
|
16
|
+
const { model } = props;
|
|
17
|
+
const [lastInViewport, setLastInViewport] = useState(true);
|
|
18
|
+
const [unreadBefore, setUnreadBefore] = useState(null);
|
|
19
|
+
const [unreadAfter, setUnreadAfter] = useState(null);
|
|
20
|
+
const gotoMessage = (msgIdx, alignToTop = true) => {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
(_b = (_a = props.refMsgBox.current) === null || _a === void 0 ? void 0 : _a.children.item(msgIdx)) === null || _b === void 0 ? void 0 : _b.scrollIntoView(alignToTop);
|
|
23
|
+
};
|
|
24
|
+
// Listen for change in unread messages, and find the first unread message before or
|
|
25
|
+
// after the current viewport, to display navigation buttons.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
var _a;
|
|
28
|
+
// Do not attempt to display navigation until messages are rendered, it can lead to
|
|
29
|
+
// wrong assumption, because more messages are in the viewport before they are
|
|
30
|
+
// rendered.
|
|
31
|
+
if (!props.allRendered) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const unreadChanged = (model, unreadIndexes) => {
|
|
35
|
+
const viewport = model.messagesInViewport;
|
|
36
|
+
if (!viewport) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Initialize the next values with the current values if there still relevant.
|
|
40
|
+
let before = unreadBefore !== null &&
|
|
41
|
+
unreadIndexes.includes(unreadBefore) &&
|
|
42
|
+
unreadBefore < Math.min(...viewport)
|
|
43
|
+
? unreadBefore
|
|
44
|
+
: null;
|
|
45
|
+
let after = unreadAfter !== null &&
|
|
46
|
+
unreadIndexes.includes(unreadAfter) &&
|
|
47
|
+
unreadAfter > Math.max(...viewport)
|
|
48
|
+
? unreadAfter
|
|
49
|
+
: null;
|
|
50
|
+
unreadIndexes.forEach(unread => {
|
|
51
|
+
if (viewport === null || viewport === void 0 ? void 0 : viewport.includes(unread)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (unread < (before !== null && before !== void 0 ? before : Math.min(...viewport))) {
|
|
55
|
+
before = unread;
|
|
56
|
+
}
|
|
57
|
+
else if (unread > Math.max(...viewport) &&
|
|
58
|
+
unread < (after !== null && after !== void 0 ? after : model.messages.length)) {
|
|
59
|
+
after = unread;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
setUnreadBefore(before);
|
|
63
|
+
setUnreadAfter(after);
|
|
64
|
+
};
|
|
65
|
+
(_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.connect(unreadChanged);
|
|
66
|
+
unreadChanged(model, model.unreadMessages);
|
|
67
|
+
// Move to the last the message after all the messages have been first rendered.
|
|
68
|
+
gotoMessage(model.messages.length - 1, false);
|
|
69
|
+
return () => {
|
|
70
|
+
var _a;
|
|
71
|
+
(_a = model.unreadChanged) === null || _a === void 0 ? void 0 : _a.disconnect(unreadChanged);
|
|
72
|
+
};
|
|
73
|
+
}, [model, props.allRendered]);
|
|
74
|
+
// Listen for change in the viewport, to add a navigation button if the last is not
|
|
75
|
+
// in viewport.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
const viewportChanged = (model, viewport) => {
|
|
79
|
+
setLastInViewport(model.messages.length === 0 ||
|
|
80
|
+
viewport.includes(model.messages.length - 1));
|
|
81
|
+
};
|
|
82
|
+
(_a = model.viewportChanged) === null || _a === void 0 ? void 0 : _a.connect(viewportChanged);
|
|
83
|
+
viewportChanged(model, (_b = model.messagesInViewport) !== null && _b !== void 0 ? _b : []);
|
|
84
|
+
return () => {
|
|
85
|
+
var _a;
|
|
86
|
+
(_a = model.viewportChanged) === null || _a === void 0 ? void 0 : _a.disconnect(viewportChanged);
|
|
87
|
+
};
|
|
88
|
+
}, [model]);
|
|
89
|
+
return (React.createElement(React.Fragment, null,
|
|
90
|
+
unreadBefore !== null && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`, onClick: () => gotoMessage(unreadBefore), title: 'Go to unread messages' },
|
|
91
|
+
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') }))),
|
|
92
|
+
(unreadAfter !== null || !lastInViewport) && (React.createElement(Button, { className: `${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`, onClick: unreadAfter === null
|
|
93
|
+
? () => gotoMessage(model.messages.length - 1, false)
|
|
94
|
+
: () => gotoMessage(unreadAfter), title: unreadAfter !== null
|
|
95
|
+
? 'Go to unread messages'
|
|
96
|
+
: 'Go to last message' },
|
|
97
|
+
React.createElement(LabIcon.resolveReact, { display: 'flex', icon: caretDownEmptyIcon, iconClass: classes('jp-Icon') })))));
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IUser } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* The writers component props.
|
|
5
|
+
*/
|
|
6
|
+
type writersProps = {
|
|
7
|
+
/**
|
|
8
|
+
* The list of users currently writing.
|
|
9
|
+
*/
|
|
10
|
+
writers: IUser[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The writers component, displaying the current writers.
|
|
14
|
+
*/
|
|
15
|
+
export declare function WritingUsersList(props: writersProps): JSX.Element | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Typography } from '@mui/material';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import { Avatar } from '../avatar';
|
|
8
|
+
const WRITERS_CLASS = 'jp-chat-writers';
|
|
9
|
+
/**
|
|
10
|
+
* Animated typing indicator component
|
|
11
|
+
*/
|
|
12
|
+
const TypingIndicator = () => (React.createElement(Box, { className: "jp-chat-typing-indicator" },
|
|
13
|
+
React.createElement("span", { className: "jp-chat-typing-dot" }),
|
|
14
|
+
React.createElement("span", { className: "jp-chat-typing-dot" }),
|
|
15
|
+
React.createElement("span", { className: "jp-chat-typing-dot" })));
|
|
16
|
+
/**
|
|
17
|
+
* The writers component, displaying the current writers.
|
|
18
|
+
*/
|
|
19
|
+
export function WritingUsersList(props) {
|
|
20
|
+
const { writers } = props;
|
|
21
|
+
// Don't render if no writers
|
|
22
|
+
if (writers.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const writersText = writers.length > 1 ? ' are writing' : ' is writing';
|
|
26
|
+
const writingUsers = useMemo(() => writers.map((writer, index) => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
return (React.createElement(Box, { key: writer.username || index, className: "jp-chat-writer-item" },
|
|
29
|
+
React.createElement(Avatar, { user: writer, small: true }),
|
|
30
|
+
React.createElement(Typography, { variant: "body2", className: "jp-chat-writer-name" }, (_b = (_a = writer.display_name) !== null && _a !== void 0 ? _a : writer.name) !== null && _b !== void 0 ? _b : (writer.username || 'User undefined')),
|
|
31
|
+
index < writers.length - 1 && (React.createElement(Typography, { variant: "body2", className: "jp-chat-writer-separator" }, index < writers.length - 2 ? ', ' : ' and '))));
|
|
32
|
+
}), [writers]);
|
|
33
|
+
return (React.createElement(Box, { className: `${WRITERS_CLASS}` },
|
|
34
|
+
React.createElement(Box, { className: "jp-chat-writers-content" },
|
|
35
|
+
writingUsers,
|
|
36
|
+
React.createElement(Box, { className: "jp-chat-writing-status" },
|
|
37
|
+
React.createElement(Typography, { variant: "body2", className: "jp-chat-writing-text" }, writersText),
|
|
38
|
+
React.createElement(TypingIndicator, null)))));
|
|
39
|
+
}
|
package/lib/context.d.ts
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
export * from './active-cell-manager';
|
|
2
|
-
export * from './chat-commands';
|
|
3
2
|
export * from './components';
|
|
4
|
-
export * from './footers';
|
|
5
3
|
export * from './icons';
|
|
6
4
|
export * from './input-model';
|
|
7
5
|
export * from './markdown-renderer';
|
|
8
6
|
export * from './model';
|
|
9
|
-
export * from './
|
|
7
|
+
export * from './registers';
|
|
10
8
|
export * from './selection-watcher';
|
|
11
9
|
export * from './types';
|
|
12
|
-
export * from './widgets
|
|
13
|
-
export * from './widgets/chat-sidebar';
|
|
14
|
-
export * from './widgets/chat-widget';
|
|
10
|
+
export * from './widgets';
|
package/lib/index.js
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
export * from './active-cell-manager';
|
|
6
|
-
export * from './chat-commands';
|
|
7
6
|
export * from './components';
|
|
8
|
-
export * from './footers';
|
|
9
7
|
export * from './icons';
|
|
10
8
|
export * from './input-model';
|
|
11
9
|
export * from './markdown-renderer';
|
|
12
10
|
export * from './model';
|
|
13
|
-
export * from './
|
|
11
|
+
export * from './registers';
|
|
14
12
|
export * from './selection-watcher';
|
|
15
13
|
export * from './types';
|
|
16
|
-
export * from './widgets
|
|
17
|
-
export * from './widgets/chat-sidebar';
|
|
18
|
-
export * from './widgets/chat-widget';
|
|
14
|
+
export * from './widgets';
|
package/lib/input-model.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { Signal } from '@lumino/signaling';
|
|
6
|
-
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
7
6
|
/**
|
|
8
7
|
* The input model.
|
|
9
8
|
*/
|
|
@@ -254,14 +253,42 @@ export class InputModel {
|
|
|
254
253
|
}
|
|
255
254
|
var Private;
|
|
256
255
|
(function (Private) {
|
|
256
|
+
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
257
|
+
/**
|
|
258
|
+
* Returns the start index (inclusive) & end index (exclusive) that contain
|
|
259
|
+
* the current word. The start & end index can be passed to `String.slice()`
|
|
260
|
+
* to extract the current word. The returned range never includes any
|
|
261
|
+
* whitespace character unless it is escaped by a backslash `\`.
|
|
262
|
+
*
|
|
263
|
+
* NOTE: the escape sequence handling here is naive and non-recursive. This
|
|
264
|
+
* function considers the space in "`\\ `" as escaped, even though "`\\ `"
|
|
265
|
+
* defines a backslash followed by an _unescaped_ space in most languages.
|
|
266
|
+
*/
|
|
257
267
|
function getCurrentWordBoundaries(input, cursorIndex) {
|
|
258
268
|
let start = cursorIndex;
|
|
259
269
|
let end = cursorIndex;
|
|
260
270
|
const n = input.length;
|
|
261
|
-
while (
|
|
271
|
+
while (
|
|
272
|
+
// terminate when `input[start - 1]` is whitespace
|
|
273
|
+
// i.e. `input[start]` is never whitespace after exiting
|
|
274
|
+
(start - 1 >= 0 && !WHITESPACE.has(input[start - 1])) ||
|
|
275
|
+
// unless it is preceded by a backslash
|
|
276
|
+
(start - 2 >= 0 &&
|
|
277
|
+
input[start - 2] === '\\' &&
|
|
278
|
+
WHITESPACE.has(input[start - 1]))) {
|
|
262
279
|
start--;
|
|
263
280
|
}
|
|
264
|
-
|
|
281
|
+
// `end` is an exclusive index unlike `start`, hence the different `while`
|
|
282
|
+
// condition here
|
|
283
|
+
while (
|
|
284
|
+
// terminate when `input[end]` is whitespace
|
|
285
|
+
// i.e. `input[end]` may be whitespace after exiting
|
|
286
|
+
(end < n && !WHITESPACE.has(input[end])) ||
|
|
287
|
+
// unless it is preceded by a backslash
|
|
288
|
+
(end < n &&
|
|
289
|
+
end - 1 >= 0 &&
|
|
290
|
+
input[end - 1] === '\\' &&
|
|
291
|
+
WHITESPACE.has(input[end]))) {
|
|
265
292
|
end++;
|
|
266
293
|
}
|
|
267
294
|
return [start, end];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Token } from '@lumino/coreutils';
|
|
4
|
+
import { IInputModel } from '../input-model';
|
|
5
|
+
/**
|
|
6
|
+
* The token for the chat command registry, which can be provided by an extension
|
|
7
|
+
* using @jupyter/chat package.
|
|
8
|
+
*/
|
|
9
|
+
export declare const IChatCommandRegistry: Token<IChatCommandRegistry>;
|
|
10
|
+
/**
|
|
11
|
+
* Interface of a chat command registry, which tracks a list of chat command
|
|
12
|
+
* providers. Providers provide a list of commands given a user's partial input,
|
|
13
|
+
* and define how commands are handled when accepted in the chat commands menu.
|
|
14
|
+
*/
|
|
15
|
+
export interface IChatCommandRegistry {
|
|
16
|
+
/**
|
|
17
|
+
* Adds a chat command provider to the registry.
|
|
18
|
+
*/
|
|
19
|
+
addProvider(provider: IChatCommandProvider): void;
|
|
20
|
+
/**
|
|
21
|
+
* Lists all chat command providers previously added via `addProvider()`.
|
|
22
|
+
*/
|
|
23
|
+
getProviders(): IChatCommandProvider[];
|
|
24
|
+
/**
|
|
25
|
+
* Calls `onSubmit()` on each command provider in serial. Each command
|
|
26
|
+
* provider's `onSubmit()` method is responsible for checking the entire input
|
|
27
|
+
* for command calls and handling them accordingly.
|
|
28
|
+
*
|
|
29
|
+
* This method is called by the application after the user presses the "Send"
|
|
30
|
+
* button but before the message is sent to server.
|
|
31
|
+
*/
|
|
32
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export type ChatCommand = {
|
|
35
|
+
/**
|
|
36
|
+
* The name of the command. This defines what the user should type in the
|
|
37
|
+
* input to have the command appear in the chat commands menu.
|
|
38
|
+
*/
|
|
39
|
+
name: string;
|
|
40
|
+
/**
|
|
41
|
+
* ID of the provider the command originated from.
|
|
42
|
+
*/
|
|
43
|
+
providerId: string;
|
|
44
|
+
/**
|
|
45
|
+
* If set, this will be rendered as the icon for the command in the chat
|
|
46
|
+
* commands menu. Jupyter Chat will choose a default if this is unset.
|
|
47
|
+
*/
|
|
48
|
+
icon?: LabIcon | JSX.Element | string | null;
|
|
49
|
+
/**
|
|
50
|
+
* If set, this will be rendered as the description for the command in the
|
|
51
|
+
* chat commands menu. Jupyter Chat will choose a default if this is unset.
|
|
52
|
+
*/
|
|
53
|
+
description?: string;
|
|
54
|
+
/**
|
|
55
|
+
* If set, Jupyter Chat will replace the current word with this string immediately when
|
|
56
|
+
* the command is accepted from the chat commands menu or the current word
|
|
57
|
+
* matches the command's `name` exactly.
|
|
58
|
+
*
|
|
59
|
+
* This is generally used by "shortcut command" providers, e.g. the emoji
|
|
60
|
+
* command provider.
|
|
61
|
+
*/
|
|
62
|
+
replaceWith?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Specifies whether the application should add a space ' ' after the command
|
|
65
|
+
* is accepted from the menu. This should be set to `true` if the command that
|
|
66
|
+
* replaces the current word needs to be handled on submit, and the command is
|
|
67
|
+
* valid on its own.
|
|
68
|
+
*
|
|
69
|
+
* Defaults to `false`.
|
|
70
|
+
*/
|
|
71
|
+
spaceOnAccept?: boolean;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Interface of a command provider.
|
|
75
|
+
*/
|
|
76
|
+
export interface IChatCommandProvider {
|
|
77
|
+
/**
|
|
78
|
+
* ID of this command provider.
|
|
79
|
+
*/
|
|
80
|
+
id: string;
|
|
81
|
+
/**
|
|
82
|
+
* A method that should return the list of valid chat commands whose names
|
|
83
|
+
* complete the current word.
|
|
84
|
+
*
|
|
85
|
+
* The current word should be accessed from `inputModel.currentWord`.
|
|
86
|
+
*/
|
|
87
|
+
listCommandCompletions(inputModel: IInputModel): Promise<ChatCommand[]>;
|
|
88
|
+
/**
|
|
89
|
+
* A method that should identify and handle *all* command calls within a
|
|
90
|
+
* message that the user intends to submit. This method is called after a user
|
|
91
|
+
* presses the "Send" button but before the message is sent to the server.
|
|
92
|
+
*
|
|
93
|
+
* The entire message should be read from `inputModel.value`. This method may
|
|
94
|
+
* modify the new message before submission by setting `inputModel.value` or
|
|
95
|
+
* by calling other methods available on `inputModel`.
|
|
96
|
+
*/
|
|
97
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default chat command registry implementation.
|
|
101
|
+
*/
|
|
102
|
+
export declare class ChatCommandRegistry implements IChatCommandRegistry {
|
|
103
|
+
constructor();
|
|
104
|
+
addProvider(provider: IChatCommandProvider): void;
|
|
105
|
+
getProviders(): IChatCommandProvider[];
|
|
106
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
107
|
+
private _providers;
|
|
108
|
+
}
|