feat(web): 前移 Knowledge Base 自動化掌控台
This commit is contained in:
@@ -1810,6 +1810,24 @@
|
||||
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
|
||||
"retry": "重新讀取"
|
||||
},
|
||||
"decision": {
|
||||
"badge": "KM 治理警戒",
|
||||
"readback": "Hermes / AwoooP 只讀回讀",
|
||||
"title": "KM 自動化掌控台",
|
||||
"subtitle": "先看 stale ratio、owner review、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
|
||||
"openWorkItems": "處理 Owner Review",
|
||||
"card": {
|
||||
"staleRatio": "Stale Ratio",
|
||||
"staleRatioSub": "門檻 {threshold}",
|
||||
"staleTotal": "Stale KM",
|
||||
"staleTotalSub": "距離門檻需處理 {count}",
|
||||
"ownerQueue": "Owner Review",
|
||||
"ownerQueueSub": "ready {ready} / blocked {blocked}",
|
||||
"writeback": "已寫回",
|
||||
"writebackSafe": "只讀回讀;讀取不寫入",
|
||||
"writebackUnsafe": "偵測到讀取寫入風險"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"metricTotal": "總條目",
|
||||
"metricLoaded": "目前列表",
|
||||
|
||||
@@ -1810,6 +1810,24 @@
|
||||
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
|
||||
"retry": "重新讀取"
|
||||
},
|
||||
"decision": {
|
||||
"badge": "KM 治理警戒",
|
||||
"readback": "Hermes / AwoooP 只讀回讀",
|
||||
"title": "KM 自動化掌控台",
|
||||
"subtitle": "先看 stale ratio、owner review、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
|
||||
"openWorkItems": "處理 Owner Review",
|
||||
"card": {
|
||||
"staleRatio": "Stale Ratio",
|
||||
"staleRatioSub": "門檻 {threshold}",
|
||||
"staleTotal": "Stale KM",
|
||||
"staleTotalSub": "距離門檻需處理 {count}",
|
||||
"ownerQueue": "Owner Review",
|
||||
"ownerQueueSub": "ready {ready} / blocked {blocked}",
|
||||
"writeback": "已寫回",
|
||||
"writebackSafe": "只讀回讀;讀取不寫入",
|
||||
"writebackUnsafe": "偵測到讀取寫入風險"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"metricTotal": "總條目",
|
||||
"metricLoaded": "目前列表",
|
||||
|
||||
@@ -800,6 +800,57 @@ export default function KnowledgeBasePage({
|
||||
}))
|
||||
}, [formatCount, governanceSummary, governanceTelemetry.burnDown, t])
|
||||
|
||||
const governanceDecisionCards = useMemo(() => {
|
||||
const ratioAboveThreshold =
|
||||
governanceSummary.ratio !== null
|
||||
&& governanceSummary.threshold !== null
|
||||
&& governanceSummary.ratio > governanceSummary.threshold
|
||||
const staleRatioValue = governanceSummary.ratio === null ? '--' : `${governanceSummary.ratio}%`
|
||||
const thresholdText = governanceSummary.threshold === null ? '--' : `${governanceSummary.threshold}%`
|
||||
const staleTotalValue = governanceSummary.staleTotal === null ? '--' : formatCount(governanceSummary.staleTotal)
|
||||
const entriesToThreshold = governanceSummary.entriesToThreshold === null ? '--' : formatCount(governanceSummary.entriesToThreshold)
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'staleRatio',
|
||||
icon: Clock3,
|
||||
value: staleRatioValue,
|
||||
sub: t('decision.card.staleRatioSub', { threshold: thresholdText }),
|
||||
tone: ratioAboveThreshold
|
||||
? 'border-status-critical/25 bg-status-critical/10 text-status-critical'
|
||||
: 'border-status-healthy/25 bg-status-healthy/10 text-status-healthy',
|
||||
},
|
||||
{
|
||||
key: 'staleTotal',
|
||||
icon: BookOpen,
|
||||
value: staleTotalValue,
|
||||
sub: t('decision.card.staleTotalSub', { count: entriesToThreshold }),
|
||||
tone: 'border-status-warning/25 bg-status-warning/10 text-status-warning',
|
||||
},
|
||||
{
|
||||
key: 'ownerQueue',
|
||||
icon: Users,
|
||||
value: formatCount(governanceSummary.ownerPending),
|
||||
sub: t('decision.card.ownerQueueSub', {
|
||||
ready: formatCount(governanceSummary.ownerReady),
|
||||
blocked: formatCount(governanceSummary.ownerBlocked),
|
||||
}),
|
||||
tone: 'border-claw-blue/25 bg-claw-blue/8 text-claw-blue',
|
||||
},
|
||||
{
|
||||
key: 'writeback',
|
||||
icon: Database,
|
||||
value: formatCount(governanceSummary.completed),
|
||||
sub: governanceSummary.writesOnRead
|
||||
? t('decision.card.writebackUnsafe')
|
||||
: t('decision.card.writebackSafe'),
|
||||
tone: governanceSummary.writesOnRead
|
||||
? 'border-status-critical/25 bg-status-critical/10 text-status-critical'
|
||||
: 'border-status-healthy/25 bg-status-healthy/10 text-status-healthy',
|
||||
},
|
||||
] as const
|
||||
}, [formatCount, governanceSummary, t])
|
||||
|
||||
const content = (
|
||||
<div className="flex min-h-[calc(100vh-64px)] flex-col lg:h-[calc(100vh-64px)] lg:flex-row">
|
||||
|
||||
@@ -942,6 +993,56 @@ export default function KnowledgeBasePage({
|
||||
)}
|
||||
|
||||
<section className="border-b border-nothing-gray-200/50 px-4 py-3 bg-white/35">
|
||||
<div className="mb-3 rounded-md border border-nothing-gray-200 bg-white/80 px-3 py-3">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-status-warning/20 bg-status-warning/10 px-2 py-0.5 text-[10px] font-label uppercase tracking-wider text-status-warning">
|
||||
<TriangleAlert className="h-3 w-3" aria-hidden="true" />
|
||||
{t('decision.badge')}
|
||||
</span>
|
||||
<span className="text-[10px] font-label uppercase tracking-wider text-muted">
|
||||
{governanceLoading ? t('workItems.loading') : t('decision.readback')}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="mt-2 text-xl font-heading font-semibold text-primary">
|
||||
{t('decision.title')}
|
||||
</h1>
|
||||
<p className="mt-1 max-w-4xl text-xs font-body leading-5 text-secondary">
|
||||
{t('decision.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={governanceSummary.workItemsHref as never}
|
||||
className="inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md border border-claw-blue/20 bg-claw-blue/8 px-3 py-1.5 text-xs font-label text-claw-blue transition-colors hover:bg-claw-blue/12"
|
||||
>
|
||||
{t('decision.openWorkItems')}
|
||||
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 xl:grid-cols-4">
|
||||
{governanceDecisionCards.map(card => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<div key={card.key} className="rounded-md border border-nothing-gray-200 bg-white/70 p-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-md border', card.tone)}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</div>
|
||||
<span className="truncate text-[10px] font-label uppercase tracking-wider text-muted">
|
||||
{t(`decision.card.${card.key}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-heading font-semibold tabular-nums text-primary">
|
||||
{governanceLoading ? '--' : card.value}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[10px] font-body text-muted">{card.sub}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{[
|
||||
|
||||
Reference in New Issue
Block a user