Render streamed assistant responses as Markdown

This commit is contained in:
yuxuanhui
2026-09-08 14:36:05 +08:00
parent aef8e1d310
commit 80cb7d2b80
7 changed files with 363 additions and 4 deletions
+34 -4
View File
@@ -1,5 +1,13 @@
import { useChat } from "@ai-sdk/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Banner,
Button,
@@ -35,6 +43,12 @@ import type {
UIAction,
} from "./types";
const MessageMarkdown = lazy(() =>
import("./MessageMarkdown").then((module) => ({
default: module.MessageMarkdown,
})),
);
export function ChatPanel({
open,
context,
@@ -427,9 +441,25 @@ export function ChatPanel({
</span>
{message.parts.map((part, index) =>
part.type === "text" ? (
<div className="ai-text" key={index}>
{part.text}
</div>
message.role === "assistant" ? (
<Suspense
key={index}
fallback={<div className="ai-text">{part.text}</div>}
>
<MessageMarkdown
content={part.text}
final={
!streaming ||
message.id !== chat.messages.at(-1)?.id ||
part.state === "done"
}
/>
</Suspense>
) : (
<div className="ai-text" key={index}>
{part.text}
</div>
)
) : part.type === "data-tool" ? (
<BusinessCard
key={part.id ?? index}
+24
View File
@@ -0,0 +1,24 @@
import { memo } from "react";
import MarkdownRender from "markstream-react";
/** Render one assistant text part; final also settles interrupted streams. */
export const MessageMarkdown = memo(function MessageMarkdown({
content,
final,
}: {
content: string;
final: boolean;
}) {
return (
<div className="ai-markdown">
<MarkdownRender
content={content}
final={final}
fade={false}
htmlPolicy="escape"
renderCodeBlocksAsPre
batchRendering={false}
/>
</div>
);
});
+54
View File
@@ -82,6 +82,60 @@
line-height: 24px;
font-weight: 400;
}
.ai-markdown {
min-width: 0;
font-size: 14px;
line-height: 24px;
}
.ai-markdown .markstream-react {
color: var(--ink);
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.ai-markdown :is(h1, h2, h3, h4, h5, h6) {
font-size: 16px;
line-height: 24px;
font-weight: 600;
margin: 16px 0 8px;
}
.ai-markdown :is(p, ul, ol, blockquote, pre, table) {
margin-top: 8px;
margin-bottom: 8px;
}
.ai-chat .ai-markdown .markstream-react pre[data-markstream-pre] {
font-size: 12px;
max-width: 100%;
overflow-x: auto;
white-space: pre;
padding: 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--canvas);
}
.ai-markdown code {
font-size: 12px;
}
.ai-markdown table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.ai-markdown :is(th, td) {
padding: 6px 10px;
border: 1px solid var(--line);
white-space: nowrap;
}
.ai-markdown a {
color: var(--accent);
text-decoration: underline;
}
.ai-markdown blockquote {
border-left: 3px solid var(--line);
padding-left: 12px;
color: var(--muted);
}
.ai-message.user .ai-text {
background: var(--canvas);
padding: var(--space-3);