fix(web): Phase 22.4 移除 ClawBot 舊檔案與命名殘留

- 刪除 clawbot-panel.tsx (已被 openclaw-panel.tsx 取代)
- 刪除 clawbot-state-machine.tsx (已被 openclaw-state-machine.tsx 取代)
- 修正 hitl-section.tsx: setClawbotStatus → setOpenClawStatus
- 符合 feedback_openclaw_naming.md 命名鐵律

Phase 22.4: 命名清理 

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-31 12:12:30 +08:00
parent f769d80c2d
commit eaca7199c2
3 changed files with 7 additions and 998 deletions

View File

@@ -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<OpenClawStatus, string> = {
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 (
<svg
viewBox="0 0 140 140"
className="w-full h-full drop-shadow-lg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* 3D Ceramic gradient - white/cream tones */}
<linearGradient id="ceramic3d" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="40%" stopColor="#F8F8F8" />
<stop offset="70%" stopColor="#E8E8E8" />
<stop offset="100%" stopColor="#D8D8D8" />
</linearGradient>
{/* Core glow filter - stronger */}
<filter id="coreGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="6" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
{/* Pulse glow animation filter */}
<filter id="pulseGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="8" result="blur">
<animate attributeName="stdDeviation" values="6;10;6" dur="1.5s" repeatCount="indefinite" />
</feGaussianBlur>
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
{/* Shadow for 3D effect */}
<filter id="shadow3d" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="4" stdDeviation="3" floodOpacity="0.15" />
</filter>
</defs>
{/* Base shadow */}
<ellipse cx="70" cy="125" rx="35" ry="8" fill="rgba(0,0,0,0.08)" />
{/* Main body - 3D ceramic sphere */}
<circle
cx="70"
cy="70"
r="32"
fill="url(#ceramic3d)"
filter="url(#shadow3d)"
stroke="#E0E0E0"
strokeWidth="1"
/>
{/* Inner ring - depth effect */}
<circle
cx="70"
cy="70"
r="26"
fill="none"
stroke="#D0D0D0"
strokeWidth="1"
opacity="0.5"
/>
{/* Core LED - Blue pulsing (the eye) */}
<circle
cx="70"
cy="70"
r="16"
fill={isActive ? '#4A90D9' : '#B0B0B0'}
filter={isPulsing ? 'url(#pulseGlow)' : 'url(#coreGlow)'}
className="transition-all duration-500"
>
{isPulsing && (
<animate
attributeName="r"
values="14;17;14"
dur="1s"
repeatCount="indefinite"
/>
)}
</circle>
{/* Core highlight */}
<circle
cx="70"
cy="70"
r="8"
fill="white"
opacity={isActive ? 0.9 : 0.4}
className="transition-opacity duration-300"
/>
{/* Claw arms - ceramic white 3D style */}
{/* Top arm */}
<g filter="url(#shadow3d)">
<path
d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6"
stroke="url(#ceramic3d)"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6"
stroke={isActive ? '#4A90D9' : '#C0C0C0'}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity="0.6"
/>
{/* Claw tips */}
<circle cx="58" cy="6" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="82" cy="6" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Left arm */}
<g filter="url(#shadow3d)">
<path
d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82"
stroke="url(#ceramic3d)"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82"
stroke={isActive ? '#4A90D9' : '#C0C0C0'}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity="0.6"
/>
<circle cx="6" cy="58" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="6" cy="82" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Right arm */}
<g filter="url(#shadow3d)">
<path
d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82"
stroke="url(#ceramic3d)"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82"
stroke={isActive ? '#4A90D9' : '#C0C0C0'}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity="0.6"
/>
<circle cx="134" cy="58" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="134" cy="82" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Bottom left arm */}
<g filter="url(#shadow3d)">
<path
d="M 48 92 L 28 112 L 16 116"
stroke="url(#ceramic3d)"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M 48 92 L 28 112 L 16 116"
stroke={isActive ? '#4A90D9' : '#C0C0C0'}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity="0.6"
/>
<circle cx="16" cy="116" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Bottom right arm */}
<g filter="url(#shadow3d)">
<path
d="M 92 92 L 112 112 L 124 116"
stroke="url(#ceramic3d)"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M 92 92 L 112 112 L 124 116"
stroke={isActive ? '#4A90D9' : '#C0C0C0'}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity="0.6"
/>
<circle cx="124" cy="116" r="4" fill="url(#ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Orbit ring when active */}
{isActive && (
<circle
cx="70"
cy="70"
r="42"
fill="none"
stroke="#4A90D9"
strokeWidth="1"
strokeDasharray="6 6"
opacity="0.4"
className="animate-spin"
style={{ animationDuration: '8s', transformOrigin: '70px 70px' }}
/>
)}
</svg>
)
}
// =============================================================================
// 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 (
<div
className={cn(
'relative overflow-hidden rounded-xl',
// awoooi-glass effect
'bg-white/70 backdrop-blur-[20px]',
// 骨架細框線
'border border-nothing-gray-200/50',
// 輕柔陰影
'shadow-[0_4px_24px_rgba(0,0,0,0.06)]',
'p-4',
className
)}
>
{/* Scan line animation when active */}
{isActive && (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute h-[1px] w-full bg-gradient-to-r from-transparent via-claw-blue/50 to-transparent animate-scan" />
</div>
)}
{/* Header - Dot Matrix Style */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="font-dot-matrix text-sm text-nothing-gray-700 tracking-wider">
AWOOOI v1.0.0
</span>
<span className="font-dot-matrix text-xs text-nothing-gray-400">
| Production
</span>
{isActive && (
<span className="w-2 h-2 rounded-full bg-claw-blue animate-ping" />
)}
</div>
{alertType && (
<span className="px-2 py-0.5 rounded text-[10px] font-dot-matrix bg-status-critical/10 text-status-critical border border-status-critical/20">
{alertType}
</span>
)}
</div>
{/* NemoClaw 3D Ceramic Visualization */}
<div className="relative w-32 h-32 mx-auto mb-4">
<NemoClaw isActive={isActive} isPulsing={isPulsing} />
{/* Sparkle effects when active */}
{isActive && (
<>
<Sparkles className="absolute -top-1 -right-1 w-4 h-4 text-claw-blue/60 animate-pulse" />
<Sparkles className="absolute -bottom-1 -left-1 w-3 h-3 text-claw-blue/40 animate-pulse" style={{ animationDelay: '0.3s' }} />
</>
)}
</div>
{/* Status Display - VT323 Dot Matrix Font */}
<div className="text-center bg-nothing-gray-50/50 rounded-lg py-2 px-3">
<div className={cn(
'font-dot-matrix text-base tracking-wide',
isActive ? 'text-claw-blue' : 'text-nothing-gray-600'
)}>
<span>{displayText}</span>
{/* Phase 8.0 #16: CSS cursor blink */}
<span
className={cn(
'inline-block w-2 h-4 ml-1 -mb-0.5 animate-pulse',
isActive ? 'bg-claw-blue' : 'bg-nothing-gray-400'
)}
/>
</div>
</div>
{/* Progress indicator when analyzing */}
{(status === 'analyzing' || status === 'generating') && (
<div className="mt-3 flex justify-center gap-1">
{[0, 1, 2, 3, 4].map((i) => (
<div
key={i}
className="w-1.5 h-1.5 rounded-full bg-claw-blue animate-pulse"
style={{ animationDelay: `${i * 150}ms` }}
/>
))}
</div>
)}
{/* Complete indicator */}
{status === 'complete' && (
<div className="mt-3 flex justify-center">
<span className="px-2 py-0.5 rounded text-[9px] font-mono bg-status-healthy/10 text-status-healthy border border-status-healthy/20">
READY
</span>
</div>
)}
</div>
)
}
export default OpenClawPanel

