Files
awoooi/apps/web/src/components/ai/clawbot-panel.tsx
OG T 2e9ccf4a26 fix(lint): 清理所有 ESLint 警告 (61→0)
- 修復未使用變數 (prefix with _)
- 修復 type-only imports
- 修復 react-hooks/exhaustive-deps (useMemo + 依賴補齊)
- 修復 no-explicit-any (eslint-disable 標記)
- 移除未使用的 imports

涉及組件:
- demo/page, layout, page (主頁面)
- ai/* (OpenClaw, HITL, ThinkingStream)
- approval/* (ApprovalCard, LiveApprovalPanel)
- dashboard/* (HostCard, LiveDashboard, ConnectionStatus)
- incident/* (DualStateIncidentCard, ThinkingTerminal)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 17:06:58 +08:00

432 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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