ccdae76dd3
The rectification surface had its own activity pipeline: a trace panel with a 20px canvas orb clipped inside a 14px marker, a list that never collapsed, a second receipt disclosure under every reply, an inline failure banner and a staged label while regenerating. Its trace and receipt are now projected onto ConsultationTimelineRow so both surfaces render one ConsultationRunTimeline with one live marker and one settled summary; receipt methods become source chips on the last completed row. The unreachable sections-report/step-tree path, the trace panel, the receipt component and the thinking-orbs dependency are removed. The server-side drop of rectification thinking deltas is untouched. BUG-476 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Check } from "lucide-react";
|
|
|
|
import { InlineSpinner } from "@/components/inline-spinner";
|
|
import { activityCompletedSteps, activityElapsedLabel } from "@/lib/chat-message-view";
|
|
|
|
export type AgentActivityState = "working" | "searching" | "solving" | "listening" | "composing" | "shaping";
|
|
|
|
const labels = {
|
|
working: "正在处理任务…",
|
|
searching: "正在搜索相关信息…",
|
|
solving: "正在分析问题…",
|
|
listening: "正在聆听…",
|
|
composing: "正在组织回答…",
|
|
shaping: "正在生成结果…",
|
|
} as const satisfies Record<AgentActivityState, string>;
|
|
|
|
function ActivityElapsed({ startedAt }: Readonly<{ startedAt: number }>) {
|
|
const [now, setNow] = useState(() => Date.now());
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
|
return () => window.clearInterval(timer);
|
|
}, []);
|
|
const label = activityElapsedLabel(startedAt, now);
|
|
if (!label) return null;
|
|
return <span className="agent-activity-status__elapsed" aria-hidden="true">{label}</span>;
|
|
}
|
|
|
|
function MessageThinkingTrace({
|
|
text,
|
|
hasAnswer,
|
|
}: Readonly<{
|
|
text: string;
|
|
hasAnswer: boolean;
|
|
}>) {
|
|
const [userOpen, setUserOpen] = useState<boolean | null>(null);
|
|
const open = userOpen ?? !hasAnswer;
|
|
if (!text.trim()) return null;
|
|
return (
|
|
<details
|
|
className="message-thinking"
|
|
open={open}
|
|
onToggle={(event) => {
|
|
setUserOpen((event.currentTarget as HTMLDetailsElement).open);
|
|
}}
|
|
>
|
|
<summary>思考</summary>
|
|
<div className="message-thinking-body">{text}</div>
|
|
</details>
|
|
);
|
|
}
|
|
|
|
function VargaTraceStep({ sentence }: Readonly<{ sentence: string }>) {
|
|
return (
|
|
<li className="agent-thinking-step is-done">
|
|
<span className="agent-thinking-marker" aria-hidden="true">
|
|
<Check />
|
|
</span>
|
|
<span>{sentence}</span>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback activity panel for an assistant row that carries no step timeline.
|
|
* Both chat surfaces normally render `ConsultationRunTimeline`; this remains
|
|
* for rows that only have a phase label, a completed trail or thinking text.
|
|
* The live marker is the shared `InlineSpinner`, the same one the timeline uses.
|
|
*/
|
|
export function AgentActivityStatus({
|
|
state,
|
|
label = labels[state],
|
|
startedAt,
|
|
completedTrail,
|
|
thinkingText,
|
|
vargaSentence,
|
|
hasAnswer = false,
|
|
showLive = true,
|
|
}: Readonly<{
|
|
state: AgentActivityState;
|
|
label?: string;
|
|
startedAt?: number;
|
|
completedTrail?: string;
|
|
thinkingText?: string;
|
|
vargaSentence?: string | null;
|
|
hasAnswer?: boolean;
|
|
showLive?: boolean;
|
|
}>) {
|
|
const completedSteps = activityCompletedSteps(completedTrail);
|
|
const live = showLive && !hasAnswer;
|
|
const varga = vargaSentence?.trim() ?? "";
|
|
const showVarga = Boolean(varga);
|
|
if (!live && completedSteps.length === 0 && !thinkingText?.trim() && !showVarga) return null;
|
|
|
|
return (
|
|
<div className="agent-thinking-panel agent-activity-status">
|
|
{(live || completedSteps.length > 0 || showVarga) && (
|
|
<ol className="agent-thinking-timeline">
|
|
{completedSteps.map((step) => (
|
|
<li className="agent-thinking-step is-done" key={step}>
|
|
<span className="agent-thinking-marker" aria-hidden="true">
|
|
<Check />
|
|
</span>
|
|
<span>{step}</span>
|
|
</li>
|
|
))}
|
|
{live ? (
|
|
<li className="agent-thinking-step is-live">
|
|
<span className="agent-thinking-marker is-live-marker" aria-hidden="true">
|
|
<InlineSpinner size={12} />
|
|
</span>
|
|
<span className="agent-activity-status__live" role="status">
|
|
<span key={label} className="agent-activity-status__text">{label}</span>
|
|
{startedAt ? <ActivityElapsed key={startedAt} startedAt={startedAt} /> : null}
|
|
</span>
|
|
</li>
|
|
) : null}
|
|
{showVarga ? <VargaTraceStep sentence={varga} /> : null}
|
|
</ol>
|
|
)}
|
|
{thinkingText ? <MessageThinkingTrace text={thinkingText} hasAnswer={hasAnswer} /> : null}
|
|
</div>
|
|
);
|
|
}
|