View File

@@ -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<MachineState>('idle')
const [openclawStatus, setClawbotStatus] = useState<OpenClawStatus>('patrolling')
const [pendingApprovals, setPendingApprovals] = useState<ApprovalRequest[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [lastFetch, setLastFetch] = useState<Date | null>(null)
// 歷史紀錄狀態
const [activeTab, setActiveTab] = useState<TabType>('pending')
const [historyApprovals, setHistoryApprovals] = useState<HistoryApproval[]>([])
const [isLoadingHistory, setIsLoadingHistory] = useState(false)
// Timer refs for cleanup
const pollTimerRef = useRef<NodeJS.Timeout | null>(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 (
<div className={cn('space-y-4', className)}>
{/* Status Bar */}
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-nothing-gray-500">
STATE:
</span>
<span
className={cn(
'px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase',
machineState === 'idle' && 'bg-nothing-gray-100 text-nothing-gray-600',
machineState === 'thinking' && 'bg-status-thinking/10 text-status-thinking',
machineState === 'awaiting_approval' && 'bg-status-warning/10 text-status-warning'
)}
>
{machineState}
</span>
</div>
<div className="flex items-center gap-2">
{/* Loading indicator */}
{isLoading && (
<RefreshCw className="w-3 h-3 text-nothing-gray-400 animate-spin" />
)}
{/* Error indicator */}
{error && (
<div className="flex items-center gap-1 text-status-critical">
<AlertCircle className="w-3 h-3" />
<span className="text-[10px] font-mono">{error}</span>
</div>
)}
{/* Manual refresh button */}
<button
onClick={fetchPendingApprovals}
disabled={isLoading}
className={cn(
'flex items-center gap-1 px-2 py-1 rounded text-[10px] font-mono',
'bg-nothing-gray-100 text-nothing-gray-600',
'hover:bg-nothing-gray-200 transition-colors',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<RefreshCw className="w-3 h-3" />
REFRESH
</button>
</div>
</div>
{/* OpenClaw Visual */}
<OpenClawPanel
status={openclawStatus}
alertType={pendingApprovals.length > 0 ? 'POD_CRASH' : undefined}
/>
{/* Nothing.tech 風格標籤切換 */}
<div className="flex items-center gap-1 p-1 bg-nothing-gray-50 rounded-lg border border-nothing-gray-200">
<button
onClick={() => setActiveTab('pending')}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-md font-mono text-xs font-semibold',
'transition-all duration-200',
activeTab === 'pending'
? 'bg-white text-nothing-gray-900 shadow-sm border border-nothing-gray-200'
: 'text-nothing-gray-500 hover:text-nothing-gray-700 hover:bg-white/50'
)}
>
<Clock className="w-3.5 h-3.5" />
{pendingApprovals.length > 0 && (
<span className="ml-1 px-1.5 py-0.5 bg-status-warning/10 text-status-warning rounded text-[10px]">
{pendingApprovals.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab('history')}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-md font-mono text-xs font-semibold',
'transition-all duration-200',
activeTab === 'history'
? 'bg-white text-nothing-gray-900 shadow-sm border border-nothing-gray-200'
: 'text-nothing-gray-500 hover:text-nothing-gray-700 hover:bg-white/50'
)}
>
<Archive className="w-3.5 h-3.5" />
</button>
</div>
{/* ========== 待處理 Tab ========== */}
{activeTab === 'pending' && (
<>
{/* Pending Approvals List (REAL DATA) */}
{pendingApprovals.length > 0 && (
<div className="space-y-3">
{pendingApprovals.map((approval) => (
<div key={approval.id} className="animate-slide-in-up">
<ApprovalCard
request={approval}
onApprove={() => handleApprove(approval.id)}
onReject={() => handleReject(approval.id)}
holdDuration={1000}
/>
</div>
))}
</div>
)}
{/* Idle state - no pending approvals */}
{machineState === 'idle' && pendingApprovals.length === 0 && !isLoading && (
<div className="text-center py-6 border border-dashed border-nothing-gray-200 rounded-lg">
<CheckCircle2 className="w-8 h-8 mx-auto mb-2 text-status-healthy/50" />
<p className="font-mono text-xs text-nothing-gray-400">
{t('ai.standby')}
</p>
<p className="font-mono text-[10px] text-nothing-gray-300 mt-1">
{lastFetch
? `Last check: ${lastFetch.toLocaleTimeString()}`
: 'Waiting for first fetch...'}
</p>
</div>
)}
</>
)}
{/* ========== 歷史紀錄 Tab ========== */}
{activeTab === 'history' && (
<div className="space-y-3">
{/* Loading 狀態 */}
{isLoadingHistory && (
<div className="flex items-center justify-center py-8">
<RefreshCw className="w-5 h-5 text-nothing-gray-400 animate-spin" />
<span className="ml-2 font-mono text-xs text-nothing-gray-400">...</span>
</div>
)}
{/* 歷史卡片列表 */}
{!isLoadingHistory && historyApprovals.length > 0 && (
<>
{historyApprovals.map((approval) => (
<div key={approval.id} className="opacity-80">
<ApprovalCard
request={approval}
readOnly
finalStatus={
approval.status === 'executed' ? 'executed' :
approval.status === 'execution_failed' ? 'failed' :
approval.status === 'rejected' ? 'rejected' : 'approved'
}
/>
</div>
))}
</>
)}
{/* 空狀態 */}
{!isLoadingHistory && historyApprovals.length === 0 && (
<div className="text-center py-8 border border-dashed border-nothing-gray-200 rounded-lg">
<Archive className="w-8 h-8 mx-auto mb-2 text-nothing-gray-300" />
<p className="font-mono text-xs text-nothing-gray-400">
</p>
<p className="font-mono text-[10px] text-nothing-gray-300 mt-1">
</p>
</div>
)}
{/* 刷新按鈕 */}
<div className="flex justify-center pt-2">
<button
onClick={fetchHistoryApprovals}
disabled={isLoadingHistory}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-xs',
'bg-nothing-gray-50 text-nothing-gray-600 border border-nothing-gray-200',
'hover:bg-nothing-gray-100 transition-colors',
'disabled:opacity-50'
)}
>
<RefreshCw className={cn('w-3 h-3', isLoadingHistory && 'animate-spin')} />
</button>
</div>
</div>
)}
{/* Demo mode warning */}
{demoMode && (
<div className="bg-status-warning/10 border border-status-warning/20 rounded-lg p-3">
<p className="font-mono text-xs text-status-warning">
Demo mode is enabled. Real API polling is disabled.
</p>
</div>
)}
</div>
)
}
export default OpenClawStateMachine

View File

@@ -94,7 +94,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
useApprovalSSE({ autoConnect: true })
// AI thinking state
const [openclawStatus, setClawbotStatus] = useState<OpenClawStatus>('patrolling')
const [openclawStatus, setOpenClawStatus] = useState<OpenClawStatus>('patrolling')
const [currentAlertType, setCurrentAlertType] = useState<string | undefined>()
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)
}