refactor: simplify thinking process UI and reduce noise

- Remove redundant status steps (understanding, preparing, generating)
  from SSE stream — only emit retrieved docs and reasoning content
- ThinkingProcess: only show when there's actual reasoning or docs
- Collapse header shows concise state: thinking count or doc count
- Clean up unused icon imports

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:26:30 +08:00
parent ecd5c7ff31
commit 19b6cdcbd8
3 changed files with 32 additions and 70 deletions
+15 -42
View File
@@ -1,7 +1,7 @@
"use client";
import { ChatMessage, ThinkingStep } from "@/types";
import { User, Bot, Copy, Edit, RotateCcw, ThumbsUp, ThumbsDown, Loader2, CheckCircle2, FileSearch, Brain, Sparkles } from "lucide-react";
import { User, Bot, Copy, Edit, RotateCcw, ThumbsUp, ThumbsDown, Loader2, Brain } from "lucide-react";
import { cn } from "@/lib/utils";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -23,30 +23,29 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
const [expanded, setExpanded] = useState(false);
if (!thinking || thinking.length === 0) return null;
// 分离状态步骤推理内容
const statusSteps = thinking.filter(s => s.stage !== 'reasoning');
// 只提取有实际内容的步骤推理内容和检索文档
const reasoningSteps = thinking.filter(s => s.stage === 'reasoning');
const retrievedStep = thinking.find(s => s.stage === 'retrieved');
const hasReasoning = reasoningSteps.length > 0;
const hasDocs = retrievedStep?.details && retrievedStep.details.length > 0;
// 没有推理内容也没有文档详情时不显示
if (!hasReasoning && !hasDocs && !isStreaming) return null;
// 合并推理文本
const reasoningText = reasoningSteps.map(s => s.message).join('');
// 汇总信息
const retrievedStep = statusSteps.find(s => s.stage === 'retrieved');
const totalTime = statusSteps.reduce((sum, s) => sum + (s.time || 0), 0);
// 流式时显示最后状态
const lastStatusStep = statusSteps[statusSteps.length - 1];
// 流式时是否正在推理
const isReasoningNow = isStreaming && thinking[thinking.length - 1]?.stage === 'reasoning';
// 折叠标题
const collapsedTitle = isStreaming
? isReasoningNow
? '深度思考中...'
: lastStatusStep?.message || '思考中...'
? isReasoningNow ? '思考中...' : '检索中...'
: hasReasoning
? `思考过程 (${reasoningText.length} 字)`
: `思考过程${totalTime > 0 ? ` (${totalTime.toFixed(1)}s)` : ''}`;
: retrievedStep
? `检索到 ${retrievedStep.doc_count} 篇相关文档`
: '思考过程';
return (
<div className="mb-2.5">
@@ -62,37 +61,14 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
</svg>
<Brain className={cn("h-3 w-3", isReasoningNow && "animate-pulse text-primary")} />
<span>{collapsedTitle}</span>
{retrievedStep?.doc_count != null && (
<span className="opacity-60 ml-1">· {retrievedStep.doc_count} </span>
)}
{totalTime > 0 && !isStreaming && (
<span className="opacity-50 ml-1">· {totalTime.toFixed(1)}s</span>
)}
</button>
{expanded && (
<div className="mt-1.5 ml-4 space-y-2 text-xs border-l-2 border-border pl-3">
{/* 状态步骤 */}
{statusSteps.map((step, index) => (
<div key={`s-${index}`} className="flex items-center gap-1.5 text-muted-foreground">
{step.stage === 'retrieving' ? (
<FileSearch className="h-3 w-3" />
) : step.stage === 'retrieved' ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : step.stage === 'generating' ? (
<Sparkles className="h-3 w-3" />
) : (
<div className="h-1.5 w-1.5 rounded-full bg-current" />
)}
<span>{step.message}</span>
{step.time != null && <span className="opacity-50">{step.time.toFixed(1)}s</span>}
</div>
))}
{/* 检索到的文档详情 */}
{retrievedStep?.details && retrievedStep.details.length > 0 && (
<div className="mt-1 space-y-1">
{hasDocs && (
<div className="space-y-1">
<div className="text-muted-foreground font-medium"></div>
{retrievedStep.details.map((detail: string, i: number) => (
{retrievedStep?.details?.map((detail: string, i: number) => (
<div key={i} className="text-muted-foreground/80 pl-2 border-l border-border/50">
{detail}
</div>
@@ -103,9 +79,6 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
{/* 推理内容 */}
{hasReasoning && (
<div className="mt-1">
<div className="text-muted-foreground font-medium mb-1">
{isReasoningNow ? '推理进行中...' : '推理过程:'}
</div>
<div className="text-foreground/80 whitespace-pre-wrap leading-relaxed bg-muted/30 rounded-lg p-2.5 max-h-80 overflow-y-auto">
{reasoningText}
</div>