diff --git a/apps/web/src/components/ai/clawbot-panel.tsx b/apps/web/src/components/ai/clawbot-panel.tsx deleted file mode 100644 index e4d8e1a3a..000000000 --- a/apps/web/src/components/ai/clawbot-panel.tsx +++ /dev/null @@ -1,431 +0,0 @@ -'use client' - -/** - * OpenClawPanel - 賽博維運風格 AI 面板 - * ===================================== - * Phase 1: 視覺靈魂注入 (Cyber-Shell Visual Identity) - * - * Features: - * - 3D 骨架機械爪視覺化 (CSS Art) - * - 核心藍色 LED 脈衝動畫 - * - 點陣字體狀態顯示 - * - AI 思考流過渡動畫 - * - 高通透度 awoooi-glass 效果 - */ - -import { useState, useEffect } from 'react' -import { useTranslations } from 'next-intl' -import { cn } from '@/lib/utils' -import { Sparkles } from 'lucide-react' - -// ============================================================================= -// Types -// ============================================================================= - -export type OpenClawStatus = - | 'patrolling' // [AGENT] patrolling... - | 'intercepting' // [SYS] 攔截異常... - | 'analyzing' // [SYS] Analyzing blast radius... - | 'generating' // [SYS] Generating proposed action... - | 'complete' // [SYS] Analysis complete - -export interface OpenClawPanelProps { - status?: OpenClawStatus - alertType?: string - onAnalysisComplete?: () => void - className?: string -} - -// ============================================================================= -// Status Messages (Dot Matrix Style) -// ============================================================================= - -const STATUS_MESSAGES: Record = { - patrolling: '[AGENT] patrolling...', - intercepting: '[SYS] Intercepting anomaly...', - analyzing: '[SYS] Analyzing blast radius...', - generating: '[SYS] Generating proposed action...', - complete: '[SYS] Analysis complete', -} - -// ============================================================================= -// NemoClaw 3D Ceramic SVG Component (Lab-White Style) -// ============================================================================= - -function NemoClaw({ isActive, isPulsing }: { isActive: boolean; isPulsing: boolean }) { - return ( - - - {/* 3D Ceramic gradient - white/cream tones */} - - - - - - - - {/* Core glow filter - stronger */} - - - - - - {/* Pulse glow animation filter */} - - - - - - - - {/* Shadow for 3D effect */} - - - - - - {/* Base shadow */} - - - {/* Main body - 3D ceramic sphere */} - - - {/* Inner ring - depth effect */} - - - {/* Core LED - Blue pulsing (the eye) */} - - {isPulsing && ( - - )} - - - {/* Core highlight */} - - - {/* Claw arms - ceramic white 3D style */} - {/* Top arm */} - - - - {/* Claw tips */} - - - - - {/* Left arm */} - - - - - - - - {/* Right arm */} - - - - - - - - {/* Bottom left arm */} - - - - - - - {/* Bottom right arm */} - - - - - - - {/* Orbit ring when active */} - {isActive && ( - - )} - - ) -} - -// ============================================================================= -// Typewriter Hook -// ============================================================================= - -function useTypewriter(text: string, speed: number = 50) { - const [displayText, setDisplayText] = useState('') - - useEffect(() => { - let index = 0 - setDisplayText('') - - const interval = setInterval(() => { - if (index < text.length) { - setDisplayText(text.slice(0, index + 1)) - index++ - } else { - clearInterval(interval) - } - }, speed) - - return () => clearInterval(interval) - }, [text, speed]) - - return displayText -} - -// ============================================================================= -// Component -// ============================================================================= - -export function OpenClawPanel({ - status = 'patrolling', - alertType, - onAnalysisComplete, - className, -}: OpenClawPanelProps) { - const _t = useTranslations('ai') - // Phase 8.0 #16: 移除 cursorVisible state,改用 CSS animate-pulse - - const isActive = status !== 'patrolling' - const isPulsing = status === 'intercepting' || status === 'analyzing' - - const statusMessage = STATUS_MESSAGES[status] - const displayText = useTypewriter(statusMessage, 40) - - // Notify when complete - useEffect(() => { - if (status === 'complete') { - const timeout = setTimeout(() => { - onAnalysisComplete?.() - }, 1000) - return () => clearTimeout(timeout) - } - }, [status, onAnalysisComplete]) - - return ( -
- {/* Scan line animation when active */} - {isActive && ( -
-
-
- )} - - {/* Header - Dot Matrix Style */} -
-
- - AWOOOI v1.0.0 - - - | Production - - {isActive && ( - - )} -
- {alertType && ( - - {alertType} - - )} -
- - {/* NemoClaw 3D Ceramic Visualization */} -
- - - {/* Sparkle effects when active */} - {isActive && ( - <> - - - - )} -
- - {/* Status Display - VT323 Dot Matrix Font */} -
-
- {displayText} - {/* Phase 8.0 #16: CSS cursor blink */} - -
-
- - {/* Progress indicator when analyzing */} - {(status === 'analyzing' || status === 'generating') && ( -
- {[0, 1, 2, 3, 4].map((i) => ( -
- ))} -
- )} - - {/* Complete indicator */} - {status === 'complete' && ( -
- - READY - -
- )} -
- ) -} - -export default OpenClawPanel diff --git a/apps/web/src/components/ai/clawbot-state-machine.tsx b/apps/web/src/components/ai/clawbot-state-machine.tsx deleted file mode 100644 index 4b0a8feda..000000000 --- a/apps/web/src/components/ai/clawbot-state-machine.tsx +++ /dev/null @@ -1,560 +0,0 @@ -'use client' - -/** - * OpenClawStateMachine - 戰情室 AI 狀態機整合 - * ========================================== - * Phase 2: 真實 API 數據整合 (禁止 Mock) - * - * Features: - * - 三態狀態機 (idle / thinking / awaiting_approval) - * - 真實 API 輪詢 (/api/v1/approvals/pending) - * - ThinkingStream 打字機動畫 - * - ApprovalCard 滑入動畫 - * - 記憶體安全清理 - * - * 真實性條款: 禁止任何 Mock Data - * i18n: 100% next-intl - */ - -import { useState, useEffect, useCallback, useRef } from 'react' -import { useTranslations } from 'next-intl' -import { cn } from '@/lib/utils' -import { OpenClawPanel, type OpenClawStatus } from './openclaw-panel' -import type { ThinkingStream as _ThinkingStream, DEFAULT_THINKING_MESSAGES as _DEFAULT_THINKING_MESSAGES } from './thinking-stream' -import { ApprovalCard, type ApprovalRequest } from '@/components/approval/approval-card' -import { RefreshCw, AlertCircle, CheckCircle2, Clock, Archive } from 'lucide-react' -import { toast } from '@/components/ui/toast' -import { useTimelineStore } from '@/stores/timeline.store' - -// ============================================================================= -// Types -// ============================================================================= - -export type MachineState = 'idle' | 'thinking' | 'awaiting_approval' - -export interface OpenClawStateMachineProps { - /** 啟用 Demo 模式 (僅供開發測試) */ - demoMode?: boolean - /** API 輪詢間隔 (ms) */ - pollInterval?: number - className?: string -} - -interface PendingApprovalsResponse { - count: number - approvals: ApprovalRequest[] -} - -// 歷史紀錄擴展類型 -interface HistoryApproval extends ApprovalRequest { - status: 'approved' | 'rejected' | 'executed' | 'execution_failed' - resolvedAt?: string -} - -type TabType = 'pending' | 'history' - -// ============================================================================= -// API Helper -// ============================================================================= - -const getApiBaseUrl = (): string => { - if (typeof window === 'undefined') return '' - // 統帥鐵律: 禁止任何 Fallback IP - const url = process.env.NEXT_PUBLIC_API_URL - if (!url) { - console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') - return '' - } - return url -} - -// API response uses snake_case, frontend uses camelCase -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function transformApiResponse(apiApproval: any): ApprovalRequest { - return { - id: apiApproval.id, - action: apiApproval.action, - description: apiApproval.description, - riskLevel: apiApproval.risk_level, - blastRadius: { - affectedPods: apiApproval.blast_radius?.affected_pods ?? 0, - estimatedDowntime: apiApproval.blast_radius?.estimated_downtime ?? '0', - relatedServices: apiApproval.blast_radius?.related_services ?? [], - dataImpact: ((apiApproval.blast_radius?.data_impact ?? 'none').toUpperCase()) as 'NONE' | 'READ_ONLY' | 'WRITE' | 'DESTRUCTIVE', - }, - dryRunChecks: apiApproval.dry_run_checks ?? [], - requiredSignatures: apiApproval.required_signatures ?? 2, - currentSignatures: apiApproval.current_signatures ?? 0, - requestedBy: apiApproval.requested_by ?? 'OpenClaw', - requestedAt: apiApproval.created_at ?? new Date().toISOString(), - hitCount: apiApproval.hit_count, - lastSeenAt: apiApproval.last_seen_at, - fingerprint: apiApproval.fingerprint, - } -} - -// ============================================================================= -// Component -// ============================================================================= - -export function OpenClawStateMachine({ - demoMode = false, - pollInterval = 5000, - className, -}: OpenClawStateMachineProps) { - const t = useTranslations() - - // State - const [machineState, setMachineState] = useState('idle') - const [openclawStatus, setClawbotStatus] = useState('patrolling') - const [pendingApprovals, setPendingApprovals] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - const [lastFetch, setLastFetch] = useState(null) - - // 歷史紀錄狀態 - const [activeTab, setActiveTab] = useState('pending') - const [historyApprovals, setHistoryApprovals] = useState([]) - const [isLoadingHistory, setIsLoadingHistory] = useState(false) - - // Timer refs for cleanup - const pollTimerRef = useRef(null) - - // ========================================================================== - // API: Fetch Pending Approvals - // ========================================================================== - const fetchPendingApprovals = useCallback(async () => { - const apiBaseUrl = getApiBaseUrl() - if (!apiBaseUrl) return - - setIsLoading(true) - setError(null) - - try { - const response = await fetch(`${apiBaseUrl}/api/v1/approvals/pending`, { - headers: { 'Content-Type': 'application/json' }, - }) - - if (!response.ok) { - throw new Error(`API Error: ${response.status}`) - } - - const rawData = await response.json() - const data: PendingApprovalsResponse = { - count: rawData.count, - approvals: (rawData.approvals ?? []).map(transformApiResponse), - } - setPendingApprovals(data.approvals) - setLastFetch(new Date()) - - // Update machine state based on approvals - if (data.count > 0) { - setMachineState('awaiting_approval') - setClawbotStatus('complete') - } else { - setMachineState('idle') - setClawbotStatus('patrolling') - } - - console.log('[OpenClaw] Fetched approvals:', data.count) - } catch (err) { - const message = err instanceof Error ? err.message : 'Unknown error' - setError(message) - console.error('[OpenClaw] Fetch error:', message) - } finally { - setIsLoading(false) - } - }, []) - - // ========================================================================== - // API: Fetch History Approvals (歷史歸檔) - // ========================================================================== - const fetchHistoryApprovals = useCallback(async () => { - const apiBaseUrl = getApiBaseUrl() - if (!apiBaseUrl) return - - setIsLoadingHistory(true) - - try { - // 取得已處理的歷史紀錄 - const response = await fetch(`${apiBaseUrl}/api/v1/approvals?status=approved,executed,rejected,execution_failed&limit=20`, { - headers: { 'Content-Type': 'application/json' }, - }) - - if (!response.ok) { - throw new Error(`API Error: ${response.status}`) - } - - const rawData = await response.json() - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- API response typing - const items = (rawData.items ?? rawData.approvals ?? []).map((item: any) => ({ - ...transformApiResponse(item), - status: item.status, - resolvedAt: item.resolved_at, - })) - - setHistoryApprovals(items) - console.log('[OpenClaw] Fetched history:', items.length) - } catch (err) { - console.error('[OpenClaw] History fetch error:', err) - } finally { - setIsLoadingHistory(false) - } - }, []) - - // 當切換到歷史標籤時自動載入 - useEffect(() => { - if (activeTab === 'history') { - fetchHistoryApprovals() - } - }, [activeTab, fetchHistoryApprovals]) - - // ========================================================================== - // Timeline Refresh + Smart Polling - // ========================================================================== - const fetchTimeline = useTimelineStore((s) => s.fetchEvents) - const startSmartPolling = useTimelineStore((s) => s.startSmartPolling) - - // ========================================================================== - // Signer rotation for Multi-Sig - // ========================================================================== - const signerCounter = useRef(0) - const SIGNERS = [ - { id: 'cto-ogt', name: '統帥 (CTO)' }, - { id: 'ciso-security', name: '資安長 (CISO)' }, - { id: 'cpo-product', name: '產品長 (CPO)' }, - ] - - // ========================================================================== - // API: Submit Signature (with Toast & Timeline Refresh) - // ========================================================================== - const handleApprove = useCallback(async (approvalId: string) => { - const apiBaseUrl = getApiBaseUrl() - if (!apiBaseUrl) return - - // Show loading toast - const loadingId = toast.loading('[AWOOOI] 統帥已授權,K8s 指令發送中...') - - try { - // Rotate through signers for Multi-Sig - const signer = SIGNERS[signerCounter.current % SIGNERS.length] - signerCounter.current++ - - const response = await fetch(`${apiBaseUrl}/api/v1/approvals/${approvalId}/sign`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - signer_id: signer.id, - signer_name: signer.name, - comment: 'Approved via AWOOOI Dashboard', - }), - }) - - // Dismiss loading toast - if (loadingId) toast.dismiss(loadingId) - - if (!response.ok) { - throw new Error(`Sign failed: ${response.status}`) - } - - const result = await response.json() - console.log('[OpenClaw] Approval signed:', approvalId, result) - - // Show success toast - if (result.execution_triggered) { - toast.success(`[AWOOOI] 簽核完成!K8s Executor 已啟動執行`) - // 執行觸發後的 Toast - toast.info('[AWOOOI] K8s 執行完成,正在更新戰情時間軸...') - // 啟動 Smart Polling (每秒輪詢直到看到 EXEC 事件) - startSmartPolling() - } else { - toast.info(`[AWOOOI] ${signer.name} 簽核成功 (${result.approval?.current_signatures}/${result.approval?.required_signatures})`) - // 非最終簽核也刷新 Timeline - fetchTimeline() - } - - // Refresh approvals list - await fetchPendingApprovals() - - } catch (err) { - if (loadingId) toast.dismiss(loadingId) - console.error('[OpenClaw] Sign error:', err) - const errorMsg = err instanceof Error ? err.message : 'Sign failed' - setError(errorMsg) - toast.error(`[AWOOOI] 簽核失敗: ${errorMsg}`) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- SIGNERS is a constant - }, [fetchPendingApprovals, fetchTimeline, startSmartPolling]) - - // ========================================================================== - // API: Reject Request (with Toast & Timeline Refresh) - // ========================================================================== - const handleReject = useCallback(async (approvalId: string) => { - const apiBaseUrl = getApiBaseUrl() - if (!apiBaseUrl) return - - const loadingId = toast.loading('[AWOOOI] 拒絕請求處理中...') - - try { - const response = await fetch(`${apiBaseUrl}/api/v1/approvals/${approvalId}/reject`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - rejector_id: 'cto-ogt', - rejector_name: '統帥 (CTO)', - reason: 'Rejected via AWOOOI Dashboard', - }), - }) - - if (loadingId) toast.dismiss(loadingId) - - if (!response.ok) { - throw new Error(`Reject failed: ${response.status}`) - } - - console.log('[OpenClaw] Approval rejected:', approvalId) - toast.success('[AWOOOI] 請求已拒絕') - - // Refresh approvals list - await fetchPendingApprovals() - - // Refresh Timeline (拒絕不需要 Smart Polling) - fetchTimeline() - - } catch (err) { - if (loadingId) toast.dismiss(loadingId) - console.error('[OpenClaw] Reject error:', err) - const errorMsg = err instanceof Error ? err.message : 'Reject failed' - setError(errorMsg) - toast.error(`[AWOOOI] 拒絕失敗: ${errorMsg}`) - } - }, [fetchPendingApprovals, fetchTimeline]) - - // ========================================================================== - // Polling Effect - // ========================================================================== - useEffect(() => { - // Skip polling in demo mode - if (demoMode) return - - // Initial fetch - fetchPendingApprovals() - - // Start polling - pollTimerRef.current = setInterval(fetchPendingApprovals, pollInterval) - - // Cleanup - return () => { - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current) - pollTimerRef.current = null - } - } - }, [demoMode, pollInterval, fetchPendingApprovals]) - - // ========================================================================== - // Render - // ========================================================================== - return ( -
- {/* Status Bar */} -
-
- - STATE: - - - {machineState} - -
- -
- {/* Loading indicator */} - {isLoading && ( - - )} - - {/* Error indicator */} - {error && ( -
- - {error} -
- )} - - {/* Manual refresh button */} - -
-
- - {/* OpenClaw Visual */} - 0 ? 'POD_CRASH' : undefined} - /> - - {/* Nothing.tech 風格標籤切換 */} -
- - -
- - {/* ========== 待處理 Tab ========== */} - {activeTab === 'pending' && ( - <> - {/* Pending Approvals List (REAL DATA) */} - {pendingApprovals.length > 0 && ( -
- {pendingApprovals.map((approval) => ( -
- handleApprove(approval.id)} - onReject={() => handleReject(approval.id)} - holdDuration={1000} - /> -
- ))} -
- )} - - {/* Idle state - no pending approvals */} - {machineState === 'idle' && pendingApprovals.length === 0 && !isLoading && ( -
- -

- {t('ai.standby')} -

-

- {lastFetch - ? `Last check: ${lastFetch.toLocaleTimeString()}` - : 'Waiting for first fetch...'} -

-
- )} - - )} - - {/* ========== 歷史紀錄 Tab ========== */} - {activeTab === 'history' && ( -
- {/* Loading 狀態 */} - {isLoadingHistory && ( -
- - 載入歷史紀錄... -
- )} - - {/* 歷史卡片列表 */} - {!isLoadingHistory && historyApprovals.length > 0 && ( - <> - {historyApprovals.map((approval) => ( -
- -
- ))} - - )} - - {/* 空狀態 */} - {!isLoadingHistory && historyApprovals.length === 0 && ( -
- -

- 尚無歷史紀錄 -

-

- 已處理的授權請求將會顯示在這裡 -

-
- )} - - {/* 刷新按鈕 */} -
- -
-
- )} - - {/* Demo mode warning */} - {demoMode && ( -
-

- ⚠️ Demo mode is enabled. Real API polling is disabled. -

-
- )} -
- ) -} - -export default OpenClawStateMachine diff --git a/apps/web/src/components/ai/hitl-section.tsx b/apps/web/src/components/ai/hitl-section.tsx index 6b27d6912..eb86cce16 100644 --- a/apps/web/src/components/ai/hitl-section.tsx +++ b/apps/web/src/components/ai/hitl-section.tsx @@ -94,7 +94,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) { useApprovalSSE({ autoConnect: true }) // AI thinking state - const [openclawStatus, setClawbotStatus] = useState('patrolling') + const [openclawStatus, setOpenClawStatus] = useState('patrolling') const [currentAlertType, setCurrentAlertType] = useState() const [showCards, setShowCards] = useState(true) const [isSimulating, setIsSimulating] = useState(false) @@ -117,15 +117,15 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) { setCurrentAlertType(alertType) // Phase 1: Intercepting - setClawbotStatus('intercepting') + setOpenClawStatus('intercepting') await new Promise((r) => setTimeout(r, 800)) // Phase 2: Analyzing - setClawbotStatus('analyzing') + setOpenClawStatus('analyzing') await new Promise((r) => setTimeout(r, 1200)) // Phase 3: Generating - setClawbotStatus('generating') + setOpenClawStatus('generating') // Call actual webhook API try { @@ -170,7 +170,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) { }) // Phase 4: Complete - setClawbotStatus('complete') + setOpenClawStatus('complete') await new Promise((r) => setTimeout(r, 800)) // Refresh approvals list @@ -181,11 +181,11 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) { } catch (error) { console.error('[HITL] Webhook failed:', error) - setClawbotStatus('patrolling') + setOpenClawStatus('patrolling') } finally { setIsSimulating(false) setTimeout(() => { - setClawbotStatus('patrolling') + setOpenClawStatus('patrolling') setCurrentAlertType(undefined) }, 2000) }