From a0477811a68494a79930b766ac4c128b1a895a19 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 12:14:57 +0100 Subject: [PATCH] Remove code-toolbar by using a simplified markdown renderer in settings --- .../src/components/chat-settings.tsx | 4 +- .../components/code-blocks/code-toolbar.tsx | 197 ------------------ .../src/components/rendermime-markdown.tsx | 141 ------------- .../settings/rendermime-markdown.tsx | 79 +++++++ 4 files changed, 80 insertions(+), 341 deletions(-) delete mode 100644 packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx delete mode 100644 packages/jupyter-ai/src/components/rendermime-markdown.tsx create mode 100644 packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx diff --git a/packages/jupyter-ai/src/components/chat-settings.tsx b/packages/jupyter-ai/src/components/chat-settings.tsx index 5922bcff1..8d936c46a 100644 --- a/packages/jupyter-ai/src/components/chat-settings.tsx +++ b/packages/jupyter-ai/src/components/chat-settings.tsx @@ -26,7 +26,7 @@ import { ExistingApiKeys } from './settings/existing-api-keys'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { minifyUpdate } from './settings/minify'; import { useStackingAlert } from './mui-extras/stacking-alert'; -import { RendermimeMarkdown } from './rendermime-markdown'; +import { RendermimeMarkdown } from './settings/rendermime-markdown'; import { IJaiCompletionProvider } from '../tokens'; import { getProviderId, getModelLocalId } from '../utils'; @@ -375,7 +375,6 @@ export function ChatSettings(props: ChatSettingsProps): JSX.Element { )} {lmGlobalId && ( @@ -491,7 +490,6 @@ export function ChatSettings(props: ChatSettingsProps): JSX.Element { )} {clmGlobalId && ( diff --git a/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx b/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx deleted file mode 100644 index 315e5d4d6..000000000 --- a/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { - addAboveIcon, - addBelowIcon, - copyIcon -} from '@jupyterlab/ui-components'; -import { replaceCellIcon } from '../../icons'; - -import { - ActiveCellManager, - useActiveCellContext -} from '../../contexts/active-cell-context'; -import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button'; -import { useReplace } from '../../hooks/use-replace'; -import { useCopy } from '../../hooks/use-copy'; -import { AiService } from '../../handler'; -import { useTelemetry } from '../../contexts/telemetry-context'; -import { TelemetryEvent } from '../../tokens'; - -export type CodeToolbarProps = { - /** - * The content of the Markdown code block this component is attached to. - */ - code: string; - /** - * Parent message which contains the code referenced by `content`. - */ - parentMessage?: AiService.ChatMessage; -}; - -export function CodeToolbar(props: CodeToolbarProps): JSX.Element { - const activeCell = useActiveCellContext(); - const sharedToolbarButtonProps: ToolbarButtonProps = { - code: props.code, - activeCellManager: activeCell.manager, - activeCellExists: activeCell.exists, - parentMessage: props.parentMessage - }; - - return ( - - - - - - - ); -} - -type ToolbarButtonProps = { - code: string; - activeCellExists: boolean; - activeCellManager: ActiveCellManager; - parentMessage?: AiService.ChatMessage; - // TODO: parentMessage should always be defined, but this can be undefined - // when the code toolbar appears in Markdown help messages in the Settings - // UI. The Settings UI should use a different component to render Markdown, - // and should never render code toolbars within it. -}; - -function buildTelemetryEvent( - type: string, - props: ToolbarButtonProps -): TelemetryEvent { - const charCount = props.code.length; - // number of lines = number of newlines + 1 - const lineCount = (props.code.match(/\n/g) ?? []).length + 1; - - return { - type, - message: { - id: props.parentMessage?.id ?? '', - type: props.parentMessage?.type ?? 'human', - time: props.parentMessage?.time ?? 0, - metadata: - props.parentMessage && 'metadata' in props.parentMessage - ? props.parentMessage.metadata - : {} - }, - code: { - charCount, - lineCount - } - }; -} - -function InsertAboveButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const tooltip = props.activeCellExists - ? 'Insert above active cell' - : 'Insert above active cell (no active cell)'; - - return ( - { - props.activeCellManager.insertAbove(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('insert-above', props)); - } catch (e) { - console.error(e); - return; - } - }} - disabled={!props.activeCellExists} - > - - - ); -} - -function InsertBelowButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const tooltip = props.activeCellExists - ? 'Insert below active cell' - : 'Insert below active cell (no active cell)'; - - return ( - { - props.activeCellManager.insertBelow(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('insert-below', props)); - } catch (e) { - console.error(e); - return; - } - }} - > - - - ); -} - -function ReplaceButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const { replace, replaceDisabled, replaceLabel } = useReplace(); - - return ( - { - replace(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('replace', props)); - } catch (e) { - console.error(e); - return; - } - }} - > - - - ); -} - -export function CopyButton(props: ToolbarButtonProps): JSX.Element { - const telemetryHandler = useTelemetry(); - const { copy, copyLabel } = useCopy(); - - return ( - { - copy(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('copy', props)); - } catch (e) { - console.error(e); - return; - } - }} - aria-label="Copy to clipboard" - > - - - ); -} diff --git a/packages/jupyter-ai/src/components/rendermime-markdown.tsx b/packages/jupyter-ai/src/components/rendermime-markdown.tsx deleted file mode 100644 index 9a0278517..000000000 --- a/packages/jupyter-ai/src/components/rendermime-markdown.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { createPortal } from 'react-dom'; - -import { CodeToolbar, CodeToolbarProps } from './code-blocks/code-toolbar'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { AiService } from '../handler'; - -const MD_MIME_TYPE = 'text/markdown'; -const RENDERMIME_MD_CLASS = 'jp-ai-rendermime-markdown'; - -type RendermimeMarkdownProps = { - markdownStr: string; - rmRegistry: IRenderMimeRegistry; - /** - * Reference to the parent message object in the Jupyter AI chat. - */ - parentMessage?: AiService.ChatMessage; - /** - * Whether the message is complete. This is generally `true` except in the - * case where `markdownStr` contains the incomplete contents of a - * `AgentStreamMessage`, in which case this should be set to `false`. - */ - complete: boolean; -}; - -/** - * Escapes backslashes in LaTeX delimiters such that they appear in the DOM - * after the initial MarkDown render. For example, this function takes '\(` and - * returns `\\(`. - * - * Required for proper rendering of MarkDown + LaTeX markup in the chat by - * `ILatexTypesetter`. - */ -function escapeLatexDelimiters(text: string) { - return text - .replace(/\\\(/g, '\\\\(') - .replace(/\\\)/g, '\\\\)') - .replace(/\\\[/g, '\\\\[') - .replace(/\\\]/g, '\\\\]'); -} - -function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element { - // create a single renderer object at component mount - const [renderer] = useState(() => { - return props.rmRegistry.createRenderer(MD_MIME_TYPE); - }); - - // ref that tracks the content container to store the rendermime node in - const renderingContainer = useRef(null); - // ref that tracks whether the rendermime node has already been inserted - const renderingInserted = useRef(false); - - // each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps]. - const [codeToolbarDefns, setCodeToolbarDefns] = useState< - Array<[HTMLDivElement, CodeToolbarProps]> - >([]); - - /** - * Effect: use Rendermime to render `props.markdownStr` into an HTML element, - * and insert it into `renderingContainer` if not yet inserted. When the - * message is completed, add code toolbars. - */ - useEffect(() => { - const renderContent = async () => { - // initialize mime model - const mdStr = escapeLatexDelimiters(props.markdownStr); - const model = props.rmRegistry.createModel({ - data: { [MD_MIME_TYPE]: mdStr } - }); - - // step 1: render markdown - await renderer.renderModel(model); - if (!renderer.node) { - throw new Error( - 'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter AI on GitHub.' - ); - } - - // step 2: render LaTeX via MathJax - props.rmRegistry.latexTypesetter?.typeset(renderer.node); - - // insert the rendering into renderingContainer if not yet inserted - if (renderingContainer.current !== null && !renderingInserted.current) { - renderingContainer.current.appendChild(renderer.node); - renderingInserted.current = true; - } - - // if complete, render code toolbars - if (!props.complete) { - return; - } - const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = []; - - // Attach CodeToolbar root element to each
 block
-      const preBlocks = renderer.node.querySelectorAll('pre');
-      preBlocks.forEach(preBlock => {
-        const codeToolbarRoot = document.createElement('div');
-        preBlock.parentNode?.insertBefore(
-          codeToolbarRoot,
-          preBlock.nextSibling
-        );
-        newCodeToolbarDefns.push([
-          codeToolbarRoot,
-          {
-            code: preBlock.textContent || '',
-            parentMessage: props.parentMessage
-          }
-        ]);
-      });
-
-      setCodeToolbarDefns(newCodeToolbarDefns);
-    };
-
-    renderContent();
-  }, [
-    props.markdownStr,
-    props.complete,
-    props.rmRegistry,
-    props.parentMessage
-  ]);
-
-  return (
-    
-
- { - // Render a `CodeToolbar` element underneath each code block. - // We use ReactDOM.createPortal() so each `CodeToolbar` element is able - // to use the context in the main React tree. - codeToolbarDefns.map(codeToolbarDefn => { - const [codeToolbarRoot, codeToolbarProps] = codeToolbarDefn; - return createPortal( - , - codeToolbarRoot - ); - }) - } -
- ); -} - -export const RendermimeMarkdown = React.memo(RendermimeMarkdownBase); diff --git a/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx b/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx new file mode 100644 index 000000000..9d600cac4 --- /dev/null +++ b/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; + +const MD_MIME_TYPE = 'text/markdown'; +const RENDERMIME_MD_CLASS = 'jp-ai-rendermime-markdown'; + +type RendermimeMarkdownProps = { + markdownStr: string; + rmRegistry: IRenderMimeRegistry; +}; + +/** + * Escapes backslashes in LaTeX delimiters such that they appear in the DOM + * after the initial MarkDown render. For example, this function takes '\(` and + * returns `\\(`. + * + * Required for proper rendering of MarkDown + LaTeX markup in the chat by + * `ILatexTypesetter`. + */ +function escapeLatexDelimiters(text: string) { + return text + .replace(/\\\(/g, '\\\\(') + .replace(/\\\)/g, '\\\\)') + .replace(/\\\[/g, '\\\\[') + .replace(/\\\]/g, '\\\\]'); +} + +export function RendermimeMarkdown( + props: RendermimeMarkdownProps +): JSX.Element { + // create a single renderer object at component mount + const [renderer] = useState(() => { + return props.rmRegistry.createRenderer(MD_MIME_TYPE); + }); + + // ref that tracks the content container to store the rendermime node in + const renderingContainer = useRef(null); + // ref that tracks whether the rendermime node has already been inserted + const renderingInserted = useRef(false); + + /** + * Effect: use Rendermime to render `props.markdownStr` into an HTML element, + * and insert it into `renderingContainer` if not yet inserted. + */ + useEffect(() => { + const renderContent = async () => { + // initialize mime model + const mdStr = escapeLatexDelimiters(props.markdownStr); + const model = props.rmRegistry.createModel({ + data: { [MD_MIME_TYPE]: mdStr } + }); + + // step 1: render markdown + await renderer.renderModel(model); + if (!renderer.node) { + throw new Error( + 'Rendermime was unable to render Markdown content. Please report this upstream to Jupyter AI on GitHub.' + ); + } + + // step 2: render LaTeX via MathJax + props.rmRegistry.latexTypesetter?.typeset(renderer.node); + + // insert the rendering into renderingContainer if not yet inserted + if (renderingContainer.current !== null && !renderingInserted.current) { + renderingContainer.current.appendChild(renderer.node); + renderingInserted.current = true; + } + }; + + renderContent(); + }, [props.markdownStr]); + + return ( +
+
+
+ ); +}