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