Skip to content

Add a welcome message #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/jupyter-chat-example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { UUID } from '@lumino/coreutils';

const welcomeMessage = `
### Welcome to Jupyter Chat!

This is a simple chat panel that allows you to send messages and share files.

The messages are rendered as markdown, allowing you to format the messages with e.g.:
- **bold**
- _italic_
- \`\`\`python
# This is a code block
print('Hello world!')
\`\`\`
- [link](https://jupyter.org)
`;

class ChatContext extends AbstractChatContext {
get users() {
return [];
Expand Down Expand Up @@ -138,7 +153,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
model,
rmRegistry,
themeManager,
attachmentOpenerRegistry
attachmentOpenerRegistry,
welcomeMessage
});
app.shell.add(panel, 'left');
}
Expand Down
15 changes: 13 additions & 2 deletions packages/jupyter-chat/src/components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import React, { useEffect, useState, useRef, forwardRef } from 'react';
import { AttachmentPreviewList } from './attachments';
import { ChatInput } from './chat-input';
import { IInputToolbarRegistry } from './input';
import { MarkdownRenderer } from './markdown-renderer';
import { MessageFooter } from './messages/footer';
import { MessageRenderer } from './messages/message-renderer';
import { WelcomeMessage } from './messages/welcome';
import { ScrollContainer } from './scroll-container';
import { IChatCommandRegistry } from '../chat-commands';
import { IMessageFooterRegistry } from '../footers';
Expand Down Expand Up @@ -64,6 +65,10 @@ type BaseMessageProps = {
* The footer registry.
*/
messageFooterRegistry?: IMessageFooterRegistry;
/**
* The welcome message.
*/
welcomeMessage?: string;
};

/**
Expand Down Expand Up @@ -184,6 +189,12 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
return (
<>
<ScrollContainer sx={{ flexGrow: 1 }}>
{props.welcomeMessage && (
<WelcomeMessage
rmRegistry={props.rmRegistry}
content={props.welcomeMessage}
/>
)}
<Box ref={refMsgBox} className={clsx(MESSAGES_BOX_CLASS)}>
{messages.map((message, i) => {
renderedPromise.current[i] = new PromiseDelegate();
Expand Down Expand Up @@ -463,7 +474,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
toolbarRegistry={props.inputToolbarRegistry}
/>
) : (
<MarkdownRenderer
<MessageRenderer
rmRegistry={rmRegistry}
markdownStr={message.body}
model={model}
Expand Down
5 changes: 5 additions & 0 deletions packages/jupyter-chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
chatCommandRegistry={props.chatCommandRegistry}
inputToolbarRegistry={inputToolbarRegistry}
messageFooterRegistry={props.messageFooterRegistry}
welcomeMessage={props.welcomeMessage}
/>
<ChatInput
sx={{
Expand Down Expand Up @@ -131,6 +132,10 @@ export namespace Chat {
* The footer registry.
*/
messageFooterRegistry?: IMessageFooterRegistry;
/**
* The welcome message.
*/
welcomeMessage?: string;
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/jupyter-chat/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export * from './chat-messages';
export * from './code-blocks';
export * from './input';
export * from './jl-theme-provider';
export * from './markdown-renderer';
export * from './mui-extras';
export * from './scroll-container';
export * from './toolbar';
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { PromiseDelegate } from '@lumino/coreutils';
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';

import { CodeToolbar, CodeToolbarProps } from './code-blocks/code-toolbar';
import { MessageToolbar } from './toolbar';
import { IChatModel } from '../model';
import { CodeToolbar, CodeToolbarProps } from '../code-blocks/code-toolbar';
import { MessageToolbar } from '../toolbar';
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
import { IChatModel } from '../../model';

const MD_MIME_TYPE = 'text/markdown';
const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';

