@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { IconButton } from '@mui/material';
|
|
7
|
+
import { ContrastingTooltip } from './contrasting-tooltip';
|
|
8
|
+
/**
|
|
9
|
+
* A component that renders an MUI `IconButton` with a high-contrast tooltip
|
|
10
|
+
* provided by `ContrastingTooltip`. This component differs from the MUI
|
|
11
|
+
* defaults in the following ways:
|
|
12
|
+
*
|
|
13
|
+
* - Shows the tooltip on hover even if disabled.
|
|
14
|
+
* - Renders the tooltip above the button by default.
|
|
15
|
+
* - Renders the tooltip closer to the button by default.
|
|
16
|
+
* - Lowers the opacity of the IconButton when disabled.
|
|
17
|
+
* - Renders the IconButton with `line-height: 0` to avoid showing extra
|
|
18
|
+
* vertical space in SVG icons.
|
|
19
|
+
*/
|
|
20
|
+
export function TooltippedIconButton(props) {
|
|
21
|
+
var _a;
|
|
22
|
+
return (React.createElement(ContrastingTooltip, { title: props.tooltip, placement: (_a = props.placement) !== null && _a !== void 0 ? _a : 'top', slotProps: {
|
|
23
|
+
popper: {
|
|
24
|
+
modifiers: [
|
|
25
|
+
{
|
|
26
|
+
name: 'offset',
|
|
27
|
+
options: {
|
|
28
|
+
offset: [0, -8]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
} },
|
|
34
|
+
React.createElement("span", { className: props.className },
|
|
35
|
+
React.createElement(IconButton, { ...props.iconButtonProps, onClick: props.onClick, disabled: props.disabled, sx: { lineHeight: 0, ...(props.disabled && { opacity: 0.5 }) }, "aria-label": props['aria-label'] }, props.children))));
|
|
36
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { IChatModel } from '../model';
|
|
3
4
|
type RendermimeMarkdownProps = {
|
|
4
5
|
markdownStr: string;
|
|
5
6
|
rmRegistry: IRenderMimeRegistry;
|
|
6
7
|
appendContent?: boolean;
|
|
8
|
+
model: IChatModel;
|
|
7
9
|
edit?: () => void;
|
|
8
10
|
delete?: () => void;
|
|
9
11
|
};
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import React, { useState, useEffect
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
5
|
+
import React, { useState, useEffect } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
import { CodeToolbar } from './code-blocks/code-toolbar';
|
|
8
8
|
import { MessageToolbar } from './toolbar';
|
|
9
9
|
const MD_MIME_TYPE = 'text/markdown';
|
|
10
10
|
const RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
|
|
@@ -21,7 +21,8 @@ function escapeLatexDelimiters(text) {
|
|
|
21
21
|
function RendermimeMarkdownBase(props) {
|
|
22
22
|
const appendContent = props.appendContent || false;
|
|
23
23
|
const [renderedContent, setRenderedContent] = useState(null);
|
|
24
|
-
|
|
24
|
+
// each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps].
|
|
25
|
+
const [codeToolbarDefns, setCodeToolbarDefns] = useState([]);
|
|
25
26
|
useEffect(() => {
|
|
26
27
|
const renderContent = async () => {
|
|
27
28
|
var _a;
|
|
@@ -32,23 +33,36 @@ function RendermimeMarkdownBase(props) {
|
|
|
32
33
|
const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
33
34
|
await renderer.renderModel(model);
|
|
34
35
|
(_a = props.rmRegistry.latexTypesetter) === null || _a === void 0 ? void 0 : _a.typeset(renderer.node);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
38
|
-
preBlocks.forEach(preBlock => {
|
|
39
|
-
var _a;
|
|
40
|
-
const copyButtonContainer = document.createElement('div');
|
|
41
|
-
(_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(copyButtonContainer, preBlock.nextSibling);
|
|
42
|
-
ReactDOM.render(React.createElement(CopyButton, { value: preBlock.textContent || '' }), copyButtonContainer);
|
|
43
|
-
});
|
|
36
|
+
if (!renderer.node) {
|
|
37
|
+
throw new Error('Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.');
|
|
44
38
|
}
|
|
39
|
+
const newCodeToolbarDefns = [];
|
|
40
|
+
// Attach CodeToolbar root element to each <pre> block
|
|
41
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
42
|
+
preBlocks.forEach(preBlock => {
|
|
43
|
+
var _a;
|
|
44
|
+
const codeToolbarRoot = document.createElement('div');
|
|
45
|
+
(_a = preBlock.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(codeToolbarRoot, preBlock.nextSibling);
|
|
46
|
+
newCodeToolbarDefns.push([
|
|
47
|
+
codeToolbarRoot,
|
|
48
|
+
{ model: props.model, content: preBlock.textContent || '' }
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
setCodeToolbarDefns(newCodeToolbarDefns);
|
|
45
52
|
setRenderedContent(renderer.node);
|
|
46
53
|
};
|
|
47
54
|
renderContent();
|
|
48
55
|
}, [props.markdownStr, props.rmRegistry]);
|
|
49
|
-
return (React.createElement("div", {
|
|
56
|
+
return (React.createElement("div", { className: RENDERMIME_MD_CLASS },
|
|
50
57
|
renderedContent &&
|
|
51
58
|
(appendContent ? (React.createElement("div", { ref: node => node && node.appendChild(renderedContent) })) : (React.createElement("div", { ref: node => node && node.replaceChildren(renderedContent) }))),
|
|
52
|
-
React.createElement(MessageToolbar, { edit: props.edit, delete: props.delete })
|
|
59
|
+
React.createElement(MessageToolbar, { edit: props.edit, delete: props.delete }),
|
|
60
|
+
// Render a `CodeToolbar` element underneath each code block.
|
|
61
|
+
// We use ReactDOM.createPortal() so each `CodeToolbar` element is able
|
|
62
|
+
// to use the context in the main React tree.
|
|
63
|
+
codeToolbarDefns.map(codeToolbarDefn => {
|
|
64
|
+
const [codeToolbarRoot, codeToolbarProps] = codeToolbarDefn;
|
|
65
|
+
return createPortal(React.createElement(CodeToolbar, { ...codeToolbarProps }), codeToolbarRoot);
|
|
66
|
+
})));
|
|
53
67
|
}
|
|
54
68
|
export const RendermimeMarkdown = React.memo(RendermimeMarkdownBase);
|
package/lib/icons.d.ts
CHANGED
package/lib/icons.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
7
7
|
import chatSvgStr from '../style/icons/chat.svg';
|
|
8
8
|
import readSvgStr from '../style/icons/read.svg';
|
|
9
|
+
import replaceCellSvg from '../style/icons/replace-cell.svg';
|
|
9
10
|
export const chatIcon = new LabIcon({
|
|
10
11
|
name: 'jupyter-chat::chat',
|
|
11
12
|
svgstr: chatSvgStr
|
|
@@ -14,3 +15,7 @@ export const readIcon = new LabIcon({
|
|
|
14
15
|
name: 'jupyter-chat::read',
|
|
15
16
|
svgstr: readSvgStr
|
|
16
17
|
});
|
|
18
|
+
export const replaceCellIcon = new LabIcon({
|
|
19
|
+
name: 'jupyter-ai::replace-cell',
|
|
20
|
+
svgstr: replaceCellSvg
|
|
21
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './icons';
|
|
|
2
2
|
export * from './model';
|
|
3
3
|
export * from './registry';
|
|
4
4
|
export * from './types';
|
|
5
|
+
export * from './active-cell-manager';
|
|
5
6
|
export * from './widgets/chat-error';
|
|
6
7
|
export * from './widgets/chat-sidebar';
|
|
7
8
|
export * from './widgets/chat-widget';
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './icons';
|
|
|
6
6
|
export * from './model';
|
|
7
7
|
export * from './registry';
|
|
8
8
|
export * from './types';
|
|
9
|
+
export * from './active-cell-manager';
|
|
9
10
|
export * from './widgets/chat-error';
|
|
10
11
|
export * from './widgets/chat-sidebar';
|
|
11
12
|
export * from './widgets/chat-widget';
|
package/lib/model.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
2
2
|
import { IDisposable } from '@lumino/disposable';
|
|
3
3
|
import { ISignal } from '@lumino/signaling';
|
|
4
4
|
import { IChatHistory, INewMessage, IChatMessage, IConfig, IUser } from './types';
|
|
5
|
+
import { IActiveCellManager } from './active-cell-manager';
|
|
5
6
|
/**
|
|
6
7
|
* The chat model interface.
|
|
7
8
|
*/
|
|
@@ -30,10 +31,18 @@ export interface IChatModel extends IDisposable {
|
|
|
30
31
|
* The chat messages list.
|
|
31
32
|
*/
|
|
32
33
|
readonly messages: IChatMessage[];
|
|
34
|
+
/**
|
|
35
|
+
* Get the active cell manager.
|
|
36
|
+
*/
|
|
37
|
+
readonly activeCellManager: IActiveCellManager | null;
|
|
33
38
|
/**
|
|
34
39
|
* A signal emitting when the messages list is updated.
|
|
35
40
|
*/
|
|
36
41
|
readonly messagesUpdated: ISignal<IChatModel, void>;
|
|
42
|
+
/**
|
|
43
|
+
* A signal emitting when the messages list is updated.
|
|
44
|
+
*/
|
|
45
|
+
get configChanged(): ISignal<IChatModel, IConfig>;
|
|
37
46
|
/**
|
|
38
47
|
* A signal emitting when unread messages change.
|
|
39
48
|
*/
|
|
@@ -110,6 +119,7 @@ export declare class ChatModel implements IChatModel {
|
|
|
110
119
|
* The chat messages list.
|
|
111
120
|
*/
|
|
112
121
|
get messages(): IChatMessage[];
|
|
122
|
+
get activeCellManager(): IActiveCellManager | null;
|
|
113
123
|
/**
|
|
114
124
|
* The chat model id.
|
|
115
125
|
*/
|
|
@@ -144,6 +154,10 @@ export declare class ChatModel implements IChatModel {
|
|
|
144
154
|
* A signal emitting when the messages list is updated.
|
|
145
155
|
*/
|
|
146
156
|
get messagesUpdated(): ISignal<IChatModel, void>;
|
|
157
|
+
/**
|
|
158
|
+
* A signal emitting when the messages list is updated.
|
|
159
|
+
*/
|
|
160
|
+
get configChanged(): ISignal<IChatModel, IConfig>;
|
|
147
161
|
/**
|
|
148
162
|
* A signal emitting when unread messages change.
|
|
149
163
|
*/
|
|
@@ -216,8 +230,10 @@ export declare class ChatModel implements IChatModel {
|
|
|
216
230
|
private _config;
|
|
217
231
|
private _isDisposed;
|
|
218
232
|
private _commands?;
|
|
233
|
+
private _activeCellManager;
|
|
219
234
|
private _notificationId;
|
|
220
235
|
private _messagesUpdated;
|
|
236
|
+
private _configChanged;
|
|
221
237
|
private _unreadChanged;
|
|
222
238
|
private _viewportChanged;
|
|
223
239
|
}
|
|
@@ -237,5 +253,9 @@ export declare namespace ChatModel {
|
|
|
237
253
|
* Commands registry.
|
|
238
254
|
*/
|
|
239
255
|
commands?: CommandRegistry;
|
|
256
|
+
/**
|
|
257
|
+
* Active cell manager
|
|
258
|
+
*/
|
|
259
|
+
activeCellManager?: IActiveCellManager | null;
|
|
240
260
|
}
|
|
241
261
|
}
|
package/lib/model.js
CHANGED
|
@@ -13,7 +13,7 @@ export class ChatModel {
|
|
|
13
13
|
* Create a new chat model.
|
|
14
14
|
*/
|
|
15
15
|
constructor(options = {}) {
|
|
16
|
-
var _a;
|
|
16
|
+
var _a, _b;
|
|
17
17
|
this._messages = [];
|
|
18
18
|
this._unreadMessages = [];
|
|
19
19
|
this._messagesInViewport = [];
|
|
@@ -21,12 +21,14 @@ export class ChatModel {
|
|
|
21
21
|
this._isDisposed = false;
|
|
22
22
|
this._notificationId = null;
|
|
23
23
|
this._messagesUpdated = new Signal(this);
|
|
24
|
+
this._configChanged = new Signal(this);
|
|
24
25
|
this._unreadChanged = new Signal(this);
|
|
25
26
|
this._viewportChanged = new Signal(this);
|
|
26
27
|
const config = (_a = options.config) !== null && _a !== void 0 ? _a : {};
|
|
27
28
|
// Stack consecutive messages from the same user by default.
|
|
28
29
|
this._config = { stackMessages: true, ...config };
|
|
29
30
|
this._commands = options.commands;
|
|
31
|
+
this._activeCellManager = (_b = options.activeCellManager) !== null && _b !== void 0 ? _b : null;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* The chat messages list.
|
|
@@ -34,6 +36,9 @@ export class ChatModel {
|
|
|
34
36
|
get messages() {
|
|
35
37
|
return this._messages;
|
|
36
38
|
}
|
|
39
|
+
get activeCellManager() {
|
|
40
|
+
return this._activeCellManager;
|
|
41
|
+
}
|
|
37
42
|
/**
|
|
38
43
|
* The chat model id.
|
|
39
44
|
*/
|
|
@@ -83,6 +88,8 @@ export class ChatModel {
|
|
|
83
88
|
const unreadNotificationsChanged = 'unreadNotifications' in value &&
|
|
84
89
|
this._config.unreadNotifications !== value.unreadNotifications;
|
|
85
90
|
this._config = { ...this._config, ...value };
|
|
91
|
+
this._configChanged.emit(this._config);
|
|
92
|
+
// Update the stacked status of the messages and the view.
|
|
86
93
|
if (stackMessagesChanged) {
|
|
87
94
|
if (this._config.stackMessages) {
|
|
88
95
|
this._messages.slice(1).forEach((message, idx) => {
|
|
@@ -147,6 +154,12 @@ export class ChatModel {
|
|
|
147
154
|
get messagesUpdated() {
|
|
148
155
|
return this._messagesUpdated;
|
|
149
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A signal emitting when the messages list is updated.
|
|
159
|
+
*/
|
|
160
|
+
get configChanged() {
|
|
161
|
+
return this._configChanged;
|
|
162
|
+
}
|
|
150
163
|
/**
|
|
151
164
|
* A signal emitting when unread messages change.
|
|
152
165
|
*/
|
package/lib/types.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface IConfig {
|
|
|
29
29
|
* Whether to enable or not the notifications on unread messages.
|
|
30
30
|
*/
|
|
31
31
|
unreadNotifications?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to enable or not the code toolbar.
|
|
34
|
+
*/
|
|
35
|
+
enableCodeToolbar?: boolean;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* The chat message description.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -55,9 +55,11 @@
|
|
|
55
55
|
"@emotion/react": "^11.10.5",
|
|
56
56
|
"@emotion/styled": "^11.10.5",
|
|
57
57
|
"@jupyter/react-components": "^0.15.2",
|
|
58
|
-
"@jupyterlab/
|
|
59
|
-
"@jupyterlab/
|
|
60
|
-
"@jupyterlab/
|
|
58
|
+
"@jupyterlab/application": "^4.2.0",
|
|
59
|
+
"@jupyterlab/apputils": "^4.3.0",
|
|
60
|
+
"@jupyterlab/notebook": "^4.2.0",
|
|
61
|
+
"@jupyterlab/rendermime": "^4.2.0",
|
|
62
|
+
"@jupyterlab/ui-components": "^4.2.0",
|
|
61
63
|
"@lumino/commands": "^2.0.0",
|
|
62
64
|
"@lumino/disposable": "^2.0.0",
|
|
63
65
|
"@lumino/signaling": "^2.0.0",
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { JupyterFrontEnd, LabShell } from '@jupyterlab/application';
|
|
7
|
+
import { Cell, ICellModel } from '@jupyterlab/cells';
|
|
8
|
+
import { IChangedArgs } from '@jupyterlab/coreutils';
|
|
9
|
+
import { INotebookTracker, NotebookActions } from '@jupyterlab/notebook';
|
|
10
|
+
import { IError as CellError } from '@jupyterlab/nbformat';
|
|
11
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
12
|
+
|
|
13
|
+
type CellContent = {
|
|
14
|
+
type: string;
|
|
15
|
+
source: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CellWithErrorContent = {
|
|
19
|
+
type: 'code';
|
|
20
|
+
source: string;
|
|
21
|
+
error: {
|
|
22
|
+
name: string;
|
|
23
|
+
value: string;
|
|
24
|
+
traceback: string[];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface IActiveCellManager {
|
|
29
|
+
/**
|
|
30
|
+
* Whether the notebook is available and an active cell exists.
|
|
31
|
+
*/
|
|
32
|
+
readonly available: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The `CellError` output within the active cell, if any.
|
|
35
|
+
*/
|
|
36
|
+
readonly activeCellError: CellError | null;
|
|
37
|
+
/**
|
|
38
|
+
* A signal emitting when the active cell changed.
|
|
39
|
+
*/
|
|
40
|
+
readonly availabilityChanged: ISignal<this, boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* A signal emitting when the error state of the active cell changed.
|
|
43
|
+
*/
|
|
44
|
+
readonly activeCellErrorChanged: ISignal<this, CellError | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Returns an `ActiveCellContent` object that describes the current active
|
|
47
|
+
* cell. If no active cell exists, this method returns `null`.
|
|
48
|
+
*
|
|
49
|
+
* When called with `withError = true`, this method returns `null` if the
|
|
50
|
+
* active cell does not have an error output. Otherwise it returns an
|
|
51
|
+
* `ActiveCellContentWithError` object that describes both the active cell and
|
|
52
|
+
* the error output.
|
|
53
|
+
*/
|
|
54
|
+
getContent(withError: boolean): CellContent | CellWithErrorContent | null;
|
|
55
|
+
/**
|
|
56
|
+
* Inserts `content` in a new cell above the active cell.
|
|
57
|
+
*/
|
|
58
|
+
insertAbove(content: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Inserts `content` in a new cell below the active cell.
|
|
61
|
+
*/
|
|
62
|
+
insertBelow(content: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Replaces the contents of the active cell.
|
|
65
|
+
*/
|
|
66
|
+
replace(content: string): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The active cell manager namespace.
|
|
71
|
+
*/
|
|
72
|
+
export namespace ActiveCellManager {
|
|
73
|
+
/**
|
|
74
|
+
* The constructor options.
|
|
75
|
+
*/
|
|
76
|
+
export interface IOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The notebook tracker.
|
|
79
|
+
*/
|
|
80
|
+
tracker: INotebookTracker;
|
|
81
|
+
/**
|
|
82
|
+
* The current shell of the application.
|
|
83
|
+
*/
|
|
84
|
+
shell: JupyterFrontEnd.IShell;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A manager that maintains a reference to the current active notebook cell in
|
|
90
|
+
* the main panel (if any), and provides methods for inserting or appending
|
|
91
|
+
* content to the active cell.
|
|
92
|
+
*
|
|
93
|
+
* The current active cell should be obtained by listening to the
|
|
94
|
+
* `activeCellChanged` signal.
|
|
95
|
+
*/
|
|
96
|
+
export class ActiveCellManager implements IActiveCellManager {
|
|
97
|
+
constructor(options: ActiveCellManager.IOptions) {
|
|
98
|
+
this._notebookTracker = options.tracker;
|
|
99
|
+
this._notebookTracker.activeCellChanged.connect(this._onActiveCellChanged);
|
|
100
|
+
options.shell.currentChanged?.connect(this._onMainAreaChanged);
|
|
101
|
+
if (options.shell instanceof LabShell) {
|
|
102
|
+
options.shell.layoutModified?.connect(this._onMainAreaChanged);
|
|
103
|
+
}
|
|
104
|
+
this._onMainAreaChanged();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the notebook is available and an active cell exists.
|
|
109
|
+
*/
|
|
110
|
+
get available(): boolean {
|
|
111
|
+
return this._available;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The `CellError` output within the active cell, if any.
|
|
116
|
+
*/
|
|
117
|
+
get activeCellError(): CellError | null {
|
|
118
|
+
return this._activeCellError;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A signal emitting when the active cell changed.
|
|
123
|
+
*/
|
|
124
|
+
get availabilityChanged(): ISignal<this, boolean> {
|
|
125
|
+
return this._availabilityChanged;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A signal emitting when the error state of the active cell changed.
|
|
130
|
+
*/
|
|
131
|
+
get activeCellErrorChanged(): ISignal<this, CellError | null> {
|
|
132
|
+
return this._activeCellErrorChanged;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns an `ActiveCellContent` object that describes the current active
|
|
137
|
+
* cell. If no active cell exists, this method returns `null`.
|
|
138
|
+
*
|
|
139
|
+
* When called with `withError = true`, this method returns `null` if the
|
|
140
|
+
* active cell does not have an error output. Otherwise it returns an
|
|
141
|
+
* `ActiveCellContentWithError` object that describes both the active cell and
|
|
142
|
+
* the error output.
|
|
143
|
+
*/
|
|
144
|
+
getContent(withError: false): CellContent | null;
|
|
145
|
+
getContent(withError: true): CellWithErrorContent | null;
|
|
146
|
+
getContent(withError = false): CellContent | CellWithErrorContent | null {
|
|
147
|
+
const sharedModel = this._notebookTracker.activeCell?.model.sharedModel;
|
|
148
|
+
if (!sharedModel) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// case where withError = false
|
|
153
|
+
if (!withError) {
|
|
154
|
+
return {
|
|
155
|
+
type: sharedModel.cell_type,
|
|
156
|
+
source: sharedModel.getSource()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// case where withError = true
|
|
161
|
+
const error = this._activeCellError;
|
|
162
|
+
if (error) {
|
|
163
|
+
return {
|
|
164
|
+
type: 'code',
|
|
165
|
+
source: sharedModel.getSource(),
|
|
166
|
+
error: {
|
|
167
|
+
name: error.ename,
|
|
168
|
+
value: error.evalue,
|
|
169
|
+
traceback: error.traceback
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Inserts `content` in a new cell above the active cell.
|
|
179
|
+
*/
|
|
180
|
+
insertAbove(content: string): void {
|
|
181
|
+
const notebookPanel = this._notebookTracker.currentWidget;
|
|
182
|
+
if (!notebookPanel || !notebookPanel.isVisible) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// create a new cell above the active cell and mark new cell as active
|
|
187
|
+
NotebookActions.insertAbove(notebookPanel.content);
|
|
188
|
+
// replace content of this new active cell
|
|
189
|
+
this.replace(content);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Inserts `content` in a new cell below the active cell.
|
|
194
|
+
*/
|
|
195
|
+
insertBelow(content: string): void {
|
|
196
|
+
const notebookPanel = this._notebookTracker.currentWidget;
|
|
197
|
+
if (!notebookPanel || !notebookPanel.isVisible) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// create a new cell below the active cell and mark new cell as active
|
|
202
|
+
NotebookActions.insertBelow(notebookPanel.content);
|
|
203
|
+
// replace content of this new active cell
|
|
204
|
+
this.replace(content);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Replaces the contents of the active cell.
|
|
209
|
+
*/
|
|
210
|
+
async replace(content: string): Promise<void> {
|
|
211
|
+
const notebookPanel = this._notebookTracker.currentWidget;
|
|
212
|
+
if (!notebookPanel || !notebookPanel.isVisible) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// get reference to active cell directly from Notebook API. this avoids the
|
|
216
|
+
// possibility of acting on an out-of-date reference.
|
|
217
|
+
const activeCell = this._notebookTracker.activeCell;
|
|
218
|
+
if (!activeCell) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// wait for editor to be ready
|
|
223
|
+
await activeCell.ready;
|
|
224
|
+
|
|
225
|
+
// replace the content of the active cell
|
|
226
|
+
/**
|
|
227
|
+
* NOTE: calling this method sometimes emits an error to the browser console:
|
|
228
|
+
*
|
|
229
|
+
* ```
|
|
230
|
+
* Error: Calls to EditorView.update are not allowed while an update is in progress
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* However, there seems to be no impact on the behavior/stability of the
|
|
234
|
+
* JupyterLab application after this error is logged. Furthermore, this is
|
|
235
|
+
* the official API for setting the content of a cell in JupyterLab 4,
|
|
236
|
+
* meaning that this is likely unavoidable.
|
|
237
|
+
*/
|
|
238
|
+
activeCell.editor?.model.sharedModel.setSource(content);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private _onMainAreaChanged = () => {
|
|
242
|
+
const value = this._notebookTracker.currentWidget?.isVisible ?? false;
|
|
243
|
+
if (value !== this._notebookVisible) {
|
|
244
|
+
this._notebookVisible = value;
|
|
245
|
+
this._available = !!this._activeCell && this._notebookVisible;
|
|
246
|
+
this._availabilityChanged.emit(this._available);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle the change of active notebook cell.
|
|
252
|
+
*/
|
|
253
|
+
private _onActiveCellChanged = (
|
|
254
|
+
_: INotebookTracker,
|
|
255
|
+
activeCell: Cell<ICellModel> | null
|
|
256
|
+
): void => {
|
|
257
|
+
if (this._activeCell !== activeCell) {
|
|
258
|
+
this._activeCell?.model.stateChanged.disconnect(this._cellStateChange);
|
|
259
|
+
this._activeCell = activeCell;
|
|
260
|
+
|
|
261
|
+
activeCell?.ready.then(() => {
|
|
262
|
+
this._activeCell?.model.stateChanged.connect(this._cellStateChange);
|
|
263
|
+
this._available = !!this._activeCell && this._notebookVisible;
|
|
264
|
+
this._availabilityChanged.emit(this._available);
|
|
265
|
+
this._activeCell?.disposed.connect(() => {
|
|
266
|
+
this._activeCell = null;
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Handle the change of the active cell state.
|
|
274
|
+
*/
|
|
275
|
+
private _cellStateChange = (
|
|
276
|
+
_: ICellModel,
|
|
277
|
+
change: IChangedArgs<boolean, boolean, any>
|
|
278
|
+
): void => {
|
|
279
|
+
if (change.name === 'executionCount') {
|
|
280
|
+
const currSharedModel = this._activeCell?.model.sharedModel;
|
|
281
|
+
const prevActiveCellError = this._activeCellError;
|
|
282
|
+
let currActiveCellError: CellError | null = null;
|
|
283
|
+
if (currSharedModel && 'outputs' in currSharedModel) {
|
|
284
|
+
currActiveCellError =
|
|
285
|
+
currSharedModel.outputs.find<CellError>(
|
|
286
|
+
(output): output is CellError => output.output_type === 'error'
|
|
287
|
+
) || null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// for some reason, the `CellError` object is not referentially stable,
|
|
291
|
+
// meaning that this condition always evaluates to `true` and the
|
|
292
|
+
// `activeCellErrorChanged` signal is emitted every 200ms, even when the
|
|
293
|
+
// error output is unchanged. this is why we have to rely on
|
|
294
|
+
// `execution_count` to track changes to the error output.
|
|
295
|
+
if (prevActiveCellError !== currActiveCellError) {
|
|
296
|
+
this._activeCellError = currActiveCellError;
|
|
297
|
+
this._activeCellErrorChanged.emit(this._activeCellError);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The notebook tracker.
|
|
304
|
+
*/
|
|
305
|
+
private _notebookTracker: INotebookTracker;
|
|
306
|
+
/**
|
|
307
|
+
* Whether the current notebook panel is visible or not.
|
|
308
|
+
*/
|
|
309
|
+
private _notebookVisible: boolean = false;
|
|
310
|
+
/**
|
|
311
|
+
* The active cell.
|
|
312
|
+
*/
|
|
313
|
+
private _activeCell: Cell | null = null;
|
|
314
|
+
private _available: boolean = false;
|
|
315
|
+
private _activeCellError: CellError | null = null;
|
|
316
|
+
private _availabilityChanged = new Signal<this, boolean>(this);
|
|
317
|
+
private _activeCellErrorChanged = new Signal<this, CellError | null>(this);
|
|
318
|
+
}
|