- 修復未使用變數 (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>
432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
'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
|