fix(km): compact knowledge readback cockpit UI
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
ogt
2026-07-09 22:07:27 +08:00
parent a408972ee9
commit c4f98de3b2
3 changed files with 125 additions and 24 deletions

View File

@@ -2220,9 +2220,22 @@
"errorTitle": "知識條目入口需重讀",
"errorDescription": "知識條目入口回應:{reason}。頁面會保留 AI source-backed KM、Hermes 受控覆核與陳舊 KM 狀態,避免誤判成知識庫歸零。",
"sourceBackedTitle": "AI source-backed KM 已接管",
"sourceBackedSummary": "資料未歸零",
"sourceBackedDescription": "Primary KM DB readback 正在受控修復;目前顯示 source-backed 條目、分類與資產 taxonomy不是知識庫歸零。原因{reason};階段:{stage};下一步:{next}。",
"degradedTitle": "知識條目讀取降級",
"degradedSummary": "Primary readback 降級;列表保留可用資料。",
"degradedDescription": "主知識條目 API 已回 degraded{stage};下一步:{next}。目前列表不視為真實歸零AI 受控修復隊列與 RAG / PlayBook readback 仍會保留。",
"mode": {
"sourceBacked": "接管"
},
"chip": {
"mode": "模式",
"reason": "原因",
"stage": "階段",
"next": "下一步",
"entries": "條目",
"taxonomy": "分類 / 資產"
},
"retry": "重新讀取"
},
"decision": {

View File

@@ -2220,9 +2220,22 @@
"errorTitle": "知識條目入口需重讀",
"errorDescription": "知識條目入口回應:{reason}。頁面會保留 AI source-backed KM、Hermes 受控覆核與陳舊 KM 狀態,避免誤判成知識庫歸零。",
"sourceBackedTitle": "AI source-backed KM 已接管",
"sourceBackedSummary": "資料未歸零",
"sourceBackedDescription": "Primary KM DB readback 正在受控修復;目前顯示 source-backed 條目、分類與資產 taxonomy不是知識庫歸零。原因{reason};階段:{stage};下一步:{next}。",
"degradedTitle": "知識條目讀取降級",
"degradedSummary": "Primary readback 降級;列表保留可用資料。",
"degradedDescription": "主知識條目 API 已回 degraded{stage};下一步:{next}。目前列表不視為真實歸零AI 受控修復隊列與 RAG / PlayBook readback 仍會保留。",
"mode": {
"sourceBacked": "接管"
},
"chip": {
"mode": "模式",
"reason": "原因",
"stage": "階段",
"next": "下一步",
"entries": "條目",
"taxonomy": "分類 / 資產"
},
"retry": "重新讀取"
},
"decision": {

View File

@@ -438,6 +438,18 @@ const humanizeSignalTag = (tag: string) => {
.join(' ')
}
const compactReadbackToken = (value: string | null | undefined) => {
if (!value) return '--'
if (value === 'primary_km_db_timeout_or_pool_exhausted') return 'DB pool'
if (value === 'knowledge_readback_source_backed_ai_controlled_repair') return '接管'
if (value === 'repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km') return '修復 KM'
if (value === 'primary_km_taxonomy_readback_degraded') return 'Taxonomy 降級'
if (value === 'knowledge_primary_entries_ready_taxonomy_degraded') return 'Primary 可讀'
if (value === 'repair_primary_km_taxonomy_readback_without_hiding_entries') return '修復 taxonomy'
if (value.startsWith('source_backed')) return '接管'
return humanizeSignalTag(value)
}
const getSignalTags = (entry: KnowledgeEntry) => {
const seen = new Set<string>()
return entry.tags
@@ -698,6 +710,58 @@ export default function KnowledgeBasePage({
(value: number) => value.toLocaleString(localeCode),
[localeCode],
)
const readbackNoticeChips = useMemo(
() => [
{
label: t('dataChain.chip.mode'),
value: isKnowledgeSourceBacked ? t('dataChain.mode.sourceBacked') : compactReadbackToken(readbackStatus),
icon: CheckCircle2,
tone: 'border-status-healthy/20 bg-status-healthy/10 text-status-healthy',
},
{
label: t('dataChain.chip.reason'),
value: compactReadbackToken(entryReadback?.degraded_reason_code),
icon: Database,
tone: 'border-status-warning/20 bg-status-warning/10 text-status-warning',
},
{
label: t('dataChain.chip.stage'),
value: compactReadbackToken(entryReadback?.operator_stage),
icon: PlayCircle,
tone: 'border-claw-blue/20 bg-claw-blue/8 text-claw-blue',
},
{
label: t('dataChain.chip.next'),
value: compactReadbackToken(entryReadback?.next_step),
icon: ArrowRight,
tone: 'border-nothing-gray-200 bg-white text-secondary',
},
{
label: t('dataChain.chip.entries'),
value: formatCount(total),
icon: BookOpen,
tone: 'border-claw-blue/20 bg-white text-claw-blue',
},
{
label: t('dataChain.chip.taxonomy'),
value: `${formatCount(categories.length)} / ${formatCount(assetTaxonomy.length)}`,
icon: Tag,
tone: 'border-purple-200 bg-purple-50 text-purple-600',
},
],
[
assetTaxonomy.length,
categories.length,
entryReadback?.degraded_reason_code,
entryReadback?.next_step,
entryReadback?.operator_stage,
formatCount,
isKnowledgeSourceBacked,
readbackStatus,
t,
total,
],
)
const formatCategoryLabel = useCallback(
(category: string) => {
if (KNOWN_CATEGORY_KEYS.has(category)) {
@@ -1422,33 +1486,44 @@ export default function KnowledgeBasePage({
</div>
{(entryFetchError || isKnowledgeListDegraded) && (
<div className="border-b border-status-warning/20 bg-status-warning/10 px-4 py-3">
<div className="flex flex-col gap-2 rounded-md border border-status-warning/25 bg-white/75 px-3 py-2 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2 text-xs font-label text-status-warning">
<TriangleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
<span>
<div className="border-b border-status-warning/20 bg-white px-4 py-2">
<div className="flex flex-col gap-2 rounded-md border border-status-warning/20 bg-status-warning/5 px-3 py-2 xl:flex-row xl:items-center xl:justify-between">
<div className="grid min-w-0 flex-1 gap-2 xl:grid-cols-[minmax(220px,0.8fr)_minmax(0,2fr)] xl:items-center">
<div className="min-w-0">
<div className="flex items-center gap-2 text-xs font-label text-status-warning">
<TriangleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">
{entryFetchError
? t('dataChain.errorTitle')
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedTitle')
: t('dataChain.degradedTitle')}
</span>
</div>
<p className="mt-1 truncate text-[11px] font-body text-muted">
{entryFetchError
? t('dataChain.errorTitle')
? t('dataChain.errorDescription', { reason: entryFetchError })
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedTitle')
: t('dataChain.degradedTitle')}
</span>
? t('dataChain.sourceBackedSummary')
: t('dataChain.degradedSummary')}
</p>
</div>
<p className="mt-1 text-xs font-body leading-5 text-secondary">
{entryFetchError
? t('dataChain.errorDescription', { reason: entryFetchError })
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedDescription', {
reason: entryReadback?.degraded_reason_code ?? '--',
stage: entryReadback?.operator_stage ?? '--',
next: entryReadback?.next_step ?? '--',
})
: t('dataChain.degradedDescription', {
stage: entryReadback?.operator_stage ?? '--',
next: entryReadback?.next_step ?? '--',
})}
</p>
{!entryFetchError && (
<div className="grid grid-cols-2 gap-1.5 sm:grid-cols-3 xl:grid-cols-6">
{readbackNoticeChips.map(chip => {
const Icon = chip.icon
return (
<div key={chip.label} className={cn('min-w-0 rounded-md border px-2 py-1.5', chip.tone)}>
<div className="flex items-center gap-1.5">
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="truncate text-[9px] font-label uppercase tracking-wider opacity-75">{chip.label}</span>
</div>
<div className="mt-1 truncate text-xs font-heading font-semibold tabular-nums">{chip.value}</div>
</div>
)
})}
</div>
)}
</div>
<button
onClick={() => {