feat(i18n): implement internationalization framework and update UI components for language support
This commit is contained in:
@@ -2,30 +2,27 @@ import { useEffect, useId, useState, type ReactNode } from "react";
|
||||
import { App, Button, Image, Modal } from "antd";
|
||||
import { Brain, CheckCircle2, ChevronDown, ChevronRight, Circle, CircleAlert, Copy, ExternalLink, FilePenLine, FileText, FolderOpen, ListChecks, LoaderCircle, Search, ShieldAlert, TerminalSquare, Wrench, XCircle } from "lucide-react";
|
||||
import { Streamdown, type LinkSafetyModalProps } from "streamdown";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import i18n from "@/i18n";
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useAgentStore, type AgentPendingApproval } from "@/stores/use-agent-store";
|
||||
import { revealAgentLocalFile } from "@/services/api/canvas-agent";
|
||||
|
||||
const streamdownProps = {
|
||||
const streamdownProps = () => ({
|
||||
className: "agent-streamdown",
|
||||
controls: { code: { copy: true, download: false }, table: { copy: true, download: false, fullscreen: false } },
|
||||
linkSafety: { enabled: true, renderModal: (props: LinkSafetyModalProps) => <AgentLinkModal {...props} /> },
|
||||
lineNumbers: false,
|
||||
translations: {
|
||||
close: "关闭",
|
||||
copied: "已复制",
|
||||
copyCode: "复制代码",
|
||||
copyLink: "复制链接",
|
||||
externalLinkWarning: "即将打开以下外部链接,请确认链接可信。",
|
||||
openExternalLink: "打开外部链接?",
|
||||
openLink: "继续打开",
|
||||
close: tr("close"), copied: tr("copied"), copyCode: tr("copyCode"), copyLink: tr("copyLink"), externalLinkWarning: tr("externalWarning"), openExternalLink: tr("openExternal"), openLink: tr("continueOpen"),
|
||||
},
|
||||
} as const;
|
||||
} as const);
|
||||
const streamdownAnimation = { duration: 20, stagger: 0, sep: "word" } as const;
|
||||
|
||||
function AgentLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const copyText = useCopyText();
|
||||
const localPath = localFilePath(url);
|
||||
@@ -36,26 +33,26 @@ function AgentLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProp
|
||||
setOpening(true);
|
||||
try {
|
||||
await revealAgentLocalFile(endpoint, token, localPath);
|
||||
message.success("已在文件管理器中定位");
|
||||
message.success(t("agent.message.revealed"));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "无法打开本地文件");
|
||||
message.error(error instanceof Error ? error.message : t("agent.message.openLocalFailed"));
|
||||
} finally {
|
||||
setOpening(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal open={isOpen} onCancel={onClose} footer={null} centered width={420} title={localPath ? "打开本地文件?" : "打开外部链接?"}>
|
||||
<Modal open={isOpen} onCancel={onClose} footer={null} centered width={420} title={t(localPath ? "agent.message.openLocal" : "agent.message.openExternal")}>
|
||||
<div className="text-sm text-black/55 dark:text-white/55">
|
||||
{localPath ? "将在本机文件管理器中定位该路径,不会通过浏览器打开。" : "即将打开以下外部链接,请确认链接可信。"}
|
||||
{t(localPath ? "agent.message.localDescription" : "agent.message.externalDescription")}
|
||||
</div>
|
||||
<div className="mt-4 max-h-32 overflow-auto break-all rounded-lg bg-black/[.035] px-3 py-2.5 font-mono text-xs leading-5 dark:bg-white/[.06]">{localPath || url}</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button type="text" icon={<Copy className="size-4" />} onClick={() => copyText(localPath || url, localPath ? "路径已复制" : "链接已复制")}>
|
||||
{localPath ? "复制路径" : "复制链接"}
|
||||
<Button type="text" icon={<Copy className="size-4" />} onClick={() => copyText(localPath || url, t(localPath ? "agent.message.pathCopied" : "agent.message.linkCopied"))}>
|
||||
{t(localPath ? "agent.message.copyPath" : "agent.message.copyLink")}
|
||||
</Button>
|
||||
<Button type="text" loading={opening} icon={localPath ? <FolderOpen className="size-4" /> : <ExternalLink className="size-4" />} onClick={open}>
|
||||
{localPath ? "在文件管理器中显示" : "继续打开"}
|
||||
{t(localPath ? "agent.message.showInFolder" : "agent.message.continueOpen")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -117,7 +114,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: {
|
||||
}
|
||||
if (item.role === "tool") {
|
||||
if (objectField(item.detail, "status") === "pending") return <AgentPendingToolCard summary={item.text} detail={item.detail} theme={theme} onReject={() => onRejectTool?.(item.id)} onApprove={() => onApproveTool?.(item.id)} />;
|
||||
return <AgentToolCard title={item.title || "工具调用"} text={item.text} detail={item.detail} theme={theme} />;
|
||||
return <AgentToolCard title={item.title || tr("toolCall")} text={item.text} detail={item.detail} theme={theme} />;
|
||||
}
|
||||
return (
|
||||
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
|
||||
@@ -128,7 +125,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: {
|
||||
{isUser ? (
|
||||
<div className="whitespace-pre-wrap break-words">{item.text}</div>
|
||||
) : (
|
||||
<Streamdown {...streamdownProps} animated={streamdownAnimation} isAnimating={!!item.streamId}>{item.text}</Streamdown>
|
||||
<Streamdown {...streamdownProps()} animated={streamdownAnimation} isAnimating={!!item.streamId}>{item.text}</Streamdown>
|
||||
)}
|
||||
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} alignRight={isUser} /> : null}
|
||||
{item.meta ? <div className={`mt-1 text-[11px] tabular-nums opacity-55 ${isUser ? "text-right" : ""}`}>{item.meta}</div> : null}
|
||||
@@ -138,6 +135,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: {
|
||||
}
|
||||
|
||||
export function AgentPendingToolCard({ summary, detail, theme, onReject, onApprove }: { summary: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onReject?: () => void; onApprove?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const view = userDetail(detail);
|
||||
return (
|
||||
<div className="min-w-0 rounded-xl border px-3 py-3" style={{ borderColor: "rgba(217,119,6,.28)", background: "rgba(217,119,6,.025)", color: theme.node.text }}>
|
||||
@@ -145,7 +143,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
|
||||
<summary className={`list-none ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-medium leading-5">
|
||||
<CircleAlert className="size-4 shrink-0 text-amber-600" />
|
||||
<span className="min-w-0 flex-1">等待确认</span>
|
||||
<span className="min-w-0 flex-1">{t("agent.message.awaitingConfirmation")}</span>
|
||||
{view ? <ChevronDown className="size-3.5 shrink-0 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
|
||||
</div>
|
||||
<div className="mt-1 pl-6 text-sm leading-5" style={{ color: theme.node.muted }}>{summary}</div>
|
||||
@@ -155,10 +153,10 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
|
||||
{onReject || onApprove ? (
|
||||
<div className="mt-3 flex justify-end gap-2 border-t pt-3" style={{ borderColor: theme.node.stroke }}>
|
||||
<Button danger type="text" className="!h-8" icon={<XCircle className="size-3.5" />} onClick={() => onReject?.()}>
|
||||
拒绝执行
|
||||
{t("agent.message.reject")}
|
||||
</Button>
|
||||
<Button type="text" className="!h-8" icon={<CheckCircle2 className="size-3.5" />} style={{ color: "#16a34a" }} onClick={() => onApprove?.()}>
|
||||
批准执行
|
||||
{t("agent.message.approve")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -167,9 +165,10 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
|
||||
}
|
||||
|
||||
export function AgentApprovalCard({ approval, theme, onDecision }: { approval: AgentPendingApproval; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onDecision: (decision: "accept" | "acceptForSession" | "decline") => void }) {
|
||||
const { t } = useTranslation();
|
||||
const isFile = approval.method === "item/fileChange/requestApproval";
|
||||
const isNetwork = Boolean(approval.networkApprovalContext);
|
||||
const title = isNetwork ? "请求网络访问" : isFile ? "请求编辑文件" : approval.method === "item/permissions/requestApproval" ? "请求扩展权限" : "请求执行命令";
|
||||
const title = t(isNetwork ? "agent.message.networkApproval" : isFile ? "agent.message.fileApproval" : approval.method === "item/permissions/requestApproval" ? "agent.message.permissionApproval" : "agent.message.commandApproval");
|
||||
const target = isNetwork ? approvalTarget(approval.networkApprovalContext) : isFile ? approval.grantRoot || approval.cwd : commandText(approval.command) || approval.cwd;
|
||||
return (
|
||||
<div className="min-w-0 rounded-xl border px-3 py-3" style={{ borderColor: "rgba(234,88,12,.32)", background: "rgba(234,88,12,.035)", color: theme.node.text }}>
|
||||
@@ -182,9 +181,9 @@ export function AgentApprovalCard({ approval, theme, onDecision }: { approval: A
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap justify-end gap-1.5 border-t pt-3" style={{ borderColor: theme.node.stroke }}>
|
||||
<Button danger type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "decline"} onClick={() => onDecision("decline")}>拒绝</Button>
|
||||
<Button type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "accept"} onClick={() => onDecision("accept")}>允许一次</Button>
|
||||
<Button type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "acceptForSession"} style={{ color: "#ea580c" }} onClick={() => onDecision("acceptForSession")}>本会话允许</Button>
|
||||
<Button danger type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "decline"} onClick={() => onDecision("decline")}>{t("agent.message.decline")}</Button>
|
||||
<Button type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "accept"} onClick={() => onDecision("accept")}>{t("agent.message.allowOnce")}</Button>
|
||||
<Button type="text" className="!h-8" disabled={Boolean(approval.deciding)} loading={approval.deciding === "acceptForSession"} style={{ color: "#ea580c" }} onClick={() => onDecision("acceptForSession")}>{t("agent.message.allowSession")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -226,6 +225,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
|
||||
}
|
||||
|
||||
function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const { t } = useTranslation();
|
||||
const status = String(objectField(detail, "status") || "");
|
||||
const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status);
|
||||
return (
|
||||
@@ -233,12 +233,12 @@ function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?:
|
||||
<summary className="cursor-pointer list-none py-1 text-sm" style={{ color: theme.node.muted }}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{running ? <LoaderCircle className="size-4 shrink-0 animate-spin" /> : <Brain className="size-4 shrink-0" />}
|
||||
<span>{running ? "正在思考" : "思考摘要"}</span>
|
||||
<span>{t(running ? "agent.message.thinking" : "agent.events.reasoning")}</span>
|
||||
<ChevronRight className="size-3.5 shrink-0 transition-transform group-open:rotate-90" />
|
||||
</div>
|
||||
</summary>
|
||||
<div className="break-words pb-1 pl-6 pr-2 text-xs leading-5 [&_code]:rounded [&_code]:px-1 [&_p]:my-1 [&_pre]:my-2" style={{ color: theme.node.muted }}>
|
||||
<Streamdown {...streamdownProps} animated={streamdownAnimation} isAnimating={running}>{text}</Streamdown>
|
||||
<Streamdown {...streamdownProps()} animated={streamdownAnimation} isAnimating={running}>{text}</Streamdown>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
@@ -247,14 +247,13 @@ function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?:
|
||||
type AgentCommandItem = Pick<AgentChatMessageItem, "id" | "text" | "detail">;
|
||||
|
||||
export function AgentCommandGroup({ items, theme }: { items: AgentCommandItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const { t } = useTranslation();
|
||||
const states = items.map((item) => commandViewState(item.detail));
|
||||
const running = states.some((state) => state.running);
|
||||
const failed = states.filter((state) => state.failed).length;
|
||||
const expandable = items.some((item) => Boolean(item.text.trim() || userDetail(item.detail)));
|
||||
const color = running ? "#d97706" : failed ? "#dc2626" : theme.node.muted;
|
||||
const label = running
|
||||
? items.length > 1 ? `正在执行 ${items.length} 条命令` : "正在执行命令"
|
||||
: `已执行 ${items.length} 条命令${failed ? ` · ${failed} 条失败` : ""}`;
|
||||
const label = running ? t(items.length > 1 ? "agent.message.commandsRunning" : "agent.message.commandRunning", { count: items.length }) : t("agent.message.commandsCompleted", { count: items.length, failed: failed ? t("agent.message.commandsFailed", { count: failed }) : "" });
|
||||
const header = (
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm" style={{ color }}>
|
||||
{running ? <LoaderCircle className="size-4 shrink-0 animate-spin" /> : <TerminalSquare className="size-4 shrink-0" />}
|
||||
@@ -285,16 +284,17 @@ function AgentSingleCommand({ item, theme }: { item: AgentCommandItem; theme: (t
|
||||
}
|
||||
|
||||
function AgentCommandEntry({ item, index, theme }: { item: AgentCommandItem; index: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const detailId = useId();
|
||||
const view = userDetail(item.detail);
|
||||
const state = commandViewState(item.detail);
|
||||
const status = state.failed ? "执行失败" : state.running ? "执行中" : "已完成";
|
||||
const status = t(state.failed ? "agent.message.failed" : state.running ? "agent.message.running" : "agent.message.completed");
|
||||
const color = state.failed ? "#dc2626" : state.running ? "#d97706" : "#16a34a";
|
||||
const content = (
|
||||
<>
|
||||
<span className="w-4 shrink-0 text-center text-[10px] tabular-nums opacity-50" style={{ color: theme.node.muted }}>{index + 1}</span>
|
||||
<code className="min-w-0 flex-1 truncate text-[11px] leading-5" style={{ color: theme.node.text }} title={item.text}>{item.text || "命令"}</code>
|
||||
<code className="min-w-0 flex-1 truncate text-[11px] leading-5" style={{ color: theme.node.text }} title={item.text}>{item.text || t("agent.message.command")}</code>
|
||||
<span className="shrink-0" style={{ color }} title={status} aria-label={status}>
|
||||
{state.running ? <LoaderCircle className="size-3.5 animate-spin" /> : state.failed ? <XCircle className="size-3.5" /> : <CheckCircle2 className="size-3.5" />}
|
||||
</span>
|
||||
@@ -350,6 +350,7 @@ function AgentPlanCard({ title, plan, theme }: { title: string; plan: PlanDetail
|
||||
}
|
||||
|
||||
export function AgentWorkingMessage({ text, detail, status = "running", mcpStatuses = [], activityKey, theme }: { text: string; detail?: string; status?: "running" | "ready" | "error"; mcpStatuses?: Array<{ name: string; status: "running" | "ready" | "error"; detail: string }>; activityKey: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const { t } = useTranslation();
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
useEffect(() => {
|
||||
const startedAt = Date.now();
|
||||
@@ -378,15 +379,15 @@ export function AgentWorkingMessage({ text, detail, status = "running", mcpStatu
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{status === "running" && elapsed >= 30 ? <div className="mt-1 text-xs leading-5 opacity-65" style={{ color: theme.node.muted }}>响应时间较长,但任务仍在运行。可以继续等待,或点击输入框右侧的停止按钮结束本轮。</div> : null}
|
||||
{status === "running" && elapsed >= 30 ? <div className="mt-1 text-xs leading-5 opacity-65" style={{ color: theme.node.muted }}>{t("agent.message.slowResponse")}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function waitingTime(seconds: number) {
|
||||
if (seconds < 60) return `已等待 ${seconds} 秒`;
|
||||
if (seconds < 60) return tr("waitingSeconds", { seconds });
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `已等待 ${minutes} 分 ${seconds % 60} 秒`;
|
||||
return tr("waitingMinutes", { minutes, seconds: seconds % 60 });
|
||||
}
|
||||
|
||||
function commandText(value: unknown) {
|
||||
@@ -406,6 +407,7 @@ type PlanDetail = { status: string; tasks: PlanTask[]; explanation?: string };
|
||||
type UserDetail = { kind?: string; status?: string; rows?: Array<{ label: string; value: string }>; output?: string; files?: Array<{ path: string; action?: string }> };
|
||||
|
||||
function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mt-3 space-y-2.5 border-t pt-3 text-xs" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
|
||||
{detail.rows?.length ? (
|
||||
@@ -420,7 +422,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo
|
||||
) : null}
|
||||
{detail.files?.length ? (
|
||||
<div className="space-y-1.5">
|
||||
<div className="opacity-60">涉及文件</div>
|
||||
<div className="opacity-60">{t("agent.message.files")}</div>
|
||||
{detail.files.map((file) => (
|
||||
<div key={`${file.action}-${file.path}`} className="flex items-start gap-2">
|
||||
<FileText className="mt-0.5 size-3.5 shrink-0" />
|
||||
@@ -432,7 +434,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo
|
||||
) : null}
|
||||
{detail.output ? (
|
||||
<div className="space-y-1.5">
|
||||
<div className="opacity-60">{detail.status === "failed" || detail.status === "error" ? "错误信息" : "运行输出"}</div>
|
||||
<div className="opacity-60">{t(detail.status === "failed" || detail.status === "error" ? "agent.message.errorInfo" : "agent.message.output")}</div>
|
||||
<pre className="thin-scrollbar max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-lg px-3 py-2 font-mono text-[11px] leading-4" style={{ background: theme.toolbar.panel, color: theme.node.text }}>{detail.output}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -441,6 +443,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo
|
||||
}
|
||||
|
||||
function AgentMessageAttachments({ attachments, alignRight }: { attachments: AgentChatAttachment[]; alignRight?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
return (
|
||||
<>
|
||||
@@ -450,7 +453,7 @@ function AgentMessageAttachments({ attachments, alignRight }: { attachments: Age
|
||||
key={item.id}
|
||||
src={item.url}
|
||||
alt={item.name}
|
||||
title="点击查看大图"
|
||||
title={t("agent.message.viewLarge")}
|
||||
className="size-10 cursor-zoom-in rounded-lg object-cover"
|
||||
draggable={false}
|
||||
onClick={() => setPreviewUrl(item.url)}
|
||||
@@ -459,7 +462,7 @@ function AgentMessageAttachments({ attachments, alignRight }: { attachments: Age
|
||||
</div>
|
||||
{previewUrl ? (
|
||||
<div className="hidden">
|
||||
<Image src={previewUrl} alt="图片附件预览" preview={{ visible: true, src: previewUrl, onVisibleChange: (visible) => !visible && setPreviewUrl(null) }} />
|
||||
<Image src={previewUrl} alt={t("agent.message.attachmentPreview")} preview={{ visible: true, src: previewUrl, onVisibleChange: (visible) => !visible && setPreviewUrl(null) }} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
@@ -470,12 +473,12 @@ function toolCardState(title: string, text: string, detail?: unknown) {
|
||||
const raw = `${title} ${text} ${normalizeText(objectField(detail, "error"))}`;
|
||||
const lower = raw.toLowerCase();
|
||||
const status = String(objectField(detail, "status") || "").toLowerCase();
|
||||
if (status === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: "未生效", color: "#d97706", icon: <CircleAlert className="size-4" />, isError: false };
|
||||
if (["declined", "rejected", "cancelled", "canceled"].includes(status) || /拒绝|取消/.test(raw)) return { label: "已取消", color: "#dc2626", icon: <XCircle className="size-4" />, isError: true };
|
||||
if (["failed", "error"].includes(status) || /失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: "执行失败", color: "#dc2626", icon: <XCircle className="size-4" />, isError: true };
|
||||
if (["inprogress", "in_progress", "running", "started", "pending"].includes(status)) return { label: "进行中", color: "#d97706", icon: <LoaderCircle className="size-4 animate-spin" />, isError: false };
|
||||
if (["completed", "succeeded", "success"].includes(status) || /完成|成功/.test(raw)) return { label: "已完成", color: "#16a34a", icon: <CheckCircle2 className="size-4" />, isError: false };
|
||||
return { label: "已记录", color: "#2563eb", icon: <Wrench className="size-4" />, isError: false };
|
||||
if (status === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: tr("noEffect"), color: "#d97706", icon: <CircleAlert className="size-4" />, isError: false };
|
||||
if (["declined", "rejected", "cancelled", "canceled"].includes(status) || /拒绝|取消/.test(raw)) return { label: tr("canceled"), color: "#dc2626", icon: <XCircle className="size-4" />, isError: true };
|
||||
if (["failed", "error"].includes(status) || /失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: tr("failed"), color: "#dc2626", icon: <XCircle className="size-4" />, isError: true };
|
||||
if (["inprogress", "in_progress", "running", "started", "pending"].includes(status)) return { label: tr("running"), color: "#d97706", icon: <LoaderCircle className="size-4 animate-spin" />, isError: false };
|
||||
if (["completed", "succeeded", "success"].includes(status) || /完成|成功/.test(raw)) return { label: tr("completed"), color: "#16a34a", icon: <CheckCircle2 className="size-4" />, isError: false };
|
||||
return { label: tr("recorded"), color: "#2563eb", icon: <Wrench className="size-4" />, isError: false };
|
||||
}
|
||||
|
||||
function toolIcon(kind: string | undefined, fallback: ReactNode) {
|
||||
@@ -486,17 +489,17 @@ function toolIcon(kind: string | undefined, fallback: ReactNode) {
|
||||
}
|
||||
|
||||
function planCardState(plan: PlanDetail, completed: number) {
|
||||
if (plan.status === "failed") return { label: "执行失败", color: "#dc2626" };
|
||||
if (["interrupted", "cancelled", "canceled"].includes(plan.status)) return { label: "已停止", color: "#d97706" };
|
||||
if (completed === plan.tasks.length) return { label: "已完成", color: "#16a34a" };
|
||||
if (plan.status === "finished") return { label: "已结束", color: "#2563eb" };
|
||||
return { label: "进行中", color: "#d97706" };
|
||||
if (plan.status === "failed") return { label: tr("failed"), color: "#dc2626" };
|
||||
if (["interrupted", "cancelled", "canceled"].includes(plan.status)) return { label: tr("stopped"), color: "#d97706" };
|
||||
if (completed === plan.tasks.length) return { label: tr("completed"), color: "#16a34a" };
|
||||
if (plan.status === "finished") return { label: tr("finished"), color: "#2563eb" };
|
||||
return { label: tr("running"), color: "#d97706" };
|
||||
}
|
||||
|
||||
function planTaskState(status: string, muted: string) {
|
||||
if (status === "completed") return { label: "已完成", color: "#16a34a", icon: <CheckCircle2 className="size-3.5" /> };
|
||||
if (status === "inProgress") return { label: "进行中", color: "#d97706", icon: <LoaderCircle className="size-3.5 animate-spin" /> };
|
||||
return { label: "待处理", color: muted, icon: <Circle className="size-3.5" /> };
|
||||
if (status === "completed") return { label: tr("completed"), color: "#16a34a", icon: <CheckCircle2 className="size-3.5" /> };
|
||||
if (status === "inProgress") return { label: tr("running"), color: "#d97706", icon: <LoaderCircle className="size-3.5 animate-spin" /> };
|
||||
return { label: tr("pending"), color: muted, icon: <Circle className="size-3.5" /> };
|
||||
}
|
||||
|
||||
function planDetail(value: unknown): PlanDetail | null {
|
||||
@@ -546,3 +549,7 @@ function normalizeText(value: unknown) {
|
||||
function objectField(value: unknown, key: string) {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>)[key] : undefined;
|
||||
}
|
||||
|
||||
function tr(key: string, options?: Record<string, unknown>) {
|
||||
return i18n.t(`agent.message.${key}`, options);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user