fix(web): clarify knowledge base signal chips
This commit is contained in:
@@ -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": "告警處理",
|
||||
|
||||
@@ -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": "告警處理",
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user