Skip to content

Commit 427129b

Browse files
authored
replace markdown renderer (#18)
1 parent 433e3fc commit 427129b

File tree

6 files changed

+1022
-376
lines changed

6 files changed

+1022
-376
lines changed

packages/react/package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,7 @@
6666
"clsx": "^2.1.1",
6767
"katex": "^0.16.22",
6868
"lucide-react": "^0.525.0",
69-
"react-markdown": "^10.1.0",
70-
"rehype-katex": "^7.0.1",
71-
"rehype-pretty-code": "^0.14.1",
72-
"remark-gfm": "^4.0.1",
73-
"remark-math": "^6.0.0",
74-
"shiki": "^3.8.0",
69+
"streamdown": "^1.3.0",
7570
"tailwind-merge": "^3.3.1",
7671
"ts-deepmerge": "^7.0.3",
7772
"tw-animate-css": "^1.3.5",

packages/react/src/components/chat/CopilotChatAssistantMessage.tsx

Lines changed: 9 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
import { AssistantMessage, Message } from "@ag-ui/core";
2-
import { MarkdownHooks } from "react-markdown";
3-
import remarkGfm from "remark-gfm";
4-
import remarkMath from "remark-math";
5-
import rehypePrettyCode from "rehype-pretty-code";
6-
import rehypeKatex from "rehype-katex";
72
import { useState } from "react";
83
import {
94
Copy,
@@ -13,7 +8,6 @@ import {
138
Volume2,
149
RefreshCw,
1510
} from "lucide-react";
16-
import { cn } from "@/lib/utils";
1711
import {
1812
useCopilotChatConfiguration,
1913
CopilotChatDefaultLabels,
@@ -27,7 +21,7 @@ import {
2721
} from "@/components/ui/tooltip";
2822
import "katex/dist/katex.min.css";
2923
import { WithSlots, renderSlot } from "@/lib/slots";
30-
import { completePartialMarkdown } from "@copilotkitnext/core";
24+
import { Streamdown } from "streamdown";
3125
import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
3226

3327
export type CopilotChatAssistantMessageProps = WithSlots<
@@ -81,6 +75,7 @@ export function CopilotChatAssistantMessage({
8175
CopilotChatAssistantMessage.MarkdownRenderer,
8276
{
8377
content: message.content || "",
78+
8479
}
8580
);
8681

@@ -206,139 +201,14 @@ export function CopilotChatAssistantMessage({
206201

207202
// eslint-disable-next-line @typescript-eslint/no-namespace
208203
export namespace CopilotChatAssistantMessage {
209-
const InlineCode = ({
210-
children,
211-
...props
212-
}: React.HTMLAttributes<HTMLElement>) => {
213-
return (
214-
<code
215-
className="px-[4.8px] py-[2.5px] bg-[rgb(236,236,236)] dark:bg-gray-800 rounded text-sm font-mono font-medium! text-foreground!"
216-
{...props}
217-
>
218-
{children}
219-
</code>
220-
);
221-
};
222-
223-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
224-
const CodeBlock = ({ children, className, onClick, ...props }: any) => {
225-
const config = useCopilotChatConfiguration();
226-
const labels = config?.labels ?? CopilotChatDefaultLabels;
227-
const [copied, setCopied] = useState(false);
228-
229-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
230-
const getCodeContent = (node: any): string => {
231-
if (typeof node === "string") return node;
232-
if (Array.isArray(node)) return node.map(getCodeContent).join("");
233-
if (node?.props?.children) return getCodeContent(node.props.children);
234-
return "";
235-
};
236-
237-
const codeContent = getCodeContent(children);
238-
const language = props["data-language"] as string | undefined;
239-
240-
const copyToClipboard = async () => {
241-
if (!codeContent.trim()) return;
242-
243-
try {
244-
setCopied(true);
245-
setTimeout(() => setCopied(false), 2000);
246-
if (onClick) {
247-
onClick();
248-
}
249-
} catch (err) {
250-
console.error("Failed to copy code:", err);
251-
}
252-
};
253-
254-
return (
255-
<div className="relative">
256-
<div className="flex items-center justify-between px-4 pr-3 py-3 text-xs">
257-
{language && (
258-
<span className="font-regular text-muted-foreground dark:text-white">
259-
{language}
260-
</span>
261-
)}
262-
263-
<button
264-
className={cn(
265-
"px-2 gap-0.5 text-xs flex items-center cursor-pointer text-muted-foreground dark:text-white"
266-
)}
267-
onClick={copyToClipboard}
268-
title={
269-
copied
270-
? labels.assistantMessageToolbarCopyCodeCopiedLabel
271-
: `${labels.assistantMessageToolbarCopyCodeLabel} code`
272-
}
273-
>
274-
{copied ? (
275-
<Check className="h-[10px]! w-[10px]!" />
276-
) : (
277-
<Copy className="h-[10px]! w-[10px]!" />
278-
)}
279-
<span className="text-[11px]">
280-
{copied
281-
? labels.assistantMessageToolbarCopyCodeCopiedLabel
282-
: labels.assistantMessageToolbarCopyCodeLabel}
283-
</span>
284-
</button>
285-
</div>
286-
287-
<pre
288-
className={cn(
289-
className,
290-
"rounded-2xl bg-transparent border-t-0 my-1!"
291-
)}
292-
{...props}
293-
>
294-
{children}
295-
</pre>
296-
</div>
297-
);
298-
};
299-
300204
export const MarkdownRenderer: React.FC<
301-
React.HTMLAttributes<HTMLDivElement> & { content: string }
302-
> = ({ content, className }) => (
303-
<div className={className}>
304-
<MarkdownHooks
305-
/* async plugins are now fine ✨ */
306-
remarkPlugins={[remarkGfm, remarkMath]}
307-
rehypePlugins={[
308-
[
309-
rehypePrettyCode,
310-
{
311-
keepBackground: false,
312-
theme: {
313-
dark: "one-dark-pro",
314-
light: "one-light",
315-
},
316-
bypassInlineCode: true,
317-
},
318-
],
319-
rehypeKatex,
320-
]}
321-
components={{
322-
pre: CodeBlock,
323-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
324-
code: ({ className, children, ...props }: any) => {
325-
// For inline code, use custom styling
326-
if (typeof children === "string") {
327-
return <InlineCode {...props}>{children}</InlineCode>;
328-
}
329-
330-
// For code blocks, just return the code element as-is
331-
return (
332-
<code className={className} {...props}>
333-
{children}
334-
</code>
335-
);
336-
},
337-
}}
338-
>
339-
{completePartialMarkdown(content || "")}
340-
</MarkdownHooks>
341-
</div>
205+
Omit<React.ComponentProps<typeof Streamdown>, "children"> & {
206+
content: string;
207+
}
208+
> = ({ content, className, ...props }) => (
209+
<Streamdown className={className} {...props}>
210+
{content ?? ""}
211+
</Streamdown>
342212
);
343213

344214
export const Toolbar: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({

packages/react/src/styles/globals.css

Lines changed: 4 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@source "../../../apps/**/*.{ts,tsx}";
44
@source "../../../components/**/*.{ts,tsx}";
55
@source "../**/*.{ts,tsx}";
6+
@source "../../node_modules/streamdown/dist/index.js";
67

78
@import "tw-animate-css";
89

@@ -119,8 +120,7 @@
119120
}
120121

121122
@theme {
122-
--animate-pulse-cursor: pulse-cursor 0.9s cubic-bezier(0.4, 0, 0.2, 1)
123-
infinite;
123+
--animate-pulse-cursor: pulse-cursor 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
124124
@keyframes pulse-cursor {
125125
0%,
126126
100% {
@@ -143,117 +143,8 @@
143143
}
144144
}
145145

146-
figure[data-rehype-pretty-code-figure] {
147-
background-color: rgb(249, 249, 249);
148-
margin-top: 8px;
149-
margin-bottom: 0px;
150-
margin-left: 0px;
151-
margin-right: 0px;
152-
@apply rounded-2xl;
153-
}
154-
155-
figure[data-rehype-pretty-code-figure] pre {
156-
background-color: rgb(249, 249, 249);
157-
}
158-
159-
html .prose code,
160-
html .prose code span {
161-
color: var(--shiki-light) !important;
162-
background-color: rgb(249, 249, 249) !important;
163-
/* Optional, if you also want font styles */
164-
font-style: var(--shiki-light-font-style) !important;
165-
font-weight: var(--shiki-light-font-weight) !important;
166-
text-decoration: var(--shiki-light-text-decoration) !important;
167-
}
168-
169-
html.dark figure[data-rehype-pretty-code-figure],
170-
html.dark figure[data-rehype-pretty-code-figure] pre {
171-
background-color: #171717;
172-
}
173-
174-
html.dark .prose code,
175-
html.dark .prose code span {
176-
color: var(--shiki-dark) !important;
177-
background-color: #171717 !important;
178-
font-style: var(--shiki-dark-font-style) !important;
179-
font-weight: var(--shiki-dark-font-weight) !important;
180-
text-decoration: var(--shiki-dark-text-decoration) !important;
181-
}
182-
183-
.prose {
184-
-webkit-font-smoothing: antialiased;
185-
-moz-osx-font-smoothing: grayscale;
186-
}
187-
188-
.prose
189-
:where(code):not(
190-
:where([class~="not-prose"], [class~="not-prose"] *)
191-
)::before {
192-
content: none;
193-
}
194-
195-
.prose
196-
:where(code):not(
197-
:where([class~="not-prose"], [class~="not-prose"] *)
198-
)::after {
199-
content: none;
200-
}
201-
202-
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
203-
font-weight: 400;
204-
}
205-
206-
.prose h1 {
207-
margin-block-end: 8px;
208-
margin-bottom: 8px;
209-
font-size: 24px;
210-
font-weight: 600;
211-
}
212-
213-
.prose h2 {
214-
margin-block-end: 4px;
215-
margin-block-start: 16px;
216-
margin-top: 16px;
217-
margin-bottom: 4px;
218-
font-size: 20px;
219-
font-weight: 600;
220-
}
221-
222-
.prose h3 {
223-
margin-block-end: 4px;
224-
margin-block-start: 16px;
225-
margin-top: 16px;
226-
margin-bottom: 4px;
227-
font-size: 18px;
228-
font-weight: 600;
229-
}
230-
231-
.prose p {
232-
margin-block-start: 8px;
233-
margin-block-end: 4px;
234-
margin-top: 4px;
235-
margin-bottom: 8px;
236-
}
237-
238-
.prose a {
239-
color: #2964aa;
240-
text-decoration: none;
241-
}
242-
243-
.prose a:hover {
244-
color: #749ac8;
245-
}
246-
247-
.prose
248-
:where(blockquote p:first-of-type):not(
249-
:where([class~="not-prose"], [class~="not-prose"] *)
250-
)::before {
251-
content: none;
252-
}
253-
254-
.prose blockquote {
255-
font-style: normal;
256-
font-weight: 400;
146+
div[data-streamdown="code-block"] > pre {
147+
@apply mt-0 mb-0;
257148
}
258149

259150
.prose input[type="checkbox"] {
@@ -284,19 +175,3 @@ html.dark .prose code span {
284175
background-repeat: no-repeat;
285176
background-size: 100% 100%;
286177
}
287-
288-
.prose em {
289-
@apply text-foreground;
290-
}
291-
292-
.prose hr {
293-
margin-block-start: 0px;
294-
margin-block-end: 0px;
295-
margin-top: 28px;
296-
margin-bottom: 28px;
297-
color: rgb(13, 13, 13);
298-
}
299-
300-
.prose td {
301-
@apply text-foreground;
302-
}

packages/react/vitest.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export default defineConfig({
66
setupFiles: ["./src/__tests__/setup.ts"],
77
include: ["**/__tests__/**/*.{test,spec}.{ts,tsx}"],
88
globals: true,
9+
server: {
10+
deps: {
11+
inline: ["streamdown"],
12+
},
13+
},
914
},
1015
resolve: {
1116
alias: {

packages/web-inspector/src/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,9 +2039,6 @@ export class WebInspectorElement extends LitElement {
20392039
: "";
20402040
20412041
const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2042-
const truncatedContent = rawContent.length > 500
2043-
? `${rawContent.slice(0, 500)}...`
2044-
: rawContent;
20452042
const hasContent = rawContent.trim().length > 0;
20462043
const contentFallback = toolCalls.length > 0
20472044
? "Invoked tool call"
@@ -2056,7 +2053,7 @@ export class WebInspectorElement extends LitElement {
20562053
</td>
20572054
<td class="px-4 py-2">
20582055
${hasContent
2059-
? html`<div class="max-w-2xl whitespace-pre-wrap break-words text-gray-700">${truncatedContent}</div>`
2056+
? html`<div class="max-w-2xl whitespace-pre-wrap break-words text-gray-700">${rawContent}</div>`
20602057
: html`<div class="text-xs italic text-gray-400">${contentFallback}</div>`}
20612058
${role === 'assistant' && toolCalls.length > 0
20622059
? this.renderToolCallDetails(toolCalls)

0 commit comments

Comments
 (0)