@jupyter/chat 0.6.1 → 0.7.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/components/chat-input.js +12 -6
- package/lib/components/chat-messages.d.ts +10 -9
- package/lib/components/chat-messages.js +77 -63
- package/lib/components/chat.js +5 -2
- package/lib/components/code-blocks/index.d.ts +2 -0
- package/lib/components/code-blocks/index.js +6 -0
- package/lib/components/index.d.ts +10 -0
- package/lib/components/index.js +14 -0
- package/lib/components/input/cancel-button.d.ts +0 -1
- package/lib/components/input/cancel-button.js +1 -2
- package/lib/components/input/index.d.ts +2 -0
- package/lib/components/input/index.js +6 -0
- package/lib/components/markdown-renderer.d.ts +37 -0
- package/lib/components/{rendermime-markdown.js → markdown-renderer.js} +10 -8
- package/lib/components/mui-extras/index.d.ts +3 -0
- package/lib/components/mui-extras/index.js +7 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/model.d.ts +4 -0
- package/lib/model.js +3 -0
- package/lib/types.d.ts +0 -4
- package/package.json +2 -1
- package/src/components/chat-input.tsx +14 -11
- package/src/components/chat-messages.tsx +151 -129
- package/src/components/chat.tsx +3 -0
- package/src/components/code-blocks/index.ts +7 -0
- package/src/components/index.ts +15 -0
- package/src/components/input/cancel-button.tsx +0 -3
- package/src/components/input/index.ts +7 -0
- package/src/components/{rendermime-markdown.tsx → markdown-renderer.tsx} +36 -10
- package/src/components/mui-extras/index.ts +8 -0
- package/src/index.ts +1 -0
- package/src/model.ts +9 -0
- package/src/types.ts +0 -4
- package/style/chat.css +14 -6
- package/lib/components/mui-extras/stacking-alert.d.ts +0 -28
- package/lib/components/mui-extras/stacking-alert.js +0 -56
- package/lib/components/rendermime-markdown.d.ts +0 -14
- package/src/components/mui-extras/stacking-alert.tsx +0 -105
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
caretDownEmptyIcon,
|
|
11
11
|
classes
|
|
12
12
|
} from '@jupyterlab/ui-components';
|
|
13
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
13
14
|
import { Avatar as MuiAvatar, Box, Typography } from '@mui/material';
|
|
14
15
|
import type { SxProps, Theme } from '@mui/material';
|
|
15
16
|
import clsx from 'clsx';
|
|
16
|
-
import React, { useEffect, useState, useRef } from 'react';
|
|
17
|
+
import React, { useEffect, useState, useRef, forwardRef } from 'react';
|
|
17
18
|
|
|
18
19
|
import { ChatInput } from './chat-input';
|
|
19
|
-
import {
|
|
20
|
+
import { MarkdownRenderer } from './markdown-renderer';
|
|
20
21
|
import { ScrollContainer } from './scroll-container';
|
|
21
22
|
import { IChatModel } from '../model';
|
|
22
23
|
import { IChatMessage, IUser } from '../types';
|
|
@@ -47,13 +48,12 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
47
48
|
const { model } = props;
|
|
48
49
|
const [messages, setMessages] = useState<IChatMessage[]>(model.messages);
|
|
49
50
|
const refMsgBox = useRef<HTMLDivElement>(null);
|
|
50
|
-
const inViewport = useRef<number[]>([]);
|
|
51
51
|
const [currentWriters, setCurrentWriters] = useState<IUser[]>([]);
|
|
52
|
+
const [allRendered, setAllRendered] = useState<boolean>(false);
|
|
52
53
|
|
|
53
|
-
// The
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
);
|
|
54
|
+
// The list of message DOM and their rendered promises.
|
|
55
|
+
const listRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
56
|
+
const renderedPromise = useRef<PromiseDelegate<void>[]>([]);
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Effect: fetch history and config on initial render
|
|
@@ -95,45 +95,73 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
95
95
|
}, [model]);
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* Observe the messages to update the current viewport and the unread messages.
|
|
99
99
|
*/
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const observer = new IntersectionObserver(entries => {
|
|
102
|
+
// Used on first rendering, to ensure all the message as been rendered once.
|
|
103
|
+
if (!allRendered) {
|
|
104
|
+
Promise.all(renderedPromise.current.map(p => p.promise)).then(() => {
|
|
105
|
+
setAllRendered(true);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const unread = [...model.unreadMessages];
|
|
110
|
+
let unreadModified = false;
|
|
111
|
+
const inViewport = [...(model.messagesInViewport ?? [])];
|
|
112
|
+
entries.forEach(entry => {
|
|
113
|
+
const index = parseInt(entry.target.getAttribute('data-index') ?? '');
|
|
114
|
+
if (!isNaN(index)) {
|
|
115
|
+
const viewportIdx = inViewport.indexOf(index);
|
|
116
|
+
if (!entry.isIntersecting && viewportIdx !== -1) {
|
|
117
|
+
inViewport.splice(viewportIdx, 1);
|
|
118
|
+
} else if (entry.isIntersecting && viewportIdx === -1) {
|
|
119
|
+
inViewport.push(index);
|
|
120
|
+
}
|
|
121
|
+
if (unread.length) {
|
|
122
|
+
const unreadIdx = unread.indexOf(index);
|
|
123
|
+
if (unreadIdx !== -1 && entry.isIntersecting) {
|
|
124
|
+
unread.splice(unreadIdx, 1);
|
|
125
|
+
unreadModified = true;
|
|
126
|
+
}
|
|
111
127
|
}
|
|
112
128
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
props.model.messagesInViewport = inViewport;
|
|
132
|
+
|
|
133
|
+
// Ensure that all messages are rendered before updating unread messages, otherwise
|
|
134
|
+
// it can lead to wrong assumption , because more message are in the viewport
|
|
135
|
+
// before they are rendered.
|
|
136
|
+
if (allRendered && unreadModified) {
|
|
137
|
+
model.unreadMessages = unread;
|
|
119
138
|
}
|
|
120
139
|
});
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Observe the messages.
|
|
143
|
+
*/
|
|
144
|
+
listRef.current.forEach(item => {
|
|
145
|
+
if (item) {
|
|
146
|
+
observer.observe(item);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
126
149
|
|
|
127
150
|
return () => {
|
|
128
|
-
|
|
151
|
+
listRef.current.forEach(item => {
|
|
152
|
+
if (item) {
|
|
153
|
+
observer.unobserve(item);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
129
156
|
};
|
|
130
|
-
}
|
|
157
|
+
}, [messages, allRendered]);
|
|
131
158
|
|
|
132
159
|
return (
|
|
133
160
|
<>
|
|
134
161
|
<ScrollContainer sx={{ flexGrow: 1 }}>
|
|
135
162
|
<Box ref={refMsgBox} className={clsx(MESSAGES_BOX_CLASS)}>
|
|
136
163
|
{messages.map((message, i) => {
|
|
164
|
+
renderedPromise.current[i] = new PromiseDelegate();
|
|
137
165
|
return (
|
|
138
166
|
// extra div needed to ensure each bubble is on a new line
|
|
139
167
|
<Box
|
|
@@ -147,8 +175,9 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
147
175
|
<ChatMessage
|
|
148
176
|
{...props}
|
|
149
177
|
message={message}
|
|
150
|
-
observer={observerRef.current}
|
|
151
178
|
index={i}
|
|
179
|
+
renderedPromise={renderedPromise.current[i]}
|
|
180
|
+
ref={el => (listRef.current[i] = el)}
|
|
152
181
|
/>
|
|
153
182
|
</Box>
|
|
154
183
|
);
|
|
@@ -156,7 +185,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
156
185
|
</Box>
|
|
157
186
|
<Writers writers={currentWriters}></Writers>
|
|
158
187
|
</ScrollContainer>
|
|
159
|
-
<Navigation {...props} refMsgBox={refMsgBox} />
|
|
188
|
+
<Navigation {...props} refMsgBox={refMsgBox} allRendered={allRendered} />
|
|
160
189
|
</>
|
|
161
190
|
);
|
|
162
191
|
}
|
|
@@ -293,102 +322,88 @@ type ChatMessageProps = BaseMessageProps & {
|
|
|
293
322
|
*/
|
|
294
323
|
index: number;
|
|
295
324
|
/**
|
|
296
|
-
* The
|
|
325
|
+
* The promise to resolve when the message is rendered.
|
|
297
326
|
*/
|
|
298
|
-
|
|
327
|
+
renderedPromise: PromiseDelegate<void>;
|
|
299
328
|
};
|
|
300
329
|
|
|
301
330
|
/**
|
|
302
331
|
* The message component body.
|
|
303
332
|
*/
|
|
304
|
-
export
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
333
|
+
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
334
|
+
(props, ref): JSX.Element => {
|
|
335
|
+
const { message, model, rmRegistry } = props;
|
|
336
|
+
const [edit, setEdit] = useState<boolean>(false);
|
|
337
|
+
const [deleted, setDeleted] = useState<boolean>(false);
|
|
338
|
+
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
339
|
+
const [canDelete, setCanDelete] = useState<boolean>(false);
|
|
340
|
+
|
|
341
|
+
// Look if the message can be deleted or edited.
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
setDeleted(message.deleted ?? false);
|
|
344
|
+
if (model.user !== undefined && !message.deleted) {
|
|
345
|
+
if (model.user.username === message.sender.username) {
|
|
346
|
+
setCanEdit(model.updateMessage !== undefined);
|
|
347
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
setCanEdit(false);
|
|
351
|
+
setCanDelete(false);
|
|
352
|
+
}
|
|
353
|
+
}, [model, message]);
|
|
317
354
|
|
|
318
|
-
//
|
|
319
|
-
|
|
355
|
+
// Cancel the current edition of the message.
|
|
356
|
+
const cancelEdition = (): void => {
|
|
357
|
+
setEdit(false);
|
|
358
|
+
};
|
|
320
359
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
360
|
+
// Update the content of the message.
|
|
361
|
+
const updateMessage = (id: string, input: string): void => {
|
|
362
|
+
if (!canEdit) {
|
|
363
|
+
return;
|
|
324
364
|
}
|
|
365
|
+
// Update the message
|
|
366
|
+
const updatedMessage = { ...message };
|
|
367
|
+
updatedMessage.body = input;
|
|
368
|
+
model.updateMessage!(id, updatedMessage);
|
|
369
|
+
setEdit(false);
|
|
325
370
|
};
|
|
326
|
-
}, [model]);
|
|
327
371
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (model.user.username === message.sender.username) {
|
|
333
|
-
setCanEdit(model.updateMessage !== undefined);
|
|
334
|
-
setCanDelete(model.deleteMessage !== undefined);
|
|
372
|
+
// Delete the message.
|
|
373
|
+
const deleteMessage = (id: string): void => {
|
|
374
|
+
if (!canDelete) {
|
|
375
|
+
return;
|
|
335
376
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
setCanDelete(false);
|
|
339
|
-
}
|
|
340
|
-
}, [model, message]);
|
|
341
|
-
|
|
342
|
-
// Cancel the current edition of the message.
|
|
343
|
-
const cancelEdition = (): void => {
|
|
344
|
-
setEdit(false);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// Update the content of the message.
|
|
348
|
-
const updateMessage = (id: string, input: string): void => {
|
|
349
|
-
if (!canEdit) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
// Update the message
|
|
353
|
-
const updatedMessage = { ...message };
|
|
354
|
-
updatedMessage.body = input;
|
|
355
|
-
model.updateMessage!(id, updatedMessage);
|
|
356
|
-
setEdit(false);
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
// Delete the message.
|
|
360
|
-
const deleteMessage = (id: string): void => {
|
|
361
|
-
if (!canDelete) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
model.deleteMessage!(id);
|
|
365
|
-
};
|
|
377
|
+
model.deleteMessage!(id);
|
|
378
|
+
};
|
|
366
379
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
380
|
+
// Empty if the message has been deleted.
|
|
381
|
+
return deleted ? (
|
|
382
|
+
<div ref={ref} data-index={props.index}></div>
|
|
383
|
+
) : (
|
|
384
|
+
<div ref={ref} data-index={props.index}>
|
|
385
|
+
{edit && canEdit ? (
|
|
386
|
+
<ChatInput
|
|
387
|
+
value={message.body}
|
|
388
|
+
onSend={(input: string) => updateMessage(message.id, input)}
|
|
389
|
+
onCancel={() => cancelEdition()}
|
|
390
|
+
model={model}
|
|
391
|
+
hideIncludeSelection={true}
|
|
392
|
+
/>
|
|
393
|
+
) : (
|
|
394
|
+
<MarkdownRenderer
|
|
395
|
+
rmRegistry={rmRegistry}
|
|
396
|
+
markdownStr={message.body}
|
|
397
|
+
model={model}
|
|
398
|
+
edit={canEdit ? () => setEdit(true) : undefined}
|
|
399
|
+
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
400
|
+
rendered={props.renderedPromise}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
);
|
|
392
407
|
|
|
393
408
|
/**
|
|
394
409
|
* The writers component props.
|
|
@@ -437,6 +452,10 @@ type NavigationProps = BaseMessageProps & {
|
|
|
437
452
|
* The reference to the messages container.
|
|
438
453
|
*/
|
|
439
454
|
refMsgBox: React.RefObject<HTMLDivElement>;
|
|
455
|
+
/**
|
|
456
|
+
* Whether all the messages has been rendered once on first display.
|
|
457
|
+
*/
|
|
458
|
+
allRendered: boolean;
|
|
440
459
|
};
|
|
441
460
|
|
|
442
461
|
/**
|
|
@@ -448,13 +467,20 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
448
467
|
const [unreadBefore, setUnreadBefore] = useState<number | null>(null);
|
|
449
468
|
const [unreadAfter, setUnreadAfter] = useState<number | null>(null);
|
|
450
469
|
|
|
451
|
-
const gotoMessage = (msgIdx: number) => {
|
|
452
|
-
props.refMsgBox.current?.children.item(msgIdx)?.scrollIntoView();
|
|
470
|
+
const gotoMessage = (msgIdx: number, alignToTop: boolean = true) => {
|
|
471
|
+
props.refMsgBox.current?.children.item(msgIdx)?.scrollIntoView(alignToTop);
|
|
453
472
|
};
|
|
454
473
|
|
|
455
474
|
// Listen for change in unread messages, and find the first unread message before or
|
|
456
475
|
// after the current viewport, to display navigation buttons.
|
|
457
476
|
useEffect(() => {
|
|
477
|
+
// Do not attempt to display navigation until messages are rendered, it can lead to
|
|
478
|
+
// wrong assumption, because more messages are in the viewport before they are
|
|
479
|
+
// rendered.
|
|
480
|
+
if (!props.allRendered) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
458
484
|
const unreadChanged = (model: IChatModel, unreadIndexes: number[]) => {
|
|
459
485
|
const viewport = model.messagesInViewport;
|
|
460
486
|
if (!viewport) {
|
|
@@ -498,17 +524,13 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
498
524
|
|
|
499
525
|
unreadChanged(model, model.unreadMessages);
|
|
500
526
|
|
|
501
|
-
// Move to
|
|
502
|
-
|
|
503
|
-
gotoMessage(Math.min(...model.unreadMessages));
|
|
504
|
-
} else {
|
|
505
|
-
gotoMessage(model.messages.length - 1);
|
|
506
|
-
}
|
|
527
|
+
// Move to the last the message after all the messages have been first rendered.
|
|
528
|
+
gotoMessage(model.messages.length - 1, false);
|
|
507
529
|
|
|
508
530
|
return () => {
|
|
509
531
|
model.unreadChanged?.disconnect(unreadChanged);
|
|
510
532
|
};
|
|
511
|
-
}, [model]);
|
|
533
|
+
}, [model, props.allRendered]);
|
|
512
534
|
|
|
513
535
|
// Listen for change in the viewport, to add a navigation button if the last is not
|
|
514
536
|
// in viewport.
|
|
@@ -544,10 +566,10 @@ export function Navigation(props: NavigationProps): JSX.Element {
|
|
|
544
566
|
{(unreadAfter !== null || !lastInViewport) && (
|
|
545
567
|
<Button
|
|
546
568
|
className={`${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`}
|
|
547
|
-
onClick={
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
569
|
+
onClick={
|
|
570
|
+
unreadAfter === null
|
|
571
|
+
? () => gotoMessage(model.messages.length - 1, false)
|
|
572
|
+
: () => gotoMessage(unreadAfter)
|
|
551
573
|
}
|
|
552
574
|
title={
|
|
553
575
|
unreadAfter !== null
|
package/src/components/chat.tsx
CHANGED
|
@@ -54,6 +54,9 @@ export function Chat(props: Chat.IOptions): JSX.Element {
|
|
|
54
54
|
return (
|
|
55
55
|
<JlThemeProvider themeManager={props.themeManager ?? null}>
|
|
56
56
|
<Box
|
|
57
|
+
// Add .jp-ThemedContainer for CSS compatibility in both JL <4.3.0 and >=4.3.0.
|
|
58
|
+
// See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
|
|
59
|
+
className="jp-ThemedContainer"
|
|
57
60
|
// root box should not include padding as it offsets the vertical
|
|
58
61
|
// scrollbar to the left
|
|
59
62
|
sx={{
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './chat';
|
|
7
|
+
export * from './chat-input';
|
|
8
|
+
export * from './chat-messages';
|
|
9
|
+
export * from './code-blocks';
|
|
10
|
+
export * from './input';
|
|
11
|
+
export * from './jl-theme-provider';
|
|
12
|
+
export * from './markdown-renderer';
|
|
13
|
+
export * from './mui-extras';
|
|
14
|
+
export * from './scroll-container';
|
|
15
|
+
export * from './toolbar';
|
|
@@ -13,7 +13,6 @@ const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
|
13
13
|
* The cancel button props.
|
|
14
14
|
*/
|
|
15
15
|
export type CancelButtonProps = {
|
|
16
|
-
inputExists: boolean;
|
|
17
16
|
onCancel: () => void;
|
|
18
17
|
};
|
|
19
18
|
|
|
@@ -22,11 +21,9 @@ export type CancelButtonProps = {
|
|
|
22
21
|
*/
|
|
23
22
|
export function CancelButton(props: CancelButtonProps): JSX.Element {
|
|
24
23
|
const tooltip = 'Cancel edition';
|
|
25
|
-
const disabled = !props.inputExists;
|
|
26
24
|
return (
|
|
27
25
|
<TooltippedButton
|
|
28
26
|
onClick={props.onCancel}
|
|
29
|
-
disabled={disabled}
|
|
30
27
|
tooltip={tooltip}
|
|
31
28
|
buttonProps={{
|
|
32
29
|
size: 'small',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
8
|
import React, { useState, useEffect } from 'react';
|
|
8
9
|
import { createPortal } from 'react-dom';
|
|
9
10
|
|
|
@@ -12,14 +13,36 @@ import { MessageToolbar } from './toolbar';
|
|
|
12
13
|
import { IChatModel } from '../model';
|
|
13
14
|
|
|
14
15
|
const MD_MIME_TYPE = 'text/markdown';
|
|
15
|
-
const
|
|
16
|
+
const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
|
|
16
17
|
|
|
17
|
-
type
|
|
18
|
+
type MarkdownRendererProps = {
|
|
19
|
+
/**
|
|
20
|
+
* The string to render.
|
|
21
|
+
*/
|
|
18
22
|
markdownStr: string;
|
|
23
|
+
/**
|
|
24
|
+
* The rendermime registry.
|
|
25
|
+
*/
|
|
19
26
|
rmRegistry: IRenderMimeRegistry;
|
|
20
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The model of the chat.
|
|
29
|
+
*/
|
|
21
30
|
model: IChatModel;
|
|
31
|
+
/**
|
|
32
|
+
* The promise to resolve when the message is rendered.
|
|
33
|
+
*/
|
|
34
|
+
rendered: PromiseDelegate<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to append the content to the existing content or not.
|
|
37
|
+
*/
|
|
38
|
+
appendContent?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The function to call to edit a message.
|
|
41
|
+
*/
|
|
22
42
|
edit?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* the function to call to delete a message.
|
|
45
|
+
*/
|
|
23
46
|
delete?: () => void;
|
|
24
47
|
};
|
|
25
48
|
|
|
@@ -33,13 +56,13 @@ type RendermimeMarkdownProps = {
|
|
|
33
56
|
*/
|
|
34
57
|
function escapeLatexDelimiters(text: string) {
|
|
35
58
|
return text
|
|
36
|
-
.replace(
|
|
37
|
-
.replace(
|
|
38
|
-
.replace(
|
|
39
|
-
.replace(
|
|
59
|
+
.replace(/\\\(/g, '\\\\(')
|
|
60
|
+
.replace(/\\\)/g, '\\\\)')
|
|
61
|
+
.replace(/\\\[/g, '\\\\[')
|
|
62
|
+
.replace(/\\\]/g, '\\\\]');
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
function
|
|
65
|
+
function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
|
|
43
66
|
const appendContent = props.appendContent || false;
|
|
44
67
|
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
|
|
45
68
|
null
|
|
@@ -90,13 +113,16 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
|
|
|
90
113
|
|
|
91
114
|
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
92
115
|
setRenderedContent(renderer.node);
|
|
116
|
+
|
|
117
|
+
// Resolve the rendered promise.
|
|
118
|
+
props.rendered.resolve();
|
|
93
119
|
};
|
|
94
120
|
|
|
95
121
|
renderContent();
|
|
96
122
|
}, [props.markdownStr, props.rmRegistry]);
|
|
97
123
|
|
|
98
124
|
return (
|
|
99
|
-
<div className={
|
|
125
|
+
<div className={MD_RENDERED_CLASS}>
|
|
100
126
|
{renderedContent &&
|
|
101
127
|
(appendContent ? (
|
|
102
128
|
<div ref={node => node && node.appendChild(renderedContent)} />
|
|
@@ -120,4 +146,4 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
|
|
|
120
146
|
);
|
|
121
147
|
}
|
|
122
148
|
|
|
123
|
-
export const
|
|
149
|
+
export const MarkdownRenderer = React.memo(MarkdownRendererBase);
|
package/src/index.ts
CHANGED
package/src/model.ts
CHANGED
|
@@ -182,6 +182,10 @@ export class ChatModel implements IChatModel {
|
|
|
182
182
|
* Create a new chat model.
|
|
183
183
|
*/
|
|
184
184
|
constructor(options: ChatModel.IOptions = {}) {
|
|
185
|
+
if (options.id) {
|
|
186
|
+
this.id = options.id;
|
|
187
|
+
}
|
|
188
|
+
|
|
185
189
|
const config = options.config ?? {};
|
|
186
190
|
|
|
187
191
|
// Stack consecutive messages from the same user by default.
|
|
@@ -592,6 +596,11 @@ export namespace ChatModel {
|
|
|
592
596
|
* The instantiation options for a ChatModel.
|
|
593
597
|
*/
|
|
594
598
|
export interface IOptions {
|
|
599
|
+
/**
|
|
600
|
+
* The id of the chat.
|
|
601
|
+
*/
|
|
602
|
+
id?: string;
|
|
603
|
+
|
|
595
604
|
/**
|
|
596
605
|
* Initial config for the chat widget.
|
|
597
606
|
*/
|
package/src/types.ts
CHANGED
|
@@ -23,10 +23,6 @@ export interface IConfig {
|
|
|
23
23
|
* Whether to send a message via Shift-Enter instead of Enter.
|
|
24
24
|
*/
|
|
25
25
|
sendWithShiftEnter?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Last read message (no use yet).
|
|
28
|
-
*/
|
|
29
|
-
lastRead?: number;
|
|
30
26
|
/**
|
|
31
27
|
* Whether to stack consecutive messages from same user.
|
|
32
28
|
*/
|
package/style/chat.css
CHANGED
|
@@ -14,15 +14,23 @@
|
|
|
14
14
|
padding: 0 1em;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
.jp-chat-
|
|
17
|
+
.jp-chat-rendered-markdown {
|
|
18
18
|
position: relative;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/*
|
|
22
|
+
*
|
|
23
|
+
* Selectors must be nested in `.jp-ThemedContainer` to have a higher
|
|
24
|
+
* specificity than selectors in rules provided by JupyterLab.
|
|
25
|
+
*
|
|
26
|
+
* See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
|
|
27
|
+
* See also: https://github.com/jupyterlab/jupyter-ai/issues/1090
|
|
28
|
+
*/
|
|
29
|
+
.jp-ThemedContainer .jp-chat-rendered-markdown .jp-RenderedHTMLCommon {
|
|
22
30
|
padding-right: 0;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
.jp-chat-
|
|
33
|
+
.jp-ThemedContainer .jp-chat-rendered-markdown pre {
|
|
26
34
|
background-color: var(--jp-cell-editor-background);
|
|
27
35
|
overflow-x: auto;
|
|
28
36
|
white-space: pre;
|
|
@@ -31,13 +39,13 @@
|
|
|
31
39
|
border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
.jp-chat-
|
|
42
|
+
.jp-ThemedContainer .jp-chat-rendered-markdown pre > code {
|
|
35
43
|
background-color: inherit;
|
|
36
44
|
overflow-x: inherit;
|
|
37
45
|
white-space: inherit;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
.jp-chat-
|
|
48
|
+
.jp-ThemedContainer .jp-chat-rendered-markdown mjx-container {
|
|
41
49
|
font-size: 119%;
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -50,7 +58,7 @@
|
|
|
50
58
|
color: var(--jp-ui-font-color3);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
.jp-chat-
|
|
61
|
+
.jp-chat-rendered-markdown:hover .jp-chat-toolbar {
|
|
54
62
|
display: inherit;
|
|
55
63
|
}
|
|
56
64
|
|