type MarkdownRendererProps = {
/**
* The type of the props for the MessageRenderer component.
*/
type MessageRendererProps = {
/**
* The string to render.
*/
Expand Down Expand Up @@ -47,22 +48,10 @@ type MarkdownRendererProps = {
};

/**
* 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`.
* The message renderer base component.
*/
function escapeLatexDelimiters(text: string) {
return text
.replace(/\\\(/g, '\\\\(')
.replace(/\\\)/g, '\\\\)')
.replace(/\\\[/g, '\\\\[')
.replace(/\\\]/g, '\\\\]');
}

function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
function MessageRendererBase(props: MessageRendererProps): JSX.Element {
const { markdownStr, rmRegistry } = props;
const appendContent = props.appendContent || false;
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
null
Expand All @@ -75,26 +64,11 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {

useEffect(() => {
const renderContent = async () => {
// initialize mime model
const mdStr = escapeLatexDelimiters(props.markdownStr);
const model = props.rmRegistry.createModel({
data: { [MD_MIME_TYPE]: mdStr }
const renderer = await MarkdownRenderer.renderContent({
content: markdownStr,
rmRegistry
});

const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);

// step 1: render markdown
await renderer.renderModel(model);
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
if (!renderer.node) {
throw new Error(
'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
);
}

// step 2: render LaTeX via MathJax.
props.rmRegistry.latexTypesetter?.typeset(renderer.node);

const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];

// Attach CodeToolbar root element to each <pre> block
Expand All @@ -119,7 +93,7 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
};

renderContent();
}, [props.markdownStr, props.rmRegistry]);
}, [markdownStr, rmRegistry]);

return (
<div className={MD_RENDERED_CLASS}>
Expand All @@ -146,4 +120,4 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
);
}

export const MarkdownRenderer = React.memo(MarkdownRendererBase);
export const MessageRenderer = React.memo(MessageRendererBase);
52 changes: 52 additions & 0 deletions packages/jupyter-chat/src/components/messages/welcome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/

import { classes } from '@jupyterlab/ui-components';
import React, { useEffect, useRef } from 'react';
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';

const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';

/**
* The welcome message component.
* This message is displayed on top of the chat messages, and is rendered using a
* markdown renderer.
*/
export function WelcomeMessage(props: MarkdownRenderer.IOptions): JSX.Element {
const { rmRegistry } = props;
const content = props.content + '\n----\n';

// ref that tracks the content container to store the rendermime node in
const renderingContainer = useRef<HTMLDivElement | null>(null);
// ref that tracks whether the rendermime node has already been inserted
const renderingInserted = useRef<boolean>(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 () => {
const renderer = await MarkdownRenderer.renderContent({
content,
rmRegistry
});

// insert the rendering into renderingContainer if not yet inserted
if (renderingContainer.current !== null && !renderingInserted.current) {
renderingContainer.current.appendChild(renderer.node);
renderingInserted.current = true;
}
};

renderContent();
}, [content]);

return (
<div className={classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS)}>
<div ref={renderingContainer} />
</div>
);
}
1 change: 1 addition & 0 deletions packages/jupyter-chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './components';
export * from './footers';
export * from './icons';
export * from './input-model';
export * from './markdown-renderer';
export * from './model';
export * from './registry';
export * from './selection-watcher';
Expand Down
78 changes: 78 additions & 0 deletions packages/jupyter-chat/src/markdown-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/

import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';

export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
export const MD_MIME_TYPE = 'text/markdown';

/**
* A namespace for the MarkdownRenderer.
*/
export namespace MarkdownRenderer {
/**
* The options for the MarkdownRenderer.
*/
export interface IOptions {
/**
* The rendermime registry.
*/
rmRegistry: IRenderMimeRegistry;
/**
* The markdown content.
*/
content: string;
}

/**
* A generic function to render a markdown string into a DOM element.
*
* @param content - the markdown content.
* @param rmRegistry - the rendermime registry.
* @returns a promise that resolves to the renderer.
*/
export async function renderContent(
options: IOptions
): Promise<IRenderMime.IRenderer> {
const { rmRegistry, content } = options;

// initialize mime model
const mdStr = escapeLatexDelimiters(content);
const model = rmRegistry.createModel({
data: { [MD_MIME_TYPE]: mdStr }
});

const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);

// 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 chat on GitHub.'
);
}

// step 2: render LaTeX via MathJax.
rmRegistry.latexTypesetter?.typeset(renderer.node);

return renderer;
}

/**
* 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`.
*/
export function escapeLatexDelimiters(text: string) {
return text
.replace(/\\\(/g, '\\\\(')
.replace(/\\\)/g, '\\\\)')
.replace(/\\\[/g, '\\\\[')
.replace(/\\\]/g, '\\\\]');
}
}
4 changes: 4 additions & 0 deletions packages/jupyter-chat/style/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
* See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
* See also: https://github.com/jupyterlab/jupyter-ai/issues/1090
*/
.jp-ThemedContainer .jp-chat-rendered-markdown.jp-chat-welcome-message {
padding: 0 1em;
}

.jp-ThemedContainer .jp-chat-rendered-markdown .jp-RenderedHTMLCommon {
padding-right: 0;
}
Expand Down
Loading
Loading