feat(web): Tab3 Chain-of-Thought 面板 + Tab4 by_anomaly Top5 + MTTR
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m1s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m1s
Tab 3 ActivityStreamTab:
- 點擊 SSE 事件展開 COT 側面板(含 provider/confidence/latency/tools/reasoning)
- 有 proposal_data 的事件顯示 COT badge
- 點擊同一事件收合面板
Tab 4 DispositionTab:
- by_anomaly Top5 水平進度條(按 auto-repair 率著色:≥80% 綠/≥50% 橙/其他紅)
- MTTR 大字顯示(分鐘)+ 無資料時 fallback
i18n: cotTitle/cotReasoning/cotConfidence/cotProvider/cotLatency/cotTools/
cotClickHint/byAnomalyTitle/byAnomalyAutoRate/mttrTitle/mttrUnit/mttrNoData
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -204,7 +204,20 @@
|
||||
"viewAllReport": "View Full Report",
|
||||
"aiModelStatus": "AI Model Status",
|
||||
"loading": "Loading...",
|
||||
"trendUp": "↑{pct}%"
|
||||
"trendUp": "↑{pct}%",
|
||||
"cotTitle": "Reasoning Timeline",
|
||||
"cotNoEvents": "Waiting for reasoning data...",
|
||||
"cotReasoning": "Reasoning",
|
||||
"cotConfidence": "Confidence",
|
||||
"cotProvider": "Model",
|
||||
"cotLatency": "Latency",
|
||||
"cotTools": "Tool Calls",
|
||||
"cotClickHint": "Click an event to view reasoning details",
|
||||
"byAnomalyTitle": "Anomaly Type Distribution Top 5",
|
||||
"byAnomalyAutoRate": "Auto-Repair Rate",
|
||||
"mttrTitle": "MTTR Overview",
|
||||
"mttrUnit": "s",
|
||||
"mttrNoData": "No MTTR data yet"
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -205,7 +205,20 @@
|
||||
"viewAllReport": "查看完整報表",
|
||||
"aiModelStatus": "AI 模型狀態",
|
||||
"loading": "載入中...",
|
||||
"trendUp": "↑{pct}%"
|
||||
"trendUp": "↑{pct}%",
|
||||
"cotTitle": "推理時間軸",
|
||||
"cotNoEvents": "等待事件推理資料...",
|
||||
"cotReasoning": "推理",
|
||||
"cotConfidence": "信心",
|
||||
"cotProvider": "模型",
|
||||
"cotLatency": "耗時",
|
||||
"cotTools": "工具呼叫",
|
||||
"cotClickHint": "點擊事件查看推理細節",
|
||||
"byAnomalyTitle": "異常類型分佈 Top 5",
|
||||
"byAnomalyAutoRate": "自動修復率",
|
||||
"mttrTitle": "MTTR 概覽",
|
||||
"mttrUnit": "秒",
|
||||
"mttrNoData": "尚無 MTTR 資料"
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -117,6 +117,7 @@ function ActivityStreamTab() {
|
||||
const t = useTranslations('dashboard')
|
||||
const [events, setEvents] = useState<any[]>([])
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [selectedIdx, setSelectedIdx] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(`${API_BASE}/api/v1/dashboard/stream`)
|
||||
@@ -131,6 +132,9 @@ function ActivityStreamTab() {
|
||||
return () => es.close()
|
||||
}, [])
|
||||
|
||||
const selected = selectedIdx !== null ? events[selectedIdx] : null
|
||||
const cot = selected?.data?.incident?.decision?.proposal_data ?? selected?.data?.proposal_data ?? null
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px 20px', flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
@@ -140,17 +144,83 @@ function ActivityStreamTab() {
|
||||
</div>
|
||||
{events.length === 0 ? (
|
||||
<div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>{t('waitingEvents')}</div>
|
||||
) : events.map((ev, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 8, padding: '6px 0', borderBottom: '0.5px solid #f0ede5', fontSize: 12 }}>
|
||||
<span style={{ fontSize: 10, color: '#87867f', fontFamily: "'JetBrains Mono', monospace", width: 60, flexShrink: 0 }}>{ev._time}</span>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: ev.type === 'HOST_UPDATE' ? '#22C55E' : '#4A90D9', marginTop: 5, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, lineHeight: 1.4 }}>
|
||||
<b>{ev.type || 'EVENT'}</b>
|
||||
{ev.data?.overall_status && ` · ${t('statusLabel')}: ${ev.data.overall_status}`}
|
||||
{ev.data?.hosts && ` · ${ev.data.hosts.length} ${t('hostsLabel')}`}
|
||||
</span>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: cot ? '1fr 1fr' : '1fr', gap: 12 }}>
|
||||
{/* Event list */}
|
||||
<div>
|
||||
{events.map((ev, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setSelectedIdx(i === selectedIdx ? null : i)}
|
||||
style={{
|
||||
display: 'flex', gap: 8, padding: '7px 8px', borderBottom: '0.5px solid #f0ede5',
|
||||
fontSize: 12, cursor: 'pointer', borderRadius: 5,
|
||||
background: i === selectedIdx ? 'rgba(74,144,217,0.06)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, color: '#87867f', fontFamily: "'JetBrains Mono', monospace", width: 60, flexShrink: 0 }}>{ev._time}</span>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: ev.type === 'HOST_UPDATE' ? '#22C55E' : '#4A90D9', marginTop: 5, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, lineHeight: 1.4 }}>
|
||||
<b>{ev.type || 'EVENT'}</b>
|
||||
{ev.data?.overall_status && ` · ${t('statusLabel')}: ${ev.data.overall_status}`}
|
||||
{ev.data?.hosts && ` · ${ev.data.hosts.length} ${t('hostsLabel')}`}
|
||||
{(ev.data?.incident?.decision?.proposal_data || ev.data?.proposal_data) && (
|
||||
<span style={{ marginLeft: 4, fontSize: 9, background: 'rgba(74,144,217,0.12)', color: '#4A90D9', borderRadius: 3, padding: '1px 5px', fontWeight: 600 }}>COT</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{selectedIdx === null && events.length > 0 && (
|
||||
<div style={{ marginTop: 8, fontSize: 10, color: '#87867f', textAlign: 'center' }}>{t('cotClickHint')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chain of Thought panel */}
|
||||
{cot && (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px', fontSize: 12 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#141413', marginBottom: 10 }}>{t('cotTitle')}</div>
|
||||
{/* Provider + confidence row */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
{cot.provider && (
|
||||
<span style={{ fontSize: 10, background: 'rgba(74,144,217,0.1)', color: '#4A90D9', borderRadius: 4, padding: '2px 8px', fontWeight: 600 }}>
|
||||
{t('cotProvider')}: {cot.provider}
|
||||
</span>
|
||||
)}
|
||||
{cot.confidence !== undefined && (
|
||||
<span style={{ fontSize: 10, background: cot.confidence >= 0.8 ? 'rgba(34,197,94,0.1)' : 'rgba(245,158,11,0.1)', color: cot.confidence >= 0.8 ? '#22C55E' : '#F59E0B', borderRadius: 4, padding: '2px 8px', fontWeight: 600 }}>
|
||||
{t('cotConfidence')}: {Math.round(cot.confidence * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{cot.nemotron_latency_ms !== undefined && (
|
||||
<span style={{ fontSize: 10, background: 'rgba(168,85,247,0.08)', color: '#A855F7', borderRadius: 4, padding: '2px 8px', fontWeight: 600 }}>
|
||||
{t('cotLatency')}: {cot.nemotron_latency_ms}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Tools used */}
|
||||
{Array.isArray(cot.nemotron_tools) && cot.nemotron_tools.length > 0 && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#87867f', marginBottom: 4 }}>{t('cotTools')}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{cot.nemotron_tools.map((tool: string, i: number) => (
|
||||
<span key={i} style={{ fontSize: 10, background: '#f5f4ed', border: '0.5px solid #e0ddd4', borderRadius: 3, padding: '2px 6px', fontFamily: "'JetBrains Mono', monospace" }}>{tool}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Reasoning */}
|
||||
{cot.reasoning && (
|
||||
<div>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#87867f', marginBottom: 4 }}>{t('cotReasoning')}</div>
|
||||
<div style={{ fontSize: 11, color: '#555550', lineHeight: 1.5, background: '#faf9f3', border: '0.5px solid #e0ddd4', borderRadius: 6, padding: '8px 10px', maxHeight: 180, overflowY: 'auto', fontFamily: "'JetBrains Mono', monospace", whiteSpace: 'pre-wrap' }}>
|
||||
{cot.reasoning}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -216,7 +286,7 @@ function DispositionTab() {
|
||||
))}
|
||||
</div>
|
||||
{/* 堆疊分佈條 */}
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 8 }}>{t('dispositionBreakdown')}</div>
|
||||
<div style={{ display: 'flex', height: 12, borderRadius: 6, overflow: 'hidden' }}>
|
||||
{s.total > 0 && <>
|
||||
@@ -227,6 +297,55 @@ function DispositionTab() {
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* By Anomaly Top 5 */}
|
||||
{Array.isArray(data?.by_anomaly) && data.by_anomaly.length > 0 && (() => {
|
||||
const top5 = [...data.by_anomaly].sort((a: any, b: any) => b.count - a.count).slice(0, 5)
|
||||
const maxCount = top5[0]?.count || 1
|
||||
return (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 10 }}>{t('byAnomalyTitle')}</div>
|
||||
{top5.map((item: any, i: number) => {
|
||||
const autoRate = item.count > 0 ? Math.round((item.auto_repair ?? 0) / item.count * 100) : 0
|
||||
return (
|
||||
<div key={i} style={{ marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 3 }}>
|
||||
<span style={{ color: '#141413', fontWeight: 500, maxWidth: '70%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.anomaly_type || item.type || '--'}</span>
|
||||
<span style={{ color: '#87867f', fontFamily: "'JetBrains Mono', monospace", fontSize: 10 }}>
|
||||
{item.count} · {t('byAnomalyAutoRate', { pct: autoRate })}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ height: 6, background: '#f0ede5', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${(item.count / maxCount) * 100}%`, background: autoRate >= 80 ? '#22C55E' : autoRate >= 50 ? '#F59E0B' : '#cc2200', borderRadius: 3, transition: 'width 0.4s ease' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* MTTR */}
|
||||
{(() => {
|
||||
const mttr = data?.mttr
|
||||
if (!mttr) return (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 6 }}>{t('mttrTitle')}</div>
|
||||
<div style={{ fontSize: 11, color: '#87867f' }}>{t('mttrNoData')}</div>
|
||||
</div>
|
||||
)
|
||||
const mttrVal = typeof mttr === 'number' ? mttr : mttr.avg_seconds ?? mttr.value ?? null
|
||||
const mttrMin = mttrVal !== null ? (mttrVal / 60).toFixed(1) : '--'
|
||||
return (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 6 }}>{t('mttrTitle')}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
|
||||
<span style={{ fontSize: 32, fontWeight: 700, color: '#4A90D9' }}>{mttrMin}</span>
|
||||
<span style={{ fontSize: 11, color: '#87867f' }}>{t('mttrUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user