mirror of
https://github.com/jnsahaj/tweakcn.git
synced 2026-08-30 18:10:28 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+185
-375
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user