Files
Jyotisha/frontend/src/components/agent-activity-status.tsx
T
Jesse_Chen 59559d4b24 fix(web): persist thinking, title sessions distinctly, and send follow-ups from the answer
Thinking disappeared on failure and never reached session storage. Keep the
sanitized chain on disk and on errors, and regroup the sidebar around reports,
charts, favorites, and dated history titles.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 09:10:47 +08:00

118 lines
3.6 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";
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>
);
}
export function AgentActivityStatus({
state,
label = labels[state],
startedAt,
completedTrail,
thinkingText,
hasAnswer = false,
showLive = true,
}: Readonly<{
state: AgentActivityState;
label?: string;
startedAt?: number;
completedTrail?: string;
thinkingText?: string;
hasAnswer?: boolean;
showLive?: boolean;
}>) {
const completedSteps = activityCompletedSteps(completedTrail);
const live = showLive && !hasAnswer;
if (!live && completedSteps.length === 0 && !thinkingText?.trim()) return null;
return (
<div className="agent-thinking-panel agent-activity-status">
{(live || completedSteps.length > 0) && (
<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}
</ol>
)}
{thinkingText ? <MessageThinkingTrace text={thinkingText} hasAnswer={hasAnswer} /> : null}
</div>
);
}