'use client' /** * NeuralLiveCenter - 即時指揮中心 * ================================= * 三欄: OpenClaw/NemoTron 狀態 | 神經傳導鏈路 | 執行串流 * * 2026-04-07 Claude Code: Sprint F 打假行動 — 移除所有假數據,接上真實 props * 統帥鐵律: 禁止假數據!無資料時顯示 '--' 或 EmptyState */ import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { Bot, CheckCircle2, Clock, Cpu, Inbox, Loader2, Server, Settings, XCircle } from 'lucide-react' import type { AutoRepairStats, RepairHistoryItem, UriScheme, RepairStatus, ActiveIncident } from './types' interface Props { stats: AutoRepairStats | null history: RepairHistoryItem[] pendingCount: number activeIncidents?: ActiveIncident[] } // URI scheme display config const SCHEME_CONFIG = { 'kubectl://': { label: 'kubectl://', color: 'text-blue-500', bg: 'bg-blue-500/10', border: 'border-blue-500/25' }, 'openclaw://': { label: 'openclaw://', color: 'text-orange-500', bg: 'bg-orange-500/10', border: 'border-orange-500/25' }, 'ansible://': { label: 'ansible://', color: 'text-purple-500', bg: 'bg-purple-500/10', border: 'border-purple-500/25' }, } const STATUS_CONFIG = { success: { Icon: CheckCircle2, color: 'text-green-500', label: '成功' }, failed: { Icon: XCircle, color: 'text-red-500', label: '失敗' }, pending_approval: { Icon: Clock, color: 'text-orange-500', label: '待授權' }, running: { Icon: Loader2, color: 'text-blue-500', label: '執行中' }, } const NODE_STATE_STYLES = { done: 'border-green-500/40 bg-green-500/5 opacity-70', active: 'border-blue-500 bg-blue-500/5 shadow-[0_0_0_3px_rgba(59,130,246,0.1)]', waiting: 'border-border opacity-40', } const SEVERITY_DOT_STYLES: Record = { P0: 'bg-red-500 ring-red-500/20', P1: 'bg-orange-500 ring-orange-500/20', P2: 'bg-yellow-500 ring-yellow-500/20', P3: 'bg-green-500 ring-green-500/20', } // 從最新一筆 history 衍生鏈路狀態 function deriveChainNodes(latestItem: RepairHistoryItem | null, t: ReturnType) { if (!latestItem) { return [ { id: 'alert', label: t('chainAlert'), sub: t('chainIdleSub'), state: 'waiting' as const }, { id: 'rag', label: t('chainRAG'), sub: '--', state: 'waiting' as const }, { id: 'decide', label: t('chainDecide'), sub: '--', state: 'waiting' as const }, { id: 'exec', label: t('chainExec'), sub: '--', state: 'waiting' as const }, ] } const isRunning = latestItem.status === 'running' const isPending = latestItem.status === 'pending_approval' const isDone = latestItem.status === 'success' || latestItem.status === 'failed' return [ { id: 'alert', label: t('chainAlert'), sub: `${latestItem.incident_id} · ${latestItem.playbook_name}`, state: 'done' as const }, { id: 'rag', label: t('chainRAG'), sub: latestItem.rag_confidence ? `${t('ragConf')} ${latestItem.rag_confidence.toFixed(2)}` : '--', state: 'done' as const }, { id: 'decide', label: t('chainDecide'), sub: isPending ? t('waitingApproval') : (isDone ? t('nodeDone') : t('nodeActive')), state: (isPending || isDone) ? 'done' as const : 'active' as const }, { id: 'exec', label: t('chainExec'), sub: latestItem.command, state: isRunning ? 'active' as const : (isDone ? 'done' as const : 'waiting' as const) }, ] } // 從 history 計算平均時長 function computeAvgDuration(history: RepairHistoryItem[]): string { const durations = history.filter(h => h.duration_ms != null).map(h => h.duration_ms!) if (durations.length === 0) return '--' const avg = durations.reduce((a, b) => a + b, 0) / durations.length return `${(avg / 1000).toFixed(1)}s` } function severityDotClass(severity: string): string { return SEVERITY_DOT_STYLES[severity] ?? SEVERITY_DOT_STYLES.P3 } export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents = [] }: Props) { const t = useTranslations('neuralCommand') const latestItem = history.length > 0 ? history[0] : null const chainNodes = deriveChainNodes(latestItem, t) // 從 stats 衍生 KPI,無資料顯示 '--' const successCount = stats ? Math.round(stats.total_executions * stats.overall_success_rate) : null const totalExec = stats?.total_executions ?? null return (
{/* ── Left: Agents + Alert radar ── */}
{/* OpenClaw */}

OpenClaw

{t('agentRoleOC')}

Playbooks{stats?.approved_playbooks ?? '--'}
{t('todayMatches')}{history.length > 0 ? history.length : '--'}
{t('ragConf')}{latestItem?.rag_confidence?.toFixed(2) ?? '--'}
{/* NemoTron */}

NemoTron

{t('agentRoleNemo')}

{t('execSuccess')}{successCount != null && totalExec != null ? `${successCount}/${totalExec}` : '--'}
{t('avgDuration')}{computeAvgDuration(history)}
{t('pendingApproval')}{pendingCount}
{/* Alert radar — 從真實 incidents 或 history 衍生 */}

{t('alertRadar')}

{activeIncidents.length > 0 ? ( activeIncidents.slice(0, 5).map((inc, i) => (

{inc.incident_id}

{inc.affected_services.join(', ') || inc.status}

{inc.severity}
)) ) : (

{t('noActiveAlerts')}

)}
{/* ── Center: Chain visualization ── */}

{t('chainTitle')}{latestItem ? ` — ${latestItem.incident_id} · ${latestItem.playbook_name}` : ''}

{chainNodes.map((node, i) => (

{node.label}

{node.sub}

{node.state === 'done' && {t('nodeDone')}} {node.state === 'active' && {t('nodeActive')}} {node.state === 'waiting'&& {t('nodeWaiting')}}
{i < chainNodes.length - 1 && (
)}
))} {/* Branches */}
{[ { scheme: 'kubectl://', Icon: Server, label: 'kubectl', cls: 'border-blue-500/25 bg-blue-500/5', iconCls: 'text-blue-500' }, { scheme: 'openclaw://', Icon: Bot, label: 'OpenClaw', cls: 'border-orange-500/25 bg-orange-500/5', iconCls: 'text-orange-500' }, { scheme: 'ansible://', Icon: Settings, label: 'Ansible .188', cls: 'border-purple-500/25 bg-purple-500/5', iconCls: 'text-purple-500' }, ].map(b => { const BranchIcon = b.Icon return (
) })}
{/* ── Right: Execution log stream ── */}

{t('execStream')}

{history.length > 0 ? (
{history.map(item => { const scheme = SCHEME_CONFIG[item.uri_scheme as UriScheme] ?? SCHEME_CONFIG['kubectl://'] const statusCfg = STATUS_CONFIG[item.status as RepairStatus] const StatusIcon = statusCfg.Icon const elapsed = Math.round((Date.now() - new Date(item.executed_at).getTime()) / 60000) return (
{elapsed}m {scheme.label}

{item.playbook_name}

{item.duration_ms && (

{(item.duration_ms / 1000).toFixed(1)}s

)} {item.error && (

{item.error}

)} {item.status === 'pending_approval' && (

{t('waitingApproval')}

)} {item.rag_confidence && (

RAG {item.rag_confidence.toFixed(2)}

)}
) })}
) : (

{t('noHistory')}

)}
) }