@jupyter/chat 0.1.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.
Files changed (66) hide show
  1. package/lib/__tests__/model.spec.d.ts +1 -0
  2. package/lib/__tests__/model.spec.js +72 -0
  3. package/lib/__tests__/widgets.spec.d.ts +1 -0
  4. package/lib/__tests__/widgets.spec.js +33 -0
  5. package/lib/components/chat-input.d.ts +33 -0
  6. package/lib/components/chat-input.js +60 -0
  7. package/lib/components/chat-messages.d.ts +32 -0
  8. package/lib/components/chat-messages.js +162 -0
  9. package/lib/components/chat.d.ts +43 -0
  10. package/lib/components/chat.js +100 -0
  11. package/lib/components/copy-button.d.ts +6 -0
  12. package/lib/components/copy-button.js +35 -0
  13. package/lib/components/jl-theme-provider.d.ts +6 -0
  14. package/lib/components/jl-theme-provider.js +19 -0
  15. package/lib/components/mui-extras/stacking-alert.d.ts +28 -0
  16. package/lib/components/mui-extras/stacking-alert.js +56 -0
  17. package/lib/components/rendermime-markdown.d.ts +12 -0
  18. package/lib/components/rendermime-markdown.js +54 -0
  19. package/lib/components/scroll-container.d.ts +23 -0
  20. package/lib/components/scroll-container.js +51 -0
  21. package/lib/components/toolbar.d.ts +11 -0
  22. package/lib/components/toolbar.js +30 -0
  23. package/lib/icons.d.ts +2 -0
  24. package/lib/icons.js +11 -0
  25. package/lib/index.d.ts +6 -0
  26. package/lib/index.js +10 -0
  27. package/lib/model.d.ts +177 -0
  28. package/lib/model.js +128 -0
  29. package/lib/theme-provider.d.ts +3 -0
  30. package/lib/theme-provider.js +133 -0
  31. package/lib/types.d.ts +49 -0
  32. package/lib/types.js +5 -0
  33. package/lib/widgets/chat-error.d.ts +2 -0
  34. package/lib/widgets/chat-error.js +26 -0
  35. package/lib/widgets/chat-sidebar.d.ts +4 -0
  36. package/lib/widgets/chat-sidebar.js +15 -0
  37. package/lib/widgets/chat-widget.d.ts +19 -0
  38. package/lib/widgets/chat-widget.js +28 -0
  39. package/package.json +209 -0
  40. package/src/__tests__/model.spec.ts +84 -0
  41. package/src/__tests__/widgets.spec.ts +43 -0
  42. package/src/components/chat-input.tsx +143 -0
  43. package/src/components/chat-messages.tsx +283 -0
  44. package/src/components/chat.tsx +179 -0
  45. package/src/components/copy-button.tsx +55 -0
  46. package/src/components/jl-theme-provider.tsx +28 -0
  47. package/src/components/mui-extras/stacking-alert.tsx +105 -0
  48. package/src/components/rendermime-markdown.tsx +88 -0
  49. package/src/components/scroll-container.tsx +74 -0
  50. package/src/components/toolbar.tsx +50 -0
  51. package/src/icons.ts +15 -0
  52. package/src/index.ts +11 -0
  53. package/src/model.ts +272 -0
  54. package/src/theme-provider.ts +137 -0
  55. package/src/types/mui.d.ts +18 -0
  56. package/src/types/svg.d.ts +17 -0
  57. package/src/types.ts +58 -0
  58. package/src/widgets/chat-error.tsx +43 -0
  59. package/src/widgets/chat-sidebar.tsx +30 -0
  60. package/src/widgets/chat-widget.tsx +51 -0
  61. package/style/base.css +13 -0
  62. package/style/chat-settings.css +10 -0
  63. package/style/chat.css +53 -0
  64. package/style/icons/chat.svg +6 -0
  65. package/style/index.css +6 -0
  66. package/style/index.js +6 -0
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useState, useMemo, useCallback } from 'react';
7
+ import { Alert, AlertColor, Collapse } from '@mui/material';
8
+
9
+ export type StackingAlert = {
10
+ /**
11
+ * A function that triggers an alert. Successive alerts are indicated in the
12
+ * JSX element.
13
+ * @param alertType Type of alert.
14
+ * @param msg Message contained within the alert.
15
+ * @returns
16
+ */
17
+ show: (alertType: AlertColor, msg: string | Error) => void;
18
+ /**
19
+ * The Alert JSX element that should be rendered by the consumer.
20
+ * This will be `null` if no alerts were triggered.
21
+ */
22
+ jsx: JSX.Element | null;
23
+ /**
24
+ * An async function that closes the alert, and returns a Promise that
25
+ * resolves when the onClose animation is completed.
26
+ */
27
+ clear: () => void | Promise<void>;
28
+ };
29
+
30
+ /**
31
+ * Hook that returns a function to trigger an alert, and a corresponding alert
32
+ * JSX element for the consumer to render. The number of successive identical
33
+ * alerts `X` is indicated in the element via the suffix "(X)".
34
+ */
35
+ export function useStackingAlert(): StackingAlert {
36
+ const [type, setType] = useState<AlertColor | null>(null);
37
+ const [msg, setMsg] = useState<string>('');
38
+ const [repeatCount, setRepeatCount] = useState(0);
39
+ const [expand, setExpand] = useState(false);
40
+ const [exitPromise, setExitPromise] = useState<Promise<void>>();
41
+ const [exitPromiseResolver, setExitPromiseResolver] = useState<() => void>();
42
+
43
+ const showAlert = useCallback(
44
+ (nextType: AlertColor, _nextMsg: string | Error) => {
45
+ // if the alert is identical to the previous alert, increment the
46
+ // `repeatCount` indicator.
47
+ const nextMsg = _nextMsg.toString();
48
+ if (nextType === type && nextMsg === msg) {
49
+ setRepeatCount(currCount => currCount + 1);
50
+ return;
51
+ }
52
+
53
+ if (type === null) {
54
+ // if this alert is being shown for the first time, initialize the
55
+ // exitPromise so we can await it on `clear()`.
56
+ setExitPromise(
57
+ new Promise(res => {
58
+ setExitPromiseResolver(() => res);
59
+ })
60
+ );
61
+ }
62
+
63
+ setType(nextType);
64
+ setMsg(nextMsg);
65
+ setRepeatCount(0);
66
+ setExpand(true);
67
+ },
68
+ [msg, type]
69
+ );
70
+
71
+ const alertJsx = useMemo(
72
+ () => (
73
+ <Collapse
74
+ in={expand}
75
+ onExited={() => {
76
+ exitPromiseResolver?.();
77
+ // only clear the alert after the Collapse exits, otherwise the alert
78
+ // disappears without any animation.
79
+ setType(null);
80
+ setMsg('');
81
+ setRepeatCount(0);
82
+ }}
83
+ timeout={200}
84
+ >
85
+ {type !== null && (
86
+ <Alert severity={type}>
87
+ {msg + (repeatCount ? ` (${repeatCount})` : '')}
88
+ </Alert>
89
+ )}
90
+ </Collapse>
91
+ ),
92
+ [msg, repeatCount, type, expand, exitPromiseResolver]
93
+ );
94
+
95
+ const clearAlert = useCallback(() => {
96
+ setExpand(false);
97
+ return exitPromise;
98
+ }, [expand, exitPromise]);
99
+
100
+ return {
101
+ show: showAlert,
102
+ jsx: alertJsx,
103
+ clear: clearAlert
104
+ };
105
+ }
@@ -0,0 +1,88 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
+ import React, { useState, useEffect, useRef } from 'react';
8
+ import ReactDOM from 'react-dom';
9
+
10
+ import { CopyButton } from './copy-button';
11
+ import { MessageToolbar } from './toolbar';
12
+
13
+ const MD_MIME_TYPE = 'text/markdown';
14
+ const RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
15
+
16
+ type RendermimeMarkdownProps = {
17
+ markdownStr: string;
18
+ rmRegistry: IRenderMimeRegistry;
19
+ appendContent?: boolean;
20
+ edit?: () => void;
21
+ delete?: () => void;
22
+ };
23
+
24
+ /**
25
+ * Takes \( and returns \\(. Escapes LaTeX delimeters by adding extra backslashes where needed for proper rendering by @jupyterlab/rendermime.
26
+ */
27
+ function escapeLatexDelimiters(text: string) {
28
+ return text
29
+ .replace('\\(', '\\\\(')
30
+ .replace('\\)', '\\\\)')
31
+ .replace('\\[', '\\\\[')
32
+ .replace('\\]', '\\\\]');
33
+ }
34
+
35
+ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
36
+ const appendContent = props.appendContent || false;
37
+ const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
38
+ null
39
+ );
40
+ const containerRef = useRef<HTMLDivElement>(null);
41
+
42
+ useEffect(() => {
43
+ const renderContent = async () => {
44
+ const mdStr = escapeLatexDelimiters(props.markdownStr);
45
+ const model = props.rmRegistry.createModel({
46
+ data: { [MD_MIME_TYPE]: mdStr }
47
+ });
48
+
49
+ const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
50
+ await renderer.renderModel(model);
51
+ props.rmRegistry.latexTypesetter?.typeset(renderer.node);
52
+
53
+ // Attach CopyButton to each <pre> block
54
+ if (containerRef.current && renderer.node) {
55
+ const preBlocks = renderer.node.querySelectorAll('pre');
56
+ preBlocks.forEach(preBlock => {
57
+ const copyButtonContainer = document.createElement('div');
58
+ preBlock.parentNode?.insertBefore(
59
+ copyButtonContainer,
60
+ preBlock.nextSibling
61
+ );
62
+ ReactDOM.render(
63
+ <CopyButton value={preBlock.textContent || ''} />,
64
+ copyButtonContainer
65
+ );
66
+ });
67
+ }
68
+
69
+ setRenderedContent(renderer.node);
70
+ };
71
+
72
+ renderContent();
73
+ }, [props.markdownStr, props.rmRegistry]);
74
+
75
+ return (
76
+ <div ref={containerRef} className={RENDERMIME_MD_CLASS}>
77
+ {renderedContent &&
78
+ (appendContent ? (
79
+ <div ref={node => node && node.appendChild(renderedContent)} />
80
+ ) : (
81
+ <div ref={node => node && node.replaceChildren(renderedContent)} />
82
+ ))}
83
+ <MessageToolbar edit={props.edit} delete={props.delete} />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ export const RendermimeMarkdown = React.memo(RendermimeMarkdownBase);
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useEffect, useMemo } from 'react';
7
+ import { Box, SxProps, Theme } from '@mui/material';
8
+
9
+ type ScrollContainerProps = {
10
+ children: React.ReactNode;
11
+ sx?: SxProps<Theme>;
12
+ };
13
+
14
+ /**
15
+ * Component that handles intelligent scrolling.
16
+ *
17
+ * - If viewport is at the bottom of the overflow container, appending new
18
+ * children keeps the viewport on the bottom of the overflow container.
19
+ *
20
+ * - If viewport is in the middle of the overflow container, appending new
21
+ * children leaves the viewport unaffected.
22
+ *
23
+ * Currently only works for Chrome and Firefox due to reliance on
24
+ * `overflow-anchor`.
25
+ *
26
+ * **References**
27
+ * - https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
28
+ */
29
+ export function ScrollContainer(props: ScrollContainerProps): JSX.Element {
30
+ const id = useMemo(
31
+ () => 'jupyter-chat-scroll-container-' + Date.now().toString(),
32
+ []
33
+ );
34
+
35
+ /**
36
+ * Effect: Scroll the container to the bottom as soon as it is visible.
37
+ */
38
+ useEffect(() => {
39
+ const el = document.querySelector<HTMLElement>(`#${id}`);
40
+ if (!el) {
41
+ return;
42
+ }
43
+
44
+ const observer = new IntersectionObserver(
45
+ entries => {
46
+ entries.forEach(entry => {
47
+ if (entry.isIntersecting) {
48
+ el.scroll({ top: 999999999 });
49
+ }
50
+ });
51
+ },
52
+ { threshold: 1.0 }
53
+ );
54
+
55
+ observer.observe(el);
56
+ return () => observer.disconnect();
57
+ }, []);
58
+
59
+ return (
60
+ <Box
61
+ id={id}
62
+ sx={{
63
+ overflowY: 'scroll',
64
+ '& *': {
65
+ overflowAnchor: 'none'
66
+ },
67
+ ...props.sx
68
+ }}
69
+ >
70
+ <Box sx={{ minHeight: '100.01%' }}>{props.children}</Box>
71
+ <Box sx={{ overflowAnchor: 'auto', height: '1px' }} />
72
+ </Box>
73
+ );
74
+ }
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import {
7
+ ToolbarButtonComponent,
8
+ deleteIcon,
9
+ editIcon
10
+ } from '@jupyterlab/ui-components';
11
+ import React from 'react';
12
+
13
+ const TOOLBAR_CLASS = 'jp-chat-toolbar';
14
+
15
+ /**
16
+ * The toolbar attached to a message.
17
+ */
18
+ export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
19
+ const buttons: JSX.Element[] = [];
20
+
21
+ if (props.edit !== undefined) {
22
+ const editButton = ToolbarButtonComponent({
23
+ icon: editIcon,
24
+ onClick: props.edit,
25
+ tooltip: 'Edit'
26
+ });
27
+ buttons.push(editButton);
28
+ }
29
+ if (props.delete !== undefined) {
30
+ const deleteButton = ToolbarButtonComponent({
31
+ icon: deleteIcon,
32
+ onClick: props.delete,
33
+ tooltip: 'Delete'
34
+ });
35
+ buttons.push(deleteButton);
36
+ }
37
+
38
+ return (
39
+ <div className={TOOLBAR_CLASS}>
40
+ {buttons.map(toolbarButton => toolbarButton)}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export namespace MessageToolbar {
46
+ export interface IProps {
47
+ edit?: () => void;
48
+ delete?: () => void;
49
+ }
50
+ }
package/src/icons.ts ADDED
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ // This file is based on iconimports.ts in @jupyterlab/ui-components, but is manually generated.
7
+
8
+ import { LabIcon } from '@jupyterlab/ui-components';
9
+
10
+ import chatSvgStr from '../style/icons/chat.svg';
11
+
12
+ export const chatIcon = new LabIcon({
13
+ name: 'jupyter-chat::chat',
14
+ svgstr: chatSvgStr
15
+ });
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ export * from './icons';
7
+ export * from './model';
8
+ export * from './types';
9
+ export * from './widgets/chat-error';
10
+ export * from './widgets/chat-sidebar';
11
+ export * from './widgets/chat-widget';
package/src/model.ts ADDED
@@ -0,0 +1,272 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IDisposable } from '@lumino/disposable';
7
+ import { ISignal, Signal } from '@lumino/signaling';
8
+
9
+ import {
10
+ IChatHistory,
11
+ INewMessage,
12
+ IChatMessage,
13
+ IConfig,
14
+ IUser
15
+ } from './types';
16
+
17
+ /**
18
+ * The chat model interface.
19
+ */
20
+ export interface IChatModel extends IDisposable {
21
+ /**
22
+ * The chat model ID.
23
+ */
24
+ id: string;
25
+
26
+ /**
27
+ * The configuration for the chat panel.
28
+ */
29
+ config: IConfig;
30
+
31
+ /**
32
+ * The user connected to the chat panel.
33
+ */
34
+ readonly user?: IUser;
35
+
36
+ /**
37
+ * The chat messages list.
38
+ */
39
+ readonly messages: IChatMessage[];
40
+
41
+ /**
42
+ * The signal emitted when the messages list is updated.
43
+ */
44
+ readonly messagesUpdated: ISignal<IChatModel, void>;
45
+
46
+ /**
47
+ * Send a message, to be defined depending on the chosen technology.
48
+ * Default to no-op.
49
+ *
50
+ * @param message - the message to send.
51
+ * @returns whether the message has been sent or not, or nothing if not needed.
52
+ */
53
+ addMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
54
+
55
+ /**
56
+ * Optional, to update a message from the chat panel.
57
+ *
58
+ * @param id - the unique ID of the message.
59
+ * @param message - the updated message.
60
+ */
61
+ updateMessage?(
62
+ id: string,
63
+ message: IChatMessage
64
+ ): Promise<boolean | void> | boolean | void;
65
+
66
+ /**
67
+ * Optional, to delete a message from the chat.
68
+ *
69
+ * @param id - the unique ID of the message.
70
+ */
71
+ deleteMessage?(id: string): Promise<boolean | void> | boolean | void;
72
+
73
+ /**
74
+ * Optional, to get messages history.
75
+ */
76
+ getHistory?(): Promise<IChatHistory>;
77
+
78
+ /**
79
+ * Dispose the chat model.
80
+ */
81
+ dispose(): void;
82
+
83
+ /**
84
+ * Whether the chat handler is disposed.
85
+ */
86
+ isDisposed: boolean;
87
+
88
+ /**
89
+ * Function to call when a message is received.
90
+ *
91
+ * @param message - the message with user information and body.
92
+ */
93
+ messageAdded(message: IChatMessage): void;
94
+
95
+ /**
96
+ * Function called when messages are inserted.
97
+ *
98
+ * @param index - the index of the first message of the list.
99
+ * @param messages - the messages list.
100
+ */
101
+ messagesInserted(index: number, messages: IChatMessage[]): void;
102
+
103
+ /**
104
+ * Function called when messages are deleted.
105
+ *
106
+ * @param index - the index of the first message to delete.
107
+ * @param count - the number of messages to delete.
108
+ */
109
+ messagesDeleted(index: number, count: number): void;
110
+ }
111
+
112
+ /**
113
+ * The default chat model implementation.
114
+ * It is not able to send or update a message by itself, since it depends on the
115
+ * chosen technology.
116
+ */
117
+ export class ChatModel implements IChatModel {
118
+ /**
119
+ * Create a new chat model.
120
+ */
121
+ constructor(options: ChatModel.IOptions = {}) {
122
+ this._config = options.config ?? {};
123
+ }
124
+
125
+ /**
126
+ * The chat messages list.
127
+ */
128
+ get messages(): IChatMessage[] {
129
+ return this._messages;
130
+ }
131
+
132
+ /**
133
+ * The chat model ID.
134
+ */
135
+ get id(): string {
136
+ return this._id;
137
+ }
138
+ set id(value: string) {
139
+ this._id = value;
140
+ }
141
+
142
+ /**
143
+ * The chat settings.
144
+ */
145
+ get config(): IConfig {
146
+ return this._config;
147
+ }
148
+ set config(value: Partial<IConfig>) {
149
+ this._config = { ...this._config, ...value };
150
+ }
151
+
152
+ /**
153
+ * The signal emitted when the messages list is updated.
154
+ */
155
+ get messagesUpdated(): ISignal<IChatModel, void> {
156
+ return this._messagesUpdated;
157
+ }
158
+
159
+ /**
160
+ * Send a message, to be defined depending on the chosen technology.
161
+ * Default to no-op.
162
+ *
163
+ * @param message - the message to send.
164
+ * @returns whether the message has been sent or not.
165
+ */
166
+ addMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
167
+
168
+ /**
169
+ * Optional, to update a message from the chat panel.
170
+ *
171
+ * @param id - the unique ID of the message.
172
+ * @param message - the message to update.
173
+ */
174
+ updateMessage?(
175
+ id: string,
176
+ message: INewMessage
177
+ ): Promise<boolean | void> | boolean | void;
178
+
179
+ /**
180
+ * Dispose the chat model.
181
+ */
182
+ dispose(): void {
183
+ if (this.isDisposed) {
184
+ return;
185
+ }
186
+ this._isDisposed = true;
187
+ }
188
+
189
+ /**
190
+ * Whether the chat handler is disposed.
191
+ */
192
+ get isDisposed(): boolean {
193
+ return this._isDisposed;
194
+ }
195
+
196
+ /**
197
+ * A function called before transferring the message to the panel(s).
198
+ * Can be useful if some actions are required on the message.
199
+ */
200
+ protected formatChatMessage(message: IChatMessage): IChatMessage {
201
+ return message;
202
+ }
203
+
204
+ /**
205
+ * Function to call when a message is received.
206
+ *
207
+ * @param message - the message with user information and body.
208
+ */
209
+ messageAdded(message: IChatMessage): void {
210
+ const messageIndex = this._messages.findIndex(msg => msg.id === message.id);
211
+ if (messageIndex > -1) {
212
+ // The message is an update of an existing one.
213
+ // Let's remove it to avoid position conflict if timestamp has changed.
214
+ this._messages.splice(messageIndex, 1);
215
+ }
216
+ // Find the first message that should be after this one.
217
+ let nextMsgIndex = this._messages.findIndex(msg => msg.time > message.time);
218
+ if (nextMsgIndex === -1) {
219
+ // There is no message after this one, so let's insert the message at the end.
220
+ nextMsgIndex = this._messages.length;
221
+ }
222
+ // Insert the message.
223
+ this.messagesInserted(nextMsgIndex, [message]);
224
+ }
225
+
226
+ /**
227
+ * Function called when messages are inserted.
228
+ *
229
+ * @param index - the index of the first message of the list.
230
+ * @param messages - the messages list.
231
+ */
232
+ messagesInserted(index: number, messages: IChatMessage[]): void {
233
+ const formattedMessages: IChatMessage[] = [];
234
+ messages.forEach(message => {
235
+ formattedMessages.push(this.formatChatMessage(message));
236
+ });
237
+ this._messages.splice(index, 0, ...formattedMessages);
238
+ this._messagesUpdated.emit();
239
+ }
240
+
241
+ /**
242
+ * Function called when messages are deleted.
243
+ *
244
+ * @param index - the index of the first message to delete.
245
+ * @param count - the number of messages to delete.
246
+ */
247
+ messagesDeleted(index: number, count: number): void {
248
+ this._messages.splice(index, count);
249
+ this._messagesUpdated.emit();
250
+ }
251
+
252
+ private _messages: IChatMessage[] = [];
253
+ private _id: string = '';
254
+ private _config: IConfig;
255
+ private _isDisposed = false;
256
+ private _messagesUpdated = new Signal<IChatModel, void>(this);
257
+ }
258
+
259
+ /**
260
+ * The chat model namespace.
261
+ */
262
+ export namespace ChatModel {
263
+ /**
264
+ * The instantiation options for a ChatModel.
265
+ */
266
+ export interface IOptions {
267
+ /**
268
+ * Initial config for the chat widget.
269
+ */
270
+ config?: IConfig;
271
+ }
272
+ }