Files
awoooi/apps/web/src/components/agent/data-pincer.tsx
OG T 4d46e6b9a7
Some checks failed
E2E Health Check / e2e-health (push) Successful in 17s
CD Pipeline / build-and-deploy (push) Has been cancelled
style(web): 全站 font-mono → font-body (DM Mono 設計系統套用)
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>
2026-04-02 09:37:03 +08:00

237 lines
5.8 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'
/**
* 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 ====================
// 狀態樣式配置 (不含 labellabel 由 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>
)
}