@jupyter/chat 0.1.0 → 0.3.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 (55) hide show
  1. package/lib/active-cell-manager.d.ts +151 -0
  2. package/lib/active-cell-manager.js +201 -0
  3. package/lib/components/chat-input.d.ts +14 -4
  4. package/lib/components/chat-input.js +118 -10
  5. package/lib/components/chat-messages.d.ts +45 -15
  6. package/lib/components/chat-messages.js +237 -55
  7. package/lib/components/chat.d.ts +21 -6
  8. package/lib/components/chat.js +15 -45
  9. package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
  10. package/lib/components/code-blocks/code-toolbar.js +70 -0
  11. package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
  12. package/lib/components/code-blocks/copy-button.js +43 -0
  13. package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
  14. package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
  15. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
  16. package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
  17. package/lib/components/rendermime-markdown.d.ts +2 -0
  18. package/lib/components/rendermime-markdown.js +29 -15
  19. package/lib/components/scroll-container.js +1 -19
  20. package/lib/icons.d.ts +2 -0
  21. package/lib/icons.js +10 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.js +2 -0
  24. package/lib/model.d.ts +98 -14
  25. package/lib/model.js +197 -6
  26. package/lib/registry.d.ts +78 -0
  27. package/lib/registry.js +83 -0
  28. package/lib/types.d.ts +60 -4
  29. package/lib/widgets/chat-sidebar.d.ts +3 -4
  30. package/lib/widgets/chat-sidebar.js +2 -2
  31. package/lib/widgets/chat-widget.d.ts +2 -8
  32. package/lib/widgets/chat-widget.js +6 -6
  33. package/package.json +204 -200
  34. package/src/active-cell-manager.ts +318 -0
  35. package/src/components/chat-input.tsx +196 -50
  36. package/src/components/chat-messages.tsx +357 -95
  37. package/src/components/chat.tsx +43 -69
  38. package/src/components/code-blocks/code-toolbar.tsx +143 -0
  39. package/src/components/code-blocks/copy-button.tsx +68 -0
  40. package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
  41. package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
  42. package/src/components/rendermime-markdown.tsx +44 -20
  43. package/src/components/scroll-container.tsx +1 -25
  44. package/src/icons.ts +12 -0
  45. package/src/index.ts +2 -0
  46. package/src/model.ts +275 -21
  47. package/src/registry.ts +129 -0
  48. package/src/types.ts +62 -4
  49. package/src/widgets/chat-sidebar.tsx +3 -15
  50. package/src/widgets/chat-widget.tsx +8 -21
  51. package/style/chat.css +40 -0
  52. package/style/icons/read.svg +11 -0
  53. package/style/icons/replace-cell.svg +8 -0
  54. package/lib/components/copy-button.js +0 -35
  55. package/src/components/copy-button.tsx +0 -55
@@ -9,57 +9,20 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
9
9
  import SettingsIcon from '@mui/icons-material/Settings';
10
10
  import { IconButton } from '@mui/material';
11
11
  import { Box } from '@mui/system';
12
- import React, { useState, useEffect } from 'react';
12
+ import React, { useState } from 'react';
13
13
 
14
14
  import { JlThemeProvider } from './jl-theme-provider';
15
15
  import { ChatMessages } from './chat-messages';
16
16
  import { ChatInput } from './chat-input';
17
- import { ScrollContainer } from './scroll-container';
18
17
  import { IChatModel } from '../model';
19
- import { IChatMessage } from '../types';
20
-
21
- type ChatBodyProps = {
22
- model: IChatModel;
23
- rmRegistry: IRenderMimeRegistry;
24
- };
25
-
26
- function ChatBody({
27
- model,
28
- rmRegistry: renderMimeRegistry
29
- }: ChatBodyProps): JSX.Element {
30
- const [messages, setMessages] = useState<IChatMessage[]>([]);
31
-
32
- /**
33
- * Effect: fetch history and config on initial render
34
- */
35
- useEffect(() => {
36
- async function fetchHistory() {
37
- if (!model.getHistory) {
38
- return;
39
- }
40
- model
41
- .getHistory()
42
- .then(history => setMessages(history.messages))
43
- .catch(e => console.error(e));
44
- }
45
-
46
- fetchHistory();
47
- }, [model]);
48
-
49
- /**
50
- * Effect: listen to chat messages
51
- */
52
- useEffect(() => {
53
- function handleChatEvents(_: IChatModel) {
54
- setMessages([...model.messages]);
55
- }
56
-
57
- model.messagesUpdated.connect(handleChatEvents);
58
- return function cleanup() {
59
- model.messagesUpdated.disconnect(handleChatEvents);
60
- };
61
- }, [model]);
18
+ import { IAutocompletionRegistry } from '../registry';
62
19
 
20
+ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
21
+ const {
22
+ model,
23
+ rmRegistry: renderMimeRegistry,
24
+ autocompletionRegistry
25
+ } = props;
63
26
  // no need to append to messageGroups imperatively here. all of that is
64
27
  // handled by the listeners registered in the effect hooks above.
65
28
  const onSend = async (input: string) => {
@@ -69,13 +32,7 @@ function ChatBody({
69
32
 
70
33
  return (
71
34
  <>
72
- <ScrollContainer sx={{ flexGrow: 1 }}>
73
- <ChatMessages
74
- messages={messages}
75
- rmRegistry={renderMimeRegistry}
76
- model={model}
77
- />
78
- </ScrollContainer>
35
+ <ChatMessages rmRegistry={renderMimeRegistry} model={model} />
79
36
  <ChatInput
80
37
  onSend={onSend}
81
38
  sx={{
@@ -85,16 +42,15 @@ function ChatBody({
85
42
  paddingBottom: 0,
86
43
  borderTop: '1px solid var(--jp-border-color1)'
87
44
  }}
88
- sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
45
+ model={model}
46
+ autocompletionRegistry={autocompletionRegistry}
89
47
  />
90
48
  </>
91
49
  );
92
50
  }
93
51
 
94
52
  export function Chat(props: Chat.IOptions): JSX.Element {
95
- const [view, setView] = useState<Chat.ChatView>(
96
- props.chatView || Chat.ChatView.Chat
97
- );
53
+ const [view, setView] = useState<Chat.View>(props.chatView || Chat.View.chat);
98
54
  return (
99
55
  <JlThemeProvider themeManager={props.themeManager ?? null}>
100
56
  <Box
@@ -111,15 +67,15 @@ export function Chat(props: Chat.IOptions): JSX.Element {
111
67
  >
112
68
  {/* top bar */}
113
69
  <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
114
- {view !== Chat.ChatView.Chat ? (
115
- <IconButton onClick={() => setView(Chat.ChatView.Chat)}>
70
+ {view !== Chat.View.chat ? (
71
+ <IconButton onClick={() => setView(Chat.View.chat)}>
116
72
  <ArrowBackIcon />
117
73
  </IconButton>
118
74
  ) : (
119
75
  <Box />
120
76
  )}
121
- {view === Chat.ChatView.Chat && props.settingsPanel ? (
122
- <IconButton onClick={() => setView(Chat.ChatView.Settings)}>
77
+ {view !== Chat.View.settings && props.settingsPanel ? (
78
+ <IconButton onClick={() => setView(Chat.View.settings)}>
123
79
  <SettingsIcon />
124
80
  </IconButton>
125
81
  ) : (
@@ -127,10 +83,14 @@ export function Chat(props: Chat.IOptions): JSX.Element {
127
83
  )}
128
84
  </Box>
129
85
  {/* body */}
130
- {view === Chat.ChatView.Chat && (
131
- <ChatBody model={props.model} rmRegistry={props.rmRegistry} />
86
+ {view === Chat.View.chat && (
87
+ <ChatBody
88
+ model={props.model}
89
+ rmRegistry={props.rmRegistry}
90
+ autocompletionRegistry={props.autocompletionRegistry}
91
+ />
132
92
  )}
133
- {view === Chat.ChatView.Settings && props.settingsPanel && (
93
+ {view === Chat.View.settings && props.settingsPanel && (
134
94
  <props.settingsPanel />
135
95
  )}
136
96
  </Box>
@@ -143,9 +103,9 @@ export function Chat(props: Chat.IOptions): JSX.Element {
143
103
  */
144
104
  export namespace Chat {
145
105
  /**
146
- * The options to build the Chat UI.
106
+ * The props for the chat body component.
147
107
  */
148
- export interface IOptions {
108
+ export interface IChatBodyProps {
149
109
  /**
150
110
  * The chat model.
151
111
  */
@@ -154,6 +114,20 @@ export namespace Chat {
154
114
  * The rendermime registry.
155
115
  */
156
116
  rmRegistry: IRenderMimeRegistry;
117
+ /**
118
+ * Autocompletion registry.
119
+ */
120
+ autocompletionRegistry?: IAutocompletionRegistry;
121
+ /**
122
+ * Autocompletion name.
123
+ */
124
+ autocompletionName?: string;
125
+ }
126
+
127
+ /**
128
+ * The options to build the Chat UI.
129
+ */
130
+ export interface IOptions extends IChatBodyProps {
157
131
  /**
158
132
  * The theme manager.
159
133
  */
@@ -161,7 +135,7 @@ export namespace Chat {
161
135
  /**
162
136
  * The view to render.
163
137
  */
164
- chatView?: ChatView;
138
+ chatView?: View;
165
139
  /**
166
140
  * A settings panel that can be used for dedicated settings (e.g. jupyter-ai)
167
141
  */
@@ -172,8 +146,8 @@ export namespace Chat {
172
146
  * The view to render.
173
147
  * The settings view is available only if the settings panel is provided in options.
174
148
  */
175
- export enum ChatView {
176
- Chat,
177
- Settings
149
+ export enum View {
150
+ chat,
151
+ settings
178
152
  }
179
153
  }
@@ -0,0 +1,143 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { addAboveIcon, addBelowIcon } from '@jupyterlab/ui-components';
7
+ import { Box } from '@mui/material';
8
+ import React, { useEffect, useState } from 'react';
9
+
10
+ import { CopyButton } from './copy-button';
11
+ import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
12
+ import { IActiveCellManager } from '../../active-cell-manager';
13
+ import { replaceCellIcon } from '../../icons';
14
+ import { IChatModel } from '../../model';
15
+
16
+ const CODE_TOOLBAR_CLASS = 'jp-chat-code-toolbar';
17
+ const CODE_TOOLBAR_ITEM_CLASS = 'jp-chat-code-toolbar-item';
18
+
19
+ export type CodeToolbarProps = {
20
+ /**
21
+ * The chat model.
22
+ */
23
+ model: IChatModel;
24
+ /**
25
+ * The content of the Markdown code block this component is attached to.
26
+ */
27
+ content: string;
28
+ };
29
+
30
+ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
31
+ const { content, model } = props;
32
+ const [toolbarEnable, setToolbarEnable] = useState<boolean>(
33
+ model.config.enableCodeToolbar ?? true
34
+ );
35
+
36
+ const activeCellManager = model.activeCellManager;
37
+
38
+ const [toolbarBtnProps, setToolbarBtnProps] = useState<ToolbarButtonProps>({
39
+ content: content,
40
+ activeCellManager: activeCellManager,
41
+ activeCellAvailable: activeCellManager?.available ?? false
42
+ });
43
+
44
+ useEffect(() => {
45
+ activeCellManager?.availabilityChanged.connect(() => {
46
+ setToolbarBtnProps({
47
+ content,
48
+ activeCellManager: activeCellManager,
49
+ activeCellAvailable: activeCellManager.available
50
+ });
51
+ });
52
+
53
+ model.configChanged.connect((_, config) => {
54
+ setToolbarEnable(config.enableCodeToolbar ?? true);
55
+ });
56
+ }, [model]);
57
+
58
+ return activeCellManager === null || !toolbarEnable ? (
59
+ <></>
60
+ ) : (
61
+ <Box
62
+ sx={{
63
+ display: 'flex',
64
+ justifyContent: 'flex-end',
65
+ alignItems: 'center',
66
+ padding: '6px 2px',
67
+ marginBottom: '1em',
68
+ border: '1px solid var(--jp-cell-editor-border-color)',
69
+ borderTop: 'none'
70
+ }}
71
+ className={CODE_TOOLBAR_CLASS}
72
+ >
73
+ <InsertAboveButton
74
+ {...toolbarBtnProps}
75
+ className={CODE_TOOLBAR_ITEM_CLASS}
76
+ />
77
+ <InsertBelowButton
78
+ {...toolbarBtnProps}
79
+ className={CODE_TOOLBAR_ITEM_CLASS}
80
+ />
81
+ <ReplaceButton {...toolbarBtnProps} className={CODE_TOOLBAR_ITEM_CLASS} />
82
+ <CopyButton value={content} className={CODE_TOOLBAR_ITEM_CLASS} />
83
+ </Box>
84
+ );
85
+ }
86
+
87
+ type ToolbarButtonProps = {
88
+ content: string;
89
+ activeCellAvailable?: boolean;
90
+ activeCellManager: IActiveCellManager | null;
91
+ className?: string;
92
+ };
93
+
94
+ function InsertAboveButton(props: ToolbarButtonProps) {
95
+ const tooltip = props.activeCellAvailable
96
+ ? 'Insert above active cell'
97
+ : 'Insert above active cell (no active cell)';
98
+
99
+ return (
100
+ <TooltippedIconButton
101
+ className={props.className}
102
+ tooltip={tooltip}
103
+ onClick={() => props.activeCellManager?.insertAbove(props.content)}
104
+ disabled={!props.activeCellAvailable}
105
+ >
106
+ <addAboveIcon.react height="16px" width="16px" />
107
+ </TooltippedIconButton>
108
+ );
109
+ }
110
+
111
+ function InsertBelowButton(props: ToolbarButtonProps) {
112
+ const tooltip = props.activeCellAvailable
113
+ ? 'Insert below active cell'
114
+ : 'Insert below active cell (no active cell)';
115
+
116
+ return (
117
+ <TooltippedIconButton
118
+ className={props.className}
119
+ tooltip={tooltip}
120
+ disabled={!props.activeCellAvailable}
121
+ onClick={() => props.activeCellManager?.insertBelow(props.content)}
122
+ >
123
+ <addBelowIcon.react height="16px" width="16px" />
124
+ </TooltippedIconButton>
125
+ );
126
+ }
127
+
128
+ function ReplaceButton(props: ToolbarButtonProps) {
129
+ const tooltip = props.activeCellAvailable
130
+ ? 'Replace active cell'
131
+ : 'Replace active cell (no active cell)';
132
+
133
+ return (
134
+ <TooltippedIconButton
135
+ className={props.className}
136
+ tooltip={tooltip}
137
+ disabled={!props.activeCellAvailable}
138
+ onClick={() => props.activeCellManager?.replace(props.content)}
139
+ >
140
+ <replaceCellIcon.react height="16px" width="16px" />
141
+ </TooltippedIconButton>
142
+ );
143
+ }
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useState, useCallback, useRef } from 'react';
7
+
8
+ import { copyIcon } from '@jupyterlab/ui-components';
9
+
10
+ import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
11
+
12
+ enum CopyStatus {
13
+ None,
14
+ Copying,
15
+ Copied
16
+ }
17
+
18
+ const COPYBTN_TEXT_BY_STATUS: Record<CopyStatus, string> = {
19
+ [CopyStatus.None]: 'Copy to clipboard',
20
+ [CopyStatus.Copying]: 'Copying…',
21
+ [CopyStatus.Copied]: 'Copied!'
22
+ };
23
+
24
+ type CopyButtonProps = {
25
+ value: string;
26
+ className?: string;
27
+ };
28
+
29
+ export function CopyButton(props: CopyButtonProps): JSX.Element {
30
+ const [copyStatus, setCopyStatus] = useState<CopyStatus>(CopyStatus.None);
31
+ const timeoutId = useRef<number | null>(null);
32
+
33
+ const copy = useCallback(async () => {
34
+ // ignore if we are already copying
35
+ if (copyStatus === CopyStatus.Copying) {
36
+ return;
37
+ }
38
+
39
+ try {
40
+ await navigator.clipboard.writeText(props.value);
41
+ } catch (err) {
42
+ console.error('Failed to copy text: ', err);
43
+ setCopyStatus(CopyStatus.None);
44
+ return;
45
+ }
46
+
47
+ setCopyStatus(CopyStatus.Copied);
48
+ if (timeoutId.current) {
49
+ clearTimeout(timeoutId.current);
50
+ }
51
+ timeoutId.current = window.setTimeout(
52
+ () => setCopyStatus(CopyStatus.None),
53
+ 1000
54
+ );
55
+ }, [copyStatus, props.value]);
56
+
57
+ return (
58
+ <TooltippedIconButton
59
+ className={props.className}
60
+ tooltip={COPYBTN_TEXT_BY_STATUS[copyStatus]}
61
+ placement="top"
62
+ onClick={copy}
63
+ aria-label="Copy to clipboard"
64
+ >
65
+ <copyIcon.react height="16px" width="16px" />
66
+ </TooltippedIconButton>
67
+ );
68
+ }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React from 'react';
7
+ import { styled, Tooltip, TooltipProps, tooltipClasses } from '@mui/material';
8
+
9
+ /**
10
+ * A restyled MUI tooltip component that is dark by default to improve contrast
11
+ * against JupyterLab's default light theme. TODO: support dark themes.
12
+ */
13
+ export const ContrastingTooltip = styled(
14
+ ({ className, ...props }: TooltipProps) => (
15
+ <Tooltip {...props} arrow classes={{ popper: className }} />
16
+ )
17
+ )(({ theme }) => ({
18
+ [`& .${tooltipClasses.tooltip}`]: {
19
+ backgroundColor: theme.palette.common.black,
20
+ color: theme.palette.common.white,
21
+ boxShadow: theme.shadows[1],
22
+ fontSize: 11
23
+ },
24
+ [`& .${tooltipClasses.arrow}`]: {
25
+ color: theme.palette.common.black
26
+ }
27
+ }));
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React from 'react';
7
+ import { IconButton, IconButtonProps, TooltipProps } from '@mui/material';
8
+
9
+ import { ContrastingTooltip } from './contrasting-tooltip';
10
+
11
+ export type TooltippedIconButtonProps = {
12
+ onClick: () => unknown;
13
+ tooltip: string;
14
+ children: JSX.Element;
15
+ className?: string;
16
+ disabled?: boolean;
17
+ placement?: TooltipProps['placement'];
18
+ /**
19
+ * The offset of the tooltip popup.
20
+ *
21
+ * The expected syntax is defined by the Popper library:
22
+ * https://popper.js.org/docs/v2/modifiers/offset/
23
+ */
24
+ offset?: [number, number];
25
+ 'aria-label'?: string;
26
+ /**
27
+ * Props passed directly to the MUI `IconButton` component.
28
+ */
29
+ iconButtonProps?: IconButtonProps;
30
+ };
31
+
32
+ /**
33
+ * A component that renders an MUI `IconButton` with a high-contrast tooltip
34
+ * provided by `ContrastingTooltip`. This component differs from the MUI
35
+ * defaults in the following ways:
36
+ *
37
+ * - Shows the tooltip on hover even if disabled.
38
+ * - Renders the tooltip above the button by default.
39
+ * - Renders the tooltip closer to the button by default.
40
+ * - Lowers the opacity of the IconButton when disabled.
41
+ * - Renders the IconButton with `line-height: 0` to avoid showing extra
42
+ * vertical space in SVG icons.
43
+ */
44
+ export function TooltippedIconButton(
45
+ props: TooltippedIconButtonProps
46
+ ): JSX.Element {
47
+ return (
48
+ <ContrastingTooltip
49
+ title={props.tooltip}
50
+ placement={props.placement ?? 'top'}
51
+ slotProps={{
52
+ popper: {
53
+ modifiers: [
54
+ {
55
+ name: 'offset',
56
+ options: {
57
+ offset: [0, -8]
58
+ }
59
+ }
60
+ ]
61
+ }
62
+ }}
63
+ >
64
+ {/*
65
+ By default, tooltips never appear when the IconButton is disabled. The
66
+ official way to support this feature in MUI is to wrap the child Button
67
+ element in a `span` element.
68
+
69
+ See: https://mui.com/material-ui/react-tooltip/#disabled-elements
70
+ */}
71
+ <span className={props.className}>
72
+ <IconButton
73
+ {...props.iconButtonProps}
74
+ onClick={props.onClick}
75
+ disabled={props.disabled}
76
+ sx={{ lineHeight: 0, ...(props.disabled && { opacity: 0.5 }) }}
77
+ aria-label={props['aria-label']}
78
+ >
79
+ {props.children}
80
+ </IconButton>
81
+ </span>
82
+ </ContrastingTooltip>
83
+ );
84
+ }
@@ -4,11 +4,12 @@
4
4
  */
5
5
 
6
6
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
- import React, { useState, useEffect, useRef } from 'react';
8
- import ReactDOM from 'react-dom';
7
+ import React, { useState, useEffect } from 'react';
8
+ import { createPortal } from 'react-dom';
9
9
 
10
- import { CopyButton } from './copy-button';
10
+ import { CodeToolbar, CodeToolbarProps } from './code-blocks/code-toolbar';
11
11
  import { MessageToolbar } from './toolbar';
12
+ import { IChatModel } from '../model';
12
13
 
13
14
  const MD_MIME_TYPE = 'text/markdown';
14
15
  const RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
@@ -17,6 +18,7 @@ type RendermimeMarkdownProps = {
17
18
  markdownStr: string;
18
19
  rmRegistry: IRenderMimeRegistry;
19
20
  appendContent?: boolean;
21
+ model: IChatModel;
20
22
  edit?: () => void;
21
23
  delete?: () => void;
22
24
  };
@@ -37,7 +39,11 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
37
39
  const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
38
40
  null
39
41
  );
40
- const containerRef = useRef<HTMLDivElement>(null);
42
+
43
+ // each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
44
+ const [codeToolbarDefns, setCodeToolbarDefns] = useState<
45
+ Array<[HTMLDivElement, CodeToolbarProps]>
46
+ >([]);
41
47
 
42
48
  useEffect(() => {
43
49
  const renderContent = async () => {
@@ -49,23 +55,29 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
49
55
  const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
50
56
  await renderer.renderModel(model);
51
57
  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
- });
58
+ if (!renderer.node) {
59
+ throw new Error(
60
+ 'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
61
+ );
67
62
  }
68
63
 
64
+ const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
65
+
66
+ // Attach CodeToolbar root element to each <pre> block
67
+ const preBlocks = renderer.node.querySelectorAll('pre');
68
+ preBlocks.forEach(preBlock => {
69
+ const codeToolbarRoot = document.createElement('div');
70
+ preBlock.parentNode?.insertBefore(
71
+ codeToolbarRoot,
72
+ preBlock.nextSibling
73
+ );
74
+ newCodeToolbarDefns.push([
75
+ codeToolbarRoot,
76
+ { model: props.model, content: preBlock.textContent || '' }
77
+ ]);
78
+ });
79
+
80
+ setCodeToolbarDefns(newCodeToolbarDefns);
69
81
  setRenderedContent(renderer.node);
70
82
  };
71
83
 
@@ -73,7 +85,7 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
73
85
  }, [props.markdownStr, props.rmRegistry]);
74
86
 
75
87
  return (
76
- <div ref={containerRef} className={RENDERMIME_MD_CLASS}>
88
+ <div className={RENDERMIME_MD_CLASS}>
77
89
  {renderedContent &&
78
90
  (appendContent ? (
79
91
  <div ref={node => node && node.appendChild(renderedContent)} />
@@ -81,6 +93,18 @@ function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
81
93
  <div ref={node => node && node.replaceChildren(renderedContent)} />
82
94
  ))}
83
95
  <MessageToolbar edit={props.edit} delete={props.delete} />
96
+ {
97
+ // Render a `CodeToolbar` element underneath each code block.
98
+ // We use ReactDOM.createPortal() so each `CodeToolbar` element is able
99
+ // to use the context in the main React tree.
100
+ codeToolbarDefns.map(codeToolbarDefn => {
101
+ const [codeToolbarRoot, codeToolbarProps] = codeToolbarDefn;
102
+ return createPortal(
103
+ <CodeToolbar {...codeToolbarProps} />,
104
+ codeToolbarRoot
105
+ );
106
+ })
107
+ }
84
108
  </div>
85
109
  );
86
110
  }
@@ -3,7 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import React, { useEffect, useMemo } from 'react';
6
+ import React, { useMemo } from 'react';
7
7
  import { Box, SxProps, Theme } from '@mui/material';
8
8
 
9
9
  type ScrollContainerProps = {
@@ -32,30 +32,6 @@ export function ScrollContainer(props: ScrollContainerProps): JSX.Element {
32
32
  []
33
33
  );
34
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
35
  return (
60
36
  <Box
61
37
  id={id}
package/src/icons.ts CHANGED
@@ -8,8 +8,20 @@
8
8
  import { LabIcon } from '@jupyterlab/ui-components';
9
9
 
10
10
  import chatSvgStr from '../style/icons/chat.svg';
11
+ import readSvgStr from '../style/icons/read.svg';
12
+ import replaceCellSvg from '../style/icons/replace-cell.svg';
11
13
 
12
14
  export const chatIcon = new LabIcon({
13
15
  name: 'jupyter-chat::chat',
14
16
  svgstr: chatSvgStr
15
17
  });
18
+
19
+ export const readIcon = new LabIcon({
20
+ name: 'jupyter-chat::read',
21
+ svgstr: readSvgStr
22
+ });
23
+
24
+ export const replaceCellIcon = new LabIcon({
25
+ name: 'jupyter-ai::replace-cell',
26
+ svgstr: replaceCellSvg
27
+ });
package/src/index.ts CHANGED
@@ -5,7 +5,9 @@
5
5
 
6
6
  export * from './icons';
7
7
  export * from './model';
8
+ export * from './registry';
8
9
  export * from './types';
10
+ export * from './active-cell-manager';
9
11
  export * from './widgets/chat-error';
10
12
  export * from './widgets/chat-sidebar';
11
13
  export * from './widgets/chat-widget';