/* Chinese radical learning game */ const CHINESE_RADICAL_STORAGE_KEY = 'ppt-chinese-radical-selection-v1'; const CHINESE_NOTES_STORAGE_KEY = 'ppt-chinese-topic-notes-v1'; const CHINESE_HANDWRITING_FONT = '"Ma Shan Zheng", "Kaiti SC", STKaiti, KaiTi, "PingFang SC", cursive'; const CHINESE_RADICALS = [ { id: 'ren', radical: '人', alt: '亻', pinyin: 'rén', tone: 'ren2', meaning: 'person', vi: 'người', hint: 'Liên quan đến người và hành động của con người.', examples: ['你', '他', '休'] }, { id: 'kou', radical: '口', alt: '', pinyin: 'kǒu', tone: 'kou3', meaning: 'mouth', vi: 'miệng', hint: 'Liên quan đến nói, ăn, miệng hoặc âm thanh.', examples: ['吗', '叫', '吃'] }, { id: 'nu', radical: '女', alt: '', pinyin: 'nǚ', tone: 'nu3', meaning: 'woman', vi: 'nữ', hint: 'Thường gặp trong chữ về phụ nữ, gia đình, quan hệ.', examples: ['好', '妈', '姐'] }, { id: 'zi', radical: '子', alt: '', pinyin: 'zǐ', tone: 'zi3', meaning: 'child', vi: 'con', hint: 'Gợi ý trẻ em, con cái, người nhỏ tuổi.', examples: ['好', '字', '孩'] }, { id: 'xin', radical: '心', alt: '忄', pinyin: 'xīn', tone: 'xin1', meaning: 'heart', vi: 'tâm', hint: 'Cảm xúc, suy nghĩ, trạng thái tinh thần.', examples: ['想', '快', '忙'] }, { id: 'shou', radical: '手', alt: '扌', pinyin: 'shǒu', tone: 'shou3', meaning: 'hand', vi: 'tay', hint: 'Hành động làm bằng tay.', examples: ['打', '找', '拿'] }, { id: 'shui', radical: '水', alt: '氵', pinyin: 'shuǐ', tone: 'shui3', meaning: 'water', vi: 'nước', hint: 'Nước, chất lỏng, sông suối, trạng thái ẩm ướt.', examples: ['河', '海', '洗'] }, { id: 'huo', radical: '火', alt: '灬', pinyin: 'huǒ', tone: 'huo3', meaning: 'fire', vi: 'lửa', hint: 'Lửa, nhiệt, nấu, cháy.', examples: ['热', '烧', '灯'] }, { id: 'mu_tree', radical: '木', alt: '', pinyin: 'mù', tone: 'mu4', meaning: 'wood/tree', vi: 'mộc', hint: 'Cây, gỗ, vật liệu gỗ.', examples: ['林', '机', '桌'] }, { id: 'ri', radical: '日', alt: '', pinyin: 'rì', tone: 'ri4', meaning: 'sun/day', vi: 'nhật', hint: 'Ngày, mặt trời, thời gian.', examples: ['明', '时', '晚'] }, { id: 'yue', radical: '月', alt: '', pinyin: 'yuè', tone: 'yue4', meaning: 'moon/month', vi: 'nguyệt', hint: 'Trăng, tháng; trong nhiều chữ cũng gợi ý bộ phận cơ thể.', examples: ['明', '期', '腿'] }, { id: 'mu_eye', radical: '目', alt: '', pinyin: 'mù', tone: 'mu4', meaning: 'eye', vi: 'mục', hint: 'Mắt, nhìn, quan sát.', examples: ['看', '眼', '睛'] }, { id: 'yan', radical: '言', alt: '讠', pinyin: 'yán', tone: 'yan2', meaning: 'speech', vi: 'ngôn', hint: 'Nói, ngôn ngữ, lời hứa.', examples: ['说', '语', '请'] }, { id: 'jin', radical: '金', alt: '钅', pinyin: 'jīn', tone: 'jin1', meaning: 'metal/gold', vi: 'kim', hint: 'Kim loại, tiền, vật dụng bằng kim loại.', examples: ['钱', '铁', '银'] }, { id: 'tu', radical: '土', alt: '', pinyin: 'tǔ', tone: 'tu3', meaning: 'earth/soil', vi: 'thổ', hint: 'Đất, vùng đất, nền móng.', examples: ['地', '场', '城'] }, { id: 'yu', radical: '玉', alt: '王', pinyin: 'yù', tone: 'yu4', meaning: 'jade', vi: 'ngọc', hint: 'Ngọc, đá quý, vật đẹp hoặc quý.', examples: ['球', '玩', '现'] }, { id: 'shan', radical: '山', alt: '', pinyin: 'shān', tone: 'shan1', meaning: 'mountain', vi: 'sơn', hint: 'Núi, địa hình cao.', examples: ['岛', '峰', '岁'] }, { id: 'tian', radical: '田', alt: '', pinyin: 'tián', tone: 'tian2', meaning: 'field', vi: 'điền', hint: 'Ruộng đồng, ô đất, canh tác.', examples: ['男', '界', '留'] }, { id: 'mi_thread', radical: '糸', alt: '纟', pinyin: 'mì', tone: 'mi4', meaning: 'silk/thread', vi: 'mịch', hint: 'Sợi chỉ, dệt, kết nối.', examples: ['线', '给', '红'] }, { id: 'cao', radical: '艹', alt: '', pinyin: 'cǎo', tone: 'cao3', meaning: 'grass', vi: 'thảo', hint: 'Cây cỏ, thực vật, hoa lá.', examples: ['花', '茶', '草'] }, { id: 'chong', radical: '虫', alt: '', pinyin: 'chóng', tone: 'chong2', meaning: 'insect', vi: 'trùng', hint: 'Côn trùng, động vật nhỏ.', examples: ['蚊', '蛇', '蛋'] }, { id: 'zu', radical: '足', alt: '', pinyin: 'zú', tone: 'zu2', meaning: 'foot', vi: 'túc', hint: 'Chân, đi lại, động tác bằng chân.', examples: ['跑', '跳', '跟'] }, { id: 'che', radical: '车', alt: '', pinyin: 'chē', tone: 'che1', meaning: 'vehicle', vi: 'xa', hint: 'Xe, bánh xe, di chuyển.', examples: ['辆', '转', '轮'] }, { id: 'ma', radical: '马', alt: '', pinyin: 'mǎ', tone: 'ma3', meaning: 'horse', vi: 'mã', hint: 'Ngựa; trong nhiều chữ dùng làm thành phần âm.', examples: ['妈', '吗', '码'] }, { id: 'men', radical: '门', alt: '', pinyin: 'mén', tone: 'men2', meaning: 'door', vi: 'môn', hint: 'Cửa, cổng, không gian bên trong hoặc bên ngoài.', examples: ['问', '间', '闻'] }, { id: 'shi_food', radical: '食', alt: '饣', pinyin: 'shí', tone: 'shi2', meaning: 'food', vi: 'thực', hint: 'Ăn uống, thực phẩm.', examples: ['饭', '饮', '饱'] }, { id: 'yi', radical: '衣', alt: '衤', pinyin: 'yī', tone: 'yi1', meaning: 'clothing', vi: 'y', hint: 'Quần áo, vải, mặc.', examples: ['被', '裙', '补'] }, { id: 'bei', radical: '贝', alt: '', pinyin: 'bèi', tone: 'bei4', meaning: 'shell/money', vi: 'bối', hint: 'Tiền, tài sản, mua bán.', examples: ['财', '货', '贵'] }, { id: 'li', radical: '力', alt: '', pinyin: 'lì', tone: 'li4', meaning: 'power', vi: 'lực', hint: 'Sức mạnh, cố gắng, lao động.', examples: ['加', '动', '办'] }, { id: 'dao', radical: '刀', alt: '刂', pinyin: 'dāo', tone: 'dao1', meaning: 'knife', vi: 'đao', hint: 'Dao, cắt, tách.', examples: ['到', '别', '分'] }, { id: 'quan', radical: '犬', alt: '犭', pinyin: 'quǎn', tone: 'quan3', meaning: 'dog/animal', vi: 'khuyển', hint: 'Chó, thú vật, hành vi của động vật.', examples: ['狗', '猫', '狼'] }, { id: 'niao', radical: '鸟', alt: '', pinyin: 'niǎo', tone: 'niao3', meaning: 'bird', vi: 'điểu', hint: 'Chim, loài bay.', examples: ['鸡', '鸭', '鹅'] }, { id: 'yu_fish', radical: '鱼', alt: '', pinyin: 'yú', tone: 'yu2', meaning: 'fish', vi: 'ngư', hint: 'Cá, thủy sản.', examples: ['鲜', '鲤', '鲸'] }, { id: 'shi_stone', radical: '石', alt: '', pinyin: 'shí', tone: 'shi2', meaning: 'stone', vi: 'thạch', hint: 'Đá, khoáng vật, vật cứng.', examples: ['码', '矿', '破'] }, { id: 'mi_rice', radical: '米', alt: '', pinyin: 'mǐ', tone: 'mi3', meaning: 'rice', vi: 'mễ', hint: 'Gạo, hạt, lương thực.', examples: ['精', '粉', '粮'] }, { id: 'zhu', radical: '竹', alt: '⺮', pinyin: 'zhú', tone: 'zhu2', meaning: 'bamboo', vi: 'trúc', hint: 'Tre, trúc, đồ làm từ tre.', examples: ['笑', '笔', '第'] }, { id: 'er', radical: '耳', alt: '', pinyin: 'ěr', tone: 'er3', meaning: 'ear', vi: 'nhĩ', hint: 'Tai, nghe.', examples: ['听', '职', '聪'] }, { id: 'mian', radical: '宀', alt: '', pinyin: 'mián', tone: 'mian2', meaning: 'roof', vi: 'miên', hint: 'Mái nhà, nơi ở, bên trong nhà.', examples: ['家', '字', '安'] }, { id: 'chuo', radical: '辶', alt: '', pinyin: 'chuò', tone: 'chuo4', meaning: 'walk/go', vi: 'sước', hint: 'Di chuyển, đường đi, quá trình.', examples: ['这', '进', '远'] }, { id: 'fu', radical: '阝', alt: '', pinyin: 'fù', tone: 'fu4', meaning: 'mound/city side', vi: 'phụ / ấp', hint: 'Bên trái thường liên quan đồi núi; bên phải thường liên quan thành ấp.', examples: ['院', '都', '阳'] }, { id: 'guang', radical: '广', alt: '', pinyin: 'guǎng', tone: 'guang3', meaning: 'shelter', vi: 'nghiễm', hint: 'Mái che, công trình, không gian rộng.', examples: ['店', '座', '床'] }, { id: 'ne', radical: '疒', alt: '', pinyin: 'nè', tone: 'ne4', meaning: 'sickness', vi: 'nạch', hint: 'Bệnh, đau, trạng thái cơ thể.', examples: ['病', '痛', '痒'] }, { id: 'pu', radical: '攵', alt: '', pinyin: 'pū', tone: 'pu1', meaning: 'tap/strike', vi: 'phộc', hint: 'Đánh, tác động, hành động thay đổi.', examples: ['教', '放', '改'] }, { id: 'qian', radical: '欠', alt: '', pinyin: 'qiàn', tone: 'qian4', meaning: 'lack/yawn', vi: 'khiếm', hint: 'Thiếu, nợ, hoặc liên quan đến hơi thở.', examples: ['欢', '歌', '欲'] }, { id: 'ye', radical: '页', alt: '', pinyin: 'yè', tone: 'ye4', meaning: 'head/page', vi: 'hiệt', hint: 'Đầu, mặt, trang giấy trong chữ giản thể.', examples: ['题', '顾', '颜'] }, { id: 'da', radical: '大', alt: '', pinyin: 'dà', tone: 'da4', meaning: 'big', vi: 'đại', hint: 'Lớn, to, mở rộng.', examples: ['天', '太', '奇'] }, { id: 'xiao', radical: '小', alt: '⺌', pinyin: 'xiǎo', tone: 'xiao3', meaning: 'small', vi: 'tiểu', hint: 'Nhỏ, ít, nhỏ bé.', examples: ['少', '尖', '尘'] }, { id: 'shi_show', radical: '示', alt: '礻', pinyin: 'shì', tone: 'shi4', meaning: 'show/altar', vi: 'thị', hint: 'Lễ nghi, tinh thần, chúc phúc.', examples: ['神', '礼', '祝'] }, { id: 'he', radical: '禾', alt: '', pinyin: 'hé', tone: 'he2', meaning: 'grain', vi: 'hòa', hint: 'Lúa, cây lương thực, nông nghiệp.', examples: ['种', '秋', '秒'] }, { id: 'yu_rain', radical: '雨', alt: '', pinyin: 'yǔ', tone: 'yu3', meaning: 'rain', vi: 'vũ', hint: 'Mưa, thời tiết, hiện tượng trời.', examples: ['雪', '雷', '露'] }, ]; const CHINESE_TOPIC_GROUPS = [ { id: 'people-body', label: 'People & Body', vi: 'Người và cơ thể', ids: ['ren', 'nu', 'zi', 'xin', 'shou', 'mu_eye', 'er', 'da', 'xiao'], }, { id: 'speech-learning', label: 'Speech & Learning', vi: 'Ngôn ngữ và học tập', ids: ['kou', 'yan', 'shi_show', 'pu'], }, { id: 'nature-time', label: 'Nature & Time', vi: 'Tự nhiên và thời gian', ids: ['shui', 'huo', 'mu_tree', 'ri', 'yue', 'tu', 'shan', 'tian', 'cao', 'yu_rain', 'he'], }, { id: 'animals', label: 'Animals', vi: 'Động vật', ids: ['chong', 'ma', 'quan', 'niao', 'yu_fish'], }, { id: 'home-place', label: 'Home & Places', vi: 'Nhà cửa và nơi chốn', ids: ['men', 'mian', 'guang', 'fu', 'chuo', 'che'], }, { id: 'food-materials', label: 'Food & Materials', vi: 'Đồ ăn và vật liệu', ids: ['shi_food', 'yi', 'bei', 'jin', 'yu', 'mi_thread', 'shi_stone', 'mi_rice', 'zhu'], }, { id: 'actions-state', label: 'Actions & State', vi: 'Hành động và trạng thái', ids: ['zu', 'li', 'dao', 'ne', 'qian', 'ye'], }, ]; const CHINESE_COMPOSITIONS = [ { topic: 'People & Family', parts: ['女', '子'], result: '好', pinyin: 'hǎo', meaning: 'good', note: 'Ghép nữ + con, học để nhớ chữ "hǎo".' }, { topic: 'Nature', parts: ['日', '月'], result: '明', pinyin: 'míng', meaning: 'bright', note: 'Mặt trời + mặt trăng gợi ý sáng rõ.' }, { topic: 'Actions', parts: ['亻', '木'], result: '休', pinyin: 'xiū', meaning: 'rest', note: 'Người dựa vào cây là nghỉ ngơi.' }, { topic: 'Nature', parts: ['木', '木'], result: '林', pinyin: 'lín', meaning: 'woods', note: 'Hai cây thành rừng nhỏ.' }, { topic: 'Nature', parts: ['氵', '青'], result: '清', pinyin: 'qīng', meaning: 'clear', note: 'Nước + xanh/trong gợi ý trong sạch.' }, { topic: 'Speech', parts: ['口', '马'], result: '吗', pinyin: 'ma', meaning: 'question particle', note: 'Khẩu là nói; mã làm thành phần âm.' }, { topic: 'Speech', parts: ['讠', '青'], result: '请', pinyin: 'qǐng', meaning: 'please/invite', note: 'Ngôn ngữ + thành phần âm qīng.' }, { topic: 'Actions', parts: ['扌', '丁'], result: '打', pinyin: 'dǎ', meaning: 'hit/call', note: 'Tay + thành phần âm, thường là hành động bằng tay.' }, { topic: 'Nature', parts: ['艹', '化'], result: '花', pinyin: 'huā', meaning: 'flower', note: 'Cỏ/cây + hóa, liên quan thực vật.' }, { topic: 'Home', parts: ['宀', '女'], result: '安', pinyin: 'ān', meaning: 'peace', note: 'Dưới mái nhà gợi ý yên ổn.' }, { topic: 'Money', parts: ['贝', '才'], result: '财', pinyin: 'cái', meaning: 'wealth', note: 'Bối gợi ý tiền/tài sản.' }, { topic: 'Health', parts: ['疒', '丙'], result: '病', pinyin: 'bìng', meaning: 'illness', note: 'Nạch là bộ bệnh, gặp trong nhiều chữ về sức khỏe.' }, ]; const CHINESE_SENTENCES = [ { topic: 'Greeting', zh: '你好,我叫安。', en: 'Hello, my name is An.' }, { topic: 'Home & Family', zh: '他在家休息。', en: 'He rests at home.' }, { topic: 'Food & Drink', zh: '妈妈喝水。', en: 'Mom drinks water.' }, { topic: 'Food & Drink', zh: '我喜欢喝茶。', en: 'I like drinking tea.' }, { topic: 'Time & Weather', zh: '今天太阳很大。', en: 'The sun is bright today.' }, { topic: 'School', zh: '明天我们去学校。', en: 'Tomorrow we go to school.' }, { topic: 'Travel', zh: '请问,车站在哪里?', en: 'Excuse me, where is the station?' }, { topic: 'School', zh: '她买了一个新书包。', en: 'She bought a new school bag.' }, { topic: 'Nature', zh: '这条河很清。', en: 'This river is very clear.' }, { topic: 'Home & Family', zh: '小猫在门口睡觉。', en: 'The kitten sleeps by the door.' }, { topic: 'School', zh: '我用手写汉字。', en: 'I write Chinese characters by hand.' }, { topic: 'Home & Family', zh: '桌子上有一本书。', en: 'There is a book on the table.' }, { topic: 'Nature', zh: '秋天的山很漂亮。', en: 'The mountains in autumn are beautiful.' }, { topic: 'Actions', zh: '他跑得很快。', en: 'He runs very fast.' }, { topic: 'Food & Drink', zh: '我们一起吃饭。', en: 'We eat together.' }, { topic: 'Health', zh: '医生说他没事。', en: 'The doctor says he is fine.' }, { topic: 'Nature', zh: '她喜欢红色的花。', en: 'She likes red flowers.' }, { topic: 'Time & Weather', zh: '雨下得很大。', en: 'It is raining heavily.' }, ]; const CHINESE_DEFAULT_IDS = CHINESE_RADICALS.slice(0, 20).map(item => item.id); const CHINESE_TOPIC_BY_RADICAL_ID = CHINESE_TOPIC_GROUPS.reduce((lookup, topic) => { topic.ids.forEach(id => { lookup[id] = topic; }); return lookup; }, {}); const CHINESE_NOTE_TOPICS = [ { id: 'general', label: 'General', vi: 'Ghi chú chung', ids: [] }, ...CHINESE_TOPIC_GROUPS, { id: 'sentences', label: 'Sentences', vi: 'Câu mẫu', ids: [] }, ]; function getMarkedPinyin(text, fallback = '') { const pinyin = window.pinyinPro && window.pinyinPro.pinyin; if (!pinyin || !text) return fallback; try { const result = pinyin(text, { toneType: 'symbol', type: 'array', nonZh: 'removed', }); return Array.isArray(result) && result.length ? result.join(' ') : fallback; } catch (err) { return fallback; } } function radicalPinyin(item) { return getMarkedPinyin(item.radical, item.pinyin) || item.pinyin; } function radicalTopic(item) { return CHINESE_TOPIC_BY_RADICAL_ID[item.id] || { id: 'other', label: 'Other', vi: 'Khác', ids: [] }; } function groupRadicalsByTopic(radicals) { return CHINESE_TOPIC_GROUPS.map(topic => ({ topic, items: radicals.filter(item => topic.ids.includes(item.id)), })).filter(group => group.items.length); } function groupItemsByTopic(items) { return items.reduce((groups, item) => { const key = item.topic || 'Other'; if (!groups[key]) groups[key] = []; groups[key].push(item); return groups; }, {}); } function sameSet(left, right) { return left.length === right.length && left.every(item => right.includes(item)); } function speakChinese(text) { if (!text || !window.speechSynthesis || !window.SpeechSynthesisUtterance) return false; const utterance = new SpeechSynthesisUtterance(text); const voices = window.speechSynthesis.getVoices ? window.speechSynthesis.getVoices() : []; const preferredVoice = voices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith('zh-cn')) || voices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith('zh')); utterance.lang = preferredVoice ? preferredVoice.lang : 'zh-CN'; utterance.rate = 0.82; utterance.pitch = 1; if (preferredVoice) utterance.voice = preferredVoice; window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); return true; } function normalizeSearchText(value) { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase(); } function sampleArray(items, count) { const pool = [...items]; for (let i = pool.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } return pool.slice(0, count); } function buildChineseChoices(current) { const used = new Set([current.pinyin]); const distractors = sampleArray( CHINESE_RADICALS.filter(item => item.pinyin !== current.pinyin), CHINESE_RADICALS.length ).filter(item => { if (used.has(item.pinyin)) return false; used.add(item.pinyin); return true; }).slice(0, 3); return sampleArray([current, ...distractors], 4); } function ChineseLearningPage({ tweaks, onBackHome }) { const primary = tweaks.primaryColor; const [activeTab, setActiveTab] = React.useState('game'); const [selectedIds, setSelectedIds] = React.useState(() => { try { const saved = JSON.parse(localStorage.getItem(CHINESE_RADICAL_STORAGE_KEY) || 'null'); return Array.isArray(saved) && saved.length ? saved : CHINESE_DEFAULT_IDS; } catch (err) { return CHINESE_DEFAULT_IDS; } }); const [duration, setDuration] = React.useState(60); const [timeLeft, setTimeLeft] = React.useState(60); const [running, setRunning] = React.useState(false); const [current, setCurrent] = React.useState(null); const [choices, setChoices] = React.useState([]); const [score, setScore] = React.useState({ correct: 0, total: 0, streak: 0 }); const [feedback, setFeedback] = React.useState(null); const [query, setQuery] = React.useState(''); const [sentenceIndex, setSentenceIndex] = React.useState(0); const [showSentenceEnglish, setShowSentenceEnglish] = React.useState(false); const [activeNoteTopic, setActiveNoteTopic] = React.useState('general'); const [notes, setNotes] = React.useState(() => { try { const saved = JSON.parse(localStorage.getItem(CHINESE_NOTES_STORAGE_KEY) || '{}'); return saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {}; } catch (err) { return {}; } }); React.useEffect(() => { try { localStorage.setItem(CHINESE_RADICAL_STORAGE_KEY, JSON.stringify(selectedIds)); } catch (err) {} }, [selectedIds]); React.useEffect(() => { try { localStorage.setItem(CHINESE_NOTES_STORAGE_KEY, JSON.stringify(notes)); } catch (err) {} }, [notes]); React.useEffect(() => { if (!running || duration === 0) return undefined; const timer = window.setInterval(() => { setTimeLeft(value => { if (value <= 1) { window.clearInterval(timer); setRunning(false); setFeedback({ type: 'done', text: 'Time is up. Review the words below, then play again.' }); return 0; } return value - 1; }); }, 1000); return () => window.clearInterval(timer); }, [running, duration]); const selectedRadicals = CHINESE_RADICALS.filter(item => selectedIds.includes(item.id)); const filteredRadicals = CHINESE_RADICALS.filter(item => { const topic = radicalTopic(item); const text = normalizeSearchText(`${item.radical} ${item.alt} ${item.pinyin} ${item.tone} ${item.meaning} ${item.vi} ${item.hint} ${topic.label} ${topic.vi} ${item.examples.join(' ')}`); return text.includes(normalizeSearchText(query.trim())); }); const nextQuestion = React.useCallback((pool = selectedRadicals) => { if (!pool.length) { setCurrent(null); setChoices([]); return; } const next = pool[Math.floor(Math.random() * pool.length)]; setCurrent(next); setChoices(buildChineseChoices(next)); }, [selectedRadicals]); const startGame = () => { const pool = selectedRadicals.length ? selectedRadicals : CHINESE_RADICALS.slice(0, 10); setScore({ correct: 0, total: 0, streak: 0 }); setFeedback(null); setTimeLeft(duration || 0); setRunning(true); nextQuestion(pool); }; const answer = (choice) => { if (!running || !current) return; const ok = choice.pinyin === current.pinyin; setScore(prev => ({ correct: prev.correct + (ok ? 1 : 0), total: prev.total + 1, streak: ok ? prev.streak + 1 : 0, })); setFeedback({ type: ok ? 'correct' : 'wrong', text: ok ? `${current.radical}${current.alt ? ` / ${current.alt}` : ''} = ${radicalPinyin(current)} (${current.tone})` : `${current.radical}${current.alt ? ` / ${current.alt}` : ''} is ${radicalPinyin(current)} (${current.tone}), not ${radicalPinyin(choice)}.`, }); window.setTimeout(() => nextQuestion(), 420); }; const toggleRadical = (id) => { setSelectedIds(currentIds => ( currentIds.includes(id) ? currentIds.filter(item => item !== id) : [...currentIds, id] )); }; const updateNote = (topicId, value) => { setNotes(currentNotes => ({ ...currentNotes, [topicId]: value, })); }; const accuracy = score.total ? Math.round((score.correct / score.total) * 100) : 0; return (
Chinese radicals practice

Chinese

Learn 50 common Chinese radicals by quick pinyin recall, vocabulary review, and character-building examples.

{[ ['game', 'Game play'], ['vocab', 'Vocabulary'], ['build', 'Build characters'], ['sentences', 'Sentences'], ['notes', 'Notes'], ].map(([id, label]) => ( ))}
{activeTab === 'game' && ( )} {activeTab === 'vocab' && ( )} {activeTab === 'build' && ( )} {activeTab === 'sentences' && ( )} {activeTab === 'notes' && ( )}
); } function ChineseGameTab({ primary, selectedIds, selectedRadicals, duration, setDuration, timeLeft, running, current, choices, score, accuracy, feedback, startGame, answer, toggleRadical, setSelectedIds, }) { return (

Quick pinyin game

Look at the radical and choose its pinyin as fast as possible.

{duration ? `${timeLeft}s` : 'No timer'}
{!running || !current ? (
Handwriting

Select at least 4 radicals, choose a timer, then start.

) : (
What is this radical's pinyin?
{current.alt ? `Common form: ${current.alt}` : current.meaning}
Handwriting
{choices.map(choice => ( ))}
{feedback && (
{feedback.text}
)}
)}
); } function ChineseVocabularyTab({ primary, query, setQuery, filteredRadicals }) { const groupedRadicals = groupRadicalsByTopic(filteredRadicals); return (

50 radicals by topic

Review the exact words used in the game, grouped by learning topic.

setQuery(event.target.value)} placeholder="Search radical, pinyin, meaning..." style={{ minWidth: 280, maxWidth: 420, width: '100%', padding: '13px 15px', borderRadius: 14, border: '1px solid rgba(var(--theme-text-rgb),0.1)', background: 'rgba(var(--theme-bg-rgb),0.38)', color: 'var(--theme-text)', outline: 'none', }} />
{groupedRadicals.map(({ topic, items }) => (

{topic.label}

{topic.vi}
{items.length} radicals
{items.map(item => (
{item.alt && {item.alt}}
Handwriting
{radicalPinyin(item)}
{item.tone}
{item.meaning} / {item.vi}
{item.examples.map(word => { const wordPinyin = getMarkedPinyin(word, ''); return ( ); })}
))}
))}
); } function ChineseBuildTab({ primary }) { const groupedCompositions = Object.entries(groupItemsByTopic(CHINESE_COMPOSITIONS)); return (

How radicals build characters by topic

Radicals often give a meaning hint. The other part may give sound, meaning, or both.

{groupedCompositions.map(([topic, items]) => (

{topic}

{items.map(item => { const resultPinyin = getMarkedPinyin(item.result, item.pinyin); return (
{item.parts.map((part, index) => ( {index === 0 && +} ))} =
{resultPinyin}
{item.meaning}

{item.note}

); })}
))}
); } function ChineseSentencesTab({ primary, sentenceIndex, setSentenceIndex, showEnglish, setShowEnglish }) { const current = CHINESE_SENTENCES[sentenceIndex] || CHINESE_SENTENCES[0]; const currentPinyin = getMarkedPinyin(current.zh, ''); const groupedSentences = Object.entries(groupItemsByTopic(CHINESE_SENTENCES.map((item, index) => ({ ...item, index })))); const goToSentence = (nextIndex) => { const normalized = (nextIndex + CHINESE_SENTENCES.length) % CHINESE_SENTENCES.length; setSentenceIndex(normalized); setShowEnglish(false); }; return (

Sentence practice

Practice with Chinese sentences, pinyin, and English meaning.

Sentence {sentenceIndex + 1}/{CHINESE_SENTENCES.length}
{current.topic}
{currentPinyin}
{showEnglish ? current.en : 'English hidden'}

Handwriting view

{groupedSentences.map(([topic, items]) => (

{topic}

{items.map(item => ( ))}
))}
); } function ChineseNotesTab({ primary, notes, activeTopicId, setActiveTopicId, updateNote }) { const activeTopic = CHINESE_NOTE_TOPICS.find(topic => topic.id === activeTopicId) || CHINESE_NOTE_TOPICS[0]; const relatedRadicals = activeTopic.ids && activeTopic.ids.length ? CHINESE_RADICALS.filter(item => activeTopic.ids.includes(item.id)) : []; const activeNote = notes[activeTopic.id] || ''; const filledNotes = CHINESE_NOTE_TOPICS.filter(topic => (notes[topic.id] || '').trim()); return (

Notes by topic

Notes are saved locally in this browser for quick review.

{activeTopic.label}

{activeTopic.vi}
Auto saved
{relatedRadicals.length > 0 && (
{relatedRadicals.map(item => ( ))}
)}