@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.
Files changed (39) hide show
  1. package/lib/components/chat-input.js +12 -6
  2. package/lib/components/chat-messages.d.ts +10 -9
  3. package/lib/components/chat-messages.js +77 -63
  4. package/lib/components/chat.js +5 -2
  5. package/lib/components/code-blocks/index.d.ts +2 -0
  6. package/lib/components/code-blocks/index.js +6 -0
  7. package/lib/components/index.d.ts +10 -0
  8. package/lib/components/index.js +14 -0
  9. package/lib/components/input/cancel-button.d.ts +0 -1
  10. package/lib/components/input/cancel-button.js +1 -2
  11. package/lib/components/input/index.d.ts +2 -0
  12. package/lib/components/input/index.js +6 -0
  13. package/lib/components/markdown-renderer.d.ts +37 -0
  14. package/lib/components/{rendermime-markdown.js → markdown-renderer.js} +10 -8
  15. package/lib/components/mui-extras/index.d.ts +3 -0
  16. package/lib/components/mui-extras/index.js +7 -0
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.js +1 -0
  19. package/lib/model.d.ts +4 -0
  20. package/lib/model.js +3 -0
  21. package/lib/types.d.ts +0 -4
  22. package/package.json +2 -1
  23. package/src/components/chat-input.tsx +14 -11
  24. package/src/components/chat-messages.tsx +151 -129
  25. package/src/components/chat.tsx +3 -0
  26. package/src/components/code-blocks/index.ts +7 -0
  27. package/src/components/index.ts +15 -0
  28. package/src/components/input/cancel-button.tsx +0 -3
  29. package/src/components/input/index.ts +7 -0
  30. package/src/components/{rendermime-markdown.tsx → markdown-renderer.tsx} +36 -10
  31. package/src/components/mui-extras/index.ts +8 -0
  32. package/src/index.ts +1 -0
  33. package/src/model.ts +9 -0
  34. package/src/types.ts +0 -4
  35. package/style/chat.css +14 -6
  36. package/lib/components/mui-extras/stacking-alert.d.ts +0 -28
  37. package/lib/components/mui-extras/stacking-alert.js +0 -56
  38. package/lib/components/rendermime-markdown.d.ts +0 -14
  39. 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 { RendermimeMarkdown } from './rendermime-markdown';
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 intersection observer that listen to all the message visibility.
54
- const observerRef = useRef<IntersectionObserver>(
55
- new IntersectionObserver(viewportChange)
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
- * Function called when a message enter or leave the viewport.
98
+ * Observe the messages to update the current viewport and the unread messages.
99
99
  */
100
- function viewportChange(entries: IntersectionObserverEntry[]) {
101
- const unread = [...model.unreadMessages];
102
- let unreadModified = false;
103
- entries.forEach(entry => {
104
- const index = parseInt(entry.target.getAttribute('data-index') ?? '');
105
- if (!isNaN(index)) {
106
- if (unread.length) {
107
- const unreadIdx = unread.indexOf(index);
108
- if (unreadIdx !== -1 && entry.isIntersecting) {
109
- unread.splice(unreadIdx, 1);
110
- unreadModified = true;
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
- const viewportIdx = inViewport.current.indexOf(index);
114
- if (!entry.isIntersecting && viewportIdx !== -1) {
115
- inViewport.current.splice(viewportIdx, 1);
116
- } else if (entry.isIntersecting && viewportIdx === -1) {
117
- inViewport.current.push(index);
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
- props.model.messagesInViewport = inViewport.current;
123
- if (unreadModified) {
124
- props.model.unreadMessages = unread;
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
- observerRef.current?.disconnect();
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 intersection observer for all the messages.
325
+ * The promise to resolve when the message is rendered.
297
326
  */
298
- observer: IntersectionObserver | null;
327
+ renderedPromise: PromiseDelegate<void>;
299
328
  };
300
329
 
301
330
  /**
302
331
  * The message component body.
303
332
  */
304
- export function ChatMessage(props: ChatMessageProps): JSX.Element {
305
- const { message, model, rmRegistry } = props;
306
- const elementRef = useRef<HTMLDivElement>(null);
307
- const [edit, setEdit] = useState<boolean>(false);
308
- const [deleted, setDeleted] = useState<boolean>(false);
309
- const [canEdit, setCanEdit] = useState<boolean>(false);
310
- const [canDelete, setCanDelete] = useState<boolean>(false);
311
-
312
- // Add the current message to the observer, to actualize viewport and unread messages.
313
- useEffect(() => {
314
- if (elementRef.current === null) {
315
- return;
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
- // If the observer is defined, let's observe the message.
319
- props.observer?.observe(elementRef.current);
355
+ // Cancel the current edition of the message.
356
+ const cancelEdition = (): void => {
357
+ setEdit(false);
358
+ };
320
359
 
321
- return () => {
322
- if (elementRef.current !== null) {
323
- props.observer?.unobserve(elementRef.current);
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
- // Look if the message can be deleted or edited.
329
- useEffect(() => {
330
- setDeleted(message.deleted ?? false);
331
- if (model.user !== undefined && !message.deleted) {
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
- } else {
337
- setCanEdit(false);
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
- // Empty if the message has been deleted.
368
- return deleted ? (
369
- <div ref={elementRef} data-index={props.index}></div>
370
- ) : (
371
- <div ref={elementRef} data-index={props.index}>
372
- {edit && canEdit ? (
373
- <ChatInput
374
- value={message.body}
375
- onSend={(input: string) => updateMessage(message.id, input)}
376
- onCancel={() => cancelEdition()}
377
- model={model}
378
- hideIncludeSelection={true}
379
- />
380
- ) : (
381
- <RendermimeMarkdown
382
- rmRegistry={rmRegistry}
383
- markdownStr={message.body}
384
- model={model}
385
- edit={canEdit ? () => setEdit(true) : undefined}
386
- delete={canDelete ? () => deleteMessage(message.id) : undefined}
387
- />
388
- )}
389
- </div>
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 first the unread message or to last message on first rendering.
502
- if (model.unreadMessages.length) {
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
- gotoMessage!(
549
- unreadAfter !== null ? unreadAfter : model.messages.length - 1
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
@@ -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,7 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ export * from './code-toolbar';
7
+ export * from './copy-button';
@@ -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',
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ export * from './cancel-button';
7
+ export * from './send-button';
@@ -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 RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
16
+ const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
16
17
 
17
- type RendermimeMarkdownProps = {
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
- appendContent?: boolean;
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('\\(/g', '\\\\(')
37
- .replace('\\)/g', '\\\\)')
38
- .replace('\\[/g', '\\\\[')
39
- .replace('\\]/g', '\\\\]');
59
+ .replace(/\\\(/g, '\\\\(')
60
+ .replace(/\\\)/g, '\\\\)')
61
+ .replace(/\\\[/g, '\\\\[')
62
+ .replace(/\\\]/g, '\\\\]');
40
63
  }
41
64
 
42
- function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
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={RENDERMIME_MD_CLASS}>
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 RendermimeMarkdown = React.memo(RendermimeMarkdownBase);
149
+ export const MarkdownRenderer = React.memo(MarkdownRendererBase);
@@ -0,0 +1,8 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ export * from './contrasting-tooltip';
7
+ export * from './tooltipped-button';
8
+ export * from './tooltipped-icon-button';
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  export * from './active-cell-manager';
7
+ export * from './components';
7
8
  export * from './icons';
8
9
  export * from './model';
9
10
  export * from './registry';
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-rendermime-markdown {
17
+ .jp-chat-rendered-markdown {
18
18
  position: relative;
19
19
  }
20
20
 
21
- .jp-chat-rendermime-markdown .jp-RenderedHTMLCommon {
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-rendermime-markdown pre {
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-rendermime-markdown pre > code {
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-rendermime-markdown mjx-container {
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-rendermime-markdown:hover .jp-chat-toolbar {
61
+ .jp-chat-rendered-markdown:hover .jp-chat-toolbar {
54
62
  display: inherit;
55
63
  }
56
64