45 個 component + 6 個 page 統一從舊 font-mono 遷移到 font-body (DM Mono),確保設計系統一致性。 font-body = DM Mono (等寬),視覺效果相同但走新設計 token。 保留: font-heading (Syne)、font-dot-matrix (VT323/DSEG7) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
5.8 KiB
TypeScript
237 lines
5.8 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* DataPincer - AWOOOI 靈魂視覺元件
|
||
* =================================
|
||
* 數據鉗狀態燈:AI 代理的核心狀態指示器
|
||
* 設計風格:Nothing.tech (極簡 + 呼吸燈 + 毛玻璃)
|
||
*
|
||
* i18n: 100% 使用 useTranslations,禁止任何寫死字串
|
||
* 符合 AWOOOI 專案開發憲法 v2.0
|
||
*
|
||
* 狀態映射:
|
||
* - idle: 灰色靜止
|
||
* - thinking: 琥珀色呼吸
|
||
* - executing: 綠色脈動
|
||
* - waiting_approval: 紅色呼吸 (需要人類介入)
|
||
* - error: 紅色閃爍
|
||
*/
|
||
|
||
import { useTranslations } from 'next-intl'
|
||
import { cn } from '@/lib/utils'
|
||
import {
|
||
useAgentStore,
|
||
selectAgentStatus,
|
||
selectHasError,
|
||
type AgentStatus,
|
||
} from '@/stores/agent.store'
|
||
|
||
// ==================== Types ====================
|
||
|
||
interface DataPincerProps {
|
||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||
showLabel?: boolean
|
||
showPulse?: boolean
|
||
className?: string
|
||
}
|
||
|
||
// ==================== Config ====================
|
||
|
||
// 狀態樣式配置 (不含 label,label 由 i18n 提供)
|
||
const statusStyleConfig: Record<
|
||
AgentStatus,
|
||
{
|
||
color: string
|
||
glowColor: string
|
||
labelKey: string // i18n key
|
||
animate: boolean
|
||
pulseClass: string
|
||
}
|
||
> = {
|
||
idle: {
|
||
color: 'bg-nothing-gray-600',
|
||
glowColor: 'shadow-none',
|
||
labelKey: 'standby',
|
||
animate: false,
|
||
pulseClass: '',
|
||
},
|
||
thinking: {
|
||
color: 'bg-status-thinking',
|
||
glowColor: 'shadow-[0_0_30px_rgba(139,92,246,0.5)]',
|
||
labelKey: 'analyzing',
|
||
animate: true,
|
||
pulseClass: 'animate-breathe',
|
||
},
|
||
executing: {
|
||
color: 'bg-status-healthy',
|
||
glowColor: 'shadow-[0_0_30px_rgba(34,197,94,0.5)]',
|
||
labelKey: 'executing',
|
||
animate: true,
|
||
pulseClass: 'animate-pulse-slow',
|
||
},
|
||
waiting_approval: {
|
||
color: 'bg-status-critical',
|
||
glowColor: 'shadow-[0_0_30px_rgba(215,25,33,0.6)]',
|
||
labelKey: 'waitingApproval',
|
||
animate: true,
|
||
pulseClass: 'animate-breathe',
|
||
},
|
||
error: {
|
||
color: 'bg-status-critical',
|
||
glowColor: 'shadow-[0_0_40px_rgba(215,25,33,0.8)]',
|
||
labelKey: 'error',
|
||
animate: true,
|
||
pulseClass: 'animate-pulse',
|
||
},
|
||
}
|
||
|
||
const sizeConfig = {
|
||
sm: {
|
||
orb: 'w-8 h-8',
|
||
ring: 'w-12 h-12',
|
||
outerRing: 'w-16 h-16',
|
||
label: 'text-xs',
|
||
},
|
||
md: {
|
||
orb: 'w-16 h-16',
|
||
ring: 'w-24 h-24',
|
||
outerRing: 'w-32 h-32',
|
||
label: 'text-sm',
|
||
},
|
||
lg: {
|
||
orb: 'w-24 h-24',
|
||
ring: 'w-36 h-36',
|
||
outerRing: 'w-48 h-48',
|
||
label: 'text-base',
|
||
},
|
||
xl: {
|
||
orb: 'w-32 h-32',
|
||
ring: 'w-48 h-48',
|
||
outerRing: 'w-64 h-64',
|
||
label: 'text-lg',
|
||
},
|
||
}
|
||
|
||
// ==================== Component ====================
|
||
|
||
export function DataPincer({
|
||
size = 'lg',
|
||
showLabel = true,
|
||
showPulse = true,
|
||
className,
|
||
}: DataPincerProps) {
|
||
const t = useTranslations('agent')
|
||
const status = useAgentStore(selectAgentStatus)
|
||
const hasError = useAgentStore(selectHasError)
|
||
|
||
const config = statusStyleConfig[status]
|
||
const sizeClass = sizeConfig[size]
|
||
|
||
// i18n: 狀態標籤
|
||
const statusLabel = t(config.labelKey)
|
||
|
||
return (
|
||
<div className={cn('relative flex flex-col items-center', className)}>
|
||
{/* Outer Pulse Ring (條件渲染) */}
|
||
{showPulse && config.animate && (
|
||
<div
|
||
className={cn(
|
||
'absolute rounded-full opacity-20',
|
||
sizeClass.outerRing,
|
||
config.color,
|
||
'animate-ping'
|
||
)}
|
||
style={{ animationDuration: '2s' }}
|
||
/>
|
||
)}
|
||
|
||
{/* Middle Ring - Glass Effect */}
|
||
<div
|
||
className={cn(
|
||
'absolute rounded-full',
|
||
'bg-gradient-to-br from-white/10 to-transparent',
|
||
'border border-white/10',
|
||
'backdrop-blur-sm',
|
||
sizeClass.ring,
|
||
config.animate && config.pulseClass
|
||
)}
|
||
/>
|
||
|
||
{/* Core Orb */}
|
||
<div
|
||
className={cn(
|
||
'relative rounded-full z-10',
|
||
'transition-all duration-500',
|
||
sizeClass.orb,
|
||
config.color,
|
||
config.glowColor,
|
||
config.animate && config.pulseClass
|
||
)}
|
||
>
|
||
{/* Inner Highlight */}
|
||
<div
|
||
className={cn(
|
||
'absolute inset-2 rounded-full',
|
||
'bg-gradient-to-br from-white/30 to-transparent'
|
||
)}
|
||
/>
|
||
|
||
{/* Center Dot */}
|
||
<div
|
||
className={cn(
|
||
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
|
||
'w-1/4 h-1/4 rounded-full',
|
||
'bg-white/50'
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Status Label */}
|
||
{showLabel && (
|
||
<div
|
||
className={cn(
|
||
'mt-6 font-body tracking-wider uppercase',
|
||
sizeClass.label,
|
||
hasError ? 'text-status-critical' : 'text-nothing-gray-400',
|
||
config.animate && 'animate-pulse'
|
||
)}
|
||
>
|
||
{statusLabel}
|
||
</div>
|
||
)}
|
||
|
||
{/* Decorative Lines (Nothing.tech 風格) */}
|
||
<div className="absolute -inset-4 pointer-events-none">
|
||
{/* Top Line */}
|
||
<div
|
||
className={cn(
|
||
'absolute top-0 left-1/2 -translate-x-1/2 w-px h-4',
|
||
'bg-gradient-to-b from-transparent to-nothing-gray-700'
|
||
)}
|
||
/>
|
||
{/* Bottom Line */}
|
||
<div
|
||
className={cn(
|
||
'absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-4',
|
||
'bg-gradient-to-t from-transparent to-nothing-gray-700'
|
||
)}
|
||
/>
|
||
{/* Left Line */}
|
||
<div
|
||
className={cn(
|
||
'absolute left-0 top-1/2 -translate-y-1/2 h-px w-4',
|
||
'bg-gradient-to-r from-transparent to-nothing-gray-700'
|
||
)}
|
||
/>
|
||
{/* Right Line */}
|
||
<div
|
||
className={cn(
|
||
'absolute right-0 top-1/2 -translate-y-1/2 h-px w-4',
|
||
'bg-gradient-to-l from-transparent to-nothing-gray-700'
|
||
)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|