Skip to content

Commit 12393ed

Browse files
Add a welcome message (#221)
* Add a generic markdown renderer function * Add a welcome message, rendered using markdown * Add a token in jupyterlab-chat to allows third party extension to provide a welcome message * Add a welcome message in jupyterlite-example * Automatic application of license header * Add object option to the renderContent function, and reorganize modules --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 860cd9d commit 12393ed

File tree

13 files changed

+218
-53
lines changed

13 files changed

+218
-53
lines changed

docs/jupyter-chat-example/src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2626
import { ISettingRegistry } from '@jupyterlab/settingregistry';
2727
import { UUID } from '@lumino/coreutils';
2828

29+
const welcomeMessage = `
30+
### Welcome to Jupyter Chat!
31+
32+
This is a simple chat panel that allows you to send messages and share files.
33+
34+
The messages are rendered as markdown, allowing you to format the messages with e.g.:
35+
- **bold**
36+
- _italic_
37+
- \`\`\`python
38+
# This is a code block
39+
print('Hello world!')
40+
\`\`\`
41+
- [link](https://jupyter.org)
42+
`;
43+
2944
class ChatContext extends AbstractChatContext {
3045
get users() {
3146
return [];
@@ -138,7 +153,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
138153
model,
139154
rmRegistry,
140155
themeManager,
141-
attachmentOpenerRegistry
156+
attachmentOpenerRegistry,
157+
welcomeMessage
142158
});
143159
app.shell.add(panel, 'left');
144160
}

packages/jupyter-chat/src/components/chat-messages.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import React, { useEffect, useState, useRef, forwardRef } from 'react';
1919
import { AttachmentPreviewList } from './attachments';
2020
import { ChatInput } from './chat-input';
2121
import { IInputToolbarRegistry } from './input';
22-
import { MarkdownRenderer } from './markdown-renderer';
2322
import { MessageFooter } from './messages/footer';
23+
import { MessageRenderer } from './messages/message-renderer';
24+
import { WelcomeMessage } from './messages/welcome';
2425
import { ScrollContainer } from './scroll-container';
2526
import { IChatCommandRegistry } from '../chat-commands';
2627
import { IMessageFooterRegistry } from '../footers';
@@ -64,6 +65,10 @@ type BaseMessageProps = {
6465
* The footer registry.
6566
*/
6667
messageFooterRegistry?: IMessageFooterRegistry;
68+
/**
69+
* The welcome message.
70+
*/
71+
welcomeMessage?: string;
6772
};
6873

6974
/**
@@ -184,6 +189,12 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
184189
return (
185190
<>
186191
<ScrollContainer sx={{ flexGrow: 1 }}>
192+
{props.welcomeMessage && (
193+
<WelcomeMessage
194+
rmRegistry={props.rmRegistry}
195+
content={props.welcomeMessage}
196+
/>
197+
)}
187198
<Box ref={refMsgBox} className={clsx(MESSAGES_BOX_CLASS)}>
188199
{messages.map((message, i) => {
189200
renderedPromise.current[i] = new PromiseDelegate();
@@ -463,7 +474,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
463474
toolbarRegistry={props.inputToolbarRegistry}
464475
/>
465476
) : (
466-
<MarkdownRenderer
477+
<MessageRenderer
467478
rmRegistry={rmRegistry}
468479
markdownStr={message.body}
469480
model={model}

packages/jupyter-chat/src/components/chat.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
3636
chatCommandRegistry={props.chatCommandRegistry}
3737
inputToolbarRegistry={inputToolbarRegistry}
3838
messageFooterRegistry={props.messageFooterRegistry}
39+
welcomeMessage={props.welcomeMessage}
3940
/>
4041
<ChatInput
4142
sx={{
@@ -131,6 +132,10 @@ export namespace Chat {
131132
* The footer registry.
132133
*/
133134
messageFooterRegistry?: IMessageFooterRegistry;
135+
/**
136+
* The welcome message.
137+
*/
138+
welcomeMessage?: string;
134139
}
135140

136141
/**

packages/jupyter-chat/src/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export * from './chat-messages';
99
export * from './code-blocks';
1010
export * from './input';
1111
export * from './jl-theme-provider';
12-
export * from './markdown-renderer';
1312
export * from './mui-extras';
1413
export * from './scroll-container';
1514
export * from './toolbar';

packages/jupyter-chat/src/components/markdown-renderer.tsx renamed to packages/jupyter-chat/src/components/messages/message-renderer.tsx

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { PromiseDelegate } from '@lumino/coreutils';
88
import React, { useState, useEffect } from 'react';
99
import { createPortal } from 'react-dom';
1010

11-
import { CodeToolbar, CodeToolbarProps } from './code-blocks/code-toolbar';
12-
import { MessageToolbar } from './toolbar';
13-
import { IChatModel } from '../model';
11+
import { CodeToolbar, CodeToolbarProps } from '../code-blocks/code-toolbar';
12+
import { MessageToolbar } from '../toolbar';
13+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
14+
import { IChatModel } from '../../model';
1415

15-
const MD_MIME_TYPE = 'text/markdown';
16-
const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
17-
18-
type MarkdownRendererProps = {
16+
/**
17+
* The type of the props for the MessageRenderer component.
18+
*/
19+
type MessageRendererProps = {
1920
/**
2021
* The string to render.
2122
*/
@@ -47,22 +48,10 @@ type MarkdownRendererProps = {
4748
};
4849

4950
/**
50-
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
51-
* after the initial MarkDown render. For example, this function takes '\(` and
52-
* returns `\\(`.
53-
*
54-
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
55-
* `ILatexTypesetter`.
51+
* The message renderer base component.
5652
*/
57-
function escapeLatexDelimiters(text: string) {
58-
return text
59-
.replace(/\\\(/g, '\\\\(')
60-
.replace(/\\\)/g, '\\\\)')
61-
.replace(/\\\[/g, '\\\\[')
62-
.replace(/\\\]/g, '\\\\]');
63-
}
64-
65-
function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
53+
function MessageRendererBase(props: MessageRendererProps): JSX.Element {
54+
const { markdownStr, rmRegistry } = props;
6655
const appendContent = props.appendContent || false;
6756
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
6857
null
@@ -75,26 +64,11 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
7564

7665
useEffect(() => {
7766
const renderContent = async () => {
78-
// initialize mime model
79-
const mdStr = escapeLatexDelimiters(props.markdownStr);
80-
const model = props.rmRegistry.createModel({
81-
data: { [MD_MIME_TYPE]: mdStr }
67+
const renderer = await MarkdownRenderer.renderContent({
68+
content: markdownStr,
69+
rmRegistry
8270
});
8371

84-
const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
85-
86-
// step 1: render markdown
87-
await renderer.renderModel(model);
88-
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
89-
if (!renderer.node) {
90-
throw new Error(
91-
'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
92-
);
93-
}
94-
95-
// step 2: render LaTeX via MathJax.
96-
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
97-
9872
const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = [];
9973

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

12195
renderContent();
122-
}, [props.markdownStr, props.rmRegistry]);
96+
}, [markdownStr, rmRegistry]);
12397

12498
return (
12599
<div className={MD_RENDERED_CLASS}>
@@ -146,4 +120,4 @@ function MarkdownRendererBase(props: MarkdownRendererProps): JSX.Element {
146120
);
147121
}
148122

149-
export const MarkdownRenderer = React.memo(MarkdownRendererBase);
123+
export const MessageRenderer = React.memo(MessageRendererBase);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
import { classes } from '@jupyterlab/ui-components';
7+
import React, { useEffect, useRef } from 'react';
8+
import { MarkdownRenderer, MD_RENDERED_CLASS } from '../../markdown-renderer';
9+
10+
const WELCOME_MESSAGE_CLASS = 'jp-chat-welcome-message';
11+
12+
/**
13+
* The welcome message component.
14+
* This message is displayed on top of the chat messages, and is rendered using a
15+
* markdown renderer.
16+
*/
17+
export function WelcomeMessage(props: MarkdownRenderer.IOptions): JSX.Element {
18+
const { rmRegistry } = props;
19+
const content = props.content + '\n----\n';
20+
21+
// ref that tracks the content container to store the rendermime node in
22+
const renderingContainer = useRef<HTMLDivElement | null>(null);
23+
// ref that tracks whether the rendermime node has already been inserted
24+
const renderingInserted = useRef<boolean>(false);
25+
26+
/**
27+
* Effect: use Rendermime to render `props.markdownStr` into an HTML element,
28+
* and insert it into `renderingContainer` if not yet inserted.
29+
*/
30+
useEffect(() => {
31+
const renderContent = async () => {
32+
const renderer = await MarkdownRenderer.renderContent({
33+
content,
34+
rmRegistry
35+
});
36+
37+
// insert the rendering into renderingContainer if not yet inserted
38+
if (renderingContainer.current !== null && !renderingInserted.current) {
39+
renderingContainer.current.appendChild(renderer.node);
40+
renderingInserted.current = true;
41+
}
42+
};
43+
44+
renderContent();
45+
}, [content]);
46+
47+
return (
48+
<div className={classes(MD_RENDERED_CLASS, WELCOME_MESSAGE_CLASS)}>
49+
<div ref={renderingContainer} />
50+
</div>
51+
);
52+
}

packages/jupyter-chat/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './components';
99
export * from './footers';
1010
export * from './icons';
1111
export * from './input-model';
12+
export * from './markdown-renderer';
1213
export * from './model';
1314
export * from './registry';
1415
export * from './selection-watcher';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
import { IRenderMime, IRenderMimeRegistry } from '@jupyterlab/rendermime';
7+
8+
export const MD_RENDERED_CLASS = 'jp-chat-rendered-markdown';
9+
export const MD_MIME_TYPE = 'text/markdown';
10+
11+
/**
12+
* A namespace for the MarkdownRenderer.
13+
*/
14+
export namespace MarkdownRenderer {
15+
/**
16+
* The options for the MarkdownRenderer.
17+
*/
18+
export interface IOptions {
19+
/**
20+
* The rendermime registry.
21+
*/
22+
rmRegistry: IRenderMimeRegistry;
23+
/**
24+
* The markdown content.
25+
*/
26+
content: string;
27+
}
28+
29+
/**
30+
* A generic function to render a markdown string into a DOM element.
31+
*
32+
* @param content - the markdown content.
33+
* @param rmRegistry - the rendermime registry.
34+
* @returns a promise that resolves to the renderer.
35+
*/
36+
export async function renderContent(
37+
options: IOptions
38+
): Promise<IRenderMime.IRenderer> {
39+
const { rmRegistry, content } = options;
40+
41+
// initialize mime model
42+
const mdStr = escapeLatexDelimiters(content);
43+
const model = rmRegistry.createModel({
44+
data: { [MD_MIME_TYPE]: mdStr }
45+
});
46+
47+
const renderer = rmRegistry.createRenderer(MD_MIME_TYPE);
48+
49+
// step 1: render markdown
50+
await renderer.renderModel(model);
51+
if (!renderer.node) {
52+
throw new Error(
53+
'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter chat on GitHub.'
54+
);
55+
}
56+
57+
// step 2: render LaTeX via MathJax.
58+
rmRegistry.latexTypesetter?.typeset(renderer.node);
59+
60+
return renderer;
61+
}
62+
63+
/**
64+
* Escapes backslashes in LaTeX delimiters such that they appear in the DOM
65+
* after the initial MarkDown render. For example, this function takes '\(` and
66+
* returns `\\(`.
67+
*
68+
* Required for proper rendering of MarkDown + LaTeX markup in the chat by
69+
* `ILatexTypesetter`.
70+
*/
71+
export function escapeLatexDelimiters(text: string) {
72+
return text
73+
.replace(/\\\(/g, '\\\\(')
74+
.replace(/\\\)/g, '\\\\)')
75+
.replace(/\\\[/g, '\\\\[')
76+
.replace(/\\\]/g, '\\\\]');
77+
}
78+
}

packages/jupyter-chat/style/chat.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
* See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling
2727
* See also: https://github.com/jupyterlab/jupyter-ai/issues/1090
2828
*/
29+
.jp-ThemedContainer .jp-chat-rendered-markdown.jp-chat-welcome-message {
30+
padding: 0 1em;
31+
}
32+
2933
.jp-ThemedContainer .jp-chat-rendered-markdown .jp-RenderedHTMLCommon {
3034
padding-right: 0;
3135
}

0 commit comments

Comments
 (0)