Render streamed assistant responses as Markdown
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user