'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Sparkles, X, Settings, ChevronUp, Star, Heart, Zap, Moon, Sun, BookOpen, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { t } from '@/lib/i18n'; import { apiGet, apiPut } from '@/lib/api'; interface VirtualCharacterData { id: string; userId: string; characterId: string; name: string; color: string; level: number; xp: number; mood: string; accessories: string | null; } const CHARACTER_TYPES = [ { id: 'owl', nameKey: 'character.owl', icon: 'owl' }, { id: 'dragon', nameKey: 'character.dragon', icon: 'dragon' }, { id: 'robot', nameKey: 'character.robot', icon: 'robot' }, { id: 'cat', nameKey: 'character.cat', icon: 'cat' }, { id: 'wizard', nameKey: 'character.wizard', icon: 'wizard' }, ]; const XP_PER_LEVEL = 100; const MOODS = ['happy', 'excited', 'thinking', 'sleepy', 'celebrating']; // ─── SVG Character Renderers ────────────────────────────────────────── function OwlCharacter({ color, mood }: { color: string; mood: string }) { const isBlinking = mood === 'thinking'; const isSleepy = mood === 'sleepy'; const isExcited = mood === 'excited' || mood === 'celebrating'; return ( {/* Body */} {/* Head */} {/* Ear tufts */} {/* Eyes */} {!isBlinking && !isSleepy ? ( <> > ) : isSleepy ? ( <> > ) : ( <> > )} {/* Beak */} {/* Wings */} {/* Feet */} {/* Celebration sparkles */} {mood === 'celebrating' && ( <> > )} ); } function DragonCharacter({ color, mood }: { color: string; mood: string }) { const isExcited = mood === 'excited' || mood === 'celebrating'; const isSleepy = mood === 'sleepy'; return ( {/* Body */} {/* Head */} {/* Horns */} {/* Eyes */} {isSleepy ? ( <> > ) : ( <> > )} {/* Nostrils */} {/* Mouth */} {mood === 'happy' || isExcited ? ( ) : ( )} {/* Wings */} {/* Tail */} {/* Belly */} {/* Feet */} {/* Fire breath when excited */} {mood === 'celebrating' && ( )} ); } function RobotCharacter({ color, mood }: { color: string; mood: string }) { const isThinking = mood === 'thinking'; const isSleepy = mood === 'sleepy'; const isExcited = mood === 'excited' || mood === 'celebrating'; return ( {/* Antenna */} {/* Head */} {/* Eyes */} {isSleepy ? ( <> > ) : ( <> > )} {/* Mouth */} {mood === 'happy' || isExcited ? ( ) : ( )} {/* Body */} {/* Chest display */} {/* Arms */} {/* Legs */} ); } function CatCharacter({ color, mood }: { color: string; mood: string }) { const isExcited = mood === 'excited' || mood === 'celebrating'; const isSleepy = mood === 'sleepy'; const isThinking = mood === 'thinking'; return ( {/* Body */} {/* Head */} {/* Ears */} {/* Eyes */} {isSleepy ? ( <> > ) : isThinking ? ( <> > ) : ( <> > )} {/* Nose */} {/* Mouth */} {mood === 'happy' || isExcited ? ( ) : ( )} {/* Whiskers */} {/* Tail */} {/* Paws */} {/* Celebration stars */} {mood === 'celebrating' && ( <> > )} ); } function WizardCharacter({ color, mood }: { color: string; mood: string }) { const isExcited = mood === 'excited' || mood === 'celebrating'; const isSleepy = mood === 'sleepy'; return ( {/* Hat */} {/* Head */} {/* Eyes */} {isSleepy ? ( <> > ) : ( <> > )} {/* Beard */} {/* Nose */} {/* Robe */} {/* Belt */} {/* Wand */} {/* Magic sparkles */} {mood === 'celebrating' && ( <> > )} {/* Hands */} ); } function CharacterRenderer({ characterId, color, mood }: { characterId: string; color: string; mood: string }) { switch (characterId) { case 'owl': return ; case 'dragon': return ; case 'robot': return ; case 'cat': return ; case 'wizard': return ; default: return ; } } // ─── Main Component ────────────────────────────────────────────────── export default function VirtualCharacter({ userRole }: { userRole: string }) { // Don't show for admin/teacher roles if (userRole === 'SCHOOL_ADMIN' || userRole === 'SUPER_ADMIN' || userRole === 'TEACHER') { return null; } return ; } function VirtualCharacterInner() { const [character, setCharacter] = useState(null); const [enabled, setEnabled] = useState(true); const [isExpanded, setIsExpanded] = useState(false); const [showCustomize, setShowCustomize] = useState(false); const [speechBubble, setSpeechBubble] = useState(null); const [editName, setEditName] = useState(''); const [editColor, setEditColor] = useState('#10b981'); const [editCharacterId, setEditCharacterId] = useState('owl'); // Load character data useEffect(() => { async function loadCharacter() { try { const data = await apiGet<{ character: VirtualCharacterData; enabled: boolean }>( '/api/ai/character' ); setCharacter(data.character); setEnabled(data.enabled); setEditName(data.character.name); setEditColor(data.character.color); setEditCharacterId(data.character.characterId); } catch { setEnabled(false); } } loadCharacter(); }, []); // Show idle speech bubbles periodically useEffect(() => { if (!character || !enabled) return; const getGreeting = () => { const hour = new Date().getHours(); if (hour < 12) return t('character.greeting_morning'); if (hour < 18) return t('character.greeting_afternoon'); return t('character.greeting_evening'); }; // Show initial greeting after 3 seconds const initialTimer = setTimeout(() => { setSpeechBubble(getGreeting()); setTimeout(() => setSpeechBubble(null), 5000); }, 3000); // Show idle messages every 60 seconds const interval = setInterval(() => { const messages = [ t('character.idle_message_1'), t('character.idle_message_2'), t('character.idle_message_3'), ]; const randomMsg = messages[Math.floor(Math.random() * messages.length)]; setSpeechBubble(randomMsg); setTimeout(() => setSpeechBubble(null), 4000); }, 60000); return () => { clearTimeout(initialTimer); clearInterval(interval); }; }, [character, enabled]); // Update character mood periodically useEffect(() => { if (!character || !enabled) return; const moodInterval = setInterval(() => { const hour = new Date().getHours(); let newMood = character.mood; if (hour >= 22 || hour < 6) { newMood = 'sleepy'; } else if (Math.random() > 0.7) { const randomMoods = ['happy', 'thinking', 'excited']; newMood = randomMoods[Math.floor(Math.random() * randomMoods.length)]; } if (newMood !== character.mood) { setCharacter((prev) => (prev ? { ...prev, mood: newMood } : prev)); } }, 30000); return () => clearInterval(moodInterval); }, [character, enabled]); const handleSaveCharacter = useCallback(async () => { try { const data = await apiPut('/api/ai/character', { characterId: editCharacterId, name: editName, color: editColor, }); setCharacter(data); setShowCustomize(false); } catch { // Ignore save errors } }, [editCharacterId, editName, editColor]); const xpProgress = character ? (character.xp % XP_PER_LEVEL) / XP_PER_LEVEL * 100 : 0; const xpToNext = character ? XP_PER_LEVEL - (character.xp % XP_PER_LEVEL) : XP_PER_LEVEL; // Get mood icon const moodIcon = useMemo(() => { switch (character?.mood) { case 'excited': case 'celebrating': return ; case 'thinking': return ; case 'sleepy': return ; default: return ; } }, [character?.mood]); if (!enabled || !character) return null; return ( {/* Speech Bubble */} {speechBubble && !isExpanded && ( {speechBubble} )} {/* Expanded Panel */} {isExpanded && ( {/* Character Display */} {character.name} {t('character.level')} {character.level} setShowCustomize(!showCustomize)} > {t('character.customize')} setIsExpanded(false)} > {/* Character SVG */} {/* Mood Badge */} {moodIcon} {t(`character.mood_${character.mood}` as keyof typeof t)} {/* XP Progress */} {t('character.xp')}: {character.xp} {t('character.xp_to_next', { xp: String(xpToNext) })} {/* Shimmer effect */} {/* Customize Panel */} {showCustomize && ( {/* Character Selection */} {t('character.select')} {CHARACTER_TYPES.map((ct) => ( setEditCharacterId(ct.id)} > {t(ct.nameKey)} ))} {/* Name */} {t('character.name')} setEditName(e.target.value)} className="h-8 text-xs border-emerald-200/50 dark:border-emerald-800/30" /> {/* Color */} {t('character.color')} setEditColor(e.target.value)} className="w-8 h-8 rounded-lg border border-emerald-200/50 dark:border-emerald-800/30 cursor-pointer" /> setEditColor(e.target.value)} className="h-8 text-xs border-emerald-200/50 dark:border-emerald-800/30 flex-1" /> {/* Save Button */} {t('character.save')} )} )} {/* Floating Character Button */} setIsExpanded(!isExpanded)} className="h-14 w-14 rounded-full bg-gradient-to-br from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 shadow-lg shadow-emerald-500/30 hover:shadow-emerald-500/50 transition-all duration-300 p-0 overflow-hidden" > {/* Level badge */} {character.level} ); }
{speechBubble}