Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/2024-1-0782dapdapog-stack_codequest.json
T

1 line
31 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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})();"}}}}