113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
// Adapted from Beautiful UI's MIT-licensed ThinkingState (Steps).
|
|
// Uses real server progress rather than the reference's timed demo sequence.
|
|
import { useEffect, useId, useState } from "react";
|
|
import { Check, ChevronDown, CircleAlert, LoaderCircle, Pause, Sparkles } from "lucide-react";
|
|
|
|
import { useMobileLayout } from '../useMobileLayout';
|
|
|
|
export function ThinkingState({
|
|
steps,
|
|
working,
|
|
started,
|
|
failed,
|
|
stopped,
|
|
outcome,
|
|
}: {
|
|
steps: string[];
|
|
working: boolean;
|
|
started: number;
|
|
failed?: boolean;
|
|
stopped?: boolean;
|
|
outcome?: string;
|
|
}) {
|
|
const mobile = useMobileLayout();
|
|
const traceId = useId();
|
|
const [manualExpanded, setExpanded] = useState<boolean | null>(null);
|
|
const [elapsed, setElapsed] = useState(0);
|
|
useEffect(() => {
|
|
if (!working) return;
|
|
const update = () => setElapsed((Date.now() - started) / 1000);
|
|
update();
|
|
const timer = window.setInterval(update, 1000);
|
|
return () => window.clearInterval(timer);
|
|
}, [working, started]);
|
|
const expanded = manualExpanded ?? (working && !mobile);
|
|
const StatusIcon = working ? LoaderCircle : stopped ? Pause : failed ? CircleAlert : Sparkles;
|
|
return (
|
|
<div className={`aq-thinking${working ? " aq-thinking-working" : ""}`} data-state={working ? "working" : stopped ? "stopped" : failed ? "failed" : "complete"}>
|
|
<button
|
|
type="button"
|
|
aria-expanded={expanded}
|
|
aria-controls={traceId}
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
<StatusIcon size={16} className={working ? "aq-spin" : ""} aria-hidden="true" />
|
|
<span className={`aq-thinking-title${working ? " aq-shimmer" : ""}`}>
|
|
{working
|
|
? (mobile ? steps.at(-1) || "正在分析" : "正在分析")
|
|
: stopped
|
|
? "已由用户停止"
|
|
: failed
|
|
? "查询失败"
|
|
: outcome === "unsupported"
|
|
? "暂不支持该指标"
|
|
: outcome === "empty"
|
|
? "查询完成 · 暂无匹配数据"
|
|
: outcome === "partial"
|
|
? "已完成可查部分"
|
|
: outcome === "clarification"
|
|
? "等待补充条件"
|
|
: outcome === "help"
|
|
? "已回答"
|
|
: "分析完成"}
|
|
</span>
|
|
<span className="aq-thinking-meta">
|
|
{steps.length > 0 && <span>{steps.length} 个步骤</span>}
|
|
{elapsed > 0 && <span className="aq-time">{Math.floor(elapsed)} 秒</span>}
|
|
</span>
|
|
<ChevronDown
|
|
size={14}
|
|
className={expanded ? "aq-rotated" : ""}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<div
|
|
id={traceId}
|
|
className="aq-trace-collapse"
|
|
aria-hidden={!expanded}
|
|
style={{
|
|
gridTemplateRows: expanded ? "1fr" : "0fr",
|
|
opacity: expanded ? 1 : 0,
|
|
}}
|
|
>
|
|
<div className="aq-trace-clip">
|
|
<ol className="aq-trace">
|
|
{!steps.length && <li><span>{working ? "正在连接分析服务…" : "暂无执行步骤"}</span></li>}
|
|
{steps.map((step, index) => (
|
|
<li key={`${index}-${step}`} data-current={index === steps.length - 1}>
|
|
{working && index === steps.length - 1 ? (
|
|
<LoaderCircle
|
|
className="aq-spin"
|
|
size={14}
|
|
aria-hidden="true"
|
|
/>
|
|
) : index === steps.length - 1 && (failed || stopped) ? (
|
|
<StatusIcon size={14} aria-hidden="true" />
|
|
) : (
|
|
<Check size={14} aria-hidden="true" />
|
|
)}
|
|
<span>{step}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
{working && (
|
|
<span className="sr-only" role="status">
|
|
{steps.at(-1) || "正在连接分析服务"}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|