fix: prevent repeated content animation during thinking streaming (#614) (#623)

* fix: prevent repeated content animation during thinking streaming (#614)

- Implement chunked rendering using reasoningContentChunks
- Static content (previous chunks) renders without animation
- Only current streaming chunk animates
- Disable animation on plan content (title, thought, steps) during streaming
- Animation applies after content finishes streaming (when complete)
- Prevents visual duplication of repeated sentences in thinking process
This commit is contained in:
Willem Jiang
2025-10-16 19:48:05 +08:00
committed by GitHub
parent d9f829b608
commit c6348e70c6

View File

@@ -314,11 +314,13 @@ function ThoughtBlock({
content, content,
isStreaming, isStreaming,
hasMainContent, hasMainContent,
contentChunks,
}: { }: {
className?: string; className?: string;
content: string; content: string;
isStreaming?: boolean; isStreaming?: boolean;
hasMainContent?: boolean; hasMainContent?: boolean;
contentChunks?: string[];
}) { }) {
const t = useTranslations("chat.research"); const t = useTranslations("chat.research");
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
@@ -336,6 +338,12 @@ function ThoughtBlock({
return null; return null;
} }
// Split content into static (previous chunks) and streaming (current chunk)
const chunks = contentChunks ?? [];
const staticContent = chunks.slice(0, -1).join("");
const streamingChunk = isStreaming && chunks.length > 0 ? (chunks[chunks.length - 1] ?? "") : "";
const hasStreamingContent = isStreaming && streamingChunk.length > 0;
return ( return (
<div className={cn("mb-6 w-full", className)}> <div className={cn("mb-6 w-full", className)}>
<Collapsible open={isOpen} onOpenChange={setIsOpen}> <Collapsible open={isOpen} onOpenChange={setIsOpen}>
@@ -399,15 +407,39 @@ function ThoughtBlock({
scrollShadow={false} scrollShadow={false}
autoScrollToBottom autoScrollToBottom
> >
{staticContent && (
<Markdown
className={cn(
"prose dark:prose-invert max-w-none transition-colors duration-200",
"opacity-80",
)}
animated={false}
>
{staticContent}
</Markdown>
)}
{hasStreamingContent && (
<Markdown
className={cn(
"prose dark:prose-invert max-w-none transition-colors duration-200",
"prose-primary",
)}
animated={true}
>
{streamingChunk}
</Markdown>
)}
{!hasStreamingContent && (
<Markdown <Markdown
className={cn( className={cn(
"prose dark:prose-invert max-w-none transition-colors duration-200", "prose dark:prose-invert max-w-none transition-colors duration-200",
isStreaming ? "prose-primary" : "opacity-80", isStreaming ? "prose-primary" : "opacity-80",
)} )}
animated={isStreaming} animated={false}
> >
{content} {content}
</Markdown> </Markdown>
)}
</ScrollContainer> </ScrollContainer>
</div> </div>
</CardContent> </CardContent>
@@ -473,6 +505,7 @@ function PlanCard({
content={reasoningContent} content={reasoningContent}
isStreaming={isThinking} isStreaming={isThinking}
hasMainContent={hasMainContent} hasMainContent={hasMainContent}
contentChunks={message.reasoningContentChunks}
/> />
)} )}
{shouldShowPlan && ( {shouldShowPlan && (
@@ -484,7 +517,7 @@ function PlanCard({
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Markdown animated={message.isStreaming}> <Markdown animated={false}>
{`### ${ {`### ${
plan.title !== undefined && plan.title !== "" plan.title !== undefined && plan.title !== ""
? plan.title ? plan.title
@@ -495,7 +528,7 @@ function PlanCard({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}> <div style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>
<Markdown className="opacity-80" animated={message.isStreaming}> <Markdown className="opacity-80" animated={false}>
{plan.thought} {plan.thought}
</Markdown> </Markdown>
{plan.steps && ( {plan.steps && (
@@ -505,7 +538,7 @@ function PlanCard({
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="flex-1"> <div className="flex-1">
<h3 className="mb flex items-center gap-2 text-lg font-medium"> <h3 className="mb flex items-center gap-2 text-lg font-medium">
<Markdown animated={message.isStreaming}> <Markdown animated={false}>
{step.title} {step.title}
</Markdown> </Markdown>
{step.tools && step.tools.length > 0 && ( {step.tools && step.tools.length > 0 && (
@@ -520,7 +553,7 @@ function PlanCard({
)} )}
</h3> </h3>
<div className="text-muted-foreground text-sm" style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}> <div className="text-muted-foreground text-sm" style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>
<Markdown animated={message.isStreaming}> <Markdown animated={false}>
{step.description} {step.description}
</Markdown> </Markdown>
</div> </div>