Files
Jyotisha/frontend/src/components/agent-activity-status.tsx
T
Jesse_ChenandCursor 7a4360d848
Independent Staging Quality Gate / validate (push) Failing after 11m44s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(web): keep rectification stages aligned and stop jump overlay covering choices
Progress and spoken text shared a 24px consultation-report gap, and the jump chip covered option D. Put D-chart names back on the activity strip from evidence rescore methods.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 19:31:14 +08:00

197 lines
6.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { Check } from "lucide-react";
import type { OrbState } from "thinking-orbs";
import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
import { activityCompletedSteps, activityElapsedLabel } from "@/lib/chat-message-view";
import type { AgentActivityTraceItem } from "@/lib/agent-activity-trace";
const labels = {
working: "正在处理任务…",
searching: "正在搜索相关信息…",
solving: "正在分析问题…",
listening: "正在聆听…",
composing: "正在组织回答…",
shaping: "正在生成结果…",
} as const satisfies Record<OrbState, string>;
const importThinkingOrb = () => import("thinking-orbs");
const ThinkingOrb = dynamic(async () => (await importThinkingOrb()).ThinkingOrb, {
loading: () => (
<span aria-hidden="true" style={{ display: "block", flex: "0 0 auto", height: 20, width: 20 }} />
),
ssr: false,
});
prefetchOnIdle(importThinkingOrb);
export type AgentActivityState = OrbState;
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>
);
}
export function AgentActivityStatus({
state,
label = labels[state],
startedAt,
completedTrail,
thinkingText,
activityTrace,
vargaSentence,
hasAnswer = false,
showLive = true,
}: Readonly<{
state: AgentActivityState;
label?: string;
startedAt?: number;
completedTrail?: string;
thinkingText?: string;
activityTrace?: readonly AgentActivityTraceItem[];
vargaSentence?: string | null;
hasAnswer?: boolean;
showLive?: boolean;
}>) {
const completedSteps = activityCompletedSteps(completedTrail);
const live = showLive && !hasAnswer;
const trace = activityTrace ?? [];
const varga = vargaSentence?.trim() ?? "";
const showVarga = Boolean(varga) && !trace.some((item) => item.label === varga);
if (trace.length > 0) {
return (
<div className="agent-thinking-panel agent-activity-status">
<ol className="agent-thinking-timeline">
{trace.map((item) => (
<TraceStep
key={item.id}
item={item}
state={state}
hasAnswer={hasAnswer}
/>
))}
{showVarga ? <VargaTraceStep sentence={varga} /> : null}
</ol>
</div>
);
}
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">
<ThinkingOrb aria-hidden="true" state={state} size={20} />
</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>
);
}
function TraceStep({
item,
state,
hasAnswer,
}: Readonly<{
item: AgentActivityTraceItem;
state: AgentActivityState;
hasAnswer: boolean;
}>) {
if (item.kind === "think") {
return (
<li className={`agent-thinking-step is-think is-${item.status}`}>
{item.text?.trim() ? (
<MessageThinkingTrace text={item.text} hasAnswer={hasAnswer || item.status === "done"} />
) : (
<>
<span className={`agent-thinking-marker${item.status === "live" ? " is-live-marker" : ""}`} aria-hidden="true">
{item.status === "live" ? <ThinkingOrb aria-hidden="true" state={state} size={20} /> : <Check />}
</span>
<span>{item.label}</span>
</>
)}
</li>
);
}
return (
<li className={`agent-thinking-step is-${item.status}`}>
<span className={`agent-thinking-marker${item.status === "live" ? " is-live-marker" : ""}`} aria-hidden="true">
{item.status === "live" ? <ThinkingOrb aria-hidden="true" state={state} size={20} /> : <Check />}
</span>
{item.status === "live" ? (
<span className="agent-activity-status__live" role="status">
<span key={item.label} className="agent-activity-status__text">{item.label}</span>
{item.startedAt ? <ActivityElapsed key={item.startedAt} startedAt={item.startedAt} /> : null}
</span>
) : (
<span>{item.label}</span>
)}
</li>
);
}