Spaces:
Sleeping
Sleeping
| // Application Data | |
| const appData = { | |
| modes: [ | |
| { | |
| id: "conversation", | |
| name: "المحادثة الحرة", | |
| icon: "💬", | |
| description: "دردشة مفتوحة مع Echo Light لممارسة المهارات اللغوية" | |
| }, | |
| { | |
| id: "vocabulary", | |
| name: "بناء المفردات", | |
| icon: "📚", | |
| description: "نظام بطاقات تعليمية ذكي مع تكرار مباعد" | |
| }, | |
| { | |
| id: "pronunciation", | |
| name: "تدريب النطق", | |
| icon: "🎤", | |
| description: "تقييم النطق الفوري مع تعليقات مفصلة" | |
| }, | |
| { | |
| id: "grammar", | |
| name: "ألعاب القواعد", | |
| icon: "🎯", | |
| description: "تمارين تفاعلية لتعلم القواعد بطريقة ممتعة" | |
| }, | |
| { | |
| id: "scenarios", | |
| name: "سيناريوهات واقعية", | |
| icon: "🌍", | |
| description: "محاكاة مواقف حياتية مثل المطاعم والمقابلات" | |
| }, | |
| { | |
| id: "daily", | |
| name: "التحدي اليومي", | |
| icon: "⭐", | |
| description: "تمارين يومية قصيرة للحفاظ على استمرارية التعلم" | |
| } | |
| ], | |
| achievements: [ | |
| { | |
| id: "first_conversation", | |
| name: "المحادثة الأولى", | |
| description: "أكمل أول محادثة مع Echo Light", | |
| icon: "🎉" | |
| }, | |
| { | |
| id: "vocabulary_master", | |
| name: "خبير المفردات", | |
| description: "تعلم 50 كلمة جديدة", | |
| icon: "🏆" | |
| }, | |
| { | |
| id: "pronunciation_expert", | |
| name: "خبير النطق", | |
| description: "احصل على تقييم ممتاز في النطق 10 مرات", | |
| icon: "🎯" | |
| }, | |
| { | |
| id: "grammar_guru", | |
| name: "معلم القواعد", | |
| description: "أكمل جميع تمارين القواعد بنجاح", | |
| icon: "📖" | |
| }, | |
| { | |
| id: "daily_streak", | |
| name: "المواظبة", | |
| description: "أكمل التحدي اليومي لمدة 7 أيام متتالية", | |
| icon: "🔥" | |
| } | |
| ], | |
| vocabulary_cards: [ | |
| { | |
| word: "Hello", | |
| translation: "مرحبا", | |
| pronunciation: "/həˈloʊ/", | |
| example: "Hello, how are you?" | |
| }, | |
| { | |
| word: "Thank you", | |
| translation: "شكراً لك", | |
| pronunciation: "/θæŋk juː/", | |
| example: "Thank you for your help." | |
| }, | |
| { | |
| word: "Beautiful", | |
| translation: "جميل", | |
| pronunciation: "/ˈbjuːtɪfəl/", | |
| example: "The sunset is beautiful." | |
| }, | |
| { | |
| word: "Important", | |
| translation: "مهم", | |
| pronunciation: "/ɪmˈpɔːrtənt/", | |
| example: "This is very important." | |
| }, | |
| { | |
| word: "Learning", | |
| translation: "تعلم", | |
| pronunciation: "/ˈlɜːrnɪŋ/", | |
| example: "Learning English is fun." | |
| } | |
| ], | |
| grammar_exercises: [ | |
| { | |
| question: "Choose the correct form: I ___ to school every day.", | |
| options: ["go", "goes", "going", "went"], | |
| correct: 0, | |
| explanation: "نستخدم 'go' مع الضمير 'I' في المضارع البسيط" | |
| }, | |
| { | |
| question: "Complete: She ___ reading a book now.", | |
| options: ["is", "are", "was", "were"], | |
| correct: 0, | |
| explanation: "نستخدم 'is' مع الضمير 'She' في المضارع المستمر" | |
| } | |
| ], | |
| scenarios: [ | |
| { | |
| title: "في المطعم", | |
| description: "تعلم كيفية طلب الطعام في المطعم", | |
| dialogue: [ | |
| { | |
| speaker: "waiter", | |
| text: "Good evening! Welcome to our restaurant. How can I help you?" | |
| }, | |
| { | |
| speaker: "customer", | |
| text: "Good evening! I'd like to see the menu, please." | |
| } | |
| ] | |
| }, | |
| { | |
| title: "مقابلة عمل", | |
| description: "تدرب على أسئلة مقابلة العمل الشائعة", | |
| dialogue: [ | |
| { | |
| speaker: "interviewer", | |
| text: "Tell me about yourself." | |
| }, | |
| { | |
| speaker: "candidate", | |
| text: "I'm a motivated professional with experience in..." | |
| } | |
| ] | |
| } | |
| ], | |
| echoResponses: [ | |
| "That's wonderful! Your English is getting better every day.", | |
| "Great job! Can you tell me more about that topic?", | |
| "Excellent! I love how you expressed that idea.", | |
| "Perfect! Let's try using some advanced vocabulary now.", | |
| "Amazing progress! What would you like to talk about next?", | |
| "Fantastic! Your grammar is really improving.", | |
| "Well done! That was a complete and clear sentence.", | |
| "Impressive! You're becoming more confident in English.", | |
| "Brilliant! Let's explore this topic further.", | |
| "Outstanding! Keep up the great work!" | |
| ] | |
| }; | |
| // Application State | |
| const appState = { | |
| currentScreen: 'homeScreen', | |
| userProgress: { | |
| streak: 5, | |
| totalPoints: 125, | |
| wordsLearned: 12, | |
| conversationsCompleted: 3, | |
| pronunciationAccuracy: 78, | |
| grammarScore: 85, | |
| unlockedAchievements: ['first_conversation'] | |
| }, | |
| currentVocabIndex: 0, | |
| currentGrammarIndex: 0, | |
| currentScenario: null, | |
| isRecording: false, | |
| soundEnabled: true, | |
| dailyProgress: 1, | |
| selectedChallengeWords: [], | |
| recordingTimer: null | |
| }; | |
| // Character Animation Controller | |
| class CharacterController { | |
| constructor() { | |
| this.character = document.getElementById('echoCharacter'); | |
| this.mouth = document.getElementById('characterMouth'); | |
| this.soundWaves = document.getElementById('soundWaves'); | |
| this.isAnimating = false; | |
| this.mouthStates = ['happy', 'speaking', 'surprised', 'neutral']; | |
| this.currentMoodIndex = 0; | |
| this.startIdleAnimation(); | |
| } | |
| startIdleAnimation() { | |
| // Eye blinking animation | |
| setInterval(() => { | |
| this.blink(); | |
| }, 3000 + Math.random() * 2000); | |
| // Mood changes | |
| setInterval(() => { | |
| this.changeMood(); | |
| }, 10000); | |
| } | |
| blink() { | |
| const eyes = document.querySelectorAll('.eye'); | |
| eyes.forEach(eye => { | |
| eye.style.transform = 'scaleY(0.1)'; | |
| setTimeout(() => { | |
| eye.style.transform = 'scaleY(1)'; | |
| }, 150); | |
| }); | |
| } | |
| changeMood() { | |
| if (this.isAnimating) return; | |
| const moods = ['happy', 'excited', 'thinking', 'encouraging']; | |
| const randomMood = moods[Math.floor(Math.random() * moods.length)]; | |
| this.setMood(randomMood); | |
| } | |
| setMood(mood) { | |
| const character = this.character; | |
| const mouth = this.mouth; | |
| // Remove existing mood classes | |
| character.classList.remove('happy', 'excited', 'thinking', 'encouraging'); | |
| character.classList.add(mood); | |
| switch(mood) { | |
| case 'happy': | |
| mouth.style.borderRadius = '0 0 40px 40px'; | |
| mouth.style.width = '40px'; | |
| mouth.style.background = '#ff6b9d'; | |
| break; | |
| case 'excited': | |
| mouth.style.borderRadius = '50%'; | |
| mouth.style.width = '30px'; | |
| mouth.style.background = '#32b8c6'; | |
| break; | |
| case 'thinking': | |
| mouth.style.borderRadius = '40px 40px 0 0'; | |
| mouth.style.width = '20px'; | |
| mouth.style.background = '#8a2be2'; | |
| break; | |
| case 'encouraging': | |
| mouth.style.borderRadius = '0 0 50px 50px'; | |
| mouth.style.width = '50px'; | |
| mouth.style.background = '#ff6b9d'; | |
| break; | |
| } | |
| } | |
| speak(text) { | |
| this.isAnimating = true; | |
| this.setMood('happy'); | |
| // Animate sound waves | |
| if (this.soundWaves) { | |
| this.soundWaves.style.display = 'block'; | |
| } | |
| // Simulate mouth movement | |
| let speakingInterval = setInterval(() => { | |
| const randomWidth = 20 + Math.random() * 30; | |
| if (this.mouth) { | |
| this.mouth.style.width = randomWidth + 'px'; | |
| } | |
| }, 150); | |
| // Use Web Speech API if available and enabled | |
| if ('speechSynthesis' in window && appState.soundEnabled) { | |
| const utterance = new SpeechSynthesisUtterance(text); | |
| utterance.lang = 'en-US'; | |
| utterance.rate = 0.9; | |
| utterance.pitch = 1.1; | |
| utterance.onend = () => { | |
| clearInterval(speakingInterval); | |
| this.stopSpeaking(); | |
| }; | |
| utterance.onerror = () => { | |
| clearInterval(speakingInterval); | |
| this.stopSpeaking(); | |
| }; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| // Fallback animation | |
| setTimeout(() => { | |
| clearInterval(speakingInterval); | |
| this.stopSpeaking(); | |
| }, Math.min(text.length * 50, 3000)); | |
| } | |
| } | |
| stopSpeaking() { | |
| this.isAnimating = false; | |
| if (this.soundWaves) { | |
| this.soundWaves.style.display = 'none'; | |
| } | |
| if (this.mouth) { | |
| this.mouth.style.width = '40px'; | |
| } | |
| this.setMood('happy'); | |
| } | |
| celebrate() { | |
| this.setMood('excited'); | |
| if (this.character) { | |
| this.character.style.animation = 'float 0.5s ease-in-out 3'; | |
| setTimeout(() => { | |
| this.character.style.animation = 'float 3s ease-in-out infinite'; | |
| this.setMood('happy'); | |
| }, 1500); | |
| } | |
| } | |
| encourage() { | |
| this.setMood('encouraging'); | |
| setTimeout(() => { | |
| this.setMood('happy'); | |
| }, 2000); | |
| } | |
| } | |
| // Initialize character controller | |
| let characterController; | |
| // Screen Management | |
| function showScreen(screenId) { | |
| // Hide all screens | |
| document.querySelectorAll('.screen').forEach(screen => { | |
| screen.classList.remove('active'); | |
| }); | |
| // Show target screen | |
| const targetScreen = document.getElementById(screenId); | |
| if (targetScreen) { | |
| targetScreen.classList.add('active'); | |
| appState.currentScreen = screenId; | |
| // Update character mood based on screen | |
| if (characterController) { | |
| switch(screenId) { | |
| case 'homeScreen': | |
| characterController.setMood('happy'); | |
| break; | |
| case 'conversationScreen': | |
| characterController.setMood('encouraging'); | |
| break; | |
| case 'vocabularyScreen': | |
| case 'grammarScreen': | |
| characterController.setMood('thinking'); | |
| break; | |
| case 'pronunciationScreen': | |
| characterController.setMood('excited'); | |
| break; | |
| case 'progressScreen': | |
| initializeAchievements(); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // Mode Management | |
| function initializeModes() { | |
| const modesGrid = document.getElementById('modesGrid'); | |
| if (!modesGrid) return; | |
| modesGrid.innerHTML = ''; | |
| appData.modes.forEach(mode => { | |
| const modeCard = document.createElement('div'); | |
| modeCard.className = 'mode-card'; | |
| modeCard.onclick = () => openMode(mode.id); | |
| modeCard.innerHTML = ` | |
| <span class="mode-icon">${mode.icon}</span> | |
| <h3 class="mode-title">${mode.name}</h3> | |
| <p class="mode-description">${mode.description}</p> | |
| `; | |
| modesGrid.appendChild(modeCard); | |
| }); | |
| } | |
| function openMode(modeId) { | |
| const screenMap = { | |
| 'conversation': 'conversationScreen', | |
| 'vocabulary': 'vocabularyScreen', | |
| 'pronunciation': 'pronunciationScreen', | |
| 'grammar': 'grammarScreen', | |
| 'scenarios': 'scenariosScreen', | |
| 'daily': 'dailyScreen' | |
| }; | |
| const screenId = screenMap[modeId]; | |
| if (screenId) { | |
| showScreen(screenId); | |
| // Initialize mode-specific content | |
| switch(modeId) { | |
| case 'vocabulary': | |
| initializeVocabulary(); | |
| break; | |
| case 'grammar': | |
| initializeGrammar(); | |
| break; | |
| case 'scenarios': | |
| initializeScenarios(); | |
| break; | |
| case 'daily': | |
| initializeDailyChallenge(); | |
| break; | |
| case 'pronunciation': | |
| initializePronunciation(); | |
| break; | |
| } | |
| } | |
| } | |
| // Conversation Mode | |
| function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| if (!input) return; | |
| const message = input.value.trim(); | |
| if (!message) return; | |
| addMessage(message, 'user'); | |
| input.value = ''; | |
| // استدعاء الذكاء الاصطناعي عبر الخادم | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({message}) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| addMessage(data.reply, 'echo'); | |
| if (characterController) characterController.speak(data.reply); | |
| }) | |
| .catch(() => { | |
| addMessage("Sorry, I couldn't connect to the AI server.", 'echo'); | |
| }); | |
| // تحديث النقاط كما هو | |
| appState.userProgress.conversationsCompleted++; | |
| appState.userProgress.totalPoints += 25; | |
| updateProgressDisplay(); | |
| } | |
| function addMessage(text, sender) { | |
| const messagesContainer = document.getElementById('chatMessages'); | |
| if (!messagesContainer) return; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${sender}`; | |
| messageDiv.textContent = text; | |
| messagesContainer.appendChild(messageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| } | |
| function startVoiceInput() { | |
| if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = 'en-US'; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| recognition.onresult = function(event) { | |
| const transcript = event.results[0][0].transcript; | |
| const input = document.getElementById('messageInput'); | |
| if (input) { | |
| input.value = transcript; | |
| } | |
| }; | |
| recognition.onerror = function(event) { | |
| console.log('Speech recognition error:', event.error); | |
| alert('حدث خطأ في التعرف على الصوت. تأكد من السماح بالوصول للميكروفون.'); | |
| }; | |
| recognition.start(); | |
| } else { | |
| alert('متصفحك لا يدعم تقنية التعرف على الصوت.'); | |
| } | |
| } | |
| // Vocabulary Mode | |
| function initializeVocabulary() { | |
| appState.currentVocabIndex = 0; | |
| updateVocabularyCard(); | |
| updateVocabularyProgress(); | |
| } | |
| function updateVocabularyCard() { | |
| const card = appData.vocabulary_cards[appState.currentVocabIndex]; | |
| const elements = { | |
| 'cardWord': card.word, | |
| 'cardPronunciation': card.pronunciation, | |
| 'cardTranslation': card.translation, | |
| 'cardExample': card.example | |
| }; | |
| Object.entries(elements).forEach(([id, value]) => { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| element.textContent = value; | |
| } | |
| }); | |
| // Reset card flip | |
| const flashcard = document.getElementById('flashcard'); | |
| if (flashcard) { | |
| flashcard.classList.remove('flipped'); | |
| } | |
| } | |
| function updateVocabularyProgress() { | |
| const progress = ((appState.currentVocabIndex + 1) / appData.vocabulary_cards.length) * 100; | |
| const progressFill = document.getElementById('vocabProgress'); | |
| const progressText = document.getElementById('vocabProgressText'); | |
| if (progressFill) { | |
| progressFill.style.width = progress + '%'; | |
| } | |
| if (progressText) { | |
| progressText.textContent = `${appState.currentVocabIndex + 1}/${appData.vocabulary_cards.length}`; | |
| } | |
| } | |
| function flipCard() { | |
| const flashcard = document.getElementById('flashcard'); | |
| if (flashcard) { | |
| flashcard.classList.toggle('flipped'); | |
| } | |
| } | |
| function nextCard() { | |
| appState.currentVocabIndex = (appState.currentVocabIndex + 1) % appData.vocabulary_cards.length; | |
| updateVocabularyCard(); | |
| updateVocabularyProgress(); | |
| // Update progress | |
| appState.userProgress.wordsLearned++; | |
| appState.userProgress.totalPoints += 10; | |
| updateProgressDisplay(); | |
| if (characterController) { | |
| characterController.encourage(); | |
| } | |
| } | |
| function playPronunciation() { | |
| const card = appData.vocabulary_cards[appState.currentVocabIndex]; | |
| if (characterController && appState.soundEnabled) { | |
| characterController.speak(card.word); | |
| } | |
| } | |
| // Pronunciation Mode | |
| function initializePronunciation() { | |
| const words = appData.vocabulary_cards; | |
| const randomWord = words[Math.floor(Math.random() * words.length)]; | |
| const practiceWord = document.getElementById('practiceWord'); | |
| const practicePronunciation = document.getElementById('practicePronunciation'); | |
| if (practiceWord) practiceWord.textContent = randomWord.word; | |
| if (practicePronunciation) practicePronunciation.textContent = randomWord.pronunciation; | |
| // Clear previous feedback | |
| const feedback = document.getElementById('pronunciationFeedback'); | |
| if (feedback) { | |
| feedback.innerHTML = ''; | |
| } | |
| } | |
| function playTargetPronunciation() { | |
| const word = document.getElementById('practiceWord'); | |
| if (word && characterController && appState.soundEnabled) { | |
| characterController.speak(word.textContent); | |
| } | |
| } | |
| function toggleRecording() { | |
| const recordBtn = document.getElementById('recordBtn'); | |
| const visualizer = document.getElementById('recordVisualizer'); | |
| if (!recordBtn || !visualizer) return; | |
| if (!appState.isRecording) { | |
| // Start recording | |
| appState.isRecording = true; | |
| recordBtn.textContent = '⏹️ إيقاف التسجيل'; | |
| recordBtn.classList.add('recording'); | |
| // Clear previous content | |
| visualizer.innerHTML = ''; | |
| // Create recording visualization | |
| const bars = []; | |
| for (let i = 0; i < 10; i++) { | |
| const bar = document.createElement('div'); | |
| bar.style.cssText = ` | |
| width: 15px; | |
| background: #32b8c6; | |
| margin: 2px; | |
| border-radius: 2px; | |
| display: inline-block; | |
| height: 20px; | |
| animation: record-bar 0.8s infinite alternate; | |
| animation-delay: ${i * 0.1}s; | |
| `; | |
| bars.push(bar); | |
| visualizer.appendChild(bar); | |
| } | |
| // Add CSS for bars animation if not exists | |
| if (!document.getElementById('recordBarStyles')) { | |
| const style = document.createElement('style'); | |
| style.id = 'recordBarStyles'; | |
| style.textContent = ` | |
| @keyframes record-bar { | |
| 0% { height: 10px; opacity: 0.5; } | |
| 100% { height: 50px; opacity: 1; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| // Auto-stop after 5 seconds | |
| appState.recordingTimer = setTimeout(() => { | |
| if (appState.isRecording) { | |
| toggleRecording(); | |
| } | |
| }, 5000); | |
| } else { | |
| // Stop recording | |
| appState.isRecording = false; | |
| recordBtn.textContent = '🎤 ابدأ التسجيل'; | |
| recordBtn.classList.remove('recording'); | |
| // Clear timer | |
| if (appState.recordingTimer) { | |
| clearTimeout(appState.recordingTimer); | |
| appState.recordingTimer = null; | |
| } | |
| // Clear visualizer | |
| visualizer.innerHTML = ''; | |
| // Show processing message then feedback | |
| const feedback = document.getElementById('pronunciationFeedback'); | |
| if (feedback) { | |
| feedback.innerHTML = '<p>جاري تحليل النطق...</p>'; | |
| setTimeout(() => { | |
| showPronunciationFeedback(); | |
| }, 1500); | |
| } | |
| } | |
| } | |
| function showPronunciationFeedback() { | |
| const feedback = document.getElementById('pronunciationFeedback'); | |
| if (!feedback) return; | |
| const accuracy = 65 + Math.random() * 30; // Simulate accuracy between 65-95% | |
| const roundedAccuracy = Math.round(accuracy); | |
| let message, color; | |
| if (roundedAccuracy >= 85) { | |
| message = 'ممتاز! نطقك رائع 🎉'; | |
| color = '#32b8c6'; | |
| } else if (roundedAccuracy >= 70) { | |
| message = 'جيد جداً! يمكنك تحسينه أكثر 👍'; | |
| color = '#8a2be2'; | |
| } else { | |
| message = 'يحتاج تحسين، استمع للنطق الصحيح 🎯'; | |
| color = '#ff6b9d'; | |
| } | |
| feedback.innerHTML = ` | |
| <h4>نتيجة النطق</h4> | |
| <div style="font-size: 3em; color: ${color}; margin: 16px 0; text-shadow: 0 0 10px ${color}40;"> | |
| ${roundedAccuracy}% | |
| </div> | |
| <p style="color: ${color}; font-weight: bold;">${message}</p> | |
| <button class="btn btn--primary" onclick="initializePronunciation()">كلمة جديدة</button> | |
| `; | |
| // Update progress | |
| appState.userProgress.pronunciationAccuracy = Math.round( | |
| (appState.userProgress.pronunciationAccuracy + accuracy) / 2 | |
| ); | |
| appState.userProgress.totalPoints += Math.round(accuracy / 10); | |
| updateProgressDisplay(); | |
| if (characterController) { | |
| if (roundedAccuracy >= 85) { | |
| characterController.celebrate(); | |
| } else { | |
| characterController.encourage(); | |
| } | |
| } | |
| } | |
| // Grammar Mode | |
| function initializeGrammar() { | |
| appState.currentGrammarIndex = 0; | |
| showGrammarQuestion(); | |
| } | |
| function showGrammarQuestion() { | |
| const exercise = appData.grammar_exercises[appState.currentGrammarIndex]; | |
| const questionElement = document.getElementById('grammarQuestion'); | |
| if (questionElement) { | |
| questionElement.textContent = exercise.question; | |
| } | |
| const optionsContainer = document.getElementById('grammarOptions'); | |
| if (optionsContainer) { | |
| optionsContainer.innerHTML = ''; | |
| exercise.options.forEach((option, index) => { | |
| const optionBtn = document.createElement('button'); | |
| optionBtn.className = 'option-btn'; | |
| optionBtn.textContent = option; | |
| optionBtn.onclick = () => selectGrammarOption(index); | |
| optionsContainer.appendChild(optionBtn); | |
| }); | |
| } | |
| // Clear previous feedback | |
| const feedback = document.getElementById('grammarFeedback'); | |
| if (feedback) { | |
| feedback.innerHTML = ''; | |
| } | |
| } | |
| function selectGrammarOption(selectedIndex) { | |
| const exercise = appData.grammar_exercises[appState.currentGrammarIndex]; | |
| const options = document.querySelectorAll('.option-btn'); | |
| const feedback = document.getElementById('grammarFeedback'); | |
| if (!feedback) return; | |
| // Disable all options | |
| options.forEach((btn, index) => { | |
| btn.disabled = true; | |
| if (index === exercise.correct) { | |
| btn.classList.add('correct'); | |
| } else if (index === selectedIndex && index !== exercise.correct) { | |
| btn.classList.add('incorrect'); | |
| } | |
| }); | |
| // Show feedback | |
| const isCorrect = selectedIndex === exercise.correct; | |
| feedback.innerHTML = ` | |
| <h4>${isCorrect ? '✅ إجابة صحيحة!' : '❌ إجابة خاطئة'}</h4> | |
| <p>${exercise.explanation}</p> | |
| <button class="btn btn--primary" onclick="nextGrammarQuestion()">السؤال التالي</button> | |
| `; | |
| // Update progress | |
| if (isCorrect) { | |
| appState.userProgress.grammarScore += 10; | |
| appState.userProgress.totalPoints += 15; | |
| updateProgressDisplay(); | |
| if (characterController) { | |
| characterController.celebrate(); | |
| } | |
| } else { | |
| if (characterController) { | |
| characterController.encourage(); | |
| } | |
| } | |
| } | |
| function nextGrammarQuestion() { | |
| appState.currentGrammarIndex = (appState.currentGrammarIndex + 1) % appData.grammar_exercises.length; | |
| showGrammarQuestion(); | |
| } | |
| // Scenarios Mode | |
| function initializeScenarios() { | |
| const buttonsContainer = document.getElementById('scenarioButtons'); | |
| if (!buttonsContainer) return; | |
| buttonsContainer.innerHTML = ''; | |
| appData.scenarios.forEach((scenario, index) => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'scenario-btn'; | |
| btn.textContent = scenario.title; | |
| btn.onclick = () => showScenario(index); | |
| buttonsContainer.appendChild(btn); | |
| }); | |
| // Show first scenario by default | |
| if (appData.scenarios.length > 0) { | |
| showScenario(0); | |
| } | |
| } | |
| function showScenario(scenarioIndex) { | |
| const scenario = appData.scenarios[scenarioIndex]; | |
| appState.currentScenario = scenarioIndex; | |
| // Update button states | |
| document.querySelectorAll('.scenario-btn').forEach((btn, index) => { | |
| btn.classList.toggle('active', index === scenarioIndex); | |
| }); | |
| // Show dialogue | |
| const dialogueContainer = document.getElementById('scenarioDialogue'); | |
| if (dialogueContainer) { | |
| dialogueContainer.innerHTML = ` | |
| <h4>${scenario.title}</h4> | |
| <p>${scenario.description}</p> | |
| <div class="dialogue-content"> | |
| ${scenario.dialogue.map(line => ` | |
| <div class="dialogue-line ${line.speaker}"> | |
| <strong>${line.speaker === 'waiter' ? 'النادل:' : | |
| line.speaker === 'customer' ? 'الزبون:' : | |
| line.speaker === 'interviewer' ? 'المحاور:' : 'المرشح:'}</strong> | |
| <p>${line.text}</p> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <button class="btn btn--primary" onclick="practiceScenario()">🎤 تدرب على هذا السيناريو</button> | |
| `; | |
| } | |
| } | |
| function practiceScenario() { | |
| if (characterController && appState.currentScenario !== null) { | |
| const scenario = appData.scenarios[appState.currentScenario]; | |
| const firstLine = scenario.dialogue[0].text; | |
| characterController.speak(firstLine); | |
| } | |
| // Show practice message | |
| const dialogueContainer = document.getElementById('scenarioDialogue'); | |
| if (dialogueContainer) { | |
| const practiceDiv = document.createElement('div'); | |
| practiceDiv.style.cssText = ` | |
| background: rgba(50, 184, 198, 0.1); | |
| border: 1px solid #32b8c6; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-top: 16px; | |
| text-align: center; | |
| `; | |
| practiceDiv.innerHTML = '<p>🎤 الآن دورك! تدرب على الرد باللغة الإنجليزية</p>'; | |
| dialogueContainer.appendChild(practiceDiv); | |
| } | |
| } | |
| // Daily Challenge Mode | |
| function initializeDailyChallenge() { | |
| appState.dailyProgress = 1; | |
| appState.selectedChallengeWords = []; | |
| updateDailyProgress(); | |
| startChallengeTimer(); | |
| } | |
| function updateDailyProgress() { | |
| const progress = (appState.dailyProgress / 3) * 100; | |
| const progressFill = document.getElementById('dailyProgress'); | |
| const progressText = document.getElementById('dailyProgressText'); | |
| if (progressFill) { | |
| progressFill.style.width = progress + '%'; | |
| } | |
| if (progressText) { | |
| progressText.textContent = `${appState.dailyProgress}/3`; | |
| } | |
| } | |
| function selectWord(wordElement) { | |
| const word = wordElement.textContent; | |
| // Toggle selection | |
| if (wordElement.classList.contains('selected')) { | |
| wordElement.classList.remove('selected'); | |
| appState.selectedChallengeWords = appState.selectedChallengeWords.filter(w => w !== word); | |
| } else { | |
| // Limit to 2 selections | |
| if (appState.selectedChallengeWords.length < 2) { | |
| wordElement.classList.add('selected'); | |
| appState.selectedChallengeWords.push(word); | |
| } | |
| } | |
| // Check if challenge is complete | |
| if (appState.selectedChallengeWords.length === 2) { | |
| setTimeout(() => { | |
| checkChallengeAnswer(); | |
| }, 500); | |
| } | |
| } | |
| function checkChallengeAnswer() { | |
| const correctWords = ['study', 'important']; | |
| const isCorrect = correctWords.every(word => appState.selectedChallengeWords.includes(word)); | |
| if (isCorrect) { | |
| appState.dailyProgress++; | |
| updateDailyProgress(); | |
| if (characterController) { | |
| characterController.celebrate(); | |
| } | |
| // Update streak and points | |
| appState.userProgress.streak++; | |
| appState.userProgress.totalPoints += 20; | |
| updateProgressDisplay(); | |
| if (appState.dailyProgress >= 3) { | |
| setTimeout(() => { | |
| alert('🎉 أكملت التحدي اليومي! أحسنت!'); | |
| checkAchievements(); | |
| }, 1000); | |
| } else { | |
| setTimeout(() => { | |
| alert('✅ إجابة صحيحة! استمر...'); | |
| }, 500); | |
| } | |
| } else { | |
| if (characterController) { | |
| characterController.encourage(); | |
| } | |
| setTimeout(() => { | |
| alert('❌ حاول مرة أخرى!'); | |
| }, 500); | |
| } | |
| // Reset selections | |
| document.querySelectorAll('.word-option').forEach(option => { | |
| option.classList.remove('selected'); | |
| }); | |
| appState.selectedChallengeWords = []; | |
| } | |
| function startChallengeTimer() { | |
| let timeLeft = 300; // 5 minutes in seconds | |
| const timerElement = document.getElementById('challengeTimer'); | |
| if (!timerElement) return; | |
| const timer = setInterval(() => { | |
| const minutes = Math.floor(timeLeft / 60); | |
| const seconds = timeLeft % 60; | |
| timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| timeLeft--; | |
| if (timeLeft < 0) { | |
| clearInterval(timer); | |
| alert('⏰ انتهى الوقت! حاول مرة أخرى غداً.'); | |
| } | |
| }, 1000); | |
| } | |
| // Progress Tracking | |
| function updateProgressDisplay() { | |
| const elements = { | |
| 'streakCount': appState.userProgress.streak, | |
| 'totalPoints': appState.userProgress.totalPoints, | |
| 'wordsLearned': appState.userProgress.wordsLearned, | |
| 'conversationsCompleted': appState.userProgress.conversationsCompleted, | |
| 'pronunciationAccuracy': appState.userProgress.pronunciationAccuracy + '%', | |
| 'grammarScore': appState.userProgress.grammarScore | |
| }; | |
| Object.entries(elements).forEach(([id, value]) => { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| element.textContent = value; | |
| } | |
| }); | |
| } | |
| // Achievements System | |
| function checkAchievements() { | |
| const progress = appState.userProgress; | |
| const achievements = [ | |
| { | |
| id: 'first_conversation', | |
| condition: progress.conversationsCompleted >= 1 | |
| }, | |
| { | |
| id: 'vocabulary_master', | |
| condition: progress.wordsLearned >= 50 | |
| }, | |
| { | |
| id: 'pronunciation_expert', | |
| condition: progress.pronunciationAccuracy >= 85 | |
| }, | |
| { | |
| id: 'grammar_guru', | |
| condition: progress.grammarScore >= 100 | |
| }, | |
| { | |
| id: 'daily_streak', | |
| condition: progress.streak >= 7 | |
| } | |
| ]; | |
| achievements.forEach(achievement => { | |
| if (achievement.condition && !progress.unlockedAchievements.includes(achievement.id)) { | |
| unlockAchievement(achievement.id); | |
| } | |
| }); | |
| } | |
| function unlockAchievement(achievementId) { | |
| const achievement = appData.achievements.find(a => a.id === achievementId); | |
| if (!achievement) return; | |
| appState.userProgress.unlockedAchievements.push(achievementId); | |
| appState.userProgress.totalPoints += 50; | |
| updateProgressDisplay(); | |
| // Show achievement notification | |
| showAchievementNotification(achievement); | |
| if (characterController) { | |
| characterController.celebrate(); | |
| } | |
| } | |
| function showAchievementNotification(achievement) { | |
| const notification = document.createElement('div'); | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: linear-gradient(135deg, #32b8c6, #8a2be2); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 12px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| z-index: 1000; | |
| animation: slideIn 0.5s ease-out; | |
| max-width: 300px; | |
| `; | |
| notification.innerHTML = ` | |
| <div style="font-size: 2em; margin-bottom: 8px; text-align: center;">${achievement.icon}</div> | |
| <h4 style="margin: 0 0 8px 0; text-align: center;">إنجاز جديد!</h4> | |
| <p style="margin: 0; font-size: 14px; text-align: center;">${achievement.name}</p> | |
| `; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.remove(); | |
| }, 5000); | |
| } | |
| function initializeAchievements() { | |
| const achievementsGrid = document.getElementById('achievementsGrid'); | |
| if (!achievementsGrid) return; | |
| achievementsGrid.innerHTML = ''; | |
| appData.achievements.forEach(achievement => { | |
| const isUnlocked = appState.userProgress.unlockedAchievements.includes(achievement.id); | |
| const achievementCard = document.createElement('div'); | |
| achievementCard.className = `achievement-card ${isUnlocked ? 'unlocked' : ''}`; | |
| achievementCard.innerHTML = ` | |
| <span class="achievement-icon">${achievement.icon}</span> | |
| <div class="achievement-name">${achievement.name}</div> | |
| <div class="achievement-description">${achievement.description}</div> | |
| `; | |
| achievementsGrid.appendChild(achievementCard); | |
| }); | |
| } | |
| // Sound Management | |
| function toggleSound() { | |
| appState.soundEnabled = !appState.soundEnabled; | |
| const soundToggle = document.getElementById('soundToggle'); | |
| if (soundToggle) { | |
| soundToggle.textContent = appState.soundEnabled ? '🔊' : '🔇'; | |
| soundToggle.title = appState.soundEnabled ? 'إيقاف الصوت' : 'تشغيل الصوت'; | |
| // Visual feedback | |
| soundToggle.style.color = appState.soundEnabled ? '#32b8c6' : '#ff6b9d'; | |
| } | |
| if (!appState.soundEnabled && 'speechSynthesis' in window) { | |
| speechSynthesis.cancel(); | |
| } | |
| // Show feedback message | |
| const message = appState.soundEnabled ? 'تم تشغيل الصوت' : 'تم إيقاف الصوت'; | |
| showTemporaryMessage(message); | |
| } | |
| function showTemporaryMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.style.cssText = ` | |
| position: fixed; | |
| bottom: 100px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| z-index: 1000; | |
| animation: slideIn 0.5s ease-out; | |
| `; | |
| messageDiv.textContent = message; | |
| document.body.appendChild(messageDiv); | |
| setTimeout(() => { | |
| messageDiv.remove(); | |
| }, 2000); | |
| } | |
| // Add slide-in animation CSS | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // === Conversational Voice Loop & Avatar State === | |
| let isMuted = false; | |
| let isConversationActive = false; | |
| let isAutoLoop = false; | |
| let recognition = null; | |
| let isSpeakingVoiceLoop = false; // حماية من التكرار | |
| function setVoiceControlButtonsState() { | |
| const btnStart = document.querySelector('.voice-controls .btn--primary'); | |
| const btnMute = document.querySelector('.voice-controls .btn--secondary'); | |
| const btnEnd = document.querySelector('.voice-controls .btn--danger'); | |
| if (!btnStart || !btnMute || !btnEnd) return; | |
| btnStart.disabled = isConversationActive || isSpeakingVoiceLoop; | |
| btnEnd.disabled = !isConversationActive && !isSpeakingVoiceLoop; | |
| btnMute.disabled = !isConversationActive && !isSpeakingVoiceLoop; | |
| btnMute.textContent = isMuted ? '🔇 كتم' : '🔊 كتم'; | |
| btnMute.classList.toggle('muted', isMuted); | |
| } | |
| function setAvatarState(state) { | |
| if (!characterController) return; | |
| switch(state) { | |
| case 'idle': | |
| characterController.setMood('happy'); | |
| break; | |
| case 'listening': | |
| characterController.setMood('encouraging'); | |
| break; | |
| case 'speaking': | |
| characterController.setMood('excited'); | |
| break; | |
| } | |
| } | |
| function startVoiceLoop() { | |
| if (isMuted || isConversationActive || isSpeakingVoiceLoop) return; | |
| isConversationActive = true; | |
| isAutoLoop = true; | |
| setAvatarState('listening'); | |
| setVoiceControlButtonsState(); | |
| startListening(); | |
| } | |
| function stopVoiceLoop() { | |
| isConversationActive = false; | |
| isAutoLoop = false; | |
| if (recognition) recognition.stop(); | |
| if ('speechSynthesis' in window) speechSynthesis.cancel(); | |
| isSpeakingVoiceLoop = false; | |
| setAvatarState('idle'); | |
| setVoiceControlButtonsState(); | |
| } | |
| function toggleMute() { | |
| isMuted = !isMuted; | |
| if (isMuted) { | |
| if (recognition) recognition.stop(); | |
| if ('speechSynthesis' in window) speechSynthesis.cancel(); | |
| setAvatarState('idle'); | |
| isConversationActive = false; | |
| isAutoLoop = false; | |
| isSpeakingVoiceLoop = false; | |
| } else if (!isConversationActive && !isSpeakingVoiceLoop) { | |
| isConversationActive = true; | |
| isAutoLoop = true; | |
| setAvatarState('listening'); | |
| startListening(); | |
| } | |
| setVoiceControlButtonsState(); | |
| } | |
| function startListening() { | |
| if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) { | |
| alert('Speech recognition not supported.'); | |
| return; | |
| } | |
| if (isSpeakingVoiceLoop) return; // لا تستمع أثناء النطق | |
| setAvatarState('listening'); | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| recognition = new SpeechRecognition(); | |
| recognition.lang = 'en-US'; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| recognition.onresult = function(event) { | |
| const transcript = event.results[0][0].transcript; | |
| addMessage(transcript, 'user'); | |
| handleAIResponse(transcript); | |
| }; | |
| recognition.onerror = function(event) { | |
| setAvatarState('idle'); | |
| if (isAutoLoop && !isMuted && isConversationActive) { | |
| setTimeout(startListening, 1000); | |
| } | |
| }; | |
| recognition.onend = function() { | |
| // Do nothing, will be restarted after AI response | |
| }; | |
| recognition.start(); | |
| } | |
| function handleAIResponse(userText) { | |
| setAvatarState('thinking'); | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({message: userText}) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| addMessage(data.reply, 'echo'); | |
| setAvatarState('speaking'); | |
| if (characterController && appState.soundEnabled) { | |
| const utterance = new SpeechSynthesisUtterance(data.reply); | |
| utterance.lang = 'en-US'; | |
| utterance.onend = () => { | |
| setAvatarState('idle'); | |
| if (isAutoLoop && !isMuted && isConversationActive) { | |
| setTimeout(startListening, 500); | |
| } | |
| }; | |
| speechSynthesis.speak(utterance); | |
| characterController.speak(data.reply); | |
| } else { | |
| setAvatarState('idle'); | |
| if (isAutoLoop && !isMuted && isConversationActive) { | |
| setTimeout(startListening, 500); | |
| } | |
| } | |
| }) | |
| .catch(() => { | |
| addMessage("Sorry, I couldn't connect to the AI server.", 'echo'); | |
| setAvatarState('idle'); | |
| if (isAutoLoop && !isMuted && isConversationActive) { | |
| setTimeout(startListening, 1500); | |
| } | |
| }); | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize character controller | |
| characterController = new CharacterController(); | |
| // Initialize modes | |
| initializeModes(); | |
| // Initialize achievements | |
| initializeAchievements(); | |
| // Update progress display | |
| updateProgressDisplay(); | |
| // Welcome message | |
| setTimeout(() => { | |
| if (characterController) { | |
| characterController.speak("Welcome to Echo Light! I'm here to help you learn English in a fun and interactive way."); | |
| } | |
| }, 2000); | |
| // Set up Enter key for chat | |
| const messageInput = document.getElementById('messageInput'); | |
| if (messageInput) { | |
| messageInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| sendMessage(); | |
| } | |
| }); | |
| } | |
| // Check for achievements periodically | |
| setInterval(() => { | |
| checkAchievements(); | |
| }, 10000); | |
| // ربط الأزرار يدويًا بعد تحميل الصفحة | |
| const btnStart = document.querySelector('.voice-controls .btn--primary'); | |
| const btnMute = document.querySelector('.voice-controls .btn--secondary'); | |
| const btnEnd = document.querySelector('.voice-controls .btn--danger'); | |
| if (btnStart) btnStart.onclick = startVoiceLoop; | |
| if (btnMute) btnMute.onclick = toggleMute; | |
| if (btnEnd) btnEnd.onclick = stopVoiceLoop; | |
| setVoiceControlButtonsState(); | |
| }); | |
| // Export functions for global access | |
| window.showScreen = showScreen; | |
| window.sendMessage = sendMessage; | |
| window.startVoiceInput = startVoiceInput; | |
| window.flipCard = flipCard; | |
| window.nextCard = nextCard; | |
| window.playPronunciation = playPronunciation; | |
| window.playTargetPronunciation = playTargetPronunciation; | |
| window.toggleRecording = toggleRecording; | |
| window.selectGrammarOption = selectGrammarOption; | |
| window.nextGrammarQuestion = nextGrammarQuestion; | |
| window.showScenario = showScenario; | |
| window.practiceScenario = practiceScenario; | |
| window.selectWord = selectWord; | |
| window.toggleSound = toggleSound; |