Skip to content

Commit 2b74042

Browse files
authored
feat: support custom markdown for assistant message (CopilotKit#1726)
1 parent 44a0e2d commit 2b74042

17 files changed

Lines changed: 1158 additions & 228 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@copilotkit/react-ui": patch
3+
---
4+
5+
- feat: support custom markdown for assistant message

CopilotKit/packages/react-ui/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@
5858
"@copilotkit/runtime-client-gql": "workspace:*",
5959
"@copilotkit/shared": "workspace:*",
6060
"@headlessui/react": "^2.1.3",
61-
"react-markdown": "^8.0.7",
62-
"react-syntax-highlighter": "^15.5.0",
63-
"remark-gfm": "^3.0.1",
64-
"remark-math": "^5.1.1"
61+
"react-markdown": "^10.1.0",
62+
"react-syntax-highlighter": "^15.6.1",
63+
"rehype-raw": "^7.0.0",
64+
"remark-gfm": "^4.0.1",
65+
"remark-math": "^6.0.0"
6566
},
6667
"keywords": [
6768
"copilotkit",

CopilotKit/packages/react-ui/src/components/chat/Chat.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import { Message, Role, TextMessage, ImageMessage } from "@copilotkit/runtime-cl
7474
import { randomId } from "@copilotkit/shared";
7575
import {
7676
AssistantMessageProps,
77+
ComponentsMap,
7778
InputProps,
7879
MessagesProps,
7980
RenderMessageProps,
@@ -82,6 +83,7 @@ import {
8283

8384
import { HintFunction, runAgent, stopAgent } from "@copilotkit/react-core";
8485
import { ImageUploadQueue } from "./ImageUploadQueue";
86+
import { Components } from "react-markdown";
8587

8688
/**
8789
* Props for CopilotChat component.
@@ -137,6 +139,12 @@ export interface CopilotChatProps {
137139
*/
138140
onThumbsDown?: (message: string) => void;
139141

142+
/**
143+
* A list of markdown components to render in assistant message.
144+
* Useful when you want to render custom elements in the message (e.g a reference tag element)
145+
*/
146+
markdownTagRenderers?: ComponentsMap;
147+
140148
/**
141149
* Icons can be used to set custom icons for the chat window.
142150
*/
@@ -291,6 +299,7 @@ export function CopilotChat({
291299
onCopy,
292300
onThumbsUp,
293301
onThumbsDown,
302+
markdownTagRenderers,
294303
Messages = DefaultMessages,
295304
RenderTextMessage = DefaultRenderTextMessage,
296305
RenderActionExecutionMessage = DefaultRenderActionExecutionMessage,
@@ -482,6 +491,7 @@ export function CopilotChat({
482491
onCopy={handleCopy}
483492
onThumbsUp={onThumbsUp}
484493
onThumbsDown={onThumbsDown}
494+
markdownTagRenderers={markdownTagRenderers}
485495
>
486496
{currentSuggestions.length > 0 && (
487497
<div className="suggestions">

CopilotKit/packages/react-ui/src/components/chat/Markdown.tsx

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,19 @@ import ReactMarkdown, { Options, Components } from "react-markdown";
33
import { CodeBlock } from "./CodeBlock";
44
import remarkGfm from "remark-gfm";
55
import remarkMath from "remark-math";
6+
import rehypeRaw from "rehype-raw";
67

7-
const MemoizedReactMarkdown: FC<Options> = memo(
8-
ReactMarkdown,
9-
(prevProps, nextProps) =>
10-
prevProps.children === nextProps.children && prevProps.className === nextProps.className,
11-
);
12-
13-
type MarkdownProps = {
14-
content: string;
15-
};
16-
17-
export const Markdown = ({ content }: MarkdownProps) => {
18-
return (
19-
<div className="copilotKitMarkdown">
20-
<MemoizedReactMarkdown components={components} remarkPlugins={[remarkGfm, remarkMath]}>
21-
{content}
22-
</MemoizedReactMarkdown>
23-
</div>
24-
);
25-
};
26-
27-
const components: Components = {
8+
const defaultComponents: Components = {
289
a({ children, ...props }) {
2910
return (
30-
<a
31-
style={{ color: "blue", textDecoration: "underline" }}
32-
{...props}
33-
target="_blank"
34-
rel="noopener noreferrer"
35-
>
11+
<a className="copilotKitMarkdownElement" {...props} target="_blank" rel="noopener noreferrer">
3612
{children}
3713
</a>
3814
);
3915
},
16+
// @ts-expect-error -- inline
4017
code({ children, className, inline, ...props }) {
41-
if (children.length) {
18+
if (Array.isArray(children) && children.length) {
4219
if (children[0] == "▍") {
4320
return (
4421
<span
@@ -52,7 +29,7 @@ const components: Components = {
5229
);
5330
}
5431

55-
children[0] = (children[0] as string).replace("`▍`", "▍");
32+
children[0] = (children?.[0] as string).replace("`▍`", "▍");
5633
}
5734

5835
const match = /language-(\w+)/.exec(className || "");
@@ -74,4 +51,84 @@ const components: Components = {
7451
/>
7552
);
7653
},
54+
h1: ({ children, ...props }) => (
55+
<h1 className="copilotKitMarkdownElement" {...props}>
56+
{children}
57+
</h1>
58+
),
59+
h2: ({ children, ...props }) => (
60+
<h2 className="copilotKitMarkdownElement" {...props}>
61+
{children}
62+
</h2>
63+
),
64+
h3: ({ children, ...props }) => (
65+
<h3 className="copilotKitMarkdownElement" {...props}>
66+
{children}
67+
</h3>
68+
),
69+
h4: ({ children, ...props }) => (
70+
<h4 className="copilotKitMarkdownElement" {...props}>
71+
{children}
72+
</h4>
73+
),
74+
h5: ({ children, ...props }) => (
75+
<h5 className="copilotKitMarkdownElement" {...props}>
76+
{children}
77+
</h5>
78+
),
79+
h6: ({ children, ...props }) => (
80+
<h6 className="copilotKitMarkdownElement" {...props}>
81+
{children}
82+
</h6>
83+
),
84+
p: ({ children, ...props }) => (
85+
<p className="copilotKitMarkdownElement" {...props}>
86+
{children}
87+
</p>
88+
),
89+
pre: ({ children, ...props }) => (
90+
<pre className="copilotKitMarkdownElement" {...props}>
91+
{children}
92+
</pre>
93+
),
94+
blockquote: ({ children, ...props }) => (
95+
<blockquote className="copilotKitMarkdownElement" {...props}>
96+
{children}
97+
</blockquote>
98+
),
99+
ul: ({ children, ...props }) => (
100+
<ul className="copilotKitMarkdownElement" {...props}>
101+
{children}
102+
</ul>
103+
),
104+
li: ({ children, ...props }) => (
105+
<li className="copilotKitMarkdownElement" {...props}>
106+
{children}
107+
</li>
108+
),
109+
};
110+
111+
const MemoizedReactMarkdown: FC<Options> = memo(
112+
ReactMarkdown,
113+
(prevProps, nextProps) =>
114+
prevProps.children === nextProps.children && prevProps.components === nextProps.components,
115+
);
116+
117+
type MarkdownProps = {
118+
content: string;
119+
components?: Components;
120+
};
121+
122+
export const Markdown = ({ content, components }: MarkdownProps) => {
123+
return (
124+
<div className="copilotKitMarkdown">
125+
<MemoizedReactMarkdown
126+
components={{ ...defaultComponents, ...components }}
127+
remarkPlugins={[remarkGfm, remarkMath]}
128+
rehypePlugins={[rehypeRaw]}
129+
>
130+
{content}
131+
</MemoizedReactMarkdown>
132+
</div>
133+
);
77134
};

CopilotKit/packages/react-ui/src/components/chat/Messages.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const Messages = ({
1919
onCopy,
2020
onThumbsUp,
2121
onThumbsDown,
22+
markdownTagRenderers,
2223
}: MessagesProps) => {
2324
const context = useChatContext();
2425
const initialMessages = useMemo(
@@ -67,6 +68,7 @@ export const Messages = ({
6768
onCopy={onCopy}
6869
onThumbsUp={onThumbsUp}
6970
onThumbsDown={onThumbsDown}
71+
markdownTagRenderers={markdownTagRenderers}
7072
/>
7173
);
7274
} else if (message.isActionExecutionMessage()) {

CopilotKit/packages/react-ui/src/components/chat/Modal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const CopilotModal = ({
8282
onThumbsDown,
8383
onCopy,
8484
onRegenerate,
85+
markdownTagRenderers,
8586
className,
8687
children,
8788
}: CopilotModalProps) => {
@@ -118,6 +119,7 @@ export const CopilotModal = ({
118119
onThumbsDown={onThumbsDown}
119120
onCopy={onCopy}
120121
onRegenerate={onRegenerate}
122+
markdownTagRenderers={markdownTagRenderers}
121123
/>
122124
</Window>
123125
</div>

CopilotKit/packages/react-ui/src/components/chat/messages/AssistantMessage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const AssistantMessage = (props: AssistantMessageProps) => {
1414
onThumbsUp,
1515
onThumbsDown,
1616
isCurrentMessage,
17+
markdownTagRenderers,
1718
} = props;
1819
const [copied, setCopied] = useState(false);
1920

@@ -54,7 +55,7 @@ export const AssistantMessage = (props: AssistantMessageProps) => {
5455
<>
5556
{(message || isLoading) && (
5657
<div className="copilotKitMessage copilotKitAssistantMessage">
57-
{message && <Markdown content={message || ""} />}
58+
{message && <Markdown content={message || ""} components={markdownTagRenderers} />}
5859
{isLoading && <LoadingIcon />}
5960

6061
{message && !isLoading && (

CopilotKit/packages/react-ui/src/components/chat/messages/RenderTextMessage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function RenderTextMessage({
1616
onCopy,
1717
onThumbsUp,
1818
onThumbsDown,
19+
markdownTagRenderers,
1920
} = props;
2021

2122
if (message.isTextMessage()) {
@@ -42,6 +43,7 @@ export function RenderTextMessage({
4243
onCopy={onCopy}
4344
onThumbsUp={onThumbsUp}
4445
onThumbsDown={onThumbsDown}
46+
markdownTagRenderers={markdownTagRenderers}
4547
/>
4648
);
4749
}

CopilotKit/packages/react-ui/src/components/chat/props.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Message } from "@copilotkit/runtime-client-gql";
2+
import { ReactNode } from "react";
23

34
export interface ButtonProps {}
45

@@ -19,6 +20,10 @@ export interface SuggestionsProps {
1920
onClick: (message: string) => void;
2021
}
2122

23+
export type ComponentsMap<T extends Record<string, object> = Record<string, object>> = {
24+
[K in keyof T]: React.FC<{ children?: ReactNode } & T[K]>;
25+
};
26+
2227
export interface MessagesProps {
2328
messages: Message[];
2429
inProgress: boolean;
@@ -50,6 +55,12 @@ export interface MessagesProps {
5055
* Callback function for thumbs down feedback
5156
*/
5257
onThumbsDown?: (message: string) => void;
58+
59+
/**
60+
* A list of markdown components to render in assistant message.
61+
* Useful when you want to render custom elements in the message (e.g a reference tag element)
62+
*/
63+
markdownTagRenderers?: ComponentsMap;
5364
}
5465

5566
export interface Renderer {
@@ -115,6 +126,12 @@ export interface AssistantMessageProps {
115126
* Callback function for thumbs down feedback
116127
*/
117128
onThumbsDown?: (message: string) => void;
129+
130+
/**
131+
* A list of markdown components to render in assistant message.
132+
* Useful when you want to render custom elements in the message (e.g a reference tag element)
133+
*/
134+
markdownTagRenderers?: ComponentsMap;
118135
}
119136

120137
export interface RenderMessageProps {
@@ -145,6 +162,12 @@ export interface RenderMessageProps {
145162
* Callback function for thumbs down feedback
146163
*/
147164
onThumbsDown?: (message: string) => void;
165+
166+
/**
167+
* A list of markdown components to render in assistant message.
168+
* Useful when you want to render custom elements in the message (e.g a reference tag element)
169+
*/
170+
markdownTagRenderers?: ComponentsMap;
148171
}
149172

150173
export interface InputProps {

0 commit comments

Comments
 (0)