Skip to content

Commit 165ecec

Browse files
HomenShumHShuM
andauthored
refactor(ai-elements): wrap live message bubble (#525)
* refactor(ai-elements): migrate live message bubble * fix(ai-elements): preserve ordered message ownership * fix(ai-elements): preserve reasoning and falsy tool outputs --------- Co-authored-by: hshum <hshum@users.noreply.github.com>
1 parent 64203de commit 165ecec

6 files changed

Lines changed: 1645 additions & 628 deletions

File tree

src/components/ai-elements/message.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,27 @@ import {
1212
TooltipTrigger,
1313
} from "@/components/ai-ui/tooltip";
1414
import { cn } from "@/lib/utils";
15-
import { cjk } from "@streamdown/cjk";
16-
import { code } from "@streamdown/code";
17-
import { math } from "@streamdown/math";
18-
import { mermaid } from "@streamdown/mermaid";
1915
import type { UIMessage } from "ai";
2016
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
2117
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
2218
import {
2319
createContext,
20+
lazy,
2421
memo,
22+
Suspense,
2523
useCallback,
2624
useContext,
2725
useEffect,
2826
useMemo,
2927
useState,
3028
} from "react";
31-
import { Streamdown } from "streamdown";
29+
import type { Streamdown } from "streamdown";
30+
31+
const StreamdownRenderer = lazy(() =>
32+
import("./streamdown-renderer").then((module) => ({
33+
default: module.StreamdownRenderer,
34+
}))
35+
);
3236

3337
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
3438
from: UIMessage["role"];
@@ -321,18 +325,25 @@ export const MessageBranchPage = ({
321325

322326
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
323327

324-
const streamdownPlugins = { cjk, code, math, mermaid };
325-
326328
export const MessageResponse = memo(
327-
({ className, ...props }: MessageResponseProps) => (
328-
<Streamdown
329-
className={cn(
330-
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
331-
className
332-
)}
333-
plugins={streamdownPlugins}
334-
{...props}
335-
/>
329+
({ children, className, ...props }: MessageResponseProps) => (
330+
<Suspense
331+
fallback={
332+
<div className={cn("size-full whitespace-pre-wrap", className)}>
333+
{children}
334+
</div>
335+
}
336+
>
337+
<StreamdownRenderer
338+
className={cn(
339+
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
340+
className
341+
)}
342+
{...props}
343+
>
344+
{children}
345+
</StreamdownRenderer>
346+
</Suspense>
336347
),
337348
(prevProps, nextProps) =>
338349
prevProps.children === nextProps.children &&

src/components/ai-elements/reasoning.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,29 @@ import {
77
CollapsibleTrigger,
88
} from "@/components/ai-ui/collapsible";
99
import { cn } from "@/lib/utils";
10-
import { cjk } from "@streamdown/cjk";
11-
import { code } from "@streamdown/code";
12-
import { math } from "@streamdown/math";
13-
import { mermaid } from "@streamdown/mermaid";
1410
import { BrainIcon, ChevronDownIcon } from "lucide-react";
1511
import type { ComponentProps, ReactNode } from "react";
1612
import {
1713
createContext,
14+
lazy,
1815
memo,
16+
Suspense,
1917
useCallback,
2018
useContext,
2119
useEffect,
2220
useMemo,
2321
useRef,
2422
useState,
2523
} from "react";
26-
import { Streamdown } from "streamdown";
2724

2825
import { Shimmer } from "./shimmer";
2926

27+
const ReasoningStreamdown = lazy(() =>
28+
import("./streamdown-renderer").then((module) => ({
29+
default: module.StreamdownRenderer,
30+
}))
31+
);
32+
3033
interface ReasoningContextValue {
3134
isStreaming: boolean;
3235
isOpen: boolean;
@@ -204,8 +207,6 @@ export type ReasoningContentProps = ComponentProps<
204207
children: string;
205208
};
206209

207-
const streamdownPlugins = { cjk, code, math, mermaid };
208-
209210
export const ReasoningContent = memo(
210211
({ className, children, ...props }: ReasoningContentProps) => (
211212
<CollapsibleContent
@@ -216,7 +217,15 @@ export const ReasoningContent = memo(
216217
)}
217218
{...props}
218219
>
219-
<Streamdown plugins={streamdownPlugins}>{children}</Streamdown>
220+
<Suspense
221+
fallback={
222+
<div className="whitespace-pre-wrap text-muted-foreground">
223+
{children}
224+
</div>
225+
}
226+
>
227+
<ReasoningStreamdown>{children}</ReasoningStreamdown>
228+
</Suspense>
220229
</CollapsibleContent>
221230
)
222231
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use client";
2+
3+
import { cjk } from "@streamdown/cjk";
4+
import { code } from "@streamdown/code";
5+
import { math } from "@streamdown/math";
6+
import { mermaid } from "@streamdown/mermaid";
7+
import type { ComponentProps } from "react";
8+
import { Streamdown } from "streamdown";
9+
10+
const streamdownPlugins = { cjk, code, math, mermaid };
11+
12+
export type StreamdownRendererProps = ComponentProps<typeof Streamdown>;
13+
14+
export const StreamdownRenderer = (props: StreamdownRendererProps) => (
15+
<Streamdown plugins={streamdownPlugins} {...props} />
16+
);

src/components/ai-elements/tool.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,18 +157,21 @@ export const ToolOutput = ({
157157
errorText,
158158
...props
159159
}: ToolOutputProps) => {
160-
if (!(output || errorText)) {
160+
const hasOutput = output !== undefined && output !== null;
161+
if (!hasOutput && !errorText) {
161162
return null;
162163
}
163164

164-
let Output = <div>{output as ReactNode}</div>;
165+
let Output = hasOutput ? <div>{output as ReactNode}</div> : null;
165166

166-
if (typeof output === "object" && !isValidElement(output)) {
167+
if (hasOutput && typeof output === "object" && !isValidElement(output)) {
167168
Output = (
168169
<DeferredCodeBlock code={JSON.stringify(output, null, 2)} />
169170
);
170-
} else if (typeof output === "string") {
171+
} else if (hasOutput && typeof output === "string") {
171172
Output = <DeferredCodeBlock code={output} />;
173+
} else if (hasOutput) {
174+
Output = <DeferredCodeBlock code={String(output)} />;
172175
}
173176

174177
return (

0 commit comments

Comments
 (0)