fix(web): show knowledge governance flow
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user