{"repo": "2024-1-0782dapdapog-stack/codequest", "files": {"js/mentor.js": {"41552d28140e2e04059aa8985d4f1990cd6bd349": {"t": 1785768936.5641983, "n": 14476, "h": "14e1821b26f7e68b", "c": "/**\r\n * CODEQUEST – MENTOR UI\r\n * Voice guide selection, mentor portraits, and AI helper popup.\r\n */\r\n(function() {\r\n const STORAGE_KEYS = {\r\n mentorId: 'codequest_selected_mentor',\r\n aiMuted: 'codequest_ai_muted'\r\n };\r\n\r\n const mentorProfiles = [\r\n {\r\n id: 'clara',\r\n label: 'Clara',\r\n voiceHints: ['zira', 'samantha', 'serena', 'female', 'clara'],\r\n langHints: ['en-us', 'en-gb'],\r\n rate: 0.98,\r\n pitch: 1.05,\r\n icon: 'Pixel Art/Clara/Clara_Icon.png',\r\n happy: 'Pixel Art/Clara/Clara_Happy.png',\r\n sad: 'Pixel Art/Clara/Clara_Sad.png',\r\n talking: 'Pixel Art/Clara/Clara_Talking.png'\r\n },\r\n {\r\n id: 'client',\r\n label: 'Client',\r\n voiceHints: ['david', 'male', 'alex', 'daniel', 'client'],\r\n langHints: ['en-us', 'en-gb'],\r\n rate: 0.95,\r\n pitch: 0.92,\r\n icon: 'Pixel Art/Client/Client_Icon.png',\r\n happy: 'Pixel Art/Client/Client_Happy.png',\r\n sad: 'Pixel Art/Client/Client_Sad.png',\r\n talking: 'Pixel Art/Client/Client_Talking.png'\r\n },\r\n {\r\n id: 'kenji',\r\n label: 'Kenji',\r\n voiceHints: ['kenji', 'japan', 'haruka', 'takumi', 'male'],\r\n langHints: ['ja', 'en-us'],\r\n rate: 0.92,\r\n pitch: 0.97,\r\n icon: 'Pixel Art/Kenji/Kenji_Icon.png',\r\n happy: 'Pixel Art/Kenji/Kenji_Happy.png',\r\n sad: 'Pixel Art/Kenji/Kenji_Sad.png',\r\n talking: 'Pixel Art/Kenji/Kenji_Talking.png'\r\n },\r\n {\r\n id: 'scarlet',\r\n label: 'Scarlet',\r\n voiceHints: ['scarlet', 'female', 'victoria', 'fiona', 'microsoft zira'],\r\n langHints: ['en-gb', 'en-us'],\r\n rate: 1.01,\r\n pitch: 1.12,\r\n icon: 'Pixel Art/Scarlet/Scarlet_Icon.png',\r\n happy: 'Pixel Art/Scarlet/Scarlet_Happy.png',\r\n sad: 'Pixel Art/Scarlet/Scarlet_Sad.png',\r\n talking: 'Pixel Art/Scarlet/Scarlet_Talking.png'\r\n }\r\n ];\r\n\r\n const state = {\r\n mentorId: 'clara',\r\n mood: 'idle',\r\n talking: false,\r\n aiMuted: false,\r\n aiOpen: false,\r\n voicesReady: false,\r\n voices: []\r\n };\r\n\r\n let elements = {};\r\n\r\n function safeGetStorage(key, fallback) {\r\n try {\r\n const value = localStorage.getItem(key);\r\n return value === null ? fallback : value;\r\n } catch (e) {\r\n return fallback;\r\n }\r\n }\r\n\r\n function safeSetStorage(key, value) {\r\n try {\r\n localStorage.setItem(key, value);\r\n } catch (e) {}\r\n }\r\n\r\n function getMentorProfile(mentorId) {\r\n return mentorProfiles.find((profile) => profile.id === mentorId) || mentorProfiles[0];\r\n }\r\n\r\n function portraitFor(profile, mood) {\r\n const source = mood === 'talking'\r\n ? profile.talking\r\n : mood === 'happy'\r\n ? profile.happy\r\n : mood === 'sad'\r\n ? profile.sad\r\n : profile.icon;\r\n return encodeURI(source);\r\n }\r\n\r\n function getActiveMood() {\r\n return state.talking ? 'talking' : state.mood;\r\n }\r\n\r\n function syncMentorDisplay() {\r\n const profile = getMentorProfile(state.mentorId);\r\n const activeMood = getActiveMood();\r\n const portrait = portraitFor(profile, activeMood);\r\n\r\n document.querySelectorAll('[data-mentor-portrait]').forEach((node) => {\r\n node.src = portrait;\r\n node.alt = profile.label + ' ' + activeMood + ' portrait';\r\n });\r\n\r\n document.querySelectorAll('[data-mentor-option]').forEach((button) => {\r\n const selected = button.dataset.mentorId === profile.id;\r\n button.classList.toggle('selected', selected);\r\n button.setAttribute('aria-checked', selected ? 'true' : 'false');\r\n });\r\n\r\n if (elements.aiMuteBtn) {\r\n elements.aiMuteBtn.textContent = state.aiMuted ? '🔇' : '🔊';\r\n elements.aiMuteBtn.title = state.aiMuted ? 'Unmute AI voice' : 'Mute AI voice';\r\n }\r\n }\r\n\r\n function setMentorMood(mood) {\r\n state.mood = mood === 'happy' || mood === 'sad' ? mood : 'idle';\r\n syncMentorDisplay();\r\n return state.mood;\r\n }\r\n\r\n function setMentorTalking(talking) {\r\n state.talking = !!talking;\r\n syncMentorDisplay();\r\n return state.talking;\r\n }\r\n\r\n function setSelectedMentorId(mentorId) {\r\n state.mentorId = getMentorProfile(mentorId).id;\r\n safeSetStorage(STORAGE_KEYS.mentorId, state.mentorId);\r\n syncMentorDisplay();\r\n return state.mentorId;\r\n }\r\n\r\n function getSelectedMentorProfile() {\r\n return getMentorProfile(state.mentorId);\r\n }\r\n\r\n function refreshVoices() {\r\n if (!window.speechSynthesis || typeof window.speechSynthesis.getVoices !== 'function') return;\r\n state.voices = window.speechSynthesis.getVoices() || [];\r\n state.voicesReady = state.voices.length > 0;\r\n }\r\n\r\n function pickSpeechVoice(profile) {\r\n refreshVoices();\r\n if (!state.voices.length) return null;\r\n\r\n const loweredHints = profile.voiceHints.map((hint) => hint.toLowerCase());\r\n const loweredLangHints = profile.langHints.map((hint) => hint.toLowerCase());\r\n\r\n const scored = state.voices.map((voice) => {\r\n const name = (voice.name || '').toLowerCase();\r\n const lang = (voice.lang || '').toLowerCase();\r\n let score = 0;\r\n loweredHints.forEach((hint) => {\r\n if (name.includes(hint)) score += 4;\r\n });\r\n loweredLangHints.forEach((hint) => {\r\n if (lang.startsWith(hint)) score += 2;\r\n });\r\n if (name.includes('microsoft')) score += 1;\r\n return { voice, score };\r\n }).sort((left, right) => right.score - left.score);\r\n\r\n return scored.length && scored[0].score > 0\r\n ? scored[0].voice\r\n : state.voices[0];\r\n }\r\n\r\n function pickAiVoice() {\r\n refreshVoices();\r\n if (!state.voices.length) return null;\r\n\r\n const syntheticHints = ['zira', 'david', 'microsoft', 'google', 'female', 'male'];\r\n const scored = state.voices.map((voice) => {\r\n const name = (voice.name || '').toLowerCase();\r\n let score = 0;\r\n syntheticHints.forEach((hint) => {\r\n if (name.includes(hint)) score += 1;\r\n });\r\n return { voice, score };\r\n }).sort((left, right) => right.score - left.score);\r\n\r\n return scored.length ? scored[0].voice : null;\r\n }\r\n\r\n function setAiMuted(muted) {\r\n state.aiMuted = !!muted;\r\n safeSetStorage(STORAGE_KEYS.aiMuted, state.aiMuted ? '1' : '0');\r\n if (state.aiMuted && window.speechSynthesis) {\r\n window.speechSynthesis.cancel();\r\n }\r\n syncMentorDisplay();\r\n return state.aiMuted;\r\n }\r\n\r\n function getAiMuted() {\r\n return state.aiMuted;\r\n }\r\n\r\n function openAiPanel() {\r\n if (!elements.aiPanel) return;\r\n state.aiOpen = true;\r\n elements.aiPanel.classList.add('open');\r\n elements.aiPanel.setAttribute('aria-hidden', 'false');\r\n if (elements.aiInput) elements.aiInput.focus();\r\n if (elements.aiMessages && !elements.aiMessages.childElementCount) {\r\n addAiMessage('bot', 'Hi, I am the AI helper. Ask me for a hint or tell me the level you are stuck on.');\r\n }\r\n }\r\n\r\n function closeAiPanel() {\r\n if (!elements.aiPanel) return;\r\n state.aiOpen = false;\r\n elements.aiPanel.classList.remove('open');\r\n elements.aiPanel.setAttribute('aria-hidden', 'true');\r\n }\r\n\r\n function toggleAiPanel() {\r\n if (state.aiOpen) closeAiPanel();\r\n else openAiPanel();\r\n }\r\n\r\n function addAiMessage(role, text) {\r\n if (!elements.aiMessages) return null;\r\n const message = document.createElement('div');\r\n message.className = 'ai-message ' + (role === 'user' ? 'user' : 'bot');\r\n message.textContent = text;\r\n elements.aiMessages.appendChild(message);\r\n elements.aiMessages.scrollTop = elements.aiMessages.scrollHeight;\r\n return message;\r\n }\r\n\r\n function speakAiText(text) {\r\n if (state.aiMuted || !text || !window.speechSynthesis || !window.SpeechSynthesisUtterance) return;\r\n try {\r\n window.speechSynthesis.cancel();\r\n const utterance = new SpeechSynthesisUtterance(text);\r\n const voice = pickAiVoice();\r\n if (voice) utterance.voice = voice;\r\n utterance.rate = 1.06;\r\n utterance.pitch = 0.72;\r\n utterance.volume = 1;\r\n window.speechSynthesis.speak(utterance);\r\n } catch (e) {}\r\n }\r\n\r\n function buildAiContext() {\r\n const levelTitle = document.getElementById('level-title');\r\n const instructions = document.getElementById('instructions');\r\n const title = levelTitle ? levelTitle.textContent : 'current level';\r\n const task = instructions ? (instructions.textContent || '').replace(/\\s+/g, ' ').trim() : '';\r\n return { title, task };\r\n }\r\n\r\n function fallbackAiReply(userText) {\r\n const prompt = (userText || '').toLowerCase();\r\n const context = buildAiContext();\r\n\r\n if (prompt.includes('html') || context.title.toLowerCase().includes('html')) {\r\n return 'Check the required tag name, then compare your markup to the instructions. If you are stuck, ask me for the exact structure.';\r\n }\r\n if (prompt.includes('css') || context.title.toLowerCase().includes('css')) {\r\n return 'Focus on selector, property, and value. The fastest fix is usually a missing colon, brace, or class name.';\r\n }\r\n if (prompt.includes('javascript') || prompt.includes('js')) {\r\n return 'Look for the event or function the task asks for, then match the variable and method names exactly.';\r\n }\r\n return 'Tell me the level name or paste the code you are trying to write, and I will give you a targeted hint.';\r\n }\r\n\r\n async function fetchAiReply(userText) {\r\n const endpoint = window.CODEQUEST_AI_ENDPOINT || window.CODEQUEST_AI_API_URL || safeGetStorage('codequest_ai_endpoint', '');\r\n const apiKey = window.CODEQUEST_AI_API_KEY || safeGetStorage('codequest_ai_api_key', '');\r\n\r\n if (!endpoint) return fallbackAiReply(userText);\r\n\r\n const context = buildAiContext();\r\n const requestBody = {\r\n model: window.CODEQUEST_AI_MODEL || 'gpt-4o-mini',\r\n messages: [\r\n {\r\n role: 'system',\r\n content: 'You are the CodeQuest AI helper. Answer briefly, clearly, and with coding-specific help. Keep replies under 90 words when possible.'\r\n },\r\n {\r\n role: 'system',\r\n content: 'Current level: ' + context.title + '. Instructions: ' + context.task\r\n },\r\n {\r\n role: 'user',\r\n content: userText\r\n }\r\n ],\r\n temperature: 0.4\r\n };\r\n\r\n const response = await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...(apiKey ? { Authorization: 'Bearer ' + apiKey } : {})\r\n },\r\n body: JSON.stringify(requestBody)\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error('AI request failed with status ' + response.status);\r\n }\r\n\r\n const data = await response.json();\r\n const reply = data?.choices?.[0]?.message?.content\r\n || data?.output_text\r\n || data?.message\r\n || data?.text\r\n || '';\r\n\r\n return (reply || fallbackAiReply(userText)).toString().trim();\r\n }\r\n\r\n async function handleAiSubmit(userText) {\r\n const cleanText = (userText || '').trim();\r\n if (!cleanText) return;\r\n\r\n addAiMessage('user', cleanText);\r\n if (elements.aiInput) elements.aiInput.value = '';\r\n if (elements.aiStatus) elements.aiStatus.textContent = 'Thinking...';\r\n\r\n let replyText = '';\r\n try {\r\n replyText = await fetchAiReply(cleanText);\r\n } catch (e) {\r\n replyText = fallbackAiReply(cleanText);\r\n }\r\n\r\n addAiMessage('bot', replyText);\r\n if (elements.aiStatus) elements.aiStatus.textContent = state.aiMuted ? 'Muted' : 'Ready';\r\n speakAiText(replyText);\r\n }\r\n\r\n function initMentorPicker() {\r\n document.querySelectorAll('[data-mentor-option]').forEach((button) => {\r\n button.addEventListener('click', () => {\r\n setSelectedMentorId(button.dataset.mentorId);\r\n });\r\n });\r\n syncMentorDisplay();\r\n }\r\n\r\n function initAiUi() {\r\n elements = {\r\n launcher: document.getElementById('ai-launcher'),\r\n panel: document.getElementById('ai-panel'),\r\n messages: document.getElementById('ai-messages'),\r\n form: document.getElementById('ai-form'),\r\n input: document.getElementById('ai-input'),\r\n sendBtn: document.getElementById('ai-send-btn'),\r\n muteBtn: document.getElementById('ai-mute-btn'),\r\n closeBtn: document.getElementById('ai-close-btn'),\r\n status: document.getElementById('ai-status')\r\n };\r\n\r\n if (!elements.launcher || !elements.panel) return;\r\n\r\n state.aiMuted = safeGetStorage(STORAGE_KEYS.aiMuted, '0') === '1';\r\n syncMentorDisplay();\r\n\r\n elements.launcher.addEventListener('click', toggleAiPanel);\r\n elements.closeBtn?.addEventListener('click', closeAiPanel);\r\n elements.muteBtn?.addEventListener('click', () => setAiMuted(!state.aiMuted));\r\n elements.form?.addEventListener('submit', (event) => {\r\n event.preventDefault();\r\n handleAiSubmit(elements.input ? elements.input.value : '');\r\n });\r\n elements.input?.addEventListener('keydown', (event) => {\r\n if (event.key === 'Escape') closeAiPanel();\r\n });\r\n\r\n if (window.speechSynthesis) {\r\n window.speechSynthesis.onvoiceschanged = refreshVoices;\r\n }\r\n\r\n if (state.aiMuted) {\r\n elements.muteBtn.textContent = '🔇';\r\n elements.muteBtn.title = 'Unmute AI voice';\r\n }\r\n }\r\n\r\n function initMentorSelection() {\r\n state.mentorId = getMentorProfile(safeGetStorage(STORAGE_KEYS.mentorId, 'clara')).id;\r\n syncMentorDisplay();\r\n }\r\n\r\n window.getSelectedMentorProfile = getSelectedMentorProfile;\r\n window.getSelectedMentorId = function() { return state.mentorId; };\r\n window.setSelectedMentorId = setSelectedMentorId;\r\n window.setMentorMood = setMentorMood;\r\n window.setMentorTalking = setMentorTalking;\r\n window.getMentorTalking = function() { return state.talking; };\r\n window.getMentorPortraitState = getActiveMood;\r\n window.getMentorVoiceForProfile = pickSpeechVoice;\r\n window.getAiMuted = getAiMuted;\r\n window.setAiMuted = setAiMuted;\r\n window.toggleAiMuted = function() { return setAiMuted(!state.aiMuted); };\r\n window.openAiHelper = openAiPanel;\r\n window.closeAiHelper = closeAiPanel;\r\n window.toggleAiHelper = toggleAiPanel;\r\n window.sendAiHelperMessage = handleAiSubmit;\r\n window.addAiHelperMessage = addAiMessage;\r\n\r\n function init() {\r\n initMentorSelection();\r\n initMentorPicker();\r\n initAiUi();\r\n syncMentorDisplay();\r\n }\r\n\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', init, { once: true });\r\n } else {\r\n init();\r\n }\r\n})();"}}, "js/mentor-ui.js": {"41552d28140e2e04059aa8985d4f1990cd6bd349": {"t": 1785768943.7658768, "n": 15040, "h": "8221cad37a204be6", "c": "/**\r\n * CODEQUEST – MENTOR UI\r\n * Mentor picker, avatar badge, and bottom-right AI helper.\r\n */\r\n(function() {\r\n const state = {\r\n aiOpen: false,\r\n voicePickerOpen: false,\r\n selectedMentorId: null,\r\n elements: {}\r\n };\r\n\r\n function getVoiceProfile(mentorId) {\r\n const profiles = window.CODEQUEST_MENTOR_PROFILES || [];\r\n console.log('getVoiceProfile called with:', mentorId, 'profiles count:', profiles.length);\r\n const profile = profiles.find((profile) => profile.id === mentorId) || profiles[0] || null;\r\n console.log('Returning profile:', profile ? profile.id : 'null');\r\n return profile;\r\n }\r\n\r\n function getActivePickerMentorId() {\r\n // First check global state (mentor-core.js) - this is the source of truth\r\n if (window.getSelectedMentorId) {\r\n const currentId = window.getSelectedMentorId();\r\n if (currentId) {\r\n state.selectedMentorId = currentId;\r\n return currentId;\r\n }\r\n }\r\n // Then check local state\r\n if (state.selectedMentorId) return state.selectedMentorId;\r\n // Then check window global\r\n if (window.__codequestSelectedMentorId) return window.__codequestSelectedMentorId;\r\n // Then check UI selection\r\n const selectedButton = state.elements.voicePicker ? state.elements.voicePicker.querySelector('[data-tts-option].selected, [data-tts-option][aria-checked=\"true\"]') : null;\r\n if (selectedButton && selectedButton.dataset.mentorId) return selectedButton.dataset.mentorId;\r\n // Default to clara\r\n return 'clara';\r\n }\r\n\r\n function getVoiceIntro(profile) {\r\n if (!profile) return 'Pick a mentor to hear a short introduction before you start.';\r\n if (window.getMentorIntroText) return window.getMentorIntroText(profile.id);\r\n return profile.intro || ('Hi, I am ' + profile.label + '.');\r\n }\r\n\r\n function syncAiMuteButton() {\r\n const button = state.elements.aiMuteBtn;\r\n if (!button) return;\r\n const muted = window.getAiMuted ? window.getAiMuted() : false;\r\n button.textContent = muted ? '🔇' : '🔊';\r\n button.title = muted ? 'Unmute AI voice' : 'Mute AI voice';\r\n }\r\n\r\n function syncMentorBadge() {\r\n if (window.syncMentorDisplay) window.syncMentorDisplay();\r\n syncVoicePicker();\r\n }\r\n\r\n function syncVoicePicker() {\r\n const panel = state.elements.voicePicker;\r\n if (!panel) return;\r\n\r\n const selectedId = getActivePickerMentorId();\r\n\r\n panel.querySelectorAll('[data-tts-option]').forEach((button) => {\r\n const selected = button.dataset.mentorId === selectedId;\r\n button.classList.toggle('selected', selected);\r\n button.setAttribute('aria-checked', selected ? 'true' : 'false');\r\n });\r\n }\r\n\r\n function commitVoiceSelection(mentorId, speak) {\r\n console.log('commitVoiceSelection called with:', mentorId, 'speak:', speak);\r\n const profile = getVoiceProfile(mentorId);\r\n if (!profile) {\r\n console.log('ERROR: No profile found for mentorId:', mentorId);\r\n return;\r\n }\r\n console.log('Selected mentor:', profile.id, profile.label);\r\n state.selectedMentorId = profile.id;\r\n window.__codequestSelectedMentorId = profile.id;\r\n \r\n // Set global flag for Scarlet to force ResponsiveVoice\r\n window.__isScarletSelected = (profile.id === 'scarlet');\r\n \r\n if (window.setSelectedMentorId) {\r\n console.log('Calling setSelectedMentorId:', profile.id);\r\n window.setSelectedMentorId(profile.id);\r\n }\r\n\r\n // Direct DOM update - update portrait immediately\r\n document.querySelectorAll('[data-mentor-portrait]').forEach((node) => {\r\n node.src = profile.icon;\r\n node.alt = profile.label + ' portrait';\r\n });\r\n\r\n const intro = getVoiceIntro(profile);\r\n if (window.setMentorMood) window.setMentorMood('idle');\r\n if (window.setMentorDialogueText) window.setMentorDialogueText(intro);\r\n if (speak && window.speakMentorText) {\r\n if (window.getTtsMuted && window.setTtsMuted && window.getTtsMuted()) {\r\n window.setTtsMuted(false);\r\n }\r\n window.speakMentorText(intro);\r\n }\r\n\r\n syncVoicePicker();\r\n syncMentorBadge();\r\n if (window.syncMentorDisplay) window.syncMentorDisplay();\r\n }\r\n\r\n function openVoicePicker() {\r\n if (!state.elements.voicePicker) {\r\n state.elements.voicePicker = document.getElementById('tts-picker');\r\n }\r\n if (!state.elements.voicePicker) return;\r\n state.voicePickerOpen = true;\r\n state.selectedMentorId = getActivePickerMentorId();\r\n syncMentorBadge();\r\n state.elements.voicePicker.classList.add('open');\r\n state.elements.voicePicker.setAttribute('aria-hidden', 'false');\r\n state.elements.voicePicker.style.opacity = '1';\r\n state.elements.voicePicker.style.pointerEvents = 'auto';\r\n state.elements.voicePicker.style.visibility = 'visible';\r\n syncVoicePicker();\r\n }\r\n\r\n function closeVoicePicker() {\r\n if (!state.elements.voicePicker) return;\r\n state.voicePickerOpen = false;\r\n state.elements.voicePicker.classList.remove('open');\r\n state.elements.voicePicker.setAttribute('aria-hidden', 'true');\r\n state.elements.voicePicker.style.opacity = '';\r\n state.elements.voicePicker.style.pointerEvents = '';\r\n state.elements.voicePicker.style.visibility = '';\r\n }\r\n\r\n function openAiPanel() {\r\n if (!state.elements.aiPanel) return;\r\n state.aiOpen = true;\r\n state.elements.aiPanel.classList.add('open');\r\n state.elements.aiPanel.setAttribute('aria-hidden', 'false');\r\n if (state.elements.aiInput) state.elements.aiInput.focus();\r\n if (state.elements.aiMessages && !state.elements.aiMessages.childElementCount) {\r\n addAiMessage('bot', 'Hi, I am the AI helper. Ask me for a hint or tell me the level you are stuck on.');\r\n }\r\n }\r\n\r\n function closeAiPanel() {\r\n if (!state.elements.aiPanel) return;\r\n state.aiOpen = false;\r\n state.elements.aiPanel.classList.remove('open');\r\n state.elements.aiPanel.setAttribute('aria-hidden', 'true');\r\n }\r\n\r\n function toggleAiPanel() {\r\n if (state.aiOpen) closeAiPanel();\r\n else openAiPanel();\r\n }\r\n\r\n function addAiMessage(role, text) {\r\n if (!state.elements.aiMessages) return null;\r\n const message = document.createElement('div');\r\n message.className = 'ai-message ' + (role === 'user' ? 'user' : 'bot');\r\n message.textContent = text;\r\n state.elements.aiMessages.appendChild(message);\r\n state.elements.aiMessages.scrollTop = state.elements.aiMessages.scrollHeight;\r\n return message;\r\n }\r\n\r\n function speakAiText(text) {\r\n if (window.getAiMuted && window.getAiMuted()) return;\r\n if (!text || !window.speechSynthesis || !window.SpeechSynthesisUtterance) return;\r\n try {\r\n window.speechSynthesis.cancel();\r\n const utterance = new SpeechSynthesisUtterance(text);\r\n const voice = window.getAiVoiceForHelper ? window.getAiVoiceForHelper() : null;\r\n if (voice) utterance.voice = voice;\r\n utterance.rate = 1.06;\r\n utterance.pitch = 0.72;\r\n utterance.volume = 1;\r\n window.speechSynthesis.speak(utterance);\r\n } catch (e) {}\r\n }\r\n\r\n function buildAiContext() {\r\n const levelTitle = document.getElementById('level-title');\r\n const instructions = document.getElementById('instructions');\r\n const title = levelTitle ? levelTitle.textContent : 'current level';\r\n const task = instructions ? (instructions.textContent || '').replace(/\\s+/g, ' ').trim() : '';\r\n return { title, task };\r\n }\r\n\r\n function fallbackAiReply(userText) {\r\n const prompt = (userText || '').toLowerCase();\r\n const context = buildAiContext();\r\n\r\n if (prompt.includes('html') || context.title.toLowerCase().includes('html')) {\r\n return 'Check the required tag name, then compare your markup to the instructions. If you are stuck, ask me for the exact structure.';\r\n }\r\n if (prompt.includes('css') || context.title.toLowerCase().includes('css')) {\r\n return 'Focus on selector, property, and value. The fastest fix is usually a missing colon, brace, or class name.';\r\n }\r\n if (prompt.includes('javascript') || prompt.includes('js')) {\r\n return 'Look for the event or function the task asks for, then match the variable and method names exactly.';\r\n }\r\n return 'Tell me the level name or paste the code you are trying to write, and I will give you a targeted hint.';\r\n }\r\n\r\n async function fetchAiReply(userText) {\r\n const endpoint = window.CODEQUEST_AI_ENDPOINT || window.CODEQUEST_AI_API_URL || '';\r\n const apiKey = window.CODEQUEST_AI_API_KEY || '';\r\n\r\n if (!endpoint) return fallbackAiReply(userText);\r\n\r\n const context = buildAiContext();\r\n const requestBody = {\r\n model: window.CODEQUEST_AI_MODEL || 'gpt-4o-mini',\r\n messages: [\r\n {\r\n role: 'system',\r\n content: 'You are the CodeQuest AI helper. Answer briefly, clearly, and with coding-specific help. Keep replies under 90 words when possible.'\r\n },\r\n {\r\n role: 'system',\r\n content: 'Current level: ' + context.title + '. Instructions: ' + context.task\r\n },\r\n {\r\n role: 'user',\r\n content: userText\r\n }\r\n ],\r\n temperature: 0.4\r\n };\r\n\r\n const response = await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...(apiKey ? { Authorization: 'Bearer ' + apiKey } : {})\r\n },\r\n body: JSON.stringify(requestBody)\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error('AI request failed with status ' + response.status);\r\n }\r\n\r\n const data = await response.json();\r\n const reply = data?.choices?.[0]?.message?.content\r\n || data?.output_text\r\n || data?.message\r\n || data?.text\r\n || '';\r\n\r\n return (reply || fallbackAiReply(userText)).toString().trim();\r\n }\r\n\r\n async function handleAiSubmit(userText) {\r\n const cleanText = (userText || '').trim();\r\n if (!cleanText) return;\r\n\r\n addAiMessage('user', cleanText);\r\n if (state.elements.aiInput) state.elements.aiInput.value = '';\r\n if (state.elements.aiStatus) state.elements.aiStatus.textContent = 'Thinking...';\r\n\r\n let replyText = '';\r\n try {\r\n replyText = await fetchAiReply(cleanText);\r\n } catch (e) {\r\n replyText = fallbackAiReply(cleanText);\r\n }\r\n\r\n addAiMessage('bot', replyText);\r\n if (state.elements.aiStatus) state.elements.aiStatus.textContent = window.getAiMuted && window.getAiMuted() ? 'Muted' : 'Ready';\r\n speakAiText(replyText);\r\n }\r\n\r\n function initMentorPicker() {\r\n document.querySelectorAll('[data-mentor-option]').forEach((button) => {\r\n button.addEventListener('click', () => {\r\n const mentorId = button.getAttribute('data-mentor-id') || button.dataset.mentorId;\r\n commitVoiceSelection(mentorId, true);\r\n });\r\n });\r\n syncMentorBadge();\r\n }\r\n\r\n function initVoicePicker() {\r\n state.elements.voicePicker = document.getElementById('tts-picker');\r\n state.elements.voicePickerContinue = document.querySelector('[data-tts-continue]');\r\n state.elements.voicePickerBack = document.querySelector('[data-tts-back]');\r\n\r\n if (!state.elements.voicePicker) return;\r\n\r\n state.elements.voicePicker.querySelectorAll('[data-tts-option]').forEach((button) => {\r\n button.addEventListener('click', () => {\r\n const mentorId = button.getAttribute('data-mentor-id') || button.dataset.mentorId;\r\n commitVoiceSelection(mentorId, true);\r\n });\r\n });\r\n\r\n state.elements.voicePickerContinue?.addEventListener('click', () => {\r\n const selectedButton = state.elements.voicePicker.querySelector('[data-tts-option].selected, [data-tts-option][aria-checked=\"true\"]');\r\n const mentorId = selectedButton ? (selectedButton.getAttribute('data-mentor-id') || selectedButton.dataset.mentorId) : getActivePickerMentorId();\r\n if (mentorId) commitVoiceSelection(mentorId, false);\r\n closeVoicePicker();\r\n if (window.startCodeQuestGame) window.startCodeQuestGame();\r\n });\r\n\r\n state.elements.voicePickerBack?.addEventListener('click', closeVoicePicker);\r\n\r\n document.querySelectorAll('[data-tts-close]').forEach((button) => {\r\n button.addEventListener('click', closeVoicePicker);\r\n });\r\n\r\n state.elements.voicePicker.addEventListener('click', (event) => {\r\n if (event.target === state.elements.voicePicker) closeVoicePicker();\r\n });\r\n\r\n syncVoicePicker();\r\n }\r\n\r\n function initAiUi() {\r\n state.elements = {\r\n ...state.elements,\r\n launcher: document.getElementById('ai-launcher'),\r\n panel: document.getElementById('ai-panel'),\r\n messages: document.getElementById('ai-messages'),\r\n form: document.getElementById('ai-form'),\r\n input: document.getElementById('ai-input'),\r\n sendBtn: document.getElementById('ai-send-btn'),\r\n muteBtn: document.getElementById('ai-mute-btn'),\r\n closeBtn: document.getElementById('ai-close-btn'),\r\n status: document.getElementById('ai-status')\r\n };\r\n\r\n if (!state.elements.launcher || !state.elements.panel) return;\r\n\r\n syncAiMuteButton();\r\n\r\n state.elements.launcher.addEventListener('click', toggleAiPanel);\r\n state.elements.closeBtn?.addEventListener('click', closeAiPanel);\r\n state.elements.muteBtn?.addEventListener('click', () => {\r\n if (window.toggleAiMuted) window.toggleAiMuted();\r\n syncAiMuteButton();\r\n });\r\n state.elements.form?.addEventListener('submit', (event) => {\r\n event.preventDefault();\r\n handleAiSubmit(state.elements.input ? state.elements.input.value : '');\r\n });\r\n state.elements.input?.addEventListener('keydown', (event) => {\r\n if (event.key === 'Escape') closeAiPanel();\r\n });\r\n }\r\n\r\n function init() {\r\n console.log('mentor-ui init starting...');\r\n initMentorPicker();\r\n initVoicePicker();\r\n initAiUi();\r\n // Sync mentor display on load - get from mentor-core state\r\n const currentMentorId = window.getSelectedMentorId ? window.getSelectedMentorId() : 'clara';\r\n console.log('Init - currentMentorId from getSelectedMentorId:', currentMentorId);\r\n const profile = getVoiceProfile(currentMentorId);\r\n console.log('Init - profile:', profile ? profile.id : 'null');\r\n if (profile) {\r\n document.querySelectorAll('[data-mentor-portrait]').forEach((node) => {\r\n node.src = profile.icon;\r\n node.alt = profile.label + ' portrait';\r\n });\r\n }\r\n if (window.syncMentorDisplay) window.syncMentorDisplay();\r\n syncMentorBadge();\r\n console.log('mentor-ui init complete');\r\n }\r\n\r\n window.syncAiMuteButton = syncAiMuteButton;\r\n window.openTtsVoicePicker = openVoicePicker;\r\n window.closeTtsVoicePicker = closeVoicePicker;\r\n window.openAiHelper = openAiPanel;\r\n window.closeAiHelper = closeAiPanel;\r\n window.toggleAiHelper = toggleAiPanel;\r\n window.sendAiHelperMessage = handleAiSubmit;\r\n window.addAiHelperMessage = addAiMessage;\r\n\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', init, { once: true });\r\n } else {\r\n init();\r\n }\r\n})();"}}}}