@jupyter/chat 0.2.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.
- package/lib/active-cell-manager.d.ts +151 -0
- package/lib/active-cell-manager.js +201 -0
- package/lib/components/chat-input.d.ts +5 -4
- package/lib/components/chat-input.js +11 -4
- package/lib/components/chat-messages.js +2 -3
- package/lib/components/chat.js +1 -2
- package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
- package/lib/components/code-blocks/code-toolbar.js +70 -0
- package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
- package/lib/components/code-blocks/copy-button.js +43 -0
- package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
- package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
- package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
- package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
- package/lib/components/rendermime-markdown.d.ts +2 -0
- package/lib/components/rendermime-markdown.js +29 -15
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/model.d.ts +20 -0
- package/lib/model.js +14 -1
- package/lib/types.d.ts +4 -0
- package/package.json +6 -4
- package/src/active-cell-manager.ts +318 -0
- package/src/components/chat-input.tsx +17 -8
- package/src/components/chat-messages.tsx +3 -2
- package/src/components/chat.tsx +1 -1
- package/src/components/code-blocks/code-toolbar.tsx +143 -0
- package/src/components/code-blocks/copy-button.tsx +68 -0
- package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
- package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
- package/src/components/rendermime-markdown.tsx +44 -20
- package/src/icons.ts +6 -0
- package/src/index.ts +1 -0
- package/src/model.ts +33 -0
- package/src/types.ts +4 -0
- package/style/icons/replace-cell.svg +8 -0
- package/lib/components/copy-button.js +0 -35
- package/src/components/copy-button.tsx +0 -55
|
@@ -16,18 +16,27 @@ import {
|
|
|
16
16
|
} from '@mui/material';
|
|
17
17
|
import { Send, Cancel } from '@mui/icons-material';
|
|
18
18
|
import clsx from 'clsx';
|
|
19
|
-
import {
|
|
19
|
+
import { IChatModel } from '../model';
|
|
20
20
|
import { IAutocompletionRegistry } from '../registry';
|
|
21
|
+
import { AutocompleteCommand, IAutocompletionCommandsProps } from '../types';
|
|
21
22
|
|
|
22
23
|
const INPUT_BOX_CLASS = 'jp-chat-input-container';
|
|
23
24
|
const SEND_BUTTON_CLASS = 'jp-chat-send-button';
|
|
24
25
|
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
|
|
25
26
|
|
|
26
27
|
export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
27
|
-
const { autocompletionName, autocompletionRegistry,
|
|
28
|
-
props;
|
|
28
|
+
const { autocompletionName, autocompletionRegistry, model } = props;
|
|
29
29
|
const autocompletion = useRef<IAutocompletionCommandsProps>();
|
|
30
30
|
const [input, setInput] = useState<string>(props.value || '');
|
|
31
|
+
const [sendWithShiftEnter, setSendWithShiftEnter] = useState<boolean>(
|
|
32
|
+
model.config.sendWithShiftEnter ?? false
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
model.configChanged.connect((_, config) => {
|
|
37
|
+
setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
|
|
38
|
+
});
|
|
39
|
+
}, [model]);
|
|
31
40
|
|
|
32
41
|
// The autocomplete commands options.
|
|
33
42
|
const [commandOptions, setCommandOptions] = useState<AutocompleteCommand[]>(
|
|
@@ -124,7 +133,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
// Set the helper text based on whether Shift+Enter is used for sending.
|
|
127
|
-
const helperText =
|
|
136
|
+
const helperText = sendWithShiftEnter ? (
|
|
128
137
|
<span>
|
|
129
138
|
Press <b>Shift</b>+<b>Enter</b> to send message
|
|
130
139
|
</span>
|
|
@@ -248,6 +257,10 @@ export namespace ChatInput {
|
|
|
248
257
|
* The properties of the react element.
|
|
249
258
|
*/
|
|
250
259
|
export interface IProps {
|
|
260
|
+
/**
|
|
261
|
+
* The chat model.
|
|
262
|
+
*/
|
|
263
|
+
model: IChatModel;
|
|
251
264
|
/**
|
|
252
265
|
* The initial value of the input (default to '')
|
|
253
266
|
*/
|
|
@@ -260,10 +273,6 @@ export namespace ChatInput {
|
|
|
260
273
|
* The function to be called to cancel editing.
|
|
261
274
|
*/
|
|
262
275
|
onCancel?: () => unknown;
|
|
263
|
-
/**
|
|
264
|
-
* Whether using shift+enter to send the message.
|
|
265
|
-
*/
|
|
266
|
-
sendWithShiftEnter: boolean;
|
|
267
276
|
/**
|
|
268
277
|
* Custom mui/material styles.
|
|
269
278
|
*/
|
|
@@ -74,7 +74,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
|
|
|
74
74
|
* Effect: listen to chat messages.
|
|
75
75
|
*/
|
|
76
76
|
useEffect(() => {
|
|
77
|
-
function handleChatEvents(
|
|
77
|
+
function handleChatEvents() {
|
|
78
78
|
setMessages([...model.messages]);
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -392,12 +392,13 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
|
|
|
392
392
|
value={message.body}
|
|
393
393
|
onSend={(input: string) => updateMessage(message.id, input)}
|
|
394
394
|
onCancel={() => cancelEdition()}
|
|
395
|
-
|
|
395
|
+
model={model}
|
|
396
396
|
/>
|
|
397
397
|
) : (
|
|
398
398
|
<RendermimeMarkdown
|
|
399
399
|
rmRegistry={rmRegistry}
|
|
400
400
|
markdownStr={message.body}
|
|
401
|
+
model={model}
|
|
401
402
|
edit={canEdit ? () => setEdit(true) : undefined}
|
|
402
403
|
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
403
404
|
/>
|
package/src/components/chat.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
|
|
|
42
42
|
paddingBottom: 0,
|
|
43
43
|
borderTop: '1px solid var(--jp-border-color1)'
|
|
44
44
|
}}
|
|
45
|
-
|
|
45
|
+
model={model}
|
|
46
46
|
autocompletionRegistry={autocompletionRegistry}
|
|
47
47
|
/>
|
|
48
48
|
</>
|
|
@@ -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
|
|
8
|
-
import
|
|
7
|
+
import React, { useState, useEffect } from 'react';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
9
|
|
|
10
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
}
|
package/src/icons.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
|
|
|
9
9
|
|
|
10
10
|
import chatSvgStr from '../style/icons/chat.svg';
|
|
11
11
|
import readSvgStr from '../style/icons/read.svg';
|
|
12
|
+
import replaceCellSvg from '../style/icons/replace-cell.svg';
|
|
12
13
|
|
|
13
14
|
export const chatIcon = new LabIcon({
|
|
14
15
|
name: 'jupyter-chat::chat',
|
|
@@ -19,3 +20,8 @@ export const readIcon = new LabIcon({
|
|
|
19
20
|
name: 'jupyter-chat::read',
|
|
20
21
|
svgstr: readSvgStr
|
|
21
22
|
});
|
|
23
|
+
|
|
24
|
+
export const replaceCellIcon = new LabIcon({
|
|
25
|
+
name: 'jupyter-ai::replace-cell',
|
|
26
|
+
svgstr: replaceCellSvg
|
|
27
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './icons';
|
|
|
7
7
|
export * from './model';
|
|
8
8
|
export * from './registry';
|
|
9
9
|
export * from './types';
|
|
10
|
+
export * from './active-cell-manager';
|
|
10
11
|
export * from './widgets/chat-error';
|
|
11
12
|
export * from './widgets/chat-sidebar';
|
|
12
13
|
export * from './widgets/chat-widget';
|
package/src/model.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
IConfig,
|
|
15
15
|
IUser
|
|
16
16
|
} from './types';
|
|
17
|
+
import { IActiveCellManager } from './active-cell-manager';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* The chat model interface.
|
|
@@ -49,11 +50,21 @@ export interface IChatModel extends IDisposable {
|
|
|
49
50
|
*/
|
|
50
51
|
readonly messages: IChatMessage[];
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Get the active cell manager.
|
|
55
|
+
*/
|
|
56
|
+
readonly activeCellManager: IActiveCellManager | null;
|
|
57
|
+
|
|
52
58
|
/**
|
|
53
59
|
* A signal emitting when the messages list is updated.
|
|
54
60
|
*/
|
|
55
61
|
readonly messagesUpdated: ISignal<IChatModel, void>;
|
|
56
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A signal emitting when the messages list is updated.
|
|
65
|
+
*/
|
|
66
|
+
get configChanged(): ISignal<IChatModel, IConfig>;
|
|
67
|
+
|
|
57
68
|
/**
|
|
58
69
|
* A signal emitting when unread messages change.
|
|
59
70
|
*/
|
|
@@ -146,6 +157,8 @@ export class ChatModel implements IChatModel {
|
|
|
146
157
|
this._config = { stackMessages: true, ...config };
|
|
147
158
|
|
|
148
159
|
this._commands = options.commands;
|
|
160
|
+
|
|
161
|
+
this._activeCellManager = options.activeCellManager ?? null;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/**
|
|
@@ -155,6 +168,9 @@ export class ChatModel implements IChatModel {
|
|
|
155
168
|
return this._messages;
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
get activeCellManager(): IActiveCellManager | null {
|
|
172
|
+
return this._activeCellManager;
|
|
173
|
+
}
|
|
158
174
|
/**
|
|
159
175
|
* The chat model id.
|
|
160
176
|
*/
|
|
@@ -215,6 +231,9 @@ export class ChatModel implements IChatModel {
|
|
|
215
231
|
|
|
216
232
|
this._config = { ...this._config, ...value };
|
|
217
233
|
|
|
234
|
+
this._configChanged.emit(this._config);
|
|
235
|
+
|
|
236
|
+
// Update the stacked status of the messages and the view.
|
|
218
237
|
if (stackMessagesChanged) {
|
|
219
238
|
if (this._config.stackMessages) {
|
|
220
239
|
this._messages.slice(1).forEach((message, idx) => {
|
|
@@ -288,6 +307,13 @@ export class ChatModel implements IChatModel {
|
|
|
288
307
|
return this._messagesUpdated;
|
|
289
308
|
}
|
|
290
309
|
|
|
310
|
+
/**
|
|
311
|
+
* A signal emitting when the messages list is updated.
|
|
312
|
+
*/
|
|
313
|
+
get configChanged(): ISignal<IChatModel, IConfig> {
|
|
314
|
+
return this._configChanged;
|
|
315
|
+
}
|
|
316
|
+
|
|
291
317
|
/**
|
|
292
318
|
* A signal emitting when unread messages change.
|
|
293
319
|
*/
|
|
@@ -466,8 +492,10 @@ export class ChatModel implements IChatModel {
|
|
|
466
492
|
private _config: IConfig;
|
|
467
493
|
private _isDisposed = false;
|
|
468
494
|
private _commands?: CommandRegistry;
|
|
495
|
+
private _activeCellManager: IActiveCellManager | null;
|
|
469
496
|
private _notificationId: string | null = null;
|
|
470
497
|
private _messagesUpdated = new Signal<IChatModel, void>(this);
|
|
498
|
+
private _configChanged = new Signal<IChatModel, IConfig>(this);
|
|
471
499
|
private _unreadChanged = new Signal<IChatModel, number[]>(this);
|
|
472
500
|
private _viewportChanged = new Signal<IChatModel, number[]>(this);
|
|
473
501
|
}
|
|
@@ -489,5 +517,10 @@ export namespace ChatModel {
|
|
|
489
517
|
* Commands registry.
|
|
490
518
|
*/
|
|
491
519
|
commands?: CommandRegistry;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Active cell manager
|
|
523
|
+
*/
|
|
524
|
+
activeCellManager?: IActiveCellManager | null;
|
|
492
525
|
}
|
|
493
526
|
}
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
3
|
+
d="M12 4.71429V7H1.71429V4.71429H12ZM12.5714 3C13.2026 3 13.7143 3.51168 13.7143 4.14286V7.57143C13.7143 8.20263 13.2026 8.71429 12.5714 8.71429H1.14286C0.51168 8.71429 0 8.20263 0 7.57143V4.14286C0 3.51168 0.511669 3 1.14286 3H12.5714Z"
|
|
4
|
+
fill="#565656" />
|
|
5
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
6
|
+
d="M14 8.71429V11H3.71429V8.71429H14ZM14.5714 7C15.2026 7 15.7143 7.51168 15.7143 8.14286V11.5714C15.7143 12.2026 15.2026 12.7143 14.5714 12.7143H3.14286C2.51168 12.7143 2 12.2026 2 11.5714V8.14286C2 7.51168 2.51167 7 3.14286 7H14.5714Z"
|
|
7
|
+
fill="#565656" />
|
|
8
|
+
</svg>
|