fix(web): show knowledge governance flow
All checks were successful
CD Pipeline / tests (push) Successful in 1m26s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m38s
CD Pipeline / post-deploy-checks (push) Successful in 1m26s

This commit is contained in:
Your Name
2026-06-03 09:20:01 +08:00
parent ec6cf8d608
commit dc6039c6ea
3 changed files with 173 additions and 0 deletions

View File

@@ -1482,6 +1482,33 @@
"owner": "Owner在 AwoooP Work Items 預覽後確認寫回。",
"noWritesOnRead": "讀取不寫入",
"unexpectedWrites": "偵測到 read endpoint 宣告會寫入",
"flow": {
"title": "治理流程圖",
"scope": "從偵測到寫回與比例回測的目前位置",
"node": {
"detected": "偵測",
"ownerReview": "Owner Review",
"dryRun": "乾跑預覽",
"ownerConfirm": "Owner 確認",
"writeback": "寫回 KM",
"recheck": "比例回測"
},
"state": {
"warning": "需處理",
"ready": "可操作",
"waiting": "等待",
"done": "已有證據",
"blocked": "卡住"
},
"detail": {
"detected": "目前 {ratio};門檻 {threshold}",
"ownerReview": "{count} 筆等待 owner 審核",
"dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住",
"ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識",
"writeback": "{count} 筆已有 completion audit",
"recheck": "{count} 筆已回測;距離門檻仍差 {remaining} 筆"
}
},
"metric": {
"staleRatio": "陳舊比例",
"staleTotal": "陳舊 KM",

View File

@@ -1482,6 +1482,33 @@
"owner": "Owner在 AwoooP Work Items 預覽後確認寫回。",
"noWritesOnRead": "讀取不寫入",
"unexpectedWrites": "偵測到 read endpoint 宣告會寫入",
"flow": {
"title": "治理流程圖",
"scope": "從偵測到寫回與比例回測的目前位置",
"node": {
"detected": "偵測",
"ownerReview": "Owner Review",
"dryRun": "乾跑預覽",
"ownerConfirm": "Owner 確認",
"writeback": "寫回 KM",
"recheck": "比例回測"
},
"state": {
"warning": "需處理",
"ready": "可操作",
"waiting": "等待",
"done": "已有證據",
"blocked": "卡住"
},
"detail": {
"detected": "目前 {ratio};門檻 {threshold}",
"ownerReview": "{count} 筆等待 owner 審核",
"dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住",
"ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識",
"writeback": "{count} 筆已有 completion audit",
"recheck": "{count} 筆已回測;距離門檻仍差 {remaining} 筆"
}
},
"metric": {
"staleRatio": "陳舊比例",
"staleTotal": "陳舊 KM",

View File

@@ -20,6 +20,7 @@ import {
Search, BookOpen, FileText, Shield, Cpu,
Server, Eye, Bot, ChevronRight, Plus, Sparkles, ClipboardList, Tag, TriangleAlert,
GitBranch, CheckCircle2, Clock3, Link2, FileSearch, ListChecks, ArrowRight, Users,
PlayCircle, Database, RotateCw,
} from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
@@ -92,6 +93,8 @@ interface KnowledgeStaleOwnerReviewBurnDownResponse {
entries_to_threshold: number
pending_owner_reviews: number
completed_owner_reviews: number
completion_audit_total?: number
stale_ratio_recheck_total?: number
current_snapshot?: {
stale_count: number
total_count: number
@@ -594,6 +597,81 @@ export default function KnowledgeBasePage({
}
}, [governanceTelemetry])
const governanceFlowNodes = useMemo(() => {
const staleRatioText = governanceSummary.ratio === null ? '--' : `${governanceSummary.ratio}%`
const thresholdText = governanceSummary.threshold === null ? '--' : `${governanceSummary.threshold}%`
const recheckTotal = governanceTelemetry.burnDown?.stale_ratio_recheck_total ?? 0
const auditTotal = governanceTelemetry.burnDown?.completion_audit_total ?? governanceSummary.completed
const nodeTone = (state: 'warning' | 'ready' | 'waiting' | 'done' | 'blocked') => {
switch (state) {
case 'warning':
return 'border-status-warning/25 bg-status-warning/10 text-status-warning'
case 'ready':
return 'border-claw-blue/25 bg-claw-blue/8 text-claw-blue'
case 'done':
return 'border-status-healthy/25 bg-status-healthy/10 text-status-healthy'
case 'blocked':
return 'border-status-critical/25 bg-status-critical/10 text-status-critical'
default:
return 'border-nothing-gray-200 bg-white text-secondary'
}
}
return [
{
key: 'detected',
icon: TriangleAlert,
value: governanceSummary.staleTotal === null ? '--' : formatCount(governanceSummary.staleTotal),
state: governanceSummary.burnDownStatus === 'above_threshold' ? 'warning' : 'done',
detail: t('workItems.flow.detail.detected', { ratio: staleRatioText, threshold: thresholdText }),
},
{
key: 'ownerReview',
icon: Users,
value: formatCount(governanceSummary.ownerPending),
state: governanceSummary.ownerPending > 0 ? 'warning' : 'done',
detail: t('workItems.flow.detail.ownerReview', { count: formatCount(governanceSummary.ownerPending) }),
},
{
key: 'dryRun',
icon: PlayCircle,
value: formatCount(governanceSummary.ownerReady),
state: governanceSummary.ownerReady > 0 ? 'ready' : 'waiting',
detail: t('workItems.flow.detail.dryRun', {
ready: formatCount(governanceSummary.ownerReady),
blocked: formatCount(governanceSummary.ownerBlocked),
}),
},
{
key: 'ownerConfirm',
icon: Shield,
value: formatCount(governanceSummary.ownerReady),
state: governanceSummary.ownerBlocked > 0 ? 'blocked' : governanceSummary.ownerReady > 0 ? 'ready' : 'waiting',
detail: t('workItems.flow.detail.ownerConfirm'),
},
{
key: 'writeback',
icon: Database,
value: formatCount(auditTotal),
state: auditTotal > 0 ? 'done' : 'waiting',
detail: t('workItems.flow.detail.writeback', { count: formatCount(auditTotal) }),
},
{
key: 'recheck',
icon: RotateCw,
value: formatCount(recheckTotal),
state: recheckTotal > 0 ? 'done' : 'waiting',
detail: t('workItems.flow.detail.recheck', {
count: formatCount(recheckTotal),
remaining: governanceSummary.entriesToThreshold === null ? '--' : formatCount(governanceSummary.entriesToThreshold),
}),
},
].map(node => ({
...node,
tone: nodeTone(node.state as 'warning' | 'ready' | 'waiting' | 'done' | 'blocked'),
}))
}, [formatCount, governanceSummary, governanceTelemetry.burnDown, t])
const content = (
<div className="flex min-h-[calc(100vh-64px)] flex-col lg:h-[calc(100vh-64px)] lg:flex-row">
@@ -898,6 +976,47 @@ export default function KnowledgeBasePage({
))}
</div>
<div className="mt-3 overflow-hidden rounded-md border border-nothing-gray-200 bg-white/60">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-nothing-gray-100 px-3 py-2">
<div className="min-w-0">
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('workItems.flow.title')}</p>
<p className="mt-0.5 truncate text-[10px] font-body text-muted">{t('workItems.flow.scope')}</p>
</div>
<span className="shrink-0 text-[10px] font-label text-status-healthy">
{governanceSummary.writesOnRead ? t('workItems.unexpectedWrites') : t('workItems.noWritesOnRead')}
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-6">
{governanceFlowNodes.map((node, index) => {
const Icon = node.icon
return (
<div key={node.key} className="relative min-w-0 border-t border-nothing-gray-100 px-3 py-2 first:border-t-0 sm:border-l sm:first:border-l-0 xl:border-t-0">
{index > 0 && (
<ArrowRight className="absolute -left-2 top-5 hidden h-3.5 w-3.5 rounded-full bg-white text-nothing-gray-300 xl:block" aria-hidden="true" />
)}
<div className="flex items-start justify-between gap-2">
<div className={cn('flex h-7 w-7 shrink-0 items-center justify-center rounded-md border', node.tone)}>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
</div>
<span className="rounded-full border border-nothing-gray-200 bg-white px-2 py-0.5 text-[10px] font-label uppercase tracking-wider text-muted">
{t(`workItems.flow.state.${node.state}` as never)}
</span>
</div>
<p className="mt-2 truncate text-[10px] font-label uppercase tracking-wider text-muted">
{t(`workItems.flow.node.${node.key}` as never)}
</p>
<p className="mt-1 text-lg font-heading font-semibold tabular-nums text-primary">
{governanceLoading ? '--' : node.value}
</p>
<p className="mt-0.5 line-clamp-2 text-[10px] font-body leading-4 text-muted">
{node.detail}
</p>
</div>
)
})}
</div>
</div>
<div className="mt-3 grid grid-cols-1 gap-2 lg:grid-cols-[minmax(0,1fr)_220px]">
<div className="rounded-md border border-nothing-gray-200 bg-white/60 px-3 py-2">
<div className="flex items-center justify-between gap-2">