Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
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>
182 lines
7.4 KiB
TypeScript
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>
|
|
)
|
|
}
|