import { useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '@/lib/utils'; export type CharacterEmotion = | 'idle' | 'thinking' | 'speaking' | 'happy' | 'concerned' | 'analyzing' | 'approving' | 'rejecting' | 'judging' | 'celebrating'; export type ActionAnimation = | 'propose_protocol' | 'revise_protocol' | 'request_info' | 'accept' | 'report_feasibility' | 'suggest_substitution' | 'reject' | 'scoring' | 'verdict_success' | 'verdict_failure' | 'verdict_partial'; const CHARACTER_IMAGES: Record = { scientist: '/characters/scientist.png', lab_manager: '/characters/lab-manager.png', judge: '/characters/judge.png', }; const EMOTION_EMOJIS: Record = { idle: '', thinking: '๐Ÿค”', speaking: '๐Ÿ’ฌ', happy: '๐Ÿ˜Š', concerned: '๐Ÿ˜Ÿ', analyzing: '๐Ÿ”', approving: 'โœ…', rejecting: 'โŒ', judging: 'โš–๏ธ', celebrating: '๐ŸŽ‰', }; const ACTION_TO_EMOTION: Record = { propose_protocol: 'speaking', revise_protocol: 'thinking', request_info: 'thinking', accept: 'happy', report_feasibility: 'analyzing', suggest_substitution: 'thinking', reject: 'concerned', scoring: 'judging', verdict_success: 'celebrating', verdict_failure: 'rejecting', verdict_partial: 'concerned', }; const idleFloat = { y: [0, -6, 0], transition: { duration: 3, repeat: Infinity, ease: 'easeInOut' as const, }, }; const speakingBounce = { scale: [1, 1.05, 1], transition: { duration: 0.4, repeat: 3, ease: 'easeInOut' as const, }, }; const thinkingTilt = { rotate: [0, -5, 5, -3, 0], transition: { duration: 1.2, repeat: 2, ease: 'easeInOut' as const, }, }; const happyJump = { y: [0, -20, 0, -12, 0], scale: [1, 1.1, 1, 1.05, 1], transition: { duration: 0.8, ease: 'easeOut' as const, }, }; const rejectShake = { x: [0, -8, 8, -6, 6, -3, 0], transition: { duration: 0.6, ease: 'easeInOut' as const, }, }; const analyzingPulse = { scale: [1, 1.03, 1], opacity: [1, 0.85, 1], transition: { duration: 1.5, repeat: 2, ease: 'easeInOut' as const, }, }; const celebrateAnimation = { y: [0, -25, 0, -15, 0, -8, 0], rotate: [0, -10, 10, -5, 5, 0], scale: [1, 1.15, 1, 1.1, 1, 1.05, 1], transition: { duration: 1.2, ease: 'easeOut' as const, }, }; function getAnimationForEmotion(emotion: CharacterEmotion) { switch (emotion) { case 'speaking': return speakingBounce; case 'thinking': return thinkingTilt; case 'happy': case 'approving': return happyJump; case 'concerned': case 'rejecting': return rejectShake; case 'analyzing': case 'judging': return analyzingPulse; case 'celebrating': return celebrateAnimation; default: return idleFloat; } } const AURA_COLORS: Record = { scientist: 'from-scientist/40 to-scientist/0', lab_manager: 'from-lab-manager/40 to-lab-manager/0', judge: 'from-judge/40 to-judge/0', }; const RING_ACTIVE: Record = { scientist: 'ring-scientist shadow-[0_0_20px_rgba(59,130,246,0.5)]', lab_manager: 'ring-lab-manager shadow-[0_0_20px_rgba(16,185,129,0.5)]', judge: 'ring-judge shadow-[0_0_20px_rgba(245,158,11,0.5)]', }; interface AnimatedCharacterProps { role: string; emotion?: CharacterEmotion; action?: string; isSpeaking?: boolean; isActive?: boolean; size?: 'sm' | 'md' | 'lg' | 'xl' | 'stage'; showEmoji?: boolean; showAura?: boolean; showName?: boolean; className?: string; } const sizeMap = { sm: 'h-10 w-10', md: 'h-16 w-16', lg: 'h-24 w-24', xl: 'h-36 w-36', stage: 'h-44 w-44', }; export default function AnimatedCharacter({ role, emotion: emotionProp, action, isSpeaking = false, isActive = false, size = 'lg', showEmoji = true, showAura = true, showName = true, className, }: AnimatedCharacterProps) { const [currentEmotion, setCurrentEmotion] = useState('idle'); const [particles, setParticles] = useState([]); useEffect(() => { if (emotionProp) { setCurrentEmotion(emotionProp); return; } if (action && ACTION_TO_EMOTION[action]) { setCurrentEmotion(ACTION_TO_EMOTION[action]); } else if (isSpeaking) { setCurrentEmotion('speaking'); } else { setCurrentEmotion('idle'); } }, [emotionProp, action, isSpeaking]); useEffect(() => { if (currentEmotion === 'celebrating' || currentEmotion === 'happy') { setParticles(Array.from({ length: 6 }, (_, i) => i)); const timer = setTimeout(() => setParticles([]), 2000); return () => clearTimeout(timer); } }, [currentEmotion]); const anim = getAnimationForEmotion(currentEmotion); const emoji = EMOTION_EMOJIS[currentEmotion]; const src = CHARACTER_IMAGES[role]; const names: Record = { scientist: 'Dr. Elara', lab_manager: 'Takuma', judge: 'Judge Aldric' }; return (
{/* Aura glow */} {showAura && isActive && ( )} {/* Particles */} {particles.map((p) => ( {['โœจ', 'โญ', '๐ŸŒŸ', '๐Ÿ’ซ', '๐Ÿ”ฌ', '๐Ÿงช'][p % 6]} ))} {/* Emoji indicator */} {showEmoji && emoji && ( {emoji} )} {/* Character image */} {src && ( {names[role] )} {/* Speaking indicator overlay */} {isSpeaking && ( )} {/* Typing dots */} {currentEmotion === 'thinking' && (
{[0, 1, 2].map((i) => ( ))}
)} {/* Name */} {showName && ( {names[role]} )}
); }