import { useQueryClient } from "@tanstack/react-query"; import type { Dashboard } from "@umt/contracts"; import { Activity, AlertTriangle, ArrowRight, Bot, CircleCheck, Download, Library as LibraryIcon, type LucideIcon, Pause, Play, } from "lucide-react"; import type { ReactNode } from "react"; import { useRef } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { useAttention } from "~/api/attention.ts"; import { useDashboard, useSitesWithAccounts } from "~/api/dashboard.ts"; import { useEngineRunsPage } from "~/api/engine.ts"; import { useUpdateSettings } from "~/api/settings.ts"; import { useSystemHealth } from "~/api/system.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { consolePath, settingsPath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { AttentionList, attentionRows } from "~/components/dashboard/attention-summary.tsx"; import { HomeRuntime } from "~/components/dashboard/home-runtime.tsx"; import { runTitle } from "~/components/tasks/labels.ts"; import { useLiveTasks } from "~/components/tasks/live.ts"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatRelative } from "~/lib/labels.ts"; import { qk } from "~/lib/query-keys.ts"; import { useSse } from "~/lib/sse.ts"; import { cn } from "~/lib/utils.ts"; // 首页概览:自动化、存储与下载器状态,以及当前工作、要我处理的汇总计数(整理需要处理 / 等你批准 / 系统健康)和最近整理。 /** 首页关心的事件:任意一个到达就(节流)刷新聚合 */ const DASHBOARD_EVENTS = [ "task.created", "task.status_changed", "download.submitted", "download.status_changed", "download.threshold", "organization.changed", "agent.run_status", "decision.created", "decision.updated", "settings.changed", ]; function useLiveDashboard() { const qc = useQueryClient(); const timer = useRef | null>(null); useSse(DASHBOARD_EVENTS, () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; qc.invalidateQueries({ queryKey: qk.dashboard }); }, 1500); }); return useDashboard(); } function Section({ title, icon: Icon, to, children, className, }: { title: string; icon: LucideIcon; to?: string; children: ReactNode; className?: string; }) { return (

{title}

{to ? ( 全部 ) : null}
{children}
); } function Row({ children }: { children: ReactNode }) { return
  • {children}
  • ; } /** 非零项拼一句:[[3, "个下载"], [0, "个整理中"]] → "3 个下载" */ function nonZero(parts: [number, string][]): string { return parts .filter(([n]) => n > 0) .map(([n, s]) => `${n} ${s}`) .join(" · "); } // ─── 顶部一行:自动化状态 + 需要知道的异常 ─── function StatusLine({ d }: { d: Dashboard }) { const a = d.automation; const update = useUpdateSettings(); const health = useSystemHealth(60_000); const sites = useSitesWithAccounts(); const setEnabled = async (enabled: boolean) => { try { await update.mutateAsync({ automation: { enabled } }); toast.success(enabled ? "自动化已恢复" : "自动化已暂停"); } catch (err) { toast.error(errorMessage(err, "切换失败")); throw err; } }; // 只列不正常的:服务异常、下载盘到暂停线、站点账号失效、下载器离线 const problems: { text: string; to: string }[] = []; const lv = health.data?.level; if (lv === "critical" || lv === "warn") problems.push({ text: lv === "critical" ? "服务异常" : "服务需要关注", to: consolePath("status") }); for (const v of d.cache.volumes) if (v.usedPercent !== null && v.usedPercent >= v.hardThresholdPercent) problems.push({ text: `下载盘已用 ${Math.round(v.usedPercent)}%,新下载已暂停`, to: consolePath("downloads") }); const badAccounts = (sites.data?.items ?? []).filter( (s) => s.enabled && s.accountStatus && ["invalid", "expired", "challenge"].includes(s.accountStatus), ).length; if (badAccounts) problems.push({ text: `${badAccounts} 个站点账号需要重新登录`, to: consolePath("sites") }); const offline = d.sources.downloaders.filter((x) => x.enabled && ["offline", "failing"].includes(x.health)).length; if (offline) problems.push({ text: `${offline} 个下载器连不上`, to: settingsPath("downloaders") }); const paused = !a.enabled; const warn = paused || a.readOnlyReconcile || problems.length > 0; return (

    {paused ? ( "自动化已暂停,手动操作仍可用" ) : a.readOnlyReconcile ? ( <> 只读模式:不会自动下载和整理。 去设置关闭 ) : a.activeSubscriptions > 0 ? ( `自动化已开启 · ${a.activeSubscriptions} 个订阅` ) : ( <> 自动化已开启 · 还没有订阅。 去搜索添加 )} {problems.map((p) => ( {" · "} {p.text} ))}

    {paused ? ( ) : ( 暂停 } title="暂停全部自动化?" description="自动收录、下载、整理会停止,已提交的下载任务不受影响。可随时恢复。" confirmLabel="暂停自动化" onConfirm={() => setEnabled(false)} /> )}
    ); } // ─── 在忙什么:下载中的任务 + 进行中的智能体会话 + 有业务对象的运行 ─── function Busy({ w }: { w: Dashboard["currentWork"] }) { const running = useEngineRunsPage({ status: "running" }, 10, 10_000); const runs = (running.data?.items ?? []).filter((r) => r.business); const downloads = w.items.slice(0, 4); const agents = w.agentRuns.slice(0, 2); const summary = nonZero([ [w.downloading, "个下载中"], [w.importing, "个整理中"], [w.stalled, "个没有进展"], [w.paused, "个已暂停"], ]); const empty = !downloads.length && !agents.length && !runs.length; return (
    {running.isPending && !downloads.length ? ( ) : empty ? ( ) : (
      {downloads.map((t) => ( {t.name} {t.status === "stalled" ? 没有进展 : null} {Math.round(t.progress * 100)}% ))} {agents.map((r) => ( {r.goal} ))} {runs.slice(0, Math.max(0, 5 - downloads.length - agents.length)).map((r) => ( {runTitle(r)} ))}
    )} {summary ?

    {summary}

    : null}
    ); } // ─── 要我处理什么:汇总计数(整理需要处理、等你批准、系统健康),点进去到对应页面 ─── function NeedsMe() { const q = useAttention(); const rows = attentionRows(q.data); return (
    {q.isPending ? ( ) : rows.length === 0 ? ( ) : ( )}
    ); } // ─── 最近整理了什么:成功 / 部分成功的整理 ─── function RecentImports({ d }: { d: Dashboard }) { const rows = d.recentOutput.filter((r) => r.status === "succeeded" || r.status === "partial").slice(0, 6); const today = d.automation.today.filesImported; return (
    {rows.length === 0 ? ( ) : (
      {rows.map((r) => ( {r.title ?? r.targetPath?.split("/").pop() ?? "未关联作品"} {r.status === "partial" ? ( {r.counts.succeeded}/{r.counts.total} 个文件已整理 ) : null} {formatRelative(r.finishedAt)} ))}
    )} {today ?

    今天整理 {today} 个文件

    : null}
    ); } export default function Home() { const q = useLiveDashboard(); useLiveTasks(); return ( <> 更新于 {formatRelative(q.data.generatedAt)} ) : null } /> {q.data ? : null} {q.isPending ? (
    {["a", "b", "c"].map((k) => (
    ))}
    ) : q.isError ? ( q.refetch()} /> ) : (
    )} ); }