Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | /** * EmojiPicker - Emoji selection component for Agent onboarding * * Displays categorized emoji presets for users to choose from. */ import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../../lib/utils'; import { EMOJI_PRESETS, ALL_EMOJIS } from '../../lib/personality-presets'; type EmojiCategory = 'all' | 'animals' | 'objects' | 'expressions'; export interface EmojiPickerProps { value?: string; onChange: (emoji: string) => void; className?: string; } const categoryLabels: Record<EmojiCategory, string> = { all: '全部', animals: '动物', objects: '物体', expressions: '表情', }; export function EmojiPicker({ value, onChange, className }: EmojiPickerProps) { const [activeCategory, setActiveCategory] = useState<EmojiCategory>('all'); const getEmojisForCategory = (category: EmojiCategory): string[] => { if (category === 'all') { return ALL_EMOJIS; } return EMOJI_PRESETS[category] || []; }; const emojis = getEmojisForCategory(activeCategory); return ( <div className={cn('space-y-3', className)}> {/* Category Tabs */} <div className="flex gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg"> {(Object.keys(categoryLabels) as EmojiCategory[]).map((category) => ( <button key={category} type="button" onClick={() => setActiveCategory(category)} className={cn( 'flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors', activeCategory === category ? 'bg-white dark:bg-gray-700 text-primary shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200' )} > {categoryLabels[category]} </button> ))} </div> {/* Emoji Grid */} <motion.div layout className="grid grid-cols-8 gap-1" > <AnimatePresence mode="popLayout"> {emojis.map((emoji) => ( <motion.button key={emoji} type="button" layout initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.15 }} onClick={() => onChange(emoji)} className={cn( 'w-9 h-9 flex items-center justify-center text-xl rounded-lg transition-all', 'hover:bg-gray-100 dark:hover:bg-gray-700', 'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1', value === emoji && 'bg-primary/10 ring-2 ring-primary' )} > {emoji} </motion.button> ))} </AnimatePresence> </motion.div> {/* Selected Preview */} {value && ( <motion.div initial={{ opacity: 0, y: 5 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-2 p-2 bg-primary/5 rounded-lg" > <span className="text-2xl">{value}</span> <span className="text-sm text-gray-600 dark:text-gray-400"> 已选择 </span> </motion.div> )} </div> ); } |