Files
awoooi/apps/web/src/components/panels/NeuralCommandPanel.tsx
OG T 7934ade3a6
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
refactor(web): 全部 13 Panel 抽取完成 + 整合頁面雙重 AppLayout 修正
Panel 抽取 (13 個):
- MonitoringPanel, ApmPanel, ErrorsPanel, AppsPanel, ServicesPanel
- AutoRepairPanel, NeuralCommandPanel, DriftPanel
- DeploymentsPanel, TicketsPanel, CostPanel, ActionLogsPanel, BillingPanel

整合頁面更新 (全部使用 Panel,無雙重 AppLayout):
- /observability: 5 Panel
- /automation: 3 Panel
- /operations: 5 Panel

首席架構師 I2 問題已解決

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:05:37 +08:00

182 lines
7.4 KiB
TypeScript

'use client'
/**
* NeuralCommandPanel — 神經指揮中心面板 (不含 AppLayout)
* =========================================================
* Sprint 5: 從 /neural-command/page.tsx 抽取
* 供原始頁面和整合頁面 (/automation) 共用
*
* 建立時間: 2026-04-09 (台北時區)
*/
import { useState, useEffect, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import {
BrainCircuit, ShieldCheck,
RefreshCw, Clock, Database,
Activity, Lock,
} from 'lucide-react'
import { NeuralPreFlight } from '@/components/neural-command/NeuralPreFlight'
import { NeuralLiveCenter } from '@/components/neural-command/NeuralLiveCenter'
import { NeuralStats } from '@/components/neural-command/NeuralStats'
import { NeuralApprovalPanel } from '@/components/neural-command/NeuralApprovalPanel'
import type { AutoRepairStats, PlaybookItem, RepairHistoryItem, NeuralTab, PendingApprovalItem, ActiveIncident } from '@/components/neural-command/types'
const TABS: { id: NeuralTab; labelKey: string; Icon: React.ElementType }[] = [
{ id: 'preflight', labelKey: 'preFlightAudit', Icon: ShieldCheck },
{ id: 'live', labelKey: 'liveCommand', Icon: Activity },
{ id: 'stats', labelKey: 'statsHistory', Icon: Database },
{ id: 'approval', labelKey: 'nuclearApproval', Icon: Lock },
]
export function NeuralCommandPanel() {
const t = useTranslations('neuralCommand')
const [activeTab, setActiveTab] = useState<NeuralTab>('preflight')
const [stats, setStats] = useState<AutoRepairStats | null>(null)
const [playbooks, setPlaybooks] = useState<PlaybookItem[]>([])
const [history, setHistory] = useState<RepairHistoryItem[]>([])
const [loading, setLoading] = useState(true)
const [lastRefresh, setLastRefresh] = useState<Date>(new Date())
const [pendingApprovals, setPendingApprovals] = useState(0)
const [pendingApprovalList, setPendingApprovalList] = useState<PendingApprovalItem[]>([])
const [activeIncidents, setActiveIncidents] = useState<ActiveIncident[]>([])
const [dispositionSummary, setDispositionSummary] = useState<{ total: number; auto_repair: number; human_approved: number; manual_resolved: number; cold_start_trust: number; auto_rate: number } | null>(null)
const fetchData = useCallback(async () => {
try {
const [statsRes, pbRes, histRes, approvalsRes, incidentsRes, dispRes] = await Promise.all([
fetch('/api/v1/auto-repair/stats'),
fetch('/api/v1/playbooks/'),
fetch('/api/v1/auto-repair/history?limit=20'),
fetch('/api/v1/approvals/pending'),
fetch('/api/v1/incidents?status=firing&limit=10'),
fetch('/api/v1/stats/disposition').catch(() => null),
])
if (statsRes.ok) {
const data = await statsRes.json()
setStats(data)
}
if (pbRes.ok) {
const data = await pbRes.json()
setPlaybooks(data.items?.map((i: { playbook: PlaybookItem }) => i.playbook) ?? [])
}
if (histRes.ok) {
const data = await histRes.json()
setHistory(data.items ?? [])
}
if (approvalsRes.ok) {
const data = await approvalsRes.json()
setPendingApprovals(data.count ?? 0)
setPendingApprovalList(data.approvals ?? [])
}
if (incidentsRes.ok) {
const data = await incidentsRes.json()
setActiveIncidents(data.incidents ?? [])
}
if (dispRes?.ok) {
const data = await dispRes.json()
setDispositionSummary(data.summary ?? null)
}
setLastRefresh(new Date())
} catch {
// silently fail — show stale data
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
const interval = setInterval(fetchData, 30_000)
return () => clearInterval(interval)
}, [fetchData])
const approvedPlaybooks = playbooks.filter(p => p.status === 'approved')
return (
<div className="flex flex-col h-full min-h-0">
{/* Page header */}
<div className="flex items-start justify-between px-6 pt-5 pb-4 border-b border-border flex-shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-500/10 border border-orange-500/25 flex items-center justify-center">
<BrainCircuit className="w-5 h-5 text-orange-500" />
</div>
<div>
<h1 className="text-lg font-semibold tracking-tight">{t('title')}</h1>
<p className="text-xs text-muted-foreground mt-0.5">{t('subtitle')}</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<span className="flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-orange-500/30 bg-orange-500/5 text-orange-500 font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 animate-pulse" />
OpenClaw
</span>
<span className="flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-blue-500/30 bg-blue-500/5 text-blue-500 font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse" />
NemoTron
</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{t('lastRefresh', { time: lastRefresh.toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) })}</span>
<button
onClick={fetchData}
className="ml-1 p-1 rounded hover:bg-muted transition-colors"
title={t('refresh')}
>
<RefreshCw className={cn('w-3 h-3', loading && 'animate-spin')} />
</button>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 px-6 py-2 border-b border-border flex-shrink-0">
{TABS.map(({ id, labelKey, Icon }) => (
<button
key={id}
onClick={() => setActiveTab(id)}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
activeTab === id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
)}
>
<Icon className="w-3.5 h-3.5" />
{t(labelKey)}
{id === 'approval' && pendingApprovals > 0 && (
<span className="ml-0.5 bg-orange-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full">
{pendingApprovals}
</span>
)}
</button>
))}
</div>
{/* Tab content */}
<div className="flex-1 min-h-0 overflow-auto">
{activeTab === 'preflight' && (
<NeuralPreFlight stats={stats} playbooks={approvedPlaybooks} />
)}
{activeTab === 'live' && (
<NeuralLiveCenter stats={stats} history={history} pendingCount={pendingApprovals} activeIncidents={activeIncidents} />
)}
{activeTab === 'stats' && (
<NeuralStats stats={stats} playbooks={approvedPlaybooks} history={history} pendingCount={pendingApprovals} disposition={dispositionSummary} />
)}
{activeTab === 'approval' && (
<NeuralApprovalPanel approvals={pendingApprovalList} onRefresh={fetchData} />
)}
</div>
</div>
)
}