Files
ln-bi/src/modules/assets/assistant/ThinkingState.tsx
T

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