Files
Jyotisha/frontend/src/components/agent-activity-status.tsx
T
Jesse_Chen ccdae76dd3 refactor(chat): render rectification activity through the shared step timeline
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
2026-09-02 04:02:15 +00:00

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>
);
}