From 9268d22dee39d53c708d9274feb21b36f94f6001 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 1 Apr 2026 20:07:58 +0800 Subject: [PATCH] feat(page): redesign AI Center layout - 3-col Feed+RightPanel, Metrics Strip Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/[locale]/page.tsx | 466 +++++++++-------------------- 1 file changed, 149 insertions(+), 317 deletions(-) diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index ba1550dcc..4d36c4fd8 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -1,367 +1,199 @@ 'use client' /** - * AWOOOI 全局戰情室 (Global War Room) + * AWOOOI AI Center 主頁 (Task 10 重構) * ==================================== - * Phase 2: 完整 AppLayout 整合 + * 3欄佈局: Metrics Strip + Feed + RightPanel * - * 佈局結構: - * - 左側側邊欄: 導航選單 - * - 主內容: 70/30 Grid (系統狀態 + AI 面板) - * - * 視覺規範: - * - awoooi-glass 白玻璃毛玻璃 - * - DataPincer 數據鉗容器 - * - 點陣紋理背景 - * - * i18n: 100% next-intl,零硬編碼 + * 統帥鐵律: 使用真實數據 Hook,禁止假數據! */ import { useTranslations } from 'next-intl' -import { AppLayout } from '@/components/layout' -import { LiveDashboard } from '@/components/dashboard/live-dashboard' -import { DataPincerPanel } from '@/components/cyber' -import { OpenClawStateMachine } from '@/components/ai/openclaw-state-machine' -import { GlobalPulseChart } from '@/components/charts/global-pulse-chart' import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics' import { useIncidents } from '@/hooks/useIncidents' -import type { DecisionInfo } from '@/lib/api-client' -import { - ThinkingTerminal, - DualStateIncidentCard, -} from '@/components/incident' -import { AlertTriangle } from 'lucide-react' -import type { IncidentResponse } from '@/lib/api-client' - -// ============================================================================= -// Types for AI Decision Chain Display -// ============================================================================= - -interface ReasoningStep { - step: string - reasoning: string - timestamp?: string -} - -interface DecisionChainDisplay { - analysis_type: 'blast_radius' | 'root_cause' | 'action_suggestion' - target_service: string - reasoning_steps: ReasoningStep[] - conclusion: string - confidence: number -} - -// ============================================================================= -// Utility: Convert API proposal_data to DecisionChain format -// ============================================================================= - -function convertToDecisionChain( - incident: IncidentResponse | null | undefined -): DecisionChainDisplay | null { - if (!incident?.decision?.proposal_data) { - return null - } - - const data = incident.decision.proposal_data - const target = incident.affected_services?.[0] || 'unknown-service' - const source = data.source || 'unknown' - const _now = new Date().toISOString() - - // 建構推理步驟 (從 API 資料) - const steps: ReasoningStep[] = [ - { - step: 'SIGNAL_RECEIVED', - reasoning: `收到 ${incident.signal_count || 1} 筆告警,影響服務: ${target}`, - timestamp: incident.created_at, - }, - { - step: 'SEVERITY_EVALUATION', - reasoning: `告警等級: ${incident.severity} | 狀態: ${incident.status}`, - }, - { - step: 'AI_ENGINE', - reasoning: `決策引擎: ${source === 'expert_system' ? 'Expert System (規則引擎)' : 'OpenClaw LLM (智能分析)'}`, - }, - ] - - // 加入 AI 推理 (如果有) - if (data.reasoning) { - steps.push({ - step: 'ROOT_CAUSE_ANALYSIS', - reasoning: data.reasoning, - }) - } - - // 加入描述 (如果有) - if (data.description) { - steps.push({ - step: 'ANALYSIS_RESULT', - reasoning: data.description, - }) - } - - // 加入建議動作 - if (data.action || data.kubectl_command) { - steps.push({ - step: 'ACTION_RECOMMENDATION', - reasoning: `建議動作: ${data.action || data.kubectl_command}\n風險等級: ${data.risk_level || 'medium'}`, - }) - } - - return { - analysis_type: 'action_suggestion', - target_service: target, - reasoning_steps: steps, - conclusion: data.action || '等待 AI 分析完成...', - confidence: data.confidence ?? 0.75, - } -} - -// ============================================================================= -// Utility: Map IncidentResponse to DualStateIncidentCard props -// ============================================================================= - -function mapToDualState(incident: IncidentResponse | null | undefined): { - id: string - serviceName: string - status: 'normal' | 'alert' - tier?: 1 | 2 | 3 - message: string - timestamp: string - decision?: DecisionInfo | null // Phase 6.5: 決策令牌 -} { - // 防禦性檢查: 若 incident 無效則返回預設值 - if (!incident) { - return { - id: 'unknown', - serviceName: 'Unknown Service', - status: 'normal', - tier: undefined, - message: '資料載入中...', - timestamp: '-', - decision: null, - } - } - - // P0/P1/P2 視為異常 (alert),只有 P3 視為正常 (normal) - const severity = incident.severity || 'P3' - const isAlert = severity === 'P0' || severity === 'P1' || severity === 'P2' - - // Tier 判定: P0 = Tier 3 (親核), P1 = Tier 2 (授權), P2+ = Tier 1 (自主) - let tier: 1 | 2 | 3 | undefined = undefined - if (isAlert) { - if (severity === 'P0') { - tier = 3 - } else if (severity === 'P1') { - tier = 2 - } else { - tier = 1 - } - } - - // 安全提取服務名稱 - const services = incident.affected_services || [] - const serviceName = services.length > 0 - ? services[0] - : incident.incident_id?.split('-')[2] || 'Unknown Service' - - // 格式化時間 (安全處理) - let timestamp = '-' - try { - const date = new Date(incident.created_at || Date.now()) - timestamp = date.toLocaleString('zh-TW', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) - } catch { - timestamp = 'Invalid Date' - } - - // 安全生成訊息 - const signalCount = incident.signal_count ?? 0 - const status = incident.status || 'unknown' - const message = `[${severity}] ${signalCount} 筆告警 | ${status}` - - return { - id: incident.incident_id || 'unknown', - serviceName, - status: isAlert ? 'alert' : 'normal', - tier, - message, - timestamp, - decision: incident.decision, // Phase 6.5: 傳遞決策令牌 - } -} +import { IncidentCard } from '@/components/incident' +import { OpenClawPanel } from '@/components/ai/openclaw-panel' +import { HostGrid } from '@/components/infra/host-grid' // ============================================================================= // Main Page // ============================================================================= export default function Home({ params }: { params: { locale: string } }) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const t = useTranslations() + // eslint-disable-next-line @typescript-eslint/no-unused-vars const locale = params.locale // 統帥鐵律: 使用真實數據 Hook,禁止假數據! - const { metrics: pulseMetrics, isLoading: isPulseLoading, error: pulseError } = useGlobalPulseMetrics({ - pollInterval: 30000, // 30 秒輪詢 + const { metrics: pulseMetrics } = useGlobalPulseMetrics({ + pollInterval: 30000, enablePolling: true, }) // Phase 7: 真實 Incident 數據 const { incidents, - pendingApprovals: _pendingApprovals, + pendingApprovals, isLoading: isIncidentsLoading, error: incidentsError, } = useIncidents({ - pollInterval: 15000, // 15 秒輪詢 + pollInterval: 15000, enablePolling: true, }) return ( - - {/* Page Title */} -
-

- {t('dashboard.title')} -

-

- {t('dashboard.subtitle')} -

+
+ {/* ── Metrics Strip (50px) ────────────────────────────────────── */} +
+ {[ + { label: '活躍事件', value: incidents?.length ?? '--', sub: incidents?.filter((i) => i.severity === 'P0').length ? `+${incidents.filter((i) => i.severity === 'P0').length} P0` : '穩定' }, + { label: '服務健康', value: `${pulseMetrics?.length ?? '--'}/${pulseMetrics?.length ?? '--'}`, sub: '正常' }, + { label: '待簽核', value: pendingApprovals ?? '--', sub: '等待授權' }, + { label: '今日事件', value: incidents?.length ?? '--', sub: '' }, + { label: '自動處置率', value: '--', sub: '' }, + { label: 'MTTR 均值', value: '--', sub: '' }, + ].map((m, i, arr) => ( +
+ + {m.label} + + + {String(m.value)} + + {m.sub && {m.sub}} +
+ ))}
- {/* Main Grid: 左側 70% | 右側 30% */} -
+ {/* ── 主體 3 欄 ────────────────────────────────────────────────── */} +
- {/* =========================================================== */} - {/* Left Column (70%): 系統脈搏 + 系統狀態 + 事件流 */} - {/* =========================================================== */} -
- - {/* Global Pulse Chart - 系統心跳 (真實血脈) */} - - {isPulseLoading ? ( -
-
- - {t('dashboard.loadingMetrics')} - -
- ) : pulseError ? ( -
- {t('dashboard.metricsError')} - {pulseError} -
- ) : ( - + {/* ── Feed:活躍事件(flex:1)──────────────────────────────── */} +
+ {/* Feed 標題列 */} +
+ + 活躍事件 + + {(incidents?.length ?? 0) > 0 && ( + + {incidents?.length} + )} - +
- {/* System Status Section */} - - - - - {/* Active Incidents Section (Phase 7: 真實血脈 + Phase 6.5b 雙態卡片) */} - 0 ? 'critical' : 'healthy'} - > + {/* Feed 內容 */} +
{isIncidentsLoading ? ( -
-
- - {t('common.loading')} - +
+ 載入中...
) : incidentsError ? ( -
- - {incidentsError} -
- ) : (incidents?.length || 0) === 0 ? ( - /* Nothing.tech 風格平靜態: 系統穩定 */ -
-
-

- {t('incident.systemStable', { defaultValue: '系統穩定' })} -

-

- 0 {t('incident.activeAlerts', { defaultValue: '活躍異常' })} -

+
{incidentsError}
+ ) : (incidents?.length ?? 0) === 0 ? ( +
+
+ 系統穩定 · 0 活躍事件
) : ( -
- {/* Phase 6.5b: 雙態戰情室卡片 (脈衝雷達 + Tier 決策層) */} -
- {(incidents || []).map((incident, index) => { - const dualProps = mapToDualState(incident) - return ( - - ) - })} -
-
+ incidents?.map((incident) => ( + + )) )} - - - {/* OpenClaw Thinking Terminal (Phase 7: 決策鏈視覺化 - 真實 AI 資料) */} - 0 ? "thinking" : "healthy"} - > - 0 - ? convertToDecisionChain(incidents?.[0]) - : null - } - incidentId={(incidents?.length || 0) > 0 ? incidents?.[0]?.incident_id : undefined} - autoPlay={(incidents?.length || 0) > 0} - maxHeight="300px" - /> - - +
- {/* =========================================================== */} - {/* Right Column (30%): AI 狀態機 (OpenClaw + ThinkingStream + ApprovalCard) */} - {/* =========================================================== */} -
- - - -
+ {/* ── Right Panel:AI + Infra(flex:1)─────────────────────── */} +
+
+ {/* 7.1 NemoClaw + Reasoning Stream */} +
+
+ OpenClaw 認知引擎 +
+ 0 ? 'analyzing' : 'patrolling' + } + /> +
+ + {/* 7.3 基礎架構 2×2 Grid */} +
+
+ 基礎架構 +
+ +
+ +
+
- - {/* =========================================================== */} - {/* Footer */} - {/* =========================================================== */} - - +
) }