Hidden!
Say the word!
{aiSentence}
import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Timer, Zap, Play, ChevronRight, ChevronLeft, Trophy, Camera, EyeOff, Check, X, Loader2, Trash2, AlertCircle, Sparkles, Image as ImageIcon, RefreshCw, AlertTriangle, Plus, Heart, Stars, Gift, Rocket, Volume2, Wand2, Download, Settings, User, Hash, Save, MessageSquare, FastForward, Sparkle, Grid, Mail, Star, Mic, MicOff, Share, Upload } from 'lucide-react'; // --- DATA: ALL 24 LISTS UPDATED WITH CAPS FROM DOCUMENTS --- const WORD_LISTS = { "1": ["Look", "the", "to", "I", "come", "here", "me", "a", "am", "The", "Come", "at", "A", "on", "is", "Here"], "2": ["In", "my", "can", "are", "said", "this", "yes", "no", "be", "by", "and", "or", "Yes", "No"], "3": ["Away", "for", "he", "go", "I'm", "into", "it", "an", "not", "over", "put", "see", "He", "Into"], "4": ["She", "will", "Us", "too", "up", "from", "had", "you", "where", "big", "Will", "You", "Us", "We", "Where"], "5": ["after", "all", "but", "came", "can't", "get", "got", "good", "has", "her", "his", "down", "After", "Get", "But"], "6": ["have", "Make", "out", "saw", "Some", "of", "what", "it's", "off", "made", "Saw", "Did", "What", "Some"], "7": ["they", "stay", "very", "fun", "your", "was", "as", "with", "that", "if", "so", "do", "Stay", "That", "If", "So"], "8": ["about", "day", "eat", "let", "take", "there", "every", "new", "please", "home", "fast", "their", "Take", "There", "Please"], "9": ["find", "when", "these", "him", "again", "now", "back", "many", "way", "leg", "Were", "Find", "When"], "10": ["round", "boy", "men", "how", "other", "then", "well", "call", "hot", "old", "which", "girl", "How", "Then", "Which"], "11": ["only", "ten", "been", "play", "mum", "our", "first", "fly", "dad", "walk", "give", "Gave", "Only", "Fly"], "12": ["before", "each", "along", "fell", "until", "right", "more", "found", "want", "just", "same", "any", "Right", "Found"], "13": ["always", "friend", "keep", "could", "open", "Most", "know", "who", "time", "One", "Tell", "Ask"], "14": ["seen", "show", "those", "would", "left", "own", "much", "list", "say", "help", "soon", "red", "Show", "Soon"], "15": ["morning", "tree", "room", "night", "kind", "Read", "head", "May", "another", "live", "water", "people", "People"], "16": ["work", "once", "should", "under", "because", "house", "school", "long", "than", "why", "year", "today", "School", "House"], "17": ["learn", "paper", "hold", "Book", "class", "sun", "Spell", "Last", "bird", "knew", "start", "page"], "18": ["draw", "letter", "picture", "story", "end", "line", "sentence", "study", "front", "through", "sound", "word", "Story"], "19": ["earth", "Cold", "rain", "grow", "sea", "Air", "threw", "Ground", "Between", "Plant", "Star", "wind", "Wind"], "20": ["field", "land", "sky", "world", "flower", "month", "space", "fire", "autumn", "tomorrow", "race", "summer", "zero", "two", "moon", "blue"], "21": ["Ride", "Together", "bike", "Toy", "game", "season", "song", "ball", "Beautiful", "Three", "Music", "Watch", "Happy"], "22": ["doll", "party", "spring", "winter", "hour", "light", "dark", "minute", "second", "seven", "baby", "eye", "five", "white", "six", "purple"], "23": ["Don't", "feet", "Yesterday", "face", "Eight", "behind", "body", "parent", "children", "family", "Brother", "brown", "kids"], "24": ["love", "sister", "middle", "beginning", "woman", "mother", "father", "grey", "nine", "pink", "colour", "orange", "eleven", "silver", "twelve", "gold"] }; // --- SAMPLE SENTENCES DICTIONARY --- const SAMPLE_SENTENCES = { "look": "Look at the big dog! ๐ถ", "the": "The cat is sleeping. ๐ฑ", "to": "I walk to the park. ๐ณ", "i": "I love playing outside! โ๏ธ", "come": "Come and play with me. ๐", "here": "Here is your book. ๐", "me": "Can you help me? ๐ค", "a": "I saw a little bird. ๐ฆ", "am": "I am so happy today! ๐", "at": "Look at the funny monkey! ๐", "on": "The cup is on the table. โ", "is": "This game is fun! ๐ฎ", "in": "The toy is in the box. ๐ฆ", "my": "This is my favourite hat. ๐งข", "can": "I can jump very high! ๐ฆ", "are": "We are going to the beach. ๐๏ธ", "said": "She said hello to me. ๐", "this": "This is a great story. ๐", "yes": "Yes, we can go now! โ ", "no": "No, the dog is not inside. ๐", "be": "I want to be a superhero! ๐ฆธ", "by": "We sat by the big tree. ๐ณ", "and": "I like apples and bananas. ๐", "or": "Do you want red or blue? ๐จ", "away": "The bird flew away. ๐๏ธ", "for": "This present is for you. ๐", "he": "He is my best friend. ๐ฆ", "go": "Let's go to the shop. ๐", "i'm": "I'm going to run fast! ๐", "into": "The frog jumped into the pond. ๐ธ", "it": "It is a sunny day. โ๏ธ", "an": "I ate an apple. ๐", "not": "Do not touch the hot stove. ๐", "over": "The cow jumped over the moon. ๐", "put": "Put your toys away, please. ๐งธ", "see": "I see a rainbow! ๐", "she": "She likes to draw pictures. ๐๏ธ", "will": "I will help you. ๐", "us": "Come read with us. ๐", "too": "I like ice cream too! ๐ฆ", "up": "Look up at the sky. โ๏ธ", "from": "I got a letter from Grandma. ๐", "had": "We had a great time. ๐", "you": "You are a good reader. ๐", "where": "Where is my teddy bear? ๐งธ", "big": "That is a big elephant! ๐", "we": "We went to the park. ๐ณ", "after": "We can play after school. ๐ซ", "all": "I ate all my dinner. ๐ฝ๏ธ", "but": "I like pink, but I love purple! ๐", "came": "My friend came over to play. ๐ง", "can't": "I can't find my shoes. ๐", "get": "Can you get the ball? โฝ", "got": "I got a new bike! ๐ฒ", "good": "You did a good job! โญ", "has": "She has a little kitten. ๐", "her": "I like her pretty dress. ๐", "his": "That is his blue car. ๐", "down": "We walked down the hill. โฐ๏ธ", "have": "I have a yummy snack. ๐ฅช", "make": "Let's make a sandcastle! ๐๏ธ", "out": "We went out to play. โ๏ธ", "saw": "I saw a shiny star. โจ", "some": "Can I have some water? ๐ง", "of": "I want a piece of cake. ๐ฐ", "what": "What is your name? โ", "it's": "It's time to go home. ๐ ", "off": "Turn off the light, please. ๐ก", "made": "I made a beautiful painting. ๐จ", "did": "Did you see that? ๐", "they": "They are running fast. ๐", "stay": "Can you stay and play? ๐", "very": "I am very happy! ๐", "fun": "Reading is so much fun! ๐", "your": "Is this your pencil? โ๏ธ", "was": "The game was exciting! ๐ฎ", "as": "You are as quick as a flash! โก", "with": "I play with my sister. ๐ง", "that": "That is a silly joke! ๐", "if": "If it rains, we stay inside. ๐ง๏ธ", "so": "I am so proud of you! ๐", "do": "What do you like to play? ๐งฉ" }; const apiKey = ""; const App = () => { // --- Persisted State --- const [view, setView] = useState('landing'); const [gender, setGender] = useState(() => localStorage.getItem('swm_gender') || 'girl'); const [userInitials, setUserInitials] = useState(() => localStorage.getItem('swm_initials') || "Olivia"); const [listIdentifier, setListIdentifier] = useState(() => localStorage.getItem('swm_list') || "1"); const [challengeDuration, setChallengeDuration] = useState(() => parseInt(localStorage.getItem('swm_speed')) || 3); const [isListenModeEnabled, setIsListenModeEnabled] = useState(false); // Disabled feature const [customList, setCustomList] = useState(() => JSON.parse(localStorage.getItem('swm_custom_list') || "[]")); const [manualCustomWords, setManualCustomWords] = useState(customList.join(', ')); const [deck, setDeck] = useState(listIdentifier === 'Custom' ? customList : WORD_LISTS[listIdentifier] || WORD_LISTS["1"]); const [currentIndex, setCurrentIndex] = useState(0); const [error, setError] = useState(null); const [isExtractingWords, setIsExtractingWords] = useState(false); // --- Game Session State --- const [gamesThisSession, setGamesThisSession] = useState(0); const [rewardImage, setRewardImage] = useState(null); const [isGeneratingReward, setIsGeneratingReward] = useState(false); const [startTime, setStartTime] = useState(null); const [endTime, setEndTime] = useState(null); const [elapsedTime, setElapsedTime] = useState(0); const [showWord, setShowWord] = useState(true); const [score, setScore] = useState({ correct: 0, wrong: 0 }); const [wrongWords, setWrongWords] = useState([]); // --- AI States --- const [aiSentence, setAiSentence] = useState(null); const [sessionSummary, setSessionSummary] = useState(null); const [isLoadingSummary, setIsLoadingSummary] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const clockIntervalRef = useRef(null); const shareCanvasRef = useRef(null); // Sync Persistence useEffect(() => { localStorage.setItem('swm_gender', gender); localStorage.setItem('swm_initials', userInitials); localStorage.setItem('swm_list', listIdentifier); localStorage.setItem('swm_speed', challengeDuration.toString()); localStorage.setItem('swm_custom_list', JSON.stringify(customList)); if (view === 'landing' || view === 'settings') { setDeck(listIdentifier === 'Custom' && customList.length > 0 ? customList : (WORD_LISTS[listIdentifier] || WORD_LISTS["1"])); } }, [gender, userInitials, listIdentifier, challengeDuration, customList, view]); useEffect(() => { setManualCustomWords(customList.join(', ')); }, [customList]); // --- Timer Logic --- useEffect(() => { const isGame = ['speed-trial', 'hide-seek'].includes(view); // Removed 'practice' from timer if (isGame && startTime && !endTime) { clockIntervalRef.current = setInterval(() => { setElapsedTime((Date.now() - startTime) / 1000); }, 50); } else { if (clockIntervalRef.current) clearInterval(clockIntervalRef.current); } return () => { if (clockIntervalRef.current) clearInterval(clockIntervalRef.current); }; }, [view, startTime, endTime]); // --- Hide & Seek Logic --- useEffect(() => { let hideTimer; if (view === 'hide-seek' && !endTime) { setShowWord(true); hideTimer = setTimeout(() => { setShowWord(false); }, challengeDuration * 1000); } else { setShowWord(true); } return () => clearTimeout(hideTimer); }, [currentIndex, score.wrong, view, challengeDuration, endTime]); const theme = { primary: gender === 'girl' ? 'bg-pink-500' : 'bg-blue-600', primaryText: gender === 'girl' ? 'text-pink-500' : 'text-blue-600', accent: gender === 'girl' ? 'bg-pink-100' : 'bg-blue-100', accentText: gender === 'girl' ? 'text-pink-600' : 'text-blue-600', border: gender === 'girl' ? 'border-pink-100' : 'border-blue-100', shadow: gender === 'girl' ? 'shadow-pink-100' : 'shadow-blue-100', icon: gender === 'girl' ? Stars : Rocket }; const getDynamicFontSize = (word) => { if (!word) return 'text-7xl'; const len = word.length; if (len <= 4) return 'text-7xl md:text-9xl'; if (len <= 7) return 'text-6xl md:text-8xl'; return 'text-5xl md:text-7xl'; }; // --- Image Upload / OCR for Custom List --- const handleImageUpload = async (e) => { const file = e.target.files[0]; if (!file) return; setIsExtractingWords(true); try { const reader = new FileReader(); reader.onloadend = async () => { const base64String = reader.result.split(',')[1]; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ role: "user", parts: [ { text: "Extract ONLY the english sight words from this spelling list image. Ignore any headings, titles, dates, or numbers. Return the words as a simple comma-separated list. No other text." }, { inlineData: { mimeType: file.type, data: base64String } } ] }] }) }); const data = await response.json(); const text = data.candidates?.[0]?.content?.parts?.[0]?.text || ""; // Clean up response into array of words const words = text.split(/[\n,]+/).map(w => w.trim().replace(/[^a-zA-Z']/g, '')).filter(w => w.length > 0); if (words.length > 0) { setCustomList(words); setListIdentifier('Custom'); setDeck(words); } else { setError("Couldn't find words in that photo."); setTimeout(() => setError(null), 3000); } setIsExtractingWords(false); }; reader.readAsDataURL(file); } catch (err) { console.error(err); setIsExtractingWords(false); } }; const fetchAudioBlob = async (text, type = "short") => { if (!text) return null; try { const instruction = type === "summary" ? `Say in an encouraging female Australian teacher voice: ${text}` : `Say clearly and cheerfully: ${text}`; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: instruction }] }], generationConfig: { responseModalities: ["AUDIO"], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: "Aoede" } } } }, model: "gemini-2.5-flash-preview-tts" }) }); if (!response.ok) return null; const json = await response.json(); const base64 = json.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data; if (base64) { const bin = atob(base64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return URL.createObjectURL(new Blob([pcmToWav(bytes, 24000)], { type: 'audio/wav' })); } } catch (err) { console.error("Audio failed", err); } return null; }; const pcmToWav = (pcmData, sampleRate) => { const buffer = new ArrayBuffer(44 + pcmData.length); new Uint8Array(buffer, 44).set(pcmData); const view = new DataView(buffer); const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) view.setUint8(offset + i, string.charCodeAt(i)); }; writeString(0, 'RIFF'); view.setUint32(4, 32 + pcmData.length, true); writeString(8, 'WAVE'); writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); writeString(36, 'data'); view.setUint32(40, pcmData.length, true); return buffer; }; const playNativeFallback = (target) => { const utterance = new SpeechSynthesisUtterance(target); const voices = window.speechSynthesis.getVoices(); let femaleVoice = voices.find(v => (v.lang.includes('AU') || v.lang.includes('au')) && /(female|karen|catherine|matilda|chloe|natasha|samantha)/i.test(v.name) ); if (!femaleVoice) { femaleVoice = voices.find(v => /(female|karen|samantha|victoria|zira|catherine|moira|tessa)/i.test(v.name)); } if (!femaleVoice) { femaleVoice = voices.find(v => v.lang.includes('AU') || v.lang.includes('au')); } if (femaleVoice) utterance.voice = femaleVoice; utterance.lang = 'en-AU'; utterance.pitch = 1.2; utterance.rate = 0.85; utterance.onend = () => setIsSpeaking(false); utterance.onerror = () => setIsSpeaking(false); window.speechSynthesis.speak(utterance); }; const handleSoundOut = async (text = null) => { if (isSpeaking) window.speechSynthesis.cancel(); setIsSpeaking(true); const target = (text || deck[currentIndex] || "").toString(); // Unlock native TTS engine synchronously on user interaction const unlockUtterance = new SpeechSynthesisUtterance(''); unlockUtterance.volume = 0; window.speechSynthesis.speak(unlockUtterance); // Warm up an HTML5 Audio element synchronously const audio = new Audio(); audio.play().catch(() => {}); const url = await fetchAudioBlob(target); if (url) { audio.src = url; audio.onended = () => setIsSpeaking(false); audio.onerror = () => playNativeFallback(target); const playPromise = audio.play(); if (playPromise !== undefined) { playPromise.catch(err => { console.error("Audio error:", err); playNativeFallback(target); }); } } else { playNativeFallback(target); } }; const generateSessionSummary = async (stats) => { setIsLoadingSummary(true); setSessionSummary(null); const isPerfect = stats.wrong === 0; // Force AI variety by injecting a random Australian term into its instructions const aussieSlang = ["superstar", "champion", "little legend", "ripper", "spot on", "cracker", "top notch", "brilliant", "fantastic", "amazing"]; const randomSlang = aussieSlang[Math.floor(Math.random() * aussieSlang.length)]; let prompt; if (view === 'practice') { prompt = isPerfect ? `Student ${userInitials} got a PERFECT score practicing their sight words. Write a super exciting 1-sentence summary for a 6yo as a female Australian teacher. Make it unique by including the word/phrase "${randomSlang}". Do NOT mention the list number or time.` : `Student ${userInitials} finished practicing their sight words, but got ${stats.wrong} words wrong. Write a highly encouraging 1-sentence summary for a 6yo as a female Australian teacher, saying something like "Great try, here are the words to practice". Make it unique by including the word/phrase "${randomSlang}". Do NOT mention the list number or time.`; } else { prompt = isPerfect ? `Student ${userInitials} got a PERFECT score reading their sight words in ${stats.time.toFixed(1)}s. Write a super exciting 1-sentence summary for a 6yo as a female Australian teacher. Make it unique by including the word/phrase "${randomSlang}". Do NOT mention the list number.` : `Student ${userInitials} finished reading their sight words in ${stats.time.toFixed(1)}s, but got ${stats.wrong} words wrong. Write a highly encouraging 1-sentence summary for a 6yo as a female Australian teacher, saying something like "Great try, here are the words to practice". Make it unique by including the word/phrase "${randomSlang}". Do NOT mention the list number.`; } // 10 varied fallbacks for offline mode / fast loading const perfectFallbacks = [ `Top job ${userInitials}! You are a sight word superstar!`, `Amazing reading ${userInitials}! A perfect score!`, `Brilliant work ${userInitials}! You're reading so well!`, `What a ripper effort, ${userInitials}! Spot on!`, `You absolute champion, ${userInitials}! Perfect reading!`, `Cracker of a job, ${userInitials}! You got every single one!`, `Fantastic reading, ${userInitials}! You're a little legend!`, `Wow, ${userInitials}! You breezed right through those!`, `Superb work, ${userInitials}! You didn't miss a beat!`, `I'm so proud of you, ${userInitials}! Top notch reading!` ]; const practiceFallbacks = [ `Great try ${userInitials}! Let's keep practicing these words.`, `Good effort ${userInitials}! Here are a few words to work on.`, `You're doing so well ${userInitials}! Let's review these tricky ones.`, `Nice work, ${userInitials}! Just a couple of sneaky words to practice!`, `Top effort, ${userInitials}! Have another look at these ones.`, `You're getting so fast, ${userInitials}! Let's practice the tricky ones.`, `Awesome try, ${userInitials}! These words are tricky, but you'll get them!`, `Great reading, ${userInitials}! Let's polish up these last few words.`, `You're a star, ${userInitials}! Just a little bit more practice needed here.`, `Nearly there, ${userInitials}! Have a quick look at the words you missed.` ]; const fallbackText = isPerfect ? perfectFallbacks[Math.floor(Math.random() * perfectFallbacks.length)] : practiceFallbacks[Math.floor(Math.random() * practiceFallbacks.length)]; try { const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) }); const data = await resp.json(); const text = data.candidates?.[0]?.content?.parts?.[0]?.text || fallbackText; setSessionSummary(text); handleSoundOut(text); } catch (err) { setSessionSummary(fallbackText); handleSoundOut(fallbackText); } finally { setIsLoadingSummary(false); } }; // --- INSTANT SAMPLE SENTENCE --- const getAiSentence = (word) => { const w = word.toLowerCase().trim(); const sentence = SAMPLE_SENTENCES[w] || `Let's read the word "${word}" together! โจ`; setAiSentence(sentence); }; const generateRewardImage = async () => { setIsGeneratingReward(true); const girlTopics = [ "Cute magical fairy koala with sparkly wings", "Beautiful rainbow unicorn playing on a sunny beach", "Baby kangaroo picking bright pink and purple flowers", "Friendly mermaid swimming with happy dolphins", "Magical princess castle sitting on fluffy pink clouds", "Cute baby dragon baking cookies" ]; const boyTopics = [ "Cool space rocket flying past the moon and stars", "Friendly T-Rex dinosaur wearing cool sunglasses", "Super fast red racing car zooming on a track", "Happy little robot exploring a distant planet", "Brave superhero flying through the blue sky", "Pirate ship sailing on a bright blue ocean" ]; const topicsList = gender === 'girl' ? girlTopics : boyTopics; const selectedTopic = topicsList[Math.floor(Math.random() * topicsList.length)]; try { const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ instances: { prompt: `${selectedTopic}, whimsical bright children's book illustration style, highly detailed, joyful, 4k` }, parameters: { sampleCount: 1 } }) }); const data = await resp.json(); if (data.predictions?.[0]?.bytesBase64Encoded) { setRewardImage(`data:image/png;base64,${data.predictions[0].bytesBase64Encoded}`); } } catch (e) { console.error(e); } finally { setIsGeneratingReward(false); } }; const startMode = (mode) => { const activeList = listIdentifier === 'Custom' && customList.length > 0 ? customList : (WORD_LISTS[listIdentifier] || WORD_LISTS["1"]); const shuffled = [...activeList].sort(() => Math.random() - 0.5); setDeck(shuffled); setCurrentIndex(0); setScore({ correct: 0, wrong: 0 }); setWrongWords([]); setElapsedTime(0); setEndTime(null); setSessionSummary(null); setStartTime(Date.now()); setShowWord(true); if (mode === 'practice') { const w = shuffled[0].toLowerCase().trim(); setAiSentence(SAMPLE_SENTENCES[w] || `Let's read the word "${shuffled[0]}" together! โจ`); } else { setAiSentence(null); } setView(mode); }; const handleScoreWord = useCallback((isCorrect) => { if (endTime) return; setScore(prev => ({ correct: isCorrect ? prev.correct + 1 : prev.correct, wrong: !isCorrect ? prev.wrong + 1 : prev.wrong })); if (!isCorrect) { setWrongWords(prev => [...new Set([...prev, deck[currentIndex]])]); // Force the exact same word to reappear and give a fresh 3 seconds setShowWord(true); // STOP here so we do NOT advance to the next card! return; } if (currentIndex < deck.length - 1) { setCurrentIndex(c => c + 1); if (view === 'practice') { const nextWord = deck[currentIndex + 1]; const w = nextWord.toLowerCase().trim(); setAiSentence(SAMPLE_SENTENCES[w] || `Let's read the word "${nextWord}" together! โจ`); } else { setAiSentence(null); } } else { const finalTime = (Date.now() - startTime) / 1000; setEndTime(finalTime); const newGamesCount = gamesThisSession + 1; setGamesThisSession(newGamesCount); generateSessionSummary({ time: finalTime, correct: isCorrect ? score.correct + 1 : score.correct, wrong: !isCorrect ? score.wrong + 1 : score.wrong }); if (newGamesCount % 3 === 0) { generateRewardImage(); } setView('results'); } }, [endTime, deck, currentIndex, startTime, score, gamesThisSession, view]); const saveShareCard = () => { const canvas = shareCanvasRef.current; if (!canvas || !rewardImage) return; const ctx = canvas.getContext('2d'); const img = new Image(); img.src = rewardImage; img.onload = () => { canvas.width = 1080; canvas.height = 1350; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = gender === 'girl' ? '#fdf2f8' : '#eff6ff'; ctx.fillRect(0, 0, 1080, 1080); ctx.drawImage(img, 40, 40, 1000, 1000); ctx.fillStyle = gender === 'girl' ? '#db2777' : '#2563eb'; ctx.font = 'bold 70px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('SIGHT WORD MAGIC STAR!', 540, 1180); ctx.font = 'bold 50px sans-serif'; ctx.fillStyle = '#64748b'; ctx.fillText(`Student: ${userInitials}`, 540, 1260); const safeName = (userInitials.trim() || "Olivia").replace(/[^a-z0-9]/gi, '_'); const fileName = `${safeName}_Success.jpeg`; canvas.toBlob(async (blob) => { if (!blob) return; const file = new File([blob], fileName, { type: 'image/jpeg' }); if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { try { await navigator.share({ files: [file], title: 'Sight Word Star', text: `Look at ${userInitials}'s reading achievement!`, }); } catch (err) { console.log('Share canceled'); } } else { const dataUrl = canvas.toDataURL('image/jpeg', 0.9); const link = document.createElement('a'); link.download = fileName; link.href = dataUrl; link.click(); } }, 'image/jpeg', 0.9); }; }; const ListGrid = ({ current, onSelect }) => (
ยฉ 2026 Sight Word Magic. All Rights Reserved.
{error}
}{userInitials} โข {listIdentifier === 'Custom' ? "Custom List" : `List ${listIdentifier}`}
Student Name
setUserInitials(e.target.value)} className="w-full text-lg font-bold text-slate-700 outline-none" />Theme
Select Standard List (1-24)
Custom List
{/* Manual Text Entry */}Hide & Seek Speed
Magic Ear
SoonConfirmation mode
{view === 'hide-seek' ? `${challengeDuration}s Challenge` : view === 'practice' ? 'Practice' : 'Speed Trial'}
{view !== 'practice' && ({elapsedTime.toFixed(1)}s
)}{currentIndex + 1}/{deck.length}
Hidden!
Say the word!
{aiSentence}
{userInitials} โข {listIdentifier === 'Custom' ? "Custom List" : `List ${listIdentifier}`}
Thinking...
{`"${sessionSummary}"`}
Time
{endTime?.toFixed(1)}s
Correct
{score.correct}
Retry
{score.wrong}
Painting Reward...