fix(web): clarify knowledge base signal chips
All checks were successful
CD Pipeline / tests (push) Successful in 1m22s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 3m58s
CD Pipeline / post-deploy-checks (push) Successful in 3m14s

This commit is contained in:
Your Name
2026-06-03 01:55:49 +08:00
parent 4018a05983
commit 8cb4af36b8
3 changed files with 190 additions and 19 deletions

View File

@@ -1446,6 +1446,26 @@
"ai_extracted": "AI 萃取",
"human": "人工建立"
},
"signals": {
"label": "訊號",
"detailTitle": "證據訊號",
"rawTitle": "原始技術標籤",
"more": "+{count}"
},
"tag": {
"ai_extracted": "AI 萃取",
"auto_runbook": "自動 Runbook",
"critical": "嚴重",
"execution": "執行紀錄",
"execution_failed": "執行失敗",
"failure": "失敗",
"human_approved": "人工批准",
"human_intervention": "人工介入",
"incident": "事故",
"postmortem": "事後檢討",
"telegram": "Telegram",
"warning": "警告"
},
"category": {
"AI治理": "AI 治理",
"alert_handling": "告警處理",

View File

@@ -1446,6 +1446,26 @@
"ai_extracted": "AI 萃取",
"human": "人工建立"
},
"signals": {
"label": "訊號",
"detailTitle": "證據訊號",
"rawTitle": "原始技術標籤",
"more": "+{count}"
},
"tag": {
"ai_extracted": "AI 萃取",
"auto_runbook": "自動 Runbook",
"critical": "嚴重",
"execution": "執行紀錄",
"execution_failed": "執行失敗",
"failure": "失敗",
"human_approved": "人工批准",
"human_intervention": "人工介入",
"incident": "事故",
"postmortem": "事後檢討",
"telegram": "Telegram",
"warning": "警告"
},
"category": {
"AI治理": "AI 治理",
"alert_handling": "告警處理",

View File

@@ -17,7 +17,7 @@ import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import {
Search, BookOpen, FileText, Shield, Cpu,
Server, Eye, Bot, ChevronRight, Plus, Sparkles, ClipboardList, TriangleAlert,
Server, Eye, Bot, ChevronRight, Plus, Sparkles, ClipboardList, Tag, TriangleAlert,
} from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
@@ -89,6 +89,34 @@ const KNOWN_CATEGORY_KEYS = new Set([
])
const CATEGORY_OVERVIEW_LIMIT = 8
const KNOWN_TAG_KEYS = new Set([
'ai_extracted',
'auto_runbook',
'critical',
'execution',
'execution_failed',
'failure',
'human_approved',
'human_intervention',
'incident',
'postmortem',
'telegram',
'warning',
])
const REDUNDANT_SIGNAL_TAGS = new Set([
'AI治理',
'alert_handling',
'application',
'ai_system',
'database',
'general',
'host_resource',
'incident_case',
'infrastructure',
'postmortem',
'security',
])
// =============================================================================
// Type Badge Colors
@@ -122,6 +150,20 @@ const CATEGORY_BAR_COLORS: Record<string, string> = {
__other__: 'bg-nothing-gray-300',
}
const TAG_TONES: Record<string, string> = {
critical: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
execution_failed: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
failure: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
human_intervention: 'border-status-warning/20 bg-status-warning/10 text-status-warning',
warning: 'border-status-warning/20 bg-status-warning/10 text-status-warning',
human_approved: 'border-status-healthy/20 bg-status-healthy/10 text-status-healthy',
ai_extracted: 'border-claw-blue/20 bg-claw-blue/10 text-claw-blue',
auto_runbook: 'border-purple-200 bg-purple-100 text-purple-600',
incident: 'border-nothing-gray-200 bg-white text-secondary',
postmortem: 'border-status-critical/20 bg-white text-status-critical',
telegram: 'border-claw-blue/20 bg-white text-claw-blue',
}
const humanizeCategory = (category: string) => {
if (!category.includes('_')) return category
return category
@@ -135,6 +177,31 @@ const humanizeCategory = (category: string) => {
.join(' ')
}
const humanizeSignalTag = (tag: string) => {
if (!tag.includes('_')) return tag
return tag
.split('_')
.filter(Boolean)
.map(part => {
const upper = part.toUpperCase()
if (['AI', 'CPU', 'DB', 'DNS', 'HTTP', 'IP', 'KB', 'P0', 'P1', 'P2', 'P3', 'SSL', 'SLO', 'SSH', 'TYPE'].includes(upper)) return upper
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`
})
.join(' ')
}
const getSignalTags = (entry: KnowledgeEntry) => {
const seen = new Set<string>()
return entry.tags
.filter(tag => tag && tag !== entry.category && tag !== entry.entry_type && tag !== entry.source)
.filter(tag => !REDUNDANT_SIGNAL_TAGS.has(tag))
.filter(tag => {
if (seen.has(tag)) return false
seen.add(tag)
return true
})
}
// =============================================================================
// Component
// =============================================================================
@@ -276,6 +343,19 @@ export default function KnowledgeBasePage({
},
[t],
)
const formatDate = useCallback(
(value: string) => new Date(value).toLocaleDateString(localeCode),
[localeCode],
)
const formatSignalTag = useCallback(
(tag: string) => {
if (KNOWN_TAG_KEYS.has(tag)) {
return t(`tag.${tag}`)
}
return humanizeSignalTag(tag)
},
[t],
)
const visibleSummary = useMemo(() => {
const loaded = displayedEntries.length
@@ -488,7 +568,12 @@ export default function KnowledgeBasePage({
</div>
) : (
<div className="divide-y divide-nothing-gray-100">
{displayedEntries.map(entry => (
{displayedEntries.map(entry => {
const signalTags = getSignalTags(entry)
const visibleSignals = signalTags.slice(0, 3)
const hiddenSignalCount = Math.max(0, signalTags.length - visibleSignals.length)
return (
<button
key={entry.id}
onClick={() => setSelectedEntry(entry)}
@@ -524,17 +609,33 @@ export default function KnowledgeBasePage({
<span>·</span>
<span>{t(`source.${entry.source}`)}</span>
<span>·</span>
<span>{new Date(entry.created_at).toLocaleDateString()}</span>
<span>{formatDate(entry.created_at)}</span>
</div>
{/* Tags */}
{entry.tags.length > 0 && (
<div className="flex gap-1 mt-1.5">
{entry.tags.slice(0, 4).map(tag => (
<span key={tag} className="text-[10px] font-body px-1.5 py-0.5 rounded bg-nothing-gray-100 text-muted">
{tag}
{/* Evidence signals */}
{visibleSignals.length > 0 && (
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
<span className="flex items-center gap-1 text-[10px] font-label uppercase tracking-wider text-muted">
<Tag className="h-3 w-3" aria-hidden="true" />
{t('signals.label')}
</span>
{visibleSignals.map(tag => (
<span
key={tag}
className={cn(
'max-w-[160px] truncate rounded-full border px-1.5 py-0.5 text-[10px] font-body',
TAG_TONES[tag] ?? 'border-nothing-gray-200 bg-nothing-gray-50 text-muted',
)}
title={tag}
>
{formatSignalTag(tag)}
</span>
))}
{hiddenSignalCount > 0 && (
<span className="rounded-full border border-nothing-gray-200 bg-white px-1.5 py-0.5 text-[10px] font-label text-muted">
{t('signals.more', { count: hiddenSignalCount })}
</span>
)}
</div>
)}
</div>
@@ -556,7 +657,8 @@ export default function KnowledgeBasePage({
</div>
</div>
</button>
))}
)
})}
</div>
)}
</div>
@@ -603,7 +705,7 @@ export default function KnowledgeBasePage({
<span>·</span>
<span><Eye className="w-3 h-3 inline" /> {selectedEntry.view_count} {t('viewCount')}</span>
<span>·</span>
<span>{new Date(selectedEntry.updated_at).toLocaleDateString()}</span>
<span>{formatDate(selectedEntry.updated_at)}</span>
</div>
{/* Content — react-markdown (KB-D 2026-04-03 ogt) */}
@@ -617,17 +719,46 @@ export default function KnowledgeBasePage({
</ReactMarkdown>
</div>
{/* Tags */}
{selectedEntry.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-4">
{selectedEntry.tags.map(tag => (
<span key={tag} className="text-[10px] font-body px-2 py-0.5 rounded-full bg-nothing-gray-100 text-muted">
{tag}
</span>
))}
{/* Evidence signals */}
{getSignalTags(selectedEntry).length > 0 && (
<div className="mb-4 rounded-md border border-nothing-gray-200 bg-white/70 p-3">
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-label uppercase tracking-wider text-muted">
<Tag className="h-3 w-3" aria-hidden="true" />
{t('signals.detailTitle')}
</div>
<div className="flex flex-wrap gap-1.5">
{getSignalTags(selectedEntry).map(tag => (
<span
key={tag}
className={cn(
'max-w-full truncate rounded-full border px-2 py-0.5 text-[10px] font-body',
TAG_TONES[tag] ?? 'border-nothing-gray-200 bg-nothing-gray-50 text-muted',
)}
title={tag}
>
{formatSignalTag(tag)}
</span>
))}
</div>
</div>
)}
{/* Raw technical tags */}
{selectedEntry.tags.length > 0 && (
<details className="mb-4 rounded-md border border-nothing-gray-200 bg-white/50 p-3">
<summary className="cursor-pointer text-[10px] font-label uppercase tracking-wider text-muted">
{t('signals.rawTitle')}
</summary>
<div className="mt-2 flex flex-wrap gap-1">
{selectedEntry.tags.map(tag => (
<span key={tag} className="rounded-full bg-nothing-gray-100 px-2 py-0.5 font-mono text-[10px] text-muted">
{tag}
</span>
))}
</div>
</details>
)}
{/* Related links */}
{selectedEntry.related_playbook_id && (
<div className="flex items-center gap-1.5 text-xs text-claw-blue font-body mb-2">