fix(web): clarify knowledge owner review operations
All checks were successful
CD Pipeline / tests (push) Successful in 1m26s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m55s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s

This commit is contained in:
Your Name
2026-06-03 09:45:31 +08:00
parent eaad1a34a6
commit 9f23c08c2e
3 changed files with 382 additions and 0 deletions

View File

@@ -3532,6 +3532,151 @@ function KnowledgeGovernancePanel({
const reviewCount = items.filter((item) =>
item.dispatch_status === "skipped" || item.workflow_stage === "waiting_owner_review"
).length;
const ownerReviewOperation = useMemo(() => {
const staleCount = burnDownSnapshot?.stale_count ?? staleCandidates?.total_stale ?? 0;
const totalCount = burnDownSnapshot?.total_count ?? 0;
const staleRatio = burnDownSnapshot
? formatStaleRatio(burnDownSnapshot.stale_ratio)
: "--";
const threshold = burnDownSnapshot
? formatStaleRatio(burnDownSnapshot.threshold)
: "--";
const pending = completionQueue?.pending_count ?? ownerReviewInbox?.total ?? 0;
const ready = completionQueue?.ready_count ?? 0;
const blocked = completionQueue?.blocked_count ?? 0;
const completed = completionQueue?.completed_count ?? burnDown?.completed_owner_reviews ?? 0;
const failed = completionQueue?.failed_count ?? 0;
const auditTotal = burnDown?.completion_audit_total ?? completed;
const recheckTotal = burnDown?.stale_ratio_recheck_total ?? 0;
const remaining = burnDown?.entries_to_threshold ?? 0;
const writesOnRead = Boolean(
staleCandidates?.writes_on_read ||
ownerReviewInbox?.writes_on_read ||
completionQueue?.writes_on_read ||
burnDown?.writes_on_read
);
const manualReviewRequired = Boolean(
staleCandidates?.manual_review_required ??
ownerReviewInbox?.manual_review_required ??
completionQueue?.manual_review_required ??
burnDown?.manual_review_required ??
true
);
const batchWritesAllowed = Boolean(completionQueue?.batch_writes_allowed ?? false);
const nextActionKey =
completionQueue === null
? "waitData"
: blocked > 0
? "clearBlocked"
: ready > 0
? "previewReady"
: pending > 0
? "waitOwner"
: staleCount > 0
? "queueReview"
: "stable";
const nodeTone = (state: "warning" | "ready" | "waiting" | "done" | "blocked") => {
switch (state) {
case "warning":
return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
case "ready":
return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
case "done":
return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
case "blocked":
return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
default:
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
}
};
const flowNodes = [
{
key: "detected",
icon: TriangleAlert,
value: staleCount,
state: burnDown?.burn_down_status === "above_threshold" ? "warning" : "done",
detail: t("staleCandidates.operationRail.flow.detail.detected", {
ratio: staleRatio,
threshold,
}),
},
{
key: "ownerReview",
icon: ClipboardList,
value: pending,
state: pending > 0 ? "warning" : "done",
detail: t("staleCandidates.operationRail.flow.detail.ownerReview", {
count: pending,
}),
},
{
key: "dryRun",
icon: SearchCheck,
value: ready,
state: blocked > 0 ? "blocked" : ready > 0 ? "ready" : "waiting",
detail: t("staleCandidates.operationRail.flow.detail.dryRun", {
ready,
blocked,
}),
},
{
key: "ownerConfirm",
icon: ShieldCheck,
value: ready,
state: ready > 0 ? "ready" : failed > 0 ? "blocked" : "waiting",
detail: t("staleCandidates.operationRail.flow.detail.ownerConfirm"),
},
{
key: "writeback",
icon: Database,
value: auditTotal,
state: auditTotal > 0 ? "done" : "waiting",
detail: t("staleCandidates.operationRail.flow.detail.writeback", {
count: auditTotal,
}),
},
{
key: "recheck",
icon: RefreshCw,
value: recheckTotal,
state: recheckTotal > 0 ? "done" : "waiting",
detail: t("staleCandidates.operationRail.flow.detail.recheck", {
count: recheckTotal,
remaining,
}),
},
].map((node) => ({
...node,
tone: nodeTone(node.state as "warning" | "ready" | "waiting" | "done" | "blocked"),
}));
return {
staleCount,
totalCount,
staleRatio,
threshold,
pending,
ready,
blocked,
completed,
failed,
remaining,
writesOnRead,
manualReviewRequired,
batchWritesAllowed,
nextActionKey,
flowNodes,
};
}, [
burnDown,
burnDownSnapshot,
completionQueue,
ownerReviewInbox,
staleCandidates,
t,
]);
const previewArchiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => {
setArchiveActions((current) => ({
...current,
@@ -4016,6 +4161,119 @@ function KnowledgeGovernancePanel({
</span>
</div>
</div>
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-3 text-xs text-[#5f5b52]">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-[#5f5b52]" aria-hidden="true" />
<p className="font-semibold text-[#141413]">
{t("staleCandidates.operationRail.title")}
</p>
</div>
<p className="mt-1 text-[11px] leading-5 text-[#77736a]">
{t("staleCandidates.operationRail.subtitle")}
</p>
</div>
<div className="grid grid-cols-2 gap-1 font-mono text-[11px] text-[#5f5b52] sm:grid-cols-4">
{[
{
key: "ratio",
value: ownerReviewOperation.staleRatio,
detail: t("staleCandidates.operationRail.metric.threshold", {
threshold: ownerReviewOperation.threshold,
}),
},
{
key: "pending",
value: ownerReviewOperation.pending,
detail: t("staleCandidates.operationRail.metric.readyBlocked", {
ready: ownerReviewOperation.ready,
blocked: ownerReviewOperation.blocked,
}),
},
{
key: "completed",
value: ownerReviewOperation.completed,
detail: t("staleCandidates.operationRail.metric.failed", {
failed: ownerReviewOperation.failed,
}),
},
{
key: "remaining",
value: ownerReviewOperation.remaining,
detail: t("staleCandidates.operationRail.metric.total", {
stale: ownerReviewOperation.staleCount,
total: ownerReviewOperation.totalCount,
}),
},
].map((metric) => (
<div key={metric.key} className="border border-[#e0ddd4] bg-[#faf9f3] px-2 py-1.5">
<p className="text-[10px] uppercase tracking-[0.08em] text-[#77736a]">
{t(`staleCandidates.operationRail.metric.${metric.key}` as never)}
</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{metric.value}
</p>
<p className="mt-0.5 truncate text-[10px] text-[#77736a]">
{metric.detail}
</p>
</div>
))}
</div>
</div>
<div className="mt-3 grid grid-cols-1 border border-[#e0ddd4] bg-[#e0ddd4] sm:grid-cols-2 xl:grid-cols-6">
{ownerReviewOperation.flowNodes.map((node, index) => {
const Icon = node.icon;
return (
<div key={node.key} className="relative min-w-0 bg-white px-3 py-2">
{index > 0 ? (
<ArrowRight className="absolute -left-2 top-5 hidden h-3.5 w-3.5 bg-white text-[#b7b0a1] xl:block" aria-hidden="true" />
) : null}
<div className="flex items-start justify-between gap-2">
<div className={cn("flex h-7 w-7 shrink-0 items-center justify-center border", node.tone)}>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
</div>
<span className={cn("border px-2 py-0.5 text-[10px] font-semibold", node.tone)}>
{t(`staleCandidates.operationRail.state.${node.state}` as never)}
</span>
</div>
<p className="mt-2 truncate text-[10px] font-semibold uppercase tracking-[0.08em] text-[#77736a]">
{t(`staleCandidates.operationRail.flow.node.${node.key}` as never)}
</p>
<p className="mt-1 font-mono text-base font-semibold text-[#141413]">
{node.value}
</p>
<p className="mt-0.5 line-clamp-2 text-[11px] leading-5 text-[#5f5b52]">
{node.detail}
</p>
</div>
);
})}
</div>
<div className="mt-3 grid gap-2 lg:grid-cols-[minmax(0,1fr)_minmax(240px,0.55fr)]">
<div className="border border-[#d9b36f] bg-[#fff7e8] px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#8a5a08]">
{t("staleCandidates.operationRail.nextAction")}
</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{t(`staleCandidates.operationRail.next.${ownerReviewOperation.nextActionKey}` as never)}
</p>
<p className="mt-1 text-[11px] leading-5 text-[#5f5b52]">
{t(`staleCandidates.operationRail.nextDetail.${ownerReviewOperation.nextActionKey}` as never)}
</p>
</div>
<div className="border border-[#d8d3c7] bg-[#faf9f3] px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#77736a]">
{t("staleCandidates.operationRail.guardrailTitle")}
</p>
<div className="mt-1 grid gap-1 font-mono text-[11px] leading-5 text-[#5f5b52]">
<p>{t("staleCandidates.operationRail.guardrail.writesOnRead", { value: String(ownerReviewOperation.writesOnRead) })}</p>
<p>{t("staleCandidates.operationRail.guardrail.manualReview", { value: String(ownerReviewOperation.manualReviewRequired) })}</p>
<p>{t("staleCandidates.operationRail.guardrail.batchWrites", { value: String(ownerReviewOperation.batchWritesAllowed) })}</p>
</div>
</div>
</div>
</div>
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0">