59559d4b24
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>
118 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|