fix(web): clarify knowledge owner review operations
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user