upgrade streamdown to v2 and fix chat theme preview height

- Upgrade streamdown 1.0.11 → 2.4.0 with native streaming animation support
- Use Streamdown's built-in isAnimating/animated props instead of custom typewriter
- Remove useStreamText hook (replaced by Streamdown's native animation)
- Simplify StreamText component to pass through to Streamdown directly
- Add streamdown/styles.css import and Tailwind @source for class detection
- Fix ChatThemePreview card height by overriding Card's default py-6/gap-6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sahaj Jain
2026-03-10 19:24:43 +05:30
parent 2667bb63a4
commit 9fb9c4c77f
7 changed files with 202 additions and 477 deletions
+3
View File
@@ -1,7 +1,10 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "streamdown/styles.css";
@import "./loaders.css";
@source "../node_modules/streamdown/dist";
@custom-variant dark (&:is(.dark *));
@theme inline {
+3 -1
View File
@@ -16,7 +16,9 @@ export const Response = memo(
{...props}
/>
),
(prevProps, nextProps) => prevProps.children === nextProps.children
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.isAnimating === nextProps.isAnimating
);
Response.displayName = "Response";
+3 -3
View File
@@ -49,7 +49,7 @@ export function ChatThemePreview({
if (loading) {
return (
<Card className={cn("w-full max-w-[550px] overflow-hidden rounded-lg shadow-none")}>
<Card className={cn("w-full max-w-[550px] gap-0 overflow-hidden rounded-lg py-0 shadow-none")}>
<div className="flex size-full h-10 items-center gap-2 p-1.5">
<div className="bg-muted flex size-7 items-center justify-center rounded-sm">
<Loader2 className="text-muted-foreground size-4 animate-spin" />
@@ -63,7 +63,7 @@ export function ChatThemePreview({
if (status === "error")
return (
<Card className={cn("max-w-[550px] overflow-hidden rounded-lg shadow-none")}>
<Card className={cn("max-w-[550px] gap-0 overflow-hidden rounded-lg py-0 shadow-none")}>
<div className="flex size-full h-10 items-center gap-2 p-1.5">
<div className="bg-destructive flex size-7 items-center justify-center rounded-sm">
<AlertCircle className="text-destructive-foreground size-4" />
@@ -81,7 +81,7 @@ export function ChatThemePreview({
if (status === "complete")
return (
<Card className={cn("max-w-[550px] overflow-hidden rounded-lg shadow-none")}>
<Card className={cn("max-w-[550px] gap-0 overflow-hidden rounded-lg py-0 shadow-none")}>
<div
className={cn(
"group/control hover:bg-background/50 flex h-10 w-full shrink-0 cursor-pointer items-center gap-2 p-1.5 pr-2 transition-colors duration-300 ease-in-out",
+7 -27
View File
@@ -1,6 +1,4 @@
import { Response } from "@/components/ai-elements/response";
import { useStreamText } from "@/hooks/use-stream-text";
import { useCallback, useEffect, useRef } from "react";
interface StreamTextProps {
text: string;
@@ -15,30 +13,12 @@ export function StreamText({
markdown = false,
className,
}: StreamTextProps) {
const contentRef = useRef("");
const { stream, addPart } = useStreamText();
if (markdown)
return (
<Response className={className} isAnimating={animate} animated>
{text}
</Response>
);
useEffect(() => {
if (!text || !animate) return;
if (contentRef.current !== text) {
const delta = text.slice(contentRef.current.length);
if (delta) {
addPart(delta);
}
contentRef.current = text;
}
}, [text, animate, addPart]);
const wrap = useCallback(
(text: string) => {
if (markdown) return <Response className={className}>{text}</Response>;
else return <span className={className}>{text}</span>;
},
[markdown]
);
if (!animate) return wrap(text);
return wrap(stream ?? text ?? "");
return <span className={className}>{text}</span>;
}
-70
View File
@@ -1,70 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
interface UseStreamTextProps {
speed?: number;
}
export function useStreamText({ speed = 5 }: UseStreamTextProps = {}) {
const [parts, setParts] = useState<string[]>([]);
const [stream, setStream] = useState("");
const frame = useRef<number | null>(null);
const lastTimeRef = useRef<number>(0);
const streamIndexRef = useRef<number>(0);
const isAnimatingRef = useRef(false);
const addPart = useCallback((part: string) => {
if (part) {
setParts((prev) => [...prev, part]);
}
}, []);
const reset = useCallback(() => {
setParts([]);
setStream("");
streamIndexRef.current = 0;
if (frame.current) {
cancelAnimationFrame(frame.current);
}
frame.current = null;
lastTimeRef.current = 0;
isAnimatingRef.current = false;
}, []);
useEffect(() => {
if (isAnimatingRef.current) return;
const typewriterSpeed = speed;
const fullText = parts.join("");
if (streamIndexRef.current >= fullText.length) {
setStream(fullText);
return;
}
isAnimatingRef.current = true;
const animate = (time: number) => {
if (streamIndexRef.current < fullText.length) {
if (time - lastTimeRef.current > typewriterSpeed) {
streamIndexRef.current++;
setStream(fullText.slice(0, streamIndexRef.current));
lastTimeRef.current = time;
}
frame.current = requestAnimationFrame(animate);
} else {
isAnimatingRef.current = false;
}
};
frame.current = requestAnimationFrame(animate);
return () => {
if (frame.current) {
cancelAnimationFrame(frame.current);
}
isAnimatingRef.current = false;
};
}, [parts]);
return { stream, addPart, reset };
}
+1 -1
View File
@@ -76,7 +76,7 @@
"server-only": "^0.0.1",
"shadcn": "^3.8.5",
"sonner": "^2.0.3",
"streamdown": "^1.0.11",
"streamdown": "^2.4.0",
"swr": "^2.3.3",
"tailwind-merge": "^3.2.0",
"tippy.js": "^6.3.7",
+185 -375
View File
File diff suppressed because it is too large Load Diff