- 侧栏与命令面板没有「待处理」;/console/inbox?kind= 按问题类型跳到整理记录「需要处理」、下载面板、运行状态或订阅 - 整理记录加「需要处理」筛选,行内写原因与推荐动作(指定作品 / 重新整理 / 让智能体识别 / 替换·保留两者·跳过 / 忽略), 详情页同样可处理;处理按钮从原待处理页抽成 components/decisions/decision-actions.tsx 复用 - 铃铛只放等你批准的智能体操作与系统健康;首页「要我处理」与运行状态「需要关注」放汇总计数,运行状态加智能体用量 - 设置 → 智能体:预算卡改为「用量与限制」(只剩单次工具调用与时长,附花费统计),自动调查卡加「整理识别不到作品时叫智能体识别」 - 集数待确认的原因显示在作品页集数映射(可确认建议规则 / 智能体建议)与订阅详情;未识别资源在媒体库「未识别」里直接选候选或指定 - 下载、站点、任务里指向待处理的链接改到对应页面 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
144 lines
5.8 KiB
TypeScript
144 lines
5.8 KiB
TypeScript
import type { Decision } from "@umt/contracts";
|
|
import { ArrowRight, Bell, CheckCheck } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { Link, useNavigate } from "react-router";
|
|
import { useAttention } from "~/api/attention.ts";
|
|
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
|
import { consolePath } from "~/components/console/nav.ts";
|
|
import { SEVERITY } from "~/components/library/labels.ts";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
|
|
import { formatRelative } from "~/lib/labels.ts";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
// 顶栏铃铛(2026-10-06 起只放两类):需要你批准的智能体操作(会话里的审批 / 方案选择、计划审批、原站页面等你接手)
|
|
// 与系统健康问题(站点不可达、下载器离线、下载目录读不到容量、磁盘空间不足、媒体库目录离线,恢复后自动消失)。
|
|
// 整理问题在整理记录里处理,集数待确认在订阅详情 / 作品页集数映射里看,不进铃铛。
|
|
|
|
const RECENT = 6;
|
|
const DOT = { info: "bg-muted-foreground/60", warning: "bg-warning", critical: "bg-destructive" } as const;
|
|
|
|
/** 等你批准的事项点开去哪:智能体会话、下载、下载任务;其余去智能体页 */
|
|
export function approvalHref(d: Pick<Decision, "subjectType" | "subjectId" | "agentRunId">): string {
|
|
if (d.subjectType === "agent_run" && d.subjectId) return consolePath("agent", d.subjectId);
|
|
if (d.agentRunId) return consolePath("agent", d.agentRunId);
|
|
if (d.subjectType === "acquisition_plan" && d.subjectId)
|
|
return `${consolePath("downloads")}?plan=${encodeURIComponent(d.subjectId)}`;
|
|
if (d.subjectType === "download_task" && d.subjectId) return consolePath("downloads", d.subjectId);
|
|
return consolePath("agent");
|
|
}
|
|
|
|
export const HEALTH_HREF = `${consolePath("status")}#attention`;
|
|
|
|
function Group({
|
|
title,
|
|
total,
|
|
items,
|
|
href,
|
|
onGo,
|
|
}: {
|
|
title: string;
|
|
total: number;
|
|
items: Decision[];
|
|
href: (d: Decision) => string;
|
|
onGo: () => void;
|
|
}) {
|
|
if (!total) return null;
|
|
return (
|
|
<section className="pt-1">
|
|
<p className="flex items-center justify-between gap-2 px-2 py-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
|
<span>{title}</span>
|
|
<span className="tracking-normal tabular-nums">{total}</span>
|
|
</p>
|
|
<ul>
|
|
{items.slice(0, RECENT).map((d) => (
|
|
<li key={d.id}>
|
|
<Link
|
|
to={href(d)}
|
|
onClick={onGo}
|
|
className="block rounded-lg px-2 py-2 outline-none transition-colors hover:bg-foreground/[0.06] focus-visible:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
>
|
|
<span className="line-clamp-2 text-body break-words text-foreground">{d.title}</span>
|
|
<span className="mt-0.5 flex items-center gap-1.5 text-meta text-muted-foreground">
|
|
<span aria-hidden className={cn("h-1.5 w-1.5 shrink-0 rounded-full", DOT[d.severity])} />
|
|
{SEVERITY[d.severity].label} · {formatRelative(d.createdAt)}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function InboxBell() {
|
|
const q = useAttention();
|
|
const [open, setOpen] = useState(false);
|
|
const navigate = useNavigate();
|
|
const approvals = q.data?.approvals ?? { total: 0, items: [] };
|
|
const health = q.data?.health ?? { total: 0, items: [] };
|
|
const total = approvals.total + health.total;
|
|
const label = total ? `需要你关注 ${total} 项` : "通知";
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" size="icon-sm" className="relative" aria-label={label} title={label}>
|
|
<Bell className="h-4 w-4" />
|
|
{total ? (
|
|
<span className="absolute -top-0.5 -right-0.5 min-w-4 rounded-full bg-primary px-1 text-center text-micro leading-4 font-semibold text-primary-foreground tabular-nums">
|
|
{total > 99 ? "99+" : total}
|
|
</span>
|
|
) : null}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
align="end"
|
|
sideOffset={8}
|
|
collisionPadding={16}
|
|
aria-label="通知"
|
|
className="pg-float-solid flex max-h-[var(--radix-popover-content-available-height)] w-[min(22rem,calc(100vw-2rem))] flex-col overflow-hidden p-2"
|
|
>
|
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-2 pt-1 pb-2">
|
|
<p className="text-sm font-semibold text-foreground">通知</p>
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
navigate(consolePath("status"));
|
|
}}
|
|
>
|
|
运行状态
|
|
<ArrowRight />
|
|
</Button>
|
|
</div>
|
|
{q.isPending ? (
|
|
<LoadingState className="px-4 py-6" />
|
|
) : q.isError ? (
|
|
<ErrorState plain error="稍后再试" className="py-8" />
|
|
) : total === 0 ? (
|
|
<EmptyState plain icon={CheckCheck} title="没有等你批准的操作,系统运行正常" className="py-8" />
|
|
) : (
|
|
<div className="min-h-0 overflow-y-auto overscroll-contain pt-1">
|
|
<Group
|
|
title="等你批准"
|
|
total={approvals.total}
|
|
items={approvals.items}
|
|
href={approvalHref}
|
|
onGo={() => setOpen(false)}
|
|
/>
|
|
<Group
|
|
title="系统健康"
|
|
total={health.total}
|
|
items={health.items}
|
|
href={() => HEALTH_HREF}
|
|
onGo={() => setOpen(false)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|