Files
UHD_Media_Tool/apps/web/app/components/status/attention-cards.tsx
T

130 lines
4.6 KiB
TypeScript

import type { AgentSpendBucket, AgentSpendKind } from "@umt/contracts";
import { BellRing, Bot } from "lucide-react";
import { useAgentSpend, useAttention } from "~/api/attention.ts";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { settingsPath } from "~/components/console/nav.ts";
import { AttentionList, attentionRows } from "~/components/dashboard/attention-summary.tsx";
import { formatRelative } from "~/lib/labels.ts";
import { humanizeReason } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
// 运行状态页(2026-10-06):「需要关注」汇总(整理需要处理、等你批准、系统健康,系统健康逐条列出、恢复后自动消失)
// 与智能体用量(今天 / 最近 7 天按会话类型;不设上限,只展示)。
function Slab({
id,
title,
icon: Icon,
children,
}: {
id?: string;
title: string;
icon: typeof Bot;
children: React.ReactNode;
}) {
return (
<section id={id} className="pg-slab min-w-0 scroll-mt-20 rounded-xl p-5">
<div className="mb-3 flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<h2 className="text-body font-semibold text-foreground">{title}</h2>
</div>
{children}
</section>
);
}
export function AttentionCard() {
const q = useAttention();
const rows = attentionRows(q.data);
const health = q.data?.health.items ?? [];
return (
<Slab id="attention" title="需要关注" icon={BellRing}>
{q.isPending ? (
<LoadingState rows={2} />
) : q.isError ? (
<ErrorState plain error={q.error} onRetry={() => q.refetch()} />
) : rows.length === 0 ? (
<p className="text-caption text-muted-foreground">
没有需要处理的整理记录,也没有等你批准的操作;站点、下载器和下载目录都正常。
</p>
) : (
<>
<AttentionList rows={rows} />
{health.length ? (
<ul className="mt-3 space-y-2 border-t border-border/60 pt-3">
{health.map((d) => (
<li key={d.id} className="min-w-0 text-caption">
<p className="font-medium break-words text-foreground">{d.title}</p>
{d.summary ? (
<p className="break-words text-meta text-muted-foreground">{humanizeReason(d.summary)}</p>
) : null}
<p className="text-meta text-muted-foreground">{formatRelative(d.createdAt)}起,恢复后自动消失</p>
</li>
))}
</ul>
) : null}
</>
)}
</Slab>
);
}
const KIND_LABEL: Record<AgentSpendKind, string> = {
chat: "对话",
patrol: "巡检",
investigate: "后台调查",
organize_identify: "整理识别",
subagent: "子会话",
};
const usd = (n: number) => `$${n < 0.01 && n > 0 ? n.toFixed(4) : n.toFixed(2)}`;
function SpendBlock({ label, b }: { label: string; b: AgentSpendBucket }) {
return (
<div className="min-w-0">
<p className="text-meta text-muted-foreground">{label}</p>
<p className="text-title-sm font-semibold text-foreground tabular-nums">
{usd(b.costUsd)}
<span className="ml-1.5 text-caption font-normal text-muted-foreground">{b.runs} 次</span>
</p>
{b.byKind.length ? (
<ul className="mt-1 space-y-0.5 text-meta text-muted-foreground tabular-nums">
{b.byKind.map((k) => (
<li key={k.kind}>
{KIND_LABEL[k.kind]} {k.runs} 次 · {usd(k.costUsd)}
</li>
))}
</ul>
) : null}
</div>
);
}
/** 智能体用量(设置页「用量与限制」也用) */
export function AgentSpendSummary() {
const q = useAgentSpend();
if (q.isPending) return <LoadingState rows={2} />;
if (q.isError) return <ErrorState plain error={q.error} onRetry={() => q.refetch()} />;
if (!q.data.last7d.runs) return <p className="text-caption text-muted-foreground">最近 7 天智能体没有运行。</p>;
return (
<div className="grid gap-4 sm:grid-cols-2">
<SpendBlock label="今天" b={q.data.today} />
<SpendBlock label="最近 7 天" b={q.data.last7d} />
</div>
);
}
export function AgentSpendCard() {
return (
<Slab title="智能体用量" icon={Bot}>
<AgentSpendSummary />
<p className="mt-3 text-meta text-muted-foreground">
不设次数和花费上限,只限制单次的工具调用次数与时长。
<Link to={settingsPath("agent", "budget")} className="ml-1 underline-offset-4 hover:underline">
调整
</Link>
</p>
</Slab>
);
}