@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
@@ -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 { AutocompleteCommand, IAutocompletionCommandsProps } from '../types';
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, sendWithShiftEnter } =
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 = props.sendWithShiftEnter ? (
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(_: IChatModel) {
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
- sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
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
  />
@@ -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
- sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
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, 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
  }
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
@@ -35,6 +35,10 @@ export interface IConfig {
35
35
  * Whether to enable or not the notifications on unread messages.
36
36
  */
37
37
  unreadNotifications?: boolean;
38
+ /**
39
+ * Whether to enable or not the code toolbar.
40
+ */
41
+ enableCodeToolbar?: boolean;
38
42
  }
39
43
 
40
44
  /**
@@ -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>