mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-29 16:54:47 +08:00
* 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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user