Spaces:
Sleeping
Sleeping
Update ai_server.py
Browse files- ai_server.py +44 -1356
ai_server.py
CHANGED
|
@@ -1,1356 +1,44 @@
|
|
| 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 |
-
description: "أكمل أول محادثة مع Echo Light",
|
| 46 |
-
icon: "🎉"
|
| 47 |
-
},
|
| 48 |
-
{
|
| 49 |
-
id: "vocabulary_master",
|
| 50 |
-
name: "خبير المفردات",
|
| 51 |
-
description: "تعلم 50 كلمة جديدة",
|
| 52 |
-
icon: "🏆"
|
| 53 |
-
},
|
| 54 |
-
{
|
| 55 |
-
id: "pronunciation_expert",
|
| 56 |
-
name: "خبير النطق",
|
| 57 |
-
description: "احصل على تقييم ممتاز في النطق 10 مرات",
|
| 58 |
-
icon: "🎯"
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
id: "grammar_guru",
|
| 62 |
-
name: "معلم القواعد",
|
| 63 |
-
description: "أكمل جميع تمارين القواعد بنجاح",
|
| 64 |
-
icon: "📖"
|
| 65 |
-
},
|
| 66 |
-
{
|
| 67 |
-
id: "daily_streak",
|
| 68 |
-
name: "المواظبة",
|
| 69 |
-
description: "أكمل التحدي اليومي لمدة 7 أيام متتالية",
|
| 70 |
-
icon: "🔥"
|
| 71 |
-
}
|
| 72 |
-
],
|
| 73 |
-
vocabulary_cards: [
|
| 74 |
-
{
|
| 75 |
-
word: "Hello",
|
| 76 |
-
translation: "مرحبا",
|
| 77 |
-
pronunciation: "/həˈloʊ/",
|
| 78 |
-
example: "Hello, how are you?"
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
word: "Thank you",
|
| 82 |
-
translation: "شكراً لك",
|
| 83 |
-
pronunciation: "/θæŋk juː/",
|
| 84 |
-
example: "Thank you for your help."
|
| 85 |
-
},
|
| 86 |
-
{
|
| 87 |
-
word: "Beautiful",
|
| 88 |
-
translation: "جميل",
|
| 89 |
-
pronunciation: "/ˈbjuːtɪfəl/",
|
| 90 |
-
example: "The sunset is beautiful."
|
| 91 |
-
},
|
| 92 |
-
{
|
| 93 |
-
word: "Important",
|
| 94 |
-
translation: "مهم",
|
| 95 |
-
pronunciation: "/ɪmˈpɔːrtənt/",
|
| 96 |
-
example: "This is very important."
|
| 97 |
-
},
|
| 98 |
-
{
|
| 99 |
-
word: "Learning",
|
| 100 |
-
translation: "تعلم",
|
| 101 |
-
pronunciation: "/ˈlɜːrnɪŋ/",
|
| 102 |
-
example: "Learning English is fun."
|
| 103 |
-
}
|
| 104 |
-
],
|
| 105 |
-
grammar_exercises: [
|
| 106 |
-
{
|
| 107 |
-
question: "Choose the correct form: I ___ to school every day.",
|
| 108 |
-
options: ["go", "goes", "going", "went"],
|
| 109 |
-
correct: 0,
|
| 110 |
-
explanation: "نستخدم 'go' مع الضمير 'I' في المضارع البسيط"
|
| 111 |
-
},
|
| 112 |
-
{
|
| 113 |
-
question: "Complete: She ___ reading a book now.",
|
| 114 |
-
options: ["is", "are", "was", "were"],
|
| 115 |
-
correct: 0,
|
| 116 |
-
explanation: "نستخدم 'is' مع الضمير 'She' في المضارع المستمر"
|
| 117 |
-
}
|
| 118 |
-
],
|
| 119 |
-
scenarios: [
|
| 120 |
-
{
|
| 121 |
-
title: "في المطعم",
|
| 122 |
-
description: "تعلم كيفية طلب الطعام في المطعم",
|
| 123 |
-
dialogue: [
|
| 124 |
-
{
|
| 125 |
-
speaker: "waiter",
|
| 126 |
-
text: "Good evening! Welcome to our restaurant. How can I help you?"
|
| 127 |
-
},
|
| 128 |
-
{
|
| 129 |
-
speaker: "customer",
|
| 130 |
-
text: "Good evening! I'd like to see the menu, please."
|
| 131 |
-
}
|
| 132 |
-
]
|
| 133 |
-
},
|
| 134 |
-
{
|
| 135 |
-
title: "مقابلة عمل",
|
| 136 |
-
description: "تدرب على أسئلة مقابلة العمل الشائعة",
|
| 137 |
-
dialogue: [
|
| 138 |
-
{
|
| 139 |
-
speaker: "interviewer",
|
| 140 |
-
text: "Tell me about yourself."
|
| 141 |
-
},
|
| 142 |
-
{
|
| 143 |
-
speaker: "candidate",
|
| 144 |
-
text: "I'm a motivated professional with experience in..."
|
| 145 |
-
}
|
| 146 |
-
]
|
| 147 |
-
}
|
| 148 |
-
],
|
| 149 |
-
echoResponses: [
|
| 150 |
-
"That's wonderful! Your English is getting better every day.",
|
| 151 |
-
"Great job! Can you tell me more about that topic?",
|
| 152 |
-
"Excellent! I love how you expressed that idea.",
|
| 153 |
-
"Perfect! Let's try using some advanced vocabulary now.",
|
| 154 |
-
"Amazing progress! What would you like to talk about next?",
|
| 155 |
-
"Fantastic! Your grammar is really improving.",
|
| 156 |
-
"Well done! That was a complete and clear sentence.",
|
| 157 |
-
"Impressive! You're becoming more confident in English.",
|
| 158 |
-
"Brilliant! Let's explore this topic further.",
|
| 159 |
-
"Outstanding! Keep up the great work!"
|
| 160 |
-
]
|
| 161 |
-
};
|
| 162 |
-
|
| 163 |
-
// Application State
|
| 164 |
-
const appState = {
|
| 165 |
-
currentScreen: 'homeScreen',
|
| 166 |
-
userProgress: {
|
| 167 |
-
streak: 5,
|
| 168 |
-
totalPoints: 125,
|
| 169 |
-
wordsLearned: 12,
|
| 170 |
-
conversationsCompleted: 3,
|
| 171 |
-
pronunciationAccuracy: 78,
|
| 172 |
-
grammarScore: 85,
|
| 173 |
-
unlockedAchievements: ['first_conversation']
|
| 174 |
-
},
|
| 175 |
-
currentVocabIndex: 0,
|
| 176 |
-
currentGrammarIndex: 0,
|
| 177 |
-
currentScenario: null,
|
| 178 |
-
isRecording: false,
|
| 179 |
-
soundEnabled: true,
|
| 180 |
-
dailyProgress: 1,
|
| 181 |
-
selectedChallengeWords: [],
|
| 182 |
-
recordingTimer: null
|
| 183 |
-
};
|
| 184 |
-
|
| 185 |
-
// Character Animation Controller
|
| 186 |
-
class CharacterController {
|
| 187 |
-
constructor() {
|
| 188 |
-
this.character = document.getElementById('echoCharacter');
|
| 189 |
-
this.mouth = document.getElementById('characterMouth');
|
| 190 |
-
this.soundWaves = document.getElementById('soundWaves');
|
| 191 |
-
this.isAnimating = false;
|
| 192 |
-
this.mouthStates = ['happy', 'speaking', 'surprised', 'neutral'];
|
| 193 |
-
this.currentMoodIndex = 0;
|
| 194 |
-
|
| 195 |
-
this.startIdleAnimation();
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
startIdleAnimation() {
|
| 199 |
-
// Eye blinking animation
|
| 200 |
-
setInterval(() => {
|
| 201 |
-
this.blink();
|
| 202 |
-
}, 3000 + Math.random() * 2000);
|
| 203 |
-
|
| 204 |
-
// Mood changes
|
| 205 |
-
setInterval(() => {
|
| 206 |
-
this.changeMood();
|
| 207 |
-
}, 10000);
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
blink() {
|
| 211 |
-
const eyes = document.querySelectorAll('.eye');
|
| 212 |
-
eyes.forEach(eye => {
|
| 213 |
-
eye.style.transform = 'scaleY(0.1)';
|
| 214 |
-
setTimeout(() => {
|
| 215 |
-
eye.style.transform = 'scaleY(1)';
|
| 216 |
-
}, 150);
|
| 217 |
-
});
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
changeMood() {
|
| 221 |
-
if (this.isAnimating) return;
|
| 222 |
-
|
| 223 |
-
const moods = ['happy', 'excited', 'thinking', 'encouraging'];
|
| 224 |
-
const randomMood = moods[Math.floor(Math.random() * moods.length)];
|
| 225 |
-
this.setMood(randomMood);
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
setMood(mood) {
|
| 229 |
-
const character = this.character;
|
| 230 |
-
const mouth = this.mouth;
|
| 231 |
-
|
| 232 |
-
// Remove existing mood classes
|
| 233 |
-
character.classList.remove('happy', 'excited', 'thinking', 'encouraging');
|
| 234 |
-
character.classList.add(mood);
|
| 235 |
-
|
| 236 |
-
switch(mood) {
|
| 237 |
-
case 'happy':
|
| 238 |
-
mouth.style.borderRadius = '0 0 40px 40px';
|
| 239 |
-
mouth.style.width = '40px';
|
| 240 |
-
mouth.style.background = '#ff6b9d';
|
| 241 |
-
break;
|
| 242 |
-
case 'excited':
|
| 243 |
-
mouth.style.borderRadius = '50%';
|
| 244 |
-
mouth.style.width = '30px';
|
| 245 |
-
mouth.style.background = '#32b8c6';
|
| 246 |
-
break;
|
| 247 |
-
case 'thinking':
|
| 248 |
-
mouth.style.borderRadius = '40px 40px 0 0';
|
| 249 |
-
mouth.style.width = '20px';
|
| 250 |
-
mouth.style.background = '#8a2be2';
|
| 251 |
-
break;
|
| 252 |
-
case 'encouraging':
|
| 253 |
-
mouth.style.borderRadius = '0 0 50px 50px';
|
| 254 |
-
mouth.style.width = '50px';
|
| 255 |
-
mouth.style.background = '#ff6b9d';
|
| 256 |
-
break;
|
| 257 |
-
}
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
speak(text) {
|
| 261 |
-
this.isAnimating = true;
|
| 262 |
-
this.setMood('happy');
|
| 263 |
-
|
| 264 |
-
// Animate sound waves
|
| 265 |
-
if (this.soundWaves) {
|
| 266 |
-
this.soundWaves.style.display = 'block';
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
// Simulate mouth movement
|
| 270 |
-
let speakingInterval = setInterval(() => {
|
| 271 |
-
const randomWidth = 20 + Math.random() * 30;
|
| 272 |
-
if (this.mouth) {
|
| 273 |
-
this.mouth.style.width = randomWidth + 'px';
|
| 274 |
-
}
|
| 275 |
-
}, 150);
|
| 276 |
-
|
| 277 |
-
// Use Web Speech API if available and enabled
|
| 278 |
-
if ('speechSynthesis' in window && appState.soundEnabled) {
|
| 279 |
-
const utterance = new SpeechSynthesisUtterance(text);
|
| 280 |
-
utterance.lang = 'en-US';
|
| 281 |
-
utterance.rate = 0.9;
|
| 282 |
-
utterance.pitch = 1.1;
|
| 283 |
-
|
| 284 |
-
utterance.onend = () => {
|
| 285 |
-
clearInterval(speakingInterval);
|
| 286 |
-
this.stopSpeaking();
|
| 287 |
-
};
|
| 288 |
-
|
| 289 |
-
utterance.onerror = () => {
|
| 290 |
-
clearInterval(speakingInterval);
|
| 291 |
-
this.stopSpeaking();
|
| 292 |
-
};
|
| 293 |
-
|
| 294 |
-
speechSynthesis.speak(utterance);
|
| 295 |
-
} else {
|
| 296 |
-
// Fallback animation
|
| 297 |
-
setTimeout(() => {
|
| 298 |
-
clearInterval(speakingInterval);
|
| 299 |
-
this.stopSpeaking();
|
| 300 |
-
}, Math.min(text.length * 50, 3000));
|
| 301 |
-
}
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
stopSpeaking() {
|
| 305 |
-
this.isAnimating = false;
|
| 306 |
-
if (this.soundWaves) {
|
| 307 |
-
this.soundWaves.style.display = 'none';
|
| 308 |
-
}
|
| 309 |
-
if (this.mouth) {
|
| 310 |
-
this.mouth.style.width = '40px';
|
| 311 |
-
}
|
| 312 |
-
this.setMood('happy');
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
celebrate() {
|
| 316 |
-
this.setMood('excited');
|
| 317 |
-
if (this.character) {
|
| 318 |
-
this.character.style.animation = 'float 0.5s ease-in-out 3';
|
| 319 |
-
|
| 320 |
-
setTimeout(() => {
|
| 321 |
-
this.character.style.animation = 'float 3s ease-in-out infinite';
|
| 322 |
-
this.setMood('happy');
|
| 323 |
-
}, 1500);
|
| 324 |
-
}
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
encourage() {
|
| 328 |
-
this.setMood('encouraging');
|
| 329 |
-
setTimeout(() => {
|
| 330 |
-
this.setMood('happy');
|
| 331 |
-
}, 2000);
|
| 332 |
-
}
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
// Initialize character controller
|
| 336 |
-
let characterController;
|
| 337 |
-
|
| 338 |
-
// Screen Management
|
| 339 |
-
function showScreen(screenId) {
|
| 340 |
-
// Hide all screens
|
| 341 |
-
document.querySelectorAll('.screen').forEach(screen => {
|
| 342 |
-
screen.classList.remove('active');
|
| 343 |
-
});
|
| 344 |
-
|
| 345 |
-
// Show target screen
|
| 346 |
-
const targetScreen = document.getElementById(screenId);
|
| 347 |
-
if (targetScreen) {
|
| 348 |
-
targetScreen.classList.add('active');
|
| 349 |
-
appState.currentScreen = screenId;
|
| 350 |
-
|
| 351 |
-
// Update character mood based on screen
|
| 352 |
-
if (characterController) {
|
| 353 |
-
switch(screenId) {
|
| 354 |
-
case 'homeScreen':
|
| 355 |
-
characterController.setMood('happy');
|
| 356 |
-
break;
|
| 357 |
-
case 'conversationScreen':
|
| 358 |
-
characterController.setMood('encouraging');
|
| 359 |
-
break;
|
| 360 |
-
case 'vocabularyScreen':
|
| 361 |
-
case 'grammarScreen':
|
| 362 |
-
characterController.setMood('thinking');
|
| 363 |
-
break;
|
| 364 |
-
case 'pronunciationScreen':
|
| 365 |
-
characterController.setMood('excited');
|
| 366 |
-
break;
|
| 367 |
-
case 'progressScreen':
|
| 368 |
-
initializeAchievements();
|
| 369 |
-
break;
|
| 370 |
-
}
|
| 371 |
-
}
|
| 372 |
-
}
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
// Mode Management
|
| 376 |
-
function initializeModes() {
|
| 377 |
-
const modesGrid = document.getElementById('modesGrid');
|
| 378 |
-
if (!modesGrid) return;
|
| 379 |
-
|
| 380 |
-
modesGrid.innerHTML = '';
|
| 381 |
-
|
| 382 |
-
appData.modes.forEach(mode => {
|
| 383 |
-
const modeCard = document.createElement('div');
|
| 384 |
-
modeCard.className = 'mode-card';
|
| 385 |
-
modeCard.onclick = () => openMode(mode.id);
|
| 386 |
-
|
| 387 |
-
modeCard.innerHTML = `
|
| 388 |
-
<span class="mode-icon">${mode.icon}</span>
|
| 389 |
-
<h3 class="mode-title">${mode.name}</h3>
|
| 390 |
-
<p class="mode-description">${mode.description}</p>
|
| 391 |
-
`;
|
| 392 |
-
|
| 393 |
-
modesGrid.appendChild(modeCard);
|
| 394 |
-
});
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
function openMode(modeId) {
|
| 398 |
-
const screenMap = {
|
| 399 |
-
'conversation': 'conversationScreen',
|
| 400 |
-
'vocabulary': 'vocabularyScreen',
|
| 401 |
-
'pronunciation': 'pronunciationScreen',
|
| 402 |
-
'grammar': 'grammarScreen',
|
| 403 |
-
'scenarios': 'scenariosScreen',
|
| 404 |
-
'daily': 'dailyScreen'
|
| 405 |
-
};
|
| 406 |
-
|
| 407 |
-
const screenId = screenMap[modeId];
|
| 408 |
-
if (screenId) {
|
| 409 |
-
showScreen(screenId);
|
| 410 |
-
|
| 411 |
-
// Initialize mode-specific content
|
| 412 |
-
switch(modeId) {
|
| 413 |
-
case 'vocabulary':
|
| 414 |
-
initializeVocabulary();
|
| 415 |
-
break;
|
| 416 |
-
case 'grammar':
|
| 417 |
-
initializeGrammar();
|
| 418 |
-
break;
|
| 419 |
-
case 'scenarios':
|
| 420 |
-
initializeScenarios();
|
| 421 |
-
break;
|
| 422 |
-
case 'daily':
|
| 423 |
-
initializeDailyChallenge();
|
| 424 |
-
break;
|
| 425 |
-
case 'pronunciation':
|
| 426 |
-
initializePronunciation();
|
| 427 |
-
break;
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
// Conversation Mode
|
| 433 |
-
function sendMessage() {
|
| 434 |
-
const input = document.getElementById('messageInput');
|
| 435 |
-
if (!input) return;
|
| 436 |
-
const message = input.value.trim();
|
| 437 |
-
if (!message) return;
|
| 438 |
-
addMessage(message, 'user');
|
| 439 |
-
input.value = '';
|
| 440 |
-
|
| 441 |
-
// استدعاء الذكاء الاصطناعي عبر الخادم
|
| 442 |
-
fetch('/chat', {
|
| 443 |
-
method: 'POST',
|
| 444 |
-
headers: {'Content-Type': 'application/json'},
|
| 445 |
-
body: JSON.stringify({message})
|
| 446 |
-
})
|
| 447 |
-
.then(res => res.json())
|
| 448 |
-
.then(data => {
|
| 449 |
-
addMessage(data.reply, 'echo');
|
| 450 |
-
if (characterController) characterController.speak(data.reply);
|
| 451 |
-
})
|
| 452 |
-
.catch(() => {
|
| 453 |
-
addMessage("Sorry, I couldn't connect to the AI server.", 'echo');
|
| 454 |
-
});
|
| 455 |
-
|
| 456 |
-
// تحديث النقاط كما هو
|
| 457 |
-
appState.userProgress.conversationsCompleted++;
|
| 458 |
-
appState.userProgress.totalPoints += 25;
|
| 459 |
-
updateProgressDisplay();
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
function addMessage(text, sender) {
|
| 463 |
-
const messagesContainer = document.getElementById('chatMessages');
|
| 464 |
-
if (!messagesContainer) return;
|
| 465 |
-
|
| 466 |
-
const messageDiv = document.createElement('div');
|
| 467 |
-
messageDiv.className = `message ${sender}`;
|
| 468 |
-
messageDiv.textContent = text;
|
| 469 |
-
|
| 470 |
-
messagesContainer.appendChild(messageDiv);
|
| 471 |
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
function startVoiceInput() {
|
| 475 |
-
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
| 476 |
-
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 477 |
-
const recognition = new SpeechRecognition();
|
| 478 |
-
|
| 479 |
-
recognition.lang = 'en-US';
|
| 480 |
-
recognition.continuous = false;
|
| 481 |
-
recognition.interimResults = false;
|
| 482 |
-
|
| 483 |
-
recognition.onresult = function(event) {
|
| 484 |
-
const transcript = event.results[0][0].transcript;
|
| 485 |
-
const input = document.getElementById('messageInput');
|
| 486 |
-
if (input) {
|
| 487 |
-
input.value = transcript;
|
| 488 |
-
}
|
| 489 |
-
};
|
| 490 |
-
|
| 491 |
-
recognition.onerror = function(event) {
|
| 492 |
-
console.log('Speech recognition error:', event.error);
|
| 493 |
-
alert('حدث خطأ في التعرف على الصوت. تأكد من السماح بالوصول للميكروفون.');
|
| 494 |
-
};
|
| 495 |
-
|
| 496 |
-
recognition.start();
|
| 497 |
-
} else {
|
| 498 |
-
alert('متصفحك لا يدعم تقنية التعرف على الصوت.');
|
| 499 |
-
}
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
// Vocabulary Mode
|
| 503 |
-
function initializeVocabulary() {
|
| 504 |
-
appState.currentVocabIndex = 0;
|
| 505 |
-
updateVocabularyCard();
|
| 506 |
-
updateVocabularyProgress();
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
function updateVocabularyCard() {
|
| 510 |
-
const card = appData.vocabulary_cards[appState.currentVocabIndex];
|
| 511 |
-
|
| 512 |
-
const elements = {
|
| 513 |
-
'cardWord': card.word,
|
| 514 |
-
'cardPronunciation': card.pronunciation,
|
| 515 |
-
'cardTranslation': card.translation,
|
| 516 |
-
'cardExample': card.example
|
| 517 |
-
};
|
| 518 |
-
|
| 519 |
-
Object.entries(elements).forEach(([id, value]) => {
|
| 520 |
-
const element = document.getElementById(id);
|
| 521 |
-
if (element) {
|
| 522 |
-
element.textContent = value;
|
| 523 |
-
}
|
| 524 |
-
});
|
| 525 |
-
|
| 526 |
-
// Reset card flip
|
| 527 |
-
const flashcard = document.getElementById('flashcard');
|
| 528 |
-
if (flashcard) {
|
| 529 |
-
flashcard.classList.remove('flipped');
|
| 530 |
-
}
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
function updateVocabularyProgress() {
|
| 534 |
-
const progress = ((appState.currentVocabIndex + 1) / appData.vocabulary_cards.length) * 100;
|
| 535 |
-
const progressFill = document.getElementById('vocabProgress');
|
| 536 |
-
const progressText = document.getElementById('vocabProgressText');
|
| 537 |
-
|
| 538 |
-
if (progressFill) {
|
| 539 |
-
progressFill.style.width = progress + '%';
|
| 540 |
-
}
|
| 541 |
-
if (progressText) {
|
| 542 |
-
progressText.textContent = `${appState.currentVocabIndex + 1}/${appData.vocabulary_cards.length}`;
|
| 543 |
-
}
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
function flipCard() {
|
| 547 |
-
const flashcard = document.getElementById('flashcard');
|
| 548 |
-
if (flashcard) {
|
| 549 |
-
flashcard.classList.toggle('flipped');
|
| 550 |
-
}
|
| 551 |
-
}
|
| 552 |
-
|
| 553 |
-
function nextCard() {
|
| 554 |
-
appState.currentVocabIndex = (appState.currentVocabIndex + 1) % appData.vocabulary_cards.length;
|
| 555 |
-
updateVocabularyCard();
|
| 556 |
-
updateVocabularyProgress();
|
| 557 |
-
|
| 558 |
-
// Update progress
|
| 559 |
-
appState.userProgress.wordsLearned++;
|
| 560 |
-
appState.userProgress.totalPoints += 10;
|
| 561 |
-
updateProgressDisplay();
|
| 562 |
-
|
| 563 |
-
if (characterController) {
|
| 564 |
-
characterController.encourage();
|
| 565 |
-
}
|
| 566 |
-
}
|
| 567 |
-
|
| 568 |
-
function playPronunciation() {
|
| 569 |
-
const card = appData.vocabulary_cards[appState.currentVocabIndex];
|
| 570 |
-
if (characterController && appState.soundEnabled) {
|
| 571 |
-
characterController.speak(card.word);
|
| 572 |
-
}
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
// Pronunciation Mode
|
| 576 |
-
function initializePronunciation() {
|
| 577 |
-
const words = appData.vocabulary_cards;
|
| 578 |
-
const randomWord = words[Math.floor(Math.random() * words.length)];
|
| 579 |
-
|
| 580 |
-
const practiceWord = document.getElementById('practiceWord');
|
| 581 |
-
const practicePronunciation = document.getElementById('practicePronunciation');
|
| 582 |
-
|
| 583 |
-
if (practiceWord) practiceWord.textContent = randomWord.word;
|
| 584 |
-
if (practicePronunciation) practicePronunciation.textContent = randomWord.pronunciation;
|
| 585 |
-
|
| 586 |
-
// Clear previous feedback
|
| 587 |
-
const feedback = document.getElementById('pronunciationFeedback');
|
| 588 |
-
if (feedback) {
|
| 589 |
-
feedback.innerHTML = '';
|
| 590 |
-
}
|
| 591 |
-
}
|
| 592 |
-
|
| 593 |
-
function playTargetPronunciation() {
|
| 594 |
-
const word = document.getElementById('practiceWord');
|
| 595 |
-
if (word && characterController && appState.soundEnabled) {
|
| 596 |
-
characterController.speak(word.textContent);
|
| 597 |
-
}
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
function toggleRecording() {
|
| 601 |
-
const recordBtn = document.getElementById('recordBtn');
|
| 602 |
-
const visualizer = document.getElementById('recordVisualizer');
|
| 603 |
-
|
| 604 |
-
if (!recordBtn || !visualizer) return;
|
| 605 |
-
|
| 606 |
-
if (!appState.isRecording) {
|
| 607 |
-
// Start recording
|
| 608 |
-
appState.isRecording = true;
|
| 609 |
-
recordBtn.textContent = '⏹️ إيقاف التسجيل';
|
| 610 |
-
recordBtn.classList.add('recording');
|
| 611 |
-
|
| 612 |
-
// Clear previous content
|
| 613 |
-
visualizer.innerHTML = '';
|
| 614 |
-
|
| 615 |
-
// Create recording visualization
|
| 616 |
-
const bars = [];
|
| 617 |
-
for (let i = 0; i < 10; i++) {
|
| 618 |
-
const bar = document.createElement('div');
|
| 619 |
-
bar.style.cssText = `
|
| 620 |
-
width: 15px;
|
| 621 |
-
background: #32b8c6;
|
| 622 |
-
margin: 2px;
|
| 623 |
-
border-radius: 2px;
|
| 624 |
-
display: inline-block;
|
| 625 |
-
height: 20px;
|
| 626 |
-
animation: record-bar 0.8s infinite alternate;
|
| 627 |
-
animation-delay: ${i * 0.1}s;
|
| 628 |
-
`;
|
| 629 |
-
bars.push(bar);
|
| 630 |
-
visualizer.appendChild(bar);
|
| 631 |
-
}
|
| 632 |
-
|
| 633 |
-
// Add CSS for bars animation if not exists
|
| 634 |
-
if (!document.getElementById('recordBarStyles')) {
|
| 635 |
-
const style = document.createElement('style');
|
| 636 |
-
style.id = 'recordBarStyles';
|
| 637 |
-
style.textContent = `
|
| 638 |
-
@keyframes record-bar {
|
| 639 |
-
0% { height: 10px; opacity: 0.5; }
|
| 640 |
-
100% { height: 50px; opacity: 1; }
|
| 641 |
-
}
|
| 642 |
-
`;
|
| 643 |
-
document.head.appendChild(style);
|
| 644 |
-
}
|
| 645 |
-
|
| 646 |
-
// Auto-stop after 5 seconds
|
| 647 |
-
appState.recordingTimer = setTimeout(() => {
|
| 648 |
-
if (appState.isRecording) {
|
| 649 |
-
toggleRecording();
|
| 650 |
-
}
|
| 651 |
-
}, 5000);
|
| 652 |
-
|
| 653 |
-
} else {
|
| 654 |
-
// Stop recording
|
| 655 |
-
appState.isRecording = false;
|
| 656 |
-
recordBtn.textContent = '🎤 ابدأ التسجيل';
|
| 657 |
-
recordBtn.classList.remove('recording');
|
| 658 |
-
|
| 659 |
-
// Clear timer
|
| 660 |
-
if (appState.recordingTimer) {
|
| 661 |
-
clearTimeout(appState.recordingTimer);
|
| 662 |
-
appState.recordingTimer = null;
|
| 663 |
-
}
|
| 664 |
-
|
| 665 |
-
// Clear visualizer
|
| 666 |
-
visualizer.innerHTML = '';
|
| 667 |
-
|
| 668 |
-
// Show processing message then feedback
|
| 669 |
-
const feedback = document.getElementById('pronunciationFeedback');
|
| 670 |
-
if (feedback) {
|
| 671 |
-
feedback.innerHTML = '<p>جاري تحليل النطق...</p>';
|
| 672 |
-
|
| 673 |
-
setTimeout(() => {
|
| 674 |
-
showPronunciationFeedback();
|
| 675 |
-
}, 1500);
|
| 676 |
-
}
|
| 677 |
-
}
|
| 678 |
-
}
|
| 679 |
-
|
| 680 |
-
function showPronunciationFeedback() {
|
| 681 |
-
const feedback = document.getElementById('pronunciationFeedback');
|
| 682 |
-
if (!feedback) return;
|
| 683 |
-
|
| 684 |
-
const accuracy = 65 + Math.random() * 30; // Simulate accuracy between 65-95%
|
| 685 |
-
const roundedAccuracy = Math.round(accuracy);
|
| 686 |
-
|
| 687 |
-
let message, color;
|
| 688 |
-
if (roundedAccuracy >= 85) {
|
| 689 |
-
message = 'ممتاز! نطقك رائع 🎉';
|
| 690 |
-
color = '#32b8c6';
|
| 691 |
-
} else if (roundedAccuracy >= 70) {
|
| 692 |
-
message = 'جيد جداً! يمكنك تحسينه أكثر 👍';
|
| 693 |
-
color = '#8a2be2';
|
| 694 |
-
} else {
|
| 695 |
-
message = 'يحتاج تحسين، استمع للنطق الصحيح 🎯';
|
| 696 |
-
color = '#ff6b9d';
|
| 697 |
-
}
|
| 698 |
-
|
| 699 |
-
feedback.innerHTML = `
|
| 700 |
-
<h4>نتيجة النطق</h4>
|
| 701 |
-
<div style="font-size: 3em; color: ${color}; margin: 16px 0; text-shadow: 0 0 10px ${color}40;">
|
| 702 |
-
${roundedAccuracy}%
|
| 703 |
-
</div>
|
| 704 |
-
<p style="color: ${color}; font-weight: bold;">${message}</p>
|
| 705 |
-
<button class="btn btn--primary" onclick="initializePronunciation()">كلمة جديدة</button>
|
| 706 |
-
`;
|
| 707 |
-
|
| 708 |
-
// Update progress
|
| 709 |
-
appState.userProgress.pronunciationAccuracy = Math.round(
|
| 710 |
-
(appState.userProgress.pronunciationAccuracy + accuracy) / 2
|
| 711 |
-
);
|
| 712 |
-
appState.userProgress.totalPoints += Math.round(accuracy / 10);
|
| 713 |
-
updateProgressDisplay();
|
| 714 |
-
|
| 715 |
-
if (characterController) {
|
| 716 |
-
if (roundedAccuracy >= 85) {
|
| 717 |
-
characterController.celebrate();
|
| 718 |
-
} else {
|
| 719 |
-
characterController.encourage();
|
| 720 |
-
}
|
| 721 |
-
}
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
// Grammar Mode
|
| 725 |
-
function initializeGrammar() {
|
| 726 |
-
appState.currentGrammarIndex = 0;
|
| 727 |
-
showGrammarQuestion();
|
| 728 |
-
}
|
| 729 |
-
|
| 730 |
-
function showGrammarQuestion() {
|
| 731 |
-
const exercise = appData.grammar_exercises[appState.currentGrammarIndex];
|
| 732 |
-
|
| 733 |
-
const questionElement = document.getElementById('grammarQuestion');
|
| 734 |
-
if (questionElement) {
|
| 735 |
-
questionElement.textContent = exercise.question;
|
| 736 |
-
}
|
| 737 |
-
|
| 738 |
-
const optionsContainer = document.getElementById('grammarOptions');
|
| 739 |
-
if (optionsContainer) {
|
| 740 |
-
optionsContainer.innerHTML = '';
|
| 741 |
-
|
| 742 |
-
exercise.options.forEach((option, index) => {
|
| 743 |
-
const optionBtn = document.createElement('button');
|
| 744 |
-
optionBtn.className = 'option-btn';
|
| 745 |
-
optionBtn.textContent = option;
|
| 746 |
-
optionBtn.onclick = () => selectGrammarOption(index);
|
| 747 |
-
|
| 748 |
-
optionsContainer.appendChild(optionBtn);
|
| 749 |
-
});
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
// Clear previous feedback
|
| 753 |
-
const feedback = document.getElementById('grammarFeedback');
|
| 754 |
-
if (feedback) {
|
| 755 |
-
feedback.innerHTML = '';
|
| 756 |
-
}
|
| 757 |
-
}
|
| 758 |
-
|
| 759 |
-
function selectGrammarOption(selectedIndex) {
|
| 760 |
-
const exercise = appData.grammar_exercises[appState.currentGrammarIndex];
|
| 761 |
-
const options = document.querySelectorAll('.option-btn');
|
| 762 |
-
const feedback = document.getElementById('grammarFeedback');
|
| 763 |
-
|
| 764 |
-
if (!feedback) return;
|
| 765 |
-
|
| 766 |
-
// Disable all options
|
| 767 |
-
options.forEach((btn, index) => {
|
| 768 |
-
btn.disabled = true;
|
| 769 |
-
if (index === exercise.correct) {
|
| 770 |
-
btn.classList.add('correct');
|
| 771 |
-
} else if (index === selectedIndex && index !== exercise.correct) {
|
| 772 |
-
btn.classList.add('incorrect');
|
| 773 |
-
}
|
| 774 |
-
});
|
| 775 |
-
|
| 776 |
-
// Show feedback
|
| 777 |
-
const isCorrect = selectedIndex === exercise.correct;
|
| 778 |
-
feedback.innerHTML = `
|
| 779 |
-
<h4>${isCorrect ? '✅ إجابة صحيحة!' : '❌ إجابة خاطئة'}</h4>
|
| 780 |
-
<p>${exercise.explanation}</p>
|
| 781 |
-
<button class="btn btn--primary" onclick="nextGrammarQuestion()">السؤال التالي</button>
|
| 782 |
-
`;
|
| 783 |
-
|
| 784 |
-
// Update progress
|
| 785 |
-
if (isCorrect) {
|
| 786 |
-
appState.userProgress.grammarScore += 10;
|
| 787 |
-
appState.userProgress.totalPoints += 15;
|
| 788 |
-
updateProgressDisplay();
|
| 789 |
-
|
| 790 |
-
if (characterController) {
|
| 791 |
-
characterController.celebrate();
|
| 792 |
-
}
|
| 793 |
-
} else {
|
| 794 |
-
if (characterController) {
|
| 795 |
-
characterController.encourage();
|
| 796 |
-
}
|
| 797 |
-
}
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
function nextGrammarQuestion() {
|
| 801 |
-
appState.currentGrammarIndex = (appState.currentGrammarIndex + 1) % appData.grammar_exercises.length;
|
| 802 |
-
showGrammarQuestion();
|
| 803 |
-
}
|
| 804 |
-
|
| 805 |
-
// Scenarios Mode
|
| 806 |
-
function initializeScenarios() {
|
| 807 |
-
const buttonsContainer = document.getElementById('scenarioButtons');
|
| 808 |
-
if (!buttonsContainer) return;
|
| 809 |
-
|
| 810 |
-
buttonsContainer.innerHTML = '';
|
| 811 |
-
|
| 812 |
-
appData.scenarios.forEach((scenario, index) => {
|
| 813 |
-
const btn = document.createElement('button');
|
| 814 |
-
btn.className = 'scenario-btn';
|
| 815 |
-
btn.textContent = scenario.title;
|
| 816 |
-
btn.onclick = () => showScenario(index);
|
| 817 |
-
|
| 818 |
-
buttonsContainer.appendChild(btn);
|
| 819 |
-
});
|
| 820 |
-
|
| 821 |
-
// Show first scenario by default
|
| 822 |
-
if (appData.scenarios.length > 0) {
|
| 823 |
-
showScenario(0);
|
| 824 |
-
}
|
| 825 |
-
}
|
| 826 |
-
|
| 827 |
-
function showScenario(scenarioIndex) {
|
| 828 |
-
const scenario = appData.scenarios[scenarioIndex];
|
| 829 |
-
appState.currentScenario = scenarioIndex;
|
| 830 |
-
|
| 831 |
-
// Update button states
|
| 832 |
-
document.querySelectorAll('.scenario-btn').forEach((btn, index) => {
|
| 833 |
-
btn.classList.toggle('active', index === scenarioIndex);
|
| 834 |
-
});
|
| 835 |
-
|
| 836 |
-
// Show dialogue
|
| 837 |
-
const dialogueContainer = document.getElementById('scenarioDialogue');
|
| 838 |
-
if (dialogueContainer) {
|
| 839 |
-
dialogueContainer.innerHTML = `
|
| 840 |
-
<h4>${scenario.title}</h4>
|
| 841 |
-
<p>${scenario.description}</p>
|
| 842 |
-
<div class="dialogue-content">
|
| 843 |
-
${scenario.dialogue.map(line => `
|
| 844 |
-
<div class="dialogue-line ${line.speaker}">
|
| 845 |
-
<strong>${line.speaker === 'waiter' ? 'النادل:' :
|
| 846 |
-
line.speaker === 'customer' ? 'الزبون:' :
|
| 847 |
-
line.speaker === 'interviewer' ? 'المحاور:' : 'المرشح:'}</strong>
|
| 848 |
-
<p>${line.text}</p>
|
| 849 |
-
</div>
|
| 850 |
-
`).join('')}
|
| 851 |
-
</div>
|
| 852 |
-
<button class="btn btn--primary" onclick="practiceScenario()">🎤 تدرب على هذا السيناريو</button>
|
| 853 |
-
`;
|
| 854 |
-
}
|
| 855 |
-
}
|
| 856 |
-
|
| 857 |
-
function practiceScenario() {
|
| 858 |
-
if (characterController && appState.currentScenario !== null) {
|
| 859 |
-
const scenario = appData.scenarios[appState.currentScenario];
|
| 860 |
-
const firstLine = scenario.dialogue[0].text;
|
| 861 |
-
characterController.speak(firstLine);
|
| 862 |
-
}
|
| 863 |
-
|
| 864 |
-
// Show practice message
|
| 865 |
-
const dialogueContainer = document.getElementById('scenarioDialogue');
|
| 866 |
-
if (dialogueContainer) {
|
| 867 |
-
const practiceDiv = document.createElement('div');
|
| 868 |
-
practiceDiv.style.cssText = `
|
| 869 |
-
background: rgba(50, 184, 198, 0.1);
|
| 870 |
-
border: 1px solid #32b8c6;
|
| 871 |
-
border-radius: 12px;
|
| 872 |
-
padding: 16px;
|
| 873 |
-
margin-top: 16px;
|
| 874 |
-
text-align: center;
|
| 875 |
-
`;
|
| 876 |
-
practiceDiv.innerHTML = '<p>🎤 الآن دورك! تدرب على الرد باللغة الإنجليزية</p>';
|
| 877 |
-
dialogueContainer.appendChild(practiceDiv);
|
| 878 |
-
}
|
| 879 |
-
}
|
| 880 |
-
|
| 881 |
-
// Daily Challenge Mode
|
| 882 |
-
function initializeDailyChallenge() {
|
| 883 |
-
appState.dailyProgress = 1;
|
| 884 |
-
appState.selectedChallengeWords = [];
|
| 885 |
-
updateDailyProgress();
|
| 886 |
-
startChallengeTimer();
|
| 887 |
-
}
|
| 888 |
-
|
| 889 |
-
function updateDailyProgress() {
|
| 890 |
-
const progress = (appState.dailyProgress / 3) * 100;
|
| 891 |
-
const progressFill = document.getElementById('dailyProgress');
|
| 892 |
-
const progressText = document.getElementById('dailyProgressText');
|
| 893 |
-
|
| 894 |
-
if (progressFill) {
|
| 895 |
-
progressFill.style.width = progress + '%';
|
| 896 |
-
}
|
| 897 |
-
if (progressText) {
|
| 898 |
-
progressText.textContent = `${appState.dailyProgress}/3`;
|
| 899 |
-
}
|
| 900 |
-
}
|
| 901 |
-
|
| 902 |
-
function selectWord(wordElement) {
|
| 903 |
-
const word = wordElement.textContent;
|
| 904 |
-
|
| 905 |
-
// Toggle selection
|
| 906 |
-
if (wordElement.classList.contains('selected')) {
|
| 907 |
-
wordElement.classList.remove('selected');
|
| 908 |
-
appState.selectedChallengeWords = appState.selectedChallengeWords.filter(w => w !== word);
|
| 909 |
-
} else {
|
| 910 |
-
// Limit to 2 selections
|
| 911 |
-
if (appState.selectedChallengeWords.length < 2) {
|
| 912 |
-
wordElement.classList.add('selected');
|
| 913 |
-
appState.selectedChallengeWords.push(word);
|
| 914 |
-
}
|
| 915 |
-
}
|
| 916 |
-
|
| 917 |
-
// Check if challenge is complete
|
| 918 |
-
if (appState.selectedChallengeWords.length === 2) {
|
| 919 |
-
setTimeout(() => {
|
| 920 |
-
checkChallengeAnswer();
|
| 921 |
-
}, 500);
|
| 922 |
-
}
|
| 923 |
-
}
|
| 924 |
-
|
| 925 |
-
function checkChallengeAnswer() {
|
| 926 |
-
const correctWords = ['study', 'important'];
|
| 927 |
-
const isCorrect = correctWords.every(word => appState.selectedChallengeWords.includes(word));
|
| 928 |
-
|
| 929 |
-
if (isCorrect) {
|
| 930 |
-
appState.dailyProgress++;
|
| 931 |
-
updateDailyProgress();
|
| 932 |
-
|
| 933 |
-
if (characterController) {
|
| 934 |
-
characterController.celebrate();
|
| 935 |
-
}
|
| 936 |
-
|
| 937 |
-
// Update streak and points
|
| 938 |
-
appState.userProgress.streak++;
|
| 939 |
-
appState.userProgress.totalPoints += 20;
|
| 940 |
-
updateProgressDisplay();
|
| 941 |
-
|
| 942 |
-
if (appState.dailyProgress >= 3) {
|
| 943 |
-
setTimeout(() => {
|
| 944 |
-
alert('🎉 أكملت التحدي اليومي! أحسنت!');
|
| 945 |
-
checkAchievements();
|
| 946 |
-
}, 1000);
|
| 947 |
-
} else {
|
| 948 |
-
setTimeout(() => {
|
| 949 |
-
alert('✅ إجابة صحيحة! استمر...');
|
| 950 |
-
}, 500);
|
| 951 |
-
}
|
| 952 |
-
} else {
|
| 953 |
-
if (characterController) {
|
| 954 |
-
characterController.encourage();
|
| 955 |
-
}
|
| 956 |
-
setTimeout(() => {
|
| 957 |
-
alert('❌ حاول مرة أخرى!');
|
| 958 |
-
}, 500);
|
| 959 |
-
}
|
| 960 |
-
|
| 961 |
-
// Reset selections
|
| 962 |
-
document.querySelectorAll('.word-option').forEach(option => {
|
| 963 |
-
option.classList.remove('selected');
|
| 964 |
-
});
|
| 965 |
-
appState.selectedChallengeWords = [];
|
| 966 |
-
}
|
| 967 |
-
|
| 968 |
-
function startChallengeTimer() {
|
| 969 |
-
let timeLeft = 300; // 5 minutes in seconds
|
| 970 |
-
const timerElement = document.getElementById('challengeTimer');
|
| 971 |
-
if (!timerElement) return;
|
| 972 |
-
|
| 973 |
-
const timer = setInterval(() => {
|
| 974 |
-
const minutes = Math.floor(timeLeft / 60);
|
| 975 |
-
const seconds = timeLeft % 60;
|
| 976 |
-
timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 977 |
-
|
| 978 |
-
timeLeft--;
|
| 979 |
-
|
| 980 |
-
if (timeLeft < 0) {
|
| 981 |
-
clearInterval(timer);
|
| 982 |
-
alert('⏰ انتهى الوقت! حاول مرة أخرى غداً.');
|
| 983 |
-
}
|
| 984 |
-
}, 1000);
|
| 985 |
-
}
|
| 986 |
-
|
| 987 |
-
// Progress Tracking
|
| 988 |
-
function updateProgressDisplay() {
|
| 989 |
-
const elements = {
|
| 990 |
-
'streakCount': appState.userProgress.streak,
|
| 991 |
-
'totalPoints': appState.userProgress.totalPoints,
|
| 992 |
-
'wordsLearned': appState.userProgress.wordsLearned,
|
| 993 |
-
'conversationsCompleted': appState.userProgress.conversationsCompleted,
|
| 994 |
-
'pronunciationAccuracy': appState.userProgress.pronunciationAccuracy + '%',
|
| 995 |
-
'grammarScore': appState.userProgress.grammarScore
|
| 996 |
-
};
|
| 997 |
-
|
| 998 |
-
Object.entries(elements).forEach(([id, value]) => {
|
| 999 |
-
const element = document.getElementById(id);
|
| 1000 |
-
if (element) {
|
| 1001 |
-
element.textContent = value;
|
| 1002 |
-
}
|
| 1003 |
-
});
|
| 1004 |
-
}
|
| 1005 |
-
|
| 1006 |
-
// Achievements System
|
| 1007 |
-
function checkAchievements() {
|
| 1008 |
-
const progress = appState.userProgress;
|
| 1009 |
-
const achievements = [
|
| 1010 |
-
{
|
| 1011 |
-
id: 'first_conversation',
|
| 1012 |
-
condition: progress.conversationsCompleted >= 1
|
| 1013 |
-
},
|
| 1014 |
-
{
|
| 1015 |
-
id: 'vocabulary_master',
|
| 1016 |
-
condition: progress.wordsLearned >= 50
|
| 1017 |
-
},
|
| 1018 |
-
{
|
| 1019 |
-
id: 'pronunciation_expert',
|
| 1020 |
-
condition: progress.pronunciationAccuracy >= 85
|
| 1021 |
-
},
|
| 1022 |
-
{
|
| 1023 |
-
id: 'grammar_guru',
|
| 1024 |
-
condition: progress.grammarScore >= 100
|
| 1025 |
-
},
|
| 1026 |
-
{
|
| 1027 |
-
id: 'daily_streak',
|
| 1028 |
-
condition: progress.streak >= 7
|
| 1029 |
-
}
|
| 1030 |
-
];
|
| 1031 |
-
|
| 1032 |
-
achievements.forEach(achievement => {
|
| 1033 |
-
if (achievement.condition && !progress.unlockedAchievements.includes(achievement.id)) {
|
| 1034 |
-
unlockAchievement(achievement.id);
|
| 1035 |
-
}
|
| 1036 |
-
});
|
| 1037 |
-
}
|
| 1038 |
-
|
| 1039 |
-
function unlockAchievement(achievementId) {
|
| 1040 |
-
const achievement = appData.achievements.find(a => a.id === achievementId);
|
| 1041 |
-
if (!achievement) return;
|
| 1042 |
-
|
| 1043 |
-
appState.userProgress.unlockedAchievements.push(achievementId);
|
| 1044 |
-
appState.userProgress.totalPoints += 50;
|
| 1045 |
-
updateProgressDisplay();
|
| 1046 |
-
|
| 1047 |
-
// Show achievement notification
|
| 1048 |
-
showAchievementNotification(achievement);
|
| 1049 |
-
|
| 1050 |
-
if (characterController) {
|
| 1051 |
-
characterController.celebrate();
|
| 1052 |
-
}
|
| 1053 |
-
}
|
| 1054 |
-
|
| 1055 |
-
function showAchievementNotification(achievement) {
|
| 1056 |
-
const notification = document.createElement('div');
|
| 1057 |
-
notification.style.cssText = `
|
| 1058 |
-
position: fixed;
|
| 1059 |
-
top: 20px;
|
| 1060 |
-
right: 20px;
|
| 1061 |
-
background: linear-gradient(135deg, #32b8c6, #8a2be2);
|
| 1062 |
-
color: white;
|
| 1063 |
-
padding: 20px;
|
| 1064 |
-
border-radius: 12px;
|
| 1065 |
-
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 1066 |
-
z-index: 1000;
|
| 1067 |
-
animation: slideIn 0.5s ease-out;
|
| 1068 |
-
max-width: 300px;
|
| 1069 |
-
`;
|
| 1070 |
-
notification.innerHTML = `
|
| 1071 |
-
<div style="font-size: 2em; margin-bottom: 8px; text-align: center;">${achievement.icon}</div>
|
| 1072 |
-
<h4 style="margin: 0 0 8px 0; text-align: center;">إنجاز جديد!</h4>
|
| 1073 |
-
<p style="margin: 0; font-size: 14px; text-align: center;">${achievement.name}</p>
|
| 1074 |
-
`;
|
| 1075 |
-
|
| 1076 |
-
document.body.appendChild(notification);
|
| 1077 |
-
|
| 1078 |
-
setTimeout(() => {
|
| 1079 |
-
notification.remove();
|
| 1080 |
-
}, 5000);
|
| 1081 |
-
}
|
| 1082 |
-
|
| 1083 |
-
function initializeAchievements() {
|
| 1084 |
-
const achievementsGrid = document.getElementById('achievementsGrid');
|
| 1085 |
-
if (!achievementsGrid) return;
|
| 1086 |
-
|
| 1087 |
-
achievementsGrid.innerHTML = '';
|
| 1088 |
-
|
| 1089 |
-
appData.achievements.forEach(achievement => {
|
| 1090 |
-
const isUnlocked = appState.userProgress.unlockedAchievements.includes(achievement.id);
|
| 1091 |
-
|
| 1092 |
-
const achievementCard = document.createElement('div');
|
| 1093 |
-
achievementCard.className = `achievement-card ${isUnlocked ? 'unlocked' : ''}`;
|
| 1094 |
-
achievementCard.innerHTML = `
|
| 1095 |
-
<span class="achievement-icon">${achievement.icon}</span>
|
| 1096 |
-
<div class="achievement-name">${achievement.name}</div>
|
| 1097 |
-
<div class="achievement-description">${achievement.description}</div>
|
| 1098 |
-
`;
|
| 1099 |
-
|
| 1100 |
-
achievementsGrid.appendChild(achievementCard);
|
| 1101 |
-
});
|
| 1102 |
-
}
|
| 1103 |
-
|
| 1104 |
-
// Sound Management
|
| 1105 |
-
function toggleSound() {
|
| 1106 |
-
appState.soundEnabled = !appState.soundEnabled;
|
| 1107 |
-
const soundToggle = document.getElementById('soundToggle');
|
| 1108 |
-
|
| 1109 |
-
if (soundToggle) {
|
| 1110 |
-
soundToggle.textContent = appState.soundEnabled ? '🔊' : '🔇';
|
| 1111 |
-
soundToggle.title = appState.soundEnabled ? 'إيقاف الصوت' : 'تشغيل الصوت';
|
| 1112 |
-
|
| 1113 |
-
// Visual feedback
|
| 1114 |
-
soundToggle.style.color = appState.soundEnabled ? '#32b8c6' : '#ff6b9d';
|
| 1115 |
-
}
|
| 1116 |
-
|
| 1117 |
-
if (!appState.soundEnabled && 'speechSynthesis' in window) {
|
| 1118 |
-
speechSynthesis.cancel();
|
| 1119 |
-
}
|
| 1120 |
-
|
| 1121 |
-
// Show feedback message
|
| 1122 |
-
const message = appState.soundEnabled ? 'تم تشغيل الصوت' : 'تم إيقاف الصوت';
|
| 1123 |
-
showTemporaryMessage(message);
|
| 1124 |
-
}
|
| 1125 |
-
|
| 1126 |
-
function showTemporaryMessage(message) {
|
| 1127 |
-
const messageDiv = document.createElement('div');
|
| 1128 |
-
messageDiv.style.cssText = `
|
| 1129 |
-
position: fixed;
|
| 1130 |
-
bottom: 100px;
|
| 1131 |
-
right: 20px;
|
| 1132 |
-
background: rgba(0, 0, 0, 0.8);
|
| 1133 |
-
color: white;
|
| 1134 |
-
padding: 12px 16px;
|
| 1135 |
-
border-radius: 8px;
|
| 1136 |
-
font-size: 14px;
|
| 1137 |
-
z-index: 1000;
|
| 1138 |
-
animation: slideIn 0.5s ease-out;
|
| 1139 |
-
`;
|
| 1140 |
-
messageDiv.textContent = message;
|
| 1141 |
-
|
| 1142 |
-
document.body.appendChild(messageDiv);
|
| 1143 |
-
|
| 1144 |
-
setTimeout(() => {
|
| 1145 |
-
messageDiv.remove();
|
| 1146 |
-
}, 2000);
|
| 1147 |
-
}
|
| 1148 |
-
|
| 1149 |
-
// Add slide-in animation CSS
|
| 1150 |
-
const style = document.createElement('style');
|
| 1151 |
-
style.textContent = `
|
| 1152 |
-
@keyframes slideIn {
|
| 1153 |
-
from { transform: translateX(100%); opacity: 0; }
|
| 1154 |
-
to { transform: translateX(0); opacity: 1; }
|
| 1155 |
-
}
|
| 1156 |
-
`;
|
| 1157 |
-
document.head.appendChild(style);
|
| 1158 |
-
|
| 1159 |
-
// === Conversational Voice Loop & Avatar State ===
|
| 1160 |
-
let isMuted = false;
|
| 1161 |
-
let isConversationActive = false;
|
| 1162 |
-
let isAutoLoop = false;
|
| 1163 |
-
let recognition = null;
|
| 1164 |
-
let isSpeakingVoiceLoop = false; // حماية من التكرار
|
| 1165 |
-
|
| 1166 |
-
function setVoiceControlButtonsState() {
|
| 1167 |
-
const btnStart = document.querySelector('.voice-controls .btn--primary');
|
| 1168 |
-
const btnMute = document.querySelector('.voice-controls .btn--secondary');
|
| 1169 |
-
const btnEnd = document.querySelector('.voice-controls .btn--danger');
|
| 1170 |
-
if (!btnStart || !btnMute || !btnEnd) return;
|
| 1171 |
-
btnStart.disabled = isConversationActive || isSpeakingVoiceLoop;
|
| 1172 |
-
btnEnd.disabled = !isConversationActive && !isSpeakingVoiceLoop;
|
| 1173 |
-
btnMute.disabled = !isConversationActive && !isSpeakingVoiceLoop;
|
| 1174 |
-
btnMute.textContent = isMuted ? '🔇 كتم' : '🔊 كتم';
|
| 1175 |
-
btnMute.classList.toggle('muted', isMuted);
|
| 1176 |
-
}
|
| 1177 |
-
|
| 1178 |
-
function setAvatarState(state) {
|
| 1179 |
-
if (!characterController) return;
|
| 1180 |
-
switch(state) {
|
| 1181 |
-
case 'idle':
|
| 1182 |
-
characterController.setMood('happy');
|
| 1183 |
-
break;
|
| 1184 |
-
case 'listening':
|
| 1185 |
-
characterController.setMood('encouraging');
|
| 1186 |
-
break;
|
| 1187 |
-
case 'speaking':
|
| 1188 |
-
characterController.setMood('excited');
|
| 1189 |
-
break;
|
| 1190 |
-
}
|
| 1191 |
-
}
|
| 1192 |
-
|
| 1193 |
-
function startVoiceLoop() {
|
| 1194 |
-
if (isMuted || isConversationActive || isSpeakingVoiceLoop) return;
|
| 1195 |
-
isConversationActive = true;
|
| 1196 |
-
isAutoLoop = true;
|
| 1197 |
-
setAvatarState('listening');
|
| 1198 |
-
setVoiceControlButtonsState();
|
| 1199 |
-
startListening();
|
| 1200 |
-
}
|
| 1201 |
-
|
| 1202 |
-
function stopVoiceLoop() {
|
| 1203 |
-
isConversationActive = false;
|
| 1204 |
-
isAutoLoop = false;
|
| 1205 |
-
if (recognition) recognition.stop();
|
| 1206 |
-
if ('speechSynthesis' in window) speechSynthesis.cancel();
|
| 1207 |
-
isSpeakingVoiceLoop = false;
|
| 1208 |
-
setAvatarState('idle');
|
| 1209 |
-
setVoiceControlButtonsState();
|
| 1210 |
-
}
|
| 1211 |
-
|
| 1212 |
-
function toggleMute() {
|
| 1213 |
-
isMuted = !isMuted;
|
| 1214 |
-
if (isMuted) {
|
| 1215 |
-
if (recognition) recognition.stop();
|
| 1216 |
-
if ('speechSynthesis' in window) speechSynthesis.cancel();
|
| 1217 |
-
setAvatarState('idle');
|
| 1218 |
-
isConversationActive = false;
|
| 1219 |
-
isAutoLoop = false;
|
| 1220 |
-
isSpeakingVoiceLoop = false;
|
| 1221 |
-
} else if (!isConversationActive && !isSpeakingVoiceLoop) {
|
| 1222 |
-
isConversationActive = true;
|
| 1223 |
-
isAutoLoop = true;
|
| 1224 |
-
setAvatarState('listening');
|
| 1225 |
-
startListening();
|
| 1226 |
-
}
|
| 1227 |
-
setVoiceControlButtonsState();
|
| 1228 |
-
}
|
| 1229 |
-
|
| 1230 |
-
function startListening() {
|
| 1231 |
-
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
|
| 1232 |
-
alert('Speech recognition not supported.');
|
| 1233 |
-
return;
|
| 1234 |
-
}
|
| 1235 |
-
if (isSpeakingVoiceLoop) return; // لا تستمع أثناء النطق
|
| 1236 |
-
setAvatarState('listening');
|
| 1237 |
-
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 1238 |
-
recognition = new SpeechRecognition();
|
| 1239 |
-
recognition.lang = 'en-US';
|
| 1240 |
-
recognition.continuous = false;
|
| 1241 |
-
recognition.interimResults = false;
|
| 1242 |
-
recognition.onresult = function(event) {
|
| 1243 |
-
const transcript = event.results[0][0].transcript;
|
| 1244 |
-
addMessage(transcript, 'user');
|
| 1245 |
-
handleAIResponse(transcript);
|
| 1246 |
-
};
|
| 1247 |
-
recognition.onerror = function(event) {
|
| 1248 |
-
setAvatarState('idle');
|
| 1249 |
-
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1250 |
-
setTimeout(startListening, 1000);
|
| 1251 |
-
}
|
| 1252 |
-
};
|
| 1253 |
-
recognition.onend = function() {
|
| 1254 |
-
// Do nothing, will be restarted after AI response
|
| 1255 |
-
};
|
| 1256 |
-
recognition.start();
|
| 1257 |
-
}
|
| 1258 |
-
|
| 1259 |
-
function handleAIResponse(userText) {
|
| 1260 |
-
setAvatarState('thinking');
|
| 1261 |
-
fetch('/chat', {
|
| 1262 |
-
method: 'POST',
|
| 1263 |
-
headers: {'Content-Type': 'application/json'},
|
| 1264 |
-
body: JSON.stringify({message: userText})
|
| 1265 |
-
})
|
| 1266 |
-
.then(res => res.json())
|
| 1267 |
-
.then(data => {
|
| 1268 |
-
addMessage(data.reply, 'echo');
|
| 1269 |
-
setAvatarState('speaking');
|
| 1270 |
-
if (characterController && appState.soundEnabled) {
|
| 1271 |
-
const utterance = new SpeechSynthesisUtterance(data.reply);
|
| 1272 |
-
utterance.lang = 'en-US';
|
| 1273 |
-
utterance.onend = () => {
|
| 1274 |
-
setAvatarState('idle');
|
| 1275 |
-
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1276 |
-
setTimeout(startListening, 500);
|
| 1277 |
-
}
|
| 1278 |
-
};
|
| 1279 |
-
speechSynthesis.speak(utterance);
|
| 1280 |
-
characterController.speak(data.reply);
|
| 1281 |
-
} else {
|
| 1282 |
-
setAvatarState('idle');
|
| 1283 |
-
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1284 |
-
setTimeout(startListening, 500);
|
| 1285 |
-
}
|
| 1286 |
-
}
|
| 1287 |
-
})
|
| 1288 |
-
.catch(() => {
|
| 1289 |
-
addMessage("Sorry, I couldn't connect to the AI server.", 'echo');
|
| 1290 |
-
setAvatarState('idle');
|
| 1291 |
-
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1292 |
-
setTimeout(startListening, 1500);
|
| 1293 |
-
}
|
| 1294 |
-
});
|
| 1295 |
-
}
|
| 1296 |
-
|
| 1297 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 1298 |
-
// Initialize character controller
|
| 1299 |
-
characterController = new CharacterController();
|
| 1300 |
-
|
| 1301 |
-
// Initialize modes
|
| 1302 |
-
initializeModes();
|
| 1303 |
-
|
| 1304 |
-
// Initialize achievements
|
| 1305 |
-
initializeAchievements();
|
| 1306 |
-
|
| 1307 |
-
// Update progress display
|
| 1308 |
-
updateProgressDisplay();
|
| 1309 |
-
|
| 1310 |
-
// Welcome message
|
| 1311 |
-
setTimeout(() => {
|
| 1312 |
-
if (characterController) {
|
| 1313 |
-
characterController.speak("Welcome to Echo Light! I'm here to help you learn English in a fun and interactive way.");
|
| 1314 |
-
}
|
| 1315 |
-
}, 2000);
|
| 1316 |
-
|
| 1317 |
-
// Set up Enter key for chat
|
| 1318 |
-
const messageInput = document.getElementById('messageInput');
|
| 1319 |
-
if (messageInput) {
|
| 1320 |
-
messageInput.addEventListener('keypress', (e) => {
|
| 1321 |
-
if (e.key === 'Enter') {
|
| 1322 |
-
sendMessage();
|
| 1323 |
-
}
|
| 1324 |
-
});
|
| 1325 |
-
}
|
| 1326 |
-
|
| 1327 |
-
// Check for achievements periodically
|
| 1328 |
-
setInterval(() => {
|
| 1329 |
-
checkAchievements();
|
| 1330 |
-
}, 10000);
|
| 1331 |
-
|
| 1332 |
-
// ربط الأزرار يدويًا بعد تحميل الصفحة
|
| 1333 |
-
const btnStart = document.querySelector('.voice-controls .btn--primary');
|
| 1334 |
-
const btnMute = document.querySelector('.voice-controls .btn--secondary');
|
| 1335 |
-
const btnEnd = document.querySelector('.voice-controls .btn--danger');
|
| 1336 |
-
if (btnStart) btnStart.onclick = startVoiceLoop;
|
| 1337 |
-
if (btnMute) btnMute.onclick = toggleMute;
|
| 1338 |
-
if (btnEnd) btnEnd.onclick = stopVoiceLoop;
|
| 1339 |
-
setVoiceControlButtonsState();
|
| 1340 |
-
});
|
| 1341 |
-
|
| 1342 |
-
// Export functions for global access
|
| 1343 |
-
window.showScreen = showScreen;
|
| 1344 |
-
window.sendMessage = sendMessage;
|
| 1345 |
-
window.startVoiceInput = startVoiceInput;
|
| 1346 |
-
window.flipCard = flipCard;
|
| 1347 |
-
window.nextCard = nextCard;
|
| 1348 |
-
window.playPronunciation = playPronunciation;
|
| 1349 |
-
window.playTargetPronunciation = playTargetPronunciation;
|
| 1350 |
-
window.toggleRecording = toggleRecording;
|
| 1351 |
-
window.selectGrammarOption = selectGrammarOption;
|
| 1352 |
-
window.nextGrammarQuestion = nextGrammarQuestion;
|
| 1353 |
-
window.showScenario = showScenario;
|
| 1354 |
-
window.practiceScenario = practiceScenario;
|
| 1355 |
-
window.selectWord = selectWord;
|
| 1356 |
-
window.toggleSound = toggleSound;
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Flask, request, jsonify, send_from_directory
|
| 3 |
+
from flask_cors import CORS
|
| 4 |
+
import openai
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
app = Flask(__name__, static_folder='.', static_url_path='')
|
| 10 |
+
CORS(app)
|
| 11 |
+
|
| 12 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("NVIDIA_API_KEY")
|
| 13 |
+
openai.api_key = OPENAI_API_KEY
|
| 14 |
+
|
| 15 |
+
@app.route("/chat", methods=["POST"])
|
| 16 |
+
def chat():
|
| 17 |
+
data = request.get_json(force=True)
|
| 18 |
+
user_message = data.get("message", "")
|
| 19 |
+
if not user_message:
|
| 20 |
+
return jsonify({"reply": "Please say something!"})
|
| 21 |
+
try:
|
| 22 |
+
# Compatible with openai>=1.0.0
|
| 23 |
+
response = openai.chat.completions.create(
|
| 24 |
+
model="gpt-3.5-turbo", # or your NVIDIA LLM if available
|
| 25 |
+
messages=[{"role": "user", "content": user_message}]
|
| 26 |
+
)
|
| 27 |
+
reply = response.choices[0].message.content
|
| 28 |
+
return jsonify({"reply": reply})
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return jsonify({"reply": f"AI error: {str(e)}"})
|
| 31 |
+
|
| 32 |
+
@app.route('/')
|
| 33 |
+
def serve_index():
|
| 34 |
+
return send_from_directory('.', 'index.html')
|
| 35 |
+
|
| 36 |
+
@app.route('/<path:path>')
|
| 37 |
+
def serve_static(path):
|
| 38 |
+
if os.path.exists(path):
|
| 39 |
+
return send_from_directory('.', path)
|
| 40 |
+
return jsonify({'error': 'File not found'}), 404
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
port = int(os.environ.get("PORT", 7860))
|
| 44 |
+
app.run(host="0.0.0.0", port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|