import { useQueryClient } from "@tanstack/react-query"; import type { Decision, DecisionKind, DecisionOption, DecisionStatus } from "@umt/contracts"; import { BellOff, Bot, CheckCheck, ChevronRight, Inbox, Radio, RotateCw } from "lucide-react"; import { type ReactNode, useMemo, useRef, useState } from "react"; import { Link, useSearchParams } from "react-router"; import { toast } from "sonner"; import { decisionKeys, useDecisionSummary, useDecisions, useResolveDecision } from "~/api/decisions.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatTile } from "~/components/console/stat-tile.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { DECISION_KIND, DECISION_STATUS, opts, SEVERITY } from "~/components/library/labels.ts"; import { Chip, relTime, Tone } from "~/components/library/parts.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { useSse, useSseStatus } from "~/lib/sse.ts"; import { formatDateTime } from "~/lib/utils.ts"; // 待处理(REQ §13.1):按作品与问题类型合并;每项给证据、建议、影响范围与预览; // 处理选项来自后端 decision.options;SSE 收到 decision.* 事件即刷新。 const OPEN: DecisionStatus[] = ["open", "investigating", "awaiting_user"]; const STATUS_FILTER = [ { value: "resolved", label: "已处理" }, { value: "dismissed", label: "已忽略" }, { value: "expired", label: "已过期" }, ]; const SEV_RANK = { info: 0, warning: 1, critical: 2 } as const; /** 证据里约定俗成的字段(Agent/后端写入);其余原样放进"原始证据" */ const str = (v: unknown) => (typeof v === "string" && v.trim() ? v : null); function entityOf(d: Decision): string | null { const m = d.evidence.mediaEntityId; if (typeof m === "string" && /^[0-9a-f-]{36}$/.test(m)) return m; return d.subjectType === "media_entity" ? d.subjectId : null; } const IMPACT_KEY: Record = { slots: "版本目标", units: "内容单元", files: "文件", releases: "资源", tasks: "任务", entities: "作品", }; function describe(v: unknown): ReactNode { if (v === null || v === undefined) return null; if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v); if (Array.isArray(v)) return v.map((x) => (typeof x === "object" ? JSON.stringify(x) : String(x))).join("、"); return Object.entries(v as Record) .map( ([k, x]) => `${IMPACT_KEY[k] ?? k} ${Array.isArray(x) ? x.join("、") : typeof x === "object" ? JSON.stringify(x) : String(x)}`, ) .join(";"); } function Evidence({ d }: { d: Decision }) { const e = d.evidence; const suggestion = str(e.suggestion); const impact = describe(e.impact); const preview = describe(e.preview); const checked = Array.isArray(e.checked) ? (e.checked as unknown[]).map(String) : []; const rows: [string, ReactNode][] = []; if (suggestion) rows.push(["建议", suggestion]); if (impact) rows.push(["影响范围", impact]); if (preview) rows.push(["预览", preview]); return (
{rows.length ? (
{rows.map(([k, v]) => (
{k}
{v}
))}
) : null} {checked.length ? (
Agent 已检查 {checked.map((c) => ( {c} ))}
) : null}
); } function OptionButton({ d, option }: { d: Decision; option: DecisionOption }) { const resolve = useResolveDecision(); const [note, setNote] = useState(""); return ( {option.label} } title={option.label} description={ <> {option.description ? `${option.description}。` : ""} {option.capabilityId ? `由能力 ${option.capabilityId} 执行,仍做授权校验。` : null} } confirmLabel={option.label} onConfirm={() => resolve.mutateAsync({ id: d.id, optionKey: option.key, note: note.trim() || undefined }).then( () => toast.success(`已处理:${option.label}`), (err) => { toast.error(errorMessage(err, "处理失败")); throw err; }, ) } >