@jupyter/chat 0.2.0 → 0.3.1

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 (40) 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 +5 -4
  4. package/lib/components/chat-input.js +11 -4
  5. package/lib/components/chat-messages.js +2 -3
  6. package/lib/components/chat.js +1 -2
  7. package/lib/components/code-blocks/code-toolbar.d.ts +13 -0
  8. package/lib/components/code-blocks/code-toolbar.js +70 -0
  9. package/lib/components/{copy-button.d.ts → code-blocks/copy-button.d.ts} +1 -0
  10. package/lib/components/code-blocks/copy-button.js +43 -0
  11. package/lib/components/mui-extras/contrasting-tooltip.d.ts +6 -0
  12. package/lib/components/mui-extras/contrasting-tooltip.js +21 -0
  13. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +35 -0
  14. package/lib/components/mui-extras/tooltipped-icon-button.js +36 -0
  15. package/lib/components/rendermime-markdown.d.ts +2 -0
  16. package/lib/components/rendermime-markdown.js +29 -15
  17. package/lib/icons.d.ts +1 -0
  18. package/lib/icons.js +5 -0
  19. package/lib/index.d.ts +1 -0
  20. package/lib/index.js +1 -0
  21. package/lib/model.d.ts +20 -0
  22. package/lib/model.js +14 -1
  23. package/lib/types.d.ts +4 -0
  24. package/package.json +6 -4
  25. package/src/active-cell-manager.ts +318 -0
  26. package/src/components/chat-input.tsx +17 -8
  27. package/src/components/chat-messages.tsx +3 -2
  28. package/src/components/chat.tsx +1 -1
  29. package/src/components/code-blocks/code-toolbar.tsx +143 -0
  30. package/src/components/code-blocks/copy-button.tsx +68 -0
  31. package/src/components/mui-extras/contrasting-tooltip.tsx +27 -0
  32. package/src/components/mui-extras/tooltipped-icon-button.tsx +84 -0
  33. package/src/components/rendermime-markdown.tsx +44 -20
  34. package/src/icons.ts +6 -0
  35. package/src/index.ts +1 -0
  36. package/src/model.ts +33 -0
  37. package/src/types.ts +4 -0
  38. package/style/icons/replace-cell.svg +8 -0
  39. package/lib/components/copy-button.js +0 -35
  40. 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, useRef } from 'react';
6
- import ReactDOM from 'react-dom';
7
- import { CopyButton } from './copy-button';
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
- const containerRef = useRef(null);
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
- // Attach CopyButton to each <pre> block
36
- if (containerRef.current && renderer.node) {
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", { ref: containerRef, className: RENDERMIME_MD_CLASS },
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
@@ -1,3 +1,4 @@
1
1
  import { LabIcon } from '@jupyterlab/ui-components';
2
2
  export declare const chatIcon: LabIcon;
3
3
  export declare const readIcon: LabIcon;
4
+ export declare const replaceCellIcon: LabIcon;
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.2.0",
3
+ "version": "0.3.1",
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/apputils": "^4.0.0",
59
- "@jupyterlab/rendermime": "^4.0.0",
60
- "@jupyterlab/ui-components": "^4.0.0",
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
+ }