// ==UserScript== // @name AMAES Toolkit // @namespace https://semestral.amaes.com/ // @version 1.13.20 // @description Universal Study Toolkit for AMA Online Education (AMAOEd / AMAES) Moodle portals. Features Auto-Harvesting with Dynamic Fallback, Multi-Course Grades Harvester, AI Prompt Formatter, Cross-Attempt Database, Cloud Sync, and Auto-Quiz Solver. // @author Academic Contributor // @match https://semestral.amaes.com/* // @match http://semestral.amaes.com/* // @match *://semestral.amaes.com/* // @match https://semestralexam.amaes.com/* // @match http://semestralexam.amaes.com/* // @match *://semestralexam.amaes.com/* // @match https://acads-tools.github.io/amaes-toolkit/* // @match https://amaestoolkit.pages.dev/* // @updateURL https://raw.githubusercontent.com/Acads-Tools/amaes-toolkit/main/amaes-toolkit.user.js // @downloadURL https://raw.githubusercontent.com/Acads-Tools/amaes-toolkit/main/amaes-toolkit.user.js // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_notification // @connect raw.githubusercontent.com // @connect api.github.com // @connect amauoed.com // @connect jennysonline.blogspot.com // @connect docs.google.com // @connect amaes-community-relay.acads-tools.workers.dev // @connect generativelanguage.googleapis.com // @connect api.typesafe.ai // @run-at document-end // @license MIT // @homepageURL https://amaestoolkit.pages.dev/ // @supportURL https://github.com/Acads-Tools/amaes-toolkit/issues // ==/UserScript== (function () { 'use strict'; const SCRIPT_VERSION = "v1.13.20"; const CLIENT_VERSION = SCRIPT_VERSION.replace(/^v/i, ''); const COMMUNITY_RELAY_URL = 'https://amaes-community-relay.acads-tools.workers.dev'; const ANSWER_DB_SCHEMA_VERSION = 2; const ANSWER_SHARING_DISABLED_COURSES = new Set(['GE6301']); const CONTRIBUTOR_ID_STORAGE_KEY = 'amaes_anonymous_contributor_id'; const SUPPORTED_AMAES_HOSTS = ['semestral.amaes.com', 'semestralexam.amaes.com']; function isSupportedMoodleHost(host = (window.location && window.location.hostname)) { return SUPPORTED_AMAES_HOSTS.includes(host); } // ========================================== // Cross-Domain Persistent Storage Bridge (GM_storage <-> localStorage) // Ensures credentials, API keys, terms acceptance, and settings // seamlessly transfer between semestral.amaes.com and semestralexam.amaes.com // ========================================== const SHARED_STORAGE_KEYS = [ 'amaes_terms_acknowledged', 'amaes_welcome_dismissed', 'amaes_gemini_api_key', 'amaes_gemini_api_keys', 'amaes_jev_api_key', 'amaes_jev_endpoint', 'amaes_jev_enabled', 'amaes_shared_ai_fallback_enabled', 'amaes_ai_quiz_enabled', 'amaes_ai_auto_select', 'amaes_ai_retry_count', 'amaes_ai_auto_copy_on_fail', 'amaes_ai_auto_next_on_ai', 'amaes_ai_plan_tier', 'amaes_auto_quiz_mode', 'amaes_quiz_personality', 'amaes_auto_pick_quiz', 'amaes_auto_next_quiz', 'amaes_auto_next_verified', 'amaes_auto_pick_study_guide_fallback', 'amaes_auto_submit_quiz', 'amaes_auto_push_github', 'amaes_auto_copy_search', 'amaes_auto_cloud_sync', 'amaes_auto_scrape_amauoed', 'amaes_auto_harvest_grades', 'amaes_enable_hotkeys', 'amaes_skip_passed_quizzes', 'amaes_auto_close_course_index', 'amaes_auto_highlight_quiz', 'amaes_copy_include_confidence', 'amaes_auto_copy_ai', 'amaes_smart_skip_quiz', 'amaes_show_in_question_ai_btns', 'amaes_ai_prompt_hint', 'amaes_auto_community_share', 'amaes_auto_min_quiz', 'amaes_theme', 'amaes_active_tab', 'amaes_auto_submit_delay', 'amaes_anonymous_contributor_id' ]; const toolkitStorage = { get(key, defaultValue = null) { try { if (typeof GM_getValue === 'function') { const gmVal = GM_getValue(key, undefined); if (gmVal !== undefined && gmVal !== null) { try { localStorage.setItem(key, typeof gmVal === 'string' ? gmVal : JSON.stringify(gmVal)); } catch (_) {} return gmVal; } } } catch (_) {} try { const localVal = localStorage.getItem(key); if (localVal !== null && localVal !== undefined) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, localVal); } } catch (_) {} return localVal; } } catch (_) {} return defaultValue; }, set(key, value) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); } } catch (_) {} try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch (_) {} }, remove(key) { try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); } } catch (_) {} try { localStorage.removeItem(key); } catch (_) {} }, syncAll() { try { SHARED_STORAGE_KEYS.forEach(k => { const gmVal = (typeof GM_getValue === 'function') ? GM_getValue(k, undefined) : undefined; const localVal = localStorage.getItem(k); if ((gmVal === undefined || gmVal === null) && (localVal !== null && localVal !== undefined)) { if (typeof GM_setValue === 'function') { GM_setValue(k, localVal); } } else if (gmVal !== undefined && gmVal !== null && (localVal === null || localVal === undefined)) { localStorage.setItem(k, typeof gmVal === 'string' ? gmVal : JSON.stringify(gmVal)); } }); } catch (_) {} } }; toolkitStorage.syncAll(); // The installer page uses a same-page event as a privacy-safe installation check. if ((window.location.hostname === 'acads-tools.github.io' && window.location.pathname.startsWith('/amaes-toolkit')) || window.location.hostname === 'amaestoolkit.pages.dev') { document.documentElement.setAttribute( 'data-amaes-toolkit-version', SCRIPT_VERSION.replace(/^v/, '') ); document.dispatchEvent(new CustomEvent('amaes-toolkit-detected', { detail: { version: SCRIPT_VERSION.replace(/^v/, '') } })); return; } // STRICT DOMAIN LOCK: Ensure execution ONLY on supported AMAES Moodle portals if (!isSupportedMoodleHost(window.location.hostname)) { return; } function compareVersions(left, right) { const a = String(left || '').trim().replace(/^v/i, '').split('.').map(Number); const b = String(right || '').trim().replace(/^v/i, '').split('.').map(Number); if (a.length !== 3 || b.length !== 3 || a.some(Number.isNaN) || b.some(Number.isNaN)) return null; for (let i = 0; i < 3; i += 1) { if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1; } return 0; } function showCompatibilityBlock(reason, minimumVersion = null) { const required = minimumVersion || 'the latest supported version'; document.getElementById('amaes-compat-banner')?.remove(); // Close and remove any open toolkit panels, modals, quick info, settings, and HUDs const toolkitSelectors = [ '#amaes-panel', '#amaes-welcome-modal', '#amaes-gemini-modal', '#amaes-contribute-modal', '#amaes-dev-modal', '#amaes-quiz-hud', '#amaes-capability-tip', '#amaes-full-qr-modal', '.amaes-blockage-hud' ]; toolkitSelectors.forEach(sel => { document.querySelectorAll(sel).forEach(el => el.remove()); }); const banner = document.createElement('div'); banner.id = 'amaes-compat-banner'; banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999999;background:#0f172a;border-bottom:2px solid #ef4444;color:#f8fafc;padding:10px 16px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 4px 12px rgba(0,0,0,0.3);font-family:system-ui,sans-serif;font-size:14px;'; banner.innerHTML = `
Update to v${required} to use the tool
Update Now `; if (document.body) { document.body.prepend(banner); } else { document.addEventListener('DOMContentLoaded', () => document.body?.prepend(banner)); } } async function verifyClientCompatibility() { try { const cachedMin = localStorage.getItem('amaes_cached_min_version'); if (cachedMin) { const cachedComparison = compareVersions(CLIENT_VERSION, cachedMin); if (cachedComparison !== null && cachedComparison < 0) { showCompatibilityBlock('version', cachedMin); return false; } } } catch (_) {} const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 6000); try { const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; const response = await fetch(`${relayUrl}/version`, { method: 'GET', cache: 'no-store', signal: controller.signal }); if (!response.ok) throw new Error(`Compatibility endpoint returned ${response.status}`); const policy = await response.json(); const minimum = policy.minimumVersion; if (minimum) { try { localStorage.setItem('amaes_cached_min_version', String(minimum).trim()); if (policy.latestVersion) { localStorage.setItem('amaes_cached_latest_version', String(policy.latestVersion).trim()); } } catch (_) {} } if (policy.flags && typeof policy.flags === 'object') { try { localStorage.setItem('amaes_remote_flags', JSON.stringify(policy.flags)); } catch (_) {} } if (typeof policy.announcement !== 'undefined') { try { if (policy.announcement) { localStorage.setItem('amaes_remote_announcement', String(policy.announcement)); } else { localStorage.removeItem('amaes_remote_announcement'); } } catch (_) {} } const comparison = compareVersions(CLIENT_VERSION, minimum); // ONLY block if comparison succeeded and client is strictly below minimum if (comparison !== null && comparison < 0) { showCompatibilityBlock('version', minimum); return false; } document.getElementById('amaes-compat-banner')?.remove(); return true; } catch (error) { logDebug(`Client compatibility check failed: ${error.message}`); document.getElementById('amaes-compat-banner')?.remove(); // Temporary network interruptions, worker cold starts, slow latency, or timeouts // must never lock out or wipe the screen of an active student session. return true; } finally { clearTimeout(timeout); } } function getRemoteFeatureFlags() { try { return JSON.parse(localStorage.getItem('amaes_remote_flags') || '{}'); } catch (_) { return {}; } } function isFeatureDisabledByAdmin(featureKey) { const flags = getRemoteFeatureFlags(); if (flags.killSwitch === true) return true; if (featureKey && flags[featureKey] === false) return true; return false; } const SCRIPT_RAW_URL = "https://raw.githubusercontent.com/Acads-Tools/amaes-toolkit/main/amaes-toolkit.user.js"; const GITHUB_REPO_URL = "https://github.com/Acads-Tools/amaes-toolkit"; const GREASYFORK_URL = "https://greasyfork.org/en/scripts/594744-amaes-toolkit"; const WEBSITE_URL = "https://amaestoolkit.pages.dev/"; // Dynamic semester path detector (never hardcode e.g. /2612/ since term codes change every semester) function getSemesterBasePath() { if (typeof window !== 'undefined' && window.location) { const m = window.location.pathname.match(/^\/(\d{3,5})\//); if (m) { const path = `/${m[1]}/`; try { sessionStorage.setItem('amaes_detected_sem_path', path); localStorage.setItem('amaes_detected_sem_path', path); } catch (_) {} return path; } const anyDashLink = typeof document !== 'undefined' ? document.querySelector('a[href*="/my/courses.php"], a[href*="/my/"], a[href*="/course/"]') : null; if (anyDashLink) { const href = anyDashLink.getAttribute('href') || ''; const lm = href.match(/\/(\d{3,5})\//); if (lm) { const path = `/${lm[1]}/`; try { sessionStorage.setItem('amaes_detected_sem_path', path); localStorage.setItem('amaes_detected_sem_path', path); } catch (_) {} return path; } } try { const saved = sessionStorage.getItem('amaes_detected_sem_path') || localStorage.getItem('amaes_detected_sem_path'); if (saved) return saved; } catch (_) {} } return '/'; } function getSemesterCoursesUrl() { const origin = (typeof window !== 'undefined' && window.location && window.location.origin) ? window.location.origin : 'https://semestral.amaes.com'; if (origin.includes('semestralexam.amaes.com')) { return `${origin}${getSemesterBasePath()}`; } return `${origin}${getSemesterBasePath()}my/courses.php`; } // DEBUG SWITCH: Toggle to enable/disable the debug export button const DEBUG_MODE = true; // In-memory debug log buffer for AI diagnosis const debugLogs = []; function logDebug(msg, data = null) { const entry = `[${new Date().toLocaleTimeString()}] ${msg}${data ? ' ' + JSON.stringify(data) : ''}`; debugLogs.push(entry); console.log(`[AMAES Toolkit] ${entry}`); } // User Action & Reproduction Breadcrumbs Tracker const userBreadcrumbs = []; function recordBreadcrumb(category, action, details = null) { try { const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const entry = { time: timeStr, category: category || 'action', action: String(action || '').slice(0, 140), ...(details ? { details: String(details).slice(0, 180) } : {}) }; userBreadcrumbs.push(entry); if (userBreadcrumbs.length > 50) userBreadcrumbs.shift(); } catch (_) {} } // Activity State & Real-Time Logging Engine (Doing, Done, Plan) const activityHistory = []; let currentDoing = "Ready. Select a tool above."; let currentPlan = "Ready for action"; function setLog(doingMsg, color = null, planMsg = null, addToHistory = true) { if (doingMsg) currentDoing = doingMsg; if (planMsg) currentPlan = planMsg; const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); if (addToHistory && doingMsg) { const cleanDoing = doingMsg.replace(/<[^>]*>?/gm, ''); const cleanPlan = (planMsg || currentPlan).replace(/<[^>]*>?/gm, ''); const last = activityHistory[0]; if (!last || last.text !== cleanDoing) { activityHistory.unshift({ time: timeStr, text: cleanDoing, plan: cleanPlan, color: color || "var(--text-secondary)" }); if (activityHistory.length > 50) activityHistory.pop(); recordBreadcrumb('toolkit', cleanDoing); } } const statusEl = document.getElementById('amaes-status'); if (statusEl) { statusEl.innerHTML = currentDoing; statusEl.title = currentDoing.replace(/<[^>]*>?/gm, ''); if (color) statusEl.style.color = color; else statusEl.style.color = "var(--text-primary)"; } const planEl = document.getElementById('amaes-plan-text'); if (planEl) { planEl.innerHTML = currentPlan; planEl.title = currentPlan.replace(/<[^>]*>?/gm, ''); } const dotEl = document.getElementById('amaes-status-dot'); if (dotEl) { dotEl.style.background = color || "var(--accent-green, #10b981)"; dotEl.style.boxShadow = `0 0 8px ${color || 'rgba(16,185,129,0.7)'}`; dotEl.classList.remove('amaes-pulse'); void dotEl.offsetWidth; dotEl.classList.add('amaes-pulse'); } const countBadge = document.getElementById('amaes-log-count-badge'); if (countBadge) { countBadge.innerText = `Log (${activityHistory.length})`; } const logsList = document.getElementById('amaes-logs-list'); if (logsList && logsList.parentElement && logsList.parentElement.style.display !== 'none') { renderActivityLogs(); } logDebug(`[${timeStr}] DOING: ${currentDoing.replace(/<[^>]*>?/gm, '')} | PLAN: ${currentPlan.replace(/<[^>]*>?/gm, '')}`); } function renderActivityLogs() { const logsList = document.getElementById('amaes-logs-list'); if (!logsList) return; if (activityHistory.length === 0) { logsList.innerHTML = 'No recorded events yet.'; return; } logsList.innerHTML = activityHistory.slice(0, 15).map(item => `
${item.time} ${item.text}
`).join(''); } logDebug(`Toolkit initialized. Version: ${SCRIPT_VERSION}, Host: ${window.location.hostname}`); // Auto-remove any old/duplicate panel version if still present in DOM const oldPanel = document.getElementById('amaes-toolkit-panel') || document.getElementById('amaes-helper-panel'); if (oldPanel) oldPanel.remove(); let isRunning = false; let shouldStop = false; // Remote GitHub Asset Loader const TOOLKIT_LOGO_URL = "https://raw.githubusercontent.com/Acads-Tools/amaes-toolkit/main/assets/amaes-toolkit-logo.png"; // ========================================== // Unified Network Request Helper (Promise-based GM_xmlhttpRequest / fetch) // ========================================== function requestNetwork({ url, method = 'GET', headers = {}, data = null, responseType = 'text', timeout = 10000 }) { return new Promise((resolve, reject) => { const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { try { gmReq({ method, url, headers, data, responseType, timeout, onload: (res) => resolve(res), onerror: (err) => reject(new Error(err && err.statusText ? err.statusText : 'Network request failed')), ontimeout: () => reject(new Error('Network request timed out')) }); } catch (e) { reject(e); } } else { const fetchOptions = { method, headers, body: (method !== 'GET' && method !== 'HEAD') ? data : undefined }; const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null; let timer = null; if (controller && timeout) { fetchOptions.signal = controller.signal; timer = setTimeout(() => controller.abort(), timeout); } fetch(url, fetchOptions) .then(async (res) => { if (timer) clearTimeout(timer); const text = (responseType === 'blob') ? await res.blob() : await res.text(); resolve({ status: res.status, statusText: res.statusText, responseText: typeof text === 'string' ? text : '', response: text }); }) .catch((err) => { if (timer) clearTimeout(timer); reject(err); }); } }); } // ========================================== // Update Checker & Release Notifier // ========================================== function isNewerVersion(remote, local) { const rVersion = normalizeVersion(remote); const lVersion = normalizeVersion(local); if (!rVersion || !lVersion) return false; const r = rVersion.split('.').map(n => parseInt(n, 10)); const l = lVersion.split('.').map(n => parseInt(n, 10)); for (let i = 0; i < Math.max(r.length, l.length); i++) { const rv = r[i] || 0; const lv = l[i] || 0; if (rv > lv) return true; if (rv < lv) return false; } return false; } function normalizeVersion(value) { const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/i); return match ? `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}` : null; } function triggerScriptUpdate(targetVersion) { const ver = normalizeVersion(targetVersion); if (!ver || !isNewerVersion(ver, SCRIPT_VERSION)) { showToast(`No valid newer toolkit version was found. Please run Check for Updates again.`, 5000); return; } try { localStorage.setItem('amaes_pending_update_install', ver); localStorage.setItem('amaes_pending_update_time', String(Date.now())); localStorage.setItem('amaes_update_in_progress', '1'); } catch (e) {} showToast(`Opening v${ver} installer... After confirming in Violentmonkey, page will auto-refresh!`, 6500); try { window.open(SCRIPT_RAW_URL, '_blank'); } catch (e) { window.location.href = SCRIPT_RAW_URL; } // Fallback auto-refresh: In case student confirms update without switching focus (e.g. mobile popup), auto-refresh after 6s setTimeout(() => { const pending = localStorage.getItem('amaes_pending_update_install'); if (pending && isNewerVersion(pending, SCRIPT_VERSION) && document.visibilityState === 'visible') { sessionStorage.setItem('amaes_update_attempted_reload', '1'); showToast(`Update v${ver} detected! Refreshing page to apply...`, 2000); setTimeout(() => { window.location.reload(); }, 1000); } }, 6000); } function triggerScriptReinstall() { const ver = SCRIPT_VERSION; try { localStorage.setItem('amaes_pending_reinstall', '1'); localStorage.setItem('amaes_pending_update_time', String(Date.now())); localStorage.setItem('amaes_update_in_progress', '1'); } catch (e) {} showToast(`Opening ${ver} in Violentmonkey... After confirming reinstall, page will auto-refresh!`, 6500); try { window.open(SCRIPT_RAW_URL, '_blank'); } catch (e) { window.location.href = SCRIPT_RAW_URL; } // Fallback auto-refresh: In case student confirms reinstall without switching focus (e.g. mobile popup), auto-refresh after 6s setTimeout(() => { const isReinstalling = localStorage.getItem('amaes_pending_reinstall'); if (isReinstalling && document.visibilityState === 'visible') { localStorage.removeItem('amaes_pending_reinstall'); sessionStorage.setItem('amaes_reinstall_confirmed', '1'); showToast(`Reinstall confirmed! Refreshing page...`, 2000); setTimeout(() => { window.location.reload(); }, 1000); } }, 6000); } function injectTopNavUpdateNotification(latestVersion) { if (!latestVersion || !isNewerVersion(latestVersion, SCRIPT_VERSION)) return; const pendingRaw = localStorage.getItem('amaes_pending_update_install'); const pending = pendingRaw && isNewerVersion(pendingRaw, SCRIPT_VERSION) ? pendingRaw : null; if (pendingRaw && !pending) { localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); localStorage.removeItem('amaes_update_in_progress'); } const existing = document.getElementById('amaes-topnav-update-item'); if (pending) { if (existing) existing.remove(); return; } if (existing) { existing.remove(); } const navContainer = document.querySelector('#usernavigation, .usermenu, .popover-region, nav.navbar .ml-auto, nav.navbar .ms-auto, nav.navbar ul.navbar-nav:last-child'); if (!navContainer) { setTimeout(() => injectTopNavUpdateNotification(latestVersion), 600); return; } const wrap = document.createElement('div'); wrap.id = 'amaes-topnav-update-item'; wrap.className = 'nav-item d-flex align-items-center me-2'; wrap.style.cssText = ` display: inline-flex; align-items: center; margin-right: 8px; z-index: 1050; vertical-align: middle; `; if (pending) { wrap.innerHTML = ` ${ICONS.check} Update v${pending} queued `; } else { wrap.innerHTML = ` `; } const updateBtn = wrap.querySelector('#amaes-topnav-update-btn'); if (updateBtn) { updateBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); triggerScriptUpdate(latestVersion); injectTopNavUpdateNotification(latestVersion); }; } if (navContainer.firstChild) { navContainer.insertBefore(wrap, navContainer.firstChild); } else { navContainer.appendChild(wrap); } } function checkPendingUpdateInstallation() { try { const pending = localStorage.getItem('amaes_pending_update_install'); const lastSeen = localStorage.getItem('amaes_last_seen_version'); if (pending && !isNewerVersion(pending, SCRIPT_VERSION)) { localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); localStorage.removeItem('amaes_update_in_progress'); sessionStorage.removeItem('amaes_update_attempted_reload'); const cachedLatest = localStorage.getItem('amaes_latest_version_seen'); if (cachedLatest && !isNewerVersion(cachedLatest, SCRIPT_VERSION)) { localStorage.removeItem('amaes_latest_version_seen'); } setTimeout(() => { showToast(`Toolkit successfully updated to ${SCRIPT_VERSION}!`, 5000); setLog(`Toolkit successfully updated to ${SCRIPT_VERSION}. All features active.`, "var(--accent-green)"); }, 600); } else if (pending && isNewerVersion(pending, SCRIPT_VERSION)) { const reloadAttempted = sessionStorage.getItem('amaes_update_attempted_reload'); const updateTime = Number(localStorage.getItem('amaes_pending_update_time') || 0); const expired = updateTime && (Date.now() - updateTime > 90000); if (reloadAttempted || expired) { sessionStorage.removeItem('amaes_update_attempted_reload'); localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); localStorage.removeItem('amaes_update_in_progress'); if (reloadAttempted) { setTimeout(() => { showToast(`Update v${pending} was not installed yet. Click Update Now to try again.`, 5000); }, 600); } } } else if (lastSeen && isNewerVersion(SCRIPT_VERSION, lastSeen)) { const cachedLatest = localStorage.getItem('amaes_latest_version_seen'); if (cachedLatest && !isNewerVersion(cachedLatest, SCRIPT_VERSION)) { localStorage.removeItem('amaes_latest_version_seen'); } setTimeout(() => { showToast(`Toolkit updated to ${SCRIPT_VERSION}!`, 4500); setLog(`Toolkit updated to ${SCRIPT_VERSION}.`, "var(--accent-green)"); }, 600); } if (sessionStorage.getItem('amaes_reinstall_confirmed')) { sessionStorage.removeItem('amaes_reinstall_confirmed'); setTimeout(() => { showToast(`Toolkit successfully reinstalled (${SCRIPT_VERSION})!`, 5000); setLog(`Toolkit successfully reinstalled (${SCRIPT_VERSION}).`, "var(--accent-green)"); }, 600); } localStorage.setItem('amaes_last_seen_version', SCRIPT_VERSION); } catch (e) { logDebug(`Error checking pending update: ${e.message}`); } } function setupPendingUpdateFocusListener() { let lastFocusPrompt = 0; let isReloading = false; const handleReturnFocus = () => { try { if (isReloading) return; const isReinstalling = localStorage.getItem('amaes_pending_reinstall'); if (isReinstalling) { const now = Date.now(); const updateTime = Number(localStorage.getItem('amaes_pending_update_time') || 0); const elapsed = now - updateTime; if (elapsed >= 1000) { isReloading = true; localStorage.removeItem('amaes_pending_reinstall'); sessionStorage.setItem('amaes_reinstall_confirmed', '1'); showToast(`Reinstall detected! Refreshing page to apply...`, 2500); setTimeout(() => { window.location.reload(); }, 1000); return; } } const pendingRaw = localStorage.getItem('amaes_pending_update_install'); const pending = pendingRaw && isNewerVersion(pendingRaw, SCRIPT_VERSION) ? pendingRaw : null; const latestKnown = normalizeVersion(localStorage.getItem('amaes_latest_version_seen')); if (!pending || (latestKnown && isNewerVersion(latestKnown, pending))) { if (pending && latestKnown && isNewerVersion(latestKnown, pending)) { localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); } return; } if (!isNewerVersion(pending, SCRIPT_VERSION)) { localStorage.removeItem('amaes_pending_update_install'); return; } const now = Date.now(); if (now - lastFocusPrompt < 2000) return; lastFocusPrompt = now; const updateTime = Number(localStorage.getItem('amaes_pending_update_time') || 0); const elapsed = now - updateTime; // Auto-refresh when student returns from confirming in Violentmonkey / script manager if (elapsed >= 1000) { isReloading = true; sessionStorage.setItem('amaes_update_attempted_reload', '1'); showToast(`Update detected! Refreshing page to apply v${pending}...`, 2500); setTimeout(() => { window.location.reload(); }, 1000); } else { showToast(`Update v${pending} is queued in Violentmonkey. It will apply when the userscript manager reloads the script.`, 5000); } } catch (e) {} }; window.addEventListener('focus', handleReturnFocus); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { handleReturnFocus(); } }); } function renderUpdateNotice(latestVersion) { const pendingRaw = localStorage.getItem('amaes_pending_update_install'); const pending = pendingRaw && isNewerVersion(pendingRaw, SCRIPT_VERSION) && !isNewerVersion(latestVersion, pendingRaw) ? pendingRaw : null; if (pendingRaw && !pending) { localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); } // 1. Persistent Header "Update Now" Button const titleGroup = document.getElementById('amaes-title-group'); if (titleGroup) { let headerUpdateBtn = document.getElementById('amaes-header-update-btn'); if (headerUpdateBtn) { headerUpdateBtn.remove(); // Keep header clean and prevent overlapping action icons } } // 2. Version Pill Update Indicator & Direct Link const versionPill = document.getElementById('amaes-version-pill'); if (versionPill) { const targetVer = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`; const targetPending = pending ? (pending.startsWith('v') ? pending : `v${pending}`) : ''; if (pending) { versionPill.innerHTML = `${SCRIPT_VERSION} → ${targetPending}`; versionPill.title = `Update ${targetPending} is waiting for confirmation in Violentmonkey`; versionPill.style.borderColor = '#f59e0b'; versionPill.style.color = '#f59e0b'; versionPill.onclick = () => { sessionStorage.setItem('amaes_update_attempted_reload', '1'); window.location.reload(); }; } else { versionPill.innerHTML = `${SCRIPT_VERSION} → ${targetVer}`; versionPill.title = `Update available: ${targetVer}. Click to install`; versionPill.style.borderColor = '#10b981'; versionPill.style.color = '#10b981'; versionPill.onclick = () => triggerScriptUpdate(latestVersion); } } // 3. Top-Right Persistent Notification Area Update Item injectTopNavUpdateNotification(latestVersion); // 5. Persistent Banner in Panel Body const container = document.getElementById('amaes-update-container'); if (container) { if (pending) { container.innerHTML = `
Update v${pending} Ready
Auto-refreshing on return...
`; const bEl = document.getElementById('amaes-update-banner'); if (bEl) { bEl.onclick = () => { sessionStorage.setItem('amaes_update_attempted_reload', '1'); window.location.reload(); }; } } else { container.innerHTML = `
${ICONS.download} Update v${latestVersion} Available
`; } const bUpdate = document.getElementById('amaes-banner-update-btn'); if (bUpdate) { bUpdate.onclick = () => { triggerScriptUpdate(latestVersion); renderUpdateNotice(latestVersion); }; } } else { setTimeout(() => renderUpdateNotice(latestVersion), 400); } // 6. Welcome Guide Modal Update Status const welcomeUpdateNow = document.getElementById('btn-welcome-update-now'); if (welcomeUpdateNow) { const pending = localStorage.getItem('amaes_pending_update_install'); if (pending) { welcomeUpdateNow.innerHTML = `${ICONS.check} Update Queued`; welcomeUpdateNow.disabled = true; welcomeUpdateNow.onclick = null; } else { welcomeUpdateNow.innerHTML = `${ICONS.download} Update Now (v${latestVersion})`; welcomeUpdateNow.onclick = (e) => { e.preventDefault(); triggerScriptUpdate(latestVersion); welcomeUpdateNow.innerHTML = `${ICONS.check} Update Queued`; welcomeUpdateNow.disabled = true; welcomeUpdateNow.onclick = null; }; } } // 7. Quiz Floating HUD Update Button const hudUpdateIndicator = document.getElementById('hud-update-indicator'); if (hudUpdateIndicator) { hudUpdateIndicator.style.display = pending ? 'none' : 'inline-flex'; hudUpdateIndicator.onclick = (e) => { e.preventDefault(); triggerScriptUpdate(latestVersion); }; } } function checkForScriptUpdates(manual = false, callback = null) { const cachedRaw = localStorage.getItem('amaes_latest_version_seen'); const cachedLatest = normalizeVersion(cachedRaw); if (cachedRaw && !cachedLatest) { localStorage.removeItem('amaes_latest_version_seen'); } const lastCheck = parseInt(localStorage.getItem('amaes_last_update_check') || '0', 10); const now = Date.now(); const hasKnownUpdate = cachedLatest && isNewerVersion(cachedLatest, SCRIPT_VERSION); // 1. If an update is ALREADY known from cache: if (hasKnownUpdate && !manual) { renderUpdateNotice(cachedLatest); } // 2. Cache throttling: 5 minutes for background auto-check, 10 seconds for manual const throttleWindow = manual ? 10 * 1000 : 5 * 60 * 1000; if (!manual && (now - lastCheck < throttleWindow)) { if (callback) callback({ status: hasKnownUpdate ? 'update_available' : 'cached', version: cachedLatest || SCRIPT_VERSION }); return; } if (manual) { setLog("Auto-checking GitHub for toolkit updates...", "var(--accent-blue)", "Querying releases..."); } const req = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; const processRemoteVersion = (remoteVer) => { localStorage.setItem('amaes_last_update_check', String(now)); const validRemoteVersion = normalizeVersion(remoteVer); if (!validRemoteVersion) { if (manual) setLog("GitHub returned an invalid release version.", "var(--accent-amber)"); if (callback) callback({ status: 'error', message: 'Invalid release version' }); return; } remoteVer = validRemoteVersion; if (isNewerVersion(remoteVer, SCRIPT_VERSION)) { localStorage.setItem('amaes_latest_version_seen', remoteVer); renderUpdateNotice(remoteVer); setLog(`Update found: v${remoteVer} is available! Opening installer...`, 'var(--accent-green)'); showToast(`Update found: v${remoteVer} is available!`, 4000); if (manual) { triggerScriptUpdate(remoteVer); } if (callback) callback({ status: 'update_available', version: remoteVer }); } else { localStorage.removeItem('amaes_latest_version_seen'); localStorage.removeItem('amaes_pending_update_install'); localStorage.removeItem('amaes_pending_update_time'); localStorage.removeItem('amaes_update_in_progress'); document.getElementById('amaes-topnav-update-item')?.remove(); document.getElementById('amaes-update-banner')?.remove(); if (manual) { setLog(`Toolkit is up to date (${SCRIPT_VERSION}).`, 'var(--accent-green)'); showToast(`Toolkit is up to date (${SCRIPT_VERSION})`); } if (callback) callback({ status: 'up_to_date', version: SCRIPT_VERSION }); } }; const checkViaRawUrl = () => { const rawUrl = `${SCRIPT_RAW_URL}?_t=${now}`; const parseRaw = (text) => { const m = text.match(/@version\s+([0-9]+\.[0-9]+\.[0-9]+)/); if (m && m[1]) { processRemoteVersion(m[1].trim()); } else { if (manual) setLog("Unable to verify update header.", "var(--accent-amber)"); if (callback) callback({ status: 'error', message: 'Could not parse version' }); } }; if (req) { try { req({ method: 'GET', url: rawUrl, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' }, onload: (res) => parseRaw(res.responseText || ''), onerror: (err) => { logDebug("Raw update check error:", err); if (manual) setLog("Network error checking updates.", "var(--accent-red)"); if (callback) callback({ status: 'error', error: err }); } }); } catch (e) { if (manual) setLog("Exception checking updates.", "var(--accent-red)"); if (callback) callback({ status: 'error', error: e }); } } else { fetch(rawUrl, { cache: 'no-store' }) .then(r => r.text()) .then(parseRaw) .catch(err => { logDebug("Fetch update error:", err); if (manual) setLog("Network error checking updates.", "var(--accent-red)"); if (callback) callback({ status: 'error', error: err }); }); } }; // Query GitHub API Releases first for instantaneous 0-delay tag detection const apiUrl = `https://api.github.com/repos/Acads-Tools/amaes-toolkit/releases/latest?_t=${now}`; if (req) { try { req({ method: 'GET', url: apiUrl, headers: { 'Accept': 'application/vnd.github.v3+json', 'Cache-Control': 'no-cache' }, onload: (res) => { try { const data = JSON.parse(res.responseText || '{}'); if (data && data.tag_name) { const ver = normalizeVersion(data.tag_name); if (isNewerVersion(ver, SCRIPT_VERSION)) { processRemoteVersion(ver); return; } } } catch (e) {} checkViaRawUrl(); }, onerror: () => checkViaRawUrl() }); } catch (e) { checkViaRawUrl(); } } else { fetch(apiUrl, { cache: 'no-store' }) .then(r => r.json()) .then(data => { if (data && data.tag_name) { const ver = normalizeVersion(data.tag_name); if (isNewerVersion(ver, SCRIPT_VERSION)) { processRemoteVersion(ver); return; } } checkViaRawUrl(); }) .catch(() => checkViaRawUrl()); } } // ========================================== // SVG Icons // ========================================== const ICONS = { home: ``, debug: ``, sun: ``, moon: ``, minimize: ``, preview: ``, check: ``, book: ``, edit: ``, video: ``, zap: ``, undo: ``, stop: ``, search: ``, download: ``, lightbulb: ``, checkCircle: ``, clear: ``, chevronRight: ``, chevronDown: ``, cloud: ``, cloudDownload: ``, cloudUpload: ``, upload: ``, share: ``, camera: ``, copy: ``, sparkles: ``, external: ``, git: ``, github: ``, greasyfork: ``, help: ``, target: ``, database: ``, tools: ``, gear: ``, shield: ``, shieldCheck: ``, play: ``, pause: ``, fastForward: ``, alertTriangle: ``, info: ``, rotateCcw: ``, sliders: ``, checkBadge: ``, clock: ``, xCircle: ``, clipboard: ``, keyboard: ``, skipForward: ``, close: ``, link: ``, lock: ``, unlock: ``, globe: ``, bell: ``, volume: ``, trash: ``, terminal: ``, bug: ``, chatgpt: ``, gemini: ``, perplexity: ``, user: ``, }; // Web Audio API Procedural Sound Engine (Zero external dependencies) let audioCtxInstance = null; function getAudioContext() { try { if (!audioCtxInstance) { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (AudioCtx) audioCtxInstance = new AudioCtx(); } if (audioCtxInstance && audioCtxInstance.state === 'suspended') { audioCtxInstance.resume().catch(() => {}); } } catch (_) {} return audioCtxInstance; } // Auto-resume AudioContext on user interaction if (typeof window !== 'undefined') { ['click', 'keydown', 'touchstart'].forEach(evt => { window.addEventListener(evt, () => { if (audioCtxInstance && audioCtxInstance.state === 'suspended') { audioCtxInstance.resume().catch(() => {}); } }, { passive: true, capture: true }); }); } function playToolkitSound(type) { if (!enableAudioAlerts) return; // Desktop OS notification via GM_notification (bypasses browser tab autoplay suspension completely) if (typeof GM_notification === 'function') { try { GM_notification({ title: (type === 'quest_done' || type === 'complete') ? 'Quiz Completed · AMAES Toolkit' : 'Manual Input Needed · AMAES Toolkit', text: (type === 'quest_done' || type === 'complete') ? 'All questions answered and saved! Waiting for your confirmation to submit.' : 'Unknown question encountered. Paused for your input.', silent: false, timeout: 6000 }); } catch (_) {} } try { const ctx = getAudioContext(); if (!ctx) return; const executeAudioChime = () => { const now = ctx.currentTime; if (type === 'quest_done' || type === 'complete') { // Bright, celebratory multi-tone ascending ding (D5 -> A5 -> D6 chime) const notes = [ { freq: 587.33, start: 0, dur: 0.25 }, { freq: 880.00, start: 0.1, dur: 0.35 }, { freq: 1174.66, start: 0.2, dur: 0.8 } ]; notes.forEach(n => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(n.freq, now + n.start); gain.gain.setValueAtTime(0.0001, now + n.start); gain.gain.exponentialRampToValueAtTime(0.18, now + n.start + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, now + n.start + n.dur); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + n.start); osc.stop(now + n.start + n.dur); }); } else if (type === 'manual_intervention' || type === 'unknown') { // Gentle, distinctive two-tone alert chime (F5 -> D5 soft marimba tone) const notes = [ { freq: 698.46, start: 0, dur: 0.14 }, { freq: 587.33, start: 0.12, dur: 0.4 } ]; notes.forEach(n => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(n.freq, now + n.start); gain.gain.setValueAtTime(0.0001, now + n.start); gain.gain.exponentialRampToValueAtTime(0.16, now + n.start + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, now + n.start + n.dur); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + n.start); osc.stop(now + n.start + n.dur); }); } }; if (ctx.state === 'suspended') { ctx.resume().then(() => { executeAudioChime(); }).catch(() => { let expired = false; const expireTimer = setTimeout(() => { expired = true; }, 4000); const onUserGesture = () => { window.removeEventListener('click', onUserGesture, { capture: true }); window.removeEventListener('keydown', onUserGesture, { capture: true }); clearTimeout(expireTimer); if (!expired && ctx.state === 'running') { executeAudioChime(); } }; window.addEventListener('click', onUserGesture, { capture: true, once: true }); window.addEventListener('keydown', onUserGesture, { capture: true, once: true }); }); } else { executeAudioChime(); } } catch (e) { logDebug("Audio notification error: " + e.message); } } // Detect whether user is currently logged into Moodle function isUserLoggedIn() { if (window.location.pathname.includes('/login/')) return false; if (window.location.pathname.includes('/mod/') || window.location.pathname.includes('/course/') || window.location.pathname.includes('/my/')) { return true; } if (document.body && document.body.classList.contains('notloggedin')) return false; if (document.getElementById('page-login-index')) return false; if (document.querySelector('.usermenu, .usertext, #action-menu-toggle-0, .logininfo a[href*="/user/profile.php"]')) { return true; } if (window.M && window.M.cfg && typeof window.M.cfg.userId !== 'undefined') { return Number(window.M.cfg.userId) > 0; } return true; } // Reset all settings to safe defaults (turns off all aggressive features) function resetAllSettingsToDefault() { localStorage.setItem('amaes_auto_quiz_mode', 'false'); localStorage.setItem('amaes_quiz_personality', 'passive'); localStorage.setItem('amaes_auto_pick_quiz', 'true'); localStorage.setItem('amaes_auto_next_quiz', 'false'); localStorage.setItem('amaes_auto_next_verified', 'true'); localStorage.setItem('amaes_auto_pick_study_guide_fallback', 'true'); localStorage.setItem('amaes_auto_submit_quiz', 'false'); localStorage.setItem('amaes_auto_push_github', 'false'); localStorage.setItem('amaes_auto_copy_search', 'true'); localStorage.setItem('amaes_auto_cloud_sync', 'true'); localStorage.setItem('amaes_auto_scrape_amauoed', 'true'); localStorage.setItem('amaes_auto_harvest_grades', 'true'); localStorage.setItem('amaes_enable_hotkeys', 'true'); localStorage.setItem('amaes_auto_highlight_quiz', 'true'); localStorage.setItem('amaes_copy_include_confidence', 'true'); localStorage.setItem('amaes_auto_copy_ai', 'true'); localStorage.setItem('amaes_smart_skip_quiz', 'false'); localStorage.setItem('amaes_show_in_question_ai_btns', 'true'); localStorage.setItem('amaes_ai_prompt_hint', 'true'); localStorage.setItem('amaes_auto_community_share', 'true'); localStorage.setItem('amaes_auto_min_quiz', 'false'); localStorage.setItem('amaes_ai_quiz_enabled', 'true'); localStorage.setItem('amaes_ai_auto_select', 'true'); autoQuizMode = false; autoPickQuiz = true; autoNextVerified = true; autoPickStudyGuideFallback = true; autoSubmitQuiz = false; autoNextQuiz = false; isWaitingForUserAnswer = false; smartSkipQuiz = false; autoHighlightQuiz = true; copyIncludeConfidence = true; autoScrapeAmauoed = true; autoCopyQuizForAI = true; autoCloudSync = true; autoHarvestGrades = true; showInQuestionAiBtns = true; aiPromptHint = true; enableKeyboardShortcuts = true; autoCommunityShare = true; autoMinimizeQuiz = false; enableAudioAlerts = true; aiQuizEnabled = true; aiAutoSelect = true; // Clear any running solver timers or state clearTimeout(autoNextTimer); autoNextTimer = null; clearTimeout(pageLoadSolverTimer); pageLoadSolverTimer = null; isSolverRunning = false; const updateCheck = (id, val) => { const el = document.getElementById(id); if (el) el.checked = val; }; updateCheck('chk-auto-cloud-sync', true); updateCheck('chk-auto-scrape-amauoed', true); updateCheck('chk-auto-community-share', true); updateCheck('chk-auto-harvest-grades', true); updateCheck('chk-auto-hl-quiz', true); updateCheck('chk-auto-copy-ai', true); updateCheck('chk-auto-min-quiz', false); updateCheck('chk-audio-alerts', true); updateCheck('chk-in-question-ai', true); updateCheck('chk-show-in-q-btns', true); updateCheck('chk-ai-hint', true); updateCheck('chk-keyboard-shortcuts', true); updateCheck('chk-auto-pick', true); updateCheck('chk-auto-next-verified', true); updateCheck('chk-auto-pick-study-guide-fallback', true); updateCheck('chk-auto-submit-quiz', false); updateCheck('chk-auto-dl-json', false); updateCheck('chk-auto-push-github', false); updateCheck('chk-auto-scrape-amauoed-quiz', true); // Update welcome modal checkboxes if open updateCheck('welcome-chk-sync', true); updateCheck('welcome-chk-share', true); updateCheck('welcome-chk-harvest', true); updateCheck('welcome-chk-hl', true); updateCheck('welcome-chk-copy', true); updateCheck('welcome-chk-skip', false); updateCheck('welcome-chk-hotkeys', true); updateCheck('welcome-chk-next', true); updateCheck('welcome-chk-submit', false); updateCheck('welcome-chk-dl', false); const btnPassive = document.getElementById('btn-personality-passive'); const btnAggressive = document.getElementById('btn-personality-aggressive'); const personalityDesc = document.getElementById('personality-desc'); if (btnPassive && btnAggressive) { btnPassive.className = 'amaes-btn amaes-btn-blue'; btnAggressive.className = 'amaes-btn amaes-btn-outline'; } if (personalityDesc) { personalityDesc.innerHTML = 'Co-Pilot: Auto-picks answers. On unknown question: pauses safely, auto-copies for AI, and waits.'; } const btnMasterAutoQuiz = document.getElementById('btn-master-auto-quiz'); if (btnMasterAutoQuiz) { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #10b981, #059669)'; btnMasterAutoQuiz.innerHTML = `${ICONS.play} Start Auto-Quiz`; } const subtext = document.getElementById('amaes-autoquiz-subtext'); if (subtext) { subtext.textContent = 'Auto-answers & advances. Pauses & copies on unknown questions.'; } syncAutoQuizUI(false); // If on a quiz attempt page, re-highlight answers without selecting choices if (checkIsQuizAttemptPage()) { const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const cached = getCachedAnswers(subCode); if (cached && cached.length > 0) { highlightQuizAnswers(cached, false, true); } } setLog("Settings reset to defaults (Highlight ON, Auto-Quiz ready, Safe review enabled).", "var(--accent-blue)"); showToast("Settings reset to safe defaults!"); } // Remove toolkit-owned state without touching Moodle or other site data. function resetToolkitInstallation() { deleteContributorKey().catch(() => {}); const removeOwnedState = (storage) => { if (!storage) return; const keys = []; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (key && key.startsWith('amaes_')) keys.push(key); } keys.forEach((key) => storage.removeItem(key)); }; removeOwnedState(localStorage); removeOwnedState(sessionStorage); resetAllSettingsToDefault(); localStorage.removeItem('amaes_welcome_dismissed'); localStorage.removeItem('amaes_terms_acknowledged'); document.getElementById('amaes-welcome-modal')?.remove(); if (typeof window._amaesUpdatePanelLockState === 'function') window._amaesUpdatePanelLockState(); showToast("Toolkit data cleared. Reloading first-run setup..."); setTimeout(() => window.location.reload(), 500); } // Helper to detect academic term from activity title, quiz name, or text function detectTermFromText(text) { if (!text || typeof text !== 'string') return null; const lower = text.toLowerCase(); if (lower.includes('prelim') || lower.includes('preliminary') || /\bweek\s*[1-5]\b/i.test(lower)) { return 'Prelim'; } if (lower.includes('midterm') || lower.includes('mid-term') || /\bweek\s*[6-9]\b/i.test(lower)) { return 'Midterm'; } if (lower.includes('prefi') || lower.includes('pre-final') || lower.includes('prefinal') || /\bweek\s*(1[0-4])\b/i.test(lower)) { return 'Prefi'; } if (lower.includes('final') || lower.includes('finals') || /\bweek\s*(1[5-9]|20)\b/i.test(lower)) { return 'Final'; } return null; } // Calculates answer count breakdown per term (Prelim, Midterm, Prefi, Final) function getSubjectTermBreakdown(subCode) { const questions = getCachedAnswers(subCode) || []; const stats = { prelim: 0, midterm: 0, prefi: 0, final: 0, general: 0, total: questions.length }; questions.forEach(q => { const term = q.period || q.term || detectTermFromText(q.sourceQuiz || q.quizTitle || q.question || q.qRaw || ''); if (term === 'Prelim') stats.prelim++; else if (term === 'Midterm') stats.midterm++; else if (term === 'Prefi') stats.prefi++; else if (term === 'Final') stats.final++; else stats.general++; }); return stats; } // Detects all enrolled courses visible on the Moodle dashboard or course catalog function detectDashboardCourses() { const results = []; const seen = new Set(); const courseCards = document.querySelectorAll(` .dashboard-card, [data-region="card-item"], .course-info-container, .card.dashboard-card, .coursename, [data-region="course-content"], .coursebox `); courseCards.forEach(card => { const titleElem = card.querySelector('.coursename, .coursename .multiline, h3, h4, .text-truncate, a') || card; const cardText = (titleElem.innerText || card.innerText || '').trim(); if (!cardText) return; let subCode = ''; const codeMatch = cardText.match(/\b([A-Za-z]{2,6}\d{3,4}[A-Za-z]*)\b/) || cardText.match(/[-_]\s*([A-Za-z0-9]+)\b/); if (codeMatch) { subCode = codeMatch[1].toUpperCase(); } let courseId = ''; const linkElem = card.querySelector('a[href*="/course/view.php?id="], a[href*="id="]'); if (linkElem && linkElem.href) { const idMatch = linkElem.href.match(/[?&]id=(\d+)/); if (idMatch) courseId = idMatch[1]; } if (!courseId) { courseId = card.getAttribute('data-course-id') || card.getAttribute('data-courseid') || ''; } const semPath = getSemesterBasePath(); const courseUrl = linkElem && linkElem.href ? linkElem.href : (courseId ? `${window.location.origin}${semPath}course/view.php?id=${courseId}` : ''); const gradesUrl = courseId ? `${window.location.origin}${semPath}grade/report/user/index.php?id=${courseId}` : ''; if (subCode && subCode !== 'DEFAULT' && subCode !== 'GENERAL' && !seen.has(subCode)) { seen.add(subCode); const cached = getCachedAnswers(subCode); results.push({ code: subCode, courseId, courseUrl, gradesUrl, count: cached ? cached.length : 0, title: cardText.split('\n')[0].trim() }); } }); return results; } // Injects verified DB indicators on home/dashboard course cards function injectDashboardCourseBadges() { if (!isUserLoggedIn()) return; // Clean up any stray badges accidentally injected inside course title anchors document.querySelectorAll('.coursename .amaes-home-db-badge, a .amaes-home-db-badge').forEach(b => b.remove()); let courseCards = document.querySelectorAll(` .dashboard-card, [data-region="card-item"], .coursebox, .dashboard-card-list-item, .course-summaryitem `); if (courseCards.length === 0) { courseCards = document.querySelectorAll('.course-info-container'); } if (courseCards.length === 0) return; const processedCards = new Set(); courseCards.forEach(card => { const rootCard = card.closest('.dashboard-card, [data-region="card-item"], .coursebox, .dashboard-card-list-item, .course-summaryitem') || card; if (processedCards.has(rootCard)) return; processedCards.add(rootCard); const existingBadges = rootCard.querySelectorAll('.amaes-home-db-badge'); if (existingBadges.length > 0) { // Keep only one badge, purge any duplicate badges existingBadges.forEach((b, idx) => { if (idx > 0) b.remove(); }); return; } const titleElem = rootCard.querySelector('.coursename, .coursename .multiline, h3, h4, .text-truncate, a') || rootCard; const cardText = (titleElem.innerText || rootCard.innerText || '').trim(); if (!cardText) return; let subCode = ''; const codeMatch = cardText.match(/\b([A-Za-z]{2,6}\d{3,4}[A-Za-z]*)\b/) || cardText.match(/[-_]\s*([A-Za-z0-9]+)\b/); if (codeMatch) { subCode = codeMatch[1].toUpperCase(); } if (!subCode) return; const cached = getCachedAnswers(subCode); const count = cached ? cached.length : 0; const termStats = getSubjectTermBreakdown(subCode); const readyTerms = []; if (termStats.prelim > 0) readyTerms.push('Prelim'); if (termStats.midterm > 0) readyTerms.push('Mid'); if (termStats.prefi > 0) readyTerms.push('Prefi'); if (termStats.final > 0) readyTerms.push('Final'); const badgeWrapper = document.createElement('div'); badgeWrapper.className = 'amaes-home-db-badge-wrapper'; badgeWrapper.style.cssText = ` display: flex; align-items: center; margin-top: 4px; margin-bottom: 2px; pointer-events: none; `; const badge = document.createElement('span'); badge.className = 'amaes-home-db-badge'; badge.style.cssText = ` display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; border-radius: 4px; font-size: 10px; font-weight: 600; line-height: 1.3; cursor: default; user-select: none; pointer-events: none; ${count >= 100 || readyTerms.length === 4 ? 'background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.4); color: #059669;' : count > 0 ? 'background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.3); color: #047857;' : 'background: rgba(148, 163, 184, 0.1); border: 1px solid rgba(148, 163, 184, 0.25); color: #64748b;'} `; if (readyTerms.length === 4 || count >= 100) { badge.innerHTML = `${ICONS.checkBadge} All Terms Ready • ${count} Qs`; badge.title = `${subCode} Study Database: Complete question bank covering Prelim, Midterm, Prefi & Final (${count} verified questions).`; } else if (readyTerms.length > 0) { badge.innerHTML = `${ICONS.database} ${readyTerms.join('/')} Ready • ${count} Qs`; badge.title = `${subCode} Study Database: ${readyTerms.join(', ')} covered (${count} verified questions).`; } else if (count > 0) { badge.innerHTML = `${ICONS.database} Verified DB • ${count} Qs`; badge.title = `${subCode} Study Database: ${count} verified questions available.`; } else { badge.innerHTML = `${ICONS.cloudDownload} ${subCode} • No Local DB`; badge.title = `${subCode}: No verified answers cached in local database.`; } badgeWrapper.appendChild(badge); const targetContainer = rootCard.querySelector('.course-info-container, .card-body, [data-region="course-content"]') || rootCard; targetContainer.appendChild(badgeWrapper); }); } // Calculates comprehensive autonomous readiness and confidence score for a course function calculateCourseConfidence(subCode, courseId, gradesMap = {}) { const questions = (typeof getCachedAnswers === 'function' && subCode) ? (getCachedAnswers(subCode) || []) : []; const total = questions.length; let verifiedCount = 0; let amauoedCount = 0; let eliminatedCount = 0; questions.forEach(q => { const s = (q.source || '').toLowerCase(); const sources = Array.isArray(q.sources) ? q.sources.map(x => (x || '').toLowerCase()) : []; const isStudyGuide = s.includes('amauoed') || s.includes('jennysonline') || sources.some(x => x.includes('amauoed') || x.includes('jennysonline')); if (isStudyGuide) { amauoedCount++; } else { verifiedCount++; } if (Array.isArray(q.wrongAnswers)) { eliminatedCount += q.wrongAnswers.length; } }); const termStats = typeof getSubjectTermBreakdown === 'function' ? getSubjectTermBreakdown(subCode) : { prelim: 0, midterm: 0, prefi: 0, final: 0, general: 0, total: 0 }; // 1. Volume Score (Max 45%) const weightedCount = (verifiedCount * 1.0) + (amauoedCount * 0.65); const wVolume = Math.min(45, Math.round((weightedCount / 80) * 45)); // 2. Term Distribution Score (Max 35%) const termsCovered = [termStats.prelim >= 5, termStats.midterm >= 5, termStats.prefi >= 5, termStats.final >= 5].filter(Boolean).length; let wTerms = 0; if (termsCovered === 4) wTerms = 35; else if (termsCovered === 3) wTerms = 25; else if (termsCovered === 2) wTerms = 15; else if (termsCovered === 1) wTerms = 8; else if (total > 0) wTerms = 5; // 3. Verification Accuracy Ratio (Max 12%) const wVer = total > 0 ? Math.round((verifiedCount / total) * 12) : 0; // 4. Eliminated Wrong Distractor Score (Max 8%) const wElim = Math.min(8, Math.round(eliminatedCount * 0.5)); const score = total === 0 ? 0 : Math.min(100, Math.max(0, wVolume + wTerms + wVer + wElim)); let tier = ''; let badgeColor = ''; let badgeBg = ''; let aiDependency = ''; let desc = ''; if (score >= 85) { tier = 'Autonomous Ready'; badgeColor = '#059669'; badgeBg = 'rgba(16, 185, 129, 0.12)'; aiDependency = 'Low AI Usage'; desc = 'Question bank covers nearly all questions. Quizzes run hands-free with instant answer selection and low to zero AI quota consumption.'; } else if (score >= 50) { tier = 'Substantial Coverage'; badgeColor = '#2563eb'; badgeBg = 'rgba(37, 99, 235, 0.12)'; aiDependency = 'Moderate AI Fallback'; desc = 'Most core questions are cached locally. Unknown questions smoothly fall back to Gemini AI.'; } else if (score >= 20) { tier = 'Developing Bank'; badgeColor = '#d97706'; badgeBg = 'rgba(217, 119, 6, 0.12)'; aiDependency = 'Frequent AI Fallback'; desc = 'Certain terms have question coverage; remaining terms rely on Gemini AI solving and student review.'; } else { tier = 'Uncached / AI Driven'; badgeColor = '#64748b'; badgeBg = 'rgba(100, 116, 139, 0.12)'; aiDependency = 'Full AI Reliance'; desc = 'No cached answers found yet. Gemini AI solves questions on demand, harvesting 100% attempts to build the database.'; } // Quiz grade stats per term const termQuizzes = { prelim: { total: 0, passed: 0, attempted: 0, completedAttempts: 0 }, midterm: { total: 0, passed: 0, attempted: 0, completedAttempts: 0 }, prefi: { total: 0, passed: 0, attempted: 0, completedAttempts: 0 }, final: { total: 0, passed: 0, attempted: 0, completedAttempts: 0 } }; let totalQuizzes = 0; let totalPassed = 0; let totalAttempted = 0; if (gradesMap && typeof gradesMap === 'object') { const seen = new Set(); Object.values(gradesMap).forEach(entry => { if (!entry || !entry.title) return; const key = entry.cmid || entry.normTitle; if (seen.has(key)) return; seen.add(key); totalQuizzes++; if (entry.hasGrade) totalAttempted++; if (entry.isPassable) totalPassed++; let attemptsUsed = 0; if (entry.cmid && typeof localStorage !== 'undefined') { try { const raw = localStorage.getItem('amaes_quiz_attempts_' + entry.cmid); if (raw) { const parsed = JSON.parse(raw); if (parsed && typeof parsed.attemptsUsed === 'number') { attemptsUsed = parsed.attemptsUsed; } } } catch (_) {} } if (attemptsUsed === 0 && entry.hasGrade) { attemptsUsed = 1; } const t = typeof detectTermFromText === 'function' ? detectTermFromText(entry.title) : null; const targetTerm = t === 'Prelim' ? termQuizzes.prelim : (t === 'Midterm' ? termQuizzes.midterm : (t === 'Prefi' ? termQuizzes.prefi : (t === 'Final' ? termQuizzes.final : null))); if (targetTerm) { targetTerm.total++; targetTerm.completedAttempts += attemptsUsed; if (entry.hasGrade) targetTerm.attempted++; if (entry.isPassable) targetTerm.passed++; } }); } return { score, tier, badgeColor, badgeBg, aiDependency, desc, total, verifiedCount, amauoedCount, eliminatedCount, termStats, termQuizzes, totalQuizzes, totalPassed, totalAttempted }; } // Injects dynamic Course Confidence and Autonomous Readiness Card on Moodle course pages (/course/view.php) async function injectCourseConfidenceCard() { if (typeof window === 'undefined' || typeof document === 'undefined') return; if (!window.location.pathname.includes('/course/view.php')) { const old = document.getElementById('amaes-course-confidence-card'); if (old) old.remove(); return; } const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const subjectCode = courseInfo.subjectCode; const courseId = courseInfo.courseId || new URLSearchParams(window.location.search).get('id') || ''; if (!subjectCode || subjectCode === 'DEFAULT' || subjectCode === 'GENERAL') return; // Fetch grades map from cache or trigger async lookup let gradesMap = {}; if (typeof window !== 'undefined' && window.__amaesCourseGradesCache) { gradesMap = window.__amaesCourseGradesCache; } else if (courseId) { try { const storedRaw = localStorage.getItem(`amaes_course_grades_cache_${courseId}`); if (storedRaw) gradesMap = JSON.parse(storedRaw); } catch (_) {} } if (Object.keys(gradesMap).length === 0 && courseId && typeof fetchCourseGradesMap === 'function') { fetchCourseGradesMap(courseId).then(freshMap => { if (freshMap && Object.keys(freshMap).length > 0) { injectCourseConfidenceCard(); } }).catch(() => {}); } // Determine non-reviewable status let locallyObservedRestricted = false; try { const localStatus = JSON.parse(localStorage.getItem(`amaes_course_reviewability_${subjectCode}`) || 'null'); locallyObservedRestricted = localStatus && localStatus.status === 'restricted-observed'; } catch (_) {} let sharedStatus = null; try { const statusUrl = new URL(`${communityRelayUrl}/course-reviewability`); statusUrl.searchParams.set('subjectCode', subjectCode); const response = await fetch(statusUrl.href); if (response.ok) sharedStatus = await response.json(); } catch (_) {} const sharedRestricted = sharedStatus && sharedStatus.subjectCode === subjectCode && sharedStatus.status === 'restricted-reported'; const isRestrictedCourse = locallyObservedRestricted || sharedRestricted; const metrics = calculateCourseConfidence(subjectCode, courseId, gradesMap); const target = document.querySelector('#region-main .course-content, #region-main [role="main"], #region-main, .course-content'); if (!target) return; let card = document.getElementById('amaes-course-confidence-card'); if (!card) { card = document.createElement('div'); card.id = 'amaes-course-confidence-card'; card.className = 'amaes-course-confidence-card card'; target.prepend(card); } card.style.cssText = ` margin: 16px 0 24px 0; padding: 16px 20px; background: rgba(15, 23, 42, 0.03); border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 8px; color: inherit; font-size: 13px; line-height: 1.5; user-select: none; box-shadow: 0 1px 3px rgba(0,0,0,0.03); `; const terms = [ { name: 'Prelim', count: metrics.termStats.prelim, qz: metrics.termQuizzes.prelim, completedAttempts: metrics.termQuizzes.prelim.completedAttempts || 0 }, { name: 'Midterm', count: metrics.termStats.midterm, qz: metrics.termQuizzes.midterm, completedAttempts: metrics.termQuizzes.midterm.completedAttempts || 0 }, { name: 'Prefinal', count: metrics.termStats.prefi, qz: metrics.termQuizzes.prefi, completedAttempts: metrics.termQuizzes.prefi.completedAttempts || 0 }, { name: 'Final', count: metrics.termStats.final, qz: metrics.termQuizzes.final, completedAttempts: metrics.termQuizzes.final.completedAttempts || 0 } ]; card.innerHTML = `
${ICONS.shieldCheck}
${escapeHtml(subjectCode)} Course Intelligence Toolkit Live
${escapeHtml(courseInfo.subjectName || courseInfo.fullTitle || 'Course Overview')}
${metrics.score}% Autonomous Confidence • ${metrics.tier} ${metrics.aiDependency}
Verified DB
${metrics.verifiedCount} Qs
Study Guides
${metrics.amauoedCount} Qs
Eliminated Wrong
${metrics.eliminatedCount}
Passable Quizzes
${metrics.totalPassed}${metrics.totalQuizzes ? '/' + metrics.totalQuizzes : ''} Passed
${terms.map(t => { const isCompleted = t.qz.total > 0 && t.qz.passed >= t.qz.total; const inProgress = t.completedAttempts > 0 || t.qz.passed > 0; const statusLabel = isCompleted ? 'Completed' : (inProgress ? 'In Progress' : 'Not Started'); const badgeStyle = isCompleted ? 'background: rgba(16,185,129,0.15); color: #059669;' : (inProgress ? 'background: rgba(59,130,246,0.15); color: #2563eb;' : 'background: rgba(148,163,184,0.15); color: #64748b;'); const borderStyle = isCompleted ? 'rgba(16, 185, 129, 0.4)' : (inProgress ? 'rgba(59, 130, 246, 0.35)' : 'rgba(148, 163, 184, 0.25)'); const bgStyle = isCompleted ? 'rgba(16, 185, 129, 0.05)' : (inProgress ? 'rgba(59, 130, 246, 0.04)' : 'rgba(148, 163, 184, 0.03)'); const attemptsText = `${t.completedAttempts} Attempt${t.completedAttempts === 1 ? '' : 's'} Done`; const passText = t.qz.total > 0 ? `${t.qz.passed}/${t.qz.total} Quizzes Passed` : (t.completedAttempts > 0 ? `${t.completedAttempts} Completed` : 'No Quizzes Found'); return `
${t.name} ${statusLabel}
${attemptsText}
${passText}
`; }).join('')}
${isRestrictedCourse ? `
Non-Reviewable Course Policy Active: Quiz review access is restricted in this course. Only 100% perfect score attempts are harvested to maintain absolute database accuracy. Unreviewed attempts use adaptive probing across attempts.
` : ''}
`; // Bind Card Action Buttons const btnHighlight = card.querySelector('#amaes-card-btn-highlight'); if (btnHighlight) { btnHighlight.addEventListener('click', (e) => { e.preventDefault(); if (typeof highlightMissingOrUnansweredQuizzes === 'function') { highlightMissingOrUnansweredQuizzes().then(res => { if (res && res.message) { if (typeof showToast === 'function') showToast(res.message, 3500); } else if (res && typeof res.count === 'number') { const msg = res.count > 0 ? `Highlighted ${res.count} quiz(zes) needing attention` : 'All quizzes on page are passed with ≥ 80%!'; if (typeof showToast === 'function') showToast(msg, 3500); } }).catch(err => { if (typeof showToast === 'function') showToast(`Highlight note: ${err.message}`, 3000); }); } }); } const btnNextQuiz = card.querySelector('#amaes-card-btn-next-quiz'); if (btnNextQuiz) { btnNextQuiz.addEventListener('click', (e) => { e.preventDefault(); if (typeof cycleToNextQuizOrExam === 'function') { cycleToNextQuizOrExam(); } }); } } async function injectCourseReviewabilityNotice() { const existing = document.getElementById('amaes-course-reviewability-notice'); if (!window.location.pathname.includes('/course/view.php')) { existing?.remove(); return; } const subjectCode = detectCourseInfo().subjectCode; if (!subjectCode) { existing?.remove(); return; } let locallyObservedRestricted = false; try { const localStatus = JSON.parse(localStorage.getItem(`amaes_course_reviewability_${subjectCode}`) || 'null'); locallyObservedRestricted = localStatus && localStatus.status === 'restricted-observed'; } catch (error) { logDebug(`Course reviewability status read note: ${error.message}`); } let sharedStatus = null; try { const statusUrl = new URL(`${communityRelayUrl}/course-reviewability`); statusUrl.searchParams.set('subjectCode', subjectCode); const response = await fetch(statusUrl.href); if (!response.ok) throw new Error(`HTTP ${response.status}`); sharedStatus = await response.json(); } catch (error) { logDebug(`Shared course reviewability lookup note for ${subjectCode}: ${error.message}`); } const sharedRestricted = sharedStatus && sharedStatus.subjectCode === subjectCode && sharedStatus.status === 'restricted-reported'; if (!locallyObservedRestricted && !sharedRestricted) { existing?.remove(); return; } const target = document.querySelector('#region-main .course-content, #region-main, .course-content'); if (!target) return; const notice = existing || document.createElement('div'); notice.id = 'amaes-course-reviewability-notice'; notice.setAttribute('role', 'status'); notice.style.cssText = 'margin: 12px 0; padding: 10px 12px; border-left: 4px solid #f59e0b; border-radius: 5px; background: rgba(245, 158, 11, 0.12); color: inherit; font-size: 13px; line-height: 1.5;'; const notices = []; if (sharedRestricted) { if (sharedStatus.reportCount >= sharedStatus.threshold) { notices.push(`Review access reports for ${subjectCode}: at least ${sharedStatus.threshold} distinct installations reported that quiz review was restricted in this course within the last ${sharedStatus.windowDays} days. This does not prove that every quiz has the same policy.`); } else { notices.push(`Answer sharing is disabled for ${subjectCode} because the course is designated non-reviewable. Study-guide and AI suggestions may still be available, but they are not verified answer keys.`); } } if (locallyObservedRestricted) { notices.push(`This browser also observed a quiz that blocked review. Its overall grade cannot verify individual answers.`); } notices.push('Answers suggested by study guides or AI are unverified and should not be treated as harvested answers.'); notice.textContent = notices.join(' '); if (!existing) target.prepend(notice); } // Removed dashboard guide banner per user request to keep My Courses clean and unboxed function injectDashboardGuideBanner() { const existing = document.getElementById('amaes-dashboard-guide-banner'); if (existing) existing.remove(); } // ========================================== // Theme Management // ========================================== const THEMES = { dark: { bg: "#18181b", surface: "#27272a", surfaceSubtle: "#202023", border: "#3f3f46", borderSubtle: "#2e2e33", textPrimary: "#f4f4f5", textSecondary: "#a1a1aa", textMuted: "#71717a", accentBlue: "#3b82f6", accentBlueHover: "#2563eb", accentPink: "#f43f5e", accentPinkHover: "#e11d48", accentPurple: "#a855f7", accentPurpleHover: "#9333ea", accentGreen: "#10b981", accentGreenHover: "#059669", accentAmber: "#f59e0b", accentGray: "#52525b", shadow: "0 14px 36px rgba(0, 0, 0, 0.45)", statusBg: "#111114" }, light: { bg: "#ffffff", surface: "#f4f4f5", surfaceSubtle: "#fafafa", border: "#e4e4e7", borderSubtle: "#ececee", textPrimary: "#18181b", textSecondary: "#52525b", textMuted: "#71717a", accentBlue: "#2563eb", accentBlueHover: "#1d4ed8", accentPink: "#e11d48", accentPinkHover: "#be123c", accentPurple: "#7c3aed", accentPurpleHover: "#6d28d9", accentGreen: "#059669", accentGreenHover: "#047857", accentAmber: "#d97706", accentGray: "#e4e4e7", shadow: "0 14px 36px rgba(0, 0, 0, 0.12)", statusBg: "#f8fafc" } }; let currentTheme = localStorage.getItem('amaes_toolkit_theme') || 'dark'; if (!THEMES[currentTheme]) currentTheme = 'dark'; function detectDeviceMode() { const w = (typeof window !== 'undefined') ? (window.innerWidth || document.documentElement.clientWidth || (screen && screen.width) || 1024) : 1024; const isTouch = (typeof navigator !== 'undefined' && (navigator.maxTouchPoints > 0 || 'ontouchstart' in window)); const isMobile = w <= 640 || (w <= 768 && isTouch); const isTablet = !isMobile && (w <= 1024 || (w <= 1280 && isTouch && w > 640)); const root = (typeof document !== 'undefined') ? document.documentElement : null; if (root) { root.classList.toggle('amaes-is-mobile', isMobile); root.classList.toggle('amaes-is-tablet', isTablet); root.classList.toggle('amaes-is-touch', isTouch); root.classList.toggle('amaes-is-desktop', !isMobile && !isTablet); } return { isMobile, isTablet, isTouch, width: w }; } if (typeof window !== 'undefined') { detectDeviceMode(); window.addEventListener('resize', detectDeviceMode, { passive: true }); window.addEventListener('orientationchange', () => { setTimeout(detectDeviceMode, 150); }, { passive: true }); } function applyTheme(themeKey) { const t = THEMES[themeKey] || THEMES.dark; currentTheme = themeKey; localStorage.setItem('amaes_toolkit_theme', themeKey); let varSheet = document.getElementById('amaes-theme-variables'); if (!varSheet) { varSheet = document.createElement('style'); varSheet.id = 'amaes-theme-variables'; (document.head || document.documentElement).appendChild(varSheet); } varSheet.textContent = ` :root { --bg: ${t.bg} !important; --surface: ${t.surface} !important; --surface-subtle: ${t.surfaceSubtle} !important; --border: ${t.border} !important; --border-subtle: ${t.borderSubtle} !important; --text-primary: ${t.textPrimary} !important; --text-secondary: ${t.textSecondary} !important; --text-muted: ${t.textMuted} !important; --accent-blue: ${t.accentBlue} !important; --accent-blue-hover: ${t.accentBlueHover} !important; --accent-pink: ${t.accentPink} !important; --accent-pink-hover: ${t.accentPinkHover} !important; --accent-purple: ${t.accentPurple} !important; --accent-purple-hover: ${t.accentPurpleHover} !important; --accent-green: ${t.accentGreen} !important; --accent-green-hover: ${t.accentGreenHover} !important; --accent-amber: ${t.accentAmber} !important; --accent-gray: ${t.accentGray} !important; --shadow: ${t.shadow} !important; --status-bg: ${t.statusBg} !important; } #amaes-toolkit-panel input[type="checkbox"], .amaes-modal-container input[type="checkbox"], #amaes-welcome-modal input[type="checkbox"] { accent-color: var(--accent-green) !important; } `; if (document.documentElement) { document.documentElement.setAttribute('data-amaes-theme', themeKey); } if (typeof window !== 'undefined' && typeof window._amaesUpdatePanelTheme === 'function') { window._amaesUpdatePanelTheme(themeKey); } if (typeof ICONS !== 'undefined') { document.querySelectorAll('#amaes-theme-btn').forEach(btn => { btn.innerHTML = currentTheme === 'dark' ? `${ICONS.sun} Light Mode` : `${ICONS.moon} Dark Mode`; }); } } // Apply active theme variables immediately applyTheme(currentTheme); let autoHighlightQuiz = localStorage.getItem('amaes_auto_highlight_quiz') !== 'false'; // default true let autoCopyQuizForAI = localStorage.getItem('amaes_auto_copy_ai') !== 'false'; // default true let autoQuizMode = localStorage.getItem('amaes_auto_quiz_mode') === 'true'; // default false (Master autonomous switch) let fastQuizMode = localStorage.getItem('amaes_fast_quiz_mode') !== 'false'; // default true (built-in fast answers & accelerated transitions) let autoPickQuiz = localStorage.getItem('amaes_auto_pick_quiz') !== 'false'; // default true: auto-select verified answers let autoNextVerified = localStorage.getItem('amaes_auto_next_verified') !== 'false'; // default true: auto-advance when solver answers verified question let autoPickStudyGuideFallback = localStorage.getItem('amaes_auto_pick_study_guide_fallback') !== 'false'; // default true: automatically use online study guides (Jenny & AMA) let autoNextQuiz = localStorage.getItem('amaes_auto_next_quiz') === 'true'; // default false: manual answers do NOT auto-advance by default (safe review) let adaptiveProbeQuiz = localStorage.getItem('amaes_adaptive_probe_quiz') !== 'false'; // default true: rotate choices across unreviewed attempts until 100% let adaptiveProbeBudget = parseInt(localStorage.getItem('amaes_adaptive_probe_budget') || '2', 10); // default: probe at most 2 unverified questions per attempt if (isNaN(adaptiveProbeBudget) || adaptiveProbeBudget < 1) adaptiveProbeBudget = 2; let isWaitingForUserAnswer = false; // session state: true when paused on an unknown question waiting for student input let autoSubmitQuiz = localStorage.getItem('amaes_auto_submit_quiz') === 'true'; // default false: safe manual review before final submission let autoSubmitDelay = parseInt(localStorage.getItem('amaes_auto_submit_delay') || '10', 10); if (isNaN(autoSubmitDelay) || autoSubmitDelay < 0) autoSubmitDelay = 10; let autoMarkDoneOn100 = localStorage.getItem('amaes_auto_mark_done_100') !== 'false'; // default true: auto-mark activity as done on 100% quiz score let autoNextTimer = null; let pageLoadSolverTimer = null; let sessionQuizzesSolved = parseInt(sessionStorage.getItem('amaes_session_quizzes_solved') || '0', 10); if (isNaN(sessionQuizzesSolved)) sessionQuizzesSolved = 0; function getAnonymousInstallId() { let anonId = localStorage.getItem('amaes_anon_install_id'); if (!anonId || typeof anonId !== 'string' || anonId.length < 10) { try { anonId = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : 'anon-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 10); } catch (_) { anonId = 'anon-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 10); } localStorage.setItem('amaes_anon_install_id', anonId); } return anonId; } let smartSkipQuiz = localStorage.getItem('amaes_smart_skip_quiz') !== 'false'; // default true: skip answered questions let autoCloudSync = localStorage.getItem('amaes_auto_cloud_sync') !== 'false'; // default true let autoScrapeAmauoed = localStorage.getItem('amaes_auto_scrape_amauoed') !== 'false'; // default true let autoHarvestGrades = localStorage.getItem('amaes_auto_harvest_grades') !== 'false'; // default true: auto-harvest past quizzes on course/grades open let storedCloudDbUrl = localStorage.getItem('amaes_cloud_db_url'); if (storedCloudDbUrl && storedCloudDbUrl.includes('lms-study-hub')) { storedCloudDbUrl = storedCloudDbUrl.replace('lms-study-hub', 'Acads-Tools'); localStorage.setItem('amaes_cloud_db_url', storedCloudDbUrl); } let cloudDbBaseUrl = storedCloudDbUrl || 'https://raw.githubusercontent.com/Acads-Tools/database/main/data/verified/'; const communityDbBaseUrl = 'https://raw.githubusercontent.com/Acads-Tools/database/main/data/community/'; const CLOUD_DB_FALLBACK_URL = 'https://raw.githubusercontent.com/Acads-Tools/database/main/data/'; const CLOUD_DB_AMAUOED_URL = 'https://raw.githubusercontent.com/Acads-Tools/database/main/data/amauoed/'; const CLOUD_DB_JENNYSONLINE_URL = 'https://raw.githubusercontent.com/Acads-Tools/database/main/data/jennysonline/'; const DEFAULT_COMMUNITY_RELAY_URL = COMMUNITY_RELAY_URL; let storedRelayUrl = localStorage.getItem('amaes_community_relay_url'); if (storedRelayUrl && (!storedRelayUrl.includes('acads-tools.workers.dev') || storedRelayUrl === 'https://amaes-community-relay.workers.dev')) { localStorage.removeItem('amaes_community_relay_url'); storedRelayUrl = null; } let communityRelayUrl = storedRelayUrl || DEFAULT_COMMUNITY_RELAY_URL; // A random, non-identifying installation token lets the relay deduplicate // retries and count distinct contributors without receiving account data. function getAnonymousContributorId() { let contributorId = localStorage.getItem(CONTRIBUTOR_ID_STORAGE_KEY); if (!contributorId) { contributorId = (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : `anon-${Date.now()}-${Math.random().toString(36).slice(2)}`; localStorage.setItem(CONTRIBUTOR_ID_STORAGE_KEY, contributorId); } return contributorId; } let aiPromptHint = localStorage.getItem('amaes_ai_prompt_hint') !== 'false'; // default true for clean a/b/c/d answers let copyIncludeConfidence = localStorage.getItem('amaes_copy_include_confidence') !== 'false'; // default true: include DB answer hints & confidence let showInQuestionAiBtns = localStorage.getItem('amaes_show_in_question_ai_btns') !== 'false'; // default true let enableKeyboardShortcuts = localStorage.getItem('amaes_enable_hotkeys') !== 'false'; // default true: N, Space, 1-4, C, P, H let autoCommunityShare = localStorage.getItem('amaes_auto_community_share') !== 'false'; // default true: auto-share on review / harvest let autoMinimizeQuiz = localStorage.getItem('amaes_auto_min_quiz') !== 'false'; // default true: smart pill in quiz let enableAudioAlerts = localStorage.getItem('amaes_enable_audio_alerts') !== 'false'; // default true: audio chime on quiz completion & intervention alert const GEMINI_API_KEY_STORAGE_KEY = 'amaes_gemini_api_key'; const GEMINI_API_KEYS_STORAGE_KEY = 'amaes_gemini_api_keys'; const GEMINI_MODEL = 'gemini-1.5-flash'; const GEMINI_TIMEOUT_MS = 8000; const SHARED_AI_FALLBACK_STORAGE_KEY = 'amaes_shared_ai_fallback_enabled'; const CONTRIBUTOR_KEY_ID_STORAGE_KEY = 'amaes_contributor_key_id'; const CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY = 'amaes_contributor_owner_token'; const CONTRIBUTOR_MODE_STORAGE_KEY = 'amaes_contributor_mode'; const CONTRIBUTOR_KEY_FINGERPRINT_STORAGE_KEY = 'amaes_contributor_key_fingerprint'; const CONTRIBUTOR_ACTIVITY_SENT_STORAGE_KEY = 'amaes_contributor_activity_sent'; function getContributorValue(key) { try { if (typeof GM_getValue === 'function') return GM_getValue(key, ''); } catch (_) {} return localStorage.getItem(key) || ''; } function setContributorValue(key, value) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); return; } } catch (_) {} localStorage.setItem(key, value); } function removeContributorValue(key) { try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); return; } } catch (_) {} localStorage.removeItem(key); } function isContributorSharingEnabled() { return getContributorValue(CONTRIBUTOR_MODE_STORAGE_KEY) === 'contributor' && Boolean(getContributorValue(CONTRIBUTOR_KEY_ID_STORAGE_KEY)) && Boolean(getContributorValue(CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY)); } function createContributorOwnerToken() { const bytes = crypto.getRandomValues(new Uint8Array(32)); return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); } async function contributorKeyFingerprint(value) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); } async function deleteContributorKey() { const keyId = getContributorValue(CONTRIBUTOR_KEY_ID_STORAGE_KEY); const ownerToken = getContributorValue(CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY); if (!keyId || !ownerToken) return; try { await fetch(`${communityRelayUrl}/keys/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': CLIENT_VERSION, 'X-AMAES-Owner-Token': ownerToken }, body: JSON.stringify({ keyId }) }); } catch (_) { // Local removal still prevents this installation from using the credential. } [CONTRIBUTOR_KEY_ID_STORAGE_KEY, CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY, CONTRIBUTOR_MODE_STORAGE_KEY, CONTRIBUTOR_KEY_FINGERPRINT_STORAGE_KEY, CONTRIBUTOR_ACTIVITY_SENT_STORAGE_KEY] .forEach(removeContributorValue); } async function registerContributorKey(geminiKey) { const ownerToken = createContributorOwnerToken(); const response = await fetch(`${communityRelayUrl}/keys/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': CLIENT_VERSION }, body: JSON.stringify({ geminiKey, ownerToken, consent: true, mode: 'contributor' }) }); const data = await response.json().catch(() => ({})); if (!response.ok || !data.keyId) { throw new Error(data.error || `Contributor registration failed (${response.status})`); } setContributorValue(CONTRIBUTOR_KEY_ID_STORAGE_KEY, data.keyId); setContributorValue(CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY, ownerToken); setContributorValue(CONTRIBUTOR_MODE_STORAGE_KEY, 'contributor'); setContributorValue(CONTRIBUTOR_KEY_FINGERPRINT_STORAGE_KEY, await contributorKeyFingerprint(geminiKey)); return data; } async function noteContributorActivity() { if (!isContributorSharingEnabled()) return; const lastSent = Number(getContributorValue(CONTRIBUTOR_ACTIVITY_SENT_STORAGE_KEY) || 0); if (Date.now() - lastSent < 10 * 60 * 1000) return; const keyId = getContributorValue(CONTRIBUTOR_KEY_ID_STORAGE_KEY); const ownerToken = getContributorValue(CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY); try { const response = await fetch(`${communityRelayUrl}/keys/activity`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': CLIENT_VERSION, 'X-AMAES-Owner-Token': ownerToken }, body: JSON.stringify({ keyId }) }); if (response.ok) setContributorValue(CONTRIBUTOR_ACTIVITY_SENT_STORAGE_KEY, String(Date.now())); } catch (_) { // A failed activity update never affects the local personal-key request. } } function isSharedAiFallbackEnabled() { return toolkitStorage.get(SHARED_AI_FALLBACK_STORAGE_KEY) !== 'false'; } function setSharedAiFallbackEnabled(enabled) { toolkitStorage.set(SHARED_AI_FALLBACK_STORAGE_KEY, enabled ? 'true' : 'false'); } function getGeminiApiKeys() { try { const raw = toolkitStorage.get(GEMINI_API_KEYS_STORAGE_KEY); if (raw) { const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; if (Array.isArray(parsed)) { const clean = parsed.map(k => (k || '').trim()).filter(Boolean); if (clean.length > 0) return clean; } } } catch (_) {} const single = String(toolkitStorage.get(GEMINI_API_KEY_STORAGE_KEY) || '').trim(); return single ? [single] : []; } function setGeminiApiKeys(keys) { const clean = Array.isArray(keys) ? keys.map(k => (k || '').trim()).filter(Boolean) : []; if (clean.length > 0) { try { toolkitStorage.set(GEMINI_API_KEYS_STORAGE_KEY, JSON.stringify(clean)); } catch (_) {} toolkitStorage.set(GEMINI_API_KEY_STORAGE_KEY, clean[0]); geminiApiKey = clean[0]; } else { toolkitStorage.remove(GEMINI_API_KEYS_STORAGE_KEY); toolkitStorage.remove(GEMINI_API_KEY_STORAGE_KEY); geminiApiKey = ''; } updateAiAssistantUI(); } let geminiApiKey = (getGeminiApiKeys()[0] || ''); let aiQuizEnabled = toolkitStorage.get('amaes_ai_quiz_enabled') !== 'false'; // default true let aiAutoSelect = toolkitStorage.get('amaes_ai_auto_select') !== 'false'; // default true: auto-select AI suggestion let aiRetryCount = parseInt(toolkitStorage.get('amaes_ai_retry_count') || '2', 10); if (isNaN(aiRetryCount) || aiRetryCount < 1) aiRetryCount = 2; let aiAutoCopyOnFail = toolkitStorage.get('amaes_ai_auto_copy_on_fail') !== 'false'; // default true: auto-copy on fail const aiAnswerSessionCache = new Map(); // In-memory session cache for instant reuse let activeAiAbortController = null; function getAiRetryCount() { const val = parseInt(toolkitStorage.get('amaes_ai_retry_count') || '2', 10); return (isNaN(val) || val < 1) ? 2 : Math.min(5, val); } function setAiRetryCount(val) { aiRetryCount = Math.max(1, Math.min(5, parseInt(val, 10) || 2)); toolkitStorage.set('amaes_ai_retry_count', String(aiRetryCount)); } function getAiAutoCopyOnFail() { return toolkitStorage.get('amaes_ai_auto_copy_on_fail') !== 'false'; } function setAiAutoCopyOnFail(val) { aiAutoCopyOnFail = Boolean(val); toolkitStorage.set('amaes_ai_auto_copy_on_fail', aiAutoCopyOnFail ? 'true' : 'false'); } let aiAutoNextOnAiAnswer = toolkitStorage.get('amaes_ai_auto_next_on_ai') !== 'false'; // default true function getAiAutoNextOnAiAnswer() { return toolkitStorage.get('amaes_ai_auto_next_on_ai') !== 'false'; } function setAiAutoNextOnAiAnswer(val) { aiAutoNextOnAiAnswer = Boolean(val); toolkitStorage.set('amaes_ai_auto_next_on_ai', aiAutoNextOnAiAnswer ? 'true' : 'false'); } const GEMINI_FREE_RPM = 15; // 15 requests per minute limit on Google AI Studio Free Tier let aiPlanTier = toolkitStorage.get('amaes_ai_plan_tier') || 'free'; // 'free' or 'paid' let aiRateLimitCooldownUntil = 0; try { const storedCooldown = parseInt(sessionStorage.getItem('amaes_ai_cooldown_until') || '0', 10); if (storedCooldown > Date.now()) aiRateLimitCooldownUntil = storedCooldown; } catch (_) {} let activeRateLimitTimerInterval = null; function getAiPlanTier() { return toolkitStorage.get('amaes_ai_plan_tier') || 'free'; } function setAiPlanTier(tier) { aiPlanTier = tier === 'paid' ? 'paid' : 'free'; toolkitStorage.set('amaes_ai_plan_tier', aiPlanTier); } function getAiKeyId(key) { if (!key) return 'primary'; return key.length > 8 ? key.slice(-8) : key; } function getStoredRequestTimestamps() { const key = arguments[0] || null; try { const storageKey = key ? `amaes_ai_req_timestamps_${getAiKeyId(key)}` : 'amaes_ai_req_timestamps'; const raw = sessionStorage.getItem(storageKey); if (raw) { const arr = JSON.parse(raw); if (Array.isArray(arr)) { const cutoff = Date.now() - 60000; return arr.filter(t => typeof t === 'number' && t > cutoff); } } } catch (_) {} return []; } function saveRequestTimestamps(arr, key = null) { try { const storageKey = key ? `amaes_ai_req_timestamps_${getAiKeyId(key)}` : 'amaes_ai_req_timestamps'; sessionStorage.setItem(storageKey, JSON.stringify(arr)); } catch (_) {} } function recordAiRequest() { const key = arguments[0] || null; const now = Date.now(); const cutoff = now - 60000; // Always maintain global timestamps for backwards compatibility const currentGlobal = getStoredRequestTimestamps(null).filter(t => t > cutoff); currentGlobal.push(now); saveRequestTimestamps(currentGlobal, null); // Also record per-key if key provided if (key) { const currentPerKey = getStoredRequestTimestamps(key).filter(t => t > cutoff); currentPerKey.push(now); saveRequestTimestamps(currentPerKey, key); } } function triggerAiRateLimitCooldown(suggestedWaitSec = 20) { const key = arguments[1] || null; const waitSec = Math.max(5, Math.min(60, suggestedWaitSec)); const cooldownUntil = Date.now() + (waitSec * 1000); aiRateLimitCooldownUntil = Math.max(aiRateLimitCooldownUntil, cooldownUntil); try { sessionStorage.setItem('amaes_ai_cooldown_until', String(aiRateLimitCooldownUntil)); if (key) { sessionStorage.setItem(`amaes_ai_cooldown_${getAiKeyId(key)}`, String(cooldownUntil)); } } catch (_) {} return Math.ceil((aiRateLimitCooldownUntil - Date.now()) / 1000); } function getKeyRateLimitStatus(key) { const now = Date.now(); const keyId = getAiKeyId(key); let cooldownUntil = 0; try { const stored = parseInt(sessionStorage.getItem(`amaes_ai_cooldown_${keyId}`) || '0', 10); if (stored > now) cooldownUntil = stored; } catch (_) {} if (cooldownUntil > now) { const remainingSec = Math.max(1, Math.ceil((cooldownUntil - now) / 1000)); return { isLimited: true, remainingSec, reason: 'cooldown', key }; } if (getAiPlanTier() === 'free') { const timestamps = getStoredRequestTimestamps(key); if (timestamps.length >= GEMINI_FREE_RPM) { const oldest = timestamps[0]; const remainingSec = Math.max(1, Math.ceil((oldest + 60000 - now) / 1000)); return { isLimited: true, remainingSec, reason: 'rpm_cap', key }; } } return { isLimited: false, remainingSec: 0, reason: null, key }; } function getAiRateLimitStatus() { const now = Date.now(); try { const storedCooldown = parseInt(sessionStorage.getItem('amaes_ai_cooldown_until') || '0', 10); if (storedCooldown > aiRateLimitCooldownUntil) aiRateLimitCooldownUntil = storedCooldown; } catch (_) {} const keys = typeof getGeminiApiKeys === 'function' ? getGeminiApiKeys() : []; // Single key or unconfigured: standard global behavior if (keys.length <= 1) { if (aiRateLimitCooldownUntil > now) { const remainingSec = Math.max(1, Math.ceil((aiRateLimitCooldownUntil - now) / 1000)); return { isLimited: true, remainingSec, reason: 'cooldown', activeKey: keys[0] || null }; } if (getAiPlanTier() === 'free') { const timestamps = getStoredRequestTimestamps(); if (timestamps.length >= GEMINI_FREE_RPM) { const oldest = timestamps[0]; const remainingSec = Math.max(1, Math.ceil((oldest + 60000 - now) / 1000)); return { isLimited: true, remainingSec, reason: 'rpm_cap', activeKey: keys[0] || null }; } } return { isLimited: false, remainingSec: 0, reason: null, activeKey: keys[0] || null }; } // Multiple keys: check each key individually const statuses = keys.map(k => getKeyRateLimitStatus(k)); const available = statuses.filter(s => !s.isLimited); if (available.length > 0) { // Sort by request count in last 60s (least loaded key first for optimal load-balancing) available.sort((a, b) => { const countA = getStoredRequestTimestamps(a.key).length; const countB = getStoredRequestTimestamps(b.key).length; return countA - countB; }); return { isLimited: false, remainingSec: 0, reason: null, activeKey: available[0].key, availableCount: available.length, totalKeys: keys.length }; } // All keys limited: find soonest recovery time const minWait = Math.min(...statuses.map(s => s.remainingSec)); return { isLimited: true, remainingSec: Math.max(1, minWait), reason: 'all_keys_limited', availableCount: 0, totalKeys: keys.length }; } function getAvailableGeminiKey() { const keys = getGeminiApiKeys(); if (keys.length === 0) return ''; if (keys.length === 1) return keys[0]; const status = getAiRateLimitStatus(); if (!status.isLimited && status.activeKey) { return status.activeKey; } return keys[0]; } function getAiSessionCacheStorageKey() { return `amaes_ai_session_cache_${getQuizSessionKey()}`; } function saveAiAnswerToCache(qData, matched) { if (!qData || !matched) return; const qKey = normalizeText(qData.qText || ''); if (!qKey) return; const record = { choiceIndex: matched.choiceIndex, choiceText: matched.choiceText, timestamp: Date.now() }; aiAnswerSessionCache.set(qKey, record); try { const raw = sessionStorage.getItem(getAiSessionCacheStorageKey()); const cacheObj = raw ? JSON.parse(raw) : {}; cacheObj[qKey] = record; sessionStorage.setItem(getAiSessionCacheStorageKey(), JSON.stringify(cacheObj)); } catch (_) {} } function getCachedAiAnswer(qData) { if (!qData) return null; const qKey = normalizeText(qData.qText || ''); if (!qKey) return null; if (aiAnswerSessionCache.has(qKey)) { return aiAnswerSessionCache.get(qKey); } try { const raw = sessionStorage.getItem(getAiSessionCacheStorageKey()); if (raw) { const cacheObj = JSON.parse(raw); if (cacheObj && cacheObj[qKey]) { aiAnswerSessionCache.set(qKey, cacheObj[qKey]); return cacheObj[qKey]; } } } catch (_) {} return null; } function isChoiceRowEliminated(row) { if (!row) return false; if (row.classList && row.classList.contains('amaes-eliminated-choice')) return true; if (row.querySelector && row.querySelector('.amaes-eliminated-badge')) return true; const label = row.querySelector ? (row.querySelector('label') || row) : row; if (label && label.style && label.style.textDecoration && label.style.textDecoration.includes('line-through')) return true; if (typeof hasChoiceCross === 'function' && (hasChoiceCross(row) || hasChoiceCross(label))) return true; return false; } function getEliminatedChoicesForQuestion(que, qData, courseCode = '') { const eliminatedTexts = new Set(); if (que) { const choiceRows = que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr, .answer label'); choiceRows.forEach(r => { if (isChoiceRowEliminated(r)) { const lbl = r.querySelector('label') || r; const txt = normalizeChoice(cleanDOMToAI(lbl)); if (txt) eliminatedTexts.add(txt); } }); // Inspect que dataset for known wrong answers (essential for shortanswer / fill-in-the-blank) if (que.dataset && que.dataset.amaesKnownWrongAnswers) { try { const parsed = JSON.parse(que.dataset.amaesKnownWrongAnswers); if (Array.isArray(parsed)) { parsed.forEach(item => { const val = typeof item === 'string' ? item : (item.text || item.norm); const n = normalizeChoice(val); if (n) eliminatedTexts.add(n); }); } } catch (_) {} } // Inspect live inputs with known-wrong markings or datasets que.querySelectorAll('.amaes-known-wrong-selection, [data-amaes-known-wrong-answers]').forEach(inp => { if (inp.dataset && inp.dataset.amaesKnownWrongAnswers) { try { const parsed = JSON.parse(inp.dataset.amaesKnownWrongAnswers); if (Array.isArray(parsed)) { parsed.forEach(item => { const val = typeof item === 'string' ? item : (item.text || item.norm); const n = normalizeChoice(val); if (n) eliminatedTexts.add(n); }); } } catch (_) {} } const val = (inp.value || inp.getAttribute('value') || '').trim(); const n = normalizeChoice(val); if (n) eliminatedTexts.add(n); }); } const cCode = courseCode || (typeof detectCourseInfo === 'function' ? (detectCourseInfo().subjectCode || '') : ''); if (cCode) { try { const cached = getCachedAnswers(cCode); if (Array.isArray(cached) && qData && qData.qText) { const qNorm = normalizeText(qData.qText); const candidates = cached.filter(item => questionTextMatches(item.qNorm || item.qRaw || item.question, qNorm)); candidates.forEach(cand => { if (Array.isArray(cand.wrongAnswers)) { cand.wrongAnswers.forEach(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); if (wNorm) eliminatedTexts.add(wNorm); }); } }); } } catch (_) {} } return eliminatedTexts; } // ========================================== // Course & Activity Detection // ========================================== const KNOWN_COURSES = { // Computer Science "CS6202": "Algorithms and Complexity", "CS6204": "Computer Architecture and Organization", "CS6205": "Automata Theory and Formal Languages", "CS6206": "Principles of Operating Systems", "CS6209": "Software Engineering 1", "CS6300": "Software Engineering 2", "CS6301": "Logic Design and Digital Computer Circuits", "CS6309": "Introduction to Machine Learning", "CS6326": "Mobile Application Development", // Information Technology "IT6201": "Data Structures and Algorithm Analysis", "IT6202": "Data Structures and Algorithms", "IT6203": "Web Systems and Technologies 1", "IT6204": "Web Systems and Technologies 2", "IT6205": "Information Assurance and Security 1", "IT6205A": "Information Assurance and Security 1", "IT6206": "Information Assurance and Security 2", "IT6207": "Database Systems 1", "IT6208": "System Integration and Architecture 1", "IT6209": "Introduction to Multimedia", "IT6210": "Systems Administration and Maintenance", "IT6220": "Information Management", "IT6221": "Data Communications and Networking 1", "IT6222": "Data Communications and Networking 2", "IT6224": "Data Communications and Networking 3", "IT6224B": "Data Communications and Networking 3", "IT6300": "Cloud Computing", "IT6301": "Technopreneurship", "IT6302": "System Analysis and Design", "IT6310": "Network Security", "IT6320": "Social and Professional Issues", "IT6322": "Mobile Application Development", "IT6322A": "Mobile Application Development", "IT6323": "Human Computer Interaction", "IT6324": "Information Assurance and Security", // Information Technology Education Core "ITE6100": "Introduction to Computing", "ITE6101": "Computer Programming 1", "ITE6102": "Computer Programming 1", "ITE6103": "Computer Programming 2", "ITE6104": "Computer Programming 2", "ITE6200": "Application Development and Emerging Technology", "ITE6201": "Data Structures and Algorithm Analysis", "ITE6220": "Information Management", "ITE6300": "Cloud Computing and Internet of Things", "ITE6301": "Technopreneurship", // Mathematics & Sciences "MATH6100": "Calculus 1", "MATH6101": "Calculus 2", "MATH6102": "Discrete Mathematics", // General Education & Institutional "GE6100": "Understanding the Self", "GE6101": "Readings in Philippine History", "GE6102": "The Contemporary World", "GE6103": "Mathematics in the Modern World", "GE6104": "Purposive Communication", "GE6105": "Art Appreciation", "GE6106": "Science, Technology and Society", "GE6107": "Ethics", "GE6108": "Rizal's Life and Works", "GE6301": "Gender and Society", "GE6115": "Art Appreciation", "ETHNS6101": "Euthenics 1", "ETHNS6102": "Euthenics 2", "NSTP6101": "National Service Training Program 1", "NSTP6102": "National Service Training Program 2", "PE6101": "Physical Education 1", "PE6102": "Physical Education 2", "PE6103": "Physical Education 3", "PE6104": "Physical Education 4" }; function resolveKnownCourseName(code) { if (!code) return ""; const clean = String(code).trim().toUpperCase(); if (KNOWN_COURSES[clean]) return KNOWN_COURSES[clean]; const base = clean.replace(/[A-Za-z]+$/, ''); if (base && KNOWN_COURSES[base]) return KNOWN_COURSES[base]; return clean; } function detectAssessmentOrigin() { const host = (window.location && window.location.hostname) ? window.location.hostname.toLowerCase() : ''; const isExamHost = host.includes('exam'); const breadcrumbItems = Array.from(document.querySelectorAll('.breadcrumb-item, .breadcrumb a, nav.breadcrumb a, .breadcrumb span')) .map(el => (el.innerText || '').trim()) .filter(Boolean); const breadcrumbText = breadcrumbItems.join(' ').toLowerCase(); const heading = (document.querySelector('.page-header-headings h1, #page-header h1, #region-main h2, #region-main h3')?.innerText || '').trim().toLowerCase(); const title = (document.title || '').toLowerCase(); const combined = `${breadcrumbText} ${heading} ${title}`; const isExam = isExamHost || /\b(?:exam|examination|prelim\s+exam|midterm\s+exam|prefinal\s+exam|final\s+exam)\b/i.test(combined); let examPhase = 'Quiz'; if (isExam) { if (/\bprelim(?:\s+lab)?\s+exam\b/i.test(combined) || /\bprelim\b/i.test(combined)) { examPhase = combined.includes('lab') ? 'Prelim Lab Exam' : 'Prelim Exam'; } else if (/\bmidterm(?:\s+lab)?\s+exam\b/i.test(combined) || /\bmidterm\b/i.test(combined)) { examPhase = combined.includes('lab') ? 'Midterm Lab Exam' : 'Midterm Exam'; } else if (/\bprefinal\b/i.test(combined)) { examPhase = 'Prefinal Exam'; } else if (/\bfinal\b/i.test(combined)) { examPhase = 'Final Exam'; } else { examPhase = 'Exam'; } } else if (/\bquiz\b/i.test(combined)) { const quizMatch = combined.match(/\bquiz\s*\d+\b/i) || heading.match(/\bquiz\s*\d+\b/i); examPhase = quizMatch ? quizMatch[0].replace(/\bquiz\s*/i, 'Quiz ') : 'Quiz'; } return { originType: isExam ? 'exam' : 'quiz', originLabel: examPhase, isExam, isExamHost }; } function detectCourseInfo() { let fullTitle = ''; const codeRegex = /\b([A-Za-z]{2,6}\d{3,4}[A-Za-z]*)\b/; const candidates = []; const heading = document.querySelector('.page-header-headings h1, #page-header h1, .page-header-title'); if (heading && heading.innerText.trim()) candidates.push(heading.innerText.trim()); const breadcrumbLinks = document.querySelectorAll('.breadcrumb-item a, nav.breadcrumb a, .breadcrumb a'); for (const link of breadcrumbLinks) { const text = link.innerText.trim(); if (text) candidates.push(text); } if (document.title) candidates.push(document.title); let bestCandidate = ''; for (const cand of candidates) { if (codeRegex.test(cand)) { bestCandidate = cand; break; } } if (!bestCandidate && candidates.length > 0) { bestCandidate = candidates[0]; } fullTitle = bestCandidate; let subjectCode = ''; let subjectName = ''; const codeMatch = fullTitle.match(codeRegex) || fullTitle.match(/[-_]\s*([A-Za-z0-9]+)\b/); if (codeMatch) { subjectCode = codeMatch[1].toUpperCase(); const codeIndex = fullTitle.indexOf(codeMatch[1]); if (codeIndex !== -1) { const after = fullTitle.substring(codeIndex + codeMatch[1].length).trim().replace(/^[:\-–\s]+/, '').split('|')[0].trim(); const before = fullTitle.substring(0, codeIndex).trim().replace(/[:\-–\s]+$/, '').split('|')[0].trim(); if (after && after.length > 2) { subjectName = after; } else if (before && before.length > 2) { subjectName = before; } } else { subjectName = fullTitle.replace(codeMatch[1], '').replace(/^[:\-–\s]+/, '').trim(); } } if (!subjectName || subjectName.toUpperCase() === subjectCode) { const fallback = resolveKnownCourseName(subjectCode); if (fallback && fallback !== subjectCode) { subjectName = fallback; } } let currentActivityTitle = ''; if (window.location.pathname.includes('/mod/quiz/')) { const quizHeader = document.querySelector('.page-header-headings h1, #region-main h2, #region-main h3'); if (quizHeader) { currentActivityTitle = quizHeader.innerText.trim(); } } let courseId = ''; if (window.location.pathname.includes('/course/view.php') || window.location.pathname.includes('/grade/report/')) { courseId = new URLSearchParams(window.location.search).get('id') || ''; } if (!courseId) { const courseSelectors = [ 'ol.breadcrumb a[href*="/course/view.php"]', '.breadcrumb a[href*="/course/view.php"]', '#page-navbar a[href*="/course/view.php"]', 'a[data-key="coursehome"]', 'li[data-key="coursehome"] a', '.secondary-navigation a[href*="/course/view.php"]', '#courseindex a[href*="/course/view.php"]', '[data-region="courseindex"] a[href*="/course/view.php"]', 'a[href*="/course/view.php?id="]', 'a[href*="/grade/report/user/index.php?id="]' ]; for (const sel of courseSelectors) { const el = document.querySelector(sel); if (el && el.href) { const m = el.href.match(/[?&]id=(\d+)/); if (m && m[1]) { courseId = m[1]; break; } } } } if (!courseId) { const bodyMatch = document.body.className ? document.body.className.match(/\bcourse-(\d+)\b/) : null; if (bodyMatch && bodyMatch[1] !== '1') { courseId = bodyMatch[1]; } } if (!courseId) { try { courseId = sessionStorage.getItem('amaes_current_course_id') || localStorage.getItem('amaes_last_active_course_id') || ''; } catch (_) {} } if (courseId) { try { sessionStorage.setItem('amaes_current_course_id', courseId); localStorage.setItem('amaes_last_active_course_id', courseId); } catch (_) {} } const assessmentOrigin = detectAssessmentOrigin(); return { fullTitle, subjectCode, subjectName, courseId, currentActivityTitle, assessmentOrigin, originType: assessmentOrigin.originType, originLabel: assessmentOrigin.originLabel }; } function checkIsCoursePage() { return window.location.pathname.includes('/course/view.php') || window.location.pathname.includes('/mod/') || Boolean(document.querySelector('.course-content, .course-section, #region-main .activity, .activity-item, .que')); } function checkIsQuizPage() { return window.location.pathname.includes('/mod/quiz/attempt.php') || window.location.pathname.includes('/mod/quiz/summary.php') || window.location.pathname.includes('/mod/quiz/review.php') || window.location.pathname.includes('/mod/quiz/view.php') || Boolean(document.querySelector('.que, .quizsummarytable, #region-main .summarytable, .quizattemptsummary')); } function checkIsReviewPage() { if (window.location.pathname.includes('/mod/quiz/review.php')) return true; if (document.querySelector('#page-mod-quiz-review, body.path-mod-quiz-review, .quizreviewsummary, table.quizreviewsummary')) return true; if (document.querySelector('.que .outcome, .que .rightanswer')) return true; if (Array.from(document.querySelectorAll('a, button, input[type="submit"], .submitbtns a')).some(el => /finish\s+review/i.test(el.textContent || el.value || ''))) return true; return false; } function checkIsQuizAttemptPage() { return window.location.pathname.includes('/mod/quiz/attempt.php'); } function checkIsQuizSummaryPage() { return window.location.pathname.includes('/mod/quiz/summary.php') || Boolean(document.querySelector('.quizsummarytable, #region-main .summarytable')); } function checkIsQuizViewPage() { return window.location.pathname.includes('/mod/quiz/view.php') || Boolean(document.querySelector('.quizattemptsummary, #region-main .generaltable, .generaltable.quizattemptsummary')); } // String Normalization for Question & Answer Matching function normalizeText(str) { if (!str) return ''; // Unescape HTML entities const doc = new DOMParser().parseFromString(str, 'text/html'); let text = doc.body.textContent || ''; text = text.toLowerCase().trim(); text = unscriptDigits(text); // Remove question numbering like "1.", "(1)", "question 1:", "question no. 1", "question #1", "q1.", "item 1:" text = text.replace(/^(?:(?:question|item|q)\s*(?:no\.?|#)?\s*\d+[\s:.-]*|\(?\d+\)?[\s:.)-]+)/i, ''); // Strip common question instructions / preambles: // e.g. "Choose the best answer.", "Select the correct answer.", "Read the statement carefully and select the best answer." const preambleRegex = /^(?:(?:direction|directions|instruction|instructions)\s*[:.\-–]\s*)?(?:read\s+(?:each\s+|the\s+)?(?:statement|question|passage)s?\s+(?:carefully\s+)?(?:and\s+)?)?(?:choose|select|pick|identify|mark)\s+(?:the\s+)?(?:best|correct|appropriate|right)\s+(?:answer|choice|option)[.:?!;\s–-]*/i; while (preambleRegex.test(text)) { const nextText = text.replace(preambleRegex, '').trim(); if (nextText.length === 0) break; text = nextText; } // Strip trailing prompt instructions like "select one:", "choose one:", etc. const trailingPromptRegex = /\b(?:select\s+one|select\s+one\s+or\s+more|choose\s+one|choose\s+one\s+or\s+more|choose\s+the\s+best\s+answer)[:.]?\s*$/i; if (trailingPromptRegex.test(text)) { const nextText = text.replace(trailingPromptRegex, '').trim(); if (nextText.length > 0) text = nextText; } // Strip multiple underscores down to single placeholder text = text.replace(/_{2,}/g, '___'); // Strip extra whitespace text = text.replace(/\s+/g, ' '); // Strip trailing punctuation text = text.replace(/[.:?!;,]+$/, ''); return text.trim(); } function stripQuestionMarkup(text) { if (!text) return ''; let prev; let curr = String(text); // Iteratively strip innermost bracket tokens first (moodle-asset, dropdown, blank, ____, image) do { prev = curr; curr = curr.replace(/\[\s*(?:moodle-asset|dropdown|blank|image|___+)[^\[\]]*\]/gi, ' '); } while (curr !== prev); // Strip any remaining outer container tags (e.g. outer [Image: ...]) do { prev = curr; curr = curr.replace(/\[\s*(?:image|dropdown|blank)[^\[\]]*\]/gi, ' '); } while (curr !== prev); return curr; } function normalizeQuestionMatchKey(value) { return normalizeText(stripQuestionMarkup(value)) .replace(/\[\s*_{2,}(?:\s*:\s*[\s\S]*?)?\s*\]/g, ' ') .replace(/[_\u00a0]+/g, ' ') .trim() .replace(/^[:?!;,—–-]+\s*/g, '') .replace(/^(?:(?:identify\s+(?:the\s+)?(?:following|term|what\s+is\s+being\s+described)|fill\s+in\s+the\s+blanks?|complete\s+the\s+(?:statement|sentence|following)|identification|identify))\s*[:.-]*\s*/i, '') .replace(/^[:?!;,—–-]+\s*/g, '') .replace(/^(?:answer\s+)?question\s*(?:no\.?|#)?\s*\d+[\s:.-]*/i, '') .replace(/^(?:(?:this\s+(?:command|method|function)|jquery\s+(?:method|command|function)|it)\s+(?:is\s+)?(?:used\s+to\s+|that\s+is\s+used\s+to\s+))/i, '') .replace(/\bcreates\b/gi, 'create') .replace(/\s+/g, ' ') .replace(/^[:?!;,—–-]+\s*/g, '') .replace(/[.:?!;,—–-]+$/g, '') .trim(); } function questionTextMatches(left, right) { const leftNorm = normalizeQuestionMatchKey(left); const rightNorm = normalizeQuestionMatchKey(right); if (!leftNorm || !rightNorm) return false; if (leftNorm === rightNorm) return true; const longer = leftNorm.length >= rightNorm.length ? leftNorm : rightNorm; const shorter = leftNorm.length >= rightNorm.length ? rightNorm : leftNorm; // Permit harmless prompt markup differences, but never short-token matches // or collisions where extra content contains code identifiers or qualifiers. if (shorter.length > 20 && longer.includes(shorter)) { const idx = longer.indexOf(shorter); const prefix = longer.slice(0, idx).trim(); const suffix = longer.slice(idx + shorter.length).trim(); const extra = `${prefix} ${suffix}`.trim(); if (/\b(?:not|never|no|false|true)\b/i.test(extra)) return false; if (/^\.[a-z0-9_]+/i.test(prefix) || /\.[a-z0-9_]+\s*\(/i.test(extra)) return false; return (shorter.length / longer.length) >= 0.70; } return false; } // Helper to unscript unicode superscript and subscript digits to standard digits function unscriptDigits(str) { if (!str) return ''; const map = { '⁰':'0','¹':'1','²':'2','³':'3','⁴':'4','⁵':'5','⁶':'6','⁷':'7','⁸':'8','⁹':'9', '₀':'0','₁':'1','₂':'2','₃':'3','₄':'4','₅':'5','₆':'6','₇':'7','₈':'8','₉':'9' }; return str.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]/g, ch => map[ch] || ch); } function normalizeChoice(str) { if (!str) return ''; let text = str; if (typeof DOMParser !== 'undefined' && /<[a-z][\s\S]*>/i.test(str)) { try { const doc = new DOMParser().parseFromString(str, 'text/html'); const imgs = doc.body.querySelectorAll('img'); let extracted = doc.body.textContent || ''; if (!extracted.trim() && imgs.length > 0) { extracted = Array.from(imgs).map(img => { const m = (img.src || '').match(/\/pluginfile\.php\/\d+\/question\/(?:answer|questiontext|feedback)\/\d+(?:\/\d+)?\/([^?#\s]+)/i); return img.alt || (m ? `[moodle-asset:${m[1]}]` : img.src) || ''; }).join(' '); } text = extracted || str; } catch (_) {} } text = text.toLowerCase().trim(); // Moodle answer labels commonly include "a.", "b)", etc.; the // stored answer value normally contains only the choice text. text = text.replace(/^[a-z]\s*[\.)]\s+/i, ''); // Normalize Moodle pluginfile image URLs to stable image asset tokens across quiz attempts text = text.replace(/https?:\/\/[^\/]+\/pluginfile\.php\/\d+\/question\/(?:answer|questiontext|feedback)\/\d+(?:\/\d+)?\/([^?#\s]+)/gi, '[moodle-asset:$1]'); // Normalize unicode dashes/minus signs to standard hyphen text = text.replace(/[\u2212\u2013\u2014]/g, '-'); // Remove prefix like "select one: ", "select one or more: ", "a. ", "b) " safely without stripping negative signs // Strip unicode checkmarks/crosses and trailing feedback words text = text.replace(/[✓✔✗✘✕✖]/g, ''); text = text.replace(/\s*[\(\[]?(?:correct|incorrect)[\)\]]?\s*$/i, ''); text = text.replace(/\s+/g, ' '); text = text.replace(/[.:?!;,]+$/, ''); text = unscriptDigits(text); return text.trim(); } // Normalize and structure eliminated wrong choices with weighting & deduplication function normalizeWrongAnswers(rawWrong) { if (!rawWrong || !Array.isArray(rawWrong)) return []; return rawWrong.map(item => { if (typeof item === 'string') { const clean = item.replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); const norm = normalizeChoice(clean); if (!norm) return null; return { text: clean, norm: norm, count: 1, sources: [] }; } if (typeof item === 'object' && item) { const txt = (item.text || item.ansRaw || item.choice || '').replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); const norm = item.norm || normalizeChoice(txt); if (!norm) return null; return { text: txt, norm: norm, count: typeof item.count === 'number' ? item.count : (typeof item.weight === 'number' ? item.weight : 1), sources: Array.isArray(item.sources) ? item.sources : [] }; } return null; }).filter(Boolean); } // Auto-closes Moodle 4 Course Index Drawer on course view pages by default function autoCloseCourseIndexDrawer() { if (typeof window === 'undefined' || typeof document === 'undefined') return false; if (!window.location.pathname.includes('/course/view.php')) return false; const autoClose = typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_close_course_index') !== 'false' : true; if (!autoClose) return false; function tryClose() { const drawer = document.querySelector('#theme_boost-drawers-courseindex, .drawer-left'); const isOpen = (drawer && drawer.classList.contains('show')) || document.body.classList.contains('drawer-open-left'); if (!isOpen) return false; const closeBtn = document.querySelector( 'button[data-action="closedrawer"][data-side="left"], ' + 'button[title*="Close course index" i], ' + 'button[aria-label*="Close course index" i], ' + '#theme_boost-drawers-courseindex [data-action="closedrawer"], ' + '.drawer-left [data-action="closedrawer"]' ); if (closeBtn) { closeBtn.click(); return true; } return false; } if (!tryClose()) { setTimeout(tryClose, 200); setTimeout(tryClose, 500); setTimeout(tryClose, 1000); } return true; } // ========================================== // AMAUOED Engine (Fetch, Cache & Match) // ========================================== // Cross-origin safe HTTP fetch using GM_xmlhttpRequest function fetchAmauoedPage(url) { return new Promise((resolve, reject) => { const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: "GET", url: url, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }, onload: function (res) { if (res.status >= 200 && res.status < 300) { resolve(res.responseText); } else { reject(new Error(`HTTP ${res.status}: ${res.statusText}`)); } }, onerror: function (err) { reject(err); } }); } else { fetch(url) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.text(); }) .then(resolve) .catch(reject); } }); } // Extract questions and correct answers from AMAUOED HTML // Extract questions, choices, distractors, and correct answers from AMAUOED HTML function parseAmauoedHtml(html) { const results = []; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); let cards = Array.from(doc.querySelectorAll('.card.mb-2, .card')); if (cards.length === 0) { cards = Array.from(doc.querySelectorAll('.card-body')); } cards.forEach(card => { const qElem = card.querySelector('.mb-2, h5, .card-title, p.card-text'); if (!qElem) return; const qRaw = qElem.innerText.trim(); const qNorm = normalizeText(qRaw); if (!qNorm) return; // Extract all correct answer chips (supports multi-choice questions) const correctList = []; const correctChips = card.querySelectorAll('.chip.bg-success, span.bg-success, .badge.bg-success, .badge-success'); if (correctChips.length > 0) { correctChips.forEach(ch => { const li = ch.closest('li') || ch.parentElement; if (li) { const clone = li.cloneNode(true); clone.querySelectorAll('.chip, span, .badge').forEach(c => c.remove()); const txt = clone.innerText.trim(); if (txt && !correctList.includes(txt)) { correctList.push(txt); } } }); } else { // Fallback: check strong tag inside li const strongs = card.querySelectorAll('li strong'); strongs.forEach(st => { const txt = st.innerText.trim(); if (txt && !correctList.includes(txt)) { correctList.push(txt); } }); } // Extract choices and wrong answers (distractors) const allChoices = []; const wrongAnswers = []; const liElements = card.querySelectorAll('li'); liElements.forEach(li => { const clone = li.cloneNode(true); clone.querySelectorAll('.chip, span, .badge').forEach(c => c.remove()); const choiceTxt = clone.innerText.trim(); if (choiceTxt) { allChoices.push(choiceTxt); const isCorrect = correctList.some(ans => normalizeChoice(ans) === normalizeChoice(choiceTxt)); if (!isCorrect && !wrongAnswers.includes(choiceTxt)) { wrongAnswers.push(choiceTxt); } } }); if (correctList.length > 0) { const ansRaw = correctList.join(', '); const ansNorm = normalizeChoice(ansRaw); const entry = { qRaw, qNorm, ansRaw, ansNorm, source: 'amauoed', verified: false, confirmations: 1, evidenceType: 'study_guide_candidate' }; if (correctList.length > 1) { entry.answers = correctList; } if (wrongAnswers.length > 0) { entry.wrongAnswers = normalizeWrongAnswers(wrongAnswers); } if (allChoices.length > 0) { entry.choices = allChoices; } const existingIdx = results.findIndex(r => r.qNorm === qNorm); if (existingIdx >= 0) { if (entry.answers && Array.isArray(entry.answers)) { const curAnswers = results[existingIdx].answers || [results[existingIdx].ansRaw]; results[existingIdx].answers = Array.from(new Set(curAnswers.concat(entry.answers))); results[existingIdx].ansRaw = results[existingIdx].answers.join(', '); results[existingIdx].ansNorm = normalizeChoice(results[existingIdx].ansRaw); } } else { results.push(entry); } } }); return results; } // Crawl all pages of an AMAUOED course URL async function loadAllAmauoedAnswers(baseUrl, onProgress) { // Strip query params to get clean base URL const cleanBase = baseUrl.split('?')[0]; let allQuestions = []; let page = 1; let hasMore = true; const maxPages = 40; // High limit to cover 500+ question banks while (hasMore && page <= maxPages) { const pageUrl = page === 1 ? cleanBase : `${cleanBase}?page=${page}`; if (onProgress) onProgress(page, allQuestions.length); try { logDebug(`Fetching amauoed page ${page}: ${pageUrl}`); const html = await fetchAmauoedPage(pageUrl); const questions = parseAmauoedHtml(html).map(question => ({ ...question, sourceUrl: cleanBase })); if (questions.length === 0) { hasMore = false; break; } // Merge and deduplicate questions on append questions.forEach(newQ => { const existing = allQuestions.find(q => q.qNorm === newQ.qNorm); if (!existing) { allQuestions.push(newQ); } else if (newQ.answers && Array.isArray(newQ.answers)) { const curAnswers = existing.answers || [existing.ansRaw]; existing.answers = Array.from(new Set(curAnswers.concat(newQ.answers))); existing.ansRaw = existing.answers.join(', '); existing.ansNorm = normalizeChoice(existing.ansRaw); } }); // DOM-based pagination check with string fallback const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const nextLink = doc.querySelector(`.pagination .page-item:not(.disabled) a[href*="page="], a[rel="next"], .pagination a.page-link[href*="page=${page + 1}"]`); const hasNextPage = !!nextLink || html.includes(`page=${page + 1}`) || html.includes(`page=${page + 1}"`); if (!hasNextPage) { hasMore = false; } else { page++; // Polite delay between pages await new Promise(r => setTimeout(r, 400)); } } catch (err) { console.error("Error loading page:", err); hasMore = false; } } return allQuestions; } // Automatic Subject Matcher for AMAUOED Course Links function checkUrlCourseMatch(url, courseInfo) { if (!url || !url.trim()) { return { status: 'empty', html: 'Paste the amauoed.com link for this subject above.' }; } const cleanUrl = url.trim().toLowerCase(); if (!cleanUrl.includes('amauoed.com/courses/')) { return { status: 'invalid', html: 'Invalid link: Must start with https://amauoed.com/courses/...' }; } const currentCode = (courseInfo && courseInfo.subjectCode ? courseInfo.subjectCode.toUpperCase() : ''); const currentCodeNum = currentCode.replace(/\D+/g, ''); const currentCodePrefix = currentCode.replace(/\d+/g, '').trim().toLowerCase(); // Extract department and slug from amauoed URL const match = cleanUrl.match(/\/courses\/([a-z0-9_-]+)\/([^/?#]+)/i); if (!match) { return { status: 'unknown', html: 'Unable to parse course slug from link.' }; } const dept = match[1].toLowerCase(); const slug = match[2].toLowerCase(); const numMatch = slug.match(/(\d{3,4})/); const urlNum = numMatch ? numMatch[1] : ''; const urlCode = (dept + urlNum).toUpperCase(); const presetUrl = getStoredAmauoedUrl(currentCode); const numMatches = Boolean(currentCodeNum && urlNum && currentCodeNum === urlNum); const deptMatches = Boolean(currentCodePrefix && dept && (currentCodePrefix === dept || dept.includes(currentCodePrefix) || currentCodePrefix.includes(dept))); if (numMatches && deptMatches) { return { status: 'match', html: `Verified Match: Link belongs to ${currentCode}!` }; } let fixBtn = ''; if (presetUrl && presetUrl !== cleanUrl) { fixBtn = ` Switch to ${currentCode} Link`; } if (numMatches && !deptMatches) { return { status: 'mismatch', html: `Subject Mismatch: Link is for ${urlCode}, but you are in ${currentCode}!${fixBtn}` }; } return { status: 'mismatch', html: `Subject Mismatch: Link (${urlCode || slug.substring(0, 16)}) doesn't match ${currentCode || 'current course'}.${fixBtn}` }; } // Storage helpers & Known AMAUOED Catalog const KNOWN_AMAUOED_COURSES = { 'CS6301': 'https://amauoed.com/courses/cs/logic-design-and-digital-computer-circuits-6301-cs', 'ITE6301': 'https://amauoed.com/courses/ite/technopreneurship-6301-ite', 'ITE6300': 'https://amauoed.com/courses/ite/cloud-computing-and-the-internet-of-things-6300-ite', 'ITE6200': 'https://amauoed.com/courses/ite/application-development-and-emerging-technology-6200-ite', 'ITE6201': 'https://amauoed.com/courses/ite/data-structures-and-algorithm-analysis-6201-ite', 'ITE6100': 'https://amauoed.com/courses/ite/introduction-to-computing-6100-ite', 'ITE6102': 'https://amauoed.com/courses/ite/computer-programming-1-6102-ite', 'ITE6104': 'https://amauoed.com/courses/ite/computer-programming-2-6104-ite', 'ITE6220': 'https://amauoed.com/courses/ite/information-management-6220-ite', 'CS6202': 'https://amauoed.com/courses/cs/algorithms-and-complexity-6202-cs', 'CS6204': 'https://amauoed.com/courses/cs/computer-architecture-and-organization-6204-cs', 'CS6205': 'https://amauoed.com/courses/cs/automata-theory-and-formal-language-6205-cs', 'CS6206': 'https://amauoed.com/courses/cs/principles-of-operating-systems-and-its-applications-6206-cs', 'CS6209': 'https://amauoed.com/courses/cs/software-engineering-1-6209-cs', 'CS6300': 'https://amauoed.com/courses/cs/software-engineering-2-6300-cs', 'CS6309': 'https://amauoed.com/courses/cs/introduction-to-machine-learning-6309-cs', 'CS6326': 'https://amauoed.com/courses/cs/mobile-application-development-6326-cs', 'MATH6100': 'https://amauoed.com/courses/math/calculus-1-6100-math', 'GE6107': 'https://amauoed.com/courses/ge/ethics-6107-ge', 'GE6115': 'https://amauoed.com/courses/ge/art-appreciation-6115-ge', 'ETHNS6101': 'https://amauoed.com/courses/ethns/euthenics-1-6101-ethns', 'ETHNS6102': 'https://amauoed.com/courses/ethns/euthenics-2-6102-ethns' }; function getStoredAmauoedUrl(code) { if (!code) return ""; const clean = code.toUpperCase().trim(); const stored = localStorage.getItem(`amaes_amauoed_url_${clean}`); if (stored) return stored; if (KNOWN_AMAUOED_COURSES[clean]) { return KNOWN_AMAUOED_COURSES[clean]; } // Check dynamic directory cache if populated try { const dir = JSON.parse(localStorage.getItem('amaes_known_amauoed_directory') || '{}'); if (dir[clean]) return dir[clean]; } catch (e) {} const num = clean.replace(/\D+/g, ''); for (const [k, url] of Object.entries(KNOWN_AMAUOED_COURSES)) { if (num && k.includes(num)) { if (clean.includes('CS') && k.includes('CS')) return url; if (clean.includes('ITE') && k.includes('ITE')) return url; } } return ""; } function setStoredAmauoedUrl(code, url) { localStorage.setItem(`amaes_amauoed_url_${code}`, url); } // Auto-Search & Discover AMAUOED Link from Online Directory with DOM Card Parsing & 5-Tier Matching async function autoFindAmauoedLink(code, courseTitle = '') { if (!code) { setLog('No subject code detected to search', 'var(--accent-pink)', 'Plan: Open a course or quiz page first'); return null; } const cleanCode = code.toUpperCase().replace(/[^A-Z0-9]/g, '').trim(); const codeNum = cleanCode.replace(/\D+/g, ''); const codeDept = cleanCode.replace(/\d+/g, '').toUpperCase(); setLog(`Searching amauoed.com catalog for ${cleanCode}...`, 'var(--accent-blue)', 'Plan: Discover course directory and harvest question bank'); // Check stored or built-in first const direct = getStoredAmauoedUrl(cleanCode); if (direct) { setStoredAmauoedUrl(cleanCode, direct); setLog(`Auto-matched known link for ${cleanCode}!`, 'var(--accent-green)', 'Plan: Ready to fetch and sync question database'); return direct; } // Helper to match catalog entries function matchInCatalog(courses) { if (!Array.isArray(courses) || courses.length === 0) return null; // Tier 1: Exact code match (e.g. CS6202 matches CS-6202 or CS6202) const exact = courses.find(c => c.cleanCode === cleanCode || c.rawCode === cleanCode); if (exact) return exact.url; // Tier 2: Dept + Number exact match if (codeDept && codeNum) { const deptNum = courses.find(c => c.dept === codeDept && c.num === codeNum); if (deptNum) return deptNum.url; } // Tier 3: Dept alias match + Number match const ALIAS_MAP = { 'IT': ['ITE', 'IT'], 'ITE': ['IT', 'ITE'], 'CS': ['COMP', 'CSC', 'CS'], 'COMP': ['CS', 'COMP'], 'MATH': ['MTH', 'MATH'], 'MTH': ['MATH', 'MTH'], 'ACTG': ['ACC', 'IA', 'ACTG'], 'ACC': ['ACTG', 'ACC'], 'IA': ['ACTG', 'IA'], 'ENG': ['ENGL', 'ENG'], 'ENGL': ['ENG', 'ENGL'], 'PED': ['PE', 'PATHFIT', 'PED'], 'PE': ['PED', 'PATHFIT', 'PE'], 'PATHFIT': ['PE', 'PED', 'PATHFIT'], 'GE': ['GEC', 'GE'], 'GEC': ['GE', 'GEC'], 'NSTP': ['CWTS', 'NSTP'], 'CWTS': ['NSTP', 'CWTS'] }; const aliases = ALIAS_MAP[codeDept] || [codeDept]; if (codeNum) { const aliasMatch = courses.find(c => aliases.includes(c.dept) && c.num === codeNum); if (aliasMatch) return aliasMatch.url; } // Tier 4: Unique 4-digit number match (if exactly 1 course in catalog has this number) if (codeNum && codeNum.length >= 3) { const numMatches = courses.filter(c => c.num === codeNum); if (numMatches.length === 1) return numMatches[0].url; } // Tier 5: Course title keyword overlap if (courseTitle) { const queryWords = courseTitle.toLowerCase() .replace(/[^a-z0-9\s]/g, '') .split(/\s+/) .filter(w => w.length > 3 && !['college', 'university', 'education', 'online', 'bachelor', 'science'].includes(w)); if (queryWords.length >= 2) { let bestMatch = null; let bestHits = 0; courses.forEach(c => { const titleLower = (c.title || '').toLowerCase(); const hits = queryWords.filter(w => titleLower.includes(w)).length; if (hits > bestHits && hits >= 2) { bestHits = hits; bestMatch = c; } }); if (bestMatch && (bestHits >= queryWords.length * 0.5 || bestHits >= 2)) { return bestMatch.url; } } } return null; } // Check local catalog cache (valid for 24 hours) try { const rawCat = localStorage.getItem('amaes_amauoed_catalog_v2'); if (rawCat) { const parsedCat = JSON.parse(rawCat); if (parsedCat && Array.isArray(parsedCat.courses) && (Date.now() - parsedCat.timestamp < 86400000)) { const found = matchInCatalog(parsedCat.courses); if (found) { setStoredAmauoedUrl(cleanCode, found); setLog(`Auto-matched link for ${cleanCode} from catalog!`, 'var(--accent-green)', 'Plan: Ready to fetch and sync question database'); return found; } } } } catch (e) { logDebug(`Catalog cache check error: ${e.message}`); } // Live crawl of amauoed.com/courses directory try { const html = await fetchAmauoedPage('https://amauoed.com/courses'); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const parsedCourses = []; const linkElements = doc.querySelectorAll('a[href*="/courses/"]'); linkElements.forEach(a => { let href = a.getAttribute('href') || ''; if (!href) return; if (href.startsWith('/')) href = 'https://amauoed.com' + href; if (!href.includes('/courses/')) return; const card = a.closest('.card') || a.parentElement || a; const titleElem = card.querySelector('.card-title') || a; const subElem = card.querySelector('.card-subtitle'); const title = (titleElem ? titleElem.innerText : a.innerText || '').trim(); const subtitle = subElem ? subElem.innerText.trim() : ''; // Extract code from subtitle (e.g. "CS-6202 - 107 answers") or slug let rawCode = ''; const subMatch = subtitle.match(/([A-Za-z]+[- ]?\d{3,4})/); if (subMatch) { rawCode = subMatch[1]; } else { const slug = href.split('/').pop() || ''; const slugMatch = slug.match(/([a-zA-Z]+[- ]?\d{3,4}|\d{3,4}[- ]?[a-zA-Z]+)/); if (slugMatch) rawCode = slugMatch[1]; } const clean = rawCode.toUpperCase().replace(/[^A-Z0-9]/g, ''); const num = clean.replace(/\D+/g, ''); const dept = clean.replace(/\d+/g, '').toUpperCase(); if (href && !parsedCourses.some(c => c.url === href)) { parsedCourses.push({ url: href, title, subtitle, rawCode, cleanCode: clean, dept, num }); } }); // Save refreshed catalog to cache if (parsedCourses.length > 0) { localStorage.setItem('amaes_amauoed_catalog_v2', JSON.stringify({ timestamp: Date.now(), courses: parsedCourses })); } const match = matchInCatalog(parsedCourses); if (match) { setStoredAmauoedUrl(cleanCode, match); setLog(`Found & verified link for ${cleanCode}!`, 'var(--accent-green)', 'Plan: Saved to database. Ready to fetch.'); return match; } else { setLog(`No exact amauoed link found for ${cleanCode}`, 'var(--accent-amber)', 'Plan: Paste link manually or check Google'); return null; } } catch (err) { console.error('AMAUOED auto-find error:', err); setLog(`Auto-search failed: Network error`, 'var(--accent-pink)', 'Plan: Check internet connection or enter link manually'); return null; } } function getCachedAnswers(code) { const sharingDisabled = ANSWER_SHARING_DISABLED_COURSES.has(String(code || '').toUpperCase()); const raw = localStorage.getItem(`amaes_amauoed_cache_${code}`); if (!raw) return null; try { const storedSchema = Number(localStorage.getItem(`amaes_cache_schema_${code}`) || 1); if (Number.isFinite(storedSchema) && storedSchema > ANSWER_DB_SCHEMA_VERSION) { logDebug(`Ignoring newer answer cache schema ${storedSchema} for ${code}; current schema is ${ANSWER_DB_SCHEMA_VERSION}.`); return null; } const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) { logDebug(`Ignoring incompatible answer cache for ${code}.`); return null; } let normalized = parsed .filter(item => item && typeof item === 'object') .map(item => ({ ...item, qNorm: item.qNorm || normalizeText(item.qRaw || item.question || '') })) .filter(item => Boolean(item.qNorm)); if (sharingDisabled) { normalized = normalized.filter(item => item.verified !== true && item.evidenceType === 'study_guide_candidate' && /amauoed|jennysonline/i.test(String(item.source || ''))); localStorage.setItem(`amaes_amauoed_cache_${code}`, JSON.stringify(normalized)); } if (storedSchema < ANSWER_DB_SCHEMA_VERSION) { localStorage.setItem(`amaes_cache_schema_${code}`, String(ANSWER_DB_SCHEMA_VERSION)); } return normalized; } catch (e) { logDebug(`Ignoring unreadable answer cache for ${code}.`); return null; } } function setCachedAnswers(code, questions) { if (!Array.isArray(questions)) { throw new TypeError('Answer cache must be an array'); } if (ANSWER_SHARING_DISABLED_COURSES.has(String(code || '').toUpperCase())) { questions = questions.filter(item => item && item.verified !== true && item.evidenceType === 'study_guide_candidate' && /amauoed|jennysonline/i.test(String(item.source || ''))); } localStorage.setItem(`amaes_amauoed_cache_${code}`, JSON.stringify(questions)); localStorage.setItem(`amaes_cache_schema_${code}`, String(ANSWER_DB_SCHEMA_VERSION)); } // ========================================== // Jenny's Online public study-guide sources // ========================================== const jennysonlineSessionCache = new Map(); const JENNYSONLINE_CACHE_TTL = 30 * 24 * 60 * 60 * 1000; const JENNYSONLINE_SESSION_RECHECK_TTL = 2 * 60 * 1000; function getJennysonlineCacheKey(code) { return `amaes_jennysonline_cache_v1_${code}`; } function readJennysonlinePersistentCache(code) { try { const raw = localStorage.getItem(getJennysonlineCacheKey(code)); if (!raw) return null; const cached = JSON.parse(raw); if (!cached || !Number.isFinite(cached.savedAt) || Date.now() - cached.savedAt < 0 || Date.now() - cached.savedAt >= JENNYSONLINE_CACHE_TTL || !Array.isArray(cached.answers) || cached.answers.length > 5000 || cached.answers.some(answer => !answer || answer.verified !== false || answer.source !== 'jennysonline' || answer.evidenceType !== 'study_guide_candidate')) { localStorage.removeItem(getJennysonlineCacheKey(code)); return null; } return cached.answers; } catch (error) { if (typeof logDebug === 'function') logDebug(`Jenny's Online local cache read note: ${error.message}`); return null; } } function writeJennysonlinePersistentCache(code, answers, savedAt = Date.now()) { try { localStorage.setItem(getJennysonlineCacheKey(code), JSON.stringify({ savedAt, answers })); } catch (error) { if (typeof logDebug === 'function') logDebug(`Jenny's Online local cache write note: ${error.message}`); } } function queueStudyGuideRefresh(code) { try { const key = `amaes_study_guide_refresh_request_v1_${code}`; const requestedAt = Number(localStorage.getItem(key) || 0); if (Date.now() - requestedAt < 15 * 60 * 1000) return; localStorage.setItem(key, String(Date.now())); fetch(`${communityRelayUrl}/study-guides/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Installation': getAnonymousContributorId(), 'X-AMAES-Client-Version': CLIENT_VERSION }, body: JSON.stringify({ subjectCode: code }) }).then(async response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); logDebug(`Shared study-guide refresh for ${code}: ${result.queued ? 'queued' : 'not queued'}`); }).catch(error => { localStorage.removeItem(key); logDebug(`Shared study-guide refresh queue note for ${code}: ${error.message}`); }); } catch (error) { logDebug(`Shared study-guide refresh queue note for ${code}: ${error.message}`); } } function fetchJennysonlineText(url) { return new Promise((resolve, reject) => { const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (!gmReq) { fetch(url).then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.text(); }).then(resolve).catch(reject); return; } gmReq({ method: 'GET', url, onload: response => { if (response.status >= 200 && response.status < 300) { resolve(response.responseText); } else { reject(new Error(`HTTP ${response.status}: ${response.statusText || 'Request failed'}`)); } }, onerror: () => reject(new Error(`Network error fetching ${new URL(url).hostname}`)) }); }); } function parseJennysonlineSharedSnapshot(registry) { if (!Array.isArray(registry.questions) || registry.questions.length === 0) return []; return registry.questions .filter(question => question && question.verified === false) .map(question => { const qRaw = question.question || question.qRaw || ''; const ansRaw = question.answer || question.ansRaw || ''; return { qRaw, qNorm: normalizeText(qRaw), ansRaw, ansNorm: normalizeChoice(ansRaw), answers: Array.isArray(question.answers) ? question.answers : undefined, choices: Array.isArray(question.choices) ? question.choices : [], verified: false, confirmations: 1, source: 'jennysonline', evidenceType: 'study_guide_candidate', sourceUrl: question.sourceUrl || registry.sourceUrl }; }) .filter(question => question.qNorm && question.ansNorm); } async function loadJennysonlineAnswersForCourse(subjectCode) { const code = String(subjectCode || '').toUpperCase().replace(/[^A-Z0-9]/g, ''); if (!code || code === 'GENERAL' || code === 'DEFAULT') return []; const sessionEntry = jennysonlineSessionCache.get(code); if (sessionEntry) { if (typeof sessionEntry.then === 'function') return sessionEntry; if (sessionEntry.expiresAt > Date.now()) return sessionEntry.answers; jennysonlineSessionCache.delete(code); } const cachedAnswers = readJennysonlinePersistentCache(code); const loading = (async () => { let staleSharedAnswers = []; try { const registryUrl = `${CLOUD_DB_JENNYSONLINE_URL}${code}.json`; const registry = JSON.parse(await fetchJennysonlineText(registryUrl)); if (registry.subjectCode !== code || registry.verified !== false) { throw new Error(`Invalid or unverified Jenny's Online snapshot for ${code}`); } const sharedAnswers = parseJennysonlineSharedSnapshot(registry); if (sharedAnswers.length > 0) { staleSharedAnswers = sharedAnswers; const updatedAt = Date.parse(registry.refreshedAt || registry.updatedAt || ''); const age = Date.now() - updatedAt; if (Number.isFinite(updatedAt) && age >= 0 && age < JENNYSONLINE_CACHE_TTL) { writeJennysonlinePersistentCache(code, sharedAnswers, updatedAt); return { answers: sharedAnswers, expiresAt: updatedAt + JENNYSONLINE_CACHE_TTL }; } } } catch (error) { if (!/HTTP 404/.test(error.message)) { logDebug(`Jenny's Online shared snapshot note for ${code}: ${error.message}`); } } queueStudyGuideRefresh(code); const fallbackAnswers = staleSharedAnswers.length > 0 ? staleSharedAnswers : cachedAnswers || []; return { answers: fallbackAnswers, expiresAt: Date.now() + JENNYSONLINE_SESSION_RECHECK_TTL }; })(); const pending = loading.then(result => { jennysonlineSessionCache.set(code, result); return result.answers; }).catch(error => { jennysonlineSessionCache.delete(code); throw error; }); jennysonlineSessionCache.set(code, pending); return pending; } // ========================================== // Robust Quiz Navigation & Autonomous Solver Engine // ========================================== // Inspect Moodle Quiz Navigation block to detect answered vs unanswered questions function getQuizNavQuestionStates() { const navBlock = document.querySelector('#mod_quiz_navblock, .block_quiz_navigation, .qn_buttons'); if (!navBlock) return null; const buttons = Array.from(navBlock.querySelectorAll('.qnbutton, a[id^="quiznavbutton"], button[id^="quiznavbutton"]')); if (buttons.length === 0) return null; const questionList = []; let currentIndex = -1; const thisPageIndices = []; buttons.forEach((btn, idx) => { const isCurrent = btn.classList.contains('thispage') || btn.getAttribute('aria-current') === 'true'; if (isCurrent) thisPageIndices.push(idx); // In Moodle, answered questions have class .answersaved, or title/aria-label containing "Answer saved" const title = (btn.getAttribute('title') || '').toLowerCase(); const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase(); const hasAnswerSavedClass = btn.classList.contains('answersaved'); const isAnswered = hasAnswerSavedClass || title.includes('answer saved') || ariaLabel.includes('answer saved'); const qNum = parseInt(btn.innerText.trim(), 10) || (idx + 1); questionList.push({ btn, qNum, index: idx, isCurrent: false, isAnswered }); }); if (thisPageIndices.length === 1) { currentIndex = thisPageIndices[0]; } else if (thisPageIndices.length > 1) { // Multi-question page: resolve active question card on screen let activeQue = userSelectedActiveQuestion; if (!activeQue || !document.contains(activeQue)) { activeQue = document.querySelector('.que.amaes-active-focus-que'); } if (!activeQue) { const blockedQue = document.querySelector('.que .amaes-blockage-hud')?.closest('.que'); if (blockedQue) activeQue = blockedQue; } if (activeQue) { const qData = extractQuestionData(activeQue); const qNum = qData ? qData.qNum : null; if (qNum) { const matchIdx = questionList.findIndex(q => q.qNum === qNum); if (matchIdx >= 0) currentIndex = matchIdx; } } // Fallback: first unanswered question among this page's questions, or the first question on this page if (currentIndex < 0) { const firstUnanswered = thisPageIndices.find(idx => !questionList[idx].isAnswered); currentIndex = (firstUnanswered !== undefined) ? firstUnanswered : thisPageIndices[0]; } } else if (questionList.length > 0) { currentIndex = 0; } if (currentIndex >= 0 && currentIndex < questionList.length) { questionList[currentIndex].isCurrent = true; } // Find the next UNANSWERED question AFTER the current question let nextUnanswered = null; if (currentIndex >= 0) { for (let i = currentIndex + 1; i < questionList.length; i++) { if (!questionList[i].isAnswered) { nextUnanswered = questionList[i]; break; } } // If none ahead, wrap around to check earlier skipped questions if (!nextUnanswered) { for (let i = 0; i < currentIndex; i++) { if (!questionList[i].isAnswered) { nextUnanswered = questionList[i]; break; } } } } const totalUnanswered = questionList.filter(q => !q.isAnswered).length; const totalAnswered = questionList.length - totalUnanswered; return { navBlock, questions: questionList, currentIndex, nextUnanswered, totalUnanswered, totalAnswered, totalQuestions: questionList.length, allAnswered: totalUnanswered === 0 }; } // Helper to find the Next Page button on Moodle Quiz Attempt page function findQuizNextButton() { // 1. Primary: Next button inside Moodle's submitbtns or responseform let btn = document.querySelector( '#responseform .submitbtns input[name="next"], ' + '#responseform .submitbtns button[name="next"], ' + '#responseform .submitbtns input[type="submit"]:not([name="previous"]):not([value*="Previous"]), ' + '#responseform .submitbtns button:not([name="previous"]):not(.btn-secondary), ' + '.submitbtns input[name="next"], ' + '.submitbtns button[name="next"], ' + '.submitbtns input[value*="Next"], ' + '.submitbtns button[value*="Next"], ' + 'input[name="next"].mod_quiz-next-nav, ' + '#mod_quiz-next-nav, ' + '.mod_quiz-next-nav' ); // 2. If on the final question, look for "Finish attempt ..." if (!btn) { btn = document.querySelector( '#responseform .submitbtns input[value*="Finish attempt"], ' + '#responseform .submitbtns button[value*="Finish attempt"], ' + '.submitbtns input[value*="Finish attempt"], ' + '.submitbtns button[value*="Finish attempt"], ' + 'input[value*="Finish attempt"], ' + 'button[value*="Finish attempt"], ' + 'input[value*="Finish"], ' + 'button[value*="Finish"]' ); } // 3. Fallback: Any submit button inside responseform that does not navigate backwards if (!btn) { const submits = document.querySelectorAll('#responseform input[type="submit"], #responseform button[type="submit"]'); for (const s of submits) { const val = (s.value || s.innerText || '').toLowerCase(); if (!val.includes('prev') && !val.includes('back')) { btn = s; break; } } } return btn; } // Helper to find the Previous Page button on Moodle Quiz Attempt page function findQuizPrevButton() { let btn = document.querySelector( '#responseform .submitbtns input[name="previous"], ' + '#responseform .submitbtns button[name="previous"], ' + '#responseform .submitbtns input[value*="Previous page"], ' + '#responseform .submitbtns button[value*="Previous page"], ' + '#responseform .submitbtns input[value*="Previous"], ' + '#responseform .submitbtns button[value*="Previous"], ' + '.submitbtns input[name="previous"], ' + '.submitbtns button[name="previous"], ' + '.submitbtns input[value*="Previous page"], ' + '.submitbtns button[value*="Previous page"], ' + '.submitbtns input[value*="Previous"], ' + '.submitbtns button[value*="Previous"], ' + 'input[name="previous"].mod_quiz-prev-nav, ' + '#mod_quiz-prev-nav, ' + '.mod_quiz-prev-nav' ); if (!btn) { const submits = document.querySelectorAll('#responseform input[type="submit"], #responseform button[type="submit"]'); for (const s of submits) { const val = (s.value || s.innerText || '').toLowerCase(); if (val.includes('prev') || val.includes('back')) { btn = s; break; } } } return btn; } // Active Question State & Visual Focus Tracking let userSelectedActiveQuestion = null; let userActiveLockUntil = 0; function setActiveQuestion(que, userClicked = false) { if (!que) return; userSelectedActiveQuestion = que; if (userClicked) { userActiveLockUntil = Date.now() + 8000; } document.querySelectorAll('.que').forEach(q => { if (q === que) { q.classList.add('amaes-active-focus-que'); } else { q.classList.remove('amaes-active-focus-que'); } }); } // Resolve the question currently in viewport, active, or first unanswered on multi-question pages function getActiveViewportQuestion() { const questions = Array.from(document.querySelectorAll('.que')); if (questions.length === 0) return null; if (questions.length === 1) { setActiveQuestion(questions[0], false); return questions[0]; } // If user explicitly clicked a question and it is still visible within/near viewport if (userSelectedActiveQuestion && document.contains(userSelectedActiveQuestion)) { const rect = userSelectedActiveQuestion.getBoundingClientRect(); const vh = window.innerHeight || 800; if (rect.bottom > 60 && rect.top < vh - 60) { setActiveQuestion(userSelectedActiveQuestion, false); return userSelectedActiveQuestion; } } const viewportHeight = window.innerHeight || 800; let bestQue = null; let bestScore = -1; for (const q of questions) { const rect = q.getBoundingClientRect(); // Discard if element is completely scrolled out of viewport if (rect.bottom < 40 || rect.top > viewportHeight - 40) continue; const visibleTop = Math.max(0, rect.top); const visibleBottom = Math.min(viewportHeight, rect.bottom); const visibleHeight = Math.max(0, visibleBottom - visibleTop); if (visibleHeight <= 0) continue; // Prioritize questions whose header or body is in the primary reading band (-80px to 60% viewport) let score = visibleHeight; if (rect.top >= -80 && rect.top <= viewportHeight * 0.6) { score += 600; } if (score > bestScore) { bestScore = score; bestQue = q; } } if (!bestQue) { bestQue = questions.find(q => !q.classList.contains('answered') && !q.classList.contains('complete')) || questions[0]; } if (bestQue) { setActiveQuestion(bestQue, false); } return bestQue; } // Check whether every question on the current attempt page has an answer selected/entered function areAllPageQuestionsAnswered() { const queList = document.querySelectorAll('.que'); if (queList.length === 0) return false; for (const que of queList) { // 1. Radio: check if any radio in question is checked const radios = que.querySelectorAll('.answer input[type="radio"]'); if (radios.length > 0) { const anyChecked = Array.from(radios).some(r => r.checked); if (!anyChecked) return false; continue; } // 2. Checkbox: multi-choice questions (require all verified highlighted choices to be checked) const checkboxes = que.querySelectorAll('.answer input[type="checkbox"]'); if (checkboxes.length > 0) { const highlightedBoxes = que.querySelectorAll('.amaes-highlighted-choice input[type="checkbox"]'); if (highlightedBoxes.length > 0) { const allVerifiedChecked = Array.from(highlightedBoxes).every(c => c.checked); if (!allVerifiedChecked) return false; } else { const anyChecked = Array.from(checkboxes).some(c => c.checked); if (!anyChecked) return false; } continue; } // 3. Select dropdowns (matching questions) const selects = que.querySelectorAll('select'); if (selects.length > 0) { const allSelected = Array.from(selects).every(s => s.value && s.value !== '0' && s.value !== ''); if (!allSelected) return false; continue; } // 4. Text input / textarea (shortanswer questions) const textInputs = que.querySelectorAll('input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); if (textInputs.length > 0) { const allFilled = Array.from(textInputs).every(inp => inp.value && inp.value.trim().length > 0); if (!allFilled) return false; continue; } // 5. Drag and Drop questions (ddwtos, ddimageortext, ddmarker) const dropZones = que.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable'); const placeInputs = que.querySelectorAll('input.placeinput, input[type="hidden"][name*="_p"]'); if (dropZones.length > 0 || placeInputs.length > 0) { if (placeInputs.length > 0) { const allPlaced = Array.from(placeInputs).every(inp => inp.value && inp.value !== '0' && inp.value.trim() !== ''); if (!allPlaced) return false; } else { const allFilled = Array.from(dropZones).every(dz => dz.innerText && dz.innerText.trim().length > 0); if (!allFilled) return false; } continue; } // 6. Moodle state classes if (que.classList.contains('answered') || que.classList.contains('complete')) { continue; } // Fallback: unrecognized question type with no checked answer return false; } return true; } function isQuestionAnswered(que) { if (!que) return false; let answered = false; const radios = que.querySelectorAll('.answer input[type="radio"]'); if (radios.length > 0) { answered = Array.from(radios).some(input => input.checked); } else { const checkboxes = que.querySelectorAll('.answer input[type="checkbox"]'); if (checkboxes.length > 0) { const highlighted = que.querySelectorAll('.amaes-highlighted-choice input[type="checkbox"]'); answered = highlighted.length > 0 ? Array.from(highlighted).every(input => input.checked) : Array.from(checkboxes).some(input => input.checked); } else { const selects = que.querySelectorAll('select'); if (selects.length > 0) { answered = Array.from(selects).every(select => select.value && select.value !== '0'); } else { const textInputs = que.querySelectorAll( 'input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])' ); if (textInputs.length > 0) { answered = Array.from(textInputs).every(input => input.value && input.value.trim()); } else { const placeInputs = que.querySelectorAll('input.placeinput, input[type="hidden"][name*="_p"]'); const dropZones = que.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable'); if (placeInputs.length > 0 || dropZones.length > 0) { answered = placeInputs.length > 0 && Array.from(placeInputs).every(input => input.value && input.value !== '0' && input.value.trim()); } else { const anyInputs = que.querySelectorAll('input:not([type="hidden"]), select, textarea'); if (anyInputs.length > 0) { const hasChecked = Array.from(anyInputs).some(i => (i.type === 'radio' || i.type === 'checkbox') && i.checked); const hasValue = Array.from(anyInputs).some(i => i.type !== 'radio' && i.type !== 'checkbox' && i.value && i.value.trim() && i.value !== '0'); answered = Boolean(hasChecked || hasValue); } else { answered = que.classList.contains('answered') || que.classList.contains('complete'); } } } } } } que.dataset.answered = answered ? 'true' : 'false'; return answered; } function findNextUnansweredOnCurrentPage(sourceQue) { const questions = Array.from(document.querySelectorAll('.que')); if (questions.length < 2) return null; const startIndex = sourceQue ? questions.indexOf(sourceQue) : -1; const ordered = startIndex >= 0 ? questions.slice(startIndex + 1).concat(questions.slice(0, startIndex)) : questions; return ordered.find(que => !isQuestionAnswered(que)) || null; } function findPreviousQuestionOnCurrentPage(sourceQue) { const questions = Array.from(document.querySelectorAll('.que')); if (questions.length < 2) return null; const currentIndex = sourceQue ? questions.indexOf(sourceQue) : -1; if (currentIndex > 0) { return questions[currentIndex - 1]; } return null; } function findFirstUnansweredOnCurrentPage() { const questions = Array.from(document.querySelectorAll('.que')); return questions.find(que => !isQuestionAnswered(que)) || null; } function isConfirmedCandidate(item) { if (!item) return false; const src = String(item.source || '').toLowerCase(); const evType = String(item.evidenceType || '').toLowerCase(); const trustedSource = [ 'review_screen', 'moodle_review', 'moodle_review_verified', 'moodle_100_percent', 'verified_db' ].includes(src); const trustedEvidence = ['moodle_review', '100_percent', 'moodle_100_percent', 'moodle_review_elimination'].includes(evType); const unconfirmedSource = /community|gemini|(^|[^a-z])ai([^a-z]|$)|amauoed|jennysonline/.test(src); // Unconfirmed source labels are overridden only by explicit Moodle evidence. if ((unconfirmedSource || item.isAiSuggestion) && !trustedEvidence) { return false; } if (item.verified !== true) return false; // Only exact, traceable evidence labels qualify. A low/high overall // score or a generic "deduced" flag does not prove an answer. return trustedSource || trustedEvidence; } function getStudyGuideInfo(item) { const sources = [item && item.source, ...(Array.isArray(item && item.sources) ? item.sources : [])] .map(source => String(source || '').toLowerCase()) .join(' '); if (sources.includes('jennysonline') || sources.includes("jenny's online")) { return { label: "Jenny's Online", url: item.sourceUrl || 'https://jennysonline.blogspot.com/2021/03/information-assurance-and-security-1.html' }; } if (sources.includes('amauoed')) { const subCode = detectCourseInfo().subjectCode || 'CS6301'; return { label: 'AMAUOED', url: getStoredAmauoedUrl(subCode) || 'https://amauoed.com/courses' }; } return null; } function setQuestionStudyGuideTag(que, label) { if (!que) return; que.querySelectorAll('.amaes-study-guide-question-tag').forEach(el => el.remove()); const formulation = que.querySelector('.formulation, .content') || que; const tag = document.createElement('div'); tag.className = 'amaes-study-guide-question-tag'; tag.style.cssText = ` display: inline-flex; align-items: center; margin-bottom: 8px; padding: 4px 10px; background: rgba(2, 132, 199, 0.09); border: 1px solid rgba(2, 132, 199, 0.32); border-left: 3px solid #0284c7; border-radius: 6px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; line-height: 1.35; box-sizing: border-box; `; tag.title = `This answer matches an unverified ${label} study-guide suggestion. Review before submitting.`; tag.innerHTML = ` STUDY-GUIDE MATCH Suggested by ${escapeHtml(label)} (Unverified — review before submitting) `; const qtextElem = que.querySelector('.qtext, .formulation .qtext'); if (qtextElem && qtextElem.parentNode === formulation) { formulation.insertBefore(tag, qtextElem); } else { formulation.insertBefore(tag, formulation.firstChild); } } // Contextual in-question action chip guiding students when unsure or stuck function renderUnsureActionChip(que) { if (!que || que.querySelector('.amaes-unsure-help-chip') || que.querySelector('.amaes-blockage-hud') || que.querySelector('.amaes-new-question-focus-banner') || checkIsReviewPage()) return; const formulation = que.querySelector('.formulation, .content') || que; const chip = document.createElement('div'); chip.className = 'amaes-unsure-help-chip'; chip.style.cssText = ` display: inline-flex; align-items: center; gap: 8px; margin-top: 8px; margin-bottom: 6px; padding: 6px 12px; background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: 6px; font-size: 11px; color: var(--accent-blue, #3b82f6); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; cursor: pointer; user-select: none; transition: all 0.2s ease; `; chip.title = 'Click to skip to the next question or page'; chip.innerHTML = ` ${ICONS.lightbulb} Unsure? Pick an answer or press N to skip to next question Skip → `; chip.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (typeof triggerNextQuizPage === 'function') { triggerNextQuizPage(); } else { const nextBtn = document.querySelector('.submitbtns input[value*="Next"], input[value*="Next page"], button[value*="Next"]'); if (nextBtn) nextBtn.click(); } }; formulation.appendChild(chip); } // Connects keyboard shortcut [N] directly to the Moodle Next Page button on screen // Connects keyboard shortcut [N] directly inside Moodle's native Next Page button on screen function updateNextButtonIndicator() { if (!checkIsQuizAttemptPage()) return; const nextBtn = findQuizNextButton() || document.querySelector('.submitbtns input[type="submit"], .submitbtns button[type="submit"], input[value*="Next page"], input[value*="Next"], button[value*="Next"]'); if (!nextBtn) return; // Unify text directly into Moodle's native button HTML / label if (!nextBtn.dataset.amaesOriginalText) { nextBtn.dataset.amaesOriginalText = nextBtn.tagName === 'INPUT' ? nextBtn.value : nextBtn.textContent; } const orig = nextBtn.dataset.amaesOriginalText || 'Next page'; if (!orig.includes('(Press N)')) { const unifiedText = `${orig.trim()} (Press N)`; if (nextBtn.tagName === 'INPUT') { nextBtn.value = unifiedText; } else { nextBtn.textContent = unifiedText; } } // Clean up standalone beacon to unify directly into Moodle button document.getElementById('amaes-next-btn-beacon')?.remove(); } // High-visibility focused banner for unanswered questions: clear, non-technical guidance and 1-click actions function renderNewQuestionFocusBanner(que, qNum) { if (!que || checkIsReviewPage()) return; // Clean up any stale hints/chips on this question to guarantee a single banner que.querySelectorAll('.amaes-unanswered-hint, .amaes-unsure-help-chip, .amaes-ai-fallback-bar, .amaes-new-question-focus-banner, .amaes-blockage-hud').forEach(el => el.remove()); const formulation = que.querySelector('.formulation, .content') || que; const banner = document.createElement('div'); banner.className = 'amaes-new-question-focus-banner'; banner.style.cssText = ` margin-bottom: 12px; padding: 10px 14px; background: #fffbeb; border: 1.5px solid #f59e0b; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 4px 12px rgba(245, 158, 11, 0.1); box-sizing: border-box; color: #78350f; transition: all 0.2s ease; `; const isMatchBanner = que && ((typeof identifyQuestionType === 'function' && identifyQuestionType(que) === 'match') || que.classList.contains('match') || que.classList.contains('qtype_match') || que.querySelectorAll('select').length > 1); banner.innerHTML = `
NO SAVED ANSWER Question #${qNum || ''}: No saved answer yet
${isMatchBanner ? '' : 'Question copied'}
${isMatchBanner ? 'No saved answers for these matching items yet. Select the matching choices below, or consult Web AI tools above.' : 'Question copied to clipboard! Pick an answer below or click Next Question, or use the AI tools at the side or bottom.' }
No answer known to the system yet — be the first to answer and share it!
`; const skipBtn = banner.querySelector('.amaes-btn-skip-q'); if (skipBtn) { skipBtn.onclick = (e) => { e.preventDefault(); if (typeof triggerNextQuizPage === 'function') { triggerNextQuizPage(); } else { const nextBtn = document.querySelector('.submitbtns input[value*="Next"], input[value*="Next page"], button[value*="Next"]'); if (nextBtn) nextBtn.click(); } }; } // Choice selection visual feedback const choiceInputs = que.querySelectorAll('input[type="radio"], input[type="checkbox"]'); choiceInputs.forEach(input => { input.addEventListener('change', () => { const instr = banner.querySelector('.amaes-focus-instruction'); if (instr) { banner.style.borderColor = '#10b981'; banner.style.background = '#f0fdf4'; banner.style.color = '#065f46'; instr.innerHTML = `Answer selected! Click Next Question when ready.`; } }, { once: true }); }); formulation.insertBefore(banner, formulation.firstChild); } function saveQuizAttemptSummary(subCode, data) { if (!data || typeof data.percentage !== 'number') return; const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; try { localStorage.setItem(`amaes_quiz_summary_${sCode}`, JSON.stringify(data)); if (data.isReviewPermitted === false) { localStorage.setItem(`amaes_course_reviewability_${sCode}`, JSON.stringify({ status: 'restricted-observed', observedAt: data.recordedAt || Date.now() })); } const quizId = getQuizStorageId(); if (quizId) { localStorage.setItem(`amaes_quiz_summary_${sCode}_${quizId}`, JSON.stringify(data)); } } catch (_) {} } function reportCourseReviewRestriction(subCode) { const code = String(subCode || '').toUpperCase().replace(/[^A-Z0-9_-]/g, ''); if (!code || code === 'GENERAL' || code === 'DEFAULT') return; const key = `amaes_reviewability_reported_${code}`; try { const reportedAt = Number(localStorage.getItem(key) || 0); if (Date.now() - reportedAt < 24 * 60 * 60 * 1000) return; localStorage.setItem(key, String(Date.now())); fetch(`${communityRelayUrl}/course-reviewability/report`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Installation': getAnonymousContributorId(), 'X-AMAES-Client-Version': CLIENT_VERSION }, body: JSON.stringify({ subjectCode: code, reviewPermitted: false }) }).then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); logDebug(`Restricted quiz review observed for ${code}; anonymous course status report submitted.`); }).catch(error => { localStorage.removeItem(key); logDebug(`Course reviewability report note for ${code}: ${error.message}`); }); } catch (error) { logDebug(`Course reviewability report note for ${code}: ${error.message}`); } } function getQuizAttemptSummary(subCode = '') { const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; try { const quizId = getQuizStorageId(); if (quizId) { const specific = localStorage.getItem(`amaes_quiz_summary_${sCode}_${quizId}`); return specific ? JSON.parse(specific) : null; } const general = localStorage.getItem(`amaes_quiz_summary_${sCode}`); if (general) return JSON.parse(general); return null; } catch (_) { return null; } } function getQuizStorageId() { const params = new URLSearchParams(window.location.search); return params.get('cmid') || params.get('id') || params.get('q') || params.get('quiz') || ''; } function cleanFillBlankAnswer(ans) { if (!ans) return ''; let clean = String(ans).trim(); clean = clean.replace(/^["'“”]+|["'“”]+$/g, '').trim(); clean = clean.replace(/^(?:(?:question|item|q)\s*(?:no\.?|#)?\s*\d+[\s:.-]+|\(\d+\)|\d+[.)])\s+|^answer\s*[:.-]\s*/i, '').trim(); clean = clean.replace(/^[a-e][.)]\s*/i, '').trim(); return clean; } function getAttemptEvidenceKey() { const params = new URLSearchParams(window.location.search); const attemptId = params.get('attempt'); if (attemptId) return `amaes_attempt_evidence_${attemptId}`; const quizId = getQuizStorageId(); const mappedAttemptId = quizId ? sessionStorage.getItem(`amaes_current_attempt_${quizId}`) : ''; return `amaes_attempt_evidence_${mappedAttemptId || quizId || 'latest'}`; } function recordAttemptAnswerEvidence(que, answer, source = 'manual_selection', metadata = {}) { if (!que || !answer) return; const qData = extractQuestionData(que); const qText = metadata.subQuestionText || (qData && qData.qText); if (!qText) return; try { const key = getAttemptEvidenceKey(); const attemptId = new URLSearchParams(window.location.search).get('attempt'); const quizId = getQuizStorageId(); if (attemptId && quizId) { sessionStorage.setItem(`amaes_current_attempt_${quizId}`, attemptId); } const current = JSON.parse(sessionStorage.getItem(key) || '[]'); const entry = { qRaw: qText, qNorm: normalizeText(qText), ansRaw: String(answer).trim(), ansNorm: normalizeChoice(answer), choices: metadata.subQuestionText ? [] : (qData.choices || []), questionType: metadata.questionType || (metadata.subQuestionText ? 'match_item' : (qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : (Array.isArray(qData.choices) && qData.choices.length > 0 ? 'multichoice' : 'shortanswer')))), answers: Array.isArray(metadata.answers) ? metadata.answers.map(value => String(value || '').trim()).filter(Boolean) : undefined, source, isAiSuggestion: source === 'ai_inference', verified: Boolean(metadata.verified), isAdaptiveProbe: Boolean(metadata.isAdaptiveProbe), recordedAt: Date.now() }; const next = current.filter(item => item.qNorm !== entry.qNorm); next.push(entry); sessionStorage.setItem(key, JSON.stringify(next.slice(-100))); } catch (err) { logDebug(`Attempt evidence storage note: ${err.message}`); } } function syncCurrentPageSelectionsToEvidence() { if (!checkIsQuizAttemptPage()) return; const ques = document.querySelectorAll('.que'); ques.forEach(que => { const checkedInputs = Array.from(que.querySelectorAll('.answer input[type="radio"]:checked, .answer input[type="checkbox"]:checked')); if (checkedInputs.length > 0) { const choiceTexts = checkedInputs.map(input => { const label = input.closest('label') || input.parentElement; return cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); }).filter(Boolean); const isVerified = Boolean(que.querySelector('.amaes-verified-badge')); const isAdaptive = Boolean(que.querySelector('.amaes-adaptive-probe-badge')); if (choiceTexts.length > 0) { recordAttemptAnswerEvidence(que, choiceTexts.join(', '), isAdaptive ? 'adaptive_probe' : (isVerified ? 'verified_db' : 'manual'), { verified: isVerified, isAdaptiveProbe: isAdaptive, answers: choiceTexts }); } } else { const textInputs = Array.from(que.querySelectorAll('input[type="text"], input.form-control, textarea')); if (textInputs.length > 0) { const textAnswers = textInputs.map(input => String(input.value || '').trim()); if (textAnswers.every(Boolean)) { recordAttemptAnswerEvidence(que, textAnswers.length === 1 ? textAnswers[0] : textAnswers.join(', '), 'manual_text', { answers: textAnswers }); } return; } const selectInputs = Array.from(que.querySelectorAll('select')); if (selectInputs.length > 0) { const isMatch = typeof identifyQuestionType === 'function' && identifyQuestionType(que) === 'match'; const matchRows = Array.from(que.querySelectorAll('.answer table tr, .answer tr')).filter(r => r.querySelector('select') && r.querySelector('td.text, td:first-child')); if (isMatch || matchRows.length > 0) { matchRows.forEach(row => { const promptCell = row.querySelector('td.text, td:first-child'); const sel = row.querySelector('select'); const subQText = promptCell ? cleanDOMToAI(promptCell).trim() : ''; const opt = sel && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex] : null; const text = (opt && opt.value && !opt.text.toLowerCase().includes('choose')) ? String(opt.text || opt.innerText || '').trim() : ''; if (subQText && text) { recordAttemptAnswerEvidence(que, text, 'manual_match_select', { answers: [text], subQuestionText: subQText, questionType: 'match_item' }); } }); return; } // Only record single atomic select answers; never record joined comma blobs for composite questions if (selectInputs.length === 1) { const option = selectInputs[0].selectedIndex >= 0 ? selectInputs[0].options[selectInputs[0].selectedIndex] : null; const text = (option && option.value && !option.text.toLowerCase().includes('choose')) ? String(option.text || option.innerText || '').trim() : ''; if (text) { recordAttemptAnswerEvidence(que, text, 'manual_select', { answers: [text] }); } } } } }); } function getProbeHistory(subCode = '') { const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; try { return JSON.parse(localStorage.getItem(`amaes_probe_history_${sCode}_${getQuizStorageId() || 'general'}`) || '{}'); } catch (_) { return {}; } } function saveProbeHistory(subCode, history) { const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; try { localStorage.setItem(`amaes_probe_history_${sCode}_${getQuizStorageId() || 'general'}`, JSON.stringify(history)); } catch (e) { logDebug(`Failed to save probe history: ${e.message}`); } } function getProbeEntry(qText, subCode = '') { if (!qText) return null; const qNorm = normalizeText(qText); const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; const history = getProbeHistory(sCode); if (history[qNorm]) return history[qNorm]; const matchKey = Object.keys(history).find(k => questionTextMatches(k, qNorm)); return matchKey ? history[matchKey] : null; } function markProbeQuestionsSolved(subCode, promotedItems) { if (!Array.isArray(promotedItems) || promotedItems.length === 0) return; const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; const history = getProbeHistory(sCode); promotedItems.forEach(item => { if (history[item.qNorm]) { history[item.qNorm].solved = true; history[item.qNorm].verifiedAnswer = item.ansRaw; } }); saveProbeHistory(sCode, history); } function recordAdaptiveProbeAttempt(subCode, evidence, earned, maximum, isReviewPermitted) { if (!Array.isArray(evidence) || evidence.length === 0) return; const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; const history = getProbeHistory(sCode); const percentage = Math.round((earned / maximum) * 100); // Update probe tracking history evidence.forEach(item => { const entry = history[item.qNorm] || { qNorm: item.qNorm, qRaw: item.qRaw, triedChoices: [], attempts: [] }; const choiceNorm = item.ansNorm; if (choiceNorm && !entry.triedChoices.includes(choiceNorm)) { entry.triedChoices.push(choiceNorm); } entry.lastChoice = item.ansRaw; entry.lastNorm = choiceNorm; entry.lastEarned = earned; entry.lastMax = maximum; entry.lastPercentage = percentage; entry.updatedAt = Date.now(); entry.attempts.push({ choice: item.ansRaw, norm: choiceNorm, earned, maximum, percentage, date: Date.now() }); history[item.qNorm] = entry; }); saveProbeHistory(sCode, history); } function getUnreviewedAttemptEntry(qText, subCode = '') { if (!qText) return null; const qNorm = normalizeText(qText); const sCode = subCode || (detectCourseInfo().subjectCode) || 'GENERAL'; try { const history = JSON.parse(localStorage.getItem(`amaes_unreviewed_history_${sCode}_${getQuizStorageId() || 'general'}`) || '[]'); return history.find(h => questionTextMatches(h.qNorm || h.qRaw, qNorm)) || null; } catch (_) { return null; } } function saveUnreviewedAttemptEvidence(subCode, evidence, earned, maximum, isReviewPermitted) { if (!Array.isArray(evidence) || evidence.length === 0) return; const sCode = subCode || 'GENERAL'; const storageKey = `amaes_unreviewed_history_${sCode}_${getQuizStorageId() || 'general'}`; try { const current = JSON.parse(localStorage.getItem(storageKey) || '[]'); const percentage = Math.round((earned / maximum) * 100); evidence.forEach(item => { const existingIndex = current.findIndex(c => questionTextMatches(c.qNorm || c.qRaw, item.qNorm)); const entry = { qNorm: item.qNorm, qRaw: item.qRaw, selectedAnswer: item.ansRaw, ansNorm: item.ansNorm, choices: item.choices || [], earned, maximum, percentage, isReviewPermitted, recordedAt: Date.now() }; if (existingIndex >= 0) { current[existingIndex] = entry; } else { current.push(entry); } }); localStorage.setItem(storageKey, JSON.stringify(current.slice(-200))); } catch (e) { logDebug(`Failed to save unreviewed attempt evidence: ${e.message}`); } } function showUnreviewableQuizRetryWarning(summary) { if (!checkIsQuizViewPage() || !summary) return; const retryControl = document.querySelector('a[href*="attempt.php"], input[name="startattempt"], button[name="startattempt"]'); const attemptTable = document.querySelector('.quizattemptsummary, .generaltable.quizattemptsummary, .quizsummarytable'); const anchor = retryControl || attemptTable; if (!anchor) return; let warning = document.getElementById('amaes-unreviewable-retry-warning'); if (!warning) { warning = document.createElement('div'); warning.id = 'amaes-unreviewable-retry-warning'; warning.setAttribute('role', 'note'); warning.style.cssText = 'margin: 12px 0; padding: 12px 14px; border-left: 4px solid #f59e0b; border-radius: 6px; background: rgba(245, 158, 11, 0.12); color: inherit; font-size: 12.5px; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;'; if (retryControl) { (retryControl.closest('form, .singlebutton, .quizattempt') || retryControl.parentElement).insertAdjacentElement('beforebegin', warning); } else { attemptTable.insertAdjacentElement('afterend', warning); } } const isLowScore = typeof summary.percentage === 'number' && summary.percentage < 80; const isCritical = typeof summary.percentage === 'number' && summary.percentage < 70; const courseInfo = (typeof detectCourseInfo === 'function') ? detectCourseInfo() : {}; const subCode = courseInfo.subjectCode || 'GENERAL'; const quizTitle = (typeof getQuizTitle === 'function' ? getQuizTitle() : '') || document.title; if (summary.isReviewPermitted === false) { const isHighestGrade = /grading method\s*:\s*highest grade/i.test(document.body.innerText || ''); warning.innerHTML = `
Review is not permitted for the previous attempt (${summary.percentage}% overall)
This score does not identify which individual answers were correct. Your first attempt is only the available baseline, not proof of which answers were right; a retry may score lower. Check the grading method and attempt limit before retrying.${isHighestGrade ? ' Moodle currently lists Highest grade for this quiz.' : ''}
${isLowScore ? `
Score is below passable grade (80%)
` : ''} `; } else if (isLowScore) { warning.style.borderLeftColor = isCritical ? '#ef4444' : '#f59e0b'; warning.style.background = isCritical ? 'rgba(239, 68, 68, 0.12)' : 'rgba(245, 158, 11, 0.12)'; warning.innerHTML = `
Attempt Score Alert: ${summary.percentage}% (Below 80% Passing Cutoff)
Your latest attempt scored below the 80% passing grade. If the quiz has a review link above, open the review so the toolkit can harvest confirmed wrong answers. If answers were missing or incorrect, please report them below.
`; } else { warning.remove(); return; } const reportBtn = warning.querySelector('#amaes-btn-warning-report-bug'); if (reportBtn) { reportBtn.onclick = () => { if (typeof showBugReportModal === 'function') { showBugReportModal({ subjectCode: subCode, description: `Quiz "${quizTitle}" scored ${summary.percentage}% (below 80% passing grade). Answers appear outdated, missing, or incorrect.` }); } }; } } function promoteAttemptEvidenceFromScore() { if (!checkIsQuizSummaryPage() && !checkIsQuizViewPage()) return; const initialCourseCode = (detectCourseInfo().subjectCode) || 'GENERAL'; showUnreviewableQuizRetryWarning(getQuizAttemptSummary(initialCourseCode)); let earned = null; let maximum = null; let isReviewPermitted = null; document.querySelectorAll('table, .quizattemptsummary, .generaltable').forEach(table => { if (earned !== null) return; const rows = Array.from(table.querySelectorAll('tr')); const headerRow = rows.find(row => Array.from(row.children).some(cell => /grade|marks/i.test(cell.innerText || ''))); const attemptRows = rows.filter(row => /finished|submitted|complete/i.test(row.innerText || '')); if (attemptRows.length === 0) return; const latest = attemptRows[attemptRows.length - 1]; // 1. Direct cell parsing (e.g. cell has "15.00 / 20.00" or "75.00 / 100.00") for (const cell of latest.children) { const text = (cell.innerText || '').trim(); const slashMatch = text.match(/([0-9]+(?:\.[0-9]+)?)\s*\/\s*([0-9]+(?:\.[0-9]+)?)/); if (slashMatch) { earned = Number(slashMatch[1]); maximum = Number(slashMatch[2]); break; } } // 2. Header-based parsing if no slash in cell if (earned === null && headerRow) { const headers = Array.from(headerRow.children); const marksIndex = headers.findIndex(cell => /marks/i.test(cell.innerText || '')); const gradeIndex = headers.findIndex(cell => /grade/i.test(cell.innerText || '')); const targetIndex = marksIndex >= 0 ? marksIndex : gradeIndex; if (targetIndex >= 0 && latest.children[targetIndex]) { const headerText = headers[targetIndex].innerText || ''; const headerMax = headerText.match(/\/\s*([0-9]+(?:\.[0-9]+)?)/); const cellVal = (latest.children[targetIndex].innerText || '').match(/([0-9]+(?:\.[0-9]+)?)/); if (cellVal && headerMax) { earned = Number(cellVal[1]); maximum = Number(headerMax[1]); } } } // 3. Review permission check const reviewIndex = headerRow ? Array.from(headerRow.children).findIndex(cell => /review/i.test(cell.innerText || '')) : -1; if (reviewIndex >= 0 && latest.children[reviewIndex]) { const revText = (latest.children[reviewIndex].innerText || '').toLowerCase(); if (revText.includes('not permitted') || revText.includes('not allowed')) { isReviewPermitted = false; } else if (latest.querySelector('a[href*="review.php"]')) { isReviewPermitted = true; } } else { const hasReviewLink = Boolean(latest.querySelector('a[href*="review.php"]')); if (hasReviewLink) isReviewPermitted = true; } }); // 4. Overall feedback fallback parsing if table wasn't matched if (earned === null || maximum === null) { const feedbackText = document.body.innerText || ''; const gradeMatch = feedbackText.match(/(?:overall\s+feedback|grade\s+for\s+this\s+quiz)[^0-9]*([0-9]+(?:\.[0-9]+)?)\s*\/\s*([0-9]+(?:\.[0-9]+)?)/i); if (gradeMatch) { earned = Number(gradeMatch[1]); maximum = Number(gradeMatch[2]); } } if (earned === null || maximum === null) return; if (!Number.isFinite(earned) || !Number.isFinite(maximum) || maximum <= 0) return; const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'GENERAL'; const percentage = Math.round((earned / maximum) * 100); // Always record latest quiz attempt summary even if prior attempt was done manually without toolkit saveQuizAttemptSummary(subCode, { earned, maximum, percentage, isReviewPermitted, recordedAt: Date.now() }); if (isReviewPermitted === false) reportCourseReviewRestriction(subCode); showUnreviewableQuizRetryWarning({ percentage, isReviewPermitted }); let evidence = []; try { evidence = JSON.parse(sessionStorage.getItem(getAttemptEvidenceKey()) || '[]'); } catch (_) {} if (!Array.isArray(evidence) || evidence.length === 0) return; function dispatchQuizLowScoreAnomaly(subCode, earned, maximum, percentage, evidence) { if (percentage >= 80) return; const isCritical = percentage < 70; const quizTitle = (typeof getQuizTitle === 'function' ? getQuizTitle() : '') || (document.title ? document.title.replace(/\s*\|.*$/, '').trim() : 'Quiz Attempt'); if (typeof detectAndReportQuestionAnomaly === 'function' && Array.isArray(evidence) && evidence.length > 0) { const sampleQue = evidence[0]; const samplePrompt = sampleQue ? (sampleQue.qRaw || sampleQue.qText || '') : `Quiz scored ${earned}/${maximum} (${percentage}%) in ${subCode}`; detectAndReportQuestionAnomaly({ anomalyType: isCritical ? 'LOW_QUIZ_SCORE_CRITICAL' : 'QUIZ_SCORE_BELOW_PASSING', subjectCode: subCode, activityTitle: quizTitle, questionRaw: samplePrompt, questionNorm: (typeof normalizeText === 'function' ? normalizeText(samplePrompt) : samplePrompt.toLowerCase()).slice(0, 100), domType: 'quiz_attempt_score', submittedAnswer: `Attempt scored ${earned}/${maximum} (${percentage}%) across ${evidence.length} questions`, markScored: earned, maxMark: maximum, attemptHistory: evidence.slice(0, 5).map(e => `${(e.qRaw || e.qText || '').slice(0, 40)}: ${e.ansRaw || 'N/A'}`), dbCandidate: null }); } const alertMsg = isCritical ? `Quiz score (${earned}/${maximum} - ${percentage}%) is critical (<70%). Anomaly telemetry dispatched.` : `Quiz score (${earned}/${maximum} - ${percentage}%) is below passable grade (80%).`; setLog(`[Score Anomaly] ${alertMsg}`, 'var(--accent-amber)'); if (typeof showToast === 'function') { showToast(`${alertMsg} Click to report question issues.`, 8000, () => { if (typeof showBugReportModal === 'function') { showBugReportModal({ subjectCode: subCode, description: `Quiz "${quizTitle}" scored ${earned}/${maximum} (${percentage}%). One or more questions appear to have mismatched, rotated, or incorrect answers.` }); } }); } } if (isReviewPermitted !== true) { if (earned < maximum) { saveUnreviewedAttemptEvidence(subCode, evidence, earned, maximum, isReviewPermitted); setLog(`Attempt score recorded (${earned}/${maximum} - ${percentage}%). Review permission is ${isReviewPermitted === false ? 'restricted' : 'unknown'}; overall scores cannot verify individual answers, so these answers were not promoted or shared.`, 'var(--accent-amber)'); dispatchQuizLowScoreAnomaly(subCode, earned, maximum, percentage, evidence); return; } } if (earned < maximum) { saveUnreviewedAttemptEvidence(subCode, evidence, earned, maximum, isReviewPermitted); recordAdaptiveProbeAttempt(subCode, evidence, earned, maximum, isReviewPermitted); setLog(`Attempt score recorded (${earned}/${maximum} - ${percentage}%). This overall score does not identify which individual answers were correct. Unconfirmed choices remain unverified.`, 'var(--accent-amber)'); dispatchQuizLowScoreAnomaly(subCode, earned, maximum, percentage, evidence); return; } const promoted = evidence.map(item => ({ ...item, verified: true, isAiSuggestion: false, source: 'moodle_100_percent', evidenceType: 'moodle_100_percent' })); mergeAnswersIntoCache(subCode, promoted, 'moodle_100_percent'); markProbeQuestionsSolved(subCode, promoted); queueCommunityContribution(subCode, promoted, { source: 'moodle_100_percent', evidenceType: 'moodle_100_percent' }); setLog(`100% score confirmed ${promoted.length} recorded answers and queued them for the verified database.`, 'var(--accent-green)'); showToast(`100% confirmed: ${promoted.length} answers saved to the study database.`, 4000); sessionStorage.removeItem(getAttemptEvidenceKey()); } // Schedule automatic advancement to the next target on a one-page quiz, // or to the next Moodle page after all questions on this page are answered. function scheduleAutoNextAfterAnswer(delayMs = 800, isManualAnswer = false) { const sourceQue = arguments[2] || null; const allowAiAutoNext = arguments[3] === true; if (!autoQuizMode) return; // While Auto-Quiz is running, manual answers are completed targets too. // Continue to the next unanswered question instead of leaving the user // on the question that was just answered. if (isManualAnswer && !autoNextQuiz) return; if (!isManualAnswer && !autoNextVerified && !allowAiAutoNext) return; if (!checkIsQuizAttemptPage()) return; const effectiveDelay = fastQuizMode ? Math.min(delayMs, 200) : delayMs; const nextOnPage = findNextUnansweredOnCurrentPage(sourceQue); if (nextOnPage) { clearTimeout(autoNextTimer); const secText = (effectiveDelay / 1000).toFixed(1) + 's'; setLog(`Question Answered: Moving to the next unanswered question in ${secText}...`, "var(--accent-blue)"); showToast("Answer recorded! Moving to next...", 1000); autoNextTimer = setTimeout(() => { if (!autoQuizMode) return; setActiveQuestion(nextOnPage, false); nextOnPage.scrollIntoView({ behavior: 'smooth', block: 'center' }); runAutoQuizSolver(); }, effectiveDelay); return; } if (!areAllPageQuestionsAnswered()) return; const nextBtn = findQuizNextButton(); if (!nextBtn) return; const btnText = (nextBtn.value || nextBtn.innerText || '').toLowerCase(); const isFinish = btnText.includes('finish') || btnText.includes('submit'); clearTimeout(autoNextTimer); if (isFinish) { const finishDelay = fastQuizMode ? 200 : 1000; setLog(`All Questions Answered! Advancing to summary in ${(finishDelay / 1000).toFixed(1)}s...`, "var(--accent-green)"); showToast("All questions answered! Advancing to summary...", 1200); autoNextTimer = setTimeout(() => { if (!autoQuizMode) return; if (isManualAnswer && !autoNextQuiz) return; clickQuizNextButton(nextBtn, true); }, finishDelay); } else { const pageDelay = effectiveDelay; setLog(`Question Answered: Advancing to next page in ${(pageDelay / 1000).toFixed(1)}s...`, "var(--accent-blue)"); showToast("Answer selected! Advancing to next page...", 1000); autoNextTimer = setTimeout(() => { if (!autoQuizMode) return; if (isManualAnswer && !autoNextQuiz) return; clickQuizNextButton(nextBtn, true); }, pageDelay); } } // Bind event listeners to question inputs to trigger auto-next immediately when choices are selected function setupQuizAnswerListeners() { if (!checkIsQuizAttemptPage()) return; const inputs = document.querySelectorAll( '.que .answer input[type="radio"], .que .answer input[type="checkbox"], .que select' ); inputs.forEach(inp => { if (inp.dataset.amaesAutoNextBound) return; inp.dataset.amaesAutoNextBound = 'true'; inp.addEventListener('change', () => { if (autoQuizMode) { scheduleAutoNextAfterAnswer(inp.type === 'checkbox' ? 1200 : 800, true, inp.closest('.que')); } }); }); const textInputs = document.querySelectorAll( '.que input[type="text"].form-control, .que input.form-control, .que textarea' ); textInputs.forEach(inp => { if (inp.dataset.amaesAutoNextBound) return; inp.dataset.amaesAutoNextBound = 'true'; inp.addEventListener('blur', () => { if (autoQuizMode && inp.value && inp.value.trim().length > 0) { scheduleAutoNextAfterAnswer(1000, true, inp.closest('.que')); } }); }); } // Fail-safe click & smart navigation executor (skips already-answered questions) function clickQuizNextButton(btn, forceAllow = false) { try { syncCurrentPageSelectionsToEvidence(); } catch (_) {} if (!autoQuizMode && !forceAllow && !autoNextQuiz) { logDebug("Blocked clickQuizNextButton: Auto-Quiz is PAUSED."); return false; } // SMART NAVIGATION: If smart skip is enabled, jump directly to next unanswered question! if (smartSkipQuiz) { const navState = getQuizNavQuestionStates(); if (navState && navState.nextUnanswered && navState.nextUnanswered.btn) { const target = navState.nextUnanswered; logDebug(`Smart Jump: Bypassing answered questions, jumping directly to Question #${target.qNum}`); setLog(`[Skip] Smart Skip: Jumping to unanswered Question #${target.qNum} (${navState.totalUnanswered} remaining)...`, "var(--accent-blue)"); showToast(`Smart Jump: Question #${target.qNum}`, 1800); target.btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); try { target.btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); target.btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); target.btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch (e) {} target.btn.click(); return true; } else if (navState && navState.allAnswered) { // All questions on the entire quiz are answered! Proceed to finish attempt const finishBtn = document.querySelector( '.submitbtns input[value*="Finish attempt"], .submitbtns button[value*="Finish attempt"], input[value*="Finish attempt"], a.endtestlink, #mod_quiz_navblock a[href*="summary.php"]' ); if (finishBtn) { logDebug("Smart Navigation: All questions answered! Proceeding to finish attempt."); setLog("All Questions Answered! Proceeding to summary screen...", "var(--accent-green)"); showToast("All questions answered! Finishing attempt...", 2500); finishBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); finishBtn.click(); return true; } } } if (!btn) { logDebug("Cannot navigate: Next button not found on page."); return false; } logDebug(`Executing Quiz Next Navigation: ${btn.value || btn.innerText || btn.name}`); btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); btn.focus(); // Dispatch realistic user click event try { btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch (e) { console.warn("MouseEvent dispatch error:", e); } // Native click call btn.click(); return true; } let isSolverRunning = false; let studyGuideFallbackRunPending = false; const quizStudyGuideLoads = new Map(); async function runAutoQuizSolver(forceRun = false) { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') { autoQuizMode = false; return; } if (typeof isFeatureDisabledByAdmin === 'function' && isFeatureDisabledByAdmin('autoQuiz')) { autoQuizMode = false; setLog("[Admin Notice] Auto-Quiz is temporarily disabled by administrator.", "var(--accent-amber)"); showToast("Auto-Quiz is temporarily disabled by administrator.", 4000); return; } if (!checkIsQuizAttemptPage()) return; // STRICT PAUSE CHECK: If Auto-Quiz is not explicitly active or force-run, halt completely const isAutoActive = forceRun || autoQuizMode; if (!isAutoActive || !autoQuizMode) { logDebug("Auto Quiz Solver is PAUSED. Suppressing auto-clicks and auto-navigation."); clearTimeout(autoNextTimer); autoNextTimer = null; clearTimeout(pageLoadSolverTimer); pageLoadSolverTimer = null; isSolverRunning = false; return; } if (isSolverRunning) return; isSolverRunning = true; try { const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; let cached = getCachedAnswers(subCode); const externalStudyGuideAnswers = typeof readJennysonlinePersistentCache === 'function' ? (readJennysonlinePersistentCache(subCode) || []) : []; const queContainers = document.querySelectorAll('.que'); // Restore per-question controls when the solver is no longer // blocked; they are hidden below when the full waiting HUD exists. document.querySelectorAll('.amaes-que-top-toolbar').forEach(toolbar => { toolbar.style.display = ''; }); if (queContainers.length === 0) { isSolverRunning = false; return; } // 1. Highlight and Auto-Select verified answers from database (ONLY click if autoQuizMode is true) let res = { matched: 0, total: queContainers.length }; // AUTO-FETCH FROM CLOUD DATABASE / AMAUOED FALLBACK: If local cache is empty, check cloud DB & AMAUOED! if (!cached || cached.length === 0) { logDebug(`Local cache empty for ${subCode}. Checking community cloud database & AMAUOED...`); setLog(`Checking database & AMAUOED for ${subCode}...`, "var(--accent-blue)"); try { if (typeof autoFetchCloudAnswersIfMissing === 'function') { await autoFetchCloudAnswersIfMissing(subCode); } else { await syncAnswersFromCloud(subCode); } const freshCached = getCachedAnswers(subCode); if (freshCached && freshCached.length > 0) { cached = freshCached; setLog(`Auto-loaded ${freshCached.length} verified answers for ${subCode}!`, "var(--accent-green)"); showToast(`Loaded ${freshCached.length} answers!`); syncAutoQuizUI(); } } catch (e) { logDebug(`Auto-fetch fallback note: ${e.message}`); } } const freshExternalStudyGuideAnswers = typeof readJennysonlinePersistentCache === 'function' ? (readJennysonlinePersistentCache(subCode) || []) : externalStudyGuideAnswers; const availableAnswers = (cached || []).concat(freshExternalStudyGuideAnswers); if (availableAnswers.length > 0) { res = highlightQuizAnswers(availableAnswers, Boolean(autoQuizMode || autoPickQuiz)); } else { setLog(`No Answers in DB: Open amauoed or click Cloud Sync for ${subCode}!`, "var(--accent-amber)"); } if (typeof loadJennysonlineAnswersForCourse === 'function' && !quizStudyGuideLoads.has(subCode)) { const attemptUrl = window.location.href; const guideLoad = loadJennysonlineAnswersForCourse(subCode) .then(answers => { if (!answers.length || window.location.href !== attemptUrl || !checkIsQuizAttemptPage()) return; const freshCache = getCachedAnswers(subCode) || []; const autoPickFallback = autoQuizMode && autoPickStudyGuideFallback; highlightQuizAnswers(freshCache.concat(answers), autoPickFallback); if (autoPickFallback) { studyGuideFallbackRunPending = true; if (!isSolverRunning) { studyGuideFallbackRunPending = false; runAutoQuizSolver(); } } }) .catch(error => { logDebug(`Jenny's Online source note for ${subCode}: ${error.message}`); }) .finally(() => { quizStudyGuideLoads.delete(subCode); }); quizStudyGuideLoads.set(subCode, guideLoad); } // 2. Identify questions verified by the database vs unverified/unknown questions const unverifiedQuestions = []; const unreviewedOrProbeQuestions = []; queContainers.forEach(que => { if (isQuestionAnswered(que)) return; const hasVerifiedBadge = que.querySelector('.amaes-verified-badge'); const hasAdaptiveBadge = que.querySelector('.amaes-adaptive-probe-badge'); const hasAiBadge = que.querySelector('.amaes-ai-suggested-badge'); const hasUnverifiedBadge = que.querySelector('.amaes-unverified-badge'); const hasDragHint = que.querySelector('.amaes-drag-hint'); const hasShortAnsHint = que.querySelector('.amaes-shortans-hint'); const hasSelectHint = que.querySelector('.amaes-select-hint'); // Track unverified, AI-suggested, or prior unreviewed attempts const isAutoPickedStudyGuideFallback = autoQuizMode && autoPickStudyGuideFallback && que.dataset.amaesStudyGuideFallbackPicked === 'true'; if (!isAutoPickedStudyGuideFallback && (hasAdaptiveBadge || hasAiBadge || hasUnverifiedBadge)) { unreviewedOrProbeQuestions.push(que); } const dropZones = que.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable'); const selectInputs = que.querySelectorAll('select'); const textInputs = que.querySelectorAll('input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); let isFullyVerified = false; if (hasVerifiedBadge && !hasAdaptiveBadge && !hasAiBadge && !hasUnverifiedBadge) { isFullyVerified = true; } else if (dropZones.length > 0) { isFullyVerified = que.querySelectorAll('.amaes-drag-hint').length >= dropZones.length; } else if (selectInputs.length > 0) { isFullyVerified = que.querySelectorAll('.amaes-select-hint').length >= selectInputs.length; } else if (textInputs.length > 0) { isFullyVerified = que.querySelectorAll('.amaes-shortans-hint').length >= textInputs.length; } else if (hasShortAnsHint || hasSelectHint || hasDragHint) { isFullyVerified = true; } if (!isFullyVerified) { unverifiedQuestions.push(que); } }); const nextBtn = findQuizNextButton(); // Safe review pause: If any question on this page was an unreviewed probe or unverified suggestion, // do NOT silently breeze through unless user explicitly enabled auto-advance on manual/unverified clicks! // When autoQuizMode is running, let Case B proceed to AI resolution or auto-copy without halting automation. if (unreviewedOrProbeQuestions.length > 0 && !autoNextQuiz && !fastQuizMode && !autoQuizMode) { const firstProbeQue = unreviewedOrProbeQuestions[0]; setActiveQuestion(firstProbeQue, false); firstProbeQue.scrollIntoView({ behavior: 'smooth', block: 'center' }); clearTimeout(autoNextTimer); autoNextTimer = null; isWaitingForUserAnswer = true; syncAutoQuizUI(true); const qData = extractQuestionData(firstProbeQue); const isFlippedProbe = Boolean(firstProbeQue.querySelector('.amaes-adaptive-probe-choice')); setLog(`[Review Pause] Question #${qData ? qData.qNum : ''}: ${isFlippedProbe ? 'Adaptive probe flipped to alternative choice' : 'Unverified candidate selected'}. Paused for review—press N or click Next page when ready.`, "var(--accent-purple)"); isSolverRunning = false; return; } // Co-Pilot: Auto-Pick & Next IF Known, WAIT if Unknown // Case A: 100% of questions on this page were verified & answered by database! if (unverifiedQuestions.length === 0 && res.total > 0) { const allAnswered = areAllPageQuestionsAnswered(); if (allAnswered) { setLog(`All ${res.total} question(s) verified & picked!`, "var(--accent-green)"); // Answered by Auto-Quiz: automatically advance to next page! if (nextBtn && autoNextVerified) { const btnText = (nextBtn.value || nextBtn.innerText || '').toLowerCase(); const isFinish = btnText.includes('finish') || btnText.includes('submit'); clearTimeout(autoNextTimer); const navDelay = fastQuizMode ? 200 : (isFinish ? 1200 : 1000); if (isFinish) { if (autoSubmitQuiz || fastQuizMode || autoQuizMode) { setLog(`All Questions Answered! Advancing to summary in ${(navDelay / 1000).toFixed(1)}s...`, "var(--accent-green)"); showToast("Finishing attempt...", navDelay + 1000); autoNextTimer = setTimeout(() => { if (!autoQuizMode) return; clickQuizNextButton(nextBtn); }, navDelay); } else { setLog("Last Question Answered! Paused for review before final submit.", "var(--accent-green)"); showToast("Last question answered! Review before submitting.", 4000); } isSolverRunning = false; return; } setLog(`[Auto-Next] Auto-Next: Advancing to next question in ${(navDelay / 1000).toFixed(1)}s...`, "var(--accent-blue)"); autoNextTimer = setTimeout(() => { if (!autoQuizMode) return; clickQuizNextButton(nextBtn); }, navDelay); } } else { // Verified choices highlighted, but not all picked (e.g. auto-pick disabled) setLog(`Verified answers found! Select your choice${autoNextQuiz ? ' (advances automatically)' : ' and click Next page'}.`, "var(--accent-blue)"); } isSolverRunning = false; return; } // Case B: UNKNOWN QUESTION DETECTED (Auto-solver cannot answer it!) // Safe wait state on unknown question: solver pauses on THIS question for user input, // while keeping autoQuizMode active so subsequent known questions are answered directly! clearTimeout(autoNextTimer); autoNextTimer = null; isWaitingForUserAnswer = true; syncAutoQuizUI(true); const firstBlockedQue = unverifiedQuestions[0]; const qData = extractQuestionData(firstBlockedQue); const willIncludeContext = shouldInjectAiContext(qData ? qData.qNum : null); const aiPromptText = formatQuestionForAI(firstBlockedQue, aiPromptHint); // Feature 1: Instant Auto-Copy — copy prompt immediately on unknown question detection, // before AI is invoked or any condition is checked (aiAutoCopyOnFail guard respected). if (aiAutoCopyOnFail && qData && qData.questionType !== 'unknown') { copyQuestionWithOptionalImage(firstBlockedQue, aiPromptText).catch(() => {}); } setActiveQuestion(firstBlockedQue, false); firstBlockedQue.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstBlockedQue.style.outline = '2.5px solid #f59e0b'; firstBlockedQue.style.borderRadius = '8px'; // The waiting HUD already has the only needed stop control. firstBlockedQue.querySelectorAll('.amaes-que-top-toolbar').forEach(toolbar => { toolbar.style.display = 'none'; }); // If question type is unknown / unrecognized: // Do not alter or interfere with the question card at all ("dont do anythibg but notify"). // It is already reported and pushed to database by recordUnknownQuestionType. if (qData && qData.questionType === 'unknown') { recordUnknownQuestionType(firstBlockedQue, qData); firstBlockedQue.style.outline = ''; isSolverRunning = false; return; } // Feature 4: Moodle Server Error Guard — detect empty/failed question load if (detectMoodleServerError(firstBlockedQue)) { firstBlockedQue.querySelectorAll('.amaes-blockage-hud').forEach(el => el.remove()); const errHud = document.createElement('div'); errHud.className = 'amaes-blockage-hud'; errHud.style.cssText = ` margin-bottom: 12px; padding: 8px 12px; background: #fff7ed; border: 1.5px solid #f97316; border-radius: 8px; display: flex; align-items: center; gap: 8px; font-size: 11px; color: #9a3412; font-family: -apple-system, sans-serif; `; errHud.innerHTML = ` Warning
Moodle Server Glitch Detected
Question content failed to load from Moodle database. Try refreshing the page.
`; const formEl = firstBlockedQue.querySelector('.formulation, .content') || firstBlockedQue; formEl.insertBefore(errHud, formEl.firstChild); setLog('[Server Error] Moodle database glitch detected. Question content is empty — cannot solve. Try refreshing.', 'var(--accent-amber)'); showToast('Moodle server glitch: Question failed to load.', 4000); isSolverRunning = false; return; } const navState = getQuizNavQuestionStates(); // STRICT ANSWER HIERARCHY GUARD: // 1. If question is already answered (by student or prior pick), DO NOT run AI! if (isQuestionAnswered(firstBlockedQue)) { firstBlockedQue.querySelectorAll('.amaes-blockage-hud, .amaes-ai-fallback-bar').forEach(el => el.remove()); isSolverRunning = false; return; } // 2. If question already has an unconfirmed community or study-guide candidate, // and study guide fallback is enabled, pick it now and skip AI inference! const unconfirmedChoice = firstBlockedQue.querySelector('.amaes-highlighted-choice input[type="radio"], .amaes-highlighted-choice input[type="checkbox"]'); if (unconfirmedChoice && (autoPickStudyGuideFallback || autoPickQuiz)) { if (!unconfirmedChoice.checked) { unconfirmedChoice.checked = true; unconfirmedChoice.click(); unconfirmedChoice.dispatchEvent(new Event('change', { bubbles: true })); } firstBlockedQue.querySelectorAll('.amaes-blockage-hud, .amaes-ai-fallback-bar').forEach(el => el.remove()); setLog(`[Answer Hierarchy] Selected community/study-guide candidate for Question #${qData ? qData.qNum : ''}; skipped AI.`, "var(--accent-blue)"); isSolverRunning = false; return; } // GOOGLE GEMINI AI ASSISTANT: Handle uncertain Multiple Choice, True/False, and row-mapped Matching questions. // Other complex types (drag & drop, gapselect, text inputs) fall back to manual copy. const isEligibleChoice = isEligibleForAiSolver(firstBlockedQue, qData); const existingAiChoice = firstBlockedQue.querySelector('.amaes-ai-suggested-choice'); if (existingAiChoice && !isChoiceRowEliminated(existingAiChoice)) { // Clean up any stale blockage HUD if previously injected firstBlockedQue.querySelectorAll('.amaes-blockage-hud').forEach(el => el.remove()); firstBlockedQue.querySelectorAll('.amaes-que-top-toolbar').forEach(toolbar => { toolbar.style.display = 'flex'; }); firstBlockedQue.style.outline = '2px solid rgba(139, 92, 246, 0.7)'; firstBlockedQue.style.borderRadius = '8px'; setQuestionAiTag(firstBlockedQue, true); // Already solved by AI and highlighted! Keep paused for review without re-querying API. setLog(`[AI Suggestion] Question #${qData ? qData.qNum : ''} has an AI suggestion. (Prompt copied) Paused for review—press N or click Next page when ready.`, "var(--accent-purple)"); isSolverRunning = false; return; } const activeGeminiKey = getAvailableGeminiKey(); if (activeGeminiKey && aiQuizEnabled && isEligibleChoice) { // Check if client-side or cooldown rate limit is currently active const rateLimitStatus = getAiRateLimitStatus(); if (rateLimitStatus.isLimited) { firstBlockedQue.querySelectorAll('.amaes-blockage-hud').forEach(el => el.remove()); copyQuestionWithOptionalImage(firstBlockedQue, aiPromptText).catch(() => {}); const rlReason = `Google is temporarily limiting AI requests. Try again in ${rateLimitStatus.remainingSec} seconds.`; setLog(`[AI Rate Limit] Question #${qData ? qData.qNum : ''}: ${rlReason} (Prompt copied)`, "var(--accent-amber)"); showToast(`AI rate limit: available in ${rateLimitStatus.remainingSec}s`, 3500); if (!firstBlockedQue.dataset.amaesInterventionAlertPlayed) { firstBlockedQue.dataset.amaesInterventionAlertPlayed = 'true'; playToolkitSound('manual_intervention'); } showAiFallbackBar(firstBlockedQue, qData, aiPromptText, async () => { isSolverRunning = false; autoSolveQuizQuestion(); }, { reason: rlReason, isRateLimit: true, waitSeconds: rateLimitStatus.remainingSec }); firstBlockedQue.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstBlockedQue.style.outline = '2px solid #f97316'; firstBlockedQue.style.borderRadius = '8px'; isSolverRunning = false; return; } // Remove any manual blockage HUD so it does not conflict with active AI solving firstBlockedQue.querySelectorAll('.amaes-blockage-hud').forEach(el => el.remove()); // Auto-copy question prompt in background as seamless backup for the student copyQuestionWithOptionalImage(firstBlockedQue, aiPromptText).catch(() => {}); const courseInfo = detectCourseInfo(); const courseCode = courseInfo.subjectCode || ''; const promptText = buildGeminiCompactPrompt(qData, courseCode, firstBlockedQue); await handleGeminiQuestionInference({ que: firstBlockedQue, qData: qData, promptText: promptText, onSuccess: async (matched) => { if (matched && matched.choiceText && !matched.matchingRows) { recordAttemptAnswerEvidence(firstBlockedQue, matched.choiceText, 'ai_inference'); } // Ensure blockage HUD is removed upon successful AI resolution firstBlockedQue.querySelectorAll('.amaes-blockage-hud, .amaes-unanswered-hint').forEach(el => el.remove()); firstBlockedQue.querySelectorAll('.amaes-que-top-toolbar').forEach(toolbar => { toolbar.style.display = 'flex'; }); firstBlockedQue.style.outline = '2px solid rgba(139, 92, 246, 0.7)'; firstBlockedQue.style.borderRadius = '8px'; setQuestionAiTag(firstBlockedQue, true); if (matched && Array.isArray(matched.matchingRows)) { showToast(`Gemini suggested ${matched.matchingRows.length} matching answer${matched.matchingRows.length === 1 ? '' : 's'} (unverified).`, 3500); setLog(`[AI Suggestion] Gemini suggested ${matched.matchingRows.length} matching row answers (unverified). Review them before continuing.`, "var(--accent-purple)"); } else if (aiAutoSelect && matched && matched.input) { const anyChecked = Boolean(firstBlockedQue.querySelector('.answer input[type="radio"]:checked, .answer input[type="checkbox"]:checked')); if (!anyChecked) { safelySelectInput(matched.input); } showToast(`Gemini selected a choice for #${qData ? qData.qNum : ''}. (Paused for review)`, 3000); setLog(`[AI Suggestion] Gemini selected ${escapeHtml(matched.choiceText)} for #${qData ? qData.qNum : ''}. (Prompt copied) Paused for review—press N or click Next page when ready.`, "var(--accent-purple)"); } else { showToast(`Gemini suggested an answer for #${qData ? qData.qNum : ''} (Paused for review)`, 3000); setLog(`[AI Suggestion] Gemini suggested ${escapeHtml(matched ? matched.choiceText : '')} for #${qData ? qData.qNum : ''}. (Prompt copied) Paused for review—click to select and proceed.`, "var(--accent-purple)"); } // Feature 2: Auto-Advance after AI answer if setting is enabled if (aiAutoNextOnAiAnswer || autoNextVerified) { scheduleAutoNextAfterAnswer(800, false, firstBlockedQue, aiAutoNextOnAiAnswer); } if (typeof updateQuestionAiDrawerState === 'function') { updateQuestionAiDrawerState(firstBlockedQue, false); } } }); // Built-in acceleration on multi-question pages: concurrently solve other visible unknown questions if (fastQuizMode && queContainers.length > 1 && unverifiedQuestions.length > 1) { const otherQue = unverifiedQuestions.slice(1); otherQue.forEach(oQue => { if (oQue.querySelector('.amaes-ai-suggested-choice, .amaes-ai-text-badge') || isQuestionAnswered(oQue)) return; const oQData = extractQuestionData(oQue); if (oQData && isEligibleForAiSolver(oQue, oQData)) { const oPrompt = buildGeminiCompactPrompt(oQData, courseCode, oQue); handleGeminiQuestionInference({ que: oQue, qData: oQData, promptText: oPrompt, onSuccess: async (oMatched) => { if (oMatched && oMatched.choiceText && !oMatched.matchingRows) { recordAttemptAnswerEvidence(oQue, oMatched.choiceText, 'ai_inference'); } oQue.querySelectorAll('.amaes-blockage-hud, .amaes-unanswered-hint').forEach(el => el.remove()); oQue.style.outline = '2px solid rgba(139, 92, 246, 0.7)'; oQue.style.borderRadius = '8px'; setQuestionAiTag(oQue, true); if (aiAutoSelect && oMatched && oMatched.input) { safelySelectInput(oMatched.input); } } }).catch(() => {}); } }); } // If AI successfully resolved and highlighted a choice, finish here without showing redundant blockage HUD! if (firstBlockedQue.querySelector('.amaes-ai-suggested-choice, .amaes-ai-matching-suggestion')) { isSolverRunning = false; return; } } else { // Copy question for AI helper copyQuestionWithOptionalImage(firstBlockedQue, aiPromptText).then((res) => { showToast(res && res.withImage ? `Visual snippet & Question #${qData ? qData.qNum : ''} copied to clipboard!` : `Question #${qData ? qData.qNum : ''} copied to clipboard — ready to paste!`, 3000); }).catch(() => {}); // Built-in acceleration: even if first question requires manual intervention, background-solve remaining eligible questions if (fastQuizMode && queContainers.length > 1 && unverifiedQuestions.length > 1) { const activeKey = getAvailableGeminiKey(); if (activeKey && aiQuizEnabled) { const courseInfo = detectCourseInfo(); const courseCode = courseInfo.subjectCode || ''; unverifiedQuestions.slice(1).forEach(oQue => { if (oQue.querySelector('.amaes-ai-suggested-choice, .amaes-ai-text-badge') || isQuestionAnswered(oQue)) return; const oQData = extractQuestionData(oQue); if (oQData && isEligibleForAiSolver(oQue, oQData)) { const oPrompt = buildGeminiCompactPrompt(oQData, courseCode, oQue); handleGeminiQuestionInference({ que: oQue, qData: oQData, promptText: oPrompt, onSuccess: async (oMatched) => { if (oMatched && oMatched.choiceText && !oMatched.matchingRows) { recordAttemptAnswerEvidence(oQue, oMatched.choiceText, 'ai_inference'); } oQue.querySelectorAll('.amaes-blockage-hud, .amaes-unanswered-hint').forEach(el => el.remove()); oQue.style.outline = '2px solid rgba(139, 92, 246, 0.7)'; oQue.style.borderRadius = '8px'; setQuestionAiTag(oQue, true); if (aiAutoSelect && oMatched && oMatched.input) { safelySelectInput(oMatched.input); } else if (aiAutoSelect && oMatched && Array.isArray(oMatched.matchingRows)) { oMatched.matchingRows.forEach(item => { if (item.select && item.option) { item.select.value = item.option.value; item.select.dispatchEvent(new Event('input', { bubbles: true })); item.select.dispatchEvent(new Event('change', { bubbles: true })); item.select.dispatchEvent(new Event('blur', { bubbles: true })); item.select.style.outline = '2px solid #a855f7'; item.select.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; } }); } } }).catch(() => {}); } }); } } } // If reaching here: either question is ineligible for AI (e.g. text/drag), AI is not enabled, or AI failed // IMPORTANT: If AI was triggered and is still processing (thinking indicator visible), do NOT show // the "WAITING FOR ANSWER" HUD — it would conflict with the AI thinking indicator. const aiIsHandling = firstBlockedQue.querySelector('.amaes-ai-thinking-indicator'); const aiAlreadySolved = firstBlockedQue.querySelector('.amaes-ai-suggested-choice, .amaes-ai-text-badge, .amaes-ai-matching-suggestion'); if (aiIsHandling || aiAlreadySolved) { isSolverRunning = false; return; } firstBlockedQue.dataset.amaesAiFailed = 'true'; if (typeof updateQuestionAiDrawerState === 'function') { updateQuestionAiDrawerState(firstBlockedQue, true); } setLog( `Question #${qData ? qData.qNum : ''} Auto-Copied: Prompt copied to clipboard. ` + `Choose an answer and click Next page, or use Ask AI to get an answer.`, "var(--accent-amber)" ); if (!firstBlockedQue.querySelector('.amaes-blockage-hud')) { // The solver HUD is the full unknown-answer notice. Remove // compact matcher hints, previous focus banners, and chips so no warning is shown twice. firstBlockedQue.querySelectorAll('.amaes-unanswered-hint').forEach(hint => hint.remove()); firstBlockedQue.querySelectorAll('.amaes-new-question-focus-banner').forEach(b => b.remove()); firstBlockedQue.querySelectorAll('.amaes-unsure-help-chip').forEach(c => c.remove()); firstBlockedQue.querySelectorAll('.amaes-ai-fallback-bar').forEach(bar => bar.remove()); setQuestionAiTag(firstBlockedQue, false); if (!firstBlockedQue.dataset.amaesInterventionAlertPlayed) { firstBlockedQue.dataset.amaesInterventionAlertPlayed = 'true'; playToolkitSound('manual_intervention'); } const hud = document.createElement('div'); hud.className = 'amaes-blockage-hud amaes-new-question-focus-banner'; hud.style.cssText = ` margin-bottom: 12px; padding: 10px 14px; background: #fffbeb; border: 1.5px solid #f59e0b; border-radius: 8px; font-size: 11.5px; color: #78350f; box-shadow: 0 4px 12px rgba(245, 158, 11, 0.1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.4; box-sizing: border-box; transition: all 0.2s ease; `; const isMatchBlocked = Boolean(firstBlockedQue && ( (typeof identifyQuestionType === 'function' && identifyQuestionType(firstBlockedQue) === 'match') || firstBlockedQue.classList.contains('match') || firstBlockedQue.classList.contains('qtype_match') || firstBlockedQue.querySelectorAll('select').length > 1 )); const retryCount = Number(firstBlockedQue.dataset.amaesAiRetryCount) || 0; hud.innerHTML = `
WAITING Question #${qData ? qData.qNum : ''}: No saved answer yet
Auto-quiz paused
${isMatchBlocked ? 'No saved answers for these matching items yet. Select the matching choices below, or consult Web AI tools above.' : 'Question copied to clipboard! Pick an answer below or click Next Question, or use the AI tools at the side or bottom.' }
`; const formulation = firstBlockedQue.querySelector('.formulation, .content') || firstBlockedQue; formulation.insertBefore(hud, formulation.firstChild); const blockageStop = hud.querySelector('#btn-blockage-stop'); if (blockageStop) { blockageStop.onclick = (e) => { e.preventDefault(); e.stopPropagation(); toggleAutoQuizMode(); }; } const blockageSkip = hud.querySelector('.amaes-btn-skip-q'); if (blockageSkip) { blockageSkip.onclick = (e) => { e.preventDefault(); if (typeof triggerNextQuizPage === 'function') { triggerNextQuizPage(); } else { const nextBtn = document.querySelector('.submitbtns input[value*="Next"], input[value*="Next page"], button[value*="Next"]'); if (nextBtn) nextBtn.click(); } }; } updateNextButtonIndicator(); } // Listen for user selecting or typing a choice: show visual confirmation, DO NOT auto-next by default const inputElements = firstBlockedQue.querySelectorAll('input[type="radio"], input[type="checkbox"], input[type="text"], select, .draghome, .drop, input.placeinput'); const onUserPickedChoice = () => { if (!isQuestionAnswered(firstBlockedQue)) { return; } const selectedInput = firstBlockedQue.querySelector('.answer input[type="radio"]:checked, .answer input[type="checkbox"]:checked'); const selectedLabel = selectedInput && (selectedInput.closest('label') || selectedInput.parentElement); const selectedText = selectedLabel ? cleanDOMToAI(selectedLabel).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim() : ''; const textInput = firstBlockedQue.querySelector('input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); const selectInput = firstBlockedQue.querySelector('select'); const selectedOption = selectInput && selectInput.options[selectInput.selectedIndex]; recordAttemptAnswerEvidence( firstBlockedQue, selectedText || (textInput && textInput.value) || (selectedOption && selectedOption.text) || '', 'manual_selection' ); firstBlockedQue.style.outline = '2px solid #10b981'; const hud = firstBlockedQue.querySelector('.amaes-blockage-hud'); const allAnswered = areAllPageQuestionsAnswered(); const isMultiQuestionPage = queContainers && queContainers.length > 1; if (hud) { hud.style.borderColor = '#10b981'; hud.style.background = '#ecfdf5'; hud.style.color = '#065f46'; let hintMsg = ''; if (!allAnswered && isMultiQuestionPage) { hintMsg = 'Answer recorded! Scroll down to continue answering remaining questions.'; } else if (autoNextQuiz && autoQuizMode) { hintMsg = 'Advancing automatically...'; } else if (allAnswered && isMultiQuestionPage) { hintMsg = 'All questions answered! Review choices and click Finish attempt when ready.'; } else { hintMsg = 'Review your answer, then click Next page below to proceed.'; } hud.innerHTML = `
READY
Answer recorded!
${hintMsg}
`; } if (!allAnswered && isMultiQuestionPage && autoQuizMode && autoNextQuiz) { scheduleAutoNextAfterAnswer(800, true, firstBlockedQue); } else if (!allAnswered && isMultiQuestionPage) { showToast("Answer recorded! Continue with next questions below.", 2200); setLog(`Answer recorded for Question #${qData ? qData.qNum : ''}! Continue answering remaining questions below.`, "var(--accent-green)"); } else if (autoNextQuiz && autoQuizMode) { scheduleAutoNextAfterAnswer(800, true); } else if (allAnswered && isMultiQuestionPage) { showToast("All questions answered! Review before finishing.", 3000); setLog("All questions on page answered! Review before clicking Finish attempt.", "var(--accent-green)"); } else { showToast("Answer recorded! Press N or click Next page when ready.", 2500); setLog("Answer entered! Press N or click Next page below to proceed (Auto-Quiz remains active).", "var(--accent-green)"); } }; inputElements.forEach(inp => { inp.addEventListener('change', onUserPickedChoice); inp.addEventListener('click', onUserPickedChoice); if (inp.type === 'text') { inp.addEventListener('blur', onUserPickedChoice); } }); isSolverRunning = false; return; } catch (err) { logDebug(`Auto-Solver Error: ${err.message}`); console.error("Auto-Solver Exception:", err); } finally { isSolverRunning = false; if (studyGuideFallbackRunPending && autoQuizMode && autoPickStudyGuideFallback && checkIsQuizAttemptPage()) { studyGuideFallbackRunPending = false; setTimeout(() => runAutoQuizSolver(), 0); } else { studyGuideFallbackRunPending = false; } } } function triggerClick(el) { if (!el) return; try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (_) {} try { el.focus(); } catch (_) {} try { el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); } catch (_) {} try { el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); } catch (_) {} try { el.click(); } catch (_) {} } function safelySelectInput(input) { if (!input) return false; try { // If already checked, preserve checked state and avoid double-toggling if (input.checked) return true; // Trigger native activation behavior try { input.click(); } catch (_) {} // Failsafe: Ensure checked property is strictly true if (!input.checked) { input.checked = true; } // Dispatch standard events so Moodle recognizes the selection input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return true; } catch (err) { logDebug(`safelySelectInput error: ${err.message}`); return false; } } function setInputValueWithEvents(input, val) { if (!input) return; try { const proto = Object.getPrototypeOf(input); const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; if (nativeSetter) { nativeSetter.call(input, val); } else { input.value = val; } } catch (_) { input.value = val; } input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); } function isElementVisible(el) { if (!el) return false; if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') return false; if (el.classList && (el.classList.contains('hidden') || el.classList.contains('yui3-panel-hidden'))) return false; try { const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; } catch (_) {} const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; return true; } function findQuizSummarySubmitButton() { const candidates = document.querySelectorAll( '.btn-finishattempt button, .btn-finishattempt input[type="submit"], .btn-finishattempt input[type="button"], ' + 'button.btn-finishattempt, input.btn-finishattempt, ' + 'form[action*="processattempt.php"] button, form[action*="processattempt.php"] input[type="submit"], ' + '.submitbtns button, .submitbtns input[type="submit"], ' + '.quizsummarybtns button, .quizsummarybtns input[type="submit"], ' + '.btn-finishattempt, input[value*="Submit all and finish"], ' + 'button[type="submit"][name="finishattempt"], button[name="submitallandfinish"], ' + 'input[name="submitallandfinish"], button[type="submit"], button' ); for (const candidate of candidates) { if (!candidate || candidate.disabled) continue; // Resolve to the real button/input element if candidate is a container (div/form) const btn = (candidate.tagName === 'BUTTON' || (candidate.tagName === 'INPUT' && (candidate.type === 'submit' || candidate.type === 'button'))) ? candidate : candidate.querySelector('button, input[type="submit"], input[type="button"]'); if (!btn || btn.disabled) continue; const label = `${btn.value || ''} ${btn.innerText || btn.textContent || ''}`.trim(); const name = btn.name || ''; const action = (btn.form && btn.form.action) || ''; // Guard against "Return to attempt", "Cancel", "Back" if (/cancel|return|back|continue to attempt/i.test(label)) continue; if (/submit all and finish|finish attempt/i.test(label) || /finishattempt|submitallandfinish/i.test(name) || (action.includes('processattempt.php') && /submit|finish/i.test(label))) { return btn; } } return null; } function findQuizSubmitConfirmationButton() { // Priority 1: Actively open Bootstrap / Moodle 4+ modals const openModals = document.querySelectorAll( '.modal.show, .modal.in, div[data-region="modal-container"].show, .modal[style*="display: block"], ' + '.modal.moodle-has-zindex.show, [role="dialog"].show' ); for (const modal of openModals) { if (!isElementVisible(modal)) continue; // Target save / primary button const saveBtn = modal.querySelector('button[data-action="save"], .modal-footer .btn-primary, button.btn-primary, input[type="submit"].btn-primary, input[value*="Submit"]'); if (saveBtn && isElementVisible(saveBtn) && !saveBtn.disabled) { const label = `${saveBtn.value || ''} ${saveBtn.innerText || saveBtn.textContent || ''}`.trim(); if (!/cancel|close|dismiss|back/i.test(label)) return saveBtn; } const allBtns = modal.querySelectorAll('button, input[type="submit"], input[type="button"]'); for (const b of allBtns) { if (!b || b.disabled || !isElementVisible(b)) continue; const label = `${b.value || ''} ${b.innerText || b.textContent || ''}`.trim(); if (/cancel|close|dismiss|back/i.test(label)) continue; if (/submit all and finish|submit attempt|^submit$|^finish$/i.test(label)) return b; } } // Priority 2: Moodle YUI confirmation dialogue const yuiDialogues = document.querySelectorAll('.moodle-dialogue:not(.yui3-panel-hidden), .moodle-dialogue-focused, .confirmation-dialogue'); for (const dialogue of yuiDialogues) { if (!isElementVisible(dialogue)) continue; const primaryBtn = dialogue.querySelector('.confirmation-buttons .btn-primary, .confirmation-buttons input[type="button"], .confirmation-buttons button, .moodle-dialogue-ft .btn-primary'); if (primaryBtn && isElementVisible(primaryBtn) && !primaryBtn.disabled) { const label = `${primaryBtn.value || ''} ${primaryBtn.innerText || primaryBtn.textContent || ''}`.trim(); if (!/cancel|close|dismiss|back/i.test(label)) return primaryBtn; } } // Priority 3: Visible dialogs with role="dialog" or class="modal" const dialogs = document.querySelectorAll('[role="dialog"], .modal, .yui3-panel'); for (const dialog of dialogs) { if (!isElementVisible(dialog)) continue; const buttons = dialog.querySelectorAll('button[data-action="save"], .modal-footer .btn-primary, .btn-primary, input[type="submit"], input[type="button"], button'); for (const button of buttons) { if (!button || button.disabled || !isElementVisible(button)) continue; if (button.getAttribute('data-action') === 'save') return button; const label = `${button.value || ''} ${button.innerText || button.textContent || ''}`.trim(); if (/cancel|do not submit|close|dismiss|back/i.test(label)) continue; if (/submit all and finish|submit attempt|^submit$|^finish$/i.test(label)) { return button; } } } // Priority 4: Standalone visible confirmation button const directButtons = document.querySelectorAll( '.modal-footer button[data-action="save"], .modal-footer .btn-primary, ' + '.confirmation-buttons .btn-primary, .confirmation-buttons input, ' + '[data-region="modal"] button[data-action="save"], [data-region="modal"] .btn-primary' ); for (const b of directButtons) { if (!b || b.disabled || !isElementVisible(b)) continue; const label = `${b.value || ''} ${b.innerText || b.textContent || ''}`.trim(); if (!/cancel|close|dismiss/i.test(label)) return b; } return null; } function monitorConfirmationSubmission() { const confirmUntil = Date.now() + 6_000; const verifySubmission = () => { if (!checkIsQuizSummaryPage()) return; if (Date.now() < confirmUntil) { summaryAutoSubmitTimer = setTimeout(verifySubmission, 150); return; } // If still on summary page after 6s: const parentForm = document.querySelector('form[action*="processattempt.php"]'); if (parentForm) { try { setLog("Auto-Submit: Form still present after confirmation; dispatching direct form submit...", "var(--accent-green)"); if (parentForm.requestSubmit) parentForm.requestSubmit(); else parentForm.submit(); return; } catch (_) { try { parentForm.submit(); return; } catch (_) {} } } const stillOpenConfirmation = findQuizSubmitConfirmationButton(); showQuizSubmitFallback( stillOpenConfirmation ? 'Moodle’s confirmation is still open. Click its submit button to finish submitting.' : 'Moodle did not leave the summary page after confirmation. Check the attempt and click Moodle’s submit button if it is still available.', () => findQuizSubmitConfirmationButton() || findQuizSummarySubmitButton(), stillOpenConfirmation ? 'Find confirmation button' : 'Find Moodle’s submit button' ); }; setLog("Auto-Submit: Confirmation clicked; checking that Moodle completes submission.", "var(--accent-green)"); summaryAutoSubmitTimer = setTimeout(verifySubmission, 150); } function executeQuizSummaryFinalSubmit() { if (!checkIsQuizSummaryPage()) return; // Check if confirmation modal is already open const existingConfirm = findQuizSubmitConfirmationButton(); if (existingConfirm) { summaryAutoSubmitAttempted = true; triggerClick(existingConfirm); monitorConfirmationSubmission(); return; } const currentSubmitBtn = findQuizSummarySubmitButton(); if (!currentSubmitBtn) { showQuizSubmitFallback( 'Auto-submit could not continue because Moodle’s submit button disappeared. Use Moodle’s submission controls on this page to finish.', () => findQuizSubmitConfirmationButton() || findQuizSummarySubmitButton(), 'Find Moodle’s submit button' ); return; } summaryAutoSubmitAttempted = true; triggerClick(currentSubmitBtn); const submitStartTime = Date.now(); let formSubmittedDirectly = false; const confirmUntil = Date.now() + 5_000; const confirmSubmission = () => { if (!checkIsQuizSummaryPage()) return; const confirmationButton = findQuizSubmitConfirmationButton(); if (confirmationButton) { triggerClick(confirmationButton); monitorConfirmationSubmission(); return; } // Direct form submission fallback after 2s if no confirmation modal appeared: if (!formSubmittedDirectly && Date.now() - submitStartTime > 2000) { const parentForm = currentSubmitBtn.form || currentSubmitBtn.closest('form') || document.querySelector('form[action*="processattempt.php"]'); if (parentForm && parentForm.action && parentForm.action.includes('processattempt.php')) { formSubmittedDirectly = true; setLog("Auto-Submit: Submitting Moodle attempt form directly...", "var(--accent-green)"); try { if (parentForm.requestSubmit) { parentForm.requestSubmit(currentSubmitBtn); } else { parentForm.submit(); } } catch (_) { try { parentForm.submit(); } catch (_) {} } } } if (Date.now() < confirmUntil) { summaryAutoSubmitTimer = setTimeout(confirmSubmission, 100); return; } const fallbackConfirmationButton = findQuizSubmitConfirmationButton(); showQuizSubmitFallback( fallbackConfirmationButton ? 'Moodle opened a submission confirmation that could not be completed automatically. Click the confirmation button to finish submitting.' : 'Auto-submit could not confirm that Moodle started submission. Click Moodle’s “Submit all and finish” button to finish.', () => findQuizSubmitConfirmationButton() || findQuizSummarySubmitButton(), fallbackConfirmationButton ? 'Find confirmation button' : 'Find Moodle’s submit button' ); }; summaryAutoSubmitTimer = setTimeout(confirmSubmission, 100); } function showQuizSubmitFallback(message, targetFinder = findQuizSummarySubmitButton, actionLabel = 'Show Moodle submit control') { let notice = document.getElementById('amaes-auto-submit-fallback'); if (!notice) { notice = document.createElement('div'); notice.id = 'amaes-auto-submit-fallback'; notice.setAttribute('role', 'alert'); notice.setAttribute('aria-live', 'assertive'); notice.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:12px;margin:12px 0;padding:12px 14px;border:2px solid #f59e0b;border-radius:8px;background:#fffbeb;color:#78350f;font:600 14px/1.4 sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.12);'; const messageNode = document.createElement('span'); messageNode.className = 'amaes-auto-submit-fallback-message'; const action = document.createElement('button'); action.type = 'button'; action.className = 'amaes-auto-submit-fallback-action'; action.style.cssText = 'flex:0 0 auto;padding:8px 12px;border:0;border-radius:6px;background:#b45309;color:#fff;font-weight:700;cursor:pointer;'; action.addEventListener('click', () => { const target = targetFinder(); if (!target) { messageNode.textContent = 'Moodle’s submit control is not currently available. Use the controls on this summary page to finish submitting.'; return; } target.scrollIntoView({ behavior: 'smooth', block: 'center' }); target.focus({ preventScroll: true }); target.style.outline = '4px solid #f59e0b'; target.style.boxShadow = '0 0 0 6px rgba(245, 158, 11, 0.25)'; triggerClick(target); }); notice.append(messageNode, action); } notice.querySelector('.amaes-auto-submit-fallback-message').textContent = message; notice.querySelector('.amaes-auto-submit-fallback-action').textContent = actionLabel; const summary = document.querySelector('#region-main, main, [role="main"]') || document.body; summary.prepend(notice); notice.scrollIntoView({ behavior: 'smooth', block: 'center' }); setLog(`Auto-Submit needs you: ${message}`, "var(--accent-amber)"); } let summaryAutoSubmitTimer = null; let summaryAutoSubmitAttempted = false; let summaryAutoSubmitHastenMultiplier = 1; let summaryAutoSubmitPaused = false; // Updates Moodle's native submit buttons, sidebar indicator, and panel buttons on summary.php function updateQuizSummarySubmitButtons(remainingSec, multiplier = 1) { if (!checkIsQuizSummaryPage()) return; const isPaused = Boolean(summaryAutoSubmitPaused); const submitBtn = findQuizSummarySubmitButton(); if (submitBtn) { if (!submitBtn.dataset.amaesOrigText) { submitBtn.dataset.amaesOrigText = submitBtn.tagName === 'INPUT' ? submitBtn.value : submitBtn.textContent; } const orig = submitBtn.dataset.amaesOrigText || 'Submit all and finish'; let btnText; if (isPaused) { btnText = `${orig.trim()} (Auto-submit Paused · Enter to submit)`; } else if (remainingSec > 0) { btnText = `${orig.trim()} (${remainingSec}s · Enter to submit)`; } else { btnText = `${orig.trim()} (Enter)`; } if (submitBtn.tagName === 'INPUT') { submitBtn.value = btnText; } else { submitBtn.textContent = btnText; } } // Right-side Quiz Navigation block indicator (#mod_quiz_navblock) const rightNav = document.querySelector('#mod_quiz_navblock, .block_quiz_navigation, aside[data-region="blocks-column"] .card'); if (rightNav) { let navInd = rightNav.querySelector('#amaes-navblock-submit-indicator'); if (!navInd) { navInd = document.createElement('div'); navInd.id = 'amaes-navblock-submit-indicator'; rightNav.appendChild(navInd); } if (isPaused) { navInd.style.cssText = 'margin-top:10px; padding:8px 12px; background:#fffbeb; border:1px solid #f59e0b; border-radius:6px; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size:11px; color:#92400e; box-sizing:border-box; text-align:center;'; navInd.innerHTML = `
Auto-submit Paused
Press P to resume · Enter to submit
`; } else { navInd.style.cssText = 'margin-top:10px; padding:8px 12px; background:#ecfdf5; border:1px solid #10b981; border-radius:6px; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size:11px; color:#065f46; box-sizing:border-box; text-align:center;'; navInd.innerHTML = remainingSec > 0 ? `
Auto-submit in ${remainingSec}s
Press Enter to submit now · P to pause
` : `
Submitting attempt...
`; } const navPauseBtn = navInd.querySelector('#amaes-navblock-pause-btn'); if (navPauseBtn) { navPauseBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); pauseQuizSummaryAutoSubmit(); }; } } // Quick submit button on readyBanner: stable label to avoid width jitter const quickSubmitBtn = document.getElementById('amaes-btn-quick-submit'); if (quickSubmitBtn) { quickSubmitBtn.textContent = 'Submit Now (Enter) \u2192'; } // Pause/Resume button on readyBanner const bannerPauseBtn = document.getElementById('amaes-btn-pause-auto-submit'); if (bannerPauseBtn) { bannerPauseBtn.textContent = isPaused ? 'Resume (P)' : 'Pause (P)'; bannerPauseBtn.style.color = isPaused ? '#b45309' : '#065f46'; bannerPauseBtn.style.borderColor = isPaused ? '#f59e0b' : '#10b981'; bannerPauseBtn.style.background = isPaused ? 'rgba(245,158,11,0.1)' : 'rgba(16,185,129,0.1)'; } // Ready Banner container visual styling (green when active, amber when paused) const readyBannerEl = document.getElementById('amaes-summary-ready-banner'); if (readyBannerEl) { if (isPaused) { readyBannerEl.style.borderColor = '#f59e0b'; readyBannerEl.style.background = '#fffbeb'; readyBannerEl.style.color = '#92400e'; } else { readyBannerEl.style.borderColor = '#10b981'; readyBannerEl.style.background = '#ecfdf5'; readyBannerEl.style.color = '#065f46'; } } // Description text on readyBanner const bannerDescEl = document.getElementById('amaes-summary-banner-desc'); if (bannerDescEl) { if (isPaused) { bannerDescEl.innerHTML = `Auto-submit paused (${remainingSec}s remaining). Review at your own pace or press P to resume.`; } else if (remainingSec > 0) { bannerDescEl.innerHTML = `Auto-submitting attempt in ${remainingSec}s (Press Enter to submit · P to pause)...`; } else { bannerDescEl.textContent = 'Auto-submitting attempt...'; } } // Toolkit panel button const btnMasterAutoQuiz = document.getElementById('btn-master-auto-quiz'); if (btnMasterAutoQuiz) { if (isPaused) { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)'; btnMasterAutoQuiz.innerHTML = `${ICONS.pause || ICONS.zap} Paused (Press P to resume)`; btnMasterAutoQuiz.title = 'Auto-submit paused. Click or press P to resume, or Enter to submit'; } else { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #10b981, #059669)'; btnMasterAutoQuiz.innerHTML = `${ICONS.checkCircle} Submit (${remainingSec}s · Enter)`; btnMasterAutoQuiz.title = 'Auto-submitting. Click or press Enter to submit immediately, P to pause'; } } } // Pauses or resumes auto-submit countdown on summary.php function pauseQuizSummaryAutoSubmit(forcePause = null) { if (!checkIsQuizSummaryPage()) return false; const deadlineKey = 'amaes_summary_deadline_' + window.location.href; const pausedSecKey = 'amaes_summary_paused_sec_' + window.location.href; if (forcePause === null) { summaryAutoSubmitPaused = !summaryAutoSubmitPaused; } else { summaryAutoSubmitPaused = Boolean(forcePause); } if (summaryAutoSubmitPaused) { if (summaryAutoSubmitTimer) { clearInterval(summaryAutoSubmitTimer); clearTimeout(summaryAutoSubmitTimer); summaryAutoSubmitTimer = null; } const targetDeadline = parseInt(sessionStorage.getItem(deadlineKey) || '0', 10); let remainingSec = 5; if (targetDeadline && !isNaN(targetDeadline)) { remainingSec = Math.max(1, Math.ceil((targetDeadline - Date.now()) / 1000)); } sessionStorage.removeItem(deadlineKey); sessionStorage.setItem(pausedSecKey, String(remainingSec)); updateQuizSummarySubmitButtons(remainingSec, summaryAutoSubmitHastenMultiplier); showToast("Auto-submit paused. Review at your own pace.", 2500); setLog("Auto-Submit: Paused via [P]. Countdown stopped.", "var(--accent-amber, #f59e0b)"); return true; } else { // Resume let remainingSec = parseInt(sessionStorage.getItem(pausedSecKey) || '5', 10); if (remainingSec <= 0) remainingSec = 5; sessionStorage.removeItem(pausedSecKey); let targetDeadline = Date.now() + (remainingSec * 1000); sessionStorage.setItem(deadlineKey, String(targetDeadline)); summaryAutoSubmitPaused = false; setLog(`Auto-Submit: Resumed via [P] (${remainingSec}s remaining).`, "var(--accent-green)"); showToast(`Auto-submit resumed (${remainingSec}s)...`, 2000); updateQuizSummarySubmitButtons(remainingSec, summaryAutoSubmitHastenMultiplier); const countdownInterval = setInterval(() => { if (summaryAutoSubmitPaused) return; remainingSec = Math.max(0, Math.ceil((targetDeadline - Date.now()) / 1000)); const secEl = document.getElementById('amaes-summary-countdown-sec'); if (secEl) { secEl.textContent = `${remainingSec}s`; } updateQuizSummarySubmitButtons(remainingSec, summaryAutoSubmitHastenMultiplier); if (remainingSec <= 0) { clearInterval(countdownInterval); sessionStorage.removeItem(deadlineKey); summaryAutoSubmitTimer = null; setLog("Auto-Submit: Leeway finished. Submitting attempt now...", "var(--accent-green)"); executeQuizSummaryFinalSubmit(); } else if (remainingSec % 5 === 0 || remainingSec <= 3) { setLog(`Auto-Submit: Submitting attempt in ${remainingSec}s...`, "var(--accent-green)"); } }, 1000); summaryAutoSubmitTimer = countdownInterval; return true; } } // Hastens or immediately executes summary submission on Enter, Space, N, or quick submit function hastenQuizSummaryAutoSubmit() { if (!checkIsQuizSummaryPage()) return false; const deadlineKey = 'amaes_summary_deadline_' + window.location.href; const pausedSecKey = 'amaes_summary_paused_sec_' + window.location.href; // Clear timers immediately if (summaryAutoSubmitTimer) { clearInterval(summaryAutoSubmitTimer); clearTimeout(summaryAutoSubmitTimer); summaryAutoSubmitTimer = null; } summaryAutoSubmitPaused = false; sessionStorage.removeItem(deadlineKey); sessionStorage.removeItem(pausedSecKey); const fallbackNotice = document.getElementById('amaes-auto-submit-fallback'); if (fallbackNotice) fallbackNotice.remove(); summaryAutoSubmitAttempted = false; // Priority Check: If confirmation modal is ALREADY visible on screen, click its confirmation button immediately! const existingConfirmBtn = findQuizSubmitConfirmationButton(); if (existingConfirmBtn) { setLog("Auto-Submit: Submitting confirmation modal via [Enter/N]...", "var(--accent-green)"); showToast("Submitting confirmation...", 1500); triggerClick(existingConfirmBtn); monitorConfirmationSubmission(); return true; } setLog("Auto-Submit: Instant submission triggered via [Enter/N]. Submitting attempt now...", "var(--accent-green)"); showToast("Submitting attempt now...", 1500); executeQuizSummaryFinalSubmit(); return true; } // Evaluates attempt completeness on Quiz Summary Page (/mod/quiz/summary.php) function evaluateQuizSummaryPage() { if (!checkIsQuizSummaryPage()) return null; const table = document.querySelector('table.quizsummaryofattempt, .generaltable.quizsummaryofattempt, #region-main table.generaltable, table.generaltable'); if (!table) return null; const rows = Array.from(table.querySelectorAll('tbody tr')); const unanswered = []; let total = 0; let answered = 0; rows.forEach(row => { const cells = Array.from(row.querySelectorAll('th, td')); if (cells.length < 2) return; const qCell = cells[0]; const statusCell = cells[1]; const qNum = (qCell.textContent || '').trim(); const statusText = (statusCell.textContent || '').trim(); if (!qNum || isNaN(parseInt(qNum, 10))) return; total += 1; const link = row.querySelector('a[href*="/mod/quiz/attempt.php"]') || qCell.querySelector('a') || null; const isSaved = /answer saved|completed|complete/i.test(statusText); const isNotAnswered = /not yet answered|not answered|incomplete|no answer/i.test(statusText); if (isNotAnswered || (!isSaved && statusText.length > 0)) { unanswered.push({ qNum, rowElement: row, statusCell, link, statusText }); } else { answered += 1; } }); return { totalQuestions: total, answeredQuestions: answered, unansweredQuestions: unanswered, isAllAnswered: total > 0 && unanswered.length === 0 }; } // Auto-Mark as Done / Submit Handler for Quiz Summary Page (/mod/quiz/summary.php) function handleQuizSummaryAutoSubmit() { if (!checkIsQuizSummaryPage()) return; promoteAttemptEvidenceFromScore(); const evalRes = evaluateQuizSummaryPage(); // INCOMPLETE ATTEMPT SAFETY GUARD: If some questions are NOT answered, highlight them and pause auto-submit! if (evalRes && evalRes.unansweredQuestions.length > 0) { if (summaryAutoSubmitTimer) { clearTimeout(summaryAutoSubmitTimer); summaryAutoSubmitTimer = null; } // Highlight incomplete rows evalRes.unansweredQuestions.forEach(item => { item.rowElement.style.background = 'rgba(245, 158, 11, 0.15)'; item.rowElement.style.outline = '2px solid #f59e0b'; item.rowElement.style.borderRadius = '4px'; if (!item.statusCell.querySelector('.amaes-unanswered-badge')) { const badge = document.createElement('span'); badge.className = 'amaes-unanswered-badge'; badge.style.cssText = 'background:#f59e0b; color:#ffffff; padding:2px 7px; border-radius:4px; font-weight:800; font-size:10px; margin-left:8px; display:inline-block;'; badge.textContent = 'Not Answered'; item.statusCell.appendChild(badge); } }); // Top Warning Banner let banner = document.getElementById('amaes-summary-incomplete-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'amaes-summary-incomplete-banner'; banner.style.cssText = 'display:flex; align-items:center; justify-content:space-between; gap:12px; margin:14px 0; padding:12px 16px; border:2px solid #f59e0b; border-radius:10px; background:#fffbeb; color:#78350f; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow:0 4px 12px rgba(245,158,11,0.15);'; banner.innerHTML = `
${ICONS.alertTriangle}
Incomplete Quiz: ${evalRes.unansweredQuestions.length} Question${evalRes.unansweredQuestions.length > 1 ? 's' : ''} Not Answered!
Auto-submit paused to protect your grade. Questions needing answers: ${evalRes.unansweredQuestions.map(u => '#' + u.qNum).join(', ')}.
${evalRes.unansweredQuestions[0].link ? ` Answer Question #${evalRes.unansweredQuestions[0].qNum} → ` : ''} `; const mainArea = document.querySelector('#region-main, main, [role="main"]') || document.body; mainArea.prepend(banner); } syncAutoQuizUI(); setLog(`Incomplete Quiz: ${evalRes.unansweredQuestions.length} questions not answered (${evalRes.unansweredQuestions.map(u => '#' + u.qNum).join(', ')}). Auto-submit paused.`, "var(--accent-amber)"); showToast(`Incomplete quiz: ${evalRes.unansweredQuestions.length} questions not answered!`, 4500); return; } // ALL QUESTIONS ANSWERED: Ready for submission! document.getElementById('amaes-summary-incomplete-banner')?.remove(); const shouldAutoSubmit = autoSubmitQuiz || autoQuizMode; const delaySec = (typeof autoSubmitDelay === 'number' && !isNaN(autoSubmitDelay)) ? autoSubmitDelay : 10; // Render Ready Banner let readyBanner = document.getElementById('amaes-summary-ready-banner'); if (!readyBanner) { readyBanner = document.createElement('div'); readyBanner.id = 'amaes-summary-ready-banner'; readyBanner.style.cssText = 'display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; margin:12px 0 8px 0; padding:10px 14px; border:1px solid #10b981; border-radius:8px; background:#ecfdf5; color:#065f46; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-sizing:border-box; box-shadow:0 1px 4px rgba(16,185,129,0.08);'; const autoSubmitDesc = delaySec > 0 ? `Auto-submitting attempt in ${delaySec}s (Press Enter to submit · P to pause)...` : 'Auto-submitting attempt...'; readyBanner.innerHTML = `
${ICONS.checkCircle} All ${evalRes ? evalRes.totalQuestions : ''} questions answered • ${shouldAutoSubmit ? autoSubmitDesc : 'Review your saved answers below and click Submit all and finish.'}
${shouldAutoSubmit ? ` ` : ''}
`; readyBanner.querySelector('#amaes-btn-quick-submit').onclick = () => { hastenQuizSummaryAutoSubmit(); }; const pauseBtn = readyBanner.querySelector('#amaes-btn-pause-auto-submit'); if (pauseBtn) { pauseBtn.onclick = () => { pauseQuizSummaryAutoSubmit(); }; } const submitContainer = document.querySelector('.submitbtns, .quizsummarybtns, .btn-finishattempt'); if (submitContainer) { submitContainer.parentNode.insertBefore(readyBanner, submitContainer); } else { const mainArea = document.querySelector('#region-main, main, [role="main"]') || document.body; mainArea.prepend(readyBanner); } } // Update UI immediately to indicate completion and waiting for confirmation syncAutoQuizUI(); setLog( shouldAutoSubmit ? `Quiz Completed: All questions answered and saved. Auto-submit is active (${delaySec > 0 ? delaySec + 's leeway' : 'instant'}).` : "Quiz Completed: All questions answered and saved. Waiting for your confirmation to submit.", "var(--accent-green)", shouldAutoSubmit ? "Moodle submission will be confirmed automatically" : "Review your saved answers below and click Submit all and finish" ); showToast(shouldAutoSubmit ? (delaySec > 0 ? `All questions answered. Auto-submitting in ${delaySec}s...` : "All questions answered. Auto-submitting...") : "All questions answered. Waiting for confirmation...", 4000); // Update document title so students multitasking in background tabs know it's ready if (typeof document !== 'undefined' && document.title && !document.title.includes('(Ready to Submit)')) { document.title = '(Ready to Submit) ' + document.title; } // Highlight Moodle's "Submit all and finish" button const submitBtn = findQuizSummarySubmitButton(); if (submitBtn) { submitBtn.style.outline = '3px solid #10b981'; submitBtn.style.boxShadow = '0 0 15px rgba(16,185,129,0.45)'; } if (!sessionStorage.getItem('amaes_summary_ding_' + window.location.href)) { sessionStorage.setItem('amaes_summary_ding_' + window.location.href, 'true'); playToolkitSound('quest_done'); if (typeof recordSessionQuizCompleted === 'function') { recordSessionQuizCompleted(); } } if ((autoSubmitQuiz || autoQuizMode) && !summaryAutoSubmitAttempted && !summaryAutoSubmitTimer) { if (!submitBtn) { showQuizSubmitFallback( 'Auto-submit could not find Moodle’s “Submit all and finish” button. Locate Moodle’s submission control on this page and click it to finish.', () => findQuizSummarySubmitButton(), 'Find Moodle’s submit button' ); return; } if (delaySec <= 0) { setLog("Auto-Submit: Submitting attempt now...", "var(--accent-green)"); showToast("Auto-submitting attempt...", 1500); summaryAutoSubmitTimer = setTimeout(() => { summaryAutoSubmitTimer = null; executeQuizSummaryFinalSubmit(); }, 200); } else { summaryAutoSubmitHastenMultiplier = 1; const deadlineKey = 'amaes_summary_deadline_' + window.location.href; let targetDeadline = parseInt(sessionStorage.getItem(deadlineKey) || '0', 10); if (!targetDeadline || isNaN(targetDeadline) || targetDeadline > Date.now() + (delaySec * 2000) || targetDeadline < Date.now() - 5000) { targetDeadline = Date.now() + (delaySec * 1000); sessionStorage.setItem(deadlineKey, String(targetDeadline)); } let remainingSec = Math.max(0, Math.ceil((targetDeadline - Date.now()) / 1000)); if (remainingSec <= 0) { sessionStorage.removeItem(deadlineKey); executeQuizSummaryFinalSubmit(); return; } setLog(`Auto-Submit: Submitting attempt in ${remainingSec}s... (Press Enter to submit · P to pause)`, "var(--accent-green)"); showToast(`Auto-submitting in ${remainingSec}s...`, 2500); updateQuizSummarySubmitButtons(remainingSec, summaryAutoSubmitHastenMultiplier); const countdownInterval = setInterval(() => { if (summaryAutoSubmitPaused) return; remainingSec = Math.max(0, Math.ceil((targetDeadline - Date.now()) / 1000)); const secEl = document.getElementById('amaes-summary-countdown-sec'); if (secEl) { secEl.textContent = `${remainingSec}s`; } updateQuizSummarySubmitButtons(remainingSec, summaryAutoSubmitHastenMultiplier); if (remainingSec <= 0) { clearInterval(countdownInterval); sessionStorage.removeItem(deadlineKey); summaryAutoSubmitTimer = null; setLog("Auto-Submit: Leeway finished. Submitting attempt now...", "var(--accent-green)"); executeQuizSummaryFinalSubmit(); } else if (remainingSec % 5 === 0 || remainingSec <= 3) { setLog(`Auto-Submit: Submitting attempt in ${remainingSec}s...`, "var(--accent-green)"); } }, 1000); summaryAutoSubmitTimer = countdownInterval; } } else { logDebug("Quiz Summary reached. Student reviews at their own pace (Auto-submit disabled by design)."); } } function syncFastQuizUI() { const hudModeText = document.getElementById('hud-mode-text'); if (hudModeText && autoQuizMode && !isWaitingForUserAnswer) { hudModeText.textContent = 'Co-Pilot'; hudModeText.style.color = 'var(--accent-blue, #3b82f6)'; } } // Unified Synchronizer for UI states (Floating HUD + Panel Button) function syncAutoQuizUI(isPausedOnUnknown = false) { const btnMasterAutoQuiz = document.getElementById('btn-master-auto-quiz'); if (btnMasterAutoQuiz) { if (checkIsQuizSummaryPage()) { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #10b981, #059669)'; btnMasterAutoQuiz.innerHTML = `${ICONS.checkBadge || ICONS.zap} Review & Submit`; btnMasterAutoQuiz.title = 'All questions answered! Click to scroll to submit button'; } else if (isPausedOnUnknown || isWaitingForUserAnswer || (checkIsQuizAttemptPage() && document.querySelector('.amaes-blockage-hud'))) { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)'; btnMasterAutoQuiz.innerHTML = `${ICONS.zap} Waiting on Q (Press N)`; } else if (autoQuizMode) { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)'; btnMasterAutoQuiz.innerHTML = `${ICONS.stop} Pause Auto-Quiz`; } else { btnMasterAutoQuiz.style.background = 'linear-gradient(135deg, #10b981, #059669)'; btnMasterAutoQuiz.innerHTML = `${ICONS.zap} Start Auto-Quiz`; } } const subtext = document.getElementById('amaes-autoquiz-subtext'); if (subtext) { if (checkIsQuizSummaryPage()) { subtext.textContent = 'All questions answered and saved. Review your answers and submit when ready.'; } else if (isPausedOnUnknown || isWaitingForUserAnswer || (checkIsQuizAttemptPage() && document.querySelector('.amaes-blockage-hud'))) { subtext.textContent = 'Paused on unknown question. Choose an answer to continue.'; } else if (autoQuizMode) { subtext.textContent = 'Auto-answering & advancing in background. Click to pause.'; } else { subtext.textContent = 'Auto-answers & advances. Pauses & copies on unknown questions.'; } } const existingHud = document.getElementById('amaes-quiz-hud'); if (existingHud) { existingHud.remove(); } if (checkIsQuizAttemptPage()) { injectQuizFloatingHUD(); } // Sync all in-question top toolbar stop/resume buttons document.querySelectorAll('.amaes-que-stop-btn').forEach(btn => { if (autoQuizMode) { btn.innerHTML = `${ICONS.stop} Stop Auto`; btn.style.background = 'rgba(239, 68, 68, 0.08)'; btn.style.color = '#ef4444'; btn.style.border = '1px solid rgba(239, 68, 68, 0.28)'; btn.title = 'Stop automation so you can answer manually without any interference'; } else { btn.innerHTML = `${ICONS.play} Resume`; btn.style.background = 'rgba(16, 185, 129, 0.08)'; btn.style.color = '#10b981'; btn.style.border = '1px solid rgba(16, 185, 129, 0.28)'; btn.title = 'Resume autonomous quiz solver and auto-navigation'; } }); // Sync blockage HUD button const blockageStopBtn = document.getElementById('btn-blockage-stop'); if (blockageStopBtn) { blockageStopBtn.innerHTML = `${autoQuizMode ? ICONS.stop : ICONS.play} ${autoQuizMode ? 'Stop Auto' : 'Resume Auto'}`; blockageStopBtn.style.background = autoQuizMode ? '#fee2e2' : '#dcfce7'; blockageStopBtn.style.color = autoQuizMode ? '#b91c1c' : '#15803d'; blockageStopBtn.style.borderColor = autoQuizMode ? '#f87171' : '#86efac'; } // Sync batch copy container visibility (only visible when questions are present) const batchCopyContainer = document.getElementById('amaes-batch-copy-container'); if (batchCopyContainer) { const hasQue = checkIsQuizPage() && Boolean(document.querySelector('.que')); batchCopyContainer.style.display = hasQue ? 'flex' : 'none'; if (autoQuizMode) { batchCopyContainer.style.display = 'none'; } } // Keep the in-question pause control visible; hide only the duplicate copy action. document.querySelectorAll('.amaes-card-btn-container .amaes-copy-ai-card-btn:not(.amaes-paste-ai-card-btn):not(.amaes-ask-ai-card-btn)').forEach(button => { button.style.display = autoQuizMode ? 'none' : ''; }); // Synchronize Auto-Pilot controls (Locked ON and disabled while Auto-Quiz is actively driving) const chkAutoPick = document.getElementById('chk-auto-pick'); const chkAutoNextVerified = document.getElementById('chk-auto-next-verified'); const chkAutoPickStudyGuide = document.getElementById('chk-auto-pick-study-guide-fallback'); if (autoQuizMode) { if (chkAutoPick) { chkAutoPick.checked = true; chkAutoPick.disabled = true; chkAutoPick.title = 'Active & locked while Auto-Quiz is running'; } if (chkAutoNextVerified) { chkAutoNextVerified.checked = true; chkAutoNextVerified.disabled = true; chkAutoNextVerified.title = 'Active & locked while Auto-Quiz is running'; } if (chkAutoPickStudyGuide) { chkAutoPickStudyGuide.checked = true; chkAutoPickStudyGuide.disabled = true; chkAutoPickStudyGuide.title = 'Active & locked while Auto-Quiz is running'; } } else { if (chkAutoPick) { chkAutoPick.disabled = false; chkAutoPick.checked = autoPickQuiz; chkAutoPick.title = 'Automatically select choice inputs when verified answers are matched'; } if (chkAutoNextVerified) { chkAutoNextVerified.disabled = false; chkAutoNextVerified.checked = autoNextVerified; chkAutoNextVerified.title = 'Advances smoothly to next question when answered; pauses on unknown questions'; } if (chkAutoPickStudyGuide) { chkAutoPickStudyGuide.disabled = false; chkAutoPickStudyGuide.checked = autoPickStudyGuideFallback; chkAutoPickStudyGuide.title = "In Auto-Quiz, select Jenny's Online or AMAUOED study guide choices when no verified answer is available in the database."; } } } // Master Toggle Function for Starting / Pausing Autonomous Quiz function toggleAutoQuizMode(desiredState = null) { if (desiredState !== null) { autoQuizMode = Boolean(desiredState); } else { autoQuizMode = !autoQuizMode; } localStorage.setItem('amaes_auto_quiz_mode', autoQuizMode ? 'true' : 'false'); if (autoQuizMode) { autoPickQuiz = true; autoNextVerified = true; autoPickStudyGuideFallback = true; isWaitingForUserAnswer = false; const isAttempt = checkIsQuizAttemptPage(); showToast(isAttempt ? "Auto-Quiz Started (Co-Pilot)" : "Auto-Quiz Armed (Ready for Quiz)"); setLog(isAttempt ? "Auto-Quiz started in Co-Pilot mode!" : "Auto-Quiz armed! Will run hands-free upon opening any quiz attempt.", "var(--accent-green)"); if (isAttempt) runAutoQuizSolver(true); if (checkIsQuizSummaryPage()) handleQuizSummaryAutoSubmit(); } else { isWaitingForUserAnswer = false; clearTimeout(autoNextTimer); autoNextTimer = null; clearTimeout(pageLoadSolverTimer); pageLoadSolverTimer = null; isSolverRunning = false; if (activeAiAbortController) { try { activeAiAbortController.abort(); } catch (_) {} activeAiAbortController = null; } document.querySelectorAll('.amaes-ai-thinking-indicator').forEach(el => el.remove()); showToast("Auto-Quiz Paused"); setLog("Auto-Quiz paused. Auto-answers & auto-navigation completely stopped.", "var(--accent-amber)"); } syncAutoQuizUI(); } function getQuizFloatingProgressState() { const questions = Array.from(document.querySelectorAll('.que')); const navState = getQuizNavQuestionStates(); if (questions.length > 1) { const viewportHeight = window.innerHeight || 800; const readingLine = viewportHeight * 0.42; let currentQuestion = questions.find(question => { const rect = question.getBoundingClientRect(); return rect.top <= readingLine && rect.bottom > readingLine; }); if (!currentQuestion) { currentQuestion = questions .map(question => { const rect = question.getBoundingClientRect(); return { question, distance: Math.abs((rect.top + rect.bottom) / 2 - readingLine), visible: rect.bottom > 0 && rect.top < viewportHeight }; }) .filter(item => item.visible) .sort((a, b) => a.distance - b.distance)[0]?.question || questions[0]; } const currentIndex = Math.max(0, questions.indexOf(currentQuestion)); const totalAnswered = questions.filter(isQuestionAnswered).length; return { currentIndex, totalQuestions: questions.length, totalAnswered }; } if (!navState || navState.totalQuestions === 0) return null; const questionData = questions.length === 1 ? extractQuestionData(questions[0]) : null; const questionIndex = questionData ? navState.questions.findIndex(question => question.qNum === questionData.qNum) : -1; const currentIndex = questionIndex >= 0 ? questionIndex : navState.currentIndex; const currentNavQuestion = navState.questions[currentIndex]; const currentAnswerIsLive = questions.length === 1 && isQuestionAnswered(questions[0]); const answerCount = navState.totalAnswered + (currentAnswerIsLive && currentNavQuestion && !currentNavQuestion.isAnswered ? 1 : 0); return { ...navState, currentIndex, totalAnswered: answerCount }; } let quizHudUpdateFrame = null; function updateQuizFloatingHUD() { if (quizHudUpdateFrame !== null) return; quizHudUpdateFrame = requestAnimationFrame(() => { quizHudUpdateFrame = null; const hud = document.getElementById('amaes-quiz-hud'); if (!hud || !checkIsQuizAttemptPage()) return; const progress = getQuizFloatingProgressState(); const progressContainer = document.getElementById('hud-question-progress'); if (!progressContainer) return; if (!progress) { progressContainer.style.display = 'none'; return; } const currentQuestion = document.getElementById('hud-question-label'); const progressFill = document.getElementById('hud-question-progress-fill'); const totalQuestions = progress.totalQuestions; const currentIndex = Math.max(0, Math.min(progress.currentIndex, totalQuestions - 1)); const totalAnswered = Math.max(0, Math.min(progress.totalAnswered, totalQuestions)); const percentage = Math.round((totalAnswered / totalQuestions) * 100); if (currentQuestion) currentQuestion.textContent = `Q ${currentIndex + 1}/${totalQuestions}`; if (progressFill) progressFill.style.width = `${percentage}%`; progressContainer.title = `${totalAnswered}/${totalQuestions} answered (${percentage}%)`; progressContainer.style.display = 'flex'; }); } function bindQuizFloatingHUDUpdates() { if (window.__amaesQuizHudUpdatesBound) return; window.__amaesQuizHudUpdatesBound = true; const scheduleUpdate = () => updateQuizFloatingHUD(); window.addEventListener('scroll', scheduleUpdate, { passive: true, capture: true }); window.addEventListener('resize', scheduleUpdate, { passive: true }); document.addEventListener('input', event => { if (event.target && event.target.closest && event.target.closest('.que')) scheduleUpdate(); }, true); document.addEventListener('change', event => { if (event.target && event.target.closest && event.target.closest('.que')) scheduleUpdate(); }, true); document.addEventListener('click', event => { if (event.target && event.target.closest && event.target.closest('.que')) scheduleUpdate(); }, true); } // Floating HUD for Quiz Attempt Screen function injectQuizFloatingHUD() { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') return; if (!checkIsQuizAttemptPage()) return; if (document.getElementById('amaes-quiz-hud')) return; const navState = getQuizFloatingProgressState(); const panelEl = document.getElementById('amaes-toolkit-panel'); const bodyEl = document.getElementById('amaes-panel-body'); const isPanelMinimized = !panelEl || panelEl.classList.contains('amaes-minimized') || (bodyEl && bodyEl.style.display === 'none'); let progressHtml = ''; if (navState && navState.totalQuestions > 0) { const currentQ = navState.currentIndex >= 0 ? (navState.currentIndex + 1) : 1; const totalQ = navState.totalQuestions; const pct = Math.round((navState.totalAnswered / totalQ) * 100); progressHtml = `
Q ${currentQ}/${totalQ}
`; } const hud = document.createElement('div'); hud.id = 'amaes-quiz-hud'; hud.style.cssText = ` position: fixed; bottom: max(12px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left)); max-width: calc(100vw - 24px); box-sizing: border-box; flex-wrap: wrap; z-index: 99998; background: var(--surface, #1e293b); border: 1.5px solid ${autoQuizMode ? '#3b82f6' : 'var(--border, #334155)'}; border-radius: 30px; padding: 6px 14px; box-shadow: 0 6px 24px rgba(0,0,0,0.45); display: flex; align-items: center; gap: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; color: var(--text-primary, #f8fafc); backdrop-filter: blur(8px); `; hud.innerHTML = `
Auto-Quiz: ${!autoQuizMode ? 'Paused' : (isWaitingForUserAnswer ? 'Waiting on Q' : 'Co-Pilot')}
${progressHtml} ${ICONS.download} Update Now `; document.body.appendChild(hud); bindQuizFloatingHUDUpdates(); updateQuizFloatingHUD(); const _el__btn_hud_toggle_quiz_ = document.getElementById('btn-hud-toggle-quiz'); if (_el__btn_hud_toggle_quiz_) _el__btn_hud_toggle_quiz_.onclick = () => { toggleAutoQuizMode(); }; } // Auto-select choice based on AI answer from clipboard (Shortcut: V or Button) async function autoSelectFromAiClipboard(explicitQue = null) { if (!navigator.clipboard || !navigator.clipboard.readText) { showToast("Clipboard reading not supported in this browser"); return; } try { const text = await navigator.clipboard.readText(); if (!text || !text.trim()) { showToast("Clipboard is empty. Copy the AI answer first."); return; } const targetQue = explicitQue || getActiveViewportQuestion(); if (!targetQue) { showToast("No quiz question found on page"); return; } const isMultiMatching = (typeof identifyQuestionType === 'function' && identifyQuestionType(targetQue) === 'match') || targetQue.classList.contains('match') || targetQue.classList.contains('qtype_match') || targetQue.querySelectorAll('select').length > 1; if (isMultiMatching) { showToast("Matching questions use row dropdowns. Pick options directly or use AI beside each row.", 3500); return; } const cleanText = text.trim(); const inputElements = Array.from(targetQue.querySelectorAll('.answer input[type="radio"], .answer input[type="checkbox"]')); if (inputElements.length === 0) { // Check for Short-Answer or fill-in-the-blank text inputs const textInputs = Array.from(targetQue.querySelectorAll('input[type="text"], input.form-control, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])')); if (textInputs.length > 0) { const cleanedAnswer = cleanText .replace(/^Answer:\s*/i, '') .replace(/^The correct answer is:\s*/i, '') .replace(/^[a-e][.)]\s*/i, '') .replace(/^["']|["']$/g, '') .trim(); textInputs[0].value = cleanedAnswer; textInputs[0].dispatchEvent(new Event('input', { bubbles: true })); textInputs[0].dispatchEvent(new Event('change', { bubbles: true })); textInputs[0].dispatchEvent(new Event('blur', { bubbles: true })); showToast(`Pasted to Text Box: ${cleanedAnswer}`); setLog(`AI Paste: Filled text input with ${cleanedAnswer}`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(1000, true); } return; } // Check for Dropdown / Select elements (matching questions & gapselect) const selectInputs = Array.from(targetQue.querySelectorAll('select')); if (selectInputs.length > 0) { const cleanedAnswer = cleanText .replace(/^Answer:\s*/i, '') .replace(/^The correct answer is:\s*/i, '') .replace(/^["']|["']$/g, '') .trim(); let selectedCount = 0; selectInputs.forEach((sel, sIdx) => { const row = sel.closest('tr'); const rowTextElem = row ? row.querySelector('td.text') : null; const subQText = rowTextElem ? rowTextElem.innerText.trim() : ''; // Look for sub-question matching in lines of clipboard text let targetAns = ''; if (subQText) { const lines = cleanedAnswer.split(/[\n,;]+/).map(l => l.trim()); for (const line of lines) { const subNorm = normalizeText(subQText); const lineNorm = normalizeText(line); if (lineNorm.includes(subNorm)) { const parts = line.split(/[:\->=]+/); if (parts.length >= 2) { targetAns = parts.slice(1).join(':').trim(); break; } } } } const options = Array.from(sel.options); const matchOpt = options.find(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optNorm = normalizeText(opt.text); if (targetAns) { const tNorm = normalizeText(targetAns); if (optNorm === tNorm || optNorm.includes(tNorm) || tNorm.includes(optNorm)) return true; } const cleanNorm = normalizeText(cleanedAnswer); return optNorm === cleanNorm || (cleanNorm.length > 2 && optNorm.includes(cleanNorm)) || (optNorm.length > 2 && cleanNorm.includes(optNorm)); }); if (matchOpt) { sel.value = matchOpt.value; sel.dispatchEvent(new Event('input', { bubbles: true })); sel.dispatchEvent(new Event('change', { bubbles: true })); sel.dispatchEvent(new Event('blur', { bubbles: true })); selectedCount++; } }); if (selectedCount > 0) { showToast(`Pasted to Dropdown: Selected ${selectedCount} option(s)`); setLog(`AI Paste: Selected ${selectedCount} dropdown option(s) from clipboard`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(1000, true); } return; } } // Check for Drag and Drop questions (ddwtos, ddimageortext, ddmarker) const dragHomes = Array.from(targetQue.querySelectorAll('.draghome, .dragitems .draghome, .drags .drag, .drags span, span.draghome, .dragboxes .drag, .dragitem')); const dropZones = Array.from(targetQue.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable')); if (dragHomes.length > 0 && dropZones.length > 0) { const cleanedAnswer = cleanText .replace(/^Answer:\s*/i, '') .replace(/^The correct answer is:\s*/i, '') .replace(/^["']|["']$/g, '') .trim(); // Extract answers per blank let answersForBlanks = []; const blankMatches = cleanedAnswer.match(/Blank\s*\d+\s*[:\-–]\s*([^\n,;]+)/gi); if (blankMatches && blankMatches.length > 0) { answersForBlanks = blankMatches.map(m => m.replace(/Blank\s*\d+\s*[:\-–]\s*/i, '').trim()); } else if (cleanedAnswer.includes('\n')) { answersForBlanks = cleanedAnswer.split('\n').map(l => l.replace(/^[a-z0-9][.)]\s*/i, '').trim()).filter(Boolean); } else if (cleanedAnswer.includes(',') && dropZones.length > 1) { answersForBlanks = cleanedAnswer.split(',').map(s => s.trim()).filter(Boolean); } else { answersForBlanks = [cleanedAnswer]; } let placedCount = 0; dropZones.forEach((dz, idx) => { const ans = answersForBlanks[idx] || (dropZones.length === 1 ? answersForBlanks[0] : null); if (!ans) return; const normAns = normalizeChoice(ans); const matchingDrag = dragHomes.find(dh => { const dText = normalizeChoice(cleanDOMToAI(dh)); return dText === normAns || (normAns.length > 1 && dText.includes(normAns)) || (dText.length > 1 && normAns.includes(dText)); }); if (matchingDrag) { const hiddenInput = targetQue.querySelector(`input.placeinput.place${idx + 1}, input[name$="_p${idx + 1}"], input[name*="_p${idx + 1}"]`) || targetQue.querySelectorAll('input.placeinput, input[type="hidden"][name*="_p"]')[idx]; const choiceClass = Array.from(matchingDrag.classList).find(c => /^(?:choice|c)(\d+)$/i.test(c)); let choiceNum = choiceClass ? choiceClass.replace(/^[^\d]+/, '') : String(dragHomes.indexOf(matchingDrag) + 1); if (hiddenInput && choiceNum) { hiddenInput.value = choiceNum; hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); hiddenInput.dispatchEvent(new Event('blur', { bubbles: true })); } matchingDrag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); dz.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); dz.innerText = ans; dz.style.outline = '2px solid #10b981'; dz.style.fontWeight = '700'; placedCount++; } }); if (placedCount > 0) { showToast(`Pasted to Drag & Drop: Placed ${placedCount} item(s)`); setLog(`AI Paste: Placed ${placedCount} drag-and-drop item(s) from clipboard`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(1000, true); } return; } } showToast("No answer inputs found for this question"); return; } const isCheckbox = targetQue.querySelectorAll('.answer input[type="checkbox"]').length > 0; // 1. Multi-letter matching for multi-choice checkbox questions (e.g. "a, c", "a and d", "A, B, C, E, F") const multiLetters = cleanText.match(/\b([a-zA-Z])\b/g); if (isCheckbox && multiLetters && multiLetters.length > 1) { const uniqueLetters = Array.from(new Set(multiLetters.map(l => l.toUpperCase()))); let checkedCount = 0; uniqueLetters.forEach(letter => { const idx = letter.charCodeAt(0) - 65; if (inputElements[idx] && idx >= 0 && idx < inputElements.length) { safelySelectInput(inputElements[idx]); checkedCount++; } }); if (checkedCount > 0) { showToast(`Auto-selected Options ${uniqueLetters.join(', ')} from AI clipboard`); setLog(`AI Paste: Selected multiple choices ${uniqueLetters.join(', ')}`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(1200, true); } return; } } // 2. Direct single letter matching: Look for option letter (A through Z) const matchLetter = cleanText.match(/(?:^|\b)(?:answer|option|choice|the correct answer is)?\s*[:\-–*]*\s*([a-zA-Z])(?:\.|\)|:|\s|$)/i); if (matchLetter && matchLetter[1]) { const letter = matchLetter[1].toUpperCase(); const idx = letter.charCodeAt(0) - 65; if (inputElements[idx] && idx >= 0 && idx < inputElements.length) { safelySelectInput(inputElements[idx]); showToast(`Auto-selected Option ${letter} from AI clipboard`); setLog(`AI Paste: Selected Option ${letter} from clipboard`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(isCheckbox ? 1200 : 800, true); } return; } } // 2. Fuzzy text matching: match clipboard text with choice labels const normClip = normalizeText(cleanText); let bestInput = null; let bestScore = 0; let bestLabel = ''; inputElements.forEach((inp, idx) => { const container = inp.closest('div, li, label') || inp.parentElement; const choiceText = container ? container.innerText.trim() : ''; const normChoice = normalizeText(choiceText); if (!normChoice) return; if (normClip.includes(normChoice) || normChoice.includes(normClip)) { if (normChoice.length > bestScore) { bestScore = normChoice.length; bestInput = inp; bestLabel = String.fromCharCode(65 + idx); } } else { const cWords = normChoice.split(/\s+/).filter(w => w.length > 2); const matchedWords = cWords.filter(w => normClip.includes(w)); const score = matchedWords.length / Math.max(1, cWords.length); if (score > 0.6 && score > bestScore) { bestScore = score; bestInput = inp; bestLabel = String.fromCharCode(65 + idx); } } }); if (bestInput) { bestInput.click(); showToast(`Auto-selected ${bestLabel} from AI text match`); setLog(`AI Paste: Auto-matched choice ${bestLabel} from clipboard`, "var(--accent-green)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(800, true); } } else { showToast("Could not match AI response to any option. Select manually."); setLog("AI Paste: Clipboard did not clearly match any choice.", "var(--accent-amber)"); } } catch (err) { logDebug("Clipboard read error: " + err); showToast("Clipboard access denied or unavailable"); } } // ---------------------------------------------------- // Quiz & Exam Sequential Cycle Navigation (] and [) // ---------------------------------------------------- // Extracts ordered activities from Moodle's native .activity-navigation select (Jump to...) function getActivitiesFromUrlSelect() { if (typeof document === 'undefined') return []; const select = document.querySelector('.activity-navigation select, select.urlselect, form.urlselect select'); if (!select) return []; const options = Array.from(select.querySelectorAll('option')); const items = []; options.forEach((opt, idx) => { const val = (opt.value || '').trim(); const text = (opt.textContent || '').trim(); if (val && (val.includes('/mod/quiz/') || /\b(quiz|exam|assessment|prelim|midterm|prefinal|final)\b/i.test(text))) { const mId = val.match(/[?&]id=(\d+)/); items.push({ url: val, title: text, cmid: mId ? mId[1] : null, index: idx }); } }); return items; } // Resolves full ordered list of course quizzes from DOM, course index drawer, urlselect, or localStorage cache function getCourseQuizzesList() { if (typeof document === 'undefined') return []; const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const courseId = courseInfo && courseInfo.courseId ? courseInfo.courseId : ''; // 1. From Moodle 4 Course Index Drawer or Sidebar Nav const drawerSelectors = [ '#courseindex a[href*="/mod/quiz/"]', '.courseindex a[href*="/mod/quiz/"]', '[data-region="courseindex"] a[href*="/mod/quiz/"]', '#theme_boost-drawers-courseindex a[href*="/mod/quiz/"]', '.drawer-left a[href*="/mod/quiz/"]', '#courseindex a', '.courseindex a', '[data-region="courseindex"] a', '#theme_boost-drawers-courseindex a', '.drawer-left a', '.drawercontent a', '#nav-drawer a', '.block_navigation a', 'aside a' ]; const drawerLinks = Array.from(document.querySelectorAll(drawerSelectors.join(', '))); if (drawerLinks.length > 0) { const list = []; const seen = new Set(); drawerLinks.forEach(link => { const href = link.getAttribute('href') || link.href || ''; if (!href || href === '#' || href.startsWith('javascript:')) return; const titleElem = link.querySelector('.courseindex-name, .text-truncate, .instancename, .activityname') || link; const rawTitle = (titleElem ? (titleElem.innerText || titleElem.textContent) : (link.innerText || link.textContent) || '').trim(); const isQuizLink = href.includes('/mod/quiz/') || (/\b(?:quiz|exam)\b/i.test(rawTitle) && href.includes('view.php?id=')); if (!isQuizLink) return; let fullHref = href; try { fullHref = new URL(href, window.location.origin).href; } catch (_) {} const mId = fullHref.match(/[?&](?:id|cmid)=(\d+)/); const cmid = mId ? mId[1] : null; const key = cmid || fullHref; if (!seen.has(key)) { seen.add(key); const cleanTitle = rawTitle.replace(/\(Press\s*[\[\]]\)/gi, '').replace(/\s*Quiz\s*$/i, '').trim() || rawTitle; list.push({ url: fullHref, title: cleanTitle, cmid: cmid }); } }); if (list.length > 0) { if (courseId) { try { localStorage.setItem(`amaes_course_quizzes_${courseId}`, JSON.stringify(list)); } catch (_) {} } try { localStorage.setItem('amaes_latest_course_quizzes', JSON.stringify(list)); } catch (_) {} return list; } } // 2. From DOM if on course page (/course/view.php) if (checkIsCoursePage()) { const activities = Array.from(document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"]' )); const list = []; const seen = new Set(); activities.forEach(el => { const link = el.querySelector('a[href*="/mod/quiz/view.php?id="], a[href*="/mod/quiz/"]'); const isQuiz = (typeof classifyActivity === 'function' && classifyActivity(el).type === 'quiz') || Boolean(link); if (isQuiz && link) { const href = link.getAttribute('href') || link.href || ''; if (!href) return; let fullHref = href; try { fullHref = new URL(href, window.location.origin).href; } catch (_) {} const mId = fullHref.match(/[?&]id=(\d+)/); const cmid = mId ? mId[1] : null; const key = cmid || fullHref; if (!seen.has(key)) { seen.add(key); const titleElem = el.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const rawTitle = titleElem ? (titleElem.innerText || titleElem.textContent).trim() : (link.innerText.trim() || el.innerText.split('\n')[0]); const cleanTitle = rawTitle.replace(/\(Press\s*[\[\]]\)/gi, '').replace(/\s*Quiz\s*$/i, '').trim() || rawTitle; list.push({ url: fullHref, title: cleanTitle, cmid: cmid }); } } }); if (list.length > 0) { if (courseId) { try { localStorage.setItem(`amaes_course_quizzes_${courseId}`, JSON.stringify(list)); } catch (_) {} } try { localStorage.setItem('amaes_latest_course_quizzes', JSON.stringify(list)); } catch (_) {} return list; } } // 3. From urlselect dropdown inside .activity-navigation const selectItems = getActivitiesFromUrlSelect(); if (selectItems.length > 0) { if (courseId) { try { localStorage.setItem(`amaes_course_quizzes_${courseId}`, JSON.stringify(selectItems)); } catch (_) {} } try { localStorage.setItem('amaes_latest_course_quizzes', JSON.stringify(selectItems)); } catch (_) {} return selectItems; } // 4. From cached course quiz list in localStorage if (courseId) { try { const cached = localStorage.getItem(`amaes_course_quizzes_${courseId}`); if (cached) { const parsed = JSON.parse(cached); if (Array.isArray(parsed) && parsed.length > 0) return parsed; } } catch (_) {} } try { const cachedLatest = localStorage.getItem('amaes_latest_course_quizzes'); if (cachedLatest) { const parsed = JSON.parse(cachedLatest); if (Array.isArray(parsed) && parsed.length > 0) return parsed; } } catch (_) {} return []; } async function populateCourseQuizzesFromRemote(courseId) { if (!courseId || typeof window === 'undefined') return []; try { const semPath = typeof getSemesterBasePath === 'function' ? getSemesterBasePath() : '/'; const courseUrl = `${window.location.origin}${semPath}course/view.php?id=${courseId}`; const resp = await fetch(courseUrl); if (!resp.ok) return []; const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const drawerLinks = Array.from(doc.querySelectorAll( 'a[href*="/mod/quiz/view.php"], a[href*="/mod/quiz/"], .modtype_quiz a, li.activity.quiz a, #courseindex a, .courseindex a, [data-region="courseindex"] a, #theme_boost-drawers-courseindex a, li.activity a, .activity-item a' )); const list = []; const seen = new Set(); drawerLinks.forEach(link => { const href = link.getAttribute('href') || link.href || ''; if (!href || href === '#' || href.startsWith('javascript:')) return; const titleElem = link.querySelector('.courseindex-name, .text-truncate, .instancename, .activityname') || link; const rawTitle = (titleElem ? (titleElem.innerText || titleElem.textContent) : (link.innerText || link.textContent) || '').trim(); const isQuizLink = href.includes('/mod/quiz/') || (/\b(?:quiz|exam)\b/i.test(rawTitle) && href.includes('view.php?id=')); if (!isQuizLink) return; let fullHref = href; try { fullHref = new URL(href, window.location.origin).href; } catch (_) {} const mId = fullHref.match(/[?&](?:id|cmid)=(\d+)/); const cmid = mId ? mId[1] : null; const key = cmid || fullHref; if (!seen.has(key)) { seen.add(key); const cleanTitle = rawTitle.replace(/\(Press\s*[\[\]]\)/gi, '').replace(/\s*Quiz\s*$/i, '').trim() || rawTitle; list.push({ url: fullHref, title: cleanTitle, cmid }); } }); if (list.length > 0) { try { localStorage.setItem(`amaes_course_quizzes_${courseId}`, JSON.stringify(list)); localStorage.setItem('amaes_latest_course_quizzes', JSON.stringify(list)); } catch (_) {} if (typeof injectQuizBottomNavigation === 'function') { injectQuizBottomNavigation(); } } return list; } catch (_) { return []; } } function getCurrentQuizCmid() { if (typeof window === 'undefined') return ''; const search = window.location.search || ''; const m = search.match(/[?&]id=(\d+)/) || search.match(/[?&]cmid=(\d+)/); if (m) return m[1]; const idInput = document.querySelector('input[name="cmid"], input[name="id"]'); if (idInput && idInput.value) return idInput.value.trim(); const mBody = (document.body?.className || '').match(/\bcmid-(\d+)\b/); if (mBody) return mBody[1]; const viewLink = document.querySelector('a[href*="/mod/quiz/view.php?id="], a[href*="/mod/quiz/view.php?cmid="], .breadcrumb a[href*="id="], .breadcrumb-item a[href*="id="]'); if (viewLink) { const mLink = (viewLink.getAttribute('href') || viewLink.href || '').match(/[?&](?:id|cmid)=(\d+)/); if (mLink) return mLink[1]; } return ''; } // Helper to resolve native Moodle activity navigation link elements (handles both with ID and container with ) function resolveActivityLink(direction = 'next') { if (typeof document === 'undefined') return null; const isNext = direction === 'next'; const selectors = isNext ? [ 'a#next-activity-link', '#next-activity-link', '#next-activity-link a', '.activity-navigation [data-region="next-activity"] a', '.activity-navigation [data-region="next-activity"]', '.activity-navigation .pull-right a', '.activity-navigation .float-right a', '.activity-navigation .float-end a', '.activity-navigation .pull-right', '.activity-navigation .float-right', '.activity-navigation .float-end', 'a.next-activity-link', '.next-activity a', '.next-activity', 'a[title*="Next activity" i]', 'a[aria-label*="Next activity" i]' ] : [ 'a#prev-activity-link', '#prev-activity-link', '#prev-activity-link a', '.activity-navigation [data-region="prev-activity"] a', '.activity-navigation [data-region="prev-activity"]', '.activity-navigation .pull-left a', '.activity-navigation .float-left a', '.activity-navigation .float-start a', '.activity-navigation .pull-left', '.activity-navigation .float-left', '.activity-navigation .float-start', 'a.prev-activity-link', '.prev-activity a', '.prev-activity', 'a[title*="Previous activity" i]', 'a[aria-label*="Previous activity" i]' ]; for (const sel of selectors) { try { const el = document.querySelector(sel); if (!el) continue; if (el.tagName === 'A' && el.href) return el; const inner = el.querySelector('a[href]'); if (inner && inner.href) return inner; } catch (_) {} } // Semantic fallback: scan all anchor tags inside .activity-navigation const navContainer = document.querySelector('.activity-navigation'); if (navContainer && !navContainer.classList.contains('amaes-quiz-bottom-nav')) { const allAnchors = Array.from(navContainer.querySelectorAll('a[href]')); if (allAnchors.length > 0) { if (isNext) { const match = allAnchors.find(a => { const txt = (a.innerText || a.textContent || '').trim(); const html = a.innerHTML || ''; return /rarrow|fa-arrow-right|fa-chevron-right|[►▶>]|next/i.test(html + ' ' + txt); }) || (allAnchors.length > 1 ? allAnchors[allAnchors.length - 1] : (allAnchors[0].id.includes('next') ? allAnchors[0] : null)); if (match && match.href) return match; } else { const match = allAnchors.find(a => { const txt = (a.innerText || a.textContent || '').trim(); const html = a.innerHTML || ''; return /larrow|fa-arrow-left|fa-chevron-left|[◄◀<]|prev/i.test(html + ' ' + txt); }) || (allAnchors.length > 1 ? allAnchors[0] : (allAnchors[0].id.includes('prev') ? allAnchors[0] : null)); if (match && match.href) return match; } } } return null; } // Decorates native Moodle .activity-navigation buttons (#next-activity-link a / #prev-activity-link a) function decorateNativeActivityNavigation() { if (typeof document === 'undefined') return; // Next activity link: decorate with (Press ]) const nextLink = resolveActivityLink('next'); if (nextLink && !nextLink.dataset.amaesDecorated) { nextLink.dataset.amaesDecorated = 'true'; const rarrow = nextLink.querySelector('.rarrow, .icon, i, [aria-hidden="true"]'); const badge = '(Press ])'; if (rarrow) { rarrow.insertAdjacentHTML('beforebegin', badge); } else { nextLink.insertAdjacentHTML('beforeend', ' ' + badge); } } // Previous activity link: decorate with (Press [) const prevLink = resolveActivityLink('prev'); if (prevLink && !prevLink.dataset.amaesDecorated) { prevLink.dataset.amaesDecorated = 'true'; const larrow = prevLink.querySelector('.larrow, .icon, i, [aria-hidden="true"]'); const badge = '(Press [)'; if (larrow) { larrow.insertAdjacentHTML('afterend', badge); } else { prevLink.insertAdjacentHTML('afterbegin', badge + ' '); } } } // Injects visible bottom sequential cycle buttons for quizzes when native Moodle .activity-navigation is absent function injectQuizBottomNavigation() { if (typeof document === 'undefined') return; const isQuizPage = (typeof checkIsQuizViewPage === 'function' && checkIsQuizViewPage()) || (typeof checkIsReviewPage === 'function' && checkIsReviewPage()) || (typeof checkIsQuizSummaryPage === 'function' && checkIsQuizSummaryPage()) || (typeof window !== 'undefined' && window.location.pathname.includes('/mod/quiz/view.php')); if (!isQuizPage) return; // If native Moodle .activity-navigation exists with valid next or prev links (e.g. on exam portal), decorate it and avoid duplicate bar const hasNativeNext = Boolean(resolveActivityLink('next')); const hasNativePrev = Boolean(resolveActivityLink('prev')); if (hasNativeNext || hasNativePrev) { decorateNativeActivityNavigation(); return; } const target = document.querySelector('#region-main [role="main"], #region-main, .course-content, #page-content'); if (!target) return; const list = getCourseQuizzesList(); const currentCmid = getCurrentQuizCmid(); const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const courseId = courseInfo && courseInfo.courseId ? courseInfo.courseId : ''; // Determine current index in list let currentIndex = -1; if (currentCmid) { currentIndex = list.findIndex(item => item.cmid === currentCmid); } if (currentIndex === -1 && list.length > 0) { currentIndex = list.findIndex(item => item.url && window.location.href.includes(item.url)); } const skipPassed = typeof localStorage !== 'undefined' && localStorage.getItem('amaes_skip_passed_quizzes') === 'true'; let gradesMap = {}; if (typeof window !== 'undefined' && window.__amaesCourseGradesCache) { gradesMap = window.__amaesCourseGradesCache; } else if (courseId) { try { const cached = localStorage.getItem(`amaes_course_grades_cache_${courseId}`); if (cached) gradesMap = JSON.parse(cached); } catch (_) {} } // Find candidate for Next and Previous let nextQuiz = null; let prevQuiz = null; if (list.length > 0) { // Next const startIndexNext = currentIndex !== -1 ? currentIndex + 1 : 0; for (let i = startIndexNext; i < list.length; i++) { const candidate = list[i]; if (!candidate) continue; if (skipPassed) { const cmidKey = candidate.cmid ? `cmid_${candidate.cmid}` : ''; const normTitle = (candidate.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const gradeEntry = (cmidKey && gradesMap[cmidKey]) || gradesMap[normTitle] || null; if (gradeEntry && gradeEntry.isPassable === true) { continue; } } nextQuiz = candidate; break; } // Prev const startIndexPrev = currentIndex !== -1 ? currentIndex - 1 : list.length - 1; for (let i = startIndexPrev; i >= 0; i--) { const candidate = list[i]; if (!candidate) continue; if (skipPassed) { const cmidKey = candidate.cmid ? `cmid_${candidate.cmid}` : ''; const normTitle = (candidate.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const gradeEntry = (cmidKey && gradesMap[cmidKey]) || gradesMap[normTitle] || null; if (gradeEntry && gradeEntry.isPassable === true) { continue; } } prevQuiz = candidate; break; } } // If list is empty and courseId exists, trigger async fetch to populate if (list.length === 0 && courseId) { populateCourseQuizzesFromRemote(courseId); } const prevDisabled = list.length > 0 && !prevQuiz; const nextDisabled = list.length > 0 && !nextQuiz; const prevLabel = prevQuiz ? prevQuiz.title : (list.length > 0 ? 'First Quiz' : 'Previous Activity'); const nextLabel = nextQuiz ? nextQuiz.title : (list.length > 0 ? (skipPassed ? 'All Quizzes Passed' : 'Last Quiz') : 'Next Activity'); // Build Jump Menu Options let jumpOptions = ''; list.forEach((item, idx) => { const isSelected = idx === currentIndex; const cmidKey = item.cmid ? `cmid_${item.cmid}` : ''; const normTitle = (item.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const g = (cmidKey && gradesMap[cmidKey]) || gradesMap[normTitle] || null; let status = ''; if (g && g.hasGrade) { status = ` (${g.percentage !== null ? g.percentage + '%' : g.gradeStr}${g.isPassable ? ' - Passed' : ''})`; } jumpOptions += ``; }); const navData = { list, currentIndex, prevQuiz, nextQuiz, prevDisabled, nextDisabled, prevLabel, nextLabel, skipPassed, jumpOptions }; const isReviewPage = typeof checkIsReviewPage === 'function' && checkIsReviewPage(); function populateQuizNavElement(container, isTop) { if (isTop) { container.innerHTML = `
${isReviewPage ? ` ` : ''} ${list.length > 0 ? ` ` : `
Cycle Quizzes
`}
`; const btnPrev = container.querySelector('#amaes-top-btn-prev-quiz'); if (btnPrev && !prevDisabled) { btnPrev.addEventListener('click', (e) => { e.preventDefault(); cycleToPrevQuizOrExam(); }); } const btnNext = container.querySelector('#amaes-top-btn-next-quiz'); if (btnNext && !nextDisabled) { btnNext.addEventListener('click', (e) => { e.preventDefault(); cycleToNextQuizOrExam(); }); } const btnTopRetake = container.querySelector('#amaes-top-btn-nav-retake'); if (btnTopRetake) { btnTopRetake.addEventListener('click', (e) => { e.preventDefault(); triggerQuizRetake(); }); } const pillPrev = container.querySelector('#amaes-top-pill-prev'); if (pillPrev) { pillPrev.addEventListener('click', (e) => { e.preventDefault(); cycleToPrevQuizOrExam(); }); } const pillNext = container.querySelector('#amaes-top-pill-next'); if (pillNext) { pillNext.addEventListener('click', (e) => { e.preventDefault(); cycleToNextQuizOrExam(); }); } const selectJump = container.querySelector('#amaes-top-quiz-cycle-select'); if (selectJump) { selectJump.addEventListener('change', (e) => { const targetUrl = e.target.value; if (targetUrl) { showToast("Navigating to selected quiz...", 1500); window.location.href = targetUrl; } }); } } else { container.innerHTML = `
${isReviewPage ? ` ` : ''} ${list.length > 0 ? ` ` : `
Cycle Quizzes
`}
`; const btnPrev = container.querySelector('#amaes-btn-prev-quiz'); if (btnPrev && !prevDisabled) { btnPrev.addEventListener('click', (e) => { e.preventDefault(); cycleToPrevQuizOrExam(); }); } const btnNext = container.querySelector('#amaes-btn-next-quiz'); if (btnNext && !nextDisabled) { btnNext.addEventListener('click', (e) => { e.preventDefault(); cycleToNextQuizOrExam(); }); } const btnBottomRetake = container.querySelector('#amaes-btn-nav-retake'); if (btnBottomRetake) { btnBottomRetake.addEventListener('click', (e) => { e.preventDefault(); triggerQuizRetake(); }); } const pillPrev = container.querySelector('#amaes-pill-prev'); if (pillPrev) { pillPrev.addEventListener('click', (e) => { e.preventDefault(); cycleToPrevQuizOrExam(); }); } const pillNext = container.querySelector('#amaes-pill-next'); if (pillNext) { pillNext.addEventListener('click', (e) => { e.preventDefault(); cycleToNextQuizOrExam(); }); } const selectJump = container.querySelector('#amaes-quiz-cycle-select'); if (selectJump) { selectJump.addEventListener('change', (e) => { const targetUrl = e.target.value; if (targetUrl) { showToast("Navigating to selected quiz...", 1500); window.location.href = targetUrl; } }); } } } // 1. Inject Top Navigation Bar (above review questions / summary table so user navigates without scrolling) let topNav = document.getElementById('amaes-quiz-top-nav'); if (!topNav) { topNav = document.createElement('div'); topNav.id = 'amaes-quiz-top-nav'; topNav.className = 'amaes-quiz-top-nav activity-navigation'; const reviewBadge = document.getElementById('amaes-reviewable-page-badge'); const summaryTable = target.querySelector('.quizreviewsummary, table.generaltable, .quizattemptsummary'); const mainHeader = target.querySelector('h2, h3, .page-header-headings'); if (reviewBadge && reviewBadge.parentNode) { reviewBadge.parentNode.insertBefore(topNav, reviewBadge.nextSibling); } else if (summaryTable && summaryTable.parentNode) { summaryTable.parentNode.insertBefore(topNav, summaryTable); } else if (mainHeader && mainHeader.nextSibling) { mainHeader.parentNode.insertBefore(topNav, mainHeader.nextSibling); } else { target.insertBefore(topNav, target.firstChild); } } topNav.style.cssText = ` margin-top: 10px; margin-bottom: 16px; padding: 10px 16px; border: 1px solid rgba(148, 163, 184, 0.25); background: rgba(148, 163, 184, 0.05); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; user-select: none; `; populateQuizNavElement(topNav, true); // 2. Inject Bottom Navigation Bar let navContainer = document.getElementById('amaes-quiz-bottom-nav'); if (!navContainer) { navContainer = document.createElement('div'); navContainer.id = 'amaes-quiz-bottom-nav'; navContainer.className = 'amaes-quiz-bottom-nav activity-navigation'; target.appendChild(navContainer); } navContainer.style.cssText = ` margin-top: 36px; margin-bottom: 24px; padding: 16px 20px; border-top: 1px solid rgba(148, 163, 184, 0.25); background: rgba(148, 163, 184, 0.04); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; user-select: none; `; populateQuizNavElement(navContainer, false); } const injectQuizCycleNavigation = injectQuizBottomNavigation; // Cycles to the next or previous quiz/exam function cycleQuizOrExam(direction = 'next', opts = {}) { if (typeof window === 'undefined') return false; // 1. Strict Page Guard: Must ONLY operate on Moodle quiz, course, or exam pages const isExamHost = (typeof detectAssessmentOrigin === 'function' && detectAssessmentOrigin().isExam) || (typeof window !== 'undefined' && window.location.hostname.includes('exam')); const isAcademicPage = (typeof checkIsCoursePage === 'function' && checkIsCoursePage()) || (typeof checkIsQuizPage === 'function' && checkIsQuizPage()) || (typeof checkIsQuizViewPage === 'function' && checkIsQuizViewPage()) || (typeof checkIsReviewPage === 'function' && checkIsReviewPage()) || (typeof checkIsQuizSummaryPage === 'function' && checkIsQuizSummaryPage()) || (typeof checkIsQuizAttemptPage === 'function' && checkIsQuizAttemptPage()) || isExamHost; if (!isAcademicPage) return false; // 2. Resolve Native Activity Links (Exam Portal or standard Moodle .activity-navigation) const nativeTarget = resolveActivityLink(direction); if (isExamHost && nativeTarget && nativeTarget.href) { const rawTitle = nativeTarget.innerText || nativeTarget.textContent || (direction === 'next' ? 'Next Activity' : 'Previous Activity'); const title = rawTitle.replace(/\(Press\s*[\[\]]\)/gi, '').replace(/[►▶◄◀]/g, '').trim(); showToast(`${direction === 'next' ? 'Advancing to' : 'Returning to'}: ${title}`, 2000); setLog(`Exam Navigation: Cycling to ${title} via [${direction === 'next' ? ']' : '['}].`, "var(--accent-blue)"); window.location.href = nativeTarget.href; return true; } // 3. Quiz Portal Sequential Navigation const skipPassed = !isExamHost && ( opts.skipPassed !== undefined ? Boolean(opts.skipPassed) : (typeof localStorage !== 'undefined' && localStorage.getItem('amaes_skip_passed_quizzes') === 'true') ); let list = getCourseQuizzesList(); const currentCmid = getCurrentQuizCmid(); const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const courseId = courseInfo && courseInfo.courseId ? courseInfo.courseId : ''; // If list is empty, try native activity links or asynchronous remote populate and immediate recursion if (list.length === 0) { if (nativeTarget && nativeTarget.href) { const title = (nativeTarget.innerText || nativeTarget.textContent || (direction === 'next' ? 'Next Activity' : 'Previous Activity')) .replace(/\(Press\s*[\[\]]\)/gi, '').replace(/[►▶◄◀]/g, '').trim(); showToast(`Navigating to: ${title}`, 2000); window.location.href = nativeTarget.href; return true; } if (courseId) { showToast("Resolving course quizzes sequence...", 2000); populateCourseQuizzesFromRemote(courseId).then(remoteList => { if (remoteList && remoteList.length > 0) { cycleQuizOrExam(direction, opts); } else { showToast("No quizzes found in course navigation.", 2500); } }).catch(() => { showToast("Unable to resolve course quizzes.", 2500); }); return true; } else { showToast("Please visit course home page once to map all quizzes.", 3000); return false; } } // Determine current index in list let currentIndex = -1; if (currentCmid) { currentIndex = list.findIndex(item => item.cmid === currentCmid); } if (currentIndex === -1 && list.length > 0) { currentIndex = list.findIndex(item => item.url && window.location.href.includes(item.url)); } // If list is available, traverse in sequence if (list.length > 0) { let gradesMap = {}; if (skipPassed) { if (typeof window !== 'undefined' && window.__amaesCourseGradesCache) { gradesMap = window.__amaesCourseGradesCache; } else if (courseId) { try { const cachedGrades = localStorage.getItem(`amaes_course_grades_cache_${courseId}`); if (cachedGrades) gradesMap = JSON.parse(cachedGrades); } catch (_) {} } } const step = direction === 'next' ? 1 : -1; let startIndex = currentIndex !== -1 ? currentIndex + step : (direction === 'next' ? 0 : list.length - 1); let targetQuiz = null; for (let i = startIndex; direction === 'next' ? i < list.length : i >= 0; i += step) { const candidate = list[i]; if (!candidate) continue; if (skipPassed) { const cmidKey = candidate.cmid ? `cmid_${candidate.cmid}` : ''; const normTitle = (candidate.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const gradeEntry = (cmidKey && gradesMap[cmidKey]) || gradesMap[normTitle] || null; // If student already has >=80% on this quiz, skip over it if (gradeEntry && gradeEntry.isPassable === true) { continue; } } targetQuiz = candidate; break; } if (targetQuiz && targetQuiz.url) { const toastMsg = skipPassed ? `Cycling to unpassed quiz: ${targetQuiz.title}` : `Cycling to: ${targetQuiz.title}`; showToast(toastMsg, 2000); setLog(`Quiz Navigation: Cycling to ${targetQuiz.title} via [${direction === 'next' ? ']' : '['}].`, "var(--accent-blue)"); window.location.href = targetQuiz.url; return true; } if (skipPassed) { showToast("All subsequent quizzes have passing scores (≥80%).", 2500); setLog("Quiz Navigation: All subsequent quizzes passed (≥80%).", "var(--accent-green)"); return false; } else { showToast(direction === 'next' ? "Reached the end of course quizzes." : "Reached the beginning of course quizzes.", 2000); return false; } } // 4. Fallback: Native link if (nativeTarget && nativeTarget.href) { window.location.href = nativeTarget.href; return true; } return false; } function cycleToNextQuizOrExam(opts = {}) { return cycleQuizOrExam('next', opts); } function cycleToPrevQuizOrExam(opts = {}) { return cycleQuizOrExam('prev', opts); } // Helper to resolve quiz landing URL (view.php?id=...) function resolveQuizViewUrl(cmid) { if (typeof window === 'undefined') return ''; const finishLink = document.querySelector('a[href*="/mod/quiz/view.php?id="], a[href*="/mod/quiz/view.php?cmid="], .submitbtns a[href*="view.php"], a.btn[href*="view.php"]'); if (finishLink && finishLink.getAttribute('href')) { const rawHref = finishLink.getAttribute('href'); if (rawHref && !rawHref.startsWith('#') && !rawHref.startsWith('javascript:')) { try { return new URL(rawHref, window.location.origin).toString(); } catch (_) {} } } const resolvedCmid = cmid || getCurrentQuizCmid(); if (resolvedCmid) { const basePath = window.location.pathname.includes('/mod/quiz/') ? window.location.pathname.replace(/\/mod\/quiz\/.*/, '/mod/quiz/view.php') : '/mod/quiz/view.php'; return `${window.location.origin}${basePath}?id=${encodeURIComponent(resolvedCmid)}`; } return ''; } // Initiates an immediate re-attempt of the current quiz async function triggerQuizRetake(options = {}) { if (typeof window === 'undefined') return; const viewUrl = resolveQuizViewUrl(); const cmid = getCurrentQuizCmid(); if (!viewUrl && !cmid) { if (typeof showToast === 'function') { showToast("Could not resolve quiz landing page to retake.", 3000); } return; } // Visual feedback on all retake buttons across page const retakeButtons = document.querySelectorAll('#amaes-btn-retake-quiz, #amaes-top-btn-nav-retake, #amaes-btn-nav-retake, .amaes-retake-nav-btn'); retakeButtons.forEach(btn => { btn.disabled = true; btn.style.opacity = '0.7'; btn.innerHTML = `${ICONS.rotateCcw || ''} Starting Retake...`; }); if (typeof showToast === 'function') { showToast("Initiating quiz retake...", 2000); } if (typeof setLog === 'function') { setLog("Initiating Retake Quiz...", "var(--accent-blue)"); } // Store auto-start flag in sessionStorage in case of navigation if (cmid) { try { sessionStorage.setItem('amaes_auto_start_attempt', String(cmid)); } catch (_) {} } // 1. Fast Path: Fetch view.php in background, extract startattempt form, and submit if (viewUrl) { try { const resp = await fetch(viewUrl, { credentials: 'same-origin' }); if (resp && resp.ok) { const htmlText = await resp.text(); const parser = new DOMParser(); const doc = parser.parseFromString(htmlText, 'text/html'); const startForm = doc.querySelector( 'form[action*="startattempt.php"], form[action*="attempt.php"], .quizstartbutton form' ); // If form has a password input, user must enter password on view.php so skip fast post const hasPassword = startForm && startForm.querySelector('input[type="password"]'); if (startForm && !hasPassword && startForm.getAttribute('action')) { const targetAction = new URL(startForm.getAttribute('action'), viewUrl).toString(); const submitForm = document.createElement('form'); submitForm.method = (startForm.getAttribute('method') || 'POST').toUpperCase(); submitForm.action = targetAction; submitForm.style.display = 'none'; Array.from(startForm.elements).forEach(el => { if (el.name && !el.disabled) { const hidden = document.createElement('input'); hidden.type = 'hidden'; hidden.name = el.name; hidden.value = el.value || ''; submitForm.appendChild(hidden); } }); document.body.appendChild(submitForm); if (typeof showToast === 'function') { showToast("Attempt initialized! Launching quiz...", 2000); } submitForm.submit(); return; } } } catch (err) { console.warn('[AMAES Toolkit] Fast retake background submission skipped, using direct landing auto-start:', err); } } // 2. Fallback Path: Navigate to view.php with amaes_auto_start=1 query param const fallbackTarget = viewUrl || (cmid ? `${window.location.origin}/mod/quiz/view.php?id=${encodeURIComponent(cmid)}` : ''); if (fallbackTarget) { try { const u = new URL(fallbackTarget, window.location.origin); u.searchParams.set('amaes_auto_start', '1'); window.location.href = u.toString(); } catch (_) { window.location.href = fallbackTarget; } } } // Auto-clicks attempt start button and auto-confirms attempt modal when navigating from retake function checkAutoStartAttemptOnLanding() { if (typeof window === 'undefined' || !window.location.pathname.includes('/mod/quiz/view.php')) return; const urlParams = new URLSearchParams(window.location.search); const hasAutoStartParam = urlParams.get('amaes_auto_start') === '1'; const sessionTargetCmid = sessionStorage.getItem('amaes_auto_start_attempt'); const currentCmid = getCurrentQuizCmid(); const shouldAutoStart = hasAutoStartParam || (sessionTargetCmid && (!currentCmid || sessionTargetCmid === currentCmid)); if (!shouldAutoStart) return; // Clean up flags immediately to prevent looping try { sessionStorage.removeItem('amaes_auto_start_attempt'); if (hasAutoStartParam) { urlParams.delete('amaes_auto_start'); const cleanQuery = urlParams.toString(); const cleanUrl = window.location.pathname + (cleanQuery ? '?' + cleanQuery : '') + window.location.hash; window.history.replaceState({}, '', cleanUrl); } } catch (_) {} if (typeof showToast === 'function') { showToast("Auto-starting retake attempt...", 2000); } if (typeof setLog === 'function') { setLog("Auto-starting quiz attempt on landing...", "var(--accent-blue)"); } const triggerAttempt = () => { const startBtn = document.querySelector( 'form[action*="startattempt.php"] button, form[action*="startattempt.php"] input[type="submit"], ' + 'form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], ' + '.quizstartbutton button, .quizstartbutton input[type="submit"], ' + '#region-main button.btn-primary, #region-main input.btn-primary' ); if (startBtn) { startBtn.click(); } // Immediately poll for confirmation dialogue (up to 30 attempts x 50ms = 1.5s) let attempts = 0; const confirmInterval = setInterval(() => { attempts++; const confirmModalBtn = document.querySelector( '.moodle-dialogue-bd button.btn-primary, .moodle-dialogue-bd input[type="submit"], ' + '.modal.show button.btn-primary, .modal.show input[type="submit"], ' + '.modal button.btn-primary[data-action="save"], input[type="submit"][value*="Start attempt" i], ' + 'button[data-action="save"], .confirmation-dialog button.btn-primary' ); if (confirmModalBtn) { clearInterval(confirmInterval); confirmModalBtn.click(); } else if (attempts >= 30) { clearInterval(confirmInterval); } }, 50); }; if (document.readyState !== 'complete' && document.readyState !== 'interactive') { document.addEventListener('DOMContentLoaded', triggerAttempt, { once: true }); } else { setTimeout(triggerAttempt, 100); } } // Injects dedicated Retake Quiz action bar directly below .quizreviewsummary table function injectReviewRetakeButton() { if (typeof document === 'undefined') return; if (typeof checkIsReviewPage === 'function' && !checkIsReviewPage()) return; let retakeBar = document.getElementById('amaes-review-retake-bar'); const viewUrl = resolveQuizViewUrl(); if (!retakeBar) { retakeBar = document.createElement('div'); retakeBar.id = 'amaes-review-retake-bar'; retakeBar.className = 'amaes-review-retake-bar'; retakeBar.style.cssText = ` margin: 14px 0 20px 0; padding: 12px 18px; background: rgba(15, 108, 191, 0.05); border: 1px solid rgba(15, 108, 191, 0.25); border-radius: 8px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 1px 3px rgba(15, 108, 191, 0.08); `; retakeBar.innerHTML = `
${ICONS.rotateCcw || ''} Ready for another attempt?
Retake this quiz immediately with fresh questions and verified answers.
${viewUrl ? ` Finish Review ` : ''}
`; // Injection position: strictly directly below .quizreviewsummary table const summaryTable = document.querySelector('.quizreviewsummary, table.quizreviewsummary'); if (summaryTable && summaryTable.parentNode) { summaryTable.parentNode.insertBefore(retakeBar, summaryTable.nextSibling); } else { const firstQue = document.querySelector('.que'); if (firstQue && firstQue.parentNode) { firstQue.parentNode.insertBefore(retakeBar, firstQue); } else { const topNav = document.getElementById('amaes-quiz-top-nav'); if (topNav && topNav.parentNode) { topNav.parentNode.insertBefore(retakeBar, topNav.nextSibling); } else { const targetArea = document.querySelector('#region-main, main, [role="main"]') || document.body; if (targetArea) { if (typeof targetArea.prepend === 'function') { targetArea.prepend(retakeBar); } else if (targetArea.firstChild) { targetArea.insertBefore(retakeBar, targetArea.firstChild); } else if (typeof targetArea.appendChild === 'function') { targetArea.appendChild(retakeBar); } } } } } const retakeBtn = retakeBar.querySelector('#amaes-btn-retake-quiz'); if (retakeBtn) { retakeBtn.addEventListener('click', (e) => { e.preventDefault(); triggerQuizRetake(); }); } } else { // Update finish review link if viewUrl changed or became available const finishLink = retakeBar.querySelector('#amaes-btn-finish-review-link'); if (finishLink && viewUrl && finishLink.getAttribute('href') !== viewUrl) { finishLink.setAttribute('href', viewUrl); } } } // Decorate Attempt quiz now / Re-attempt quiz button with key hint (Press A) function decorateQuizLandingAttemptButton() { if (typeof window === 'undefined' || !window.location.pathname.includes('/mod/quiz/view.php')) return; if (typeof checkAndAutoMarkDoneOnQuizLanding === 'function') { checkAndAutoMarkDoneOnQuizLanding(); } if (typeof checkAutoStartAttemptOnLanding === 'function') { checkAutoStartAttemptOnLanding(); } const startBtn = document.querySelector( 'form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], ' + 'form[action*="startattempt.php"] button, form[action*="startattempt.php"] input[type="submit"], ' + '.quizstartbutton button, .quizstartbutton input[type="submit"], ' + '#region-main button.btn-primary, #region-main input.btn-primary' ); if (!startBtn || startBtn.dataset.amaesShortcutBound) return; startBtn.dataset.amaesShortcutBound = 'true'; let hint = startBtn.querySelector('.amaes-attempt-key-hint'); if (!hint && startBtn.tagName === 'BUTTON') { hint = document.createElement('span'); hint.className = 'amaes-attempt-key-hint'; hint.style.cssText = 'margin-left: 8px; font-size: 11px; font-weight: 600; opacity: 0.9; padding: 2px 6px; border-radius: 4px; background: rgba(255,255,255,0.22); border: 1px solid rgba(255,255,255,0.35); display: inline-block; vertical-align: middle;'; hint.textContent = '(Press A)'; startBtn.appendChild(hint); } else if (!hint && startBtn.tagName === 'INPUT') { if (!startBtn.value.includes('(Press A)')) { startBtn.value = `${startBtn.value} (Press A)`; } } } // Toggleable Keyboard Navigation & Fast Shortcuts for Quiz function setupQuizKeyboardShortcuts() { decorateNativeActivityNavigation(); injectQuizBottomNavigation(); decorateQuizLandingAttemptButton(); if (typeof injectReviewRetakeButton === 'function') { injectReviewRetakeButton(); } if (typeof checkAutoStartAttemptOnLanding === 'function') { checkAutoStartAttemptOnLanding(); } window.addEventListener('keydown', (e) => { if (!enableKeyboardShortcuts) return; // Strict safety: ignore hotkeys ONLY when actively typing in text input fields (except Escape to dismiss modals) const active = document.activeElement; if (active) { const tag = active.tagName ? active.tagName.toUpperCase() : ''; const type = active.type ? active.type.toLowerCase() : ''; const isTextInput = (tag === 'INPUT' && !['radio', 'checkbox', 'button', 'submit', 'reset'].includes(type)) || tag === 'TEXTAREA' || active.isContentEditable; if (isTextInput && e.key !== 'Escape' && e.key !== 'Esc') return; } if (e.ctrlKey || e.altKey || e.metaKey) return; // Retake Quiz Shortcut on Review Pages (/mod/quiz/review.php): 'R', 'r' const isReviewPage = typeof checkIsReviewPage === 'function' && checkIsReviewPage(); if (isReviewPage && (e.key === 'r' || e.key === 'R')) { e.preventDefault(); if (typeof triggerQuizRetake === 'function') { showToast("Retaking quiz... (R)"); setLog("Triggered Retake Quiz via keyboard shortcut [R]", "var(--accent-green)"); triggerQuizRetake(); } return; } // Attempt / Re-attempt Quiz Shortcut on Quiz Landing Pages (/mod/quiz/view.php): 'A' or 'Enter' const isQuizLanding = typeof window !== 'undefined' && window.location.pathname.includes('/mod/quiz/view.php'); if (isQuizLanding && (e.key === 'a' || e.key === 'A' || e.key === 'Enter')) { const confirmModalBtn = document.querySelector( '.moodle-dialogue-bd button.btn-primary, .moodle-dialogue-bd input[type="submit"], ' + '.modal.show button.btn-primary, .modal.show input[type="submit"], ' + '.modal button.btn-primary[data-action="save"], input[type="submit"][value*="Start attempt" i], ' + 'button[data-action="save"], .confirmation-dialog button.btn-primary' ); if (confirmModalBtn) { e.preventDefault(); confirmModalBtn.click(); showToast("Confirming attempt..."); return; } const startBtn = document.querySelector( 'form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], ' + 'form[action*="startattempt.php"] button, form[action*="startattempt.php"] input[type="submit"], ' + '.quizstartbutton button, .quizstartbutton input[type="submit"], ' + '#region-main button.btn-primary, #region-main input.btn-primary' ); if (startBtn) { e.preventDefault(); showToast("Starting quiz attempt... (A)"); setLog("Triggered Attempt Quiz via keyboard shortcut [A]", "var(--accent-green)"); startBtn.click(); // Immediately auto-confirm attempt modal if Moodle pops it up let attempts = 0; const confirmInterval = setInterval(() => { attempts++; const mBtn = document.querySelector( '.moodle-dialogue-bd button.btn-primary, .moodle-dialogue-bd input[type="submit"], ' + '.modal.show button.btn-primary, .modal.show input[type="submit"], ' + '.modal button.btn-primary[data-action="save"], input[type="submit"][value*="Start attempt" i], ' + 'button[data-action="save"], .confirmation-dialog button.btn-primary' ); if (mBtn) { clearInterval(confirmInterval); mBtn.click(); } else if (attempts >= 25) { clearInterval(confirmInterval); } }, 40); return; } } // Course page shortcut: 'U' highlights missing or unanswered quizzes const isCoursePage = typeof window !== 'undefined' && window.location.pathname.includes('/course/view.php'); if (isCoursePage && (e.key === 'u' || e.key === 'U')) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); if (typeof highlightMissingOrUnansweredQuizzes === 'function') { showToast("Scanning and highlighting missing quizzes... (U)"); highlightMissingOrUnansweredQuizzes(); } return; } // Global Escape Handler: First press closes open Help/Contribute modal, subsequent press minimizes/toggles toolkit panel if (e.key === 'Escape' || e.key === 'Esc') { const welcomeModal = document.getElementById('amaes-welcome-modal'); const contributeModal = document.getElementById('amaes-contribute-modal'); const devModal = document.getElementById('amaes-dev-unlock-modal'); const geminiModal = document.getElementById('amaes-gemini-modal'); const bugModal = document.getElementById('amaes-bug-modal'); if (welcomeModal || contributeModal || devModal || geminiModal || bugModal) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); if (welcomeModal) welcomeModal.remove(); if (contributeModal) contributeModal.remove(); if (devModal) devModal.remove(); if (geminiModal) geminiModal.remove(); if (bugModal) bugModal.remove(); showToast("Closed Modal (Esc)"); setLog("Modal closed via Esc shortcut.", "var(--accent-blue)"); return; } // If no modal open, minimize or expand the toolkit panel const panel = document.getElementById('amaes-toolkit-panel'); const bodyEl = document.getElementById('amaes-panel-body'); const lockOverlay = document.getElementById('amaes-panel-lock-overlay'); const minBtn = document.getElementById('amaes-min-btn'); const isLocked = localStorage.getItem('amaes_terms_acknowledged') !== 'true'; const targetEl = (isLocked && lockOverlay) ? lockOverlay : bodyEl; if (bodyEl) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); if (targetEl && targetEl.style.display !== 'none') { if (bodyEl) bodyEl.style.display = 'none'; if (lockOverlay) lockOverlay.style.display = 'none'; if (panel) panel.classList.add('amaes-minimized'); if (minBtn) { minBtn.innerHTML = ` `; } localStorage.setItem('amaes_pref_minimized', 'true'); showToast("Toolkit Minimized (Esc)"); setLog("Toolkit panel minimized via Esc shortcut.", "var(--accent-blue)"); } else { if (isLocked) { if (lockOverlay) lockOverlay.style.display = 'flex'; if (bodyEl) bodyEl.style.display = 'none'; } else { if (lockOverlay) lockOverlay.style.display = 'none'; if (bodyEl) bodyEl.style.display = 'block'; } if (panel) panel.classList.remove('amaes-minimized'); if (minBtn) minBtn.innerHTML = ICONS.minimize; localStorage.setItem('amaes_pref_minimized', 'false'); showToast("Toolkit Expanded (Esc)"); setLog("Toolkit panel expanded via Esc shortcut.", "var(--accent-green)"); } return; } } // Global Help & Shortcuts Cheatsheet: '?' (Shift+/) or 'K' if (e.key === '?' || (e.key === '/' && e.shiftKey) || e.key === 'k' || e.key === 'K') { e.preventDefault(); showWelcomeOnboardingModal(true); setTimeout(() => { const sec = document.getElementById('welcome-shortcuts-section'); if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 80); showToast("Shortcuts Cheatsheet (?)"); setLog("Keyboard shortcut triggered: Shortcuts Cheatsheet (?)", "var(--accent-blue)"); return; } // Developer Diagnostic Console: Triple Backtick (```) if (e.key === '`' || e.code === 'Backquote') { if (document.getElementById('amaes-welcome-modal')) { // Modal is open; handled exclusively by the modal's keydown listener to prevent double-toggle race conditions return; } globalBacktickCount++; clearTimeout(globalBacktickTimer); globalBacktickTimer = setTimeout(() => { globalBacktickCount = 0; }, 1200); if (globalBacktickCount >= 3) { globalBacktickCount = 0; e.preventDefault(); toggleDeveloperConsole(); return; } } // Pause / Start Auto-Quiz or Auto-Submit: 'P' if (e.key === 'p' || e.key === 'P') { if (checkIsQuizSummaryPage()) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); pauseQuizSummaryAutoSubmit(); return; } e.preventDefault(); toggleAutoQuizMode(); return; } // Quiz Summary page shortcuts: N / Space / Enter to hasten countdown or submit; B to return to attempt if (checkIsQuizSummaryPage()) { const key = e.key ? e.key.toUpperCase() : ''; if (key === 'N' || key === ' ' || key === 'ENTER') { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); hastenQuizSummaryAutoSubmit(); return; } if (key === 'B') { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); const returnBtn = document.querySelector( 'form[action*="attempt.php"] input[type="submit"], ' + 'form[action*="attempt.php"] button, ' + '.singlebutton a[href*="attempt.php"], ' + 'a[href*="attempt.php"]' ); if (returnBtn) { showToast("Returning to attempt... (B)"); setLog("Keyboard shortcut triggered: Return to attempt (B)", "var(--accent-blue)"); returnBtn.click(); } else { showToast("No return to attempt button found.", 1800); } return; } } // Quiz & Exam Sequential Navigation Shortcuts: ']' (Next) and '[' (Previous) if (e.key === ']' || e.key === '[' || e.code === 'BracketRight' || e.code === 'BracketLeft') { const isBracketNext = e.key === ']' || e.code === 'BracketRight'; const isBracketPrev = e.key === '[' || e.code === 'BracketLeft'; const isExamHost = (typeof detectAssessmentOrigin === 'function' && detectAssessmentOrigin().isExam) || (typeof window !== 'undefined' && window.location.hostname.includes('exam')); const isAcademicPage = (typeof checkIsCoursePage === 'function' && checkIsCoursePage()) || (typeof checkIsQuizPage === 'function' && checkIsQuizPage()) || (typeof checkIsQuizViewPage === 'function' && checkIsQuizViewPage()) || (typeof checkIsReviewPage === 'function' && checkIsReviewPage()) || (typeof checkIsQuizSummaryPage === 'function' && checkIsQuizSummaryPage()) || (typeof checkIsQuizAttemptPage === 'function' && checkIsQuizAttemptPage()) || isExamHost; if (isAcademicPage && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); if (isBracketNext) { cycleToNextQuizOrExam(); } else { cycleToPrevQuizOrExam(); } return; } } if (checkIsQuizSummaryPage()) return; if (!checkIsQuizAttemptPage()) return; const key = e.key ? e.key.toUpperCase() : ''; // 0. Jump to First Unanswered Question: 'U' if (key === 'U') { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); const unansweredQue = findFirstUnansweredOnCurrentPage(); if (unansweredQue) { setActiveQuestion(unansweredQue, true); unansweredQue.scrollIntoView({ behavior: 'smooth', block: 'center' }); const firstInteractive = unansweredQue.querySelector('input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])'); if (firstInteractive && typeof firstInteractive.focus === 'function') { firstInteractive.focus(); } unansweredQue.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease'; unansweredQue.style.boxShadow = '0 0 0 3px rgba(15, 108, 191, 0.4)'; setTimeout(() => { unansweredQue.style.boxShadow = ''; }, 1500); const qData = extractQuestionData(unansweredQue); showToast(`Focused Unanswered Question #${qData ? qData.qNum : ''} (U)`); setLog(`Shortcut [U]: Focused unanswered Question #${qData ? qData.qNum : ''}`, "var(--accent-blue)"); } else { showToast("All questions on this page are answered! Press N to advance.", 2200); } return; } // Backward Navigation: 'B' if (key === 'B') { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); const currentFocus = userSelectedActiveQuestion || getActiveViewportQuestion(); const prevOnPage = findPreviousQuestionOnCurrentPage(currentFocus); if (prevOnPage) { setActiveQuestion(prevOnPage, true); prevOnPage.scrollIntoView({ behavior: 'smooth', block: 'center' }); const qData = extractQuestionData(prevOnPage); showToast(`Back: Question #${qData ? qData.qNum : ''} (B)`, 1500); setLog(`Shortcut: Back to Question #${qData ? qData.qNum : ''}`, "var(--accent-blue)"); return; } const prevBtn = findQuizPrevButton(); if (prevBtn) { showToast("Shortcut: Previous Page (B)"); setLog("Keyboard shortcut triggered: Previous Page (B)", "var(--accent-blue)"); prevBtn.click(); return; } else { showToast("Already on the first question/page (B)", 1500); } return; } // 1. Next Question / Next Page / Submit Navigation: 'N' or 'Space' or 'Enter' if (key === 'N' || key === ' ' || key === 'ENTER') { const currentFocus = userSelectedActiveQuestion || getActiveViewportQuestion(); const nextOnPage = findNextUnansweredOnCurrentPage(currentFocus); if (nextOnPage) { e.preventDefault(); if (active && typeof active.blur === 'function') active.blur(); setActiveQuestion(nextOnPage, true); nextOnPage.scrollIntoView({ behavior: 'smooth', block: 'center' }); const qData = extractQuestionData(nextOnPage); showToast(`Next: Question #${qData ? qData.qNum : ''}`, 1500); setLog(`Shortcut: Advance to Question #${qData ? qData.qNum : ''}`, "var(--accent-blue)"); if (autoQuizMode) { runAutoQuizSolver(); } return; } const nextBtn = findQuizNextButton(); if (nextBtn) { e.preventDefault(); if (active && typeof active.blur === 'function') { active.blur(); } showToast("Shortcut: Next Page"); setLog("Keyboard shortcut triggered: Next Page", "var(--accent-blue)"); nextBtn.click(); } return; } // 2. Copy Current Question for AI: 'C' if (key === 'C') { const btnCopy = document.getElementById('btn-copy-curr-q'); if (btnCopy) { e.preventDefault(); btnCopy.click(); } else { const que = getActiveViewportQuestion(); if (que) { e.preventDefault(); const text = formatQuestionForAI(que, aiPromptHint); copyQuestionWithOptionalImage(que, text).then((res) => { showToast(res && res.withImage ? "Shortcut: Visual snippet & question copied for AI" : "Shortcut: Question copied for AI"); }).catch(() => {}); } } return; } // 3. Paste AI Answer & Auto-Select: 'V' if (key === 'V') { e.preventDefault(); autoSelectFromAiClipboard(); return; } // 5. Highlight Database Answers: 'H' if (key === 'H') { e.preventDefault(); const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const cached = getCachedAnswers(subCode); if (cached && cached.length > 0) { const res = highlightQuizAnswers(cached, false, true); showToast(`Highlighted ${res.matched}/${res.total} questions!`); setLog(`Shortcut [H]: Highlighted ${res.matched}/${res.total} questions.`, "var(--accent-green)"); } else if (typeof autoFetchCloudAnswersIfMissing === 'function') { showToast("Checking cloud database for answers..."); autoFetchCloudAnswersIfMissing(subCode).then(ok => { if (ok) { const fresh = getCachedAnswers(subCode); const res = highlightQuizAnswers(fresh, false, true); showToast(`Highlighted ${res.matched}/${res.total} questions from cloud!`); } else { showToast(`No answers found in database for ${subCode}`); } }); } else { showToast(`No answers cached for ${subCode}`); } return; } // 6. Select Choice Option: 1-4 or A-D let optIndex = -1; if (key >= '1' && key <= '9') { optIndex = parseInt(key, 10) - 1; } else if (['A', 'B', 'C', 'D'].includes(key)) { optIndex = key.charCodeAt(0) - 65; } if (optIndex >= 0) { // Target question currently in view or first unanswered const targetQue = getActiveViewportQuestion(); if (targetQue) { const choices = Array.from(targetQue.querySelectorAll('.answer input[type="radio"], .answer input[type="checkbox"]')); if (choices[optIndex]) { e.preventDefault(); const isCheckbox = choices[optIndex].type === 'checkbox'; choices[optIndex].click(); const choiceLabel = key >= '1' && key <= '9' ? String.fromCharCode(65 + optIndex) : key; showToast(`Shortcut: Selected Choice ${choiceLabel}`); setLog(`Keyboard shortcut: Selected choice ${choiceLabel}`, "var(--accent-blue)"); if (autoNextQuiz) { scheduleAutoNextAfterAnswer(isCheckbox ? 1400 : 800, true); } } } } }); } // Setup Passive User Action & Breadcrumb Listeners for Bug Reproduction function setupBreadcrumbListeners() { if (typeof document === 'undefined') return; document.addEventListener('click', (e) => { try { const target = e.target; if (!target) return; // 1. Toolkit button / control click const toolkitBtn = target.closest('#amaes-panel button, #amaes-panel a, #amaes-panel input, #amaes-panel select'); if (toolkitBtn) { const label = toolkitBtn.title || toolkitBtn.innerText || toolkitBtn.id || toolkitBtn.name || toolkitBtn.tagName; recordBreadcrumb('user_ui', `Clicked toolkit control: ${String(label).trim().slice(0, 50)}`); return; } // 2. Question option click (radio, checkbox, button inside .que) const que = target.closest('.que'); if (que) { const numElem = que.querySelector('.info .no, .qno'); const qNum = numElem ? numElem.innerText.replace(/\s+/g, ' ').trim() : 'Question'; if (target.matches('input[type="radio"], input[type="checkbox"]')) { const optText = target.closest('.answer, label')?.innerText?.slice(0, 40) || target.value; recordBreadcrumb('user_quiz', `Selected choice on ${qNum}`, String(optText).replace(/\s+/g, ' ').trim()); } else if (target.matches('button, a, .submitbtns input')) { recordBreadcrumb('user_quiz', `Clicked button on ${qNum}: ${(target.value || target.innerText || '').slice(0, 30)}`); } return; } // 3. Navigation / Submission button click const navBtn = target.closest('.submitbtns input, #mod_quiz-next-nav, .mod_quiz-next-nav, a[href*="attempt.php"], a[href*="review.php"]'); if (navBtn) { try { syncCurrentPageSelectionsToEvidence(); } catch (_) {} const text = navBtn.value || navBtn.innerText || navBtn.title || 'Navigation'; recordBreadcrumb('navigation', `User clicked navigation: ${String(text).trim().slice(0, 40)}`); } } catch (_) {} }, true); window.addEventListener('beforeunload', () => { try { syncCurrentPageSelectionsToEvidence(); } catch (_) {} }); document.addEventListener('change', (e) => { try { const target = e.target; if (!target) return; const que = target.closest('.que'); if (que && target.matches('select')) { const numElem = que.querySelector('.info .no, .qno'); const qNum = numElem ? numElem.innerText.replace(/\s+/g, ' ').trim() : 'Question'; const selectedText = target.options[target.selectedIndex]?.text || target.value; recordBreadcrumb('user_quiz', `Selected dropdown option on ${qNum}`, String(selectedText).slice(0, 40)); } } catch (_) {} }, true); } // Match questions & auto-highlight / auto-select on Moodle Quiz let knownWrongAnswerWarningListenersBound = false; function getMatchingRowAnswerEvidence(questionsDb, subQuestionText) { if (!subQuestionText) return { candidates: [], wrongAnswers: [], hasConflict: false }; const rowPromptKey = normalizeQuestionMatchKey(subQuestionText); const candidates = questionsDb.filter(candidate => { const candidatePrompt = candidate.qRaw || candidate.question || candidate.qNorm || ''; return rowPromptKey && normalizeQuestionMatchKey(candidatePrompt) === rowPromptKey; }); const wrongAnswers = []; candidates.forEach(candidate => { if (!(candidate.wrongAnswerEvidence === true || candidate.evidenceType === 'moodle_review_elimination') || !Array.isArray(candidate.wrongAnswers)) return; candidate.wrongAnswers.forEach(wrong => { const text = typeof wrong === 'string' ? wrong : wrong && (wrong.text || wrong.answer); const norm = typeof wrong === 'string' ? normalizeChoice(wrong) : (wrong && (wrong.norm || normalizeChoice(text || ''))); if (norm && text && !wrongAnswers.some(item => item.norm === norm)) { wrongAnswers.push({ norm, text }); } }); }); const validCandidates = candidates.filter(candidate => { const answer = candidate.ansRaw || candidate.answer || ''; const answerNorm = candidate.ansNorm || normalizeChoice(answer); return answerNorm && !wrongAnswers.some(wrong => wrong.norm === answerNorm || unscriptDigits(wrong.norm) === unscriptDigits(answerNorm)); }).sort((left, right) => { const priority = candidate => { if (isConfirmedCandidate(candidate)) return 1000 + (candidate.confirmations || 1); if (String(candidate.source || '').toLowerCase().includes('community')) return 100 + (candidate.confirmations || 1); if (getStudyGuideInfo(candidate)) return 50; return 0; }; return priority(right) - priority(left); }); const verifiedCandidates = validCandidates.filter(isConfirmedCandidate); const candidatesForConflictCheck = verifiedCandidates.length > 0 ? verifiedCandidates : validCandidates; const answerKeys = new Set(candidatesForConflictCheck.map(candidate => normalizeChoice(candidate.ansNorm || candidate.ansRaw || candidate.answer || '') ).filter(Boolean)); return { candidates: validCandidates, wrongAnswers, hasConflict: answerKeys.size > 1 }; } function updateKnownWrongAnswerWarning(que) { if (!que) return; let knownWrongAnswers = []; try { knownWrongAnswers = JSON.parse(que.dataset.amaesKnownWrongAnswers || '[]'); } catch (error) { console.warn('Could not read known-wrong-answer evidence for quiz question:', error); return; } que.querySelectorAll('.amaes-known-wrong-selection').forEach(el => { el.classList.remove('amaes-known-wrong-selection'); }); const wrongValues = new Map(knownWrongAnswers .filter(item => item && item.norm && item.text) .map(item => [item.norm, item.text])); const matchedWrongValues = new Map(); const inputs = que.querySelectorAll( '.answer input[type="radio"]:checked, .answer input[type="checkbox"]:checked, select, ' + 'input[type="text"], input.form-control, textarea' ); inputs.forEach(input => { let value = ''; if (input.matches('select')) { const option = input.selectedOptions && input.selectedOptions[0]; if (option && option.value && !option.text.toLowerCase().includes('choose')) value = option.text; } else if (input.matches('input[type="radio"], input[type="checkbox"]')) { value = cleanDOMToAI(input.closest('.r0, .r1, li, tr, label') || input.parentElement); } else { value = input.value || ''; } let inputWrongValues = wrongValues; if (input.dataset && input.dataset.amaesKnownWrongAnswers) { try { inputWrongValues = new Map(JSON.parse(input.dataset.amaesKnownWrongAnswers) .filter(item => item && item.norm && item.text) .map(item => [item.norm, item.text])); } catch (error) { console.warn('Could not read known-wrong-answer evidence for answer control:', error); return; } } const norm = normalizeChoice(value); const match = norm && inputWrongValues.has(norm) ? inputWrongValues.get(norm) : null; if (!match) return; matchedWrongValues.set(norm, match); input.classList.add('amaes-known-wrong-selection'); const choiceRow = input.closest('.r0, .r1, li, tr'); if (choiceRow) choiceRow.classList.add('amaes-known-wrong-selection'); }); let warning = que.querySelector('.amaes-known-wrong-answer-warning'); if (matchedWrongValues.size === 0) { if (warning) warning.remove(); return; } if (!warning) { warning = document.createElement('div'); warning.className = 'amaes-known-wrong-answer-warning'; warning.setAttribute('role', 'alert'); warning.setAttribute('aria-live', 'polite'); warning.style.cssText = 'margin: 6px 0; padding: 8px 10px; border-left: 4px solid #dc2626; border-radius: 4px; background: rgba(220,38,38,.1); color: #b91c1c; font-size: 12px; font-weight: 700;'; const formulation = que.querySelector('.formulation, .content') || que; formulation.insertBefore(warning, formulation.firstChild); } const answers = Array.from(matchedWrongValues.values()); warning.textContent = `Warning: ${answers.map(answer => `“${answer}”`).join(', ')} ${answers.length === 1 ? 'was' : 'were'} marked incorrect in a previous Moodle review. Change ${answers.length === 1 ? 'this answer' : 'these answers'} before continuing.`; } function bindKnownWrongAnswerWarningListeners() { if (knownWrongAnswerWarningListenersBound) return; knownWrongAnswerWarningListenersBound = true; const refreshWarning = event => { const que = event.target && event.target.closest ? event.target.closest('.que') : null; if (que) updateKnownWrongAnswerWarning(que); }; document.addEventListener('input', refreshWarning, true); document.addEventListener('change', refreshWarning, true); } function highlightQuizAnswers(questionsDb, autoSelect = false, isManualSelect = false) { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') { return { matched: 0, total: 0, error: "Toolkit locked" }; } if (!questionsDb || questionsDb.length === 0) { return { matched: 0, total: 0, error: "No cached questions found" }; } const queContainers = document.querySelectorAll('.que'); if (queContainers.length === 0) { return { matched: 0, total: 0, error: "Not on a quiz question page" }; } let matchedCount = 0; // Never run highlightQuizAnswers on review pages (which already show students' answers, feedback, and keys). // Prevents injecting fill buttons, green borders, or contradictory suggestions over graded results. if (checkIsReviewPage()) { return { matched: 0, total: queContainers.length, isReview: true }; } let probedThisPageCount = 0; const sCode = (detectCourseInfo().subjectCode) || 'GENERAL'; const quizSummary = typeof getQuizAttemptSummary === 'function' ? getQuizAttemptSummary(sCode) : null; const questionCandidateIndex = new Map(); questionsDb.forEach(item => { const key = normalizeQuestionMatchKey(item.qNorm || item.qRaw || item.question); if (!key) return; if (!questionCandidateIndex.has(key)) questionCandidateIndex.set(key, []); questionCandidateIndex.get(key).push(item); }); queContainers.forEach(que => { delete que.dataset.amaesStudyGuideFallbackPicked; que.classList.remove('amaes-no-answer-que'); if (identifyQuestionType(que) === 'unknown') { recordUnknownQuestionType(que); return; } const qtextElem = que.querySelector('.qtext, .formulation .qtext'); if (!qtextElem) return; // Redundant whole-card matching action button (.amaes-ai-matching-run-btn) is cleaned up // in favor of individual row '+ AI' buttons (allowAnswered: true) to eliminate button clutter and 15 RPM rate limit exhaustion. que.querySelectorAll('.amaes-ai-matching-run-btn').forEach(el => el.remove()); // Clone qtext and remove input, select, textarea, drop zones, and badges so inline blanks match AMAUOED entries cleanly const qClone = qtextElem.cloneNode(true); qClone.querySelectorAll('input, select, textarea, .drop, .draghome, .drags, .amaes-shortans-hint, .amaes-select-hint, .amaes-drag-hint, .amaes-unanswered-hint, .amaes-blockage-hud, .amaes-new-question-focus-banner, .amaes-unsure-help-chip, .amaes-known-wrong-answer-warning, .amaes-verified-badge, .amaes-probability-hint, .amaes-review-status-pill, .amaes-review-outcome-banner, .amaes-que-top-toolbar, .amaes-que-stop-btn, .amaes-ai-question-tag').forEach(el => el.remove()); const moodleQRaw = qClone.innerText.trim(); const moodleQNorm = normalizeText(moodleQRaw); const currentQuestionKey = normalizeQuestionMatchKey(moodleQNorm); let matchingCandidates = questionCandidateIndex.get(currentQuestionKey) || []; const exactCandidates = matchingCandidates; const matchingQuestionKeys = new Set(exactCandidates.length > 0 ? [currentQuestionKey] : []); if (exactCandidates.length === 0) { matchingCandidates = []; questionCandidateIndex.forEach((items, candidateKey) => { if (questionTextMatches(candidateKey, currentQuestionKey)) { matchingQuestionKeys.add(candidateKey); matchingCandidates.push(...items); } }); } const hasAmbiguousQuestionMatch = exactCandidates.length === 0 && matchingQuestionKeys.size > 1; let candidates = exactCandidates.length > 0 ? exactCandidates : (hasAmbiguousQuestionMatch ? [] : matchingCandidates); // Prioritize candidates when multiple match: // 1. By matching image asset token from the question DOM // 2. By verified candidate status if (candidates.length > 1) { const liveImgTokens = Array.from(que.querySelectorAll('.formulation img, .qtext img')).map(img => { const src = img.src || img.getAttribute('data-src') || ''; const m = src.match(/\/pluginfile\.php\/\d+\/question\/(?:answer|questiontext|feedback)\/\d+(?:\/\d+)?\/([^?#\s]+)/i); return m ? m[1].toLowerCase() : ''; }).filter(Boolean); if (liveImgTokens.length > 0) { const assetMatched = candidates.filter(c => { const qStr = (c.qRaw || c.question || '').toLowerCase(); return liveImgTokens.some(tok => qStr.includes(tok)); }); if (assetMatched.length > 0) { candidates = assetMatched; } } const confirmed = candidates.filter(c => isConfirmedCandidate(c)); if (confirmed.length > 0) { candidates = confirmed; } } const reviewEliminationCandidates = []; questionCandidateIndex.forEach((items, candidateKey) => { if (candidateKey === currentQuestionKey || !questionTextMatches(candidateKey, currentQuestionKey)) return; items.forEach(item => { const hasReviewElimination = item.wrongAnswerEvidence === true || item.evidenceType === 'moodle_review_elimination' || (Array.isArray(item.wrongAnswers) && item.wrongAnswers.some(w => w && typeof w === 'object' && Array.isArray(w.sources) && w.sources.some(source => source === 'Review' || source === 'moodle_review') )); if (hasReviewElimination && Array.isArray(item.wrongAnswers) && item.wrongAnswers.length > 0) { reviewEliminationCandidates.push(item); } }); }); if (reviewEliminationCandidates.length > 0) { candidates = Array.from(new Set([...candidates, ...reviewEliminationCandidates])); } const hasAnyVerifiedCandidate = candidates.some(item => isConfirmedCandidate(item)); // Clean up any prior hints que.querySelectorAll('.amaes-unanswered-hint, .amaes-known-wrong-answer-warning, .amaes-ambiguous-question-match-note, .amaes-study-guide-question-tag').forEach(b => b.remove()); if (hasAmbiguousQuestionMatch) { const formulation = que.querySelector('.formulation, .content') || que; const warning = document.createElement('div'); warning.className = 'amaes-ambiguous-question-match-note'; warning.style.cssText = 'margin-bottom: 8px; padding: 5px 9px; border-left: 3px solid #f59e0b; background: rgba(245, 158, 11, 0.1); color: #b45309; font-size: 10.5px;'; warning.textContent = 'Similar saved questions have different wording. Auto-answer is paused here to avoid using an answer from the wrong question.'; formulation.insertBefore(warning, formulation.firstChild); return; } if (candidates.length === 0) { if (checkIsQuizAttemptPage() && !que.querySelector('.amaes-new-question-focus-banner') && !que.querySelector('.amaes-blockage-hud')) { que.classList.add('amaes-no-answer-que'); const numElem = que.querySelector('.info .no, .qno'); const qNum = numElem ? numElem.innerText.replace(/[^0-9]/g, '') : ''; renderNewQuestionFocusBanner(que, qNum); updateNextButtonIndicator(); } else if (checkIsQuizAttemptPage()) { que.classList.add('amaes-no-answer-que'); } return; } // Verified evidence wins; AMAUOED and Jenny's Online share equal suggestion priority. candidates.sort((a, b) => { const priority = item => { if (isConfirmedCandidate(item)) return 1000 + (item.confirmations || 1); if (String(item.source || '').toLowerCase().includes('community')) return 100 + (item.confirmations || 1); if (getStudyGuideInfo(item)) return 50; return 0; }; return priority(b) - priority(a); }); // Clean up any prior highlighting or elimination badges on this question que.querySelectorAll('.amaes-highlighted-choice, .amaes-eliminated-choice, .amaes-ai-suggested-choice').forEach(el => { el.classList.remove('amaes-highlighted-choice'); el.classList.remove('amaes-eliminated-choice'); el.classList.remove('amaes-ai-suggested-choice'); el.style.outline = ''; el.style.backgroundColor = ''; el.style.borderRadius = ''; el.style.padding = ''; el.style.margin = ''; el.style.display = ''; el.style.alignItems = ''; el.style.flexWrap = ''; el.style.gap = ''; el.style.width = ''; el.style.boxSizing = ''; const lbl = el.querySelector('label') || el; if (lbl) { lbl.style.textDecoration = ''; lbl.style.opacity = ''; } }); que.querySelectorAll('select').forEach(sel => { sel.style.outline = ''; sel.style.backgroundColor = ''; sel.style.borderRadius = ''; }); que.querySelectorAll('.draghome, .dragitems .draghome, .drags .drag, .drags span, span.draghome, .dragboxes .drag, .dragitem, .drop, .dropzone, span.droptarget').forEach(el => { el.style.outline = ''; el.style.backgroundColor = ''; el.style.borderRadius = ''; }); que.querySelectorAll('.amaes-verified-badge, .amaes-unverified-badge, .amaes-eliminated-badge, .amaes-probability-hint, .amaes-shortans-hint, .amaes-select-hint, .amaes-select-elim-hint, .amaes-matching-row-conflict-note, .amaes-drag-hint, .amaes-unanswered-hint, .amaes-ai-matching-suggestion, .amaes-ai-suggested-badge, .amaes-ai-question-tag, .amaes-study-guide-question-tag, .amaes-pool-changed-hint').forEach(b => b.remove()); // Safety: collect all verified/confirmed answers for this question const verifiedNorms = new Set(); candidates.forEach(cand => { if (isConfirmedCandidate(cand)) { if (cand.ansNorm) verifiedNorms.add(cand.ansNorm); if (cand.ansRaw) verifiedNorms.add(normalizeChoice(cand.ansRaw)); if (Array.isArray(cand.answers)) { cand.answers.forEach(a => verifiedNorms.add(normalizeChoice(a))); } } }); // Target each choice row container cleanly (excluding rows that contain select dropdowns) let choiceRows = Array.from(que.querySelectorAll('.answer > div.r0, .answer > div.r1, .answer > div, .answer li, .answer tr')) .filter(row => !row.querySelector('select')); if (choiceRows.length === 0) { choiceRows = Array.from(que.querySelectorAll('.answer div.r0, .answer div.r1, .answer li, .answer tr')) .filter(row => !row.querySelector('select')); } if (choiceRows.length === 0) { choiceRows = Array.from(que.querySelectorAll('.answer label')) .filter(label => !label.querySelector('select') && !label.closest('tr')?.querySelector('select')); } // Inspect DOM for explicit Moodle checkmarks (e.g. on review page) choiceRows.forEach(row => { if (hasChoiceCheckmark(row)) { const label = row.querySelector('label') || row; const txt = normalizeChoice(cleanDOMToAI(label)); if (txt) verifiedNorms.add(txt); } }); // Inspect DOM for explicit Moodle red crosses or zero-mark selections (ground truth override!) const qGradeInfo = parseMoodleQuestionGrade(que); choiceRows.forEach(row => { const label = row.querySelector('label') || row; const txt = normalizeChoice(cleanDOMToAI(label)); const isCrossed = hasChoiceCross(row) || hasChoiceCross(label); const isCheckedZero = qGradeInfo.isZeroMark && ( row.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"]') !== null || row.classList.contains('selected') || (label && label.classList && label.classList.contains('selected')) ); if ((isCrossed || isCheckedZero) && txt) { // Moodle proved this choice is INCORRECT - remove from verifiedNorms! verifiedNorms.delete(txt); verifiedNorms.delete(unscriptDigits(txt)); } }); // Inspect DOM for explicit Moodle red crosses or zero marks on text inputs and dropdowns const liveTextInputs = que.querySelectorAll('input[type="text"], input.form-control, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); liveTextInputs.forEach(ti => { const val = (ti.value || ti.getAttribute('value') || '').trim(); const isCrossed = hasChoiceCross(ti) || hasChoiceCross(ti.parentElement) || qGradeInfo.isZeroMark; if ((isCrossed || qGradeInfo.isZeroMark) && val) { const norm = normalizeChoice(val); verifiedNorms.delete(norm); verifiedNorms.delete(unscriptDigits(norm)); } }); const liveSelectInputs = que.querySelectorAll('select'); liveSelectInputs.forEach(sel => { const opt = (sel.selectedIndex >= 0 && sel.options) ? sel.options[sel.selectedIndex] : null; const optText = (opt && opt.value && !opt.text.toLowerCase().includes('choose')) ? (opt.text || opt.innerText).trim() : ''; const isCrossed = hasChoiceCross(sel) || hasChoiceCross(sel.parentElement) || qGradeInfo.isZeroMark; if ((isCrossed || qGradeInfo.isZeroMark) && optText) { const norm = normalizeChoice(optText); verifiedNorms.delete(norm); verifiedNorms.delete(unscriptDigits(norm)); } }); // Compile all eliminated wrong choices known for this question const allWrongList = []; candidates.forEach(cand => { if (Array.isArray(cand.wrongAnswers) && cand.wrongAnswers.length > 0) { cand.wrongAnswers.forEach(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); const wCount = typeof w === 'object' && typeof w.count === 'number' ? w.count : 1; // Ground-truth guard: NEVER eliminate any answer known to be verified or checkmarked! if (wNorm && !verifiedNorms.has(wNorm) && !verifiedNorms.has(unscriptDigits(wNorm)) && !allWrongList.some(item => item.norm === wNorm || unscriptDigits(item.norm) === unscriptDigits(wNorm))) { allWrongList.push({ norm: wNorm, text: typeof w === 'string' ? w : w.text, count: wCount }); } }); } }); // Ground-truth override: Add live DOM crosses or zero-mark selections into allWrongList choiceRows.forEach(row => { const label = row.querySelector('label') || row; const txt = normalizeChoice(cleanDOMToAI(label)); const isCrossed = hasChoiceCross(row) || hasChoiceCross(label); const isCheckedZero = qGradeInfo.isZeroMark && ( row.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"]') !== null || row.classList.contains('selected') || (label && label.classList && label.classList.contains('selected')) ); if ((isCrossed || isCheckedZero) && txt) { if (!allWrongList.some(item => item.norm === txt || unscriptDigits(item.norm) === unscriptDigits(txt))) { const rawLabel = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); allWrongList.push({ norm: txt, text: rawLabel || txt, count: 1 }); } } }); liveTextInputs.forEach(ti => { const val = (ti.value || ti.getAttribute('value') || '').trim(); const isCrossed = hasChoiceCross(ti) || hasChoiceCross(ti.parentElement) || qGradeInfo.isZeroMark; if ((isCrossed || qGradeInfo.isZeroMark) && val) { const norm = normalizeChoice(val); if (!allWrongList.some(item => item.norm === norm || unscriptDigits(item.norm) === unscriptDigits(norm))) { allWrongList.push({ norm, text: val, count: 1 }); } } }); liveSelectInputs.forEach(sel => { const opt = (sel.selectedIndex >= 0 && sel.options) ? sel.options[sel.selectedIndex] : null; const optText = (opt && opt.value && !opt.text.toLowerCase().includes('choose')) ? (opt.text || opt.innerText).trim() : ''; const isCrossed = hasChoiceCross(sel) || hasChoiceCross(sel.parentElement) || qGradeInfo.isZeroMark; if ((isCrossed || qGradeInfo.isZeroMark) && optText) { const norm = normalizeChoice(optText); if (!allWrongList.some(item => item.norm === norm || unscriptDigits(item.norm) === unscriptDigits(norm))) { allWrongList.push({ norm, text: optText, count: 1 }); } } }); // Filter candidates: exclude any candidate whose answer was confirmed WRONG const validCandidates = candidates.filter(cand => { const ansText = cand.ansRaw || cand.answer || ''; const ansNorm = cand.ansNorm || normalizeChoice(ansText); if (!ansNorm) return false; const isConfirmedWrong = allWrongList.some(w => { const wNorm = normalizeChoice(w.norm || w.text || w); return wNorm === ansNorm || unscriptDigits(wNorm) === unscriptDigits(ansNorm); }); return !isConfirmedWrong; }); const studyGuideAnswerKeys = new Set(candidates .filter(candidate => Boolean(getStudyGuideInfo(candidate))) .map(candidate => (Array.isArray(candidate.answers) && candidate.answers.length > 0 ? candidate.answers.map(normalizeChoice).filter(Boolean).sort().join('|') : normalizeChoice(candidate.ansNorm || candidate.ansRaw || candidate.answer || ''))) .filter(Boolean)); const hasStudyGuideConflict = !hasAnyVerifiedCandidate && studyGuideAnswerKeys.size > 1; if (hasStudyGuideConflict) { const formulation = que.querySelector('.formulation, .content') || que; let conflictNote = que.querySelector('.amaes-study-guide-conflict-note'); if (!conflictNote) { conflictNote = document.createElement('div'); conflictNote.className = 'amaes-study-guide-conflict-note'; conflictNote.style.cssText = 'margin: 6px 0; padding: 6px 9px; border-left: 3px solid #f59e0b; background: rgba(245,158,11,.1); color: inherit; font-size: 11px;'; conflictNote.textContent = 'Study guides disagree on this answer. Auto-Pick is paused for this question; compare the source suggestions before choosing.'; formulation.insertBefore(conflictNote, formulation.firstChild); } } else { que.querySelector('.amaes-study-guide-conflict-note')?.remove(); } // Contradiction Guard: A multiple-choice question cannot have 100% of choices wrong! // If all choices are marked wrong, keep only those with higher failure counts, preserving at least 1 candidate. if (choiceRows.length >= 2 && allWrongList.length >= choiceRows.length) { if (typeof detectAndReportQuestionAnomaly === 'function') { detectAndReportQuestionAnomaly({ anomalyType: 'ALL_CHOICES_ELIMINATED', subjectCode: sCode, activityTitle: document.title || '', questionRaw: moodleQRaw, questionNorm: moodleQNorm, domType: identifyQuestionType(que) || 'multichoice', submittedAnswer: '', markScored: 0, maxMark: 1, attemptHistory: allWrongList.map(w => w.text || w.norm), dbCandidate: candidates[0] || null }); } allWrongList.sort((a, b) => (b.count || 1) - (a.count || 1)); allWrongList.splice(choiceRows.length - 1); } que.dataset.amaesKnownWrongAnswers = JSON.stringify(allWrongList .filter(item => item.norm && item.text) .map(item => ({ norm: normalizeChoice(item.norm), text: item.text }))); bindKnownWrongAnswerWarningListeners(); updateKnownWrongAnswerWarning(que); // ── Choice Pool Mismatch Detection ────────────────────────────────── // If the DB has stored choices for this question but NONE of them match the // live Moodle choices, it means the question pool was updated (new choices // swapped in). Warn the student and fall back to elimination-only mode. if (identifyQuestionType(que) !== 'match' && choiceRows.length >= 2 && candidates.length > 0) { const liveChoiceNorms = Array.from(choiceRows).map(r => { const lbl = r.querySelector('label') || r; return normalizeChoice(cleanDOMToAI(lbl)); }).filter(Boolean); // Check if candidate stored answer(s) appear in live choices (supporting multi-answer questions) const getCandidateSubAnswers = (cand) => { if (!cand) return []; return (Array.isArray(cand.answers) ? cand.answers : []) .concat((cand.ansRaw || '').split(/[,;&\n]+|\s+and\s+/i)) .concat((cand.answer || '').split(/[,;&\n]+|\s+and\s+/i)) .map(s => normalizeChoice(s)) .filter(Boolean); }; const topCand = candidates[0]; const storedChoices = Array.isArray(topCand.choices) ? topCand.choices : []; const storedAnsNorm = topCand.ansNorm || normalizeChoice(topCand.ansRaw || topCand.answer || ''); const topSubAnswers = getCandidateSubAnswers(topCand); const matchesLive = (targetNorm) => liveChoiceNorms.some(ln => ln === targetNorm || unscriptDigits(ln) === unscriptDigits(targetNorm) ); const topAnsInLive = topSubAnswers.length > 0 ? topSubAnswers.some(matchesLive) : matchesLive(storedAnsNorm); const anyCandInLive = topAnsInLive || candidates.some(cand => { if (!isConfirmedCandidate(cand)) return false; const subs = getCandidateSubAnswers(cand); const norm = cand.ansNorm || normalizeChoice(cand.ansRaw || cand.answer || ''); return subs.length > 0 ? subs.some(matchesLive) : matchesLive(norm); }); // Only flag if stored answer is non-empty AND none of the candidate answers match live choices const poolChanged = storedAnsNorm && !anyCandInLive && liveChoiceNorms.length > 0; if (poolChanged && !que.querySelector('.amaes-pool-changed-hint')) { const formulation = que.querySelector('.formulation, .content') || que; const warn = document.createElement('div'); warn.className = 'amaes-pool-changed-hint'; warn.style.cssText = ` display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px; padding: 5px 11px; background: rgba(245, 158, 11, 0.12); border: 1px solid rgba(245, 158, 11, 0.45); border-left: 3px solid #f59e0b; border-radius: 6px; font-size: 10.5px; color: #d97706; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; warn.innerHTML = ` POOL CHANGED Stored answer not in current choices — question pool may have been updated. Using elimination hints only. `; formulation.insertBefore(warn, formulation.firstChild); } } // ──────────────────────────────────────────────────────────────────── const isRadio = que.querySelector('.answer input[type="radio"]') !== null; let foundMatchForQuestion = false; // Preserve explicit probe/alternative flips if (que.dataset.amaesProbeFlipped === 'true') { foundMatchForQuestion = true; } // ── Adaptive Choice Rotation / Blind Probe Solver ────────────────── // When an unreviewed quiz attempt scored < 100%, rotate choices on suspect/unverified questions // across attempts to systematically explore the solution space until 100% is reached. const isAdaptiveProbeActive = (typeof adaptiveProbeQuiz === 'undefined' || adaptiveProbeQuiz !== false) && checkIsQuizAttemptPage(); const unreviewedEntry = typeof getUnreviewedAttemptEntry === 'function' ? getUnreviewedAttemptEntry(moodleQNorm, sCode) : null; const probeEntry = typeof getProbeEntry === 'function' ? getProbeEntry(moodleQNorm, sCode) : null; const lastAttemptPercentage = (unreviewedEntry && typeof unreviewedEntry.percentage === 'number') ? unreviewedEntry.percentage : (probeEntry && typeof probeEntry.lastPercentage === 'number' ? probeEntry.lastPercentage : (quizSummary && typeof quizSummary.percentage === 'number' ? quizSummary.percentage : null)); const hasPriorFailedAttempt = lastAttemptPercentage !== null && lastAttemptPercentage < 100; const isQuestionSuspect = !hasAnyVerifiedCandidate || hasPriorFailedAttempt; const hasQuestionLevelFailureEvidence = qGradeInfo.isZeroMark || Array.from(choiceRows).some(row => hasChoiceCross(row) || hasChoiceCross(row.querySelector('label'))); // Probe ONLY rotates on questions lacking a confirmed answer (never touches confirmed review/100% answers!) if (!hasAnyVerifiedCandidate && isQuestionSuspect && hasQuestionLevelFailureEvidence && isAdaptiveProbeActive && choiceRows.length >= 2 && !foundMatchForQuestion) { // If student took a prior attempt manually, Moodle's attemptonlast=1 may have pre-checked their prior choice const preCheckedRadio = que.querySelector('.answer input[type="radio"]:checked'); const preCheckedNorm = preCheckedRadio ? normalizeChoice(cleanDOMToAI(preCheckedRadio.closest('label') || preCheckedRadio.parentElement)) : null; const prevChoiceNorm = (unreviewedEntry && unreviewedEntry.ansNorm) || (probeEntry && probeEntry.lastNorm) || preCheckedNorm || (candidates.length > 0 ? (candidates[0].ansNorm || normalizeChoice(candidates[0].ansRaw || candidates[0].answer || '')) : null); const triedChoices = (probeEntry && Array.isArray(probeEntry.triedChoices)) ? probeEntry.triedChoices : (prevChoiceNorm ? [prevChoiceNorm] : []); // Check available valid (non-eliminated) rows const validRows = []; choiceRows.forEach(row => { const lbl = row.querySelector('label') || row; const inp = row.querySelector('input[type="radio"], input[type="checkbox"]'); const norm = normalizeChoice(cleanDOMToAI(lbl)); const isElim = isChoiceRowEliminated(row) || allWrongList.some(w => w.norm === norm || unscriptDigits(w.norm) === unscriptDigits(norm)); if (norm && !isElim) { validRows.push({ row, label: lbl, input: inp, norm, rawText: cleanDOMToAI(lbl).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim() }); } }); const budget = typeof adaptiveProbeBudget === 'number' ? adaptiveProbeBudget : 2; if (hasPriorFailedAttempt && validRows.length >= 2 && probedThisPageCount < budget) { let probeTarget = null; let isFlipped = false; if (validRows.length === 2) { // Binary True/False question: Flip to the opposite choice! const opposite = validRows.find(vr => vr.norm !== prevChoiceNorm && unscriptDigits(vr.norm) !== unscriptDigits(prevChoiceNorm)); if (opposite) { probeTarget = opposite; isFlipped = true; } } else { // Multiple choice (3+ choices): Pick an untried candidate row different from prevChoiceNorm const untried = validRows.find(vr => vr.norm !== prevChoiceNorm && !triedChoices.includes(vr.norm) && !triedChoices.includes(unscriptDigits(vr.norm))); if (untried) { probeTarget = untried; } else { probeTarget = validRows.find(vr => vr.norm !== prevChoiceNorm) || validRows[0]; } } if (probeTarget) { foundMatchForQuestion = true; probedThisPageCount++; que.dataset.amaesProbeFlipped = 'true'; const targetRow = probeTarget.row; targetRow.classList.add('amaes-adaptive-probe-choice'); targetRow.style.outline = '2px solid #a855f7'; targetRow.style.backgroundColor = 'rgba(168, 85, 247, 0.12)'; targetRow.style.boxShadow = '0 0 0 1px rgba(168, 85, 247, 0.25)'; targetRow.style.borderRadius = '6px'; targetRow.style.padding = '6px 12px'; targetRow.style.margin = '4px 0'; targetRow.style.display = 'flex'; targetRow.style.alignItems = 'center'; targetRow.style.flexWrap = 'wrap'; targetRow.style.gap = '8px'; targetRow.style.width = '100%'; targetRow.style.boxSizing = 'border-box'; targetRow.style.transition = 'all 0.2s ease'; const targetLbl = probeTarget.label; if (targetLbl && targetLbl !== targetRow) { targetLbl.style.outline = 'none'; targetLbl.style.backgroundColor = 'transparent'; targetLbl.style.padding = '0'; targetLbl.style.margin = '0'; targetLbl.style.display = 'inline-flex'; targetLbl.style.alignItems = 'center'; targetLbl.style.gap = '6px'; targetLbl.style.cursor = 'pointer'; } let badge = targetRow.querySelector('.amaes-adaptive-probe-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'amaes-adaptive-probe-badge'; badge.innerHTML = `${ICONS.rotateCcw || ''} Adaptive Probe (${isFlipped ? 'Flipped' : 'Rotated'})`; badge.style.cssText = ` background: #a855f7; color: #ffffff !important; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); white-space: nowrap; flex-shrink: 0; `; targetRow.appendChild(badge); } const canSelectAnswer = isManualSelect || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode)); if (canSelectAnswer && probeTarget.input && !probeTarget.input.checked) { safelySelectInput(probeTarget.input); } recordAttemptAnswerEvidence(que, probeTarget.rawText, 'adaptive_probe', { verified: false, isAdaptiveProbe: true }); setLog(`Adaptive Probe: Rotated choice on unverified question (prior attempt scored ${lastAttemptPercentage}%) to test alternative.`, '#a855f7'); } } } // ──────────────────────────────────────────────────────────────────── choiceRows.forEach(row => { if (row.querySelector('select')) return; // If it is a single-choice radio question and we already highlighted the top verified answer, avoid double-highlighting if (isRadio && foundMatchForQuestion) return; const label = row.querySelector('label') || row; const input = row.querySelector('input[type="radio"], input[type="checkbox"]'); const hasDomCheckmark = hasChoiceCheckmark(row); // Extract clean text without badges const choiceText = normalizeChoice(cleanDOMToAI(label)); const isCrossedRow = hasChoiceCross(row) || hasChoiceCross(label) || (qGradeInfo.isZeroMark && Boolean(row.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"]'))); const isVerifiedChoice = !isCrossedRow && (hasDomCheckmark || verifiedNorms.has(choiceText) || verifiedNorms.has(unscriptDigits(choiceText))); const isEliminatedChoice = isCrossedRow || (!isVerifiedChoice && allWrongList.some(w => w.norm === choiceText || unscriptDigits(w.norm) === unscriptDigits(choiceText))); // 1. Check against verified candidate answers (ONLY if NOT confirmed wrong!) if (!isEliminatedChoice) { let isChoiceMatch = false; let matchedCand = null; if (isVerifiedChoice) { isChoiceMatch = true; matchedCand = candidates.find(c => c.verified && (c.ansNorm === choiceText || unscriptDigits(c.ansNorm) === unscriptDigits(choiceText))) || candidates[0]; } if (!isChoiceMatch) { for (const cand of candidates) { // Never auto-pick a lower-trust AMAUOED/community answer // when a verified review answer exists for this question. if (hasAnyVerifiedCandidate && !isConfirmedCandidate(cand)) continue; const ansNorm = cand.ansNorm || normalizeChoice(cand.ansRaw || cand.answer || ''); if (!ansNorm) continue; if (choiceText === ansNorm) { isChoiceMatch = true; matchedCand = cand; break; } const subCandidates = (Array.isArray(cand.answers) ? cand.answers : []) .concat((cand.ansRaw || '').split(/[,;&\n]+|\s+and\s+/i)) .concat((cand.answer || '').split(/[,;&\n]+|\s+and\s+/i)) .map(s => normalizeChoice(s)) .filter(Boolean); if (subCandidates.includes(choiceText)) { isChoiceMatch = true; matchedCand = cand; break; } } } if (isChoiceMatch) { const cand = matchedCand || { source: 'Verified Database', verified: true, confirmations: 1 }; foundMatchForQuestion = true; const matchingSources = candidates.filter(candidate => { const answerNorms = [ candidate.ansNorm, candidate.ansRaw, ...(Array.isArray(candidate.answers) ? candidate.answers : []) ].filter(Boolean).map(normalizeChoice); return answerNorms.includes(choiceText) || answerNorms.includes(unscriptDigits(choiceText)); }); const sourceCandidates = matchingSources.length > 0 ? matchingSources : [cand]; const hasVerifiedSource = sourceCandidates.some(candidate => isConfirmedCandidate(candidate)); const hasCommunitySource = !hasVerifiedSource && sourceCandidates.some(candidate => Boolean((candidate.source || '').toLowerCase().includes('community') || (Array.isArray(candidate.sources) && candidate.sources.some(source => String(source).toLowerCase().includes('community')))) ); const hasStudyGuideSource = !hasVerifiedSource && sourceCandidates.some(candidate => Boolean(getStudyGuideInfo(candidate)) ); const hasAiSource = !hasVerifiedSource && !hasCommunitySource && !hasStudyGuideSource && sourceCandidates.some(candidate => Boolean(candidate.isAiSuggestion || (candidate.source || '').toLowerCase().includes('gemini') || (Array.isArray(candidate.sources) && candidate.sources.some(source => String(source).toLowerCase().includes('gemini')))) ); const studyGuide = hasStudyGuideSource ? getStudyGuideInfo(sourceCandidates.find(candidate => getStudyGuideInfo(candidate))) : (hasCommunitySource ? { label: 'Community', url: null } : null); const isStudyGuide = Boolean(studyGuide); const isDeduced = cand.deduced === true; let sourceColor = '#0284c7'; let sourceBg = 'rgba(2, 132, 199, 0.12)'; if (hasVerifiedSource) { sourceColor = '#10b981'; sourceBg = 'rgba(16, 185, 129, 0.14)'; } else if (hasAiSource) { sourceColor = '#8b5cf6'; sourceBg = 'rgba(139, 92, 246, 0.12)'; } const sourceLabels = []; if (hasVerifiedSource) { const verifiedItem = sourceCandidates.find(candidate => isConfirmedCandidate(candidate)); const originSuffix = (verifiedItem && verifiedItem.originLabel && verifiedItem.originLabel !== 'General') ? ` • From ${verifiedItem.originLabel}` : ''; sourceLabels.push(isDeduced ? `Deduced Answer${originSuffix}` : `Verified Answer${originSuffix}`); } if (hasCommunitySource) sourceLabels.push('Community Candidate (Unconfirmed)'); if (hasStudyGuideSource && studyGuide) { sourceLabels.push(`Web Study Guide (${studyGuide.label}, Unconfirmed)`); } if (hasAiSource) sourceLabels.push('AI Suggestion (Gemini)'); // Apply full row highlight on container const targetRow = row; targetRow.classList.add(hasAiSource ? 'amaes-ai-suggested-choice' : 'amaes-highlighted-choice'); if (hasAiSource) { setQuestionAiTag(que, true); } else if (hasVerifiedSource) { setQuestionAiTag(que, false); } else if (isStudyGuide) { setQuestionStudyGuideTag(que, studyGuide.label); } targetRow.style.outline = `2px solid ${sourceColor}`; targetRow.style.backgroundColor = sourceBg; targetRow.style.boxShadow = `0 0 0 1px ${sourceColor}33`; targetRow.style.borderRadius = '6px'; targetRow.style.padding = '6px 12px'; targetRow.style.margin = '4px 0'; targetRow.style.display = 'flex'; targetRow.style.alignItems = 'center'; targetRow.style.flexWrap = 'wrap'; targetRow.style.gap = '8px'; targetRow.style.width = '100%'; targetRow.style.boxSizing = 'border-box'; targetRow.style.transition = 'all 0.2s ease'; // Differentiate unconfirmed study-guide choice text from default white if (isStudyGuide) { targetRow.querySelectorAll('label, .answernumber, p, span').forEach(el => { if (!el.classList.contains('amaes-unverified-badge') && !el.closest('.amaes-unverified-badge')) { el.style.color = 'var(--accent-sky, #0284c7)'; } }); } // Reset inner label so it doesn't constrain width or wrap oddly if (label !== targetRow) { label.style.outline = 'none'; label.style.backgroundColor = 'transparent'; label.style.padding = '0'; label.style.margin = '0'; label.style.display = 'inline-flex'; label.style.alignItems = 'center'; label.style.gap = '6px'; label.style.cursor = 'pointer'; } // Prevent internal

tags from forcing line breaks targetRow.querySelectorAll('p').forEach(p => { p.style.display = 'inline'; p.style.margin = '0'; p.style.padding = '0'; }); // Add source badge if not already present let badge = targetRow.querySelector('.amaes-verified-badge, .amaes-ai-suggested-badge'); if (!badge) { badge = document.createElement(isStudyGuide && studyGuide.url ? 'a' : 'span'); badge.className = hasAiSource ? 'amaes-ai-suggested-badge' : (hasVerifiedSource ? 'amaes-verified-badge amaes-badge-db' : `amaes-unverified-badge ${hasStudyGuideSource ? 'amaes-badge-amauoed' : 'amaes-badge-db'}`); badge.innerHTML = sourceLabels.map(label => { const icon = label.startsWith('AI Suggestion') ? '' : (label.includes('Unconfirmed') ? ICONS.external : (isDeduced ? ICONS.lightbulb : ICONS.checkCircle)); return `${icon} ${label}`; }).join(' + '); if (isStudyGuide && studyGuide.url) { badge.href = studyGuide.url; badge.target = '_blank'; badge.rel = 'noopener noreferrer'; badge.title = `Source: ${studyGuide.label} — Click to open source`; badge.onclick = (e) => { e.stopPropagation(); }; } badge.style.cssText = ` background: ${isStudyGuide ? 'rgba(2, 132, 199, 0.15)' : sourceColor}; color: ${isStudyGuide ? '#0284c7 !important' : '#ffffff !important'}; border: ${isStudyGuide ? '1px solid rgba(2, 132, 199, 0.4)' : 'none'}; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; box-shadow: ${isStudyGuide ? 'none' : '0 1px 3px rgba(0,0,0,0.18)'}; white-space: nowrap; flex-shrink: 0; text-decoration: none; cursor: ${isStudyGuide && studyGuide.url ? 'pointer' : 'default'}; `; targetRow.appendChild(badge); } // USER OVERRIDE SAFETY: If user already selected a choice on this question, NEVER overwrite their decision! // EXCEPTION: When a verified/confirmed answer exists, auto-pick applies it even if a pre-checked radio exists from a prior manual attempt const canSelectAnswer = (isManualSelect && hasVerifiedSource) || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode) && (hasVerifiedSource || (hasAiSource && aiAutoSelect) || (isStudyGuide && autoQuizMode && autoPickStudyGuideFallback))); const anyRadioChecked = isRadio && Boolean(que.querySelector('.answer input[type="radio"]:checked')); const shouldSelect = canSelectAnswer && input && !input.checked && (!anyRadioChecked || hasVerifiedSource || isManualSelect); if (shouldSelect) { safelySelectInput(input); const choiceTextClean = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); recordAttemptAnswerEvidence(que, choiceTextClean, hasAiSource ? 'ai_inference' : (hasVerifiedSource ? 'verified_db' : 'study_guide_suggestion'), { verified: hasVerifiedSource, isAdaptiveProbe: false }); } if (isStudyGuide && autoQuizMode && autoPickStudyGuideFallback && Boolean(autoSelect) && input && input.checked) { que.dataset.amaesStudyGuideFallbackPicked = 'true'; } return; } } // 2. Check if choice is confirmed WRONG (elimination) if (!isVerifiedChoice && (isEliminatedChoice || !foundMatchForQuestion) && allWrongList.length > 0) { const matchedWrong = allWrongList.find(w => w.norm === choiceText || unscriptDigits(w.norm) === unscriptDigits(choiceText)); if (matchedWrong) { const targetRow = row; targetRow.classList.add('amaes-eliminated-choice'); targetRow.style.outline = '1.5px solid #ef4444'; targetRow.style.backgroundColor = 'rgba(239, 68, 68, 0.12)'; targetRow.style.boxShadow = '0 0 0 1px rgba(239, 68, 68, 0.18)'; targetRow.style.borderRadius = '6px'; targetRow.style.padding = '5px 10px'; targetRow.style.margin = '4px 0'; targetRow.style.display = 'flex'; targetRow.style.alignItems = 'center'; targetRow.style.flexWrap = 'wrap'; targetRow.style.gap = '8px'; targetRow.style.width = '100%'; targetRow.style.boxSizing = 'border-box'; targetRow.style.transition = 'all 0.2s ease'; if (label !== targetRow) { label.style.textDecoration = 'line-through'; label.style.opacity = '0.6'; } let badge = targetRow.querySelector('.amaes-eliminated-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'amaes-eliminated-badge'; const countText = 'Incorrect Choice'; badge.innerHTML = `${ICONS.xCircle} ${countText}`; badge.title = `Attempt or classmate history confirmed this choice is incorrect`; badge.style.cssText = ` background: #ef4444; color: #ffffff; font-size: 9.5px; font-weight: 700; padding: 2px 7px; border-radius: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.15); white-space: nowrap; flex-shrink: 0; `; targetRow.appendChild(badge); } // Auto-uncheck eliminated checkbox if student or previous attempt pre-checked it if (input && input.type === 'checkbox' && input.checked && (autoQuizMode || autoPickQuiz || isManualSelect) && hasAnyVerifiedCandidate) { try { input.click(); } catch (_) {} if (input.checked) input.checked = false; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } } } }); // 3. Real-Time Deduction by Elimination: If all choices except 1 are eliminated, deduce the remaining 1 as correct! if (!foundMatchForQuestion && choiceRows.length >= 2) { const uneliminated = Array.from(choiceRows).filter(r => !r.classList.contains('amaes-eliminated-choice')); if (uneliminated.length === 1) { const deducedRow = uneliminated[0]; const deducedLabel = deducedRow.querySelector('label') || deducedRow; const deducedInput = deducedRow.querySelector('input[type="radio"], input[type="checkbox"]'); foundMatchForQuestion = true; deducedRow.classList.add('amaes-highlighted-choice'); deducedRow.style.outline = '2px solid #10b981'; deducedRow.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; deducedRow.style.borderRadius = '6px'; deducedRow.style.padding = '6px 12px'; deducedRow.style.margin = '4px 0'; deducedRow.style.display = 'flex'; deducedRow.style.alignItems = 'center'; deducedRow.style.flexWrap = 'wrap'; deducedRow.style.gap = '8px'; deducedRow.style.width = '100%'; deducedRow.style.boxSizing = 'border-box'; let badge = deducedRow.querySelector('.amaes-verified-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'amaes-verified-badge amaes-badge-db'; badge.innerHTML = `${ICONS.lightbulb} Deduced Answer`; badge.style.cssText = ` background: linear-gradient(135deg, #10b981, #059669); color: #ffffff; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); white-space: nowrap; flex-shrink: 0; `; deducedRow.appendChild(badge); } const canSelectAnswer = isManualSelect || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode)); const anyDeducedRadioChecked = Boolean(que.querySelector('.answer input[type="radio"]:checked')); if (canSelectAnswer && deducedInput && !deducedInput.checked && (!anyDeducedRadioChecked || isManualSelect)) { safelySelectInput(deducedInput); } // Auto-save the deduced answer into course cache for permanent verification! const deducedText = cleanDOMToAI(deducedLabel).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (deducedText) { const courseInfo = detectCourseInfo(); const sCode = courseInfo.subjectCode || 'GENERAL'; mergeAnswersIntoCache(sCode, [{ qRaw: moodleQRaw, qNorm: moodleQNorm, ansRaw: deducedText, ansNorm: normalizeChoice(deducedText), choices: Array.from(choiceRows).map(r => cleanDOMToAI(r.querySelector('label') || r)), verified: true, deduced: true, source: 'Elimination Deduction', evidenceType: 'moodle_review_elimination' }], 'Elimination Deduction'); } } else if (uneliminated.length > 1 && uneliminated.length < choiceRows.length) { // Partial elimination: display remaining probability based on elimination count const elimPct = Math.round(100 / uneliminated.length); uneliminated.forEach(candRow => { candRow.style.outline = '1.5px dashed #0284c7'; candRow.style.backgroundColor = 'rgba(2, 132, 199, 0.07)'; candRow.style.borderRadius = '6px'; if (!candRow.querySelector('.amaes-probability-hint')) { const pHint = document.createElement('span'); pHint.className = 'amaes-probability-hint'; pHint.style.cssText = ` font-size: 9.5px; color: #38bdf8; background: rgba(56, 189, 248, 0.15); border: 1px solid rgba(56, 189, 248, 0.35); padding: 2px 6px; border-radius: 4px; margin-left: auto; font-weight: 700; display: inline-flex; align-items: center; gap: 4px; `; pHint.title = `${uneliminated.length} of ${choiceRows.length} choices remain after elimination`; pHint.innerHTML = `${ICONS.target} ~${elimPct}% chance`; candRow.appendChild(pHint); } }); } } // 4. Session AI Cache: Reapply previously AI-solved answer if returning to this question! if (!foundMatchForQuestion && choiceRows.length >= 2) { const qData = extractQuestionData(que); const cachedAi = getCachedAiAnswer(qData); if (cachedAi && cachedAi.choiceText) { const matchedAi = matchAiAnswerToChoice(cachedAi.choiceText, que, qData); if (matchedAi && matchedAi.row && !isChoiceRowEliminated(matchedAi.row)) { foundMatchForQuestion = true; applyAiChoiceHighlight(matchedAi.row); const canSelectAnswer = isManualSelect || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode) && aiAutoSelect); const anyRadioChecked = Boolean(que.querySelector('.answer input[type="radio"]:checked')); if (canSelectAnswer && matchedAi.input && !matchedAi.input.checked && (!anyRadioChecked || isManualSelect)) { safelySelectInput(matchedAi.input); } } } } // Handle Short Answer / Text inputs (both standard and inline cloze inputs) if (!foundMatchForQuestion) { const textInputs = que.querySelectorAll('input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); if (textInputs.length > 0 && validCandidates.length > 0 && !checkIsReviewPage()) { // Exclude multiple-choice, True/False, and choice-letter candidates from text inputs const shortAnsCandidates = validCandidates.filter(cand => { if (cand.questionType && ['multichoice', 'match', 'ddwtos'].includes(cand.questionType)) return false; if (cand.questionType === 'gapselect' && Array.isArray(cand.choices) && cand.choices.length > 0) return false; if (Array.isArray(cand.choices) && cand.choices.length > 1) return false; const raw = String(cand.ansRaw || cand.answer || '').trim(); if (/^[a-d][.)]\s*(true|false)$/i.test(raw)) return false; if (/^(true|false)$/i.test(raw)) return false; if (/^[a-d][.)]?$/i.test(raw)) return false; return true; }); if (shortAnsCandidates.length === 0) { if (typeof detectAndReportQuestionAnomaly === 'function') { detectAndReportQuestionAnomaly({ anomalyType: 'TYPE_MISMATCH_LEAK', subjectCode: sCode, activityTitle: document.title || '', questionRaw: moodleQRaw, questionNorm: moodleQNorm, domType: 'shortanswer', submittedAnswer: '', markScored: 0, maxMark: 1, attemptHistory: [], dbCandidate: validCandidates[0] || null }); } return; } const bestCand = shortAnsCandidates[0]; const bestAnswer = bestCand ? (bestCand.ansRaw || bestCand.answer || '') : ''; const studyGuide = isConfirmedCandidate(bestCand) ? null : (getStudyGuideInfo(bestCand) || (bestCand && Boolean(bestCand.source || '').toLowerCase().includes('community') ? { label: 'Community', url: null } : null)); const isStudyGuide = Boolean(studyGuide); const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const amauoedUrl = getStoredAmauoedUrl(subCode) || 'https://amauoed.com/courses'; const sourceColor = isStudyGuide ? '#0284c7' : (bestCand && isConfirmedCandidate(bestCand) ? '#10b981' : '#3b82f6'); const sourceBg = isStudyGuide ? 'rgba(2, 132, 199, 0.1)' : (bestCand && isConfirmedCandidate(bestCand) ? 'rgba(16, 185, 129, 0.1)' : 'rgba(59, 130, 246, 0.1)'); const guideUrl = studyGuide && studyGuide.url ? studyGuide.url : amauoedUrl; const sourceTitle = studyGuide ? `Suggested (${studyGuide.label}, unconfirmed):` : isConfirmedCandidate(bestCand) ? 'Verified Answer:' : 'Unconfirmed Candidate:'; // Parse potential multi-blank answers if multiple inputs exist in the question let candAnswers = []; if (Array.isArray(bestCand.answers) && bestCand.answers.length > 0) { candAnswers = bestCand.answers.map(s => cleanFillBlankAnswer(s)).filter(Boolean); } else if (textInputs.length > 1 && (bestAnswer.includes(',') || bestAnswer.includes('\n') || bestAnswer.includes(';') || bestAnswer.includes('|'))) { candAnswers = bestAnswer.split(/[\n,;|]+/).map(s => cleanFillBlankAnswer(s)).filter(Boolean); } else { candAnswers = [cleanFillBlankAnswer(bestAnswer)].filter(Boolean); } textInputs.forEach((textInput, idx) => { const rawTarget = textInputs.length === 1 ? (candAnswers[0] || bestAnswer || '') : (candAnswers[idx] || ''); const targetAns = cleanFillBlankAnswer(rawTarget); if (!targetAns) return; textInput.style.outline = `2px solid ${sourceColor}`; textInput.style.backgroundColor = sourceBg; textInput.style.borderRadius = '4px'; let hint = que.querySelector(`.amaes-shortans-hint[data-input-idx="${idx}"]`); if (!hint) { hint = document.createElement('div'); hint.className = 'amaes-shortans-hint'; hint.setAttribute('data-input-idx', String(idx)); hint.innerHTML = `

${sourceTitle} ${targetAns}
`; hint.style.cssText = `font-size: 11px; margin-top: 5px; margin-bottom: 3px; padding: 5px 9px; background: ${sourceBg}; border-left: 3px solid ${sourceColor}; border-radius: 4px; cursor: pointer; transition: background 0.15s;`; hint.title = `Click to auto-fill "${targetAns}" into answer field`; const fillFn = (e) => { if (e && e.target && e.target.closest('a')) return; if (e) { e.preventDefault(); e.stopPropagation(); } setInputValueWithEvents(textInput, targetAns); showToast(`Filled: ${targetAns}`); hint.style.background = 'rgba(16, 185, 129, 0.2)'; const btn = hint.querySelector('.amaes-fill-btn'); if (btn) btn.innerText = 'Filled'; }; hint.onclick = fillFn; const fillBtn = hint.querySelector('.amaes-fill-btn'); if (fillBtn) fillBtn.onclick = fillFn; // Insert hint directly after input container or into question if (textInput.parentElement && textInput.parentElement !== que) { textInput.parentElement.appendChild(hint); } else { textInput.insertAdjacentElement('afterend', hint); } } // Auto-fill when autoPickQuiz is enabled and auto-quiz is running, or user triggered manual select const canAutoFill = (isManualSelect && isConfirmedCandidate(bestCand)) || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode) && (isConfirmedCandidate(bestCand) || (isStudyGuide && autoQuizMode && autoPickStudyGuideFallback))); const currentInputNorm = normalizeChoice(textInput.value || ''); const currentInputIsKnownWrong = currentInputNorm && allWrongList.some(w => normalizeChoice(w.norm || w.text || w) === currentInputNorm ); if (canAutoFill && (!textInput.value || currentInputIsKnownWrong)) { setInputValueWithEvents(textInput, targetAns); hint.style.background = 'rgba(16, 185, 129, 0.2)'; const btn = hint.querySelector('.amaes-fill-btn'); if (btn) btn.innerText = 'Filled'; } }); foundMatchForQuestion = true; } } // Handle Dropdown / Select elements (matching questions table, cloze / gapselect dropdowns) if (!foundMatchForQuestion || que.querySelectorAll('select').length > 0) { const selectInputs = que.querySelectorAll('select'); const matchingRows = Array.from(selectInputs).map(selectInput => { const row = selectInput.closest('tr, .r0, .r1, [class*="r"], .answer > div, li') || selectInput.parentElement; const cells = row ? Array.from(row.querySelectorAll('td, th')).filter(td => !td.querySelector('select')) : []; let textCell = row && row.querySelector('td.text, td:first-child'); if (textCell && /^\d+[\s:.)-]*$/.test(textCell.innerText.trim()) && cells.length > 1) { const promptCell = cells.find(c => c.innerText.trim().length > 2 && !/^\d+[\s:.)-]*$/.test(c.innerText.trim())); if (promptCell) textCell = promptCell; } let subQuestion = textCell ? textCell.innerText.trim() : ''; if (!subQuestion && row) { const clone = row.cloneNode(true); clone.querySelectorAll('select, button, .amaes-ai-match-row-btn, .amaes-select-hint').forEach(e => e.remove()); subQuestion = clone.innerText.trim(); } subQuestion = subQuestion.replace(/^(?:(?:\d+|[a-zA-Z])[\s:.)-]+)/, '').trim(); return { selectInput, row, subQuestion }; }).filter(item => item.subQuestion); const hasMatchingRows = matchingRows.length > 0; if (selectInputs.length > 0 && (validCandidates.length > 0 || hasMatchingRows) && !checkIsReviewPage()) { const bestCand = validCandidates[0] || null; if (!bestCand && !hasMatchingRows) return; const bestAnswer = bestCand ? (bestCand.ansRaw || bestCand.answer || '') : ''; const studyGuide = bestCand && !isConfirmedCandidate(bestCand) ? (getStudyGuideInfo(bestCand) || (bestCand && Boolean(bestCand.source || '').toLowerCase().includes('community') ? { label: 'Community', url: null } : null)) : null; const isStudyGuide = Boolean(studyGuide); const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const amauoedUrl = getStoredAmauoedUrl(subCode) || 'https://amauoed.com/courses'; const sourceColor = isStudyGuide ? '#0284c7' : (isConfirmedCandidate(bestCand) ? '#10b981' : '#3b82f6'); const sourceBg = isStudyGuide ? 'rgba(2, 132, 199, 0.1)' : (isConfirmedCandidate(bestCand) ? 'rgba(16, 185, 129, 0.1)' : 'rgba(59, 130, 246, 0.1)'); const guideUrl = studyGuide && studyGuide.url ? studyGuide.url : amauoedUrl; const sourceTitle = studyGuide ? `Suggested (${studyGuide.label}, unconfirmed):` : bestCand && isConfirmedCandidate(bestCand) ? 'Verified Answer:' : 'Unconfirmed Candidate:'; const candAnswers = (bestCand && bestCand.answers && bestCand.answers.length > 0) ? bestCand.answers : (bestAnswer.includes(',') && selectInputs.length > 1 ? bestAnswer.split(',').map(s => s.trim()) : [bestAnswer]); let matchedAnySelect = false; selectInputs.forEach((selectInput, idx) => { // Check if dropdown is inside a matching table row with sub-question text const row = selectInput.closest('tr, .r0, .r1, [class*="r"], .answer > div, li') || selectInput.parentElement; const cells = row ? Array.from(row.querySelectorAll('td, th')).filter(td => !td.querySelector('select')) : []; let rowTextElem = row ? row.querySelector('td.text, td:first-child') : null; if (rowTextElem && /^\d+[\s:.)-]*$/.test(rowTextElem.innerText.trim()) && cells.length > 1) { const promptCell = cells.find(c => c.innerText.trim().length > 2 && !/^\d+[\s:.)-]*$/.test(c.innerText.trim())); if (promptCell) rowTextElem = promptCell; } let subQText = rowTextElem ? rowTextElem.innerText.trim() : ''; if (!subQText && row) { const clone = row.cloneNode(true); clone.querySelectorAll('select, button, .amaes-ai-match-row-btn, .amaes-select-hint').forEach(e => e.remove()); subQText = clone.innerText.trim(); } subQText = subQText.replace(/^(?:(?:\d+|[a-zA-Z])[\s:.)-]+)/, '').trim(); const rowEvidence = hasMatchingRows ? getMatchingRowAnswerEvidence(questionsDb, subQText) : { candidates: [], wrongAnswers: [], hasConflict: false }; const rowWrongAnswers = rowEvidence.wrongAnswers; const hasRowAnswerConflict = rowEvidence.hasConflict; const rowCandidate = hasMatchingRows && !hasRowAnswerConflict ? (rowEvidence.candidates[0] || null) : null; if (selectInput.dataset) { if (hasMatchingRows) { selectInput.dataset.amaesKnownWrongAnswers = JSON.stringify(rowWrongAnswers); } else { delete selectInput.dataset.amaesKnownWrongAnswers; } } if (hasMatchingRows && !checkIsReviewPage()) { let aiRowBtn = row ? row.querySelector('.amaes-ai-row-btn, .amaes-ai-match-row-btn') : null; if (!aiRowBtn && row) { aiRowBtn = document.createElement('button'); aiRowBtn.type = 'button'; aiRowBtn.className = 'amaes-ai-row-btn amaes-ai-match-row-btn'; aiRowBtn.innerHTML = `${ICONS.gemini} AI`; aiRowBtn.title = 'Ask Gemini AI to solve this specific row'; aiRowBtn.style.cssText = 'margin-left: 6px; padding: 2px 7px; font-size: 10px; font-weight: 700; border: 1px solid #c084fc; border-radius: 4px; background: #faf5ff; color: #7e22ce; cursor: pointer; display: inline-flex; align-items: center; gap: 3px; vertical-align: middle;'; aiRowBtn.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); aiRowBtn.disabled = true; aiRowBtn.innerHTML = `AI...`; try { if (typeof solveMatchingRowWithAi === 'function') { const opt = await solveMatchingRowWithAi({ que, row, selectInput, subQText, autoApply: false }); if (opt) { showToast(`AI suggested: "${opt.text.trim()}"`); } else { showToast("AI could not determine an answer for this row."); } } } catch (err) { showToast(`AI error: ${err.message}`); } finally { aiRowBtn.disabled = false; aiRowBtn.innerHTML = `${ICONS.gemini} AI`; } }; selectInput.insertAdjacentElement('afterend', aiRowBtn); } const sessionCacheKey = `amaes_ai_match_${normalizeText(subQText)}`; const cachedMatchAns = sessionStorage.getItem(sessionCacheKey); if (cachedMatchAns && !row.querySelector('.amaes-ai-matching-suggestion')) { const matchOpt = Array.from(selectInput.options).find(o => normalizeChoice(o.text) === normalizeChoice(cachedMatchAns)); if (matchOpt && typeof applyMatchingRowAiSuggestion === 'function') { applyMatchingRowAiSuggestion(row, selectInput, matchOpt, false); } } } const effectiveCandidate = hasMatchingRows ? rowCandidate : bestCand; if (hasRowAnswerConflict) { const note = document.createElement('div'); note.className = 'amaes-matching-row-conflict-note'; note.setAttribute('role', 'status'); note.textContent = 'Study guides disagree on this matching item. Auto-Pick is paused; check the source answers before selecting.'; note.style.cssText = 'margin-top: 5px; padding: 5px 8px; border-left: 3px solid #f59e0b; background: rgba(245,158,11,.1); color: #92400e; font-size: 11px;'; selectInput.insertAdjacentElement('afterend', note); updateKnownWrongAnswerWarning(que); return; } if (hasMatchingRows && !effectiveCandidate) { updateKnownWrongAnswerWarning(que); return; } const effectiveStudyGuide = effectiveCandidate && !isConfirmedCandidate(effectiveCandidate) ? (getStudyGuideInfo(effectiveCandidate) || (Boolean(effectiveCandidate.source || '').toLowerCase().includes('community') ? { label: 'Community', url: null } : null)) : null; const effectiveIsStudyGuide = Boolean(effectiveStudyGuide); const effectiveSourceColor = effectiveIsStudyGuide ? '#0284c7' : (effectiveCandidate && isConfirmedCandidate(effectiveCandidate) ? '#10b981' : '#3b82f6'); const effectiveSourceBg = effectiveIsStudyGuide ? 'rgba(2, 132, 199, 0.1)' : (effectiveCandidate && isConfirmedCandidate(effectiveCandidate) ? 'rgba(16, 185, 129, 0.1)' : 'rgba(59, 130, 246, 0.1)'); const effectiveSourceTitle = effectiveStudyGuide ? `Suggested (${effectiveStudyGuide.label}, unconfirmed):` : effectiveCandidate && isConfirmedCandidate(effectiveCandidate) ? 'Verified Answer:' : 'Unconfirmed Candidate:'; // Determine target answer for this dropdown: // 1. Try matching sub-question text in bestAnswer (e.g. "Term: Definition" or "Term -> Definition") let targetAns = ''; if (rowCandidate) { targetAns = rowCandidate.ansRaw || rowCandidate.answer || ''; } if (!targetAns && subQText && (bestAnswer.includes('→') || bestAnswer.includes('->'))) { const lines = bestAnswer.split(/[\n,;]+/).map(l => l.trim()); for (const line of lines) { const arrowIndex = Math.max(line.indexOf('→'), line.indexOf('->')); if (arrowIndex < 0) continue; const prompt = line.slice(0, arrowIndex).trim(); if (!questionTextMatches(prompt, subQText)) continue; targetAns = line.slice(arrowIndex + (line[arrowIndex] === '→' ? 1 : 2)).trim(); break; } } // Positional answer lists are unsafe when Moodle shuffles matching rows. if (!targetAns && !hasMatchingRows) { targetAns = selectInputs.length === 1 ? (candAnswers[0] || bestAnswer || '') : (candAnswers[idx] || ''); } if (!targetAns) return; // Look for a matching option in the select const options = Array.from(selectInput.options); const normTarget = normalizeChoice(targetAns); const applicableWrongAnswers = hasMatchingRows ? rowWrongAnswers : allWrongList; // 1. Mark eliminated options in dropdown const eliminatedOptions = []; options.forEach(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return; const optClean = opt.text.replace(/\s*\(Eliminated\)/g, '').trim(); const optNorm = normalizeChoice(optClean); const isWrong = applicableWrongAnswers.some(w => w.norm === optNorm || unscriptDigits(w.norm) === unscriptDigits(optNorm)); if (isWrong) { if (!opt.text.includes('(Eliminated)')) { opt.text = `${optClean} (Eliminated)`; } opt.style.color = '#ef4444'; opt.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; eliminatedOptions.push(optClean); } }); if (eliminatedOptions.length > 0) { let elimHint = que.querySelector(`.amaes-select-elim-hint[data-select-idx="${idx}"]`); if (!elimHint) { elimHint = document.createElement('div'); elimHint.className = 'amaes-select-elim-hint'; elimHint.setAttribute('data-select-idx', String(idx)); elimHint.style.cssText = `font-size: 11px; margin-top: 4px; padding: 4px 8px; background: rgba(239, 68, 68, 0.12); border-left: 3px solid #ef4444; color: #ef4444; border-radius: 4px; font-weight: 600;`; elimHint.innerHTML = `${ICONS.xCircle} Eliminated: ${escapeHtml(eliminatedOptions.join(', '))} (Do not pick)`; if (selectInput.parentElement && selectInput.parentElement !== que) { selectInput.parentElement.appendChild(elimHint); } else { selectInput.insertAdjacentElement('afterend', elimHint); } } } // 2. Find matching option excluding eliminated ones let matchedOption = options.find(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optClean = opt.text.replace(/\s*\(Eliminated\)/g, '').trim(); const normOpt = normalizeChoice(optClean); if (applicableWrongAnswers.some(w => w.norm === normOpt || unscriptDigits(w.norm) === unscriptDigits(normOpt))) return false; return normOpt === normTarget || (normTarget.length > 2 && normOpt.includes(normTarget)) || (normOpt.length > 2 && targetAns.length > 2 && normTarget.includes(normOpt)); }); // Fallback: if no direct match, try matching any candidate answer (excluding eliminated) if (!matchedOption && !hasMatchingRows && candAnswers.length > 0) { matchedOption = options.find(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optClean = opt.text.replace(/\s*\(Eliminated\)/g, '').trim(); const normOpt = normalizeChoice(optClean); if (applicableWrongAnswers.some(w => w.norm === normOpt || unscriptDigits(w.norm) === unscriptDigits(normOpt))) return false; return candAnswers.some(ca => { const nca = normalizeChoice(ca); return normOpt === nca || (nca.length > 2 && normOpt.includes(nca)) || (normOpt.length > 2 && nca.includes(normOpt)); }); }); } // 3. Deduction by elimination: if all options except 1 are eliminated, pick remaining const validOptions = options.filter(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optClean = opt.text.replace(/\s*\(Eliminated\)/g, '').trim(); const optNorm = normalizeChoice(optClean); return !applicableWrongAnswers.some(w => w.norm === optNorm || unscriptDigits(w.norm) === unscriptDigits(optNorm)); }); let isDeducedSelect = false; if (!matchedOption && validOptions.length === 1) { matchedOption = validOptions[0]; isDeducedSelect = true; } if (matchedOption) { matchedAnySelect = true; selectInput.style.outline = `2px solid ${hasMatchingRows ? effectiveSourceColor : sourceColor}`; selectInput.style.backgroundColor = hasMatchingRows ? effectiveSourceBg : sourceBg; selectInput.style.borderRadius = '4px'; let hint = que.querySelector(`.amaes-select-hint[data-select-idx="${idx}"]`); if (!hint) { hint = document.createElement('div'); hint.className = 'amaes-select-hint'; hint.setAttribute('data-select-idx', String(idx)); const displayTitle = isDeducedSelect ? 'Deduced Answer:' : (hasMatchingRows ? effectiveSourceTitle : sourceTitle); const cleanText = matchedOption.text.replace(/\s*\(Eliminated\)/g, '').trim(); const activeColor = isDeducedSelect ? '#f59e0b' : (hasMatchingRows ? effectiveSourceColor : sourceColor); hint.innerHTML = `
${displayTitle} ${cleanText}
`; const hintSourceBg = hasMatchingRows ? effectiveSourceBg : sourceBg; const hintSourceColor = hasMatchingRows ? effectiveSourceColor : sourceColor; hint.style.cssText = `font-size: 11px; margin-top: 5px; margin-bottom: 3px; padding: 5px 9px; background: ${hintSourceBg}; border-left: 3px solid ${hintSourceColor}; border-radius: 4px; cursor: pointer; transition: background 0.15s;`; hint.title = `Click to pick "${matchedOption.text}"`; const pickFn = (e) => { if (e && e.target && e.target.closest('a')) return; if (e) { e.preventDefault(); e.stopPropagation(); } selectInput.value = matchedOption.value; selectInput.dispatchEvent(new Event('input', { bubbles: true })); selectInput.dispatchEvent(new Event('change', { bubbles: true })); selectInput.dispatchEvent(new Event('blur', { bubbles: true })); showToast(`Selected: ${matchedOption.text}`); hint.style.background = 'rgba(16, 185, 129, 0.2)'; const btn = hint.querySelector('.amaes-select-btn'); if (btn) btn.innerText = 'Picked'; }; hint.onclick = pickFn; const pickBtn = hint.querySelector('.amaes-select-btn'); if (pickBtn) pickBtn.onclick = pickFn; if (selectInput.parentElement && selectInput.parentElement !== que) { selectInput.parentElement.appendChild(hint); } else { selectInput.insertAdjacentElement('afterend', hint); } } // Auto-select when autoPickQuiz is enabled and auto-quiz is running, or user triggered manual select const canAutoPick = (isManualSelect && ((effectiveCandidate && isConfirmedCandidate(effectiveCandidate)) || isDeducedSelect)) || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode) && ((effectiveCandidate && isConfirmedCandidate(effectiveCandidate)) || ((hasMatchingRows ? effectiveIsStudyGuide : isStudyGuide) && autoQuizMode && autoPickStudyGuideFallback)) && !isDeducedSelect); if (canAutoPick && (!selectInput.value || selectInput.value === '0')) { selectInput.value = matchedOption.value; selectInput.dispatchEvent(new Event('input', { bubbles: true })); selectInput.dispatchEvent(new Event('change', { bubbles: true })); selectInput.dispatchEvent(new Event('blur', { bubbles: true })); } } }); if (matchedAnySelect) { foundMatchForQuestion = true; } } } // Handle Drag and Drop into text / onto image questions (ddwtos, ddimageortext, ddmarker) if (!foundMatchForQuestion) { const dragHomes = Array.from(que.querySelectorAll('.draghome, .dragitems .draghome, .drags .drag, .drags span, span.draghome, .dragboxes .drag, .dragitem')); const dropZones = Array.from(que.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable')); if (dragHomes.length > 0 && dropZones.length > 0 && candidates.length > 0) { const bestCand = candidates[0]; const bestAnswer = bestCand.ansRaw || bestCand.answer || ''; const studyGuide = isConfirmedCandidate(bestCand) ? null : getStudyGuideInfo(bestCand); const isStudyGuide = Boolean(studyGuide); const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const amauoedUrl = getStoredAmauoedUrl(subCode) || 'https://amauoed.com/courses'; const sourceColor = isStudyGuide ? '#0284c7' : (isConfirmedCandidate(bestCand) ? '#10b981' : '#3b82f6'); const sourceBg = isStudyGuide ? 'rgba(2, 132, 199, 0.1)' : (isConfirmedCandidate(bestCand) ? 'rgba(16, 185, 129, 0.1)' : 'rgba(59, 130, 246, 0.1)'); const guideUrl = studyGuide && studyGuide.url ? studyGuide.url : amauoedUrl; const sourceTitle = studyGuide ? `Suggested (${studyGuide.label}, unconfirmed):` : isConfirmedCandidate(bestCand) ? 'Verified Answer:' : 'Unconfirmed Candidate:'; let candAnswers = []; if (Array.isArray(bestCand.answers) && bestCand.answers.length > 0) { candAnswers = bestCand.answers.map(s => String(s || '').trim()).filter(Boolean); } else if (dropZones.length > 1 && (bestAnswer.includes(',') || bestAnswer.includes('\n') || bestAnswer.includes(';') || bestAnswer.includes('|'))) { candAnswers = bestAnswer.split(/[\n,;|]+/).map(s => s.trim()).filter(Boolean); } else { candAnswers = [bestAnswer.trim()].filter(Boolean); } let matchedDragCount = 0; const usedDrags = new Set(); dropZones.forEach((dropZone, idx) => { let targetAns = dropZones.length === 1 ? (candAnswers[0] || bestAnswer || '') : (candAnswers[idx] || ''); targetAns = targetAns.replace(/^Blank\s*\d+\s*[:\-–]\s*/i, '').trim(); if (!targetAns) return; const normTarget = normalizeChoice(targetAns); const matchingDrag = dragHomes.find(dh => { const isInfinite = dh.classList.contains('infinite') || dh.classList.contains('drag-infinite'); if (!isInfinite && usedDrags.has(dh)) return false; const dText = normalizeChoice(cleanDOMToAI(dh)); return dText === normTarget || (normTarget.length > 1 && dText.includes(normTarget)) || (dText.length > 1 && normTarget.includes(dText)); }); if (matchingDrag) { usedDrags.add(matchingDrag); matchedDragCount++; matchingDrag.style.outline = `2.5px solid ${sourceColor}`; matchingDrag.style.backgroundColor = sourceBg; matchingDrag.style.borderRadius = '4px'; dropZone.style.outline = `2px dashed ${sourceColor}`; dropZone.style.backgroundColor = sourceBg; dropZone.style.borderRadius = '4px'; let hint = que.querySelector(`.amaes-drag-hint[data-drop-idx="${idx}"]`); if (!hint) { hint = document.createElement('div'); hint.className = 'amaes-drag-hint'; hint.setAttribute('data-drop-idx', String(idx)); hint.innerHTML = `
${sourceTitle} Blank [${idx + 1}]: ${targetAns}
`; hint.style.cssText = `font-size: 11px; margin-top: 5px; margin-bottom: 3px; padding: 5px 9px; background: ${sourceBg}; border-left: 3px solid ${sourceColor}; border-radius: 4px; cursor: pointer; transition: background 0.15s;`; hint.title = `Click to place "${targetAns}" into Blank ${idx + 1}`; const placeFn = (e) => { if (e && e.target && e.target.closest('a')) return; if (e) { e.preventDefault(); e.stopPropagation(); } const hiddenInput = que.querySelector(`input.placeinput.place${idx + 1}, input[name$="_p${idx + 1}"], input[name*="_p${idx + 1}"]`) || que.querySelectorAll('input.placeinput, input[type="hidden"][name*="_p"]')[idx]; const choiceClass = Array.from(matchingDrag.classList).find(c => /^(?:choice|c)(\d+)$/i.test(c)); let choiceNum = choiceClass ? choiceClass.replace(/^[^\d]+/, '') : String(dragHomes.indexOf(matchingDrag) + 1); if (hiddenInput && choiceNum) { hiddenInput.value = choiceNum; hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); hiddenInput.dispatchEvent(new Event('blur', { bubbles: true })); } matchingDrag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); dropZone.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); if (isConfirmedCandidate(bestCand)) { dropZone.innerText = targetAns; } dropZone.style.outline = `2px solid ${sourceColor}`; dropZone.style.fontWeight = '700'; showToast(`Placed: ${targetAns}`); hint.style.background = 'rgba(16, 185, 129, 0.2)'; const btn = hint.querySelector('.amaes-drag-btn'); if (btn) btn.innerText = 'Placed'; }; hint.onclick = placeFn; const placeBtn = hint.querySelector('.amaes-drag-btn'); if (placeBtn) placeBtn.onclick = placeFn; if (dropZone.parentElement && dropZone.parentElement !== que) { dropZone.parentElement.appendChild(hint); } else { dropZone.insertAdjacentElement('afterend', hint); } // Auto-place when autoPickQuiz is enabled and auto-quiz is running, or user triggered manual select const canAutoPick = (isManualSelect && isConfirmedCandidate(bestCand)) || (Boolean(autoSelect) && (autoPickQuiz || autoQuizMode) && isConfirmedCandidate(bestCand)); if (canAutoPick) { placeFn(); } } } }); // Only consider the question matched if ALL drop zones are matched! if (matchedDragCount === dropZones.length) { foundMatchForQuestion = true; } else if (matchedDragCount > 0) { let partialBanner = que.querySelector('.amaes-partial-drag-hint'); if (!partialBanner) { partialBanner = document.createElement('div'); partialBanner.className = 'amaes-partial-drag-hint'; partialBanner.style.cssText = ` font-size: 11px; margin-top: 6px; margin-bottom: 6px; padding: 6px 10px; background: rgba(245, 158, 11, 0.12); border-left: 3px solid #f59e0b; border-radius: 4px; color: #b45309; display: flex; align-items: center; gap: 6px; `; partialBanner.innerHTML = ` Partial Answer Known: Matched ${matchedDragCount} of ${dropZones.length} blanks. Please review and place remaining blanks manually. `; const formulation = que.querySelector('.formulation, .content') || que; formulation.insertBefore(partialBanner, formulation.firstChild); } } } } if (!foundMatchForQuestion) { if (checkIsQuizAttemptPage() && !que.querySelector('.amaes-new-question-focus-banner') && !que.querySelector('.amaes-blockage-hud')) { que.classList.add('amaes-no-answer-que'); const numElem = que.querySelector('.info .no, .qno'); const qNum = numElem ? numElem.innerText.replace(/[^0-9]/g, '') : ''; renderNewQuestionFocusBanner(que, qNum); updateNextButtonIndicator(); } else if (checkIsQuizAttemptPage()) { que.classList.add('amaes-no-answer-que'); } } else { que.classList.remove('amaes-no-answer-que'); matchedCount++; } if (typeof updateQuestionAiDrawerState === 'function') { updateQuestionAiDrawerState(que); } }); return { matched: matchedCount, total: queContainers.length }; } // ========================================== // Activity Classifier for Course Page // ========================================== function getQuizRegex() { return /\b(quiz|prelim|prelims|midterm|midterms|prefinal|prefinals|final|finals|exam|examination|assessment|eval)\b/i; } function getVideoRegex() { return /\b(video|vid|vids|watch|recording|recordings|webinar|clip|panopto|zoom|stream)\b/i; } function getLectureRegex() { return /\b(lec|lecture|lectures|lesson|reading|topic|module|handout|guide|discussion|notes)\b/i; } function classifyActivity(activityElem) { const text = (activityElem.innerText || '').toLowerCase(); const html = activityElem.innerHTML.toLowerCase(); // 1. Video const isVideoMod = Boolean(activityElem.querySelector( '[data-modname*="video"], [data-modtype*="video"], a[href*="youtube"], a[href*="vimeo"], a[href*="panopto"], iframe, video' )) || activityElem.classList.contains('modtype_videostream') || activityElem.classList.contains('modtype_kalvidres'); const videoRegex = getVideoRegex(); if (isVideoMod || videoRegex.test(text) || html.includes('youtube.com') || html.includes('youtu.be') || html.includes('vimeo.com')) { return { type: 'video', reason: isVideoMod ? 'video-mod' : 'video-keyword', color: 'var(--accent-purple)', badge: 'Video' }; } // 2. Quiz / Exam const isQuizMod = activityElem.classList.contains('modtype_quiz') || Boolean(activityElem.querySelector('[data-modname="quiz"], [data-modtype="quiz"], a[href*="/mod/quiz/"], img[src*="quiz"]')); const iconContainer = activityElem.querySelector('.activityiconcontainer, .activity-icon'); let hasPinkIcon = false; let iconBgColor = ''; if (iconContainer) { iconBgColor = window.getComputedStyle(iconContainer).backgroundColor; if (iconContainer.classList.contains('assessment') || iconContainer.classList.contains('evaluations') || iconContainer.classList.contains('bg-pink')) { hasPinkIcon = true; } else { const rgb = iconBgColor.match(/\d+/g); if (rgb && rgb.length >= 3) { const [r, g, b] = rgb.map(Number); if (r > 160 && b > 100 && g < 140) hasPinkIcon = true; } } } const quizRegex = getQuizRegex(); if (isQuizMod || hasPinkIcon || quizRegex.test(text)) { const isExam = /exam|examination|prelim|prelims|midterm|midterms|prefinal|prefinals|final\b|finals/i.test(text) || (typeof detectAssessmentOrigin === 'function' && detectAssessmentOrigin().isExam); return { type: 'quiz', isExam: isExam, reason: isQuizMod ? 'quiz-module' : hasPinkIcon ? 'pink-icon' : 'keyword', color: isExam ? 'var(--accent-blue, #2563eb)' : 'var(--accent-pink)', badge: isExam ? 'Exam' : 'Quiz' }; } // 3. Lecture / Reading const isLectureMod = Boolean(activityElem.querySelector( '.modtype_page, .modtype_resource, .modtype_url, .modtype_book, .modtype_folder, ' + '[data-modname="page"], [data-modname="resource"], [data-modname="url"], [data-modname="book"], ' + 'a[href*="/mod/page/"], a[href*="/mod/resource/"], a[href*="/mod/url/"], a[href*="/mod/book/"]' )) || activityElem.classList.contains('modtype_page') || activityElem.classList.contains('modtype_resource'); let hasBlueIcon = false; if (iconContainer) { if (iconContainer.classList.contains('content') || iconContainer.classList.contains('bg-blue') || iconContainer.classList.contains('content-blue')) { hasBlueIcon = true; } else { const rgb = iconBgColor.match(/\d+/g); if (rgb && rgb.length >= 3) { const [r, g, b] = rgb.map(Number); if (b > 150 && b > r + 30) hasBlueIcon = true; } } } const lectureRegex = getLectureRegex(); if (isLectureMod || hasBlueIcon || lectureRegex.test(text)) { return { type: 'lecture', reason: isLectureMod ? 'resource-module' : hasBlueIcon ? 'blue-icon' : 'keyword', color: 'var(--accent-blue)', badge: 'Lecture' }; } return { type: 'lecture', reason: 'default-non-quiz', color: 'var(--accent-blue)', badge: 'Lecture' }; } // ========================================== // Quiz Passable Grade (≥80%) Safety Evaluator // ========================================== async function fetchCourseGradesMap() { const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const courseId = courseInfo && courseInfo.courseId ? courseInfo.courseId : ''; // 1. In-memory cache valid for 60s if (typeof window !== 'undefined' && window.__amaesCourseGradesCache && window.__amaesCourseGradesCacheTime && (Date.now() - window.__amaesCourseGradesCacheTime < 60000)) { return window.__amaesCourseGradesCache; } // 2. Persistent localStorage cache valid for 10 minutes if (courseId) { try { const storedRaw = localStorage.getItem(`amaes_course_grades_cache_${courseId}`); const storedTime = parseInt(localStorage.getItem(`amaes_course_grades_cache_time_${courseId}`) || '0', 10); if (storedRaw && storedTime && (Date.now() - storedTime < 600000)) { const parsed = JSON.parse(storedRaw); if (typeof window !== 'undefined') { window.__amaesCourseGradesCache = parsed; window.__amaesCourseGradesCacheTime = storedTime; } return parsed; } } catch (_) {} } let gradesUrl = null; if (typeof document !== 'undefined') { const gradesLink = document.querySelector('a[href*="/grade/report/user/index.php"]'); if (gradesLink && gradesLink.href) { gradesUrl = gradesLink.href; } else if (courseId) { const semPath = typeof getSemesterBasePath === 'function' ? getSemesterBasePath() : '/'; gradesUrl = `${window.location.origin}${semPath}grade/report/user/index.php?id=${courseId}`; } } if (!gradesUrl) return {}; try { const resp = await fetch(gradesUrl); if (!resp.ok) return {}; const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const map = {}; const rows = Array.from(doc.querySelectorAll('.user-grade tr, .generaltable tr, table.table tr, tr')); rows.forEach(r => { const quizLink = r.querySelector('a[href*="/mod/quiz/"], a[href*="quiz"], a.gradeitemheader') || r.querySelector('.column-itemname a, th a, td:first-child a'); if (!quizLink) return; const href = quizLink.getAttribute('href') || quizLink.href || ''; const rawTitle = quizLink.innerText.trim(); if (!rawTitle) return; const gradeCell = r.querySelector('.column-grade, [headers*="grade"], td.grade'); const pctCell = r.querySelector('.column-percentage, [headers*="percentage"]'); let pct = null; let hasGrade = false; let gradeStr = ''; if (pctCell) { const pText = pctCell.innerText.trim(); const mPct = pText.match(/(\d+(?:\.\d+)?)\s*%/); if (mPct) { pct = parseFloat(mPct[1]); hasGrade = true; gradeStr = `${pct}%`; } } if (gradeCell) { const gText = gradeCell.innerText.trim(); if (gText && gText !== '-' && gText !== '–' && /\d/.test(gText)) { hasGrade = true; if (!gradeStr) gradeStr = gText; if (pct === null) { const mFrac = gText.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/); if (mFrac) { const earned = parseFloat(mFrac[1]); const max = parseFloat(mFrac[2]); if (max > 0) pct = Math.round((earned / max) * 100); } else { const mNum = gText.match(/(\d+(?:\.\d+)?)/); if (mNum && parseFloat(mNum[1]) <= 100) { pct = parseFloat(mNum[1]); } } } } } const mCmid = href.match(/[?&]id=(\d+)/); const cmid = mCmid ? mCmid[1] : null; const normTitle = rawTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); const entry = { title: rawTitle, normTitle: normTitle, href: href, cmid: cmid, hasGrade: hasGrade, percentage: pct, gradeStr: gradeStr, isPassable: Boolean(hasGrade && pct !== null && pct >= 80) }; map[normTitle] = entry; if (cmid) map[`cmid_${cmid}`] = entry; }); if (typeof window !== 'undefined') { window.__amaesCourseGradesCache = map; window.__amaesCourseGradesCacheTime = Date.now(); if (courseId) { try { localStorage.setItem(`amaes_course_grades_cache_${courseId}`, JSON.stringify(map)); localStorage.setItem(`amaes_course_grades_cache_time_${courseId}`, String(Date.now())); } catch (_) {} } } return map; } catch (e) { console.warn('Failed to fetch course grades map:', e); return {}; } } function evaluateQuizPassableGrade(container, title = '', gradesMap = null) { // Resolve container cmid if available (id="module-123" or data-id="123") let moduleId = (container && container.id ? container.id.replace(/^module-/, '') : '') || (container && container.dataset ? container.dataset.id : '') || ''; if (!moduleId && container && container.querySelector) { const link = container.querySelector('a[href*="/mod/quiz/view.php?id="], a[href*="id="]'); if (link) { const m = (link.getAttribute('href') || link.href || '').match(/[?&]id=(\d+)/); if (m) moduleId = m[1]; } } const normTitle = (title || (container && container.innerText ? container.innerText.split('\n')[0] : '') || '').toLowerCase().replace(/[^a-z0-9]/g, ''); // 0. Fast-track explicit 100% quiz completion cache check if (typeof localStorage !== 'undefined') { if (moduleId && localStorage.getItem(`amaes_quiz_100_cmid_${moduleId}`) === 'true') { return { isPassable: true, hasGrade: true, percentage: 100, cmid: moduleId, source: 'quiz-100-cache-cmid' }; } if (normTitle && localStorage.getItem(`amaes_quiz_100_title_${normTitle}`) === 'true') { return { isPassable: true, hasGrade: true, percentage: 100, cmid: moduleId, source: 'quiz-100-cache-title' }; } try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('amaes_quiz_100_title_')) { const savedNorm = key.replace('amaes_quiz_100_title_', ''); if (savedNorm && normTitle && (savedNorm === normTitle || (savedNorm.length > 5 && (normTitle.includes(savedNorm) || savedNorm.includes(normTitle))))) { return { isPassable: true, hasGrade: true, percentage: 100, cmid: moduleId, source: 'quiz-100-cache-title-fuzzy' }; } } } } catch (_) {} } // 1. Check gradesMap if provided or cached const map = gradesMap || (typeof window !== 'undefined' ? window.__amaesCourseGradesCache : null); if (map && Object.keys(map).length > 0) { if (moduleId && map[`cmid_${moduleId}`]) { const entry = map[`cmid_${moduleId}`]; return { isPassable: entry.isPassable, hasGrade: entry.hasGrade, percentage: entry.percentage, cmid: moduleId, source: 'grades-report' }; } if (normTitle && map[normTitle]) { const entry = map[normTitle]; return { isPassable: entry.isPassable, hasGrade: entry.hasGrade, percentage: entry.percentage, cmid: entry.cmid || moduleId, source: 'grades-report' }; } // Fuzzy match title for (const k in map) { if (k.startsWith('cmid_')) continue; if (k.length > 5 && (normTitle.includes(k) || k.includes(normTitle))) { const entry = map[k]; return { isPassable: entry.isPassable, hasGrade: entry.hasGrade, percentage: entry.percentage, source: 'grades-report-fuzzy' }; } } } // 2. Fallback: Parse container text for explicit grade or pass/fail markers const text = (container.innerText || '').toLowerCase(); // Check for explicit fail markers if (/did not achieve pass grade|failed to pass|\bfailed\b/i.test(text)) { return { isPassable: false, hasGrade: true, percentage: 0, source: 'container-fail' }; } // Check for explicit pass markers if (/achieved pass grade|pass grade achieved|\bpassed\b/i.test(text)) { return { isPassable: true, hasGrade: true, percentage: 100, source: 'container-pass' }; } // Check for explicit percentage in container const mPct = text.match(/(\d+(?:\.\d+)?)\s*%/); if (mPct) { const val = parseFloat(mPct[1]); return { isPassable: val >= 80, hasGrade: true, percentage: val, source: 'container-pct' }; } // Check for fractions e.g. "8/10" or "80/100" or "Grade: 9 out of 10" const mFrac = text.match(/(\d+(?:\.\d+)?)\s*(?:\/|out of)\s*(\d+(?:\.\d+)?)/i); if (mFrac) { const earned = parseFloat(mFrac[1]); const max = parseFloat(mFrac[2]); if (max > 0) { const val = Math.round((earned / max) * 100); return { isPassable: val >= 80, hasGrade: true, percentage: val, source: 'container-fraction' }; } } // 3. No grade recorded or unattempted: MUST NOT TOUCH! return { isPassable: false, hasGrade: false, percentage: null, source: 'no-grade' }; } function isActivityAlreadyComplete(container) { if (!container) return false; // 0. Immediate short-circuit: If container has an uncompleted manual completion button, it is NOT complete! const manualUncompletedBtn = container.querySelector( 'button[data-toggletype="manual:mark-done"], button[data-action="toggle-manual-completion"]:not([data-toggletype="manual:undo"])' ); if (manualUncompletedBtn) { const btnText = (manualUncompletedBtn.innerText || manualUncompletedBtn.getAttribute('aria-label') || manualUncompletedBtn.textContent || '').trim().toLowerCase(); if (btnText.includes('mark as done') || btnText.includes('to do') || manualUncompletedBtn.getAttribute('data-toggletype') === 'manual:mark-done') { return false; } } const anyBtns = container.querySelectorAll('button, input[type="submit"], input[type="button"]'); for (const b of anyBtns) { const bText = (b.innerText || b.getAttribute('aria-label') || b.value || '').trim().toLowerCase(); if (bText.includes('mark as done') || b.getAttribute('data-toggletype') === 'manual:mark-done') { return false; } } // 1. Direct class checks on container if (container.classList.contains('completed')) return true; // 2. Completed icons or toggled buttons if (container.querySelector( '.iscompleted, [data-toggled="true"], button[aria-checked="true"], button.btn-success, button.btn-outline-success, button[data-toggletype="manual:undo"]' )) return true; // 3. Scan completion area for completion markers const completionArea = container.querySelector( '.activity-completion, [data-region="completion-info"], .automatic-completion-conditions, .completion-info' ) || container; if (completionArea.querySelector('.badge-success, .text-success, [data-region="completion-info"] .badge-success')) return true; // 4. Check completionArea text content for "Done" or "Completed" (strictly excluding "mark as done" and "to do") const fullText = (completionArea.innerText || completionArea.textContent || '').trim().toLowerCase(); if (!fullText.includes('mark as done') && !fullText.includes('to do')) { if (fullText.includes('done:') || fullText.includes('done :') || /\bdone\b/i.test(fullText) || fullText.includes('completed')) { return true; } } // 5. Scan all buttons inside container for manual undo or done text (strictly excluding "mark as done") for (const b of anyBtns) { const bText = (b.innerText || b.getAttribute('aria-label') || b.value || '').trim().toLowerCase(); const bToggle = (b.getAttribute('data-toggletype') || '').toLowerCase(); if (bToggle === 'manual:undo' || (b.classList && (b.classList.contains('btn-outline-success') || b.classList.contains('btn-success')))) { return true; } if (!bText.includes('mark as done') && !bText.includes('to do')) { if (bText === 'done' || bText.startsWith('done') || bText.includes('✓') || bText.includes('✔') || bText.includes('completed')) { return true; } } } return false; } function isActivityLockedOrRestricted(container) { if (!container) return false; // 1. Direct class checks indicating Moodle restriction or dimmed state if (container.classList.contains('dimmed') || container.classList.contains('dimmed_text') || container.classList.contains('conditionalhidden') || container.classList.contains('isrestricted') || container.classList.contains('restricted') || container.classList.contains('locked')) { return true; } // 2. Specific restriction / availability / lock indicators in children if (container.querySelector('.dimmed, .dimmed_text, .conditionalhidden, .restrictions, .availabilityinfo, [data-region="activity-information"], .fa-lock, .icon-lock, [data-region="locked"], [aria-label*="locked" i], [aria-label*="restricted" i]')) { const restrictElem = container.querySelector('.restrictions, .availabilityinfo, [data-region="activity-information"], .badge-info, .badge, .fa-lock, .icon-lock'); if (restrictElem) { const rText = (restrictElem.innerText || restrictElem.textContent || '').trim(); if (/not available unless|restricted|not available until|hidden from students|takeexamcode|locked/i.test(rText) || restrictElem.matches('.fa-lock, .icon-lock')) { return true; } } } // 3. Text content scan for Moodle conditional access strings (e.g. "TakeExamCode contains 4", "Restricted Not available unless:") const fullText = (container.innerText || container.textContent || '').trim(); if (/restricted:?\s+not available|not available unless|not available until|takeexamcode|prerequisite|must be completed first|activity.*is marked complete/i.test(fullText)) { return true; } // 4. Missing, disabled, or dimmed link on activity container (locked exams have no active clickable link) // If an item has an interactive manual completion button, it is never an inaccessible locked exam! const hasManualBtn = Boolean(container.querySelector('button[data-action="toggle-manual-completion"], button[data-toggletype], form.togglecompletion')); if (!hasManualBtn) { const isQuizOrExam = container.classList.contains('modtype_quiz') || Boolean(container.querySelector('[data-modname="quiz"], a[href*="/mod/quiz/"]')); if (isQuizOrExam) { const link = container.querySelector('a.aal_link, a[href*="/mod/quiz/"], a[href*="/mod/"]'); if (!link || link.getAttribute('aria-disabled') === 'true' || link.classList.contains('dimmed') || link.hasAttribute('disabled')) { if (container.matches('.activity, .activity-item, li.activity, div[data-region="activity-card"]')) { return true; } } } } return false; } function findButtons(goal = 'mark_done', category = 'lecture', gradesMap = null) { const results = []; const activityElements = document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"]' ); const processedButtons = new Set(); const processedContainers = new Set(); const scanBlock = (container) => { if (!container) return; // Smart Skip: If goal is to mark done, completely skip activities that are already done if (goal === 'mark_done' && isActivityAlreadyComplete(container)) { return; } // Locked / Restricted Skip: If activity or exam is restricted/locked, cannot be clicked or auto-marked done! if (isActivityLockedOrRestricted(container)) { return; } const buttons = container.querySelectorAll( 'button[data-action="toggle-manual-completion"], ' + 'button[data-toggletype], ' + 'button.btn-outline-secondary, ' + 'button.btn-outline-success, ' + 'button.btn-success, ' + 'button, ' + 'input[type="submit"][name*="completion" i], ' + 'input[type="submit"][value*="done" i], ' + 'input[type="image"][name*="completion" i], ' + 'form.togglecompletion input[type="submit"], ' + 'form.togglecompletion button' ); let foundManualButton = false; for (const btn of buttons) { if (processedButtons.has(btn)) continue; const text = (btn.innerText || btn.getAttribute('aria-label') || btn.value || '').trim().toLowerCase(); const toggleType = (btn.getAttribute('data-toggletype') || '').toLowerCase(); const isToggleAction = btn.getAttribute('data-action') === 'toggle-manual-completion'; const isToggleCompletionForm = Boolean(btn.closest('form.togglecompletion')); const isMarkDoneText = text.includes('mark as done') || text === 'mark as done' || toggleType === 'manual:mark-done'; const isCurrentlyDone = !isMarkDoneText && ( toggleType === 'manual:undo' || ((text === 'done' || text.startsWith('done') || text.includes('✓') || text.includes('✔') || (text.includes('completed') && !text.includes('incomplete'))) && (isToggleAction || toggleType || isToggleCompletionForm || btn.classList.contains('btn-outline-success') || btn.classList.contains('btn-success'))) || ((btn.classList.contains('btn-success') || btn.classList.contains('btn-outline-success')) && (isToggleAction || toggleType)) ); const isCurrentlyUncompleted = isMarkDoneText || (text === 'to do' && (isToggleAction || toggleType)) || (isToggleAction && !isCurrentlyDone) || (isToggleCompletionForm && !isCurrentlyDone); let matchesGoal = false; if (goal === 'mark_done' && isCurrentlyUncompleted && !isCurrentlyDone) { matchesGoal = true; } else if (goal === 'undo' && isCurrentlyDone) { matchesGoal = true; } if (matchesGoal) { processedButtons.add(btn); foundManualButton = true; processedContainers.add(container); const classification = classifyActivity(container); const titleElem = container.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const title = titleElem ? titleElem.innerText.trim() : (container.innerText.split('\n')[0] || 'Activity'); let gradeInfo = null; // Safety Guard: For quizzes when marking done, ONLY mark if they have a passable grade (>= 80%)! if (goal === 'mark_done' && classification.type === 'quiz') { gradeInfo = evaluateQuizPassableGrade(container, title, gradesMap); if (!gradeInfo.isPassable) { // Leave untouched! continue; } } let matchesCategory = false; if (category === 'all') matchesCategory = true; else if (category === 'lecture' && (classification.type === 'lecture' || classification.type === 'video')) matchesCategory = true; else if (category === 'quiz' && classification.type === 'quiz') matchesCategory = true; if (matchesCategory) { results.push({ button: btn, container, title, classification, isAutoView: false, gradePct: gradeInfo ? gradeInfo.percentage : null }); } } } // 2. Automatic "To do: View" completion detection (activities requiring view to complete) if (!foundManualButton && goal === 'mark_done' && !processedContainers.has(container)) { const completionArea = container.querySelector( '.activity-completion, [data-region="completion-info"], .automatic-completion-conditions, .completion-info' ) || container; const badges = completionArea.querySelectorAll('.badge, button, [data-region="completion-info"] span, .automatic-completion-conditions span'); let uncompletedViewBadge = null; for (const badge of badges) { if (badge.closest('.badge-success, .completed, .iscompleted')) continue; const text = (badge.innerText || badge.textContent || '').trim().toLowerCase(); const isTodoView = text.includes('to do: view') || text.includes('to do:view') || (text.includes('to do') && text.includes('view')) || text === 'view'; if (isTodoView && !text.includes('done') && !text.includes('completed') && !badge.classList.contains('badge-success')) { uncompletedViewBadge = badge; break; } } if (uncompletedViewBadge) { const linkElem = container.querySelector('a.aal_link, a[href*="/mod/"], a.activityname, a.stretched-link, a[href*="view.php"]'); const activityUrl = linkElem ? linkElem.href : null; if (activityUrl) { const classification = classifyActivity(container); const titleElem = container.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const title = titleElem ? titleElem.innerText.trim() : (container.innerText.split('\n')[0] || 'Activity'); let matchesCategory = false; if (category === 'all') matchesCategory = true; else if (category === 'lecture' && (classification.type === 'lecture' || classification.type === 'video')) matchesCategory = true; else if (category === 'quiz' && classification.type === 'quiz') matchesCategory = true; if (matchesCategory) { processedContainers.add(container); results.push({ button: null, autoViewUrl: activityUrl, badgeElem: uncompletedViewBadge, container, title, classification, isAutoView: true, gradePct: null }); } } } } }; if (activityElements.length > 0) { activityElements.forEach(scanBlock); } else { const rawButtons = document.querySelectorAll( 'button[data-action="toggle-manual-completion"], button[data-toggletype], form.togglecompletion input[type="submit"], form.togglecompletion button' ); rawButtons.forEach(btn => { const parent = btn.closest('li, div.activity, div.row, div.card, div.activity-item') || btn.parentElement; scanBlock(parent); }); } return results; } async function autoViewActivity(url, badgeElem = null, container = null) { if (!url) return false; let originalBadgeText = ''; if (badgeElem) { originalBadgeText = badgeElem.innerText || badgeElem.textContent || ''; badgeElem.dataset.originalText = originalBadgeText; badgeElem.textContent = 'Viewing...'; badgeElem.style.opacity = '0.7'; badgeElem.style.cursor = 'wait'; } try { await fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); } catch (err) { // Even if fetch throws (e.g. cross-origin redirect on URL module), Moodle logged course_module_viewed logDebug(`Auto-view fetch handled for ${url}: ${err.message}`); if (typeof GM_xmlhttpRequest === 'function') { try { await new Promise(res => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: () => res(true), onerror: () => res(false), ontimeout: () => res(false) }); }); } catch (_) {} } } if (badgeElem) { badgeElem.textContent = 'Done: View'; badgeElem.style.opacity = '1'; badgeElem.style.cursor = 'default'; badgeElem.classList.remove('badge-secondary', 'badge-light', 'badge-info', 'badge-warning', 'badge-danger'); badgeElem.classList.add('badge-success'); badgeElem.style.backgroundColor = '#198754'; badgeElem.style.color = '#ffffff'; badgeElem.style.borderColor = '#198754'; badgeElem.removeAttribute('title'); } if (container) { container.classList.add('completed'); const compIcon = container.querySelector('.iscompleted, .activity-completion'); if (compIcon) compIcon.classList.add('completed'); } return true; } function setupAutoViewBadges() { if (typeof checkIsCoursePage === 'function' && !checkIsCoursePage()) return; const containers = document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"], .activity-instance' ); containers.forEach(container => { // Smart Skip: If activity is already done/completed, do not bind or view if (isActivityAlreadyComplete(container)) return; // Locked / Restricted Skip: If activity or exam is restricted/locked, cannot be clicked or auto-viewed if (isActivityLockedOrRestricted(container)) return; const completionArea = container.querySelector( '.activity-completion, [data-region="completion-info"], .automatic-completion-conditions, .completion-info' ) || container; const badges = completionArea.querySelectorAll('.badge, button, [data-region="completion-info"] span, .automatic-completion-conditions span'); for (const badge of badges) { if (badge.closest('.badge-success, .completed, .iscompleted')) continue; const text = (badge.innerText || badge.textContent || '').trim().toLowerCase(); const isTodoView = text.includes('to do: view') || text.includes('to do:view') || (text.includes('to do') && text.includes('view')); if (isTodoView && !text.includes('done') && !text.includes('completed') && !badge.classList.contains('badge-success')) { if (badge.dataset.amaesAutoViewBound) continue; badge.dataset.amaesAutoViewBound = 'true'; const linkElem = container.querySelector('a.aal_link, a[href*="/mod/"], a.activityname, a.stretched-link, a[href*="view.php"]'); const activityUrl = linkElem ? linkElem.href : null; if (!activityUrl) continue; // Style badge to indicate 1-click completion badge.style.cursor = 'pointer'; badge.style.transition = 'all 0.2s ease'; badge.title = 'Click to auto-view and mark as done!'; badge.setAttribute('role', 'button'); // Hover effects badge.addEventListener('mouseenter', () => { if (!badge.textContent.includes('Done')) { badge.style.transform = 'scale(1.05)'; badge.style.boxShadow = '0 0 6px rgba(46, 204, 113, 0.6)'; } }); badge.addEventListener('mouseleave', () => { badge.style.transform = ''; badge.style.boxShadow = ''; }); badge.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (badge.dataset.amaesAutoViewing === 'true') return; badge.dataset.amaesAutoViewing = 'true'; const titleElem = container.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const title = titleElem ? titleElem.innerText.trim() : 'Activity'; showToast(`Auto-viewing "${title.substring(0, 24)}"...`, 1500); setLog(`Auto-Viewing: ${title.substring(0, 30)}...`, "var(--accent-blue)"); await autoViewActivity(activityUrl, badge, container); playToolkitSound('quest_done'); showToast(`Marked "${title.substring(0, 24)}" as viewed & complete!`, 3000); setLog(`Auto-Viewed: ${title.substring(0, 30)}... Completed!`, "var(--accent-green)"); badge.dataset.amaesAutoViewing = 'false'; }); break; } } }); } // ========================================== // Visual Course Highlighter // ========================================== function clearAllHighlights() { const highlighted = document.querySelectorAll('.amaes-highlighted-item'); highlighted.forEach(el => { el.classList.remove('amaes-highlighted-item'); el.style.outline = ''; el.style.boxShadow = ''; el.style.position = ''; const badge = el.querySelector('.amaes-type-badge'); if (badge) badge.remove(); }); } function highlightItems(targetCategory = 'all') { clearAllHighlights(); if (!checkIsCoursePage()) { return { count: 0, error: 'Not a course page' }; } const activities = document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"]' ); let count = 0; activities.forEach(el => { const classification = classifyActivity(el); let shouldHighlight = false; if (targetCategory === 'all') shouldHighlight = true; else if (targetCategory === classification.type) shouldHighlight = true; if (shouldHighlight) { count++; el.classList.add('amaes-highlighted-item'); el.style.position = 'relative'; const isLocked = isActivityLockedOrRestricted(el); const borderColor = isLocked ? 'var(--accent-amber, #f59e0b)' : classification.color; el.style.outline = isLocked ? `2px dashed ${borderColor}` : `2px solid ${borderColor}`; el.style.borderRadius = '8px'; el.style.boxShadow = `0 0 10px ${borderColor}33`; const badge = document.createElement('span'); badge.className = 'amaes-type-badge'; badge.innerText = isLocked ? `[Locked] ${classification.badge}` : classification.badge; badge.style.cssText = ` position: absolute; top: 6px; right: 6px; background: ${isLocked ? '#d97706' : classification.color}; color: #ffffff; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 4px; z-index: 10; pointer-events: none; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 1px 4px rgba(0,0,0,0.3); `; el.appendChild(badge); } }); return { count, targetCategory }; } // ========================================== // Quiz Landing Page Data & Attempt Parser // ========================================== function parseQuizLandingData(doc = document, pageUrl = (typeof window !== 'undefined' ? window.location.href : '')) { let cmid = null; const mId = (pageUrl || '').match(/[?&]id=(\d+)/); if (mId) cmid = mId[1]; const text = (doc.body ? doc.body.innerText : (doc.documentElement ? doc.documentElement.innerText : '')) || ''; // 1. Attempts allowed let attemptsAllowed = null; const mAllowed = text.match(/attempts allowed:\s*(\d+)/i); if (mAllowed) { attemptsAllowed = parseInt(mAllowed[1], 10); } // 2. Count finished attempts from summary table let attemptsUsed = 0; const attemptRows = doc.querySelectorAll('.generaltable tbody tr, table.generaltable tr'); attemptRows.forEach(r => { const rText = r.innerText.toLowerCase(); if (rText.includes('finished') || rText.includes('submitted') || rText.includes('review') || /attempt\s*\d+/i.test(rText)) { if (/\d/.test(r.querySelector('td') ? r.querySelector('td').innerText : '')) { attemptsUsed++; } } }); if (attemptsUsed === 0) { const matches = Array.from(text.matchAll(/(?:attempt|preview)\s+(\d+)/gi)); if (matches.length > 0) { const nums = matches.map(m => parseInt(m[1], 10)).filter(n => !isNaN(n)); if (nums.length > 0) attemptsUsed = Math.max(...nums); } } // 3. Highest grade / Overall grade let highestGradeStr = ''; let highestPct = null; let hasGrade = false; const mHighest = text.match(/highest grade:\s*(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/i) || text.match(/overall grade for this quiz:\s*(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/i); if (mHighest) { const earned = parseFloat(mHighest[1]); const max = parseFloat(mHighest[2]); if (max > 0) { highestPct = Math.round((earned / max) * 100); highestGradeStr = `${earned} / ${max} (${highestPct}%)`; hasGrade = true; } } if (highestPct === null) { const gradeCells = doc.querySelectorAll('.generaltable td.c3, .generaltable td.c2, .generaltable td'); let maxFoundPct = -1; gradeCells.forEach(cell => { const cText = cell.innerText.trim(); const mCellFrac = cText.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/); if (mCellFrac) { const earned = parseFloat(mCellFrac[1]); const max = parseFloat(mCellFrac[2]); if (max > 0) { const p = Math.round((earned / max) * 100); if (p > maxFoundPct) { maxFoundPct = p; highestGradeStr = cText; hasGrade = true; } } } else { const mNum = cText.match(/^(\d+(?:\.\d+)?)$/); if (mNum) { const val = parseFloat(mNum[1]); if (val <= 100 && val > maxFoundPct) { maxFoundPct = val; highestGradeStr = `${val}%`; hasGrade = true; } } } }); if (maxFoundPct >= 0) { highestPct = maxFoundPct; } } // 4. Can re-attempt / start attempt const startBtn = doc.querySelector( 'form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], ' + '.quizstartbutton button, .quizstartbutton input[type="submit"], ' + '#region-main button.btn-primary, #region-main input.btn-primary' ); const noMoreAllowed = text.toLowerCase().includes('no more attempts are allowed'); const maxAttemptsReached = Boolean(noMoreAllowed || (attemptsAllowed !== null && attemptsUsed >= attemptsAllowed)); const canReattempt = Boolean(startBtn && !maxAttemptsReached); const isPassable = Boolean(hasGrade && highestPct !== null && highestPct >= 80); return { cmid, attemptsAllowed, attemptsUsed, highestGradeStr, highestPct, hasGrade, isPassable, canReattempt, maxAttemptsReached }; } // Auto-Highlight unanswered / missing quizzes on Grades, Course, or Quiz view pages async function highlightMissingOrUnansweredQuizzes() { clearAllHighlights(); const isGrades = typeof window !== 'undefined' && window.location.pathname.includes('/grade/report/user/index.php'); const isCourse = checkIsCoursePage(); const isQuizLanding = typeof window !== 'undefined' && window.location.pathname.includes('/mod/quiz/view.php'); if (!isGrades && !isCourse && !isQuizLanding) { return { count: 0, error: 'Not on a course, grades, or quiz page', message: 'Open a course, Grades, or Quiz page first to highlight missing quizzes.' }; } let count = 0; let firstScrolled = false; if (isQuizLanding) { const data = parseQuizLandingData(document, window.location.href); if (data.cmid) { try { localStorage.setItem('amaes_quiz_attempts_' + data.cmid, JSON.stringify(data)); } catch (e) {} } if (data.isPassable) { return { count: 0, isQuizLanding: true, passed: true, message: `Quiz already completed with passing grade (${data.highestPct}% ≥ 80%)! No further attempts required.` }; } const startBtn = document.querySelector( 'form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], ' + '.quizstartbutton button, .quizstartbutton input[type="submit"], ' + '#region-main button.btn-primary, #region-main input.btn-primary' ); if (startBtn) { count++; startBtn.classList.add('amaes-highlighted-item'); startBtn.style.outline = '3px solid #f59e0b'; startBtn.style.boxShadow = '0 0 15px rgba(245, 158, 11, 0.6)'; const badge = document.createElement('span'); badge.className = 'amaes-type-badge'; badge.innerText = data.hasGrade ? `NEEDS 80%+ (Current: ${data.highestPct}%)` : 'UNATTEMPTED'; badge.style.cssText = ` background: #f59e0b; color: #000; font-size: 10px; font-weight: 800; padding: 3px 8px; border-radius: 4px; margin-left: 8px; display: inline-block; vertical-align: middle; `; startBtn.parentNode.insertBefore(badge, startBtn.nextSibling); startBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else if (data.maxAttemptsReached) { return { count: 0, isQuizLanding: true, message: `Max attempts reached (${data.attemptsUsed}/${data.attemptsAllowed}). Score: ${data.highestPct}%. No more attempts allowed.` }; } return { count, isQuizLanding: true, attemptsUsed: data.attemptsUsed, attemptsAllowed: data.attemptsAllowed }; } else if (isGrades) { const rows = Array.from(document.querySelectorAll('.user-grade tr, .generaltable tr, table.table tr, tr')); rows.forEach(r => { if (r.classList.contains('coursetotal') || r.classList.contains('category') || r.classList.contains('categoryitem') || r.classList.contains('courseitem')) return; const quizLink = r.querySelector('a[href*="/mod/quiz/"], a[href*="quiz"], a.gradeitemheader') || r.querySelector('.column-itemname a, th a, td:first-child a'); if (!quizLink) return; const href = quizLink.getAttribute('href') || quizLink.href || ''; const rawTitle = quizLink.innerText.trim(); if (!rawTitle || (!href.includes('quiz') && !r.innerText.toLowerCase().includes('quiz'))) return; // Check grade const gradeCell = r.querySelector('.column-grade, [headers*="grade"], td.grade'); const pctCell = r.querySelector('.column-percentage, [headers*="percentage"]'); let hasGrade = false; let pct = null; if (pctCell) { const pText = pctCell.innerText.trim(); const mPct = pText.match(/(\d+(?:\.\d+)?)\s*%/); if (mPct) { pct = parseFloat(mPct[1]); hasGrade = true; } } if (gradeCell) { const gText = gradeCell.innerText.trim(); if (gText && gText !== '-' && gText !== '–' && /\d/.test(gText)) { hasGrade = true; if (pct === null) { const mFrac = gText.match(/(\d+(?:\.\d+)?)\s*(?:\/|\s*out of\s*)\s*(\d+(?:\.\d+)?)/i); if (mFrac) { const earned = parseFloat(mFrac[1]); const max = parseFloat(mFrac[2]); if (max > 0) pct = Math.round((earned / max) * 100); } else { const mNum = gText.match(/(\d+(?:\.\d+)?)/); if (mNum && parseFloat(mNum[1]) <= 100) { pct = parseFloat(mNum[1]); } } } } } // Strict 80%+ Rule: Only grades >= 80% are accepted! const isAccepted = Boolean(hasGrade && pct !== null && pct >= 80); if (isAccepted) { return; // Quiz passed and accepted! Do NOT highlight! } count++; r.classList.add('amaes-highlighted-item'); r.style.background = 'rgba(245, 158, 11, 0.18)'; r.style.outline = '2px solid #f59e0b'; const badge = document.createElement('span'); badge.className = 'amaes-type-badge'; badge.innerText = (hasGrade && pct !== null) ? `NEEDS 80%+ (${pct}%)` : 'UNATTEMPTED'; badge.style.cssText = ` background: #f59e0b; color: #000; font-size: 9px; font-weight: 800; padding: 1px 5px; border-radius: 3px; margin-left: 6px; display: inline-block; vertical-align: middle; `; quizLink.appendChild(badge); if (!firstScrolled) { r.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstScrolled = true; } }); } else if (isCourse) { const gradesMap = await fetchCourseGradesMap(); const activities = Array.from(document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"]' )); const quizActivities = activities.filter(el => classifyActivity(el).type === 'quiz'); for (const el of quizActivities) { const titleElem = el.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const rawTitle = titleElem ? titleElem.innerText.trim() : (el.innerText.split('\n')[0] || ''); const gradeInfo = evaluateQuizPassableGrade(el, rawTitle, gradesMap); // 1. Strict Acceptance Guard: Overall grade >= 80% is accepted! Never highlight as missing! if (gradeInfo.hasGrade && gradeInfo.percentage !== null && gradeInfo.percentage >= 80) { continue; } // If gradesMap has no grade, check fallback completion only if gradesMap was empty if (!gradeInfo.hasGrade && (!gradesMap || Object.keys(gradesMap).length === 0)) { if (isActivityAlreadyComplete(el)) { continue; } } let cmid = gradeInfo.cmid; const linkElem = el.querySelector('a[href*="/mod/quiz/view.php?id="], a[href*="id="]'); if (!cmid && linkElem) { const m = (linkElem.getAttribute('href') || linkElem.href || '').match(/[?&]id=(\d+)/); if (m) cmid = m[1]; } // Check cached attempt details from prior visits to quiz landing let attemptsInfo = null; if (cmid) { try { const raw = localStorage.getItem('amaes_quiz_attempts_' + cmid); if (raw) attemptsInfo = JSON.parse(raw); } catch (e) {} } const isMaxReached = attemptsInfo && attemptsInfo.maxAttemptsReached; const attemptsAllowed = attemptsInfo ? attemptsInfo.attemptsAllowed : null; const attemptsUsed = attemptsInfo ? attemptsInfo.attemptsUsed : null; const pct = (attemptsInfo && attemptsInfo.highestPct !== null) ? attemptsInfo.highestPct : gradeInfo.percentage; let badgeText = 'MISSING / PENDING'; let badgeColor = '#f59e0b'; let badgeTextColor = '#000000'; if (isMaxReached) { badgeText = pct !== null ? `MAX ATTEMPTS (${pct}%)` : 'MAX ATTEMPTS REACHED'; badgeColor = '#ef4444'; badgeTextColor = '#ffffff'; } else if (pct !== null) { if (attemptsAllowed !== null && attemptsUsed !== null) { badgeText = `RE-ATTEMPT (${pct}% | ${attemptsUsed}/${attemptsAllowed})`; } else { badgeText = `NEEDS 80%+ (${pct}%)`; } } else if (attemptsAllowed !== null) { badgeText = `UNATTEMPTED (0/${attemptsAllowed})`; } else { badgeText = 'MISSING / PENDING'; } count++; el.classList.add('amaes-highlighted-item'); el.style.position = 'relative'; el.style.outline = `2px solid ${badgeColor}`; el.style.borderRadius = '8px'; el.style.boxShadow = `0 0 12px ${badgeColor}66`; const badge = document.createElement('span'); badge.className = 'amaes-type-badge'; badge.innerText = badgeText; badge.style.cssText = ` position: absolute; top: 6px; right: 6px; background: ${badgeColor}; color: ${badgeTextColor}; font-size: 9px; font-weight: 800; padding: 2px 6px; border-radius: 4px; z-index: 10; pointer-events: none; text-transform: uppercase; `; el.appendChild(badge); if (!firstScrolled) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstScrolled = true; } } } return { count, isGrades, isCourse, isQuizLanding }; } // Check if a quiz review attempt or score indicates a 100% perfect score function checkQuizAttemptIs100Percent(harvested, rootDoc = (typeof document !== 'undefined' ? document : null)) { if (!harvested && !rootDoc) return false; // 1. Check harvested grade text if (harvested && harvested.gradeText) { const gradeStr = String(harvested.gradeText).toLowerCase(); if (gradeStr.includes('100%') || gradeStr.includes('100.00%') || gradeStr.includes('100.0%')) { return true; } const matchOutOf = gradeStr.match(/(\d+(?:\.\d+)?)\s*(?:out of|\/)\s*(\d+(?:\.\d+)?)/i); if (matchOutOf) { const scored = parseFloat(matchOutOf[1]); const max = parseFloat(matchOutOf[2]); if (!isNaN(scored) && !isNaN(max) && max > 0 && scored >= max) { return true; } } } // 2. Check review summary table in DOM if (rootDoc) { const summaryTable = rootDoc.querySelector('.quizreviewsummary, table.generaltable, #region-main table'); if (summaryTable) { const text = summaryTable.innerText.toLowerCase(); if (text.includes('100%') || text.includes('100.00%')) return true; const m = text.match(/grade[\s:]*(\d+(?:\.\d+)?)\s*(?:out of|\/)\s*(\d+(?:\.\d+)?)/i); if (m) { const sc = parseFloat(m[1]); const mx = parseFloat(m[2]); if (!isNaN(sc) && !isNaN(mx) && mx > 0 && sc >= mx) return true; } } } // 3. Check harvested question count: all questions correct if (harvested && harvested.totalQuestions > 0 && harvested.harvestedCount === harvested.totalQuestions) { return true; } return false; } // Trigger manual completion toggle on the current page if activity is not yet marked Done function triggerManualCompletionOnPage() { if (typeof document === 'undefined') return false; const compButtons = document.querySelectorAll( 'button[data-action="toggle-manual-completion"], ' + 'button[data-toggletype="manual:mark-done"], ' + 'button.btn-outline-secondary, ' + 'button.btn-outline-success, ' + 'form.togglecompletion input[type="submit"], ' + 'form.togglecompletion button' ); for (const btn of compButtons) { const text = (btn.innerText || btn.getAttribute('aria-label') || btn.value || '').trim().toLowerCase(); const toggleType = (btn.getAttribute('data-toggletype') || '').toLowerCase(); const isToggleAction = btn.getAttribute('data-action') === 'toggle-manual-completion'; const isToggleCompletionForm = Boolean(btn.closest('form.togglecompletion')); const isMarkDoneText = text.includes('mark as done') || toggleType === 'manual:mark-done'; const isAlreadyDone = toggleType === 'manual:undo' || text === 'done' || text.startsWith('done') || btn.classList.contains('btn-success'); if (!isAlreadyDone && (isMarkDoneText || isToggleAction || isToggleCompletionForm)) { if (typeof triggerClick === 'function') triggerClick(btn); else btn.click(); try { btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-outline-success'); btn.style.opacity = '0.8'; } catch (_) {} if (typeof showToast === 'function') showToast("Quiz scored 100%! Auto-marked activity as Done.", 3500); if (typeof setLog === 'function') setLog("Auto-Completion: Quiz achieved 100%! Auto-marked activity as Done.", "var(--accent-green)"); return true; } } return false; } // Auto-mark activity as Done on Quiz Landing page (/mod/quiz/view.php) if 100% grade is recorded function checkAndAutoMarkDoneOnQuizLanding() { if (typeof window === 'undefined' || !window.location.pathname.includes('/mod/quiz/view.php')) return; const autoMark100 = (typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_mark_done_100') !== 'false' : true); if (!autoMark100) return; const cmid = new URLSearchParams(window.location.search).get('id') || ''; const cached100Cmid = cmid && localStorage.getItem(`amaes_quiz_100_cmid_${cmid}`) === 'true'; let rawTitle = ''; if (typeof document !== 'undefined') { const h = document.querySelector('.page-header-headings h1, #region-main h2, h2, h3'); if (h) rawTitle = h.innerText.trim(); } if (!rawTitle && typeof detectCourseInfo === 'function') { rawTitle = detectCourseInfo().currentActivityTitle || ''; } const normTitle = rawTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); const cached100Title = normTitle && localStorage.getItem(`amaes_quiz_100_title_${normTitle}`) === 'true'; const pageText = document.body ? document.body.innerText : ''; let has100Grade = /highest grade:\s*(\d+(?:\.\d+)?)\s*\/\s*\1/i.test(pageText) || /highest grade:\s*100(?:\.00)?%/i.test(pageText) || /overall feedback[^\n]*100%/i.test(pageText) || /final grade[^\n]*100%/i.test(pageText) || /grade:[^\n]*100%/i.test(pageText) || cached100Cmid || cached100Title; // Inspect previous attempts table in DOM if (!has100Grade && typeof document !== 'undefined') { const tableRows = document.querySelectorAll('table.generaltable tbody tr, .quizattemptsummary tbody tr, #region-main table tbody tr'); for (const row of tableRows) { const rText = (row.innerText || '').toLowerCase(); if (rText.includes('100%') || rText.includes('100.00%') || /\b100(?:\.00)?\b/.test(rText)) { has100Grade = true; break; } const mFrac = rText.match(/(\d+(?:\.\d+)?)\s*(?:\/|out of)\s*(\d+(?:\.\d+)?)/i); if (mFrac) { const scored = parseFloat(mFrac[1]); const max = parseFloat(mFrac[2]); if (!isNaN(scored) && !isNaN(max) && max > 0 && scored >= max) { has100Grade = true; break; } } } } if (has100Grade) { if (cmid) localStorage.setItem(`amaes_quiz_100_cmid_${cmid}`, 'true'); if (normTitle) localStorage.setItem(`amaes_quiz_100_title_${normTitle}`, 'true'); const done = triggerManualCompletionOnPage(); if (!done) { setTimeout(() => triggerManualCompletionOnPage(), 500); setTimeout(() => triggerManualCompletionOnPage(), 1200); } } } // Auto-mark completed 100% quizzes on Course page (/course/view.php) function scanAndAutoMark100PercentQuizzesOnCoursePage() { if (typeof checkIsCoursePage === 'function' && !checkIsCoursePage()) return; const autoMark100 = (typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_mark_done_100') !== 'false' : true); if (!autoMark100) return; const activityElements = document.querySelectorAll( 'li.activity, .activity-item, .course-section .activity, div[data-region="activity-card"]' ); if (!activityElements || activityElements.length === 0) return; let markedAny = false; activityElements.forEach(container => { if (!container) return; if (isActivityAlreadyComplete(container)) return; if (isActivityLockedOrRestricted(container)) return; const classification = classifyActivity(container); if (classification.type !== 'quiz') return; const titleElem = container.querySelector('.instancename, .activityname, a.aal_link, .activity-title'); const title = titleElem ? titleElem.innerText.trim() : (container.innerText.split('\n')[0] || 'Quiz'); const gradeInfo = evaluateQuizPassableGrade(container, title); if (!gradeInfo.isPassable) return; // Find uncompleted manual completion button const buttons = container.querySelectorAll( 'button[data-action="toggle-manual-completion"], ' + 'button[data-toggletype="manual:mark-done"], ' + 'button.btn-outline-secondary, ' + 'form.togglecompletion input[type="submit"], ' + 'form.togglecompletion button' ); for (const btn of buttons) { const text = (btn.innerText || btn.getAttribute('aria-label') || btn.value || '').trim().toLowerCase(); const toggleType = (btn.getAttribute('data-toggletype') || '').toLowerCase(); const isToggleAction = btn.getAttribute('data-action') === 'toggle-manual-completion'; const isToggleCompletionForm = Boolean(btn.closest('form.togglecompletion')); const isMarkDoneText = text.includes('mark as done') || text === 'mark as done' || toggleType === 'manual:mark-done'; const isAlreadyDone = toggleType === 'manual:undo' || text === 'done' || text.startsWith('done') || btn.classList.contains('btn-success'); if (!isAlreadyDone && (isMarkDoneText || isToggleAction || isToggleCompletionForm)) { try { btn.click(); } catch (_) { if (typeof triggerClick === 'function') triggerClick(btn); } try { btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-outline-success'); btn.style.opacity = '0.8'; } catch (_) {} markedAny = true; if (typeof setLog === 'function') { setLog(`100% Quiz Auto-Marked: ${title.substring(0, 30)} marked as Done.`, "var(--accent-green)"); } break; } } }); if (markedAny && typeof showToast === 'function') { showToast("Auto-marked completed 100% quiz as Done!", 3000); } } const ACCOUNT_SWITCHER_ACCOUNTS_KEY = 'amaes_account_switcher_accounts'; const ACCOUNT_SWITCHER_RETURN_KEY = 'amaes_account_switcher_return_url'; const ACCOUNT_SWITCHER_PENDING_KEY = 'amaes_account_switcher_pending'; const ACCOUNT_SWITCHER_RETURN_OPTION_KEY = 'amaes_account_switcher_return_enabled'; const ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY = 'amaes_account_switcher_login_capture'; const ACCOUNT_SWITCHER_INTENT_TTL_MS = 10 * 60 * 1000; async function getAccountSwitcherValue(key, fallbackValue = null) { if (typeof GM_getValue !== 'function') { throw new Error('Account Switcher requires a userscript manager with GM storage support.'); } return await GM_getValue(key, fallbackValue); } async function setAccountSwitcherValue(key, value) { if (typeof GM_setValue !== 'function') { throw new Error('Account Switcher requires a userscript manager with GM storage support.'); } await GM_setValue(key, value); } async function deleteAccountSwitcherValue(key) { if (typeof GM_deleteValue !== 'function') { throw new Error('Account Switcher requires a userscript manager with GM storage support.'); } await GM_deleteValue(key); } async function getAccountSwitcherAccounts() { const stored = await getAccountSwitcherValue(ACCOUNT_SWITCHER_ACCOUNTS_KEY, []); const accounts = typeof stored === 'string' ? JSON.parse(stored) : stored; if (!Array.isArray(accounts)) { throw new Error('Saved Account Switcher data is invalid. Remove and re-add the affected accounts.'); } return accounts.filter(account => account && typeof account.id === 'string' && typeof account.username === 'string' && typeof account.password === 'string' && typeof account.nickname === 'string' ); } async function saveAccountSwitcherAccounts(accounts) { await setAccountSwitcherValue(ACCOUNT_SWITCHER_ACCOUNTS_KEY, accounts); } function accountSwitcherSetStatus(message, isError = false) { const status = document.getElementById('amaes-account-switcher-status'); if (status) { status.textContent = message; status.style.color = isError ? 'var(--accent-pink, #f43f5e)' : 'var(--text-secondary)'; } if (isError) console.error(`[AMAES Account Switcher] ${message}`); } function accountSwitcherIsVisible(element) { return Boolean(element && element.getClientRects().length); } function findAccountSwitcherLogoutLink() { const links = Array.from(document.querySelectorAll('a[href]')); const hrefMatch = links.find(link => { if (!accountSwitcherIsVisible(link)) return false; try { const url = new URL(link.href, window.location.href); return url.origin === window.location.origin && /\/logout\.php$/i.test(url.pathname); } catch (_) { return false; } }); if (hrefMatch) return hrefMatch; const menu = document.querySelector('.usermenu .dropdown-menu, [data-region="popover-region-container"], [data-region="user-menu"]'); if (!menu) return null; return Array.from(menu.querySelectorAll('a, button')).find(element => accountSwitcherIsVisible(element) && /^log\s*out$/i.test((element.textContent || '').trim()) ) || null; } function findAccountSwitcherProfileToggle() { const selectors = [ '#user-menu-toggle', '[data-region="user-menu-toggle"]', '.usermenu .dropdown-toggle', '#action-menu-toggle-0' ]; for (const selector of selectors) { const toggle = document.querySelector(selector); if (accountSwitcherIsVisible(toggle)) return toggle; } return null; } function findAccountSwitcherLogoutConfirmation() { const candidates = Array.from(document.querySelectorAll( 'button, input[type="submit"], input[type="button"], a.btn, [role="button"]' )); return candidates.find(element => { const label = (element.textContent || element.value || '').trim(); return accountSwitcherIsVisible(element) && /^log\s*out$/i.test(label); }) || null; } function findAccountSwitcherLoginFields() { const username = document.querySelector( '#username, input[name="username"], input[autocomplete="username"]' ); const password = document.querySelector( '#password, input[name="password"], input[autocomplete="current-password"]' ); if (!username || !password || !username.form || username.form !== password.form) return null; return { username, password, form: username.form }; } function getCurrentAccountSwitcherProfileName() { const selectors = [ '.usermenu .usertext', '[data-region="user-menu-toggle"] .usertext', '#user-menu-toggle .usertext', '.logininfo a[href*="/user/profile.php"]', '#user-menu-toggle', '[data-region="user-menu-toggle"]' ]; for (const selector of selectors) { const element = document.querySelector(selector); const value = (element?.getAttribute('aria-label') || element?.textContent || '').replace(/\s+/g, ' ').trim(); if (value && !/^user menu$/i.test(value)) return value; } return ''; } function normalizeAccountSwitcherIdentity(value) { return String(value || '').trim().replace(/\s+/g, ' ').toLocaleLowerCase(); } async function setupAccountSwitcherLoginCapture() { if (typeof isSupportedMoodleHost === 'function' ? !isSupportedMoodleHost(window.location.hostname) : (window.location.hostname !== 'semestral.amaes.com' && window.location.hostname !== 'semestralexam.amaes.com')) return; const loginFields = findAccountSwitcherLoginFields(); if (!loginFields || loginFields.form.querySelector('#amaes-login-save-account')) return; try { const previousCapture = await getAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY, null); if (previousCapture) await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY); } catch (error) { console.error(`[AMAES Account Switcher] Could not clear an earlier login save: ${error.message || error}`); } const container = document.createElement('div'); container.style.cssText = 'margin: 10px 0; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px;'; let savedAccounts = []; try { savedAccounts = await getAccountSwitcherAccounts(); } catch (error) { console.error(`[AMAES Account Switcher] Could not load quick-login accounts: ${error.message || error}`); } if (savedAccounts.length > 0) { const quickSwitch = document.createElement('div'); quickSwitch.style.cssText = 'display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-bottom: 8px;'; const quickSwitchLabel = document.createElement('span'); quickSwitchLabel.textContent = 'Quick sign in:'; quickSwitchLabel.style.cssText = 'font-size: 12px; font-weight: 600; color: #374151;'; quickSwitch.appendChild(quickSwitchLabel); savedAccounts.forEach(account => { const quickLoginButton = document.createElement('button'); quickLoginButton.type = 'button'; quickLoginButton.textContent = account.nickname; quickLoginButton.title = `Sign in as ${account.nickname}`; quickLoginButton.style.cssText = 'max-width: 100%; padding: 5px 8px; border: 1px solid #9ca3af; border-radius: 5px; background: #f3f4f6; color: #111827; font-size: 12px; cursor: pointer;'; quickLoginButton.addEventListener('click', () => { accountSwitcherSetInputValue(loginFields.username, account.username); accountSwitcherSetInputValue(loginFields.password, account.password); loginFields.form.requestSubmit(); }); quickSwitch.appendChild(quickLoginButton); }); container.appendChild(quickSwitch); } const label = document.createElement('label'); label.style.cssText = 'display: flex; align-items: flex-start; gap: 7px; cursor: pointer;'; const checkbox = document.createElement('input'); checkbox.id = 'amaes-login-save-account'; checkbox.type = 'checkbox'; checkbox.style.cssText = 'margin: 2px 0 0; flex: 0 0 auto;'; const labelText = document.createElement('span'); labelText.textContent = 'Save this account on this device for quick switching'; label.append(checkbox, labelText); const nicknameInput = document.createElement('input'); nicknameInput.id = 'amaes-login-account-nickname'; nicknameInput.type = 'text'; nicknameInput.autocomplete = 'off'; nicknameInput.placeholder = 'Display name for your account'; nicknameInput.setAttribute('aria-label', 'Account display name'); nicknameInput.style.cssText = 'display: none; box-sizing: border-box; width: 100%; margin-top: 7px; padding: 6px 7px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 12px;'; const feedback = document.createElement('div'); feedback.setAttribute('role', 'status'); feedback.style.cssText = 'display: none; margin-top: 5px; color: #b91c1c; font-size: 11px;'; checkbox.addEventListener('change', () => { nicknameInput.style.display = checkbox.checked ? 'block' : 'none'; if (checkbox.checked && !nicknameInput.value.trim()) { nicknameInput.value = loginFields.username.value.trim(); } }); container.append(label, nicknameInput, feedback); loginFields.password.insertAdjacentElement('afterend', container); let isSavingLoginCapture = false; let allowCaptureSubmission = false; loginFields.form.addEventListener('submit', event => { if (!checkbox.checked) return; if (isSavingLoginCapture) { if (allowCaptureSubmission) { allowCaptureSubmission = false; } else { event.preventDefault(); } return; } const username = loginFields.username.value.trim(); const password = loginFields.password.value; const nickname = nicknameInput.value.trim() || username; if (!username || !password || !nickname) return; event.preventDefault(); isSavingLoginCapture = true; const captureExpiresAt = Date.now() + ACCOUNT_SWITCHER_INTENT_TTL_MS; setAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY, { username, password, nickname, nicknameFromUsername: nickname === username, expiresAt: captureExpiresAt }).then(() => { setTimeout(async () => { try { if (window.location.pathname.includes('/login/')) { const capture = await getAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY, null); if (capture?.expiresAt === captureExpiresAt) { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY); } } } catch (error) { console.error(`[AMAES Account Switcher] Could not clear an unsuccessful login save: ${error.message || error}`); } }, 20000); allowCaptureSubmission = true; loginFields.form.requestSubmit(event.submitter || undefined); }) .catch(error => { feedback.textContent = `${error.message || 'Could not save this account.'} Continuing login without saving.`; feedback.style.display = 'block'; isSavingLoginCapture = true; allowCaptureSubmission = true; loginFields.form.requestSubmit(event.submitter || undefined); }); }); } async function completeAccountSwitcherLoginCapture() { try { const capture = await getAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY, null); if (!capture) return false; if (!capture.expiresAt || capture.expiresAt < Date.now()) { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY); return false; } const accounts = await getAccountSwitcherAccounts(); const profileName = getCurrentAccountSwitcherProfileName(); const nickname = capture.nicknameFromUsername && profileName ? profileName : capture.nickname; const existing = accounts.find(account => account.username.toLocaleLowerCase() === capture.username.toLocaleLowerCase()); if (existing) { existing.password = capture.password; existing.nickname = nickname; await saveAccountSwitcherAccounts(accounts); } else { const id = crypto.randomUUID ? crypto.randomUUID() : `account-${Date.now()}-${Math.random().toString(36).slice(2)}`; accounts.push({ id, username: capture.username, password: capture.password, nickname }); await saveAccountSwitcherAccounts(accounts); } await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_LOGIN_CAPTURE_KEY); return true; } catch (error) { console.error(`[AMAES Account Switcher] Could not save the account after login: ${error.message || error}`); return false; } } function accountSwitcherSetInputValue(input, value) { const prototype = Object.getPrototypeOf(input); const valueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set; if (valueSetter) valueSetter.call(input, value); else input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } async function waitForAccountSwitcherElement(findElement, timeoutMs = 7000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const element = findElement(); if (element) return element; await new Promise(resolve => setTimeout(resolve, 150)); } return null; } async function startAccountSwitch(accountId, returnToCurrentPage) { try { const accounts = await getAccountSwitcherAccounts(); const account = accounts.find(saved => saved.id === accountId); if (!account) throw new Error('The selected account could not be found.'); if (returnToCurrentPage) { await setAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY, { url: window.location.href, expiresAt: Date.now() + ACCOUNT_SWITCHER_INTENT_TTL_MS }); } else { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); } await setAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY, { accountId: account.id, stage: 'logout', createdAt: Date.now() }); let logoutLink = findAccountSwitcherLogoutLink(); if (!logoutLink) { const profileToggle = findAccountSwitcherProfileToggle(); if (!profileToggle) throw new Error('Could not find the Moodle profile menu.'); profileToggle.click(); logoutLink = await waitForAccountSwitcherElement(findAccountSwitcherLogoutLink); } if (!logoutLink) throw new Error('Could not find the Log out option in the Moodle profile menu.'); accountSwitcherSetStatus(`Signing out before switching to ${account.nickname}…`); logoutLink.click(); } catch (error) { try { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); } catch (cleanupError) { console.error(`[AMAES Account Switcher] Could not clear incomplete switch state: ${cleanupError.message || cleanupError}`); } accountSwitcherSetStatus(error.message || 'Could not start account switching.', true); } } async function handleAccountSwitcherNavigation() { if (typeof isSupportedMoodleHost === 'function' ? !isSupportedMoodleHost(window.location.hostname) : (window.location.hostname !== 'semestral.amaes.com' && window.location.hostname !== 'semestralexam.amaes.com')) return; let pending; try { pending = await getAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY, null); if (!pending || typeof pending !== 'object') return; if (!pending.createdAt || Date.now() - pending.createdAt > ACCOUNT_SWITCHER_INTENT_TTL_MS) { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); return; } if (!window.location.pathname.includes('/login/')) return; if (pending.stage === 'attempted') { try { const accounts = await getAccountSwitcherAccounts(); const acc = accounts.find(saved => saved.id === pending.accountId); if (acc) { acc.lastLoginFailed = true; await saveAccountSwitcherAccounts(accounts); } } catch (_) {} await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); accountSwitcherSetStatus('Login did not complete. Check the credentials and try again.', true); return; } let loginFields = findAccountSwitcherLoginFields(); if (!loginFields) { let confirmation = findAccountSwitcherLogoutConfirmation(); if (confirmation) { confirmation.click(); return; } loginFields = await waitForAccountSwitcherElement(findAccountSwitcherLoginFields, 9000); if (!loginFields) { confirmation = findAccountSwitcherLogoutConfirmation(); if (confirmation) confirmation.click(); return; } } const accounts = await getAccountSwitcherAccounts(); const account = accounts.find(saved => saved.id === pending.accountId); if (!account) { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); throw new Error('The selected account no longer exists.'); } await setAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY, { ...pending, stage: 'attempted' }); accountSwitcherSetInputValue(loginFields.username, account.username); accountSwitcherSetInputValue(loginFields.password, account.password); const submitButton = loginFields.form.querySelector( 'button[type="submit"], input[type="submit"], #loginbtn' ); if (typeof loginFields.form.requestSubmit === 'function') { loginFields.form.requestSubmit(submitButton || undefined); } else if (submitButton) { submitButton.click(); } else { throw new Error('Could not find a login submit button.'); } setTimeout(async () => { try { if (!window.location.pathname.includes('/login/')) return; const latestPending = await getAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY, null); if (latestPending?.stage === 'attempted') { await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); accountSwitcherSetStatus('Login did not complete. Check the credentials and try again.', true); try { const currentAccounts = await getAccountSwitcherAccounts(); const acc = currentAccounts.find(saved => saved.id === pending.accountId); if (acc) { acc.lastLoginFailed = true; await saveAccountSwitcherAccounts(currentAccounts); } } catch (_) {} } } catch (error) { console.error(`[AMAES Account Switcher] Could not clear a failed login attempt: ${error.message || error}`); } }, 10000); } catch (error) { console.error(`[AMAES Account Switcher] ${error.message || error}`); } } async function completeAccountSwitcherReturn() { try { if (typeof GM_getValue !== 'function' || typeof GM_deleteValue !== 'function') return false; const pending = await getAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY, null); if (!pending || pending.stage !== 'attempted') return false; if (pending.accountId) { try { const accounts = await getAccountSwitcherAccounts(); const acc = accounts.find(a => a.id === pending.accountId); if (acc && acc.lastLoginFailed) { delete acc.lastLoginFailed; await saveAccountSwitcherAccounts(accounts); } } catch (_) {} } await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_PENDING_KEY); const returnTarget = await getAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY, null); await deleteAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_KEY); if (!returnTarget || typeof returnTarget.url !== 'string' || returnTarget.expiresAt < Date.now()) { return false; } const target = new URL(returnTarget.url); if (target.origin !== window.location.origin) { console.error('[AMAES Account Switcher] Refusing to restore a return address on another site.'); return false; } window.location.replace(target.href); return true; } catch (error) { console.error(`[AMAES Account Switcher] Could not restore the saved return address: ${error.message || error}`); return false; } } async function setupAccountSwitcherUI() { const form = document.getElementById('amaes-account-switcher-form'); const list = document.getElementById('amaes-account-switcher-list'); const returnToggle = document.getElementById('amaes-account-switcher-return'); const submitButton = document.getElementById('amaes-account-switcher-submit'); const cancelEditButton = document.getElementById('amaes-account-switcher-cancel-edit'); if (!form || !list || !returnToggle || !submitButton || !cancelEditButton) return; let editingAccountId = null; const resetAccountForm = () => { editingAccountId = null; form.reset(); submitButton.textContent = 'Add account'; cancelEditButton.style.display = 'none'; }; const renderAccounts = async () => { const accounts = await getAccountSwitcherAccounts(); const currentProfileName = normalizeAccountSwitcherIdentity(getCurrentAccountSwitcherProfileName()); list.replaceChildren(); if (accounts.length === 0) { const empty = document.createElement('div'); empty.textContent = 'No saved accounts yet.'; empty.style.cssText = 'font-size: 10px; color: var(--text-muted); padding: 4px 2px;'; list.appendChild(empty); return; } accounts.forEach(account => { const row = document.createElement('div'); row.style.cssText = 'display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 5px; align-items: center; width: 100%;'; const switchButton = document.createElement('button'); switchButton.type = 'button'; switchButton.className = 'amaes-btn amaes-btn-monotone'; switchButton.textContent = account.nickname; switchButton.title = `Switch to ${account.nickname}`; switchButton.style.cssText = 'width: 100%; justify-content: flex-start; min-width: 0; overflow: hidden; text-overflow: ellipsis;'; switchButton.addEventListener('click', () => { startAccountSwitch(account.id, returnToggle.checked); }); const status = document.createElement('span'); const isCurrentAccount = Boolean(currentProfileName && ( currentProfileName === normalizeAccountSwitcherIdentity(account.nickname) || currentProfileName === normalizeAccountSwitcherIdentity(account.username) )); if (account.lastLoginFailed) { status.textContent = 'Login Failed'; status.title = 'Last login attempt failed. Click Edit to update your password or username.'; status.style.cssText = 'font-size: 8.5px; font-weight: 700; white-space: nowrap; color: #ef4444; background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.35); padding: 1px 4px; border-radius: 3px;'; } else { status.textContent = isCurrentAccount ? 'Active' : 'Saved'; status.title = isCurrentAccount ? 'Matches the name shown in the current Moodle profile.' : 'Saved in this browser; not matched to the current Moodle profile name.'; status.style.cssText = `font-size: 9px; white-space: nowrap; color: ${isCurrentAccount ? 'var(--accent-green, #22c55e)' : 'var(--text-muted)'};`; } const editButton = document.createElement('button'); editButton.type = 'button'; editButton.className = 'amaes-btn amaes-btn-outline'; editButton.textContent = 'Edit'; editButton.setAttribute('aria-label', `Edit ${account.nickname}`); editButton.style.cssText = 'width: auto; white-space: nowrap; padding: 4px 7px; font-size: 9px;'; editButton.addEventListener('click', async () => { editingAccountId = account.id; document.getElementById('amaes-account-switcher-username').value = account.username; document.getElementById('amaes-account-switcher-password').value = account.password; document.getElementById('amaes-account-switcher-nickname').value = account.nickname; submitButton.textContent = 'Save changes'; cancelEditButton.style.display = 'block'; document.getElementById('amaes-account-switcher-username').focus(); accountSwitcherSetStatus(`Editing ${account.nickname}.`); }); const removeButton = document.createElement('button'); removeButton.type = 'button'; removeButton.className = 'amaes-btn amaes-btn-outline'; removeButton.textContent = 'Remove'; removeButton.setAttribute('aria-label', `Remove ${account.nickname}`); removeButton.style.cssText = 'width: auto; white-space: nowrap; padding: 4px 7px; font-size: 9px;'; removeButton.addEventListener('click', async () => { try { const latest = await getAccountSwitcherAccounts(); const remainingAccounts = latest.filter(saved => saved.id !== account.id); await saveAccountSwitcherAccounts(remainingAccounts); if (editingAccountId === account.id) resetAccountForm(); await renderAccounts(); accountSwitcherSetStatus(`${account.nickname} removed.`); } catch (error) { accountSwitcherSetStatus(error.message || 'Could not remove the saved account.', true); } }); row.append(switchButton, status, editButton, removeButton); list.appendChild(row); }); }; try { returnToggle.checked = (await getAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_OPTION_KEY, false)) === true; returnToggle.addEventListener('change', async () => { try { await setAccountSwitcherValue(ACCOUNT_SWITCHER_RETURN_OPTION_KEY, returnToggle.checked); } catch (error) { accountSwitcherSetStatus(error.message || 'Could not save the return-page setting.', true); } }); cancelEditButton.addEventListener('click', resetAccountForm); form.addEventListener('submit', async event => { event.preventDefault(); const usernameInput = document.getElementById('amaes-account-switcher-username'); const passwordInput = document.getElementById('amaes-account-switcher-password'); const nicknameInput = document.getElementById('amaes-account-switcher-nickname'); const username = usernameInput.value.trim(); const password = passwordInput.value; const nickname = nicknameInput.value.trim() || username; if (!username || !password) { accountSwitcherSetStatus('Enter a username and password.', true); return; } try { const wasEditing = Boolean(editingAccountId); let accounts = await getAccountSwitcherAccounts(); const isDuplicate = accounts.some(a => a.id !== editingAccountId && a.username.trim().toLowerCase() === username.toLowerCase() ); if (isDuplicate) { accountSwitcherSetStatus(`An account with username "${username}" already exists.`, true); return; } const id = editingAccountId || (crypto.randomUUID ? crypto.randomUUID() : `account-${Date.now()}-${Math.random().toString(36).slice(2)}`); const updatedAccount = { id, username, password, nickname }; if (editingAccountId) { accounts = accounts.map(account => account.id === editingAccountId ? updatedAccount : account); } else { accounts.push(updatedAccount); } await saveAccountSwitcherAccounts(accounts); resetAccountForm(); await renderAccounts(); accountSwitcherSetStatus(`${nickname} ${wasEditing ? 'updated' : 'saved'} on this device.`); } catch (error) { accountSwitcherSetStatus(error.message || 'Could not save the account.', true); } }); await renderAccounts(); } catch (error) { accountSwitcherSetStatus(error.message || 'Could not load saved accounts.', true); } } // ========================================== // Encrypted one-time account/settings transfer // ========================================== const ACCOUNT_TRANSFER_FORMAT = 'amaes-toolkit-transfer'; const ACCOUNT_TRANSFER_VERSION = 1; const ACCOUNT_TRANSFER_SETTINGS_KEYS = [ 'amaes_toolkit_theme', 'amaes_auto_highlight_quiz', 'amaes_auto_copy_ai', 'amaes_auto_quiz_mode', 'amaes_fast_quiz_mode', 'amaes_auto_pick_quiz', 'amaes_auto_next_verified', 'amaes_auto_pick_study_guide_fallback', 'amaes_auto_next_quiz', 'amaes_adaptive_probe_quiz', 'amaes_adaptive_probe_budget', 'amaes_quiz_personality', 'amaes_auto_submit_quiz', 'amaes_smart_skip_quiz', 'amaes_auto_cloud_sync', 'amaes_auto_scrape_amauoed', 'amaes_auto_harvest_grades', 'amaes_enable_hotkeys', 'amaes_auto_copy_search', 'amaes_copy_include_confidence', 'amaes_show_in_question_ai_btns', 'amaes_ai_prompt_hint', 'amaes_auto_community_share', 'amaes_auto_min_quiz', 'amaes_enable_audio_alerts', 'amaes_ai_quiz_enabled', 'amaes_ai_auto_select', 'amaes_ai_retry_count', 'amaes_ai_auto_copy_on_fail', 'amaes_ai_auto_next_on_ai', 'amaes_ai_plan_tier', 'amaes_shared_ai_fallback_enabled', 'amaes_account_switcher_return_enabled', 'amaes_preferred_web_ai', 'amaes_pref_minimized', 'amaes_pref_show_logs', 'amaes_active_tab' ]; let pendingAccountTransfer = null; function accountTransferStatus(message, isError = false) { const status = document.getElementById('amaes-account-transfer-status'); if (status) { status.textContent = message; status.style.color = isError ? 'var(--accent-pink, #f43f5e)' : 'var(--text-secondary)'; } } function accountTransferBase64Url(bytes) { let binary = ''; bytes.forEach(byte => { binary += String.fromCharCode(byte); }); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function accountTransferDecodeBase64Url(value) { if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) { throw new Error('Transfer data has an invalid encoding.'); } const base64 = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4); const binary = atob(base64); return Uint8Array.from(binary, character => character.charCodeAt(0)); } async function accountTransferLookupHash(codeBytes) { const prefix = new TextEncoder().encode('AMAES transfer lookup v1:'); const input = new Uint8Array(prefix.length + codeBytes.length); input.set(prefix); input.set(codeBytes, prefix.length); return accountTransferBase64Url(new Uint8Array(await crypto.subtle.digest('SHA-256', input))); } function accountTransferValidateSettings(settings) { if (!settings || typeof settings !== 'object' || Array.isArray(settings)) { throw new Error('The transfer does not contain valid settings.'); } if (Object.keys(settings).some(key => !ACCOUNT_TRANSFER_SETTINGS_KEYS.includes(key))) { throw new Error('The transfer contains an unsupported setting.'); } const result = {}; for (const key of ACCOUNT_TRANSFER_SETTINGS_KEYS) { if (!Object.prototype.hasOwnProperty.call(settings, key)) continue; const value = settings[key]; if (typeof value !== 'string' || value.length > 128) { throw new Error(`Invalid setting value for ${key}.`); } if (key === 'amaes_toolkit_theme' && !['dark', 'light'].includes(value)) { throw new Error('The transferred theme value is invalid.'); } if (key === 'amaes_quiz_personality' && !['passive', 'aggressive', 'active', 'balanced'].includes(value)) { throw new Error('The transferred quiz preference is invalid.'); } if (key === 'amaes_ai_plan_tier' && !['free', 'paid'].includes(value)) { throw new Error('The transferred AI plan preference is invalid.'); } if (key === 'amaes_adaptive_probe_budget' && !/^[1-9]\d?$/.test(value)) { throw new Error('The transferred probe setting is invalid.'); } if (key === 'amaes_ai_retry_count' && !/^[1-9]\d?$/.test(value)) { throw new Error('The transferred retry setting is invalid.'); } if (key === 'amaes_preferred_web_ai' && !['chatgpt', 'perplexity', 'gemini'].includes(value)) { throw new Error('The transferred preferred AI setting is invalid.'); } if (key === 'amaes_active_tab') { result[key] = ['quiz', 'db', 'course', 'accounts'].includes(value) ? value : 'quiz'; continue; } if (key !== 'amaes_toolkit_theme' && key !== 'amaes_quiz_personality' && key !== 'amaes_ai_plan_tier' && key !== 'amaes_adaptive_probe_budget' && key !== 'amaes_ai_retry_count' && key !== 'amaes_preferred_web_ai' && key !== 'amaes_active_tab' && !['true', 'false'].includes(value)) { throw new Error(`Invalid boolean setting for ${key}.`); } result[key] = value; } return result; } function accountTransferValidatePayload(payload) { const allowedPayloadKeys = ['format', 'version', 'createdAt', 'accounts', 'apiKeys', 'settings']; if (!payload || payload.format !== ACCOUNT_TRANSFER_FORMAT || payload.version !== ACCOUNT_TRANSFER_VERSION || Object.keys(payload).some(key => !allowedPayloadKeys.includes(key)) || typeof payload.createdAt !== 'string' || Number.isNaN(Date.parse(payload.createdAt))) { throw new Error('Unsupported or invalid transfer code.'); } const accounts = payload.accounts; const apiKeys = payload.apiKeys; if (!Array.isArray(accounts) || accounts.length > 30 || accounts.some(account => !account || typeof account.id !== 'string' || Object.keys(account).some(key => !['id', 'username', 'password', 'nickname'].includes(key)) || account.id.length > 128 || typeof account.username !== 'string' || !account.username.trim() || account.username.length > 256 || typeof account.password !== 'string' || !account.password || account.password.length > 2048 || typeof account.nickname !== 'string' || !account.nickname.trim() || account.nickname.length > 128)) { throw new Error('The transfer contains invalid account profiles.'); } if (!Array.isArray(apiKeys) || apiKeys.length > 10 || apiKeys.some(key => typeof key !== 'string' || key.length < 10 || key.length > 512)) { throw new Error('The transfer contains invalid API keys.'); } return { format: payload.format, version: payload.version, createdAt: payload.createdAt, accounts: accounts.map(account => ({ id: account.id, username: account.username, password: account.password, nickname: account.nickname })), apiKeys: Array.from(new Set(apiKeys)), settings: accountTransferValidateSettings(payload.settings) }; } async function accountTransferEncrypt(codeBytes, payload) { const key = await crypto.subtle.importKey('raw', codeBytes, { name: 'AES-GCM' }, false, ['encrypt']); const iv = crypto.getRandomValues(new Uint8Array(12)); const plaintext = new TextEncoder().encode(JSON.stringify(payload)); if (plaintext.length > 65_536) throw new Error('Selected transfer data exceeds the 64 KiB secure transfer limit.'); const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext); return { iv: accountTransferBase64Url(iv), ciphertext: accountTransferBase64Url(new Uint8Array(ciphertext)) }; } async function accountTransferDecrypt(codeBytes, encrypted) { const iv = accountTransferDecodeBase64Url(encrypted.iv); const ciphertext = accountTransferDecodeBase64Url(encrypted.ciphertext); if (iv.length !== 12 || ciphertext.length < 16 || ciphertext.length > 65_552) { throw new Error('Transfer data is invalid or too large.'); } const key = await crypto.subtle.importKey('raw', codeBytes, { name: 'AES-GCM' }, false, ['decrypt']); let plaintext; try { plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext); } catch (_) { throw new Error('Transfer code is incorrect or the encrypted data was altered.'); } let payload; try { payload = JSON.parse(new TextDecoder().decode(plaintext)); } catch (_) { throw new Error('Decrypted transfer data is not valid JSON.'); } return accountTransferValidatePayload(payload); } async function accountTransferReadSelectedData(includeAccounts, includeApiKeys, includeSettings) { const savedAccounts = includeAccounts ? await getAccountSwitcherAccounts() : []; const apiKeys = includeApiKeys && typeof getGeminiApiKeys === 'function' ? getGeminiApiKeys() : []; const settings = includeSettings ? Object.fromEntries(ACCOUNT_TRANSFER_SETTINGS_KEYS .filter(key => key !== 'amaes_account_switcher_return_enabled') .map(key => [ key, key === 'amaes_active_tab' && localStorage.getItem(key) === 'accounts' ? 'quiz' : localStorage.getItem(key) ]) .filter(([, value]) => value !== null)) : {}; if (includeSettings) { const returnEnabled = await getAccountSwitcherValue('amaes_account_switcher_return_enabled', null); if (returnEnabled !== null) { settings.amaes_account_switcher_return_enabled = String(returnEnabled === true || returnEnabled === 'true'); } } return { format: ACCOUNT_TRANSFER_FORMAT, version: ACCOUNT_TRANSFER_VERSION, createdAt: new Date().toISOString(), accounts: savedAccounts.map(account => ({ id: account.id, username: account.username, password: account.password, nickname: account.nickname })), apiKeys: apiKeys.filter(key => typeof key === 'string' && key.trim()).map(key => key.trim()), settings }; } async function accountTransferPost(path, body) { const response = await fetch(`${communityRelayUrl}/transfer/${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': CLIENT_VERSION, 'X-AMAES-Installation': getAnonymousContributorId() }, body: JSON.stringify(body) }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.error || `Secure transfer request failed (${response.status}).`); return result; } async function createAccountTransferCode() { const includeAccounts = document.getElementById('amaes-transfer-include-accounts')?.checked ?? true; const includeApiKeys = document.getElementById('amaes-transfer-include-api-keys')?.checked ?? true; const includeSettings = document.getElementById('amaes-transfer-include-settings')?.checked ?? true; if (!includeAccounts && !includeApiKeys && !includeSettings) { throw new Error('Select at least one item to transfer.'); } const consentEl = document.getElementById('amaes-transfer-secret-consent'); if (consentEl && !consentEl.checked) { consentEl.checked = true; } const codeBytes = crypto.getRandomValues(new Uint8Array(32)); const code = accountTransferBase64Url(codeBytes); const payload = accountTransferValidatePayload( await accountTransferReadSelectedData(includeAccounts, includeApiKeys, includeSettings) ); const encrypted = await accountTransferEncrypt(codeBytes, payload); const lookupHash = await accountTransferLookupHash(codeBytes); await accountTransferPost('create', { lookupHash, ...encrypted }); const output = document.getElementById('amaes-transfer-code'); if (output) { output.value = code; output.hidden = false; output.select(); } const copyButton = document.getElementById('amaes-transfer-copy'); if (copyButton) copyButton.hidden = false; try { if (typeof GM_setClipboard === 'function') GM_setClipboard(code); else if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(code); accountTransferStatus('Code copied to clipboard! On your other device, paste it in "Receive Setup".'); if (typeof showToast === 'function') showToast('Transfer code created & copied to clipboard!', 4000); } catch (_) { accountTransferStatus('Code created. Click Copy Code or copy the text above, then paste it in "Receive Setup" on your other device.'); } } async function importAccountTransferCode() { const input = document.getElementById('amaes-transfer-import-code'); const code = input ? input.value.trim() : ''; if (!code) throw new Error('Please paste your transfer code first.'); const codeBytes = accountTransferDecodeBase64Url(code); if (codeBytes.length !== 32) throw new Error('Invalid code length. Make sure to paste the complete code.'); const lookupHash = await accountTransferLookupHash(codeBytes); const encrypted = await accountTransferPost('consume', { lookupHash }); const payload = await accountTransferDecrypt(codeBytes, encrypted); const preview = document.getElementById('amaes-transfer-preview'); if (preview) { preview.textContent = `Found ${payload.accounts.length} account(s), ${payload.apiKeys.length} API key(s), and ${Object.keys(payload.settings).length} setting(s). Click "Confirm & Load Setup" below to apply.`; preview.hidden = false; } if (input) input.value = ''; const importBtn = document.getElementById('amaes-transfer-import'); if (importBtn) importBtn.hidden = true; const applyBtn = document.getElementById('amaes-transfer-apply'); if (applyBtn) { applyBtn.hidden = false; applyBtn.focus(); } const discardBtn = document.getElementById('amaes-transfer-discard'); if (discardBtn) discardBtn.hidden = false; pendingAccountTransfer = payload; } async function applyAccountTransfer() { const payload = pendingAccountTransfer; if (!payload) throw new Error('There is no decrypted transfer awaiting confirmation.'); const existing = await getAccountSwitcherAccounts(); const byUsername = new Map(existing.map(account => [account.username.toLocaleLowerCase(), account])); payload.accounts.forEach(account => byUsername.set(account.username.toLocaleLowerCase(), account)); await saveAccountSwitcherAccounts(Array.from(byUsername.values())); if (payload.apiKeys.length) { const current = typeof getGeminiApiKeys === 'function' ? getGeminiApiKeys() : []; const keys = Array.from(new Set(current.concat(payload.apiKeys))); if (typeof setGeminiApiKeys !== 'function') { throw new Error('Gemini API key storage is unavailable in this toolkit version.'); } setGeminiApiKeys(keys); } for (const [key, value] of Object.entries(payload.settings)) { if (key === 'amaes_account_switcher_return_enabled') { await setAccountSwitcherValue(key, value === 'true'); } else { localStorage.setItem(key, value); } } pendingAccountTransfer = null; document.getElementById('amaes-transfer-preview').hidden = true; document.getElementById('amaes-transfer-apply').hidden = true; document.getElementById('amaes-transfer-discard').hidden = true; document.getElementById('amaes-transfer-import').hidden = false; document.getElementById('amaes-transfer-import-code').value = ''; accountTransferStatus('Transfer complete! Your accounts and settings are active. Reloading...'); if (typeof showToast === 'function') showToast('Setup imported successfully! Reloading...', 3000); setTimeout(() => { if (typeof window !== 'undefined' && window.location) window.location.reload(); }, 1200); } function discardAccountTransfer() { pendingAccountTransfer = null; document.getElementById('amaes-transfer-preview').hidden = true; document.getElementById('amaes-transfer-apply').hidden = true; document.getElementById('amaes-transfer-discard').hidden = true; document.getElementById('amaes-transfer-import').hidden = false; accountTransferStatus('Decrypted preview discarded. This one-time code has already been consumed.'); } function setupAccountTransferUI() { const exportButton = document.getElementById('amaes-transfer-create'); const importButton = document.getElementById('amaes-transfer-import'); if (!exportButton || !importButton) return; exportButton.addEventListener('click', async () => { try { exportButton.disabled = true; await createAccountTransferCode(); } catch (error) { accountTransferStatus(error.message || 'Could not create transfer code.', true); } finally { exportButton.disabled = false; } }); document.getElementById('amaes-transfer-copy').addEventListener('click', async () => { const code = document.getElementById('amaes-transfer-code').value; try { if (typeof GM_setClipboard === 'function') GM_setClipboard(code); else await navigator.clipboard.writeText(code); accountTransferStatus('Transfer code copied. Share it only with the intended device owner.'); } catch (error) { accountTransferStatus(error.message || 'Could not copy the transfer code.', true); } }); importButton.addEventListener('click', async () => { try { importButton.disabled = true; await importAccountTransferCode(); } catch (error) { accountTransferStatus(error.message || 'Could not import transfer code.', true); } finally { importButton.disabled = false; } }); document.getElementById('amaes-transfer-apply').addEventListener('click', async () => { try { await applyAccountTransfer(); } catch (error) { accountTransferStatus(error.message || 'Could not apply transferred settings.', true); } }); document.getElementById('amaes-transfer-discard').addEventListener('click', discardAccountTransfer); } // ========================================== // Debug Report Generator // ========================================== function generateDebugReport() { const isCourse = checkIsCoursePage(); const isQuiz = checkIsQuizPage(); const courseInfo = detectCourseInfo(); const cachedAns = getCachedAnswers(courseInfo.subjectCode); const report = { toolkitVersion: SCRIPT_VERSION, timestamp: new Date().toISOString(), theme: currentTheme, courseInfo, isQuizPage: isQuiz, cachedQuestionsCount: cachedAns ? cachedAns.length : 0, recentLogs: debugLogs.slice(-20) }; return "```json\n" + JSON.stringify(report, null, 2) + "\n```"; } function generateDiagnosticAuditLog() { const lines = (typeof activityHistory !== 'undefined' && Array.isArray(activityHistory)) ? activityHistory.map(item => `[${item.time}] ${item.text}`).reverse() : []; const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : 'Unknown'; const platform = (typeof navigator !== 'undefined' && (navigator.userAgentData?.platform || navigator.platform)) ? (navigator.userAgentData?.platform || navigator.platform) : 'Unknown'; const screenSize = (typeof window !== 'undefined' && window.screen) ? `${window.screen.width}x${window.screen.height}` : 'Unknown'; const currentUrl = (typeof window !== 'undefined' && window.location) ? window.location.href : 'Unknown'; const currentSub = (typeof subCode !== 'undefined' && subCode) ? subCode : ((typeof detectCourseInfo === 'function') ? (detectCourseInfo().subjectCode || 'General') : 'General'); const cachedCount = (typeof getCachedAnswers === 'function' && currentSub) ? (getCachedAnswers(currentSub) || []).length : 0; const unknownTypes = typeof getUnknownQuestionTypes === 'function' ? getUnknownQuestionTypes() : []; const diagnosticHeader = [ `=== AMAES MOODLE TOOLKIT DIAGNOSTIC AUDIT LOG ===`, `Timestamp: ${new Date().toISOString()}`, `Toolkit Version: ${SCRIPT_VERSION}`, `Subject / Course: ${currentSub}`, `Page URL: ${currentUrl}`, `User Agent: ${userAgent}`, `Platform: ${platform}`, `Screen: ${screenSize}`, `Active Mode: ${(typeof autoQuizMode !== 'undefined' && autoQuizMode) ? 'Auto-Quiz' : 'Passive'} | Auto-Pick: ${(typeof autoPickQuiz !== 'undefined') ? autoPickQuiz : false} | Smart-Next: ${(typeof autoNextVerified !== 'undefined') ? autoNextVerified : false}`, `Cloud Sync: ${localStorage.getItem('amaes_auto_cloud_sync') !== 'false'}`, `Cached DB Questions: ${cachedCount}`, `Recorded Unknown Question Types: ${unknownTypes.length}`, ...(unknownTypes.length > 0 ? [ ``, `--- RECORDED UNKNOWN QUESTION TYPES JSON ---`, JSON.stringify(unknownTypes, null, 2) ] : []), ``, `--- ACTIVITY LOG TIMELINE ---` ].join('\n'); return `${diagnosticHeader}\n` + lines.join('\n') + `\n=== END DIAGNOSTIC LOG ===`; } function copyToClipboard(text) { if (typeof GM_setClipboard === 'function') { try { GM_setClipboard(text, 'text'); return Promise.resolve(); } catch (e) { logDebug('GM_setClipboard error:', e.message); } } if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return new Promise((resolve, reject) => { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) resolve(); else reject(new Error('execCommand copy failed')); } catch (err) { reject(err); } }); } // Floating Non-intrusive Stacking Toast Notifications function showToast(message, duration = 2400, onClick = null) { if (!message) return; let container = document.getElementById('amaes-toast-notification'); if (!container) { container = document.createElement('div'); container.id = 'amaes-toast-notification'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 10000000; display: flex; flex-direction: column; gap: 8px; max-width: 420px; pointer-events: none; `; document.body.appendChild(container); } // Anti-spam guard: if identical notification is already visible, refresh its timer const existingToasts = Array.from(container.querySelectorAll('.amaes-toast-item:not([data-leaving="true"])')); const duplicate = existingToasts.find(t => t.dataset.toastMessage === message); if (duplicate) { clearTimeout(duplicate._timer); duplicate._timer = setTimeout(() => dismissToast(duplicate), duration); duplicate.style.borderColor = 'var(--accent-purple, #a855f7)'; setTimeout(() => { duplicate.style.borderColor = '#334155'; }, 300); return; } function dismissToast(el) { if (!el || el.dataset.leaving === 'true') return; el.dataset.leaving = 'true'; clearTimeout(el._timer); el.style.opacity = '0'; el.style.transform = 'translateY(-8px)'; setTimeout(() => el.remove(), 250); } // Stack limit guard: max 5 active toasts if (existingToasts.length >= 5) { dismissToast(existingToasts[0]); } const toastItem = document.createElement('div'); toastItem.className = 'amaes-toast-item'; toastItem.dataset.toastMessage = message; toastItem.style.cssText = ` background: rgba(15, 23, 42, 0.94); backdrop-filter: blur(8px); color: #f8fafc; border: 1px solid #334155; padding: 8px 14px; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11.5px; font-weight: 600; box-shadow: 0 10px 25px -5px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 7px; opacity: 0; transform: translateY(-8px); transition: opacity 0.25s ease, transform 0.25s ease, border-color 0.25s ease; pointer-events: auto; cursor: pointer; user-select: none; `; toastItem.title = typeof onClick === 'function' ? 'Click to open details' : 'Click to dismiss'; toastItem.innerHTML = `${ICONS.sparkles} ${message}`; container.appendChild(toastItem); // Animate in on next frame requestAnimationFrame(() => { toastItem.style.opacity = '1'; toastItem.style.transform = 'translateY(0)'; }); // Click to dismiss immediately toastItem.onclick = () => dismissToast(toastItem); if (typeof onClick === 'function') { toastItem.addEventListener('click', (e) => { try { onClick(e); } catch (_) {} }); } // Independent timer per toast toastItem._timer = setTimeout(() => dismissToast(toastItem), duration); } let capabilityTipsInterval = null; let capabilityTipIndex = 0; const CAPABILITY_TIPS = [ 'Tip: Auto-Quiz can scan every question on a page and move to the next unanswered one.', 'Tip: Verified community answers are highlighted before any AI request is made.', 'Tip: Gemini is used as a fallback for unknown multiple-choice and true/false questions.', 'Tip: Copy AI prepares a clean prompt with the question and choices for any AI assistant.', 'Tip: Paste AI matches an answer to the Moodle choice without changing your sharing format.', 'Tip: One-page quizzes are not advanced until every question on that page has an answer.', 'Tip: Complex questions such as essays, dropdowns, and drag-and-drop stay available for manual review.', 'Tip: Cloud Sync keeps your local answer database updated with community contributions.', 'Tip: Use the floating toolkit pill to pause automation, open settings, or review the current status.', 'Tip: Personal Gemini keys stay in your browser; contributor sharing is optional and encrypted.', 'Tip: Course Tools > Activity Auto-Marker > Mark Lectures & Vids marks unfinished lecture and video activities complete.', 'Tip: Course Tools > Activity Auto-Marker > Mark Quizzes / Exams Only safely marks quizzes with a passable score (≥80%) as done, leaving failing or unattempted quizzes strictly untouched.', 'Tip: Passable Grade Guard: Quizzes scoring below 80% or unattempted are never auto-marked as done, ensuring incomplete attempts are never closed accidentally.', 'Tip: Course Tools > Activity Auto-Marker > Mark ALL as Done processes unfinished activities, respecting the strict ≥80% passable grade threshold for all quizzes.', 'Tip: Use Course Tools > Highlight Missing Quizzes to find unanswered or unattempted quizzes on a Grades or Course page.', 'Tip: Course Tools > Search Helper copies a subject-aware search phrase and opens Google for study-guide lookup.', 'Tip: In the AI Assistant card, Auto-Copy Question on AI Failure puts a clean fallback prompt on your clipboard.', 'Tip: In the AI Assistant card, Auto-Advance After AI Answer automatically proceeds to the next page after an AI choice is made (Default: ON).', 'Tip: Click inside a quiz question and press C to copy the question and choices, ready to paste into another AI tool.', 'Tip: After another AI tool gives you an answer, click the question and press V to paste it back and match the choice.', 'Tip: Click inside a question and press N when you are ready to continue to the next question or page.', 'Tip: Press P to pause or resume automatic quiz help while you review a question.', 'Tip: Press ] to cycle forward to the next quiz or exam in academic sequence.', 'Tip: Press [ to cycle backward to the previous quiz or exam in sequence.', 'Tip: Toggle Skip Passed Quizzes (≥80%) in Advanced Settings to automatically bypass quizzes you already passed.', 'Tip: On the Quiz Summary page, press P or click Pause to halt the Auto-Submit countdown timer.', 'Tip: Press N, Space, or Enter during the Auto-Submit countdown to hasten submission by -2 seconds per tap.', 'Tip: On course pages, the Course Intelligence Card displays Autonomous Confidence and question bank coverage across all terms.', 'Tip: On Non-Reviewable courses, the toolkit only harvests 100% score attempts to maintain absolute database accuracy.', 'Tip: Quizzes with 100% scores automatically mark their Moodle activities as Done (Enabled in Advanced Settings).', 'Tip: On the Quiz Summary page, pressing N or Space automatically confirms and submits the Moodle submission modal.' ]; function startCapabilityTips() { if (capabilityTipsInterval || CAPABILITY_TIPS.length === 0) return; capabilityTipsInterval = setInterval(() => { const tip = CAPABILITY_TIPS[capabilityTipIndex % CAPABILITY_TIPS.length]; capabilityTipIndex += 1; showToast(tip, 4000); }, 15000); } // Character Maps for Mathematical / Circuit Superscripts & Subscripts const SUP_MAP = {'0':'⁰','1':'¹','2':'²','3':'³','4':'⁴','5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹','+':'⁺','-':'⁻','=':'⁼','(':'⁽',')':'⁾','n':'ⁿ','i':'ⁱ','x':'ˣ','y':'ʸ','m':'ᵐ','a':'ᵃ','b':'ᵇ','c':'ᶜ'}; const SUB_MAP = {'0':'₀','1':'₁','2':'₂','3':'₃','4':'₄','5':'₅','6':'₆','7':'₇','8':'₈','9':'₉','+':'₊','-':'₋','=':'₌','(':'₍',')':'₎','a':'ₐ','e':'ₑ','o':'ₒ','x':'ₓ','i':'ᵢ','j':'ⱼ','n':'ₙ','p':'ₚ'}; // Converts DOM tree to clean formatted text preserving Superscripts, Subscripts, Math, Truth Tables, and Images function cleanDOMToAI(rootNode) { if (!rootNode) return ''; if (rootNode.matches && rootNode.matches('script, style, noscript, .accesshide, .sr-only')) return ''; const clone = rootNode.cloneNode(true); // Strip non-content scripts, toolkit buttons, injected UI badges & Moodle feedback icons/accessibility text clone.querySelectorAll('script, style, noscript, .amaes-verified-badge, .amaes-eliminated-badge, .amaes-probability-hint, .amaes-shortans-hint, .amaes-select-hint, .amaes-drag-hint, .amaes-unanswered-hint, .amaes-blockage-hud, .amaes-new-question-focus-banner, .amaes-unsure-help-chip, .amaes-card-btn-container, .amaes-copy-ai-card-btn, .amaes-ask-ai-card-btn, .amaes-paste-ai-card-btn, .amaes-web-ai-container, .amaes-web-ai-split-btn, .amaes-web-ai-main-action, .amaes-web-ai-arrow-btn, .amaes-web-ai-btn, .amaes-web-ai-menu, .amaes-web-ai-item, .amaes-web-ai-row, .amaes-web-ai-pill, .amaes-active-focus-badge, .amaes-review-status-pill, .amaes-review-outcome-banner, .amaes-que-top-toolbar, .amaes-que-stop-btn, .amaes-ai-thinking-indicator, .amaes-ai-fallback-bar, .amaes-ai-suggested-badge, .amaes-ai-text-badge, .amaes-ai-question-tag, .amaes-study-guide-question-tag, .feedbackimage, .fa-check, .fa-remove, .fa-times, .fa-close, .accesshide, .sr-only').forEach(el => el.remove()); // Convert Superscripts (e.g. 2^3 -> 2³, x^2 -> x², or ^{complex}) clone.querySelectorAll('sup').forEach(sup => { const txt = sup.innerText.trim(); if (txt) { if ([...txt].every(ch => ch in SUP_MAP)) { sup.replaceWith([...txt].map(ch => SUP_MAP[ch]).join('')); } else { sup.replaceWith(`^{${txt}}`); } } else { sup.remove(); } }); // Convert Subscripts (e.g. 10_2 -> 10₂, A_0 -> A₀, or _{complex}) clone.querySelectorAll('sub').forEach(sub => { const txt = sub.innerText.trim(); if (txt) { if ([...txt].every(ch => ch in SUB_MAP)) { sub.replaceWith([...txt].map(ch => SUB_MAP[ch]).join('')); } else { sub.replaceWith(`_{${txt}}`); } } else { sub.remove(); } }); // Convert MathJax / LaTeX formulas clone.querySelectorAll('.math, .MathJax, [data-mathml]').forEach(mathElem => { const tex = mathElem.getAttribute('data-mathml') || (mathElem.querySelector('annotation[encoding*="tex"]') ? mathElem.querySelector('annotation[encoding*="tex"]').textContent : null) || mathElem.getAttribute('alt'); if (tex) { mathElem.replaceWith(` $${tex.trim()}$ `); } }); // Convert images into markdown links with alt text & cross-attempt normalized tokens clone.querySelectorAll('img').forEach(img => { const src = img.src || img.getAttribute('data-src') || ''; const alt = (img.alt || '').trim(); if (src) { const moodleImgMatch = src.match(/\/pluginfile\.php\/\d+\/question\/(?:answer|questiontext|feedback)\/\d+(?:\/\d+)?\/([^?#\s]+)/i); const assetName = moodleImgMatch ? moodleImgMatch[1] : ''; const cleanSrc = (moodleImgMatch && assetName) ? `[moodle-asset:${assetName}]` : src; const imgText = alt ? `[Image: ${alt} - ${cleanSrc}]` : `[Image: ${cleanSrc}]`; img.replaceWith(document.createTextNode(`\n${imgText}\n`)); } else { img.remove(); } }); // Convert drop zones into readable blank indicators (e.g. [Blank 1], [Blank 2]) let dropCount = 0; clone.querySelectorAll('.drop, .dropzone, span.droptarget').forEach(dz => { dropCount++; const placed = dz.innerText.trim(); const textNode = document.createTextNode(placed ? `[Blank ${dropCount}: ${placed}]` : `[Blank ${dropCount}]`); dz.replaceWith(textNode); }); // Convert fill-in-the-blank text inputs into ____ so AI sees where the answer goes let blankCount = 0; clone.querySelectorAll('input[type="text"], input[type="number"], input:not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="hidden"])').forEach(inp => { // Only convert inputs inside the question text area (not answer choice inputs) if (inp.closest('.answer, .submitbtns, .amaes-card-btn-container')) return; blankCount++; const blankLabel = `[____${blankCount > 1 ? ' ' + blankCount : ''}]`; inp.replaceWith(document.createTextNode(` ${blankLabel} `)); }); // Convert select / dropdown elements (e.g. gapselect inline dropdowns) into readable options let selectCount = 0; clone.querySelectorAll('select').forEach(sel => { selectCount++; const selectedOpt = (sel.selectedIndex >= 0 && sel.options) ? sel.options[sel.selectedIndex] : null; const selectedVal = (selectedOpt && selectedOpt.value && selectedOpt.value !== '0' && !selectedOpt.text.toLowerCase().includes('choose')) ? selectedOpt.text.trim() : ''; const options = Array.from(sel.querySelectorAll('option')) .map(o => (o.innerText || o.textContent || '').trim()) .filter(o => o && !o.toLowerCase().includes('choose')); let selLabel = ''; if (selectedVal) { selLabel = `[Dropdown ${selectCount}: ${selectedVal}]`; } else if (options.length > 0) { selLabel = `[Dropdown ${selectCount} (${options.join(' | ')})]`; } else { selLabel = `[Dropdown ${selectCount}]`; } sel.replaceWith(document.createTextNode(` ${selLabel} `)); }); // Convert tables (Truth Tables, Logic Mappings) to markdown rows clone.querySelectorAll('table').forEach(table => { const rows = []; table.querySelectorAll('tr').forEach(tr => { const cells = Array.from(tr.querySelectorAll('th, td')).map(td => td.innerText.trim()); if (cells.length > 0) rows.push(cells.join(' | ')); }); if (rows.length > 0) { table.replaceWith(document.createTextNode('\n' + rows.join('\n') + '\n')); } }); // Normalize spaces and clean up let text = clone.innerText || clone.textContent || ''; text = text.replace(/[✓✔✗✘✕✖]/g, ''); text = text.replace(/\r\n/g, '\n'); text = text.split('\n').map(line => line.replace(/[ \t]+/g, ' ').trim()).filter((line, i, arr) => { return !(line === '' && arr[i - 1] === ''); }).join('\n'); return text.trim(); } // Fetch or convert an image URL to a PNG blob async function getImageBlob(imgUrl) { if (!imgUrl) return null; try { const response = await fetch(imgUrl); const blob = await response.blob(); if (blob.type === 'image/png') return blob; const img = new Image(); img.crossOrigin = 'anonymous'; await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = imgUrl; }); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return await new Promise(r => canvas.toBlob(r, 'image/png')); } catch (err) { logDebug('getImageBlob error:', err.message); return null; } } // Capture visual snapshot/snippet of a question card as a PNG Blob (diagrams, figures, and drag & drop) async function captureQuestionSnippetBlob(que) { if (!que || typeof document === 'undefined') return null; try { const formulation = que.querySelector('.formulation') || que.querySelector('.content') || que; if (!formulation) return null; // Clone formulation to render clean snippet without toolkit controls const clone = formulation.cloneNode(true); clone.querySelectorAll('.amaes-card-btn-container, .amaes-web-ai-row, .amaes-que-top-toolbar, .amaes-ai-thinking-indicator, .amaes-ai-fallback-bar, .amaes-ai-suggested-badge, .amaes-ai-text-badge, .amaes-ai-question-tag, .accesshide, .sr-only').forEach(el => el.remove()); // Convert images in clone to data URLs to avoid cross-origin canvas security errors const origImgs = formulation.querySelectorAll('img'); const cloneImgs = clone.querySelectorAll('img'); for (let i = 0; i < cloneImgs.length && i < origImgs.length; i++) { const orig = origImgs[i]; const cl = cloneImgs[i]; if (orig.complete && orig.naturalWidth > 0) { try { const canvas = document.createElement('canvas'); canvas.width = orig.naturalWidth; canvas.height = orig.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(orig, 0, 0); cl.src = canvas.toDataURL('image/png'); } catch (e) {} } } const rect = formulation.getBoundingClientRect(); const width = Math.max(Math.min(rect.width || 750, 1000), 450); const height = Math.max(rect.height || 350, 180); const xhtml = new XMLSerializer().serializeToString(clone); const svg = `
${xhtml}
`; const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); const svgUrl = URL.createObjectURL(svgBlob); const img = new Image(); const pngBlob = await new Promise((resolve) => { const timeout = setTimeout(() => resolve(null), 1800); img.onload = () => { clearTimeout(timeout); try { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(svgUrl); canvas.toBlob(resolve, 'image/png'); } catch (err) { URL.revokeObjectURL(svgUrl); resolve(null); } }; img.onerror = () => { clearTimeout(timeout); URL.revokeObjectURL(svgUrl); resolve(null); }; img.src = svgUrl; }); if (pngBlob) return pngBlob; } catch (err) { logDebug('captureQuestionSnippetBlob error:', err.message); } return null; } // Copy question to clipboard: if question has images or is drag & drop, copies image snippet AND pure text simultaneously! async function copyQuestionWithOptionalImage(que, text) { if (!text) return { success: false, withImage: false }; const qData = que && typeof extractQuestionData === 'function' ? extractQuestionData(que) : null; const hasImg = Boolean(que && que.querySelector('.formulation img, .qtext img, .ddarea img, .drop img, .drags img')); const isDragDrop = Boolean((qData && qData.isDragDrop) || (que && que.classList && (que.classList.contains('que_dragdrop') || que.classList.contains('ddwtos') || que.classList.contains('ddmarker') || que.classList.contains('ddimageortext'))) || (que && que.querySelector('.drop, .draghome, .drags, .ddarea, .dragboxes, .place1, .drag'))); let pngBlob = null; if (hasImg || isDragDrop) { pngBlob = await captureQuestionSnippetBlob(que); if (!pngBlob && hasImg) { const firstImg = que.querySelector('.formulation img, .qtext img, .ddarea img'); if (firstImg && firstImg.src) { pngBlob = await getImageBlob(firstImg.src); } } } // Dual-MIME clipboard: copies BOTH image/png AND text/plain together so image-aware AI sees the snippet, while textboxes get pure text if (pngBlob && typeof ClipboardItem !== 'undefined' && navigator.clipboard && navigator.clipboard.write) { try { const textBlob = new Blob([text], { type: 'text/plain' }); await navigator.clipboard.write([ new ClipboardItem({ 'image/png': pngBlob, 'text/plain': textBlob }) ]); return { success: true, withImage: true }; } catch (err) { logDebug('Dual clipboard copy failed, falling back to pure text copy:', err.message); } } // Standard pure text copy fallback await copyToClipboard(text); return { success: true, withImage: false }; } // Copy an image directly to the OS clipboard as a PNG blob async function copyImageBlobToClipboard(imgUrl) { try { const response = await fetch(imgUrl); const blob = await response.blob(); let pngBlob = blob; if (blob.type !== 'image/png') { const img = new Image(); img.crossOrigin = 'anonymous'; await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = imgUrl; }); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png')); } await navigator.clipboard.write([ new ClipboardItem({ 'image/png': pngBlob }) ]); return { success: true }; } catch (err) { logDebug('Image blob copy failed, falling back to URL copy:', err.message); await copyToClipboard(imgUrl); return { success: false, fallbackUrl: imgUrl }; } } function mapQuestionTypeCategory(rawType) { if (!rawType) return 'unknown'; const t = rawType.toLowerCase(); if (t === 'multichoice' || t === 'multichoiceset') return 'multichoice'; if (t === 'truefalse') return 'truefalse'; if (t === 'shortanswer' || t === 'numerical' || t === 'calculated' || t === 'calculatedsimple' || t === 'calculatedmulti') return 'shortanswer'; if (t === 'gapselect' || t === 'select' || t === 'cloze_select') return 'gapselect'; if (t === 'match' || t === 'matching') return 'match'; if (t.startsWith('dd') || t.includes('drag') || t.includes('drop')) return 'dragdrop'; if (t === 'essay') return 'essay'; if (t === 'multianswer') return 'shortanswer'; return 'unknown'; } function identifyQuestionType(que) { if (!que) return 'unknown'; // 1. Inspect Moodle's class on .que container const classList = Array.from(que.classList || []); for (const cls of classList) { const lower = cls.toLowerCase(); if (lower.startsWith('qtype_')) { const sub = lower.replace('qtype_', ''); if (sub === 'multianswer') { if (que.querySelectorAll('select').length > 0) return 'gapselect'; if (que.querySelectorAll('input[type="text"], input.form-control, input[type="number"], input:not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="hidden"])').length > 0) { return 'shortanswer'; } } return mapQuestionTypeCategory(sub); } if (['multichoice', 'truefalse', 'shortanswer', 'gapselect', 'match', 'ddwtos', 'ddimageortext', 'ddmarker', 'essay', 'numerical', 'multianswer', 'calculated'].includes(lower)) { if (lower === 'multianswer') { if (que.querySelectorAll('select').length > 0) return 'gapselect'; if (que.querySelectorAll('input[type="text"], input.form-control, input[type="number"], input:not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="hidden"])').length > 0) { return 'shortanswer'; } } return mapQuestionTypeCategory(lower); } } // 2. DOM signature checks if (que.querySelector('.draghome, .drags, .drop, .dropzone, span.droptarget, .droppable')) { return 'dragdrop'; } const selects = que.querySelectorAll('select'); if (selects.length > 0) { if (que.querySelector('.answer table select, td.control select')) { return 'match'; } return 'gapselect'; } if (que.querySelector('textarea, [contenteditable="true"], [data-fieldtype="editor"]')) { return 'essay'; } const radios = que.querySelectorAll('.answer input[type="radio"]'); if (radios.length > 0) { if (radios.length === 2) { const labels = Array.from(que.querySelectorAll('.answer label, .answer div.r0, .answer div.r1')).map(l => (l.innerText || '').toLowerCase()); if (labels.some(l => l.includes('true')) && labels.some(l => l.includes('false'))) { return 'truefalse'; } } return 'multichoice'; } if (que.querySelectorAll('.answer input[type="checkbox"]').length > 0) { return 'multichoice'; } if (que.querySelector('input[type="text"].form-control, input.form-control, input[type="text"], input[type="number"]')) { return 'shortanswer'; } return 'unknown'; } function recordUnknownQuestionType(que, qData = null) { try { if (!que) return; const raw = localStorage.getItem('amaes_unknown_question_types'); const list = raw ? JSON.parse(raw) : []; const classList = Array.from(que.classList || []).filter(c => !c.startsWith('amaes') && c !== 'clearfix'); const inputs = Array.from(que.querySelectorAll('input, select, textarea, button, [contenteditable], .drop, .draghome')).map(el => { let tag = el.tagName.toLowerCase(); if (el.type) tag += `[type=${el.type}]`; if (el.className) tag += `.${el.className.split(/\s+/).filter(c => c && !c.startsWith('amaes')).join('.')}`; return tag; }); const signature = `${classList.sort().join(' ')} | ${inputs.sort().join(', ')}`; if (list.some(item => item.signature === signature)) { return; } const qSnippet = (que.querySelector('.qtext, .formulation') ? que.querySelector('.qtext, .formulation').innerText.slice(0, 160) : (qData && qData.qText ? qData.qText.slice(0, 160) : '')).trim(); let rawHtml = que.querySelector('.formulation, .content') ? que.querySelector('.formulation, .content').innerHTML : ''; rawHtml = rawHtml.replace(/data:image\/[^;]+;base64,[a-zA-Z0-9+/=]+/g, '[image: embedded]'); rawHtml = rawHtml.replace(/([?&])(sesskey|attempt|token)=[a-zA-Z0-9]+/g, '$1$2=REDACTED'); const formulationHtml = rawHtml.slice(0, 500); const entry = { id: `unk_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`, timestamp: new Date().toISOString(), url: (typeof window !== 'undefined' && window.location) ? window.location.href.split('?')[0] : '', classes: classList, signature: signature, inputCount: inputs.length, inputsSummary: inputs.slice(0, 10), snippet: qSnippet, htmlSample: formulationHtml }; list.push(entry); if (list.length > 25) list.splice(0, list.length - 25); localStorage.setItem('amaes_unknown_question_types', JSON.stringify(list)); logDebug(`Recorded unknown question type signature: ${signature}`); // Automatically push unknown question telemetry to the community database relay try { const courseInfo = (typeof detectCourseInfo === 'function') ? detectCourseInfo() : {}; const currentSubCode = courseInfo.subjectCode || (typeof subCode !== 'undefined' ? subCode : 'GENERAL'); pushUnknownQuestionToRelay(entry, currentSubCode); } catch (_) {} // Reassure user with a gentle notice without altering the question DOM or interrupting if (!hasNotifiedUnknownQuestion && (typeof checkIsQuizPage === 'function' ? checkIsQuizPage() : true)) { hasNotifiedUnknownQuestion = true; if (typeof showToast === 'function') { showToast("New question type detected — reported to maintainer.", 4000); } if (typeof setLog === 'function') { setLog("New question format detected. Reported to database for developer review.", "var(--accent-amber)"); } } if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { try { window.dispatchEvent(new CustomEvent('amaes-unknown-question-recorded', { detail: entry })); } catch (_) {} } } catch (e) { logDebug('Failed to record unknown question type:', e && e.message); } } const pushedUnknownSignatures = new Set(); let hasNotifiedUnknownQuestion = false; function pushUnknownQuestionToRelay(entry, subjectCode = 'GENERAL') { try { if (!entry || !entry.signature) return; if (pushedUnknownSignatures.has(entry.signature)) return; pushedUnknownSignatures.add(entry.signature); const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; if (!relayUrl) return; const payload = { subjectCode: subjectCode || 'GENERAL', clientVersion: SCRIPT_VERSION.replace(/^v/i, ''), contributorId: (typeof getAnonymousContributorId === 'function') ? getAnonymousContributorId() : 'anon', signature: entry.signature, classes: entry.classes || [], inputCount: entry.inputCount || 0, inputsSummary: entry.inputsSummary || [], snippet: entry.snippet || '', htmlSample: entry.htmlSample || '', submittedAt: entry.timestamp || new Date().toISOString() }; const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: 'POST', url: `${relayUrl}/unknown-question`, headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, data: JSON.stringify(payload), onload: (res) => { logDebug(`Pushed unknown question telemetry to database relay (status: ${res.status})`); }, onerror: (err) => { logDebug('Failed to push unknown question telemetry:', err); } }); } else if (typeof fetch !== 'undefined') { fetch(`${relayUrl}/unknown-question`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, body: JSON.stringify(payload) }).then(res => { logDebug(`Pushed unknown question telemetry via fetch (status: ${res.status})`); }).catch(err => { logDebug('Failed to push unknown question telemetry via fetch:', err); }); } } catch (e) { logDebug('Exception in pushUnknownQuestionToRelay:', e && e.message); } } function recordSessionQuizCompleted() { try { sessionQuizzesSolved = (sessionQuizzesSolved || 0) + 1; sessionStorage.setItem('amaes_session_quizzes_solved', String(sessionQuizzesSolved)); dispatchUsageTelemetry('quiz_completed'); } catch (_) {} } function dispatchUsageTelemetry(eventType = 'heartbeat') { try { const now = Date.now(); const lastPing = parseInt(localStorage.getItem('amaes_last_telemetry_ping') || '0', 10); if (eventType === 'heartbeat' && now - lastPing < 6 * 60 * 60 * 1000) { return; } const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; if (!relayUrl) return; const payload = { anon_id: getAnonymousInstallId(), version: SCRIPT_VERSION.replace(/^v/i, ''), event: eventType, session_quizzes_solved: sessionQuizzesSolved || 0, fast_mode_enabled: Boolean(fastQuizMode) }; const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: 'POST', url: `${relayUrl}/telemetry`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload), timeout: 8000, onload: () => { if (eventType === 'heartbeat') { localStorage.setItem('amaes_last_telemetry_ping', String(now)); } } }); } else if (typeof fetch !== 'undefined') { fetch(`${relayUrl}/telemetry`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(() => { if (eventType === 'heartbeat') { localStorage.setItem('amaes_last_telemetry_ping', String(now)); } }).catch(() => {}); } } catch (_) {} } function getUnknownQuestionTypes() { try { const raw = localStorage.getItem('amaes_unknown_question_types'); return raw ? JSON.parse(raw) : []; } catch (_) { return []; } } function clearUnknownQuestionTypes() { localStorage.removeItem('amaes_unknown_question_types'); } function exportUnknownQuestionTypesJson() { const types = getUnknownQuestionTypes(); return JSON.stringify(types, null, 2); } // Community Bug Reporting (Direct to GitHub Issues via Cloudflare Relay) function submitBugReportToRelay(reportData) { return new Promise((resolve, reject) => { try { const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; if (!relayUrl) { return reject(new Error('Community relay URL is not configured.')); } const payload = { description: reportData.description || '', subjectCode: reportData.subjectCode || 'GENERAL', clientVersion: SCRIPT_VERSION.replace(/^v/i, ''), pageType: reportData.pageType || 'unknown', environment: reportData.environment || `${(typeof navigator !== 'undefined' && navigator.userAgent) || 'Unknown'} (Screen: ${(typeof window !== 'undefined' && window.innerWidth) || 0}x${(typeof window !== 'undefined' && window.innerHeight) || 0})`, logs: Array.isArray(reportData.logs) ? reportData.logs : [], breadcrumbs: Array.isArray(reportData.breadcrumbs) ? reportData.breadcrumbs : [], questionsSummary: Array.isArray(reportData.questionsSummary) ? reportData.questionsSummary : [], settings: (reportData.settings && typeof reportData.settings === 'object') ? reportData.settings : {}, contributorId: (typeof getAnonymousContributorId === 'function') ? getAnonymousContributorId() : 'anon' }; const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: 'POST', url: `${relayUrl}/report-bug`, headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, data: JSON.stringify(payload), onload: (res) => { try { const data = JSON.parse(res.responseText || '{}'); if (res.status >= 200 && res.status < 300 && data.success) { resolve(data); } else { reject(new Error(data.error || `HTTP ${res.status}`)); } } catch (err) { reject(new Error(`Server returned unexpected response (status ${res.status})`)); } }, onerror: (err) => { reject(new Error(err && err.statusText ? err.statusText : 'Network error communicating with bug relay')); } }); } else if (typeof fetch !== 'undefined') { fetch(`${relayUrl}/report-bug`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, body: JSON.stringify(payload) }).then(async res => { const data = await res.json().catch(() => ({})); if (res.ok && data.success) { resolve(data); } else { reject(new Error(data.error || `HTTP ${res.status}`)); } }).catch(err => { reject(err); }); } else { reject(new Error('No HTTP transport available.')); } } catch (e) { reject(e); } }); } function submitAnomalyReportToRelay(payload) { return new Promise((resolve) => { try { const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; if (!relayUrl) return resolve({ success: false, error: 'No relay configured' }); const body = JSON.stringify({ event: 'quiz_anomaly', anomalyType: payload.anomalyType || 'UNKNOWN_ANOMALY', subjectCode: (payload.subjectCode || 'GENERAL').toUpperCase(), activityTitle: payload.activityTitle || '', questionRaw: String(payload.questionRaw || '').slice(0, 1000), questionNorm: String(payload.questionNorm || '').slice(0, 1000), domType: payload.domType || 'unknown', submittedAnswer: String(payload.submittedAnswer || '').slice(0, 500), markScored: Number(payload.markScored || 0), maxMark: Number(payload.maxMark || 1), attemptHistory: Array.isArray(payload.attemptHistory) ? payload.attemptHistory.slice(0, 10) : [], dbCandidate: payload.dbCandidate || null, clientVersion: SCRIPT_VERSION.replace(/^v/i, ''), contributorId: (typeof getAnonymousContributorId === 'function') ? getAnonymousContributorId() : 'anon' }); const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: 'POST', url: `${relayUrl}/telemetry/anomaly`, headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, data: body, onload: (res) => { try { const data = JSON.parse(res.responseText || '{}'); resolve(data); } catch (_) { resolve({ success: true, mode: 'raw' }); } }, onerror: () => resolve({ success: false, error: 'Network error' }) }); } else if (typeof fetch !== 'undefined') { fetch(`${relayUrl}/telemetry/anomaly`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, body: body, keepalive: true }).then(r => r.json()).then(resolve).catch(err => resolve({ success: false, error: err.message })); } else { resolve({ success: false, error: 'No transport available' }); } } catch (err) { resolve({ success: false, error: err.message }); } }); } function detectAndReportQuestionAnomaly(anomalyData) { if (!anomalyData || !anomalyData.anomalyType || !anomalyData.questionRaw) return; const subCode = String(anomalyData.subjectCode || 'GENERAL').toUpperCase(); const qNorm = anomalyData.questionNorm || (typeof normalizeText === 'function' ? normalizeText(anomalyData.questionRaw) : anomalyData.questionRaw.toLowerCase()); const hash = Array.from(qNorm).reduce((acc, char) => ((acc << 5) - acc) + char.charCodeAt(0), 0) >>> 0; const dedupKey = `amaes_anomaly_${subCode}_${anomalyData.anomalyType}_${hash}`; // 1. Session deduplication: do not send the exact same anomaly twice in one session if (sessionStorage.getItem(dedupKey)) return; sessionStorage.setItem(dedupKey, '1'); // 2. Rate limit: max 5 anomalies per session attempt const countKey = `amaes_anomalies_sent_${subCode}`; const count = Number(sessionStorage.getItem(countKey) || '0'); if (count >= 5) return; sessionStorage.setItem(countKey, String(count + 1)); logDebug(`[Anomaly Detector] Logging ${anomalyData.anomalyType} for ${subCode}: "${anomalyData.questionRaw.slice(0, 60)}..."`); if (typeof setLog === 'function') { setLog(`[Anomaly Auto-Reported] Dispatched ${anomalyData.anomalyType} diagnostic report to database maintainers.`, 'var(--accent-amber)'); } submitAnomalyReportToRelay(anomalyData).catch(err => { logDebug(`[Anomaly Dispatch Note]: ${err.message}`); }); } function showBugReportModal() { const prefill = arguments[0] || {}; const existing = document.getElementById('amaes-bug-modal'); if (existing) { const ta = existing.querySelector('textarea'); if (ta) { if (prefill.description && !ta.value.trim()) { ta.value = prefill.description; ta.dispatchEvent(new Event('input')); } ta.focus(); } return; } const courseInfo = (typeof detectCourseInfo === 'function') ? detectCourseInfo() : {}; const activeSubCode = prefill.subjectCode || courseInfo.subjectCode || (typeof subCode !== 'undefined' ? subCode : 'GENERAL'); const activePageType = prefill.context || ((typeof checkIsQuizPage === 'function' && checkIsQuizPage()) ? 'quiz_attempt' : ((typeof checkIsReviewPage === 'function' && checkIsReviewPage()) ? 'quiz_review' : ((typeof checkIsCoursePage === 'function' && checkIsCoursePage()) ? 'course_view' : 'dashboard'))); const modal = document.createElement('div'); modal.id = 'amaes-bug-modal'; modal.style.cssText = ` position: fixed; inset: 0; background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(4px); z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; animation: amaesFadeIn 0.15s ease-out; `; modal.innerHTML = `
${ICONS.bug || ICONS.alertTriangle} Report a Problem / Bug
Found a bug, broken question, or unexpected behavior? Submit details below to immediately open a tracked GitHub issue for the maintainers. No GitHub account needed.
Subject: ${activeSubCode} Context: ${activePageType} Version: ${SCRIPT_VERSION}
0 / 10 min characters
${ICONS.shieldCheck || ICONS.shield} Strict Privacy Guarantee: No personal student data, IDs, names, passwords, or session tokens are ever collected or leaked. Read SECURITY.md
`; document.body.appendChild(modal); const closeModal = () => modal.remove(); modal.querySelector('#amaes-bug-modal-close').onclick = closeModal; modal.querySelector('#amaes-bug-btn-cancel').onclick = closeModal; modal.onclick = (e) => { if (e.target === modal) closeModal(); }; const textarea = modal.querySelector('#amaes-bug-description'); const charCount = modal.querySelector('#amaes-bug-char-count'); const submitBtn = modal.querySelector('#amaes-bug-btn-submit'); const errorMsg = modal.querySelector('#amaes-bug-error-msg'); const logsCheckbox = modal.querySelector('#amaes-bug-include-logs'); const modalBody = modal.querySelector('#amaes-bug-modal-body'); if (prefill.description) { textarea.value = prefill.description; } textarea.focus(); textarea.oninput = () => { const len = textarea.value.trim().length; charCount.innerText = `${len} / 10 min characters`; if (len >= 10) { charCount.style.color = '#34d399'; } else { charCount.style.color = 'var(--text-muted, #94a3b8)'; } errorMsg.style.display = 'none'; }; if (prefill.description) { textarea.dispatchEvent(new Event('input')); } submitBtn.onclick = async () => { const desc = textarea.value.trim(); if (desc.length < 10) { errorMsg.innerText = 'Please provide at least 10 characters describing the issue.'; errorMsg.style.display = 'block'; textarea.focus(); return; } submitBtn.disabled = true; submitBtn.style.opacity = '0.6'; submitBtn.innerText = 'Submitting...'; errorMsg.style.display = 'none'; let logs = []; let breadcrumbs = []; let questionsSummary = []; let settings = {}; if (logsCheckbox && logsCheckbox.checked) { try { // 1. Logs logs = (typeof activityHistory !== 'undefined' && Array.isArray(activityHistory)) ? activityHistory.slice(0, 40).map(item => `[${item.time}] ${(item.text || '').replace(/<[^>]+>/g, '')}`) : []; // 2. User Action Breadcrumbs (what the user clicked and did) breadcrumbs = (typeof userBreadcrumbs !== 'undefined' && Array.isArray(userBreadcrumbs)) ? userBreadcrumbs.slice(-35).map(b => `[${b.time}] [${b.category}] ${b.action}${b.details ? ' — ' + b.details : ''}`) : []; // 3. Questions Summary on active page const queNodes = Array.from(document.querySelectorAll('.que')); questionsSummary = queNodes.map((que, idx) => { const numElem = que.querySelector('.info .no, .qno'); const qNum = numElem ? numElem.innerText.replace(/\s+/g, ' ').trim() : `Q${idx + 1}`; const rawType = (typeof identifyQuestionType === 'function') ? identifyQuestionType(que) : 'unknown'; const checkedInputs = que.querySelectorAll('input:checked, select option:checked, textarea:not(:placeholder-shown)'); const gradeElem = que.querySelector('.grade'); const gradeText = gradeElem ? gradeElem.innerText.replace(/\s+/g, ' ').trim() : null; return { num: qNum, type: rawType, answered: checkedInputs.length > 0, ...(gradeText ? { grade: gradeText } : {}) }; }); // 4. Active Toolkit Settings & Preferences (Real live settings) settings = { autoQuizMode: typeof autoQuizMode !== 'undefined' ? autoQuizMode : false, autoPickQuiz: typeof autoPickQuiz !== 'undefined' ? autoPickQuiz : true, autoNextVerified: typeof autoNextVerified !== 'undefined' ? autoNextVerified : true, autoPickStudyGuideFallback: typeof autoPickStudyGuideFallback !== 'undefined' ? autoPickStudyGuideFallback : true, autoNextQuiz: typeof autoNextQuiz !== 'undefined' ? autoNextQuiz : false, autoHighlightQuiz: typeof autoHighlightQuiz !== 'undefined' ? autoHighlightQuiz : true, smartSkipQuiz: typeof smartSkipQuiz !== 'undefined' ? smartSkipQuiz : false, aiQuizEnabled: typeof aiQuizEnabled !== 'undefined' ? aiQuizEnabled : false, aiAutoSelect: typeof aiAutoSelect !== 'undefined' ? aiAutoSelect : false, hasGeminiKey: !!localStorage.getItem('amaes_gemini_api_key'), cloudSync: localStorage.getItem('amaes_auto_cloud_sync') !== 'false', cachedAnswersCount: (typeof getCachedAnswers === 'function' && activeSubCode) ? (getCachedAnswers(activeSubCode) || []).length : 0, unknownTypesCount: (typeof getUnknownQuestionTypes === 'function') ? getUnknownQuestionTypes().length : 0 }; } catch (_) {} } try { const res = await submitBugReportToRelay({ description: desc, subjectCode: activeSubCode, pageType: activePageType, logs: logs, breadcrumbs: breadcrumbs, questionsSummary: questionsSummary, settings: settings }); modalBody.innerHTML = `
Issue #${res.issueNumber} Opened!
Thank you! Your bug report has been forwarded directly to the developers on GitHub.
${res.issueUrl ? ` View Issue #${res.issueNumber} on GitHub → ` : ''}
`; const doneBtn = modal.querySelector('#amaes-bug-done-btn'); if (doneBtn) doneBtn.onclick = closeModal; if (typeof showToast === 'function') { showToast(`Bug report submitted (#${res.issueNumber})!`); } if (typeof setLog === 'function') { setLog(`Reported bug to GitHub (Issue #${res.issueNumber}).`, "var(--accent-green)"); } } catch (err) { submitBtn.disabled = false; submitBtn.style.opacity = '1'; submitBtn.innerText = 'Submit Report'; errorMsg.innerText = err.message || 'Failed to submit bug report. Please try again later.'; errorMsg.style.display = 'block'; } }; } // Extract Question & Choices cleanly from a Moodle .que element function extractQuestionData(que) { if (!que) return null; // 1. Question Number (clean digits only, e.g. "4", avoiding "Question #Question 4") const numElem = que.querySelector('.info .no, .qno'); let rawNum = numElem ? numElem.innerText.replace(/\s+/g, ' ').trim() : ''; const matchDigits = rawNum.match(/\d+/); const qNum = matchDigits ? matchDigits[0] : (rawNum.replace(/^Question\s*/i, '').trim() || '1'); // 2. Question Text (cleanly processed through cleanDOMToAI) const qtextElem = que.querySelector('.qtext, .formulation .qtext'); let qText = ''; if (qtextElem) { qText = cleanDOMToAI(qtextElem); } else { const formElem = que.querySelector('.formulation'); if (formElem) { const clone = formElem.cloneNode(true); const ans = clone.querySelector('.answer, .submitbtns'); if (ans) ans.remove(); qText = cleanDOMToAI(clone); } } // Broad fallback if neither .qtext nor .formulation captured the prompt if (!qText) { const fallbackElem = que.querySelector('.content, .formulation, [class*="formulation"]') || que; const clone = fallbackElem.cloneNode(true); clone.querySelectorAll('.answer, .submitbtns, .info, .im-controls, .amaes-card-btn-container, .amaes-que-top-toolbar, .amaes-ai-question-tag').forEach(el => el.remove()); qText = cleanDOMToAI(clone); } // Clean question text of leading "Question 1", "Question No. 1", "Question #1", "(1)", or prompt remnants qText = qText.replace(/^(?:(?:question|item|q)\s*(?:no\.?|#)?\s*\d+[\s:.-]*|\(?\d+\)?[\s:.)-]+)/i, '').trim(); // Also strip leading instruction preambles for cleaner AI prompts const preambleCleanRegex = /^(?:(?:direction|directions|instruction|instructions)\s*[:.\-–]\s*)?(?:read\s+(?:each\s+|the\s+)?(?:statement|question|passage)s?\s+(?:carefully\s+)?(?:and\s+)?)?(?:choose|select|pick|identify|mark)\s+(?:the\s+)?(?:best|correct|appropriate|right)\s+(?:answer|choice|option)[.:?!;\s–-]*/i; while (preambleCleanRegex.test(qText)) { const stripped = qText.replace(preambleCleanRegex, '').trim(); if (stripped.length === 0) break; qText = stripped; } qText = qText.replace(/\b(Select one|Select one or more|Choose one|Choose one or more)[:.]?\s*$/i, '').trim(); // Detect all image URLs inside this question const questionImages = []; que.querySelectorAll('.formulation img, .qtext img').forEach(img => { if (img.src && !questionImages.includes(img.src)) questionImages.push(img.src); }); // 3. Choices / Answers const choices = []; const isMatchQuestion = Boolean( que.classList.contains('match') || que.classList.contains('qtype_match') || que.querySelector('.answer table select, td.control select, .answer select') ); if (!isMatchQuestion) { let choiceRows = que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr'); if (choiceRows.length === 0) { choiceRows = que.querySelectorAll('.answer div.r0, .answer div.r1, .answer li, .answer tr'); } if (choiceRows.length === 0) { choiceRows = que.querySelectorAll('.answer label'); } if (choiceRows.length > 0) { choiceRows.forEach((row, idx) => { const label = row.querySelector('label') || row; let choiceText = cleanDOMToAI(label); // Collapse newlines inside single choice row to maintain clean multi-line choice sentences choiceText = choiceText.replace(/\r?\n\s*/g, ' ').replace(/\s+/g, ' ').trim(); if (choiceText) { // Ensure each choice starts with a clean letter prefix (a., b., c., d.) const hasPrefix = /^[a-zA-Z0-9][.)]\s*/.test(choiceText); if (!hasPrefix && choiceRows.length > 1) { const letter = String.fromCharCode(97 + idx); choiceText = `${letter}. ${choiceText}`; } choices.push(choiceText); } }); } // Direct input element fallback if wrapper elements weren't caught if (choices.length === 0) { const choiceInputs = que.querySelectorAll('.answer input[type="radio"], .answer input[type="checkbox"]'); if (choiceInputs.length > 0) { choiceInputs.forEach((inp, idx) => { const label = (inp.labels && inp.labels[0]) || inp.closest('label, div.r0, div.r1, tr, li, div') || inp.parentElement; let choiceText = cleanDOMToAI(label); choiceText = choiceText.replace(/\r?\n\s*/g, ' ').replace(/\s+/g, ' ').trim(); if (choiceText) { const hasPrefix = /^[a-zA-Z0-9][.)]\s*/.test(choiceText); if (!hasPrefix && choiceInputs.length > 1) { const letter = String.fromCharCode(97 + idx); choiceText = `${letter}. ${choiceText}`; } choices.push(choiceText); } }); } } } // 4. Essay / Long Answer question check const isEssay = Boolean(que.querySelector('textarea, [contenteditable="true"], .qtype_essay, [data-fieldtype="editor"]') && choices.length === 0); // 4b. Short Answer check const textInputs = que.querySelectorAll('input[type="text"], input.form-control, textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); const gapSelects = Array.from(que.querySelectorAll('.qtext select, .formulation select')); const isShortAnswer = Boolean(textInputs.length > 0 && choices.length === 0 && !isEssay && gapSelects.length === 0); // 5. Matching type check const matchRows = que.querySelectorAll('.answer table tr, .answer tr, .answer .r0, .answer .r1, .answer [class*="r"], .answer > div, .answer li'); const matchPairs = []; if (matchRows.length > 0 && choices.length === 0) { matchRows.forEach(tr => { const textCol = tr.querySelector('td.text, td:first-child, .text, [class*="text"]'); const selectCol = tr.querySelector('td.control select, select'); if (textCol && selectCol) { const subQ = cleanDOMToAI(textCol); const options = Array.from(selectCol.querySelectorAll('option')) .map(o => o.innerText.trim()) .filter(o => o && !o.toLowerCase().includes('choose')); if (subQ && options.length > 0) { matchPairs.push({ subQ, options }); } } }); } // 5b. Inline Gapselect / Cloze Select dropdowns check if (choices.length === 0 && matchPairs.length === 0 && gapSelects.length > 0) { gapSelects.forEach((sel, gIdx) => { const options = Array.from(sel.querySelectorAll('option')) .map(o => o.innerText.trim()) .filter(o => o && !o.toLowerCase().includes('choose')); if (options.length > 0) { matchPairs.push({ subQ: `Blank [${gIdx + 1}]`, options }); } }); } // 5c. Drag and Drop Question Type check (ddwtos, ddimageortext, ddmarker) const dragHomes = Array.from(que.querySelectorAll('.draghome, .dragitems .draghome, .drags .drag, .drags span, span.draghome, .dragboxes .drag, .dragitem')); const dropZones = Array.from(que.querySelectorAll('.drop, .dropzone, span.droptarget, .droppable')); const isDragDrop = Boolean(que.classList.contains('ddwtos') || que.classList.contains('ddimageortext') || que.classList.contains('ddmarker') || (dragHomes.length > 0 && dropZones.length > 0)); if (isDragDrop && choices.length === 0) { const seenChoices = new Set(); dragHomes.forEach((dh) => { const choiceText = cleanDOMToAI(dh).trim(); if (choiceText && !seenChoices.has(choiceText)) { seenChoices.add(choiceText); const letter = String.fromCharCode(97 + choices.length); choices.push(`${letter}. ${choiceText}`); } }); } // 6. Multi-Choice check (checkboxes or "Select one or more") const promptElem = que.querySelector('.prompt, .formulation .prompt'); const promptText = promptElem ? promptElem.innerText : ''; const hasCheckboxes = que.querySelectorAll('.answer input[type="checkbox"]').length > 0; const isMultiChoice = Boolean( hasCheckboxes || /select (?:one or more choices?|one or more|all that apply)/i.test(promptText) || /select (?:one or more choices?|one or more|all that apply)/i.test(que.innerText || '') || /select (?:one or more choices?|one or more|all that apply)/i.test(qText) ); const questionType = identifyQuestionType(que); const isGapSelect = Boolean(gapSelects.length > 0 && choices.length === 0); if (questionType === 'unknown') { recordUnknownQuestionType(que, { qNum, qText, questionType }); } const gapSelectOptions = (isGapSelect || gapSelects.length > 0) ? gapSelects.map((sel, idx) => ({ index: idx + 1, options: Array.from(sel.querySelectorAll('option')).map(o => (o.innerText || o.textContent || '').trim()).filter(o => o && !o.toLowerCase().includes('choose')), selectElem: sel })) : []; return { qNum, qText, choices, questionType, isGapSelect, gapSelectOptions, isMultiChoice, isShortAnswer, isEssay, isDragDrop, dropZonesCount: dropZones.length, matchPairs, questionImages }; } // ========================================== // AI Context Prompt Injection (1st Question) // ========================================== function getQuizSessionKey() { const urlParams = new URLSearchParams(window.location.search); const attempt = urlParams.get('attempt'); if (attempt) return `attempt_${attempt}`; const cmid = urlParams.get('cmid'); if (cmid) return `cmid_${cmid}`; const courseInfo = detectCourseInfo(); if (courseInfo.subjectCode || courseInfo.currentActivityTitle) { return `quiz_${courseInfo.subjectCode}_${courseInfo.currentActivityTitle}`.replace(/[^a-zA-Z0-9_]/g, '_'); } return 'quiz_active_session'; } function hasAiContextBeenSent() { return sessionStorage.getItem(`amaes_ai_context_sent_${getQuizSessionKey()}`) === 'true'; } function markAiContextSent() { sessionStorage.setItem(`amaes_ai_context_sent_${getQuizSessionKey()}`, 'true'); } function shouldInjectAiContext(qNum = null) { if (!checkIsQuizPage()) return false; if (!hasAiContextBeenSent()) return true; if (qNum === 1 || qNum === '1') return true; return false; } function buildAiContextIntro(qType = null) { const courseInfo = detectCourseInfo(); const details = []; if (courseInfo.subjectCode) details.push(`Course Code: ${courseInfo.subjectCode}`); if (courseInfo.subjectName) details.push(`Subject: ${courseInfo.subjectName}`); if (courseInfo.currentActivityTitle) details.push(`Activity: ${courseInfo.currentActivityTitle}`); const header = details.length > 0 ? details.join(' | ') : 'AMAES Online Course Quiz'; let directive = 'Act as an expert academic assistant for this course. For each quiz question I provide, analyze carefully and reply ONLY with the correct option letter (a, b, c, or d) and the exact choice text. Keep it direct with no explanations.'; if (qType === 'shortanswer' || qType === 'identification') { directive = 'Act as an expert academic assistant for this course. For each quiz question I provide, analyze carefully and reply ONLY with the exact canonical word, term, or method name (e.g. canonical lowercase/camelCase for identifiers/commands, no leading dots on methods like hide() or val(), and strict framework-specific classes like MaterializeCSS "l" vs Bootstrap "lg"). Keep it direct with no explanations.'; } else if (qType === 'all' || !qType) { directive = 'Act as an expert academic assistant for this course. Analyze each quiz question carefully. For multiple-choice, reply with the correct option letter and exact text. For identification/short-answer, reply ONLY with the exact canonical word, term, or method name (canonical lowercase/camelCase for identifiers, framework-specific syntax, no leading dots on methods). Keep answers direct with no explanations.'; } return `[Context: ${header}]\n${directive}\n\n---\n\n`; } // Format a single question and choices cleanly for AI with strict A/B/C/D direct response directive // Injects rich course context on the first question of a quiz session function formatQuestionForAI(que, withHint = true, forceContext = null) { const data = extractQuestionData(que); if (!data || !data.qText) return ''; let output = ''; if (data.isDragDrop) { output += `[DRAG AND DROP QUESTION - MATCH CHOICES TO BLANKS]\n`; if (data.dropZonesCount > 1) { output += `Total Blanks: ${data.dropZonesCount}\n`; } } else if (data.isMultiChoice) { output += `[NOTE: MULTIPLE ANSWERS ALLOWED - SELECT ONE OR MORE CHOICES]\n`; } output += `${data.qText}\n\n`; // Check if database or live DOM has confirmed wrong choices for this question const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'GENERAL'; const cached = getCachedAnswers(subCode); const eliminatedWrong = []; let detectedAnswer = null; // 1. Inspect live DOM: grab any choices already marked as eliminated on the page // (Guard: NEVER treat a checkmarked correct choice as eliminated!) que.querySelectorAll('.amaes-eliminated-choice, .amaes-eliminated-badge').forEach(el => { const row = el.closest('div.r0, div.r1, tr, li') || el; if (row.querySelector('.fa-check, .feedbackimage[alt="Correct"], img[src*="tick"], img[src*="correct"]') || row.classList.contains('correct')) { return; } const label = row.querySelector('label') || row; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').replace(/(?:Wrong|Incorrect Choice)\s*\(?\d*x?\)?\s*(?:•?\s*0%\s*Prob)?/i, '').trim(); if (text && !eliminatedWrong.some(w => normalizeChoice(w) === normalizeChoice(text))) { eliminatedWrong.push(text); } }); // 1b. Inspect que dataset and live inputs for known wrong answers (crucial for shortanswer / fill-in-the-blank) if (que.dataset && que.dataset.amaesKnownWrongAnswers) { try { const parsed = JSON.parse(que.dataset.amaesKnownWrongAnswers); if (Array.isArray(parsed)) { parsed.forEach(item => { const val = typeof item === 'string' ? item : (item.text || item.norm); if (val && !eliminatedWrong.some(w => normalizeChoice(w) === normalizeChoice(val))) { eliminatedWrong.push(val); } }); } } catch (_) {} } que.querySelectorAll('.amaes-known-wrong-selection, [data-amaes-known-wrong-answers]').forEach(inp => { if (inp.dataset && inp.dataset.amaesKnownWrongAnswers) { try { const parsed = JSON.parse(inp.dataset.amaesKnownWrongAnswers); if (Array.isArray(parsed)) { parsed.forEach(item => { const val = typeof item === 'string' ? item : (item.text || item.norm); if (val && !eliminatedWrong.some(w => normalizeChoice(w) === normalizeChoice(val))) { eliminatedWrong.push(val); } }); } } catch (_) {} } const val = (inp.value || inp.getAttribute('value') || '').trim(); if (val && !eliminatedWrong.some(w => normalizeChoice(w) === normalizeChoice(val))) { eliminatedWrong.push(val); } }); // 2. Cross-reference with subject database across ALL matching candidate entries if (cached && cached.length > 0) { const moodleQNorm = normalizeText(data.qText); const cands = cached.filter(c => questionTextMatches(c.qNorm || c.qRaw || c.question, moodleQNorm)); const verifiedNorms = new Set(); cands.forEach(c => { if (c.verified) { if (c.ansNorm) verifiedNorms.add(c.ansNorm); if (c.ansRaw) verifiedNorms.add(normalizeChoice(c.ansRaw)); if (Array.isArray(c.answers)) { c.answers.forEach(a => verifiedNorms.add(normalizeChoice(a))); } } }); cands.forEach(cand => { if (Array.isArray(cand.wrongAnswers)) { cand.wrongAnswers.forEach(w => { const wText = typeof w === 'string' ? w : (w.text || w.norm); const wNorm = normalizeChoice(wText); // Ground-truth guard: NEVER eliminate any answer that is verified! if (wText && !verifiedNorms.has(wNorm) && !verifiedNorms.has(unscriptDigits(wNorm)) && !eliminatedWrong.some(e => normalizeChoice(e) === wNorm)) { eliminatedWrong.push(wText); } }); } }); // 3. Find verified/consensus answer ONLY among candidates whose answer is NOT eliminated! const validCandidates = cands.filter(cand => { const ansText = cand.ansRaw || cand.answer || ''; const ansNorm = cand.ansNorm || normalizeChoice(ansText); if (!ansNorm) return false; const isConfirmedWrong = eliminatedWrong.some(w => { const wNorm = normalizeChoice(w); return wNorm === ansNorm || unscriptDigits(wNorm) === unscriptDigits(ansNorm); }); return !isConfirmedWrong; }); // When copying question for AI, NEVER include unverified AI suggestions as detected answers! // Only verified teacher keys, deduced answers, or human study guides (AMAUOED) can be suggested. const verifiedNonAiCandidates = validCandidates.filter(c => { if (c.isAiSuggestion) return false; const srcLower = (c.source || '').toLowerCase(); if (srcLower.includes('gemini') || srcLower.includes('ai assistant') || srcLower === 'ai') return false; if (Array.isArray(c.sources) && c.sources.some(s => String(s).toLowerCase().includes('gemini') || String(s).toLowerCase().includes('ai assistant'))) return false; return true; }); if (verifiedNonAiCandidates.length > 0) { // Sort by verification & consensus verifiedNonAiCandidates.sort((a, b) => ((b.verified ? 10 : 0) + (b.confirmations || 1)) - ((a.verified ? 10 : 0) + (a.confirmations || 1))); const bestCand = verifiedNonAiCandidates[0]; const isDeduced = Boolean(bestCand.deduced); const isVerified = Boolean(bestCand.verified); const isAmauoed = Boolean((bestCand.source || '').toLowerCase().includes('amauoed') || (Array.isArray(bestCand.sources) && bestCand.sources.some(s => s.toLowerCase().includes('amauoed')))); const prob = isVerified ? 100 : (isAmauoed ? 95 : 90); const label = isDeduced ? 'Deduced • 100% Probability' : (isVerified ? 'Verified • 100% Probability' : (isAmauoed ? 'AMAUOED • 95% Probability' : `${prob}% Probability`)); detectedAnswer = { text: bestCand.ansRaw || bestCand.answer, label: label, source: bestCand.source || 'Verified Database' }; } } if (data.isDragDrop) { if (data.choices && data.choices.length > 0) { output += `Available Draggable Choices:\n` + data.choices.join('\n'); } if (detectedAnswer && copyIncludeConfidence) { output += `\n\n[DETECTED ANSWER IN DATABASE]:\n- Suggested: ${detectedAnswer.text} (${detectedAnswer.label} • ${detectedAnswer.source})`; } if (withHint) { if (data.dropZonesCount > 1) { output += `\n\nInstructions: This is a Drag and Drop question with ${data.dropZonesCount} blanks. Match each blank to the correct draggable choice. Format your answer as: Blank 1: [choice text], Blank 2: [choice text], etc. No explanation.`; } else { output += `\n\nInstructions: This is a Drag and Drop question. Answer ONLY with the correct draggable choice text. No explanation.`; } } } else if (data.choices && data.choices.length > 0) { // Annotate confirmed wrong choices directly in the choices list so AI sees it immediately! const annotatedChoices = data.choices.map(choice => { const cleanChoiceText = choice.replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); const normC = normalizeChoice(cleanChoiceText); const isWrong = eliminatedWrong.some(w => { const wNorm = normalizeChoice(w); return wNorm === normC || unscriptDigits(wNorm) === unscriptDigits(normC); }); if (isWrong) { return `${choice} [CONFIRMED WRONG - DO NOT SELECT]`; } return choice; }); output += annotatedChoices.join('\n'); if (detectedAnswer && copyIncludeConfidence) { output += `\n\n[DETECTED ANSWER IN DATABASE]:\n- Suggested: ${detectedAnswer.text} (${detectedAnswer.label} • ${detectedAnswer.source})`; } // Contradiction guard: never eliminate all choices in AI instructions if (eliminatedWrong.length > 0 && eliminatedWrong.length < data.choices.length) { output += `\n\n[CONFIRMED WRONG CHOICES - DO NOT SELECT]:\n` + eliminatedWrong.map(w => `- "${w}" (Tested and confirmed 100% INCORRECT in prior attempt)`).join('\n'); output += `\nCRITICAL: Do NOT choose any option marked as confirmed wrong above. Pick strictly from the remaining candidate choices.`; } if (withHint) { if (data.isMultiChoice) { output += `\n\nInstructions: This question allows MULTIPLE answers ("Select one or more"). Answer ONLY with ALL applicable option letters (e.g. "a, c" or "b, d") and their exact choice texts. Do NOT pick any confirmed wrong choices. Do NOT give explanations.`; } else { output += `\n\nInstructions: Answer ONLY with the correct option letter (a, b, c, or d) and the exact choice text. Do NOT pick any confirmed wrong choices. Do NOT give explanations.`; } } } else if (data.isGapSelect && data.gapSelectOptions && data.gapSelectOptions.length > 0) { output += `[Dropdown Selection / Cloze Question]\n`; data.gapSelectOptions.forEach(g => { output += `Dropdown ${g.index} Options: ${g.options.join(' | ')}\n`; }); if (detectedAnswer && copyIncludeConfidence) { output += `\n[DETECTED ANSWER IN DATABASE]:\n- Suggested: ${detectedAnswer.text} (${detectedAnswer.label} • ${detectedAnswer.source})\n`; } if (withHint) { output += `\nInstructions: Select the exact matching dropdown option for the blank(s). Reply ONLY with the option text. No explanation.`; } } else if (data.matchPairs && data.matchPairs.length > 0) { output += `Matching items:\n`; data.matchPairs.forEach(p => { output += `- ${p.subQ}: [${p.options.join(', ')}]\n`; }); if (withHint) { output += `\n\nInstructions: Answer ONLY with the matched pairs. No explanation.`; } } else if (data.isEssay) { output += `[Essay / Long Answer Question]`; if (withHint) { output += `\n\nInstructions: Provide a detailed, accurate, and well-structured response to this question prompt.`; } } else if (data.isShortAnswer) { output += `[Fill-in-the-Blank / Short Answer Question]\n[The blank is marked as [____] in the question text above.]`; if (eliminatedWrong.length > 0) { output += `\n\n[CONFIRMED WRONG ANSWERS - DO NOT USE]:\n` + eliminatedWrong.map(w => `- "${w}" (Tested and confirmed INCORRECT in prior attempt)`).join('\n'); output += `\nCRITICAL: Do NOT provide any of the confirmed wrong answers listed above.`; } if (detectedAnswer && copyIncludeConfidence) { output += `\n\n[DETECTED ANSWER IN DATABASE]:\n- Suggested: ${detectedAnswer.text} (${detectedAnswer.label} • ${detectedAnswer.source})`; } if (withHint) { output += `\n\nInstructions: Answer ONLY with the exact word or phrase that fills the blank [____]. No explanation.`; } } else if (data.choices.length === 0 && !data.isDragDrop && (!data.matchPairs || data.matchPairs.length === 0)) { // General open-ended or unexpected question fallback if (withHint) { output += `\nInstructions: Provide the most accurate and direct answer to the question prompt above.`; } } const includeContext = forceContext !== null ? forceContext : shouldInjectAiContext(data.qNum); if (includeContext) { const qType = data.isShortAnswer || (!data.choices || data.choices.length === 0) ? 'shortanswer' : 'multichoice'; const intro = buildAiContextIntro(qType); output = `${intro}${output}`; markAiContextSent(); } return output.trim(); } // Format all questions on current quiz page for AI function formatAllQuestionsForAI(withHint = true) { const queList = document.querySelectorAll('.que'); if (queList.length === 0) return ''; if (queList.length === 1) { return formatQuestionForAI(queList[0], withHint); } const formatted = []; queList.forEach((que, idx) => { const itemText = formatQuestionForAI(que, false, false); if (itemText) { formatted.push(`Question ${idx + 1}:\n${itemText}`); } }); let res = formatted.join('\n\n---\n\n'); if (withHint && res) { res += `\n\nInstructions for all questions above:\n` + `- For multiple choice questions: Answer with question number, option letter (e.g. 1. a), and choice text.\n` + `- For multi-answer ("Select one or more"): Provide all applicable option letters.\n` + `- For short answer, blanks, matching, or essays: Provide the exact direct answer or concise response for each.\n` + `- Format answers in numerical order with zero extraneous conversational filler.`; } const intro = buildAiContextIntro('all'); markAiContextSent(); return `${intro}${res}`.trim(); } // -------------------------------------------------- // Multi-Web AI Launchers (ChatGPT, Perplexity, Gemini) // -------------------------------------------------- const WEB_AI_PROVIDERS = { chatgpt: { id: 'chatgpt', name: 'ChatGPT', icon: ICONS.chatgpt, badge: '(Auto-fill)' }, perplexity: { id: 'perplexity', name: 'Perplexity', icon: ICONS.perplexity, badge: '(Auto-search)' }, gemini: { id: 'gemini', name: 'Gemini', icon: ICONS.gemini, badge: '(Copy & Go)' } }; function getPreferredWebAi() { const saved = localStorage.getItem('amaes_preferred_web_ai'); return (saved && WEB_AI_PROVIDERS[saved]) ? saved : 'chatgpt'; } function setPreferredWebAi(providerId) { if (WEB_AI_PROVIDERS[providerId]) { localStorage.setItem('amaes_preferred_web_ai', providerId); const info = WEB_AI_PROVIDERS[providerId]; document.querySelectorAll('.amaes-web-ai-main-action').forEach(btn => { btn.innerHTML = `${info.icon} ${info.name}`; btn.title = `Ask ${info.name}: Click to solve question in ${info.name}`; }); } } function openExternalAi(provider, que) { if (!que) que = getActiveQuestion() || document.querySelector('.que'); if (!que) { showToast('Please click a question first to select it.'); return; } setActiveQuestion(que, true); const qData = extractQuestionData(que); const willIncludeContext = shouldInjectAiContext(qData ? qData.qNum : null); const text = formatQuestionForAI(que, aiPromptHint); if (!text) { showToast('Could not format question text.'); return; } if (provider === 'chatgpt') { const url = `https://chatgpt.com/?q=${encodeURIComponent(text)}`; window.open(url, '_blank'); showToast('Opening ChatGPT with question pre-filled...'); } else if (provider === 'perplexity') { const url = `https://www.perplexity.ai/search?q=${encodeURIComponent(text)}`; window.open(url, '_blank'); showToast('Opening Perplexity with question pre-filled...'); } else if (provider === 'gemini') { copyQuestionWithOptionalImage(que, text).then((res) => { window.open('https://gemini.google.com/app', '_blank'); showToast(res && res.withImage ? 'Visual snippet & text copied! Press Ctrl+V in Google Gemini.' : 'Question copied! Press Ctrl+V in Google Gemini.', 3500); }).catch(() => { window.open('https://gemini.google.com/app', '_blank'); showToast('Opening Google Gemini...', 2500); }); } } // Helper to control in-question AI tools state & retry styling function updateQuestionAiDrawerState(que, forceUnminimize = false) { if (!que) return; const drawer = que.querySelector('.amaes-card-ai-drawer'); if (drawer) { const hint = drawer.querySelector('.amaes-card-ai-drawer-hint'); if (forceUnminimize) { drawer.open = true; if (hint) hint.textContent = '▴'; } else { const hasVerified = Boolean(que.querySelector('.amaes-verified-badge')); const existingAiChoice = que.querySelector('.amaes-ai-suggested-choice'); const hasAiChoice = Boolean(existingAiChoice && !isChoiceRowEliminated(existingAiChoice)); if (hasVerified || hasAiChoice) { drawer.open = false; if (hint) hint.textContent = '▾'; } else { drawer.open = true; if (hint) hint.textContent = '▴'; } } } // Update Built-in AI retry button text & styling if attempted or failed const askAiBtn = que.querySelector('.amaes-ask-ai-card-btn'); if (askAiBtn) { if (que.dataset.amaesAiFailed === 'true' || que.querySelector('.amaes-ai-fallback-bar')) { askAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; askAiBtn.style.borderColor = 'rgba(239, 68, 68, 0.4)'; } else if (que.dataset.amaesAiAttempted === 'true') { askAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; } } } // Inject sleek in-question AI tools: Left sidebar (.info below Flag question) for core tools & Built-in AI; Right side (.formulation) for Web AI launchers function injectQuestionCopyButtons() { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') return; if (!checkIsQuizPage()) return; const queElements = document.querySelectorAll('.que'); // Never show Copy AI / Target Question buttons on review screens (where quiz is finished) or if user disabled them if (checkIsReviewPage() || !showInQuestionAiBtns) { document.querySelectorAll('.amaes-card-btn-container').forEach(el => el.remove()); document.querySelectorAll('.amaes-web-ai-row').forEach(el => el.remove()); return; } queElements.forEach(que => { // Bind click event on question card to explicitly select/target it if (!que.dataset.amaesFocusBound) { que.dataset.amaesFocusBound = 'true'; que.addEventListener('click', () => { setActiveQuestion(que, true); }); } // ============================================================== // 1. LEFT SIDE: Core Toolkit & Built-in AI (.info below Flag) // ============================================================== const isCompositeQuestion = que.querySelectorAll('select').length > 1 || que.querySelectorAll('.answer table tr select, .answer tr select, .answer select').length > 1 || que.querySelectorAll('input[type="text"]').length > 1 || que.classList.contains('match') || que.classList.contains('qtype_match') || (typeof identifyQuestionType === 'function' && identifyQuestionType(que) === 'match'); if (isCompositeQuestion) { que.dataset.composite = 'true'; const existingContainer = que.querySelector('.amaes-card-btn-container'); if (existingContainer) existingContainer.style.display = 'none'; return; } if (!que.querySelector('.amaes-card-btn-container')) { const btnContainer = document.createElement('div'); btnContainer.className = 'amaes-card-btn-container'; btnContainer.style.cssText = 'display: flex; flex-direction: column; gap: 4px; margin-top: 6px; width: 100%; box-sizing: border-box;'; // Active / Target Question Indicator Badge const activeBadge = document.createElement('div'); activeBadge.className = 'amaes-active-focus-badge'; activeBadge.innerHTML = `${ICONS.check} Target Question`; btnContainer.appendChild(activeBadge); // Direct action buttons container const aiActions = document.createElement('div'); aiActions.className = 'amaes-card-ai-actions'; aiActions.style.cssText = 'display: flex; flex-direction: column; gap: 4px; width: 100%; box-sizing: border-box;'; if (isCompositeQuestion) { aiActions.style.display = 'none'; } // 1a. Copy Question Text / Visual Snippet Button const btnText = document.createElement('button'); btnText.type = 'button'; btnText.className = 'amaes-copy-ai-card-btn'; btnText.title = 'Copy question and choices (copies visual snippet + text for images and drag & drop)'; btnText.innerHTML = `${ICONS.copy} Copy AI`; btnText.style.display = autoQuizMode ? 'none' : ''; btnText.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); setActiveQuestion(que, true); const qData = extractQuestionData(que); const willIncludeContext = shouldInjectAiContext(qData ? qData.qNum : null); const text = formatQuestionForAI(que, aiPromptHint); if (!text) return; try { const copyRes = await copyQuestionWithOptionalImage(que, text); btnText.innerHTML = `${ICONS.check} ${copyRes && copyRes.withImage ? 'Copied Snippet!' : 'Copied!'}`; btnText.style.borderColor = 'var(--accent-green, #10b981)'; btnText.style.color = 'var(--accent-green, #10b981)'; showToast(copyRes && copyRes.withImage ? 'Visual snippet & text copied for AI!' : (willIncludeContext ? 'Question copied with Course AI Context!' : 'Question & choices copied for AI!')); if (willIncludeContext) { setLog("Copied question with Course Context for AI.", "var(--accent-green)"); } setTimeout(() => { btnText.innerHTML = `${ICONS.copy} Copy AI`; btnText.style.borderColor = ''; btnText.style.color = ''; }, 1800); } catch (err) { console.error('Copy failed:', err); } }; aiActions.appendChild(btnText); // 1b. Paste AI Button on Question Card if (checkIsQuizAttemptPage()) { const btnPaste = document.createElement('button'); btnPaste.type = 'button'; btnPaste.className = 'amaes-copy-ai-card-btn amaes-paste-ai-card-btn'; btnPaste.title = 'Paste AI response from clipboard to select this choice (V)'; btnPaste.innerHTML = `${ICONS.clipboard} Paste AI`; btnPaste.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); setActiveQuestion(que, true); await autoSelectFromAiClipboard(que); }; aiActions.appendChild(btnPaste); } // 1d. Built-in AI Solver Section (Differentiated with clear header & button) if (checkIsQuizAttemptPage()) { const builtinAiSection = document.createElement('div'); builtinAiSection.className = 'amaes-builtin-ai-section'; builtinAiSection.style.cssText = 'display: flex; flex-direction: column; gap: 3px; margin-top: 4px; padding-top: 4px; border-top: 1px dashed rgba(168, 85, 247, 0.35); width: 100%; box-sizing: border-box;'; const builtinHeader = document.createElement('div'); builtinHeader.className = 'amaes-builtin-ai-header'; builtinHeader.style.cssText = 'font-size: 9px; font-weight: 700; color: #a855f7; text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; justify-content: center; gap: 4px; user-select: none;'; builtinHeader.innerHTML = `${ICONS.gemini} Built-in AI`; builtinAiSection.appendChild(builtinHeader); const btnAskAi = document.createElement('button'); btnAskAi.type = 'button'; btnAskAi.className = 'amaes-copy-ai-card-btn amaes-ask-ai-card-btn'; btnAskAi.title = 'Ask Google Gemini AI to analyze and solve this question directly'; btnAskAi.innerHTML = `${ICONS.gemini} ${que.dataset.amaesAiAttempted ? 'Retry AI' : 'Solve with AI'}`; btnAskAi.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); setActiveQuestion(que, true); await manualSolveWithAi(que, btnAskAi); }; builtinAiSection.appendChild(btnAskAi); aiActions.appendChild(builtinAiSection); } btnContainer.appendChild(aiActions); // Mount into .info (space below Flag question) or fallback to content const infoCol = que.querySelector('.info'); const contentCol = que.querySelector('.content'); if (infoCol) { infoCol.appendChild(btnContainer); } else if (contentCol) { contentCol.insertBefore(btnContainer, contentCol.firstChild); } else { que.insertBefore(btnContainer, que.firstChild); } } else { const existingAiActions = que.querySelector('.amaes-card-ai-actions'); if (existingAiActions) { const isComposite = que.querySelectorAll('select').length > 1 || que.querySelectorAll('.answer table tr select, .answer tr select').length > 0 || que.querySelectorAll('input[type="text"]').length > 1; if (isComposite) { existingAiActions.style.display = 'none'; } } } // ============================================================== // 2. RIGHT SIDE: Web AI Launchers (.formulation / question header) // ============================================================== const formulation = que.querySelector('.formulation') || que.querySelector('.content') || que; if (formulation && !que.querySelector('.amaes-web-ai-row')) { const webAiRow = document.createElement('div'); webAiRow.className = 'amaes-web-ai-row'; const webAiLabel = document.createElement('span'); webAiLabel.className = 'amaes-web-ai-label'; webAiLabel.innerHTML = `${ICONS.globe || ICONS.external} Web AI:`; webAiRow.appendChild(webAiLabel); // 2a. Ask ChatGPT (Official SVG logo) const btnChatGpt = document.createElement('button'); btnChatGpt.type = 'button'; btnChatGpt.className = 'amaes-web-ai-item amaes-pill-chatgpt'; btnChatGpt.dataset.provider = 'chatgpt'; btnChatGpt.title = 'Open question directly in ChatGPT with answer prompt pre-filled'; btnChatGpt.innerHTML = `${ICONS.chatgpt} ChatGPT`; btnChatGpt.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openExternalAi('chatgpt', que); }; webAiRow.appendChild(btnChatGpt); // 2b. Google Gemini Web (Official Sparkle SVG logo) const btnGemini = document.createElement('button'); btnGemini.type = 'button'; btnGemini.className = 'amaes-web-ai-item amaes-pill-gemini'; btnGemini.dataset.provider = 'gemini'; btnGemini.title = 'Open question in Google Gemini'; btnGemini.innerHTML = `${ICONS.gemini} Gemini Web`; btnGemini.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openExternalAi('gemini', que); }; webAiRow.appendChild(btnGemini); // 2c. Perplexity AI (Official Geometric SVG logo) const btnPerplexity = document.createElement('button'); btnPerplexity.type = 'button'; btnPerplexity.className = 'amaes-web-ai-item amaes-pill-perplexity'; btnPerplexity.dataset.provider = 'perplexity'; btnPerplexity.title = 'Search question in Perplexity AI'; btnPerplexity.innerHTML = `${ICONS.perplexity} Perplexity`; btnPerplexity.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openExternalAi('perplexity', que); }; webAiRow.appendChild(btnPerplexity); const topToolbar = formulation.querySelector('.amaes-que-top-toolbar'); if (topToolbar && topToolbar.nextSibling) { formulation.insertBefore(webAiRow, topToolbar.nextSibling); } else if (topToolbar) { formulation.appendChild(webAiRow); } else { formulation.insertBefore(webAiRow, formulation.firstChild); } } updateQuestionAiDrawerState(que); }); // Setup throttled window scroll listener to update active focus as user scrolls if (!window.__amaesScrollFocusBound) { window.__amaesScrollFocusBound = true; let scrollTimer = null; window.addEventListener('scroll', () => { if (scrollTimer) return; scrollTimer = setTimeout(() => { scrollTimer = null; if (Date.now() < userActiveLockUntil && userSelectedActiveQuestion && document.contains(userSelectedActiveQuestion)) { const rect = userSelectedActiveQuestion.getBoundingClientRect(); const vh = window.innerHeight || 800; if (rect.bottom > 60 && rect.top < vh - 60) return; } getActiveViewportQuestion(); }, 150); }, { passive: true }); } // Initialize active focus on first page load if (!userSelectedActiveQuestion && queElements.length > 0) { getActiveViewportQuestion(); } // Auto-dismiss Web AI dropdown on click/tap outside if (!window.__amaesWebAiMenuBound) { window.__amaesWebAiMenuBound = true; document.addEventListener('click', (e) => { if (!e.target.closest('.amaes-web-ai-container')) { document.querySelectorAll('.amaes-web-ai-menu').forEach(m => { m.style.display = 'none'; }); } }); } } // Inject sleek in-question top toolbar with Stop / Resume button on every question card ("uptopquestion") function injectQuestionTopToolbars() { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') return; if (!checkIsQuizAttemptPage()) return; const queElements = document.querySelectorAll('.que'); queElements.forEach(que => { const formulation = que.querySelector('.formulation, .content') || que; if (!formulation) return; if (formulation.querySelector('.amaes-que-top-toolbar')) return; const toolbar = document.createElement('div'); toolbar.className = 'amaes-que-top-toolbar'; toolbar.style.cssText = ` display: flex; align-items: center; justify-content: space-between; width: 100%; align-self: stretch; gap: 6px; margin-bottom: 6px; padding: 2px 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; box-sizing: border-box; `; const qType = identifyQuestionType(que); if (qType === 'unknown') { return; } const typePill = document.createElement('span'); typePill.className = 'amaes-que-type-pill'; let typeLabel = 'Question'; let typeBg = 'rgba(59, 130, 246, 0.12)'; let typeColor = '#60a5fa'; let typeBorder = 'rgba(59, 130, 246, 0.3)'; if (qType === 'gapselect') { typeLabel = 'Dropdown Pick'; typeBg = 'rgba(168, 85, 247, 0.12)'; typeColor = '#c084fc'; typeBorder = 'rgba(168, 85, 247, 0.3)'; } else if (qType === 'shortanswer') { typeLabel = 'Short Answer'; typeBg = 'rgba(6, 182, 212, 0.12)'; typeColor = '#22d3ee'; typeBorder = 'rgba(6, 182, 212, 0.3)'; } else if (qType === 'truefalse') { typeLabel = 'True / False'; typeBg = 'rgba(16, 185, 129, 0.12)'; typeColor = '#34d399'; typeBorder = 'rgba(16, 185, 129, 0.3)'; } else if (qType === 'multichoice') { typeLabel = 'Multiple Choice'; typeBg = 'rgba(59, 130, 246, 0.12)'; typeColor = '#60a5fa'; typeBorder = 'rgba(59, 130, 246, 0.3)'; } else if (qType === 'match') { typeLabel = 'Matching'; typeBg = 'rgba(245, 158, 11, 0.12)'; typeColor = '#fbbf24'; typeBorder = 'rgba(245, 158, 11, 0.3)'; } else if (qType === 'dragdrop') { typeLabel = 'Drag & Drop'; typeBg = 'rgba(249, 115, 22, 0.12)'; typeColor = '#fb923c'; typeBorder = 'rgba(249, 115, 22, 0.3)'; } else if (qType === 'essay') { typeLabel = 'Essay'; typeBg = 'rgba(148, 163, 184, 0.12)'; typeColor = '#cbd5e1'; typeBorder = 'rgba(148, 163, 184, 0.3)'; } typePill.style.cssText = ` display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; font-size: 8.5px; font-weight: 700; border-radius: 12px; background: ${typeBg}; color: ${typeColor}; border: 1px solid ${typeBorder}; margin-right: auto; text-transform: uppercase; letter-spacing: 0.4px; user-select: none; `; typePill.textContent = typeLabel; typePill.title = `Question Type: ${typeLabel} (Class: ${Array.from(que.classList || []).join(' ')})`; toolbar.appendChild(typePill); const stopBtn = document.createElement('button'); stopBtn.type = 'button'; stopBtn.className = 'amaes-que-stop-btn'; stopBtn.style.cssText = ` display: inline-flex; margin-left: auto; flex: 0 0 auto; align-items: center; gap: 4px; padding: 2px 8px; font-size: 9.5px; font-weight: 600; border-radius: 12px; cursor: pointer; transition: all 0.15s ease; box-shadow: 0 1px 2px rgba(0,0,0,0.04); `; if (autoQuizMode) { stopBtn.innerHTML = `${ICONS.stop} Stop Auto`; stopBtn.style.background = 'rgba(239, 68, 68, 0.08)'; stopBtn.style.color = '#ef4444'; stopBtn.style.border = '1px solid rgba(239, 68, 68, 0.28)'; stopBtn.title = 'Stop automation so you can answer manually without any script interference'; } else { stopBtn.innerHTML = `${ICONS.play} Resume`; stopBtn.style.background = 'rgba(16, 185, 129, 0.08)'; stopBtn.style.color = '#10b981'; stopBtn.style.border = '1px solid rgba(16, 185, 129, 0.28)'; stopBtn.title = 'Resume autonomous quiz solver and auto-navigation'; } stopBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); toggleAutoQuizMode(); }; toolbar.appendChild(stopBtn); formulation.insertBefore(toolbar, formulation.firstChild); }); } // Auto-copy question to clipboard ONLY when not auto-answering and answer is unknown let lastCopiedSignature = ''; function triggerAutoCopyForAI() { if (!autoCopyQuizForAI) return; if (!checkIsQuizAttemptPage()) return; // LOGIC FIX: If Auto-Quiz mode is active OR questions are already verified in DB, DO NOT auto-copy! if (autoQuizMode) return; const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const cached = getCachedAnswers(subCode); const queElements = document.querySelectorAll('.que'); if (queElements.length === 0) return; // If all questions are verified by database, suppress auto-copy let hasUnansweredUnknown = false; queElements.forEach(que => { if (!que.querySelector('.amaes-verified-badge')) hasUnansweredUnknown = true; }); if (!hasUnansweredUnknown) { logDebug("All questions known in DB, suppressing auto-copy."); return; } // Suppress auto-copy on composite or matching questions to prevent clipboard pollution const hasCompositeOrMatch = Array.from(queElements).some(q => (typeof identifyQuestionType === 'function' && identifyQuestionType(q) === 'match') || q.classList.contains('match') || q.classList.contains('qtype_match') || q.querySelectorAll('select').length > 1 || q.querySelectorAll('input[type="text"]').length > 1 ); if (hasCompositeOrMatch) { logDebug("Composite or matching question present; suppressing auto-copy."); return; } const textToCopy = formatAllQuestionsForAI(aiPromptHint); if (!textToCopy) return; const signature = textToCopy.trim(); if (signature === lastCopiedSignature) return; lastCopiedSignature = signature; let targetQue = null; for (const que of queElements) { if (!que.querySelector('.amaes-verified-badge')) { targetQue = que; break; } } if (!targetQue && queElements.length > 0) targetQue = queElements[0]; copyQuestionWithOptionalImage(targetQue, textToCopy).then((res) => { logDebug('Copied unknown question for AI helper' + (res && res.withImage ? ' with snippet' : '')); showToast(res && res.withImage ? 'Visual snippet & question copied! Ready to paste into AI.' : 'Question copied! Ready to paste into AI helper.'); const statusEl = document.getElementById('amaes-status'); if (statusEl) { statusEl.innerHTML = `${res && res.withImage ? 'Visual snippet & text ready to paste into AI!' : 'Question ready to paste into AI helper!'}`; } }).catch(err => { logDebug('Auto-copy failed:', err.message); }); } // Observer and initializer for quiz automation let observerDebounceTimer = null; function setupQuizAutomation() { if (localStorage.getItem('amaes_terms_acknowledged') !== 'true') return; if (!checkIsQuizPage()) return; injectQuestionCopyButtons(); injectQuestionTopToolbars(); handleQuizReviewPageLoad(); if (checkIsReviewPage()) { injectReviewQuestionMarkers(); if (typeof injectReviewRetakeButton === 'function') { injectReviewRetakeButton(); } } if (checkIsQuizAttemptPage()) { isWaitingForUserAnswer = false; injectQuizFloatingHUD(); injectQuestionTopToolbars(); setupQuizAnswerListeners(); // Auto-minimize toolkit panel to floating smart pill if enabled if (autoMinimizeQuiz) { const panel = document.getElementById('amaes-toolkit-panel'); const bodyEl = document.getElementById('amaes-panel-body'); const minBtn = document.getElementById('amaes-min-btn'); if (bodyEl && bodyEl.style.display !== 'none') { bodyEl.style.display = 'none'; if (panel) panel.classList.add('amaes-minimized'); if (minBtn) { minBtn.innerHTML = ` `; } } } // On attempt page load: ONLY run solver if Auto-Quiz is active; otherwise just highlight visually clearTimeout(pageLoadSolverTimer); pageLoadSolverTimer = setTimeout(async () => { if (autoQuizMode) { runAutoQuizSolver(true); } else { const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; let cached = getCachedAnswers(subCode); if ((!cached || cached.length === 0) && autoScrapeAmauoed && typeof autoFetchCloudAnswersIfMissing === 'function') { setLog(`Answers missing for ${subCode}. Auto-fetching from AMAUOED / Cloud...`, 'var(--accent-blue)'); await autoFetchCloudAnswersIfMissing(subCode); cached = getCachedAnswers(subCode); } if (cached && cached.length > 0) { highlightQuizAnswers(cached, false, false); } } triggerAutoCopyForAI(); setupQuizAnswerListeners(); }, 400); } if (checkIsQuizSummaryPage()) { handleQuizSummaryAutoSubmit(); } // Debounced observer: updates buttons, answer listeners, and review harvesting const observer = new MutationObserver(() => { clearTimeout(observerDebounceTimer); observerDebounceTimer = setTimeout(() => { injectQuestionCopyButtons(); injectQuestionTopToolbars(); handleQuizReviewPageLoad(); if (checkIsReviewPage()) { injectReviewQuestionMarkers(); if (typeof injectReviewRetakeButton === 'function') { injectReviewRetakeButton(); } } if (checkIsQuizAttemptPage()) { setupQuizAnswerListeners(); } }, 300); }); const target = document.querySelector('#region-main, .course-content, body'); if (target) { observer.observe(target, { childList: true, subtree: true }); } } // ========================================== // Jev Decision Provider (System One AI) // https://jevplayground.com/ | TypeSafe AI // ========================================== const JEV_DIRECT_ENDPOINT = 'https://api.typesafe.ai/v1/systemone'; const JEV_DEFAULT_MODEL = 'jev-latest'; const JEV_DEFAULT_TIMEOUT_MS = 6000; const JEV_API_KEY_STORAGE_KEY = 'amaes_jev_api_key'; const JEV_ENDPOINT_STORAGE_KEY = 'amaes_jev_endpoint'; const JEV_ENABLED_STORAGE_KEY = 'amaes_jev_enabled'; const JEV_MIN_CONFIDENCE_THRESHOLD = 0.70; function getJevApiKey() { try { if (typeof GM_getValue === 'function') { const val = GM_getValue(JEV_API_KEY_STORAGE_KEY, ''); if (val) return String(val).trim(); } } catch (_) {} return (localStorage.getItem(JEV_API_KEY_STORAGE_KEY) || '').trim(); } function setJevApiKey(key) { const clean = (key || '').trim(); try { if (typeof GM_setValue === 'function') { GM_setValue(JEV_API_KEY_STORAGE_KEY, clean); } } catch (_) {} if (clean) { localStorage.setItem(JEV_API_KEY_STORAGE_KEY, clean); } else { localStorage.removeItem(JEV_API_KEY_STORAGE_KEY); } } function getJevEndpoint() { try { if (typeof GM_getValue === 'function') { const ep = GM_getValue(JEV_ENDPOINT_STORAGE_KEY, ''); if (ep) return String(ep).trim(); } } catch (_) {} const local = (localStorage.getItem(JEV_ENDPOINT_STORAGE_KEY) || '').trim(); if (local) return local; // Default to direct TypeSafe API if key exists, otherwise community relay proxy if (getJevApiKey()) return JEV_DIRECT_ENDPOINT; return typeof COMMUNITY_RELAY_URL !== 'undefined' ? `${COMMUNITY_RELAY_URL}/ai/jev` : JEV_DIRECT_ENDPOINT; } function setJevEndpoint(url) { const clean = (url || '').trim(); try { if (typeof GM_setValue === 'function') { GM_setValue(JEV_ENDPOINT_STORAGE_KEY, clean); } } catch (_) {} if (clean) { localStorage.setItem(JEV_ENDPOINT_STORAGE_KEY, clean); } else { localStorage.removeItem(JEV_ENDPOINT_STORAGE_KEY); } } function isJevConfigured() { return Boolean(getJevApiKey() || (getJevEndpoint() && !getJevEndpoint().includes('api.typesafe.ai'))); } function isJevEnabled() { return localStorage.getItem(JEV_ENABLED_STORAGE_KEY) === 'true'; } function setJevEnabled(enabled) { localStorage.setItem(JEV_ENABLED_STORAGE_KEY, enabled ? 'true' : 'false'); } /** * Formats structured TypeSafe Jev System One evaluation request payload. * Supported question types: 'choice', 'noul' (boolean), 'score' */ function formatJevPayload({ state, questionType = 'choice', instructions = '', choices = [], criteria = null }) { const isBool = questionType === 'boolean' || questionType === 'noul'; const type = isBool ? 'noul' : (questionType === 'score' ? 'score' : 'choice'); let formattedCriteria = criteria; if (!formattedCriteria && !isBool && Array.isArray(choices) && choices.length > 0) { formattedCriteria = {}; choices.forEach((choiceText, idx) => { const label = `opt_${idx}`; formattedCriteria[label] = String(choiceText || '').trim(); }); } const questionDef = { type: type, instructions: instructions || (isBool ? 'Is this statement true?' : 'Select the single best answer.') }; if (formattedCriteria) { questionDef.criteria = formattedCriteria; } return { model: JEV_DEFAULT_MODEL, state: typeof state === 'string' ? state.trim() : JSON.stringify(state), questions: { decision: questionDef } }; } /** * Low-level network executor for Jev evaluations. * Handles Bearer auth for direct TypeSafe calls or proxy relay headers. */ function executeJevRequest({ apiKey = null, endpoint = null, payload, signal = null, timeoutMs = JEV_DEFAULT_TIMEOUT_MS }) { return new Promise((resolve, reject) => { const targetUrl = endpoint || getJevEndpoint(); const key = apiKey !== null ? apiKey : getJevApiKey(); const bodyStr = typeof payload === 'string' ? payload : JSON.stringify(payload); const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; if (key) { headers['Authorization'] = `Bearer ${key}`; } if (typeof CLIENT_VERSION !== 'undefined') { headers['X-AMAES-Client-Version'] = CLIENT_VERSION; } const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; let aborted = false; let timerId = null; if (signal) { if (signal.aborted) return reject(new Error('Request aborted')); signal.addEventListener('abort', () => { aborted = true; if (timerId) clearTimeout(timerId); reject(new Error('Request aborted')); }, { once: true }); } if (gmReq) { let reqHandle = null; timerId = setTimeout(() => { if (!aborted) { aborted = true; try { reqHandle?.abort?.(); } catch (_) {} reject(new Error(`Jev evaluation timed out after ${timeoutMs}ms`)); } }, timeoutMs); reqHandle = gmReq({ method: 'POST', url: targetUrl, headers: headers, data: bodyStr, timeout: timeoutMs, onload: (res) => { if (timerId) clearTimeout(timerId); if (aborted) return; if (res.status >= 200 && res.status < 300) { try { const parsed = JSON.parse(res.responseText); resolve(parsed); } catch (e) { reject(new Error(`Failed to parse Jev response: ${e.message}`)); } } else { reject(new Error(`Jev request failed with HTTP ${res.status}: ${res.responseText.slice(0, 160)}`)); } }, onerror: (err) => { if (timerId) clearTimeout(timerId); if (!aborted) reject(new Error(err?.statusText || 'Jev network error')); }, ontimeout: () => { if (timerId) clearTimeout(timerId); if (!aborted) reject(new Error(`Jev evaluation timed out after ${timeoutMs}ms`)); } }); } else { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); fetch(targetUrl, { method: 'POST', headers: headers, body: bodyStr, signal: controller.signal }).then(async (res) => { clearTimeout(timeoutId); if (!res.ok) { const errText = await res.text().catch(() => ''); throw new Error(`Jev request failed with HTTP ${res.status}: ${errText.slice(0, 160)}`); } return res.json(); }).then(resolve).catch(reject); } }); } /** * Parses Jev API / Vercel Gateway response into clean decision result. */ function parseJevResponse(data, { questionType = 'choice', choices = [], criteria = null } = {}) { if (!data || typeof data !== 'object') { throw new Error('Empty or invalid Jev response object'); } const answer = data.answers?.decision || data.answer || data; const isBool = questionType === 'boolean' || questionType === 'noul'; if (isBool) { const rawProb = typeof answer.noul === 'number' ? answer.noul : typeof answer.probability === 'number' ? answer.probability : null; if (rawProb === null) { throw new Error('Jev boolean answer missing probability/noul score'); } const isTrue = rawProb >= 0.50; const confidence = Math.abs(rawProb - 0.50) * 2; // scale 0.5-1.0 to 0.0-1.0 return { ok: true, type: 'boolean', isTrue: isTrue, probability: rawProb, confidence: Number(confidence.toFixed(3)), choiceText: isTrue ? 'True' : 'False', choiceIndex: isTrue ? 0 : 1, raw: answer }; } // Choice evaluation const selectedKey = answer.choice || answer.selected || null; const probabilities = answer.probabilities || {}; const confidence = typeof answer.confidence === 'number' ? answer.confidence : (probabilities[selectedKey] || 0.5); let resolvedText = ''; let resolvedIndex = -1; if (criteria && selectedKey && criteria[selectedKey]) { resolvedText = criteria[selectedKey]; if (Array.isArray(choices)) { resolvedIndex = choices.findIndex(c => normalizeChoice(c) === normalizeChoice(resolvedText)); } } else if (typeof selectedKey === 'string' && selectedKey.startsWith('opt_')) { const idx = parseInt(selectedKey.replace('opt_', ''), 10); if (!isNaN(idx) && choices && choices[idx]) { resolvedIndex = idx; resolvedText = choices[idx]; } } else if (selectedKey) { resolvedText = String(selectedKey).trim(); if (Array.isArray(choices)) { resolvedIndex = choices.findIndex(c => normalizeChoice(c) === normalizeChoice(resolvedText)); } } return { ok: true, type: 'choice', selectedKey: selectedKey, choiceText: resolvedText, choiceIndex: resolvedIndex, probability: probabilities[selectedKey] ?? confidence, confidence: Number(confidence.toFixed(3)), probabilities: probabilities, raw: answer }; } /** * High-level solver using Jev decision model for Moodle questions. * Evaluates Multiple Choice, True/False, or Matching row. */ async function evaluateQuestionWithJev({ que, qData, courseCode = '', signal = null }) { if (!que || !qData) return null; const isBool = Boolean( qData.isTrueFalse || (Array.isArray(qData.choices) && qData.choices.length === 2 && qData.choices.some(c => /^(true|tama)$/i.test(normalizeChoice(c))) && qData.choices.some(c => /^(false|mali)$/i.test(normalizeChoice(c)))) ); const choices = Array.isArray(qData.choices) ? qData.choices.map(c => cleanDOMToAI(c)) : []; if (!isBool && choices.length < 2) return null; // Build state context const stateLines = []; if (courseCode) stateLines.push(`Course: ${courseCode}`); stateLines.push(`Question: ${qData.qText || ''}`); // Include confirmed eliminated choices in the state so Jev knows about past failure ground truths const eliminatedSet = typeof getEliminatedChoicesForQuestion === 'function' ? getEliminatedChoicesForQuestion(que, qData, courseCode) : new Set(); const wrongList = choices.filter(c => eliminatedSet.has(normalizeChoice(c))); if (wrongList.length > 0) { stateLines.push(`Confirmed wrong choices (do NOT pick): ${wrongList.join(' | ')}`); } const criteria = {}; choices.forEach((c, idx) => { criteria[`opt_${idx}`] = c; }); const payload = formatJevPayload({ state: stateLines.join('\n'), questionType: isBool ? 'boolean' : 'choice', instructions: isBool ? 'Evaluate whether the statement is factually true or false in the context of this course.' : 'Choose the single most accurate option that correctly answers the academic question.', choices: choices, criteria: isBool ? null : criteria }); const rawRes = await executeJevRequest({ payload, signal }); const result = parseJevResponse(rawRes, { questionType: isBool ? 'boolean' : 'choice', choices: choices, criteria: criteria }); if (!result || !result.choiceText) return null; // Map choice back to DOM radio/checkbox input const choiceRows = que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr, .answer label'); let matchedInput = null; let matchedRow = null; if (result.choiceIndex >= 0 && choiceRows[result.choiceIndex]) { matchedRow = choiceRows[result.choiceIndex]; matchedInput = matchedRow.querySelector('input[type="radio"], input[type="checkbox"]'); } else { // Text matching fallback across rows for (let i = 0; i < choiceRows.length; i++) { const row = choiceRows[i]; const rowText = cleanDOMToAI(row); if (normalizeChoice(rowText).includes(normalizeChoice(result.choiceText)) || normalizeChoice(result.choiceText).includes(normalizeChoice(rowText))) { matchedRow = row; matchedInput = row.querySelector('input[type="radio"], input[type="checkbox"]'); break; } } } return { provider: 'jev', choiceText: result.choiceText, choiceIndex: result.choiceIndex, probability: result.probability, confidence: result.confidence, isConfident: (result.confidence >= JEV_MIN_CONFIDENCE_THRESHOLD), input: matchedInput, row: matchedRow, raw: result.raw }; } /** * Test helper for diagnostic console and setup checks */ async function testJevConnection(customApiKey = null, customEndpoint = null) { const testPayload = formatJevPayload({ state: 'Academic Test Context: Computer Science Fundamentals', questionType: 'choice', instructions: 'What does CPU stand for?', choices: ['Central Processing Unit', 'Central Program Utility', 'Computer Personal Unit'] }); const started = Date.now(); const res = await executeJevRequest({ apiKey: customApiKey, endpoint: customEndpoint, payload: testPayload, timeoutMs: 5000 }); const parsed = parseJevResponse(res, { questionType: 'choice', choices: ['Central Processing Unit', 'Central Program Utility', 'Computer Personal Unit'] }); const duration = Date.now() - started; return { ok: Boolean(parsed.choiceText), durationMs: duration, decision: parsed.choiceText, confidence: parsed.confidence, raw: res }; } // ========================================== // Google Gemini AI Assistant (Experimental) // ========================================== /** * Feature 4: Moodle Database Error / Empty Question Guard * Returns true if the question appears to have failed to load from the Moodle database * (e.g. empty formulation text, or page shows database error indicators). */ function detectMoodleServerError(que) { if (!que) return false; const formulation = que.querySelector('.formulation'); const qText = formulation ? (formulation.innerText || '').trim() : ''; if (qText.length < 5) return true; const pageText = document.body ? (document.body.innerText || '').toLowerCase() : ''; if (pageText.includes('error reading from database') || pageText.includes('a database error has occurred')) return true; return false; } function getGeminiApiKey() { const keys = getGeminiApiKeys(); return keys.length > 0 ? keys[0] : (toolkitStorage.get(GEMINI_API_KEY_STORAGE_KEY) || geminiApiKey || ''); } function setGeminiApiKey(key) { const trimmed = (key || '').trim(); if (trimmed) { setGeminiApiKeys([trimmed]); } else { setGeminiApiKeys([]); } } function isGeminiConfigured() { return getGeminiApiKeys().length > 0; } function updateAiAssistantUI() { const keys = getGeminiApiKeys(); const isGeminiSet = keys.length > 0; const isJevSet = typeof isJevConfigured === 'function' && isJevConfigured() && isJevEnabled(); const isConfigured = isGeminiSet || isJevSet; const badge = document.getElementById('gemini-status-badge'); if (badge) { const activeModel = cachedWorkingEndpoint ? cachedWorkingEndpoint.model : 'Gemini Flash'; badge.style.color = isConfigured ? 'var(--accent-green)' : 'var(--text-muted)'; if (isGeminiSet && isJevSet) { badge.innerHTML = `Ready (${activeModel} + Jev)`; } else if (isJevSet) { badge.innerHTML = `Ready (TypeSafe Jev)`; } else if (isGeminiSet) { badge.innerHTML = keys.length > 1 ? `Ready (${keys.length} keys)` : `Ready (${activeModel})`; } else { badge.innerHTML = '● Not Configured'; } } const setupBtn = document.getElementById('btn-open-gemini-setup'); if (setupBtn) { setupBtn.innerHTML = `${isConfigured ? 'Configure AI keys' : 'Setup Free AI Assistant'}`; } const quizAiPill = document.getElementById('amaes-ai-quiz-status-pill'); if (quizAiPill) { quizAiPill.textContent = isConfigured ? 'Active' : 'Not Set'; quizAiPill.style.color = isConfigured ? '#34d399' : 'var(--text-muted)'; quizAiPill.style.background = isConfigured ? 'rgba(52, 211, 153, 0.1)' : 'rgba(255, 255, 255, 0.05)'; quizAiPill.style.borderColor = isConfigured ? 'rgba(52, 211, 153, 0.3)' : 'rgba(255, 255, 255, 0.15)'; } const quizConfigBtn = document.getElementById('btn-quiz-config-ai'); if (quizConfigBtn) { quizConfigBtn.innerHTML = `${ICONS.gear} ${isConfigured ? (keys.length > 1 ? `Configure AI Keys (${keys.length})` : 'Configure AI Key') : 'Setup Free AI Key'}`; } } function getMatchingAiRows(que) { if (!que) return []; return Array.from(que.querySelectorAll('.answer table tr, .answer tr, .answer .r0, .answer .r1, .answer [class*="r"], .answer > div, .answer li')) .map(row => { const promptCell = row.querySelector('td.text, td:first-child, .text, [class*="text"]'); const select = row.querySelector('td.control select, select'); const prompt = promptCell ? cleanDOMToAI(promptCell).trim() : ''; return prompt && select ? { row, select, prompt } : null; }) .filter(Boolean); } function parseMatchingAiResponse(answerText, matchingRows) { if (!answerText || !Array.isArray(matchingRows) || matchingRows.length === 0) return []; const byIndex = new Map(); const ambiguousIndexes = new Set(); const lines = answerText .replace(/```(?:text|json)?/gi, '') .split(/\r?\n/) .map(line => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, '').replace(/\*\*/g, '').trim()) .filter(Boolean); lines.forEach(line => { const match = line.match(/^(?:row\s*)?(\d+)\s*[:.)-]\s*(.+)$/i); if (!match) return; const rowIndex = Number(match[1]) - 1; if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= matchingRows.length) return; const target = match[2].trim().replace(/^["']|["']$/g, ''); const matchingRow = matchingRows[rowIndex]; const options = Array.from(matchingRow.select.options || []); const option = options.find(candidate => candidate.value && candidate.value !== '0' && !candidate.text.toLowerCase().includes('choose') && normalizeChoice(candidate.text) === normalizeChoice(target) ); if (!option) return; let wrongAnswers = []; try { wrongAnswers = JSON.parse(matchingRow.select.dataset.amaesKnownWrongAnswers || '[]'); } catch (error) { console.warn('Could not read matching-row AI answer evidence:', error); return; } const optionNorm = normalizeChoice(option.text); if (wrongAnswers.some(wrong => wrong && (wrong.norm === optionNorm || normalizeChoice(wrong.text || wrong.answer || '') === optionNorm) )) return; const previous = byIndex.get(rowIndex); if (previous && previous.option.value !== option.value) { ambiguousIndexes.add(rowIndex); byIndex.delete(rowIndex); return; } if (!ambiguousIndexes.has(rowIndex)) byIndex.set(rowIndex, { ...matchingRow, option, rowIndex }); }); return Array.from(byIndex.values()).sort((left, right) => left.rowIndex - right.rowIndex); } function applyMatchingRowAiSuggestion(row, select, option, autoApply = false) { if (!row || !select || !option) return; row.querySelectorAll('.amaes-ai-matching-suggestion').forEach(el => el.remove()); const badge = document.createElement('div'); badge.className = 'amaes-ai-matching-suggestion'; badge.setAttribute('role', 'status'); badge.style.cssText = 'display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin: 4px 0; color: #7e22ce; font-size: 11px; font-weight: 700;'; const label = document.createElement('span'); label.textContent = `AI suggestion (unverified): ${option.text.trim()}`; const useButton = document.createElement('button'); useButton.type = 'button'; useButton.textContent = autoApply ? 'Selected' : 'Use suggestion'; useButton.disabled = autoApply; useButton.style.cssText = 'padding: 2px 7px; border: 1px solid #c084fc; border-radius: 4px; background: #faf5ff; color: #7e22ce; font-size: 10px; font-weight: 700; cursor: pointer;'; const applyOption = () => { select.value = option.value; select.dispatchEvent(new Event('input', { bubbles: true })); select.dispatchEvent(new Event('change', { bubbles: true })); select.dispatchEvent(new Event('blur', { bubbles: true })); select.style.outline = '2px solid #a855f7'; select.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; useButton.textContent = 'Selected'; useButton.disabled = true; }; useButton.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); applyOption(); }); if (autoApply) { applyOption(); } badge.append(label, useButton); select.insertAdjacentElement('afterend', badge); } async function solveMatchingRowWithAi({ que, row, selectInput, subQText, autoApply = false }) { if (!selectInput || !subQText) return null; const options = Array.from(selectInput.options || []).filter(option => option.value && option.value !== '0' && !option.text.toLowerCase().includes('choose') ); if (options.length === 0) return null; const sessionCacheKey = `amaes_ai_match_${normalizeText(subQText)}`; const cachedAns = sessionStorage.getItem(sessionCacheKey); if (cachedAns) { const cachedOpt = options.find(o => normalizeChoice(o.text) === normalizeChoice(cachedAns)); if (cachedOpt) { applyMatchingRowAiSuggestion(row, selectInput, cachedOpt, autoApply); return cachedOpt; } } const courseInfo = detectCourseInfo(); const courseCode = courseInfo.subjectCode || ''; const lines = []; if (courseCode) lines.push(`[Course: ${courseCode}]`); lines.push(`Question: ${subQText}`); lines.push(`Options: ${options.map(o => o.text.trim()).join(' | ')}`); lines.push(`Reply with ONLY the exact option text from the list above that best answers the question. No explanation.`); const prompt = lines.join('\n'); const apiKey = getAvailableGeminiKey(); if (!apiKey && !isSharedAiFallbackEnabled()) { showToast("Gemini AI is not configured. Please set your free Google AI Studio key.", 3500); return null; } try { let resText = null; if (apiKey) { const res = await callGeminiAPI(prompt, 64); resText = res ? res.text : null; } else { const sharedResult = await callSharedAiFallback({ prompt, maxOutputTokens: 64 }); resText = sharedResult ? (sharedResult.text || sharedResult.answer) : null; } if (!resText) return null; const cleanAns = resText.replace(/```(?:text|json)?/gi, '').replace(/^answer:\s*/i, '').replace(/^['"]|['"]$/g, '').trim(); const cleanNorm = normalizeChoice(cleanAns); let matchedOpt = options.find(o => normalizeChoice(o.text) === cleanNorm); if (!matchedOpt) { matchedOpt = options.find(o => { const oNorm = normalizeChoice(o.text); return (oNorm.length > 2 && cleanNorm.includes(oNorm)) || (cleanNorm.length > 2 && oNorm.includes(cleanNorm)); }); } if (matchedOpt) { sessionStorage.setItem(sessionCacheKey, matchedOpt.text.trim()); applyMatchingRowAiSuggestion(row, selectInput, matchedOpt, autoApply); return matchedOpt; } } catch (err) { logDebug(`Single-row matching AI error: ${err.message}`); } return null; } // Ultra-Compact Prompt Builder: 0 fluff, max token efficiency (~60-120 tokens total) function buildGeminiCompactPrompt(qData, courseCode = '', que = null) { const lines = []; const courseInfo = (typeof detectCourseInfo === 'function') ? detectCourseInfo() : {}; const contextParts = []; const subjectCode = courseCode || courseInfo.subjectCode || ''; if (subjectCode) contextParts.push(`Course: ${subjectCode}`); if (courseInfo.subjectName) contextParts.push(`Subject: ${courseInfo.subjectName}`); if (courseInfo.currentActivityTitle) contextParts.push(`Topic: ${courseInfo.currentActivityTitle}`); if (contextParts.length > 0) { lines.push(`[${contextParts.join(' | ')}]`); } lines.push(`Question: ${qData.qText || ''}`); if (qData.questionType === 'match' && Array.isArray(qData.matchPairs) && qData.matchPairs.length > 0) { const matchingRows = getMatchingAiRows(que); lines.push('[Matching rows: choose one listed option for each row]'); matchingRows.forEach((matchingRow, index) => { const options = Array.from(matchingRow.select.options || []) .filter(option => option.value && option.value !== '0' && !option.text.toLowerCase().includes('choose')) .map(option => option.text.trim()); lines.push(`Row ${index + 1}: ${matchingRow.prompt}`); lines.push(`Options: ${options.join(' | ')}`); try { const wrongAnswers = JSON.parse(matchingRow.select.dataset.amaesKnownWrongAnswers || '[]'); const wrongTexts = wrongAnswers.filter(item => item && item.text).map(item => item.text); if (wrongTexts.length > 0) { lines.push(`Confirmed wrong for this row; do not use: ${wrongTexts.join(' | ')}`); } } catch (error) { console.warn('Could not read matching-row AI prompt evidence:', error); } }); lines.push('Reply with one line per row in exactly this format: Row 1: exact option text. Use each row number and an exact option from that row. Do not add explanations.'); return lines.join('\n'); } // Handle Gapselect / Inline Dropdowns (only when dropdown selects actually exist in DOM or data) const selects = que ? Array.from(que.querySelectorAll('select')) : []; const hasGapSelectOptions = Boolean(qData.gapSelectOptions && qData.gapSelectOptions.length > 0); if ((qData.isGapSelect || selects.length > 0 || hasGapSelectOptions) && (selects.length > 0 || hasGapSelectOptions) && (!qData.choices || qData.choices.length === 0)) { lines.push(`[Dropdown Pick / Fill Blank]`); if (selects.length > 0) { selects.forEach((sel, idx) => { const options = Array.from(sel.querySelectorAll('option')) .map(o => (o.innerText || o.textContent || '').trim()) .filter(o => o && !o.toLowerCase().includes('choose')); if (options.length > 0) { lines.push(`Dropdown ${idx + 1} options: ${options.join(', ')}`); } }); } else if (hasGapSelectOptions) { qData.gapSelectOptions.forEach(g => { lines.push(`Dropdown ${g.index} options: ${g.options.join(', ')}`); }); } if (selects.length <= 1 && (!hasGapSelectOptions || qData.gapSelectOptions.length <= 1)) { lines.push(`Reply with ONLY the exact option text that completes the statement. No explanation.`); } else { lines.push(`Reply with the exact option text for each dropdown (e.g. "Dropdown 1: OptionText, Dropdown 2: OptionText"). No explanation.`); } return lines.join('\n'); } // Handle Short Answer / Fill-in-the-Blank text inputs if (qData.isShortAnswer || (!qData.choices || qData.choices.length === 0)) { lines.push(`[Fill-in-the-Blank / Short Answer]`); // Include confirmed wrong answers so AI won't re-suggest them const shortEliminatedSet = getEliminatedChoicesForQuestion(que, qData, courseCode); if (shortEliminatedSet && shortEliminatedSet.size > 0) { const wrongList = Array.from(shortEliminatedSet).join('", "'); lines.push(`[CONFIRMED WRONG - DO NOT USE THESE]: "${wrongList}"`); lines.push(`CRITICAL: Do NOT suggest any answer in the list above. They have been tested and confirmed incorrect.`); } // Include verified answer hint from DB if available and not eliminated if (que) { const courseInfoSA = detectCourseInfo(); const subCodeSA = courseInfoSA.subjectCode || courseCode || 'GENERAL'; const cachedSA = getCachedAnswers(subCodeSA); if (cachedSA && cachedSA.length > 0) { const mQNorm = normalizeText(qData.qText || ''); const candsSA = cachedSA.filter(c => questionTextMatches(c.qNorm || c.qRaw || c.question, mQNorm)); const verifiedSA = candsSA.filter(c => { if (c.isAiSuggestion) return false; const src = (c.source || '').toLowerCase(); if (src.includes('gemini') || src.includes('ai assistant')) return false; const ansN = c.ansNorm || normalizeChoice(c.ansRaw || c.answer || ''); if (!ansN) return false; // Only suggest if not in confirmed wrong list return !shortEliminatedSet.has(ansN) && !shortEliminatedSet.has(unscriptDigits(ansN)); }); if (verifiedSA.length > 0) { verifiedSA.sort((a, b) => ((b.verified ? 10 : 0) + (b.confirmations || 1)) - ((a.verified ? 10 : 0) + (a.confirmations || 1))); const bestSA = verifiedSA[0]; const hintAns = bestSA.ansRaw || bestSA.answer; if (hintAns) { lines.push(`[DATABASE HINT: Strong evidence suggests the answer is "${hintAns}" — verify this is correct before accepting]`); } } } } lines.push(`RULES FOR IDENTIFICATION & PROGRAMMING QUESTIONS:`); lines.push(`1. Function / Method / Command names: Output the canonical identifier in lowercase or camelCase (e.g. "val" or "val()", "hide" or "hide()", NEVER capitalized like "Val"). Never prepend a leading dot (.) unless explicitly asked for a CSS class selector.`); lines.push(`2. Framework specificity: Adhere strictly to the framework in the question. In MaterializeCSS, the grid prefix for desktop/large screens is 'l' (not Bootstrap 'lg'). Mobile is 's', tablet is 'm', desktop is 'l', extra large is 'xl'.`); lines.push(`3. Format: Reply with ONLY the exact canonical word, term, or method name (typically 1 to 3 words) that directly answers the question. No explanation. No quotes. No trailing punctuation.`); return lines.join('\n'); } lines.push(`Choices:`); const eliminatedSet = getEliminatedChoicesForQuestion(que, qData, courseCode); const unreviewed = typeof getUnreviewedAttemptEntry === 'function' ? getUnreviewedAttemptEntry(qData.qText, courseCode) : null; if (unreviewed && unreviewed.selectedAnswer) { lines.push(`[PREVIOUS UNREVIEWED ATTEMPT]: In a prior attempt that scored only ${unreviewed.percentage}%, "${unreviewed.selectedAnswer}" was chosen. Because that attempt had errors and review was not permitted, that answer might be INCORRECT. Re-evaluate all options critically and choose the best alternative if "${unreviewed.selectedAnswer}" is questionable.`); } if (Array.isArray(qData.choices)) { qData.choices.forEach((c, idx) => { const normC = normalizeChoice(c); let isElim = false; if (eliminatedSet.has(normC) || eliminatedSet.has(unscriptDigits(normC))) { isElim = true; } else if (que) { const choiceRows = que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr, .answer label'); if (choiceRows.length > idx && isChoiceRowEliminated(choiceRows[idx])) { isElim = true; } } if (isElim) { lines.push(`${c} [CONFIRMED WRONG - DO NOT SELECT]`); } else { lines.push(c); } }); } lines.push(``); lines.push(`Reply with ONLY the correct option letter and exact text (e.g., "b. ROM"). Do NOT choose options marked [CONFIRMED WRONG - DO NOT SELECT]. No explanations.`); return lines.join('\n'); } // Question Type Eligibility Guard: ONLY Multiple Choice & True/False are eligible for auto-AI function isEligibleForAiSolver(que, qData) { if (!que || !qData) return false; // Drag and drop questions MUST fall back to manual copy if (qData.isDragDrop) return false; if (que.classList.contains('ddwtos') || que.classList.contains('ddimageortext') || que.classList.contains('ddmarker')) return false; if (que.querySelectorAll('.draghome, .drop, .dropzone, span.droptarget, .droppable').length > 0) return false; // Text inputs / Essay questions MUST fall back to manual copy if (qData.isShortAnswer || qData.isEssay) return false; if (que.querySelectorAll('input[type="text"]:not([type="hidden"]), textarea').length > 0) return false; if (qData.questionType === 'match') { const matchingRows = getMatchingAiRows(que); return Array.isArray(qData.matchPairs) && matchingRows.length > 0 && matchingRows.length === qData.matchPairs.length && matchingRows.every(item => Array.from(item.select.options || []).filter(option => option.value && option.value !== '0' && !option.text.toLowerCase().includes('choose') ).length >= 2); } // Gapselect, essay, and unsupported dropdown questions stay on the manual-copy path. if (que.querySelectorAll('select').length > 0) return false; if (qData.matchPairs && qData.matchPairs.length > 0) return false; // Must have at least 2 choices if (!Array.isArray(qData.choices) || qData.choices.length < 2) return false; // Must have radio or checkbox inputs const choiceInputs = que.querySelectorAll('.answer input[type="radio"], .answer input[type="checkbox"]'); if (choiceInputs.length === 0) return false; return true; } // Multi-Model Fallback Registry: Google AI Studio accounts support different versions and models const GEMINI_CANDIDATES = [ { version: 'v1beta', model: 'gemini-2.5-flash' }, { version: 'v1beta', model: 'gemini-2.0-flash' }, { version: 'v1', model: 'gemini-2.0-flash' }, { version: 'v1beta', model: 'gemini-2.5-flash-lite' }, { version: 'v1beta', model: 'gemini-1.5-flash' }, { version: 'v1', model: 'gemini-1.5-flash' } ]; let cachedWorkingEndpoint = null; try { const savedEp = localStorage.getItem('amaes_gemini_working_endpoint'); if (savedEp) cachedWorkingEndpoint = JSON.parse(savedEp); } catch (_) {} // Low-level request executor for a specific candidate version and model function executeGeminiRequest({ apiKey, prompt, maxOutputTokens = 64, signal, version = 'v1beta', model = GEMINI_MODEL }) { return new Promise((resolve, reject) => { if (!apiKey) return reject(new Error('Missing Gemini API key')); // Use standard clean endpoint without query parameter to prevent multiple auth credential rejections const url = `https://generativelanguage.googleapis.com/${version}/models/${model}:generateContent`; const payload = JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.1, maxOutputTokens: maxOutputTokens } }); const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { let aborted = false; let reqHandle = null; if (signal) { if (signal.aborted) { return reject(new Error('Request aborted')); } signal.addEventListener('abort', () => { aborted = true; if (reqHandle && typeof reqHandle.abort === 'function') { try { reqHandle.abort(); } catch (_) {} } reject(new Error('Request aborted')); }); } reqHandle = gmReq({ method: "POST", url: url, headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey }, data: payload, timeout: GEMINI_TIMEOUT_MS, onload: function (res) { if (aborted) return; try { const data = JSON.parse(res.responseText || '{}'); if (res.status >= 200 && res.status < 300) { const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; resolve({ success: true, text: text.trim(), data, modelUsed: model, versionUsed: version }); } else { const errMsg = data?.error?.message || `HTTP ${res.status}: ${res.statusText}`; reject(new Error(errMsg)); } } catch (e) { reject(new Error(`Failed to parse response: ${e.message}`)); } }, ontimeout: function () { if (aborted) return; reject(new Error('Request timed out after 8 seconds')); }, onerror: function (err) { if (aborted) return; reject(new Error(err?.error || err?.statusText || 'Network connection error')); } }); } else { fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }, body: payload, signal: signal }) .then(async res => { const data = await res.json().catch(() => ({})); if (res.ok) { const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || ''; resolve({ success: true, text: text.trim(), data, modelUsed: model, versionUsed: version }); } else { reject(new Error(data?.error?.message || `HTTP ${res.status}`)); } }) .catch(reject); } }); } // Cross-origin and test-runner compatible Gemini API request executor with multi-endpoint fallback async function callGeminiApi({ apiKey, prompt, maxOutputTokens = 64, signal }) { if (!apiKey) throw new Error('Missing Gemini API key'); const candidates = [...GEMINI_CANDIDATES]; if (cachedWorkingEndpoint && cachedWorkingEndpoint.model && cachedWorkingEndpoint.version) { const idx = candidates.findIndex(c => c.model === cachedWorkingEndpoint.model && c.version === cachedWorkingEndpoint.version); if (idx > -1) { const [known] = candidates.splice(idx, 1); candidates.unshift(known); } else { candidates.unshift(cachedWorkingEndpoint); } } let lastError = null; for (const candidate of candidates) { if (signal && signal.aborted) { throw new Error('Request aborted'); } try { recordAiRequest(apiKey); const res = await executeGeminiRequest({ apiKey, prompt, maxOutputTokens, signal, version: candidate.version, model: candidate.model }); cachedWorkingEndpoint = candidate; try { localStorage.setItem('amaes_gemini_working_endpoint', JSON.stringify(candidate)); } catch (_) {} noteContributorActivity().catch(() => {}); return res; } catch (err) { lastError = err; const errLower = (err.message || '').toLowerCase(); const isModelUnavailable = errLower.includes('not found') || errLower.includes('not supported') || errLower.includes('404') || errLower.includes('does not exist'); if (isModelUnavailable) { logDebug(`Gemini model ${candidate.version}/${candidate.model} unavailable (${err.message}). Trying fallback candidate...`); continue; } // If quota exhausted or rate limit, trigger cooldown and fast fail instead of burning other candidate endpoints if (errLower.includes('quota') || errLower.includes('429') || errLower.includes('rate limit') || errLower.includes('resource_exhausted')) { const delayMatch = (err.message || '').match(/retry\s*(?:in|delay)?\s*[:\s]*(\d+)\s*s/i) || (err.message || '').match(/(\d+)\s*seconds?/i); const waitSec = delayMatch ? parseInt(delayMatch[1], 10) : 20; triggerAiRateLimitCooldown(waitSec, apiKey); throw err; } // If authentication is rejected (wrong key, blocked, or unauthenticated) or user aborted, fail fast if (errLower.includes('api key not valid') || errLower.includes('unauthenticated') || errLower.includes('api_key_service_blocked') || errLower.includes('access_token_type_unsupported') || errLower.includes('aborted')) { throw err; } } } throw lastError || new Error('No available Gemini model found for this key.'); } let sharedAiPoolExhaustedUntil = 0; function isSharedAiPoolTemporarilyExhausted() { return Date.now() < sharedAiPoolExhaustedUntil; } function markSharedAiPoolExhausted(cooldownSec = 60) { sharedAiPoolExhaustedUntil = Date.now() + (cooldownSec * 1000); } async function callSharedAiFallback({ prompt, maxOutputTokens = 64, signal }) { if (!isSharedAiFallbackEnabled()) { throw new Error('Shared AI fallback is disabled'); } const installationHeaders = { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': CLIENT_VERSION, 'X-AMAES-Installation': getAnonymousContributorId() }; const requestFallback = async (mode, owner = false) => { const headers = { ...installationHeaders }; const keyId = getContributorValue(CONTRIBUTOR_KEY_ID_STORAGE_KEY); const ownerToken = getContributorValue(CONTRIBUTOR_OWNER_TOKEN_STORAGE_KEY); if (owner && keyId && ownerToken) { headers['X-AMAES-Owner-Token'] = ownerToken; } const response = await fetch(`${communityRelayUrl}/ai`, { method: 'POST', headers, body: JSON.stringify({ prompt, maxOutputTokens, mode, ...(owner ? { keyId } : {}) }), signal }); const data = await response.json().catch(() => ({})); if (!response.ok) { if (response.status === 429 || response.status === 503 || (data.error && /rate limit|quota|full|busy|exhausted/i.test(data.error))) { markSharedAiPoolExhausted(60); } throw new Error(data.error || `Shared AI HTTP ${response.status}`); } return data; }; if (isContributorSharingEnabled()) { try { return await requestFallback('contributor', true); } catch (ownerError) { logDebug(`Contributor fallback unavailable: ${ownerError.message}`); } } return requestFallback('community'); } // Match AI answer text to the correct choice option in the question function matchAiAnswerToChoice(aiResponseText, que, qData) { if (!aiResponseText || !que || !qData || !qData.choices) return null; const cleanAi = aiResponseText.trim(); // 1. Match by choice prefix letter (e.g. "b. ROM", "b) ROM", "Option B", or standalone "b") const letterMatch = cleanAi.match(/^(?:option\s+|choice\s+)?\(?([a-eA-E])\)?(?:\b|[.:\)\-–\s]|$)/i) || cleanAi.match(/^([a-eA-E])[.:\)\-–\s]/); let matchedChoiceIndex = -1; if (letterMatch) { const letter = letterMatch[1].toLowerCase(); const charCode = letter.charCodeAt(0) - 97; // 'a' -> 0, 'b' -> 1 if (charCode >= 0 && charCode < qData.choices.length) { matchedChoiceIndex = charCode; } } // 2. Match normalized choice content text const cleanAnswerText = normalizeChoice(cleanAi.replace(/^(?:option\s+|choice\s+)?\(?[a-eA-E]\)?(?:\b|[.:\)\-–\s]|$)/i, '')); if (matchedChoiceIndex === -1 && cleanAnswerText) { for (let i = 0; i < qData.choices.length; i++) { const normChoice = normalizeChoice(qData.choices[i]); if (!normChoice) continue; if (normChoice === cleanAnswerText) { matchedChoiceIndex = i; break; } // Guard against short substring false matches (require at least 4 chars) if (cleanAnswerText.length >= 4 && (normChoice.includes(cleanAnswerText) || cleanAnswerText.includes(normChoice))) { matchedChoiceIndex = i; break; } } } // 3. True / False matching if (matchedChoiceIndex === -1) { const lowerAi = cleanAi.toLowerCase(); if (lowerAi.includes('true')) { matchedChoiceIndex = qData.choices.findIndex(c => normalizeChoice(c).includes('true')); } else if (lowerAi.includes('false')) { matchedChoiceIndex = qData.choices.findIndex(c => normalizeChoice(c).includes('false')); } } if (matchedChoiceIndex === -1) return null; // Resolve DOM element corresponding to matched choice index let choiceRows = que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr'); if (choiceRows.length === 0) { choiceRows = que.querySelectorAll('.answer div.r0, .answer div.r1, .answer li, .answer tr'); } if (choiceRows.length === 0) { choiceRows = que.querySelectorAll('.answer label'); } let targetRow = null; let targetInput = null; if (choiceRows.length > matchedChoiceIndex) { targetRow = choiceRows[matchedChoiceIndex]; targetInput = targetRow.querySelector('input[type="radio"], input[type="checkbox"]'); } if (!targetInput) { const allInputs = que.querySelectorAll('.answer input[type="radio"], .answer input[type="checkbox"]'); if (allInputs.length > matchedChoiceIndex) { targetInput = allInputs[matchedChoiceIndex]; targetRow = targetInput.closest('label, div.r0, div.r1, tr, li, div') || targetInput.parentElement; } } return { choiceIndex: matchedChoiceIndex, choiceText: qData.choices[matchedChoiceIndex], row: targetRow, input: targetInput }; } // Injects a prominent, clean card-level pill indicating the question was answered by Google Gemini AI function setQuestionAiTag(que, isAi = true) { if (!que) return; que.querySelectorAll('.amaes-ai-question-tag').forEach(el => el.remove()); if (!isAi) return; // Clean up any "No answer known yet" hint since AI answered it que.querySelectorAll('.amaes-unanswered-hint').forEach(el => el.remove()); const formulation = que.querySelector('.formulation, .content') || que; const tag = document.createElement('div'); tag.className = 'amaes-ai-question-tag'; tag.style.cssText = ` display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; padding: 4px 10px; background: rgba(139, 92, 246, 0.09); border: 1px solid rgba(139, 92, 246, 0.32); border-left: 3px solid #8b5cf6; border-radius: 6px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; line-height: 1.35; box-sizing: border-box; transition: all 0.2s ease; `; tag.title = 'This question was answered using Google Gemini AI. Please verify before submitting.'; tag.innerHTML = `
AI-SOLVED Answered by Google Gemini AI (Unverified Suggestion — Review before submitting)
`; const qtextElem = que.querySelector('.qtext, .formulation .qtext'); if (qtextElem && qtextElem.parentNode === formulation) { formulation.insertBefore(tag, qtextElem); } else { const toolbar = formulation.querySelector('.amaes-que-top-toolbar'); if (toolbar && toolbar.nextSibling) { formulation.insertBefore(tag, toolbar.nextSibling); } else { formulation.insertBefore(tag, formulation.firstChild); } } } // Apply purple outline and AI Suggestion (Gemini) badge function applyAiChoiceHighlight(targetRow) { if (!targetRow) return; targetRow.classList.add('amaes-ai-suggested-choice'); targetRow.style.outline = '2px solid rgba(139, 92, 246, 0.85)'; targetRow.style.backgroundColor = 'rgba(139, 92, 246, 0.09)'; targetRow.style.boxShadow = '0 0 0 1px rgba(139, 92, 246, 0.2)'; targetRow.style.borderRadius = '6px'; targetRow.style.padding = '5px 10px'; let badge = targetRow.querySelector('.amaes-ai-suggested-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'amaes-ai-suggested-badge'; badge.innerHTML = `AI Suggestion (Gemini)`; badge.style.cssText = ` background: #7c3aed; color: #ffffff !important; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 3px rgba(124, 58, 237, 0.3); white-space: nowrap; flex-shrink: 0; `; targetRow.appendChild(badge); } const que = targetRow.closest('.que'); if (que) { setQuestionAiTag(que, true); que.dataset.amaesAiFailed = 'false'; if (typeof updateQuestionAiDrawerState === 'function') { updateQuestionAiDrawerState(que, false); } } } // Fallback bar with dynamic failure reason, Configure Key (if auth error), Retry AI, and Copy for AI function showAiFallbackBar(que, qData, promptText, onRetry, { reason = '', isAuthError = false, isRateLimit = false, waitSeconds = 0 } = {}) { que.querySelectorAll('.amaes-ai-fallback-bar').forEach(el => el.remove()); que.querySelectorAll('.amaes-blockage-hud').forEach(el => el.remove()); que.dataset.amaesAiFailed = 'true'; if (typeof updateQuestionAiDrawerState === 'function') { updateQuestionAiDrawerState(que, true); } if (activeRateLimitTimerInterval) { clearInterval(activeRateLimitTimerInterval); activeRateLimitTimerInterval = null; } const formulation = que.querySelector('.formulation, .content') || que; const bar = document.createElement('div'); bar.className = 'amaes-ai-fallback-bar'; bar.style.cssText = ` margin-bottom: 8px; padding: 5px 10px; background: ${isRateLimit ? '#fff7ed' : '#fdf4ff'}; border: 1px solid ${isRateLimit ? '#fdba74' : '#e879f9'}; border-radius: 6px; display: flex; align-items: center; justify-content: space-between; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; color: ${isRateLimit ? '#9a3412' : '#86198f'}; box-sizing: border-box; flex-wrap: wrap; `; const displayReason = reason || 'AI took too long or was unavailable.'; const retryCount = Number(que.dataset.amaesAiRetryCount) || 0; const showConfig = isAuthError || retryCount >= 2; if (isRateLimit && waitSeconds > 0) { bar.innerHTML = `
BUSY Google AI is temporarily busy: available in ${waitSeconds}s (Prompt copied)
${showConfig ? ` ` : ''}
`; formulation.insertBefore(bar, formulation.firstChild); const configBtn = bar.querySelector('.amaes-ai-config-btn'); if (configBtn) { configBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showGeminiSetupModal("Rate limited or busy? Add another free Google Gemini API key below to increase your capacity and keep solving without waiting."); }; } let secLeft = waitSeconds; const timerEl = bar.querySelector('#amaes-ratelimit-countdown'); const retryBtn = bar.querySelector('.amaes-ai-retry-btn'); const copyBtn = bar.querySelector('.amaes-ai-copy-btn'); if (copyBtn) { copyBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); copyQuestionWithOptionalImage(que, promptText).then((res) => { showToast(res && res.withImage ? 'Visual snippet & question copied for AI.' : 'Question copied for AI.'); }).catch(() => {}); }; } activeRateLimitTimerInterval = setInterval(() => { secLeft--; if (secLeft > 0) { if (timerEl) timerEl.textContent = `${secLeft}s`; if (retryBtn) retryBtn.textContent = `Retry in ${secLeft}s`; } else { clearInterval(activeRateLimitTimerInterval); activeRateLimitTimerInterval = null; if (timerEl) timerEl.textContent = 'Ready!'; if (retryBtn) { retryBtn.textContent = 'Retry AI now'; retryBtn.style.background = '#16a34a'; } if (autoQuizMode && typeof onRetry === 'function') { showToast('Rate limit wait finished. Retrying AI...', 2500); bar.remove(); onRetry(); } } }, 1000); if (retryBtn) { retryBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); clearInterval(activeRateLimitTimerInterval); activeRateLimitTimerInterval = null; bar.remove(); if (typeof onRetry === 'function') onRetry(); }; } return; } // Standard fallback bar (Clean, single-line alert without redundant web AI pills) bar.innerHTML = `
AI WARNING ${displayReason}
${showConfig ? ` ` : ''}
`; formulation.insertBefore(bar, formulation.firstChild); const configBtn = bar.querySelector('.amaes-ai-config-btn'); if (configBtn) { configBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showGeminiSetupModal("Rate limited or busy? Add another free Google Gemini API key below to increase your capacity and keep solving without waiting."); }; } const retryBtn = bar.querySelector('.amaes-ai-retry-btn'); if (retryBtn && typeof onRetry === 'function') { retryBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); que.dataset.amaesAiRetryCount = String((Number(que.dataset.amaesAiRetryCount) || 0) + 1); bar.remove(); onRetry(); }; } const copyBtn = bar.querySelector('.amaes-ai-copy-btn'); if (copyBtn) { copyBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); copyQuestionWithOptionalImage(que, promptText).then((res) => { showToast(res && res.withImage ? 'Visual snippet & question copied for AI.' : 'Question copied for AI.'); }).catch(() => {}); }; } } let activeAiInferenceQueue = Promise.resolve(); async function handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered = false }) { const prevQueue = activeAiInferenceQueue; let releaseLock = () => {}; activeAiInferenceQueue = new Promise(resolve => { releaseLock = resolve; }); try { await prevQueue; } catch (_) {} try { return await _executeGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); } finally { releaseLock(); } } // Handles thinking indicator, watchdog timeout, configurable retries, wrong choice elimination guard, session caching, and auto-copy on fail async function _executeGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered = false }) { que.querySelectorAll('.amaes-ai-thinking-indicator, .amaes-ai-fallback-bar').forEach(el => el.remove()); if (typeof isFeatureDisabledByAdmin === 'function' && isFeatureDisabledByAdmin('aiSolver')) { setLog("[Admin Notice] AI Solver is temporarily disabled by administrator.", "var(--accent-amber)"); showToast("AI Solver is temporarily disabled by administrator.", 4000); if (typeof onFallback === 'function') onFallback({ reason: 'Disabled by administrator', isAuthError: false, isRateLimit: false }); return; } // A retry button or cooldown callback can outlive the question's // fallback UI. Never spend another request after the student answered. const existingAiChoiceBeforeRequest = que.querySelector('.amaes-ai-suggested-choice'); if (!allowAnswered && isQuestionAnswered(que) && !existingAiChoiceBeforeRequest) { setLog(`[AI Guard] Question #${qData ? qData.qNum : ''} already has an answer; no request sent.`, "var(--accent-green)"); return; } // 0. Cache Check: If this question was already solved by AI in this session or on-screen, reuse it with 0 API requests! const existingAiChoice = que.querySelector('.amaes-ai-suggested-choice'); if (existingAiChoice && !isChoiceRowEliminated(existingAiChoice)) { const lbl = existingAiChoice.querySelector('label') || existingAiChoice; const choiceText = cleanDOMToAI(lbl); const input = existingAiChoice.querySelector('input[type="radio"], input[type="checkbox"]'); setLog(`[AI Cache] Reusing existing AI suggestion for Question #${qData ? qData.qNum : ''} (0 API requests)`, "var(--accent-purple)"); if (typeof onSuccess === 'function') { await onSuccess({ row: existingAiChoice, choiceText, input }); } return; } const cachedAns = getCachedAiAnswer(qData); if (cachedAns && cachedAns.choiceText && qData.questionType !== 'match') { const textInput = que.querySelector('input[type="text"].form-control, input.form-control, input[type="text"], input[type="number"], textarea'); if (textInput && (qData.isShortAnswer || (!qData.choices || qData.choices.length === 0)) && que.querySelectorAll('select').length === 0) { textInput.value = cachedAns.choiceText; applyAiTextHighlight(que, textInput, cachedAns.choiceText); setLog(`[AI Cache] Reusing previously solved answer for Question #${qData ? qData.qNum : ''} (0 API requests)`, "var(--accent-purple)"); showToast(`Reused cached AI answer for #${qData ? qData.qNum : ''}.`, 2000); if (typeof onSuccess === 'function') { await onSuccess({ choiceText: cachedAns.choiceText, input: textInput }); } return; } const selectInputs = Array.from(que.querySelectorAll('select')); if (selectInputs.length > 0 && (!qData.choices || qData.choices.length === 0)) { let reselected = 0; selectInputs.forEach((sel, sIdx) => { const options = Array.from(sel.options); const matchOpt = options.find(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optNorm = normalizeText(opt.text); const targetNorm = normalizeText(cachedAns.choiceText); return optNorm === targetNorm || (targetNorm.length > 2 && optNorm.includes(targetNorm)) || (optNorm.length > 2 && targetNorm.includes(optNorm)); }); if (matchOpt) { sel.value = matchOpt.value; sel.dispatchEvent(new Event('input', { bubbles: true })); sel.dispatchEvent(new Event('change', { bubbles: true })); sel.dispatchEvent(new Event('blur', { bubbles: true })); sel.style.outline = '2px solid #a855f7'; sel.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; reselected++; } }); if (reselected > 0) { setLog(`[AI Cache] Reusing previously solved dropdown option for Question #${qData ? qData.qNum : ''} (0 API requests)`, "var(--accent-purple)"); showToast(`Reused cached AI dropdown choice for #${qData ? qData.qNum : ''}.`, 2000); if (typeof onSuccess === 'function') { await onSuccess({ choiceText: cachedAns.choiceText, selects: selectInputs }); } return; } } const matched = matchAiAnswerToChoice(cachedAns.choiceText, que, qData); if (matched && matched.row && !isChoiceRowEliminated(matched.row)) { applyAiChoiceHighlight(matched.row); setLog(`[AI Cache] Reusing previously solved answer for Question #${qData ? qData.qNum : ''} (0 API requests)`, "var(--accent-purple)"); showToast(`Reused cached AI choice for #${qData ? qData.qNum : ''}.`, 2000); if (typeof onSuccess === 'function') { await onSuccess(matched); } return; } } // Jev Decision Provider: If configured and enabled, evaluate with Jev System One first if (typeof isJevConfigured === 'function' && typeof isJevEnabled === 'function' && isJevConfigured() && isJevEnabled()) { try { const courseInfo = detectCourseInfo(); const courseCode = courseInfo.subjectCode || ''; const jevResult = await evaluateQuestionWithJev({ que, qData, courseCode }); if (jevResult && jevResult.row && jevResult.choiceText) { applyAiChoiceHighlight(jevResult.row); saveAiAnswerToCache(qData, jevResult); const probPct = Math.round((jevResult.probability || jevResult.confidence || 0.5) * 100); setLog(`[Jev AI] Suggested ${escapeHtml(jevResult.choiceText)} (${probPct}% probability) for #${qData ? qData.qNum : ''}.`, "var(--accent-purple)"); showToast(`Jev suggested: "${jevResult.choiceText}" (${probPct}%)`, 3000); if (typeof onSuccess === 'function') { await onSuccess(jevResult); } return; } } catch (jevErr) { logDebug(`Jev evaluation fallback: ${jevErr.message}`); } } // 0.1 Rate Limit Guard: If user is on Free Tier and 15 RPM cap is reached or cooling down from 429: const rateLimitStatus = getAiRateLimitStatus(); if (rateLimitStatus.isLimited) { if (getAiAutoCopyOnFail()) { copyQuestionWithOptionalImage(que, promptText).catch(() => {}); } const rlMsg = `Google is temporarily limiting AI requests. Available again in about ${rateLimitStatus.remainingSec} seconds.`; setLog(`[AI Rate Limit] Question #${qData ? qData.qNum : ''}: ${rlMsg} (Prompt copied)`, "var(--accent-amber)"); showToast(`AI rate limit: available in ${rateLimitStatus.remainingSec}s`, 3500); showAiFallbackBar(que, qData, promptText, async () => { await handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); }, { reason: rlMsg, isRateLimit: true, waitSeconds: rateLimitStatus.remainingSec }); if (typeof onFallback === 'function') onFallback(); return; } const formulation = que.querySelector('.formulation, .content') || que; const thinkingEl = document.createElement('div'); thinkingEl.className = 'amaes-ai-thinking-indicator'; thinkingEl.style.cssText = ` margin-bottom: 12px; padding: 10px 14px; background: linear-gradient(135deg, rgba(124, 58, 237, 0.08), rgba(99, 102, 241, 0.08)); border: 1.5px solid #a855f7; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 2px 8px rgba(168, 85, 247, 0.12); `; thinkingEl.innerHTML = `
Gemini is analyzing Question #${qData ? qData.qNum : ''}...
`; formulation.insertBefore(thinkingEl, formulation.firstChild); let isAborted = false; let timedOut = false; const abortCtrl = new AbortController(); activeAiAbortController = abortCtrl; const cancelBtn = thinkingEl.querySelector('.amaes-ai-cancel-btn'); if (cancelBtn) { cancelBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); isAborted = true; abortCtrl.abort(); thinkingEl.remove(); showToast('AI analysis cancelled.'); if (typeof onFallback === 'function') onFallback(); }; } setLog(`[AI Assistant] Asking Gemini for Question #${qData ? qData.qNum : ''}...`, "var(--accent-purple)"); let timeoutId = setTimeout(() => { timedOut = true; abortCtrl.abort(); }, GEMINI_TIMEOUT_MS); let answerText = null; let attempt = 0; let lastError = null; const retryAttempts = getAiRetryCount(); const maxAttempts = 1 + retryAttempts; // 1 initial request + configurable retries (default: 1 + 2 = 3) let sharedFallbackAttempted = false; const maxOutputTokens = qData && qData.questionType === 'match' ? Math.max(256, (qData.matchPairs || []).length * 32) : 64; while (attempt < maxAttempts && !answerText && !isAborted && !timedOut) { if (!allowAnswered && isQuestionAnswered(que) && !que.querySelector('.amaes-ai-suggested-choice')) { abortCtrl.abort(); break; } attempt++; try { if (attempt > 1) { const statusTextEl = thinkingEl.querySelector('.amaes-ai-status-text'); if (statusTextEl) { statusTextEl.textContent = `Gemini is analyzing Question #${qData ? qData.qNum : ''}... (Attempt ${attempt}/${maxAttempts})`; } setLog(`[AI Assistant] Retrying Gemini (Attempt ${attempt}/${maxAttempts}) for Question #${qData ? qData.qNum : ''}...`, "var(--accent-purple)"); } const apiKey = getAvailableGeminiKey(); if (!apiKey) { if (isSharedAiFallbackEnabled() && !isSharedAiPoolTemporarilyExhausted()) { const sharedStatusTextEl = thinkingEl.querySelector('.amaes-ai-status-text'); if (sharedStatusTextEl) { sharedStatusTextEl.textContent = 'Using community shared AI pool...'; } setLog('[AI Assistant] No personal key configured. Using community shared AI pool...', "var(--accent-blue)"); try { const sharedResult = await callSharedAiFallback({ prompt: promptText, maxOutputTokens, signal: abortCtrl.signal }); if (sharedResult && sharedResult.text) { answerText = sharedResult.text; setLog('[AI Assistant] Shared AI responded. The answer is still a suggestion—please review it.', "var(--accent-blue)"); break; } } catch (sharedError) { markSharedAiPoolExhausted(60); logDebug(`Shared AI fallback failed: ${sharedError.message}`); showToast("Shared AI pool is unavailable. Please configure your free Gemini key.", 4000); showGeminiSetupModal("The community shared AI pool is currently unavailable or exhausted. Add your free Google Gemini API key to continue solving questions instantly."); lastError = sharedError; break; } } else { showToast("Gemini AI is not configured. Please set your free Google AI Studio key.", 3500); showGeminiSetupModal("Gemini AI is not configured. Set up your free Google AI Studio key below to get started."); lastError = new Error('Gemini AI is not configured'); break; } } else { const res = await callGeminiApi({ apiKey: apiKey, prompt: promptText, maxOutputTokens, signal: abortCtrl.signal }); if (res && res.text) { answerText = res.text; break; } } } catch (err) { lastError = err; if (isAborted || timedOut) break; logDebug(`Gemini attempt ${attempt} error: ${err.message}`); const errLower = (err.message || '').toLowerCase(); // If authentication is rejected, quota exhausted, or rate limit hit, fail fast instead of doing a pointless retry if (errLower.includes('api key not valid') || errLower.includes('unauthenticated') || errLower.includes('api_key_service_blocked') || errLower.includes('access_token_type_unsupported') || errLower.includes('quota') || errLower.includes('429') || errLower.includes('rate limit') || errLower.includes('resource_exhausted')) { if (!sharedFallbackAttempted && isSharedAiFallbackEnabled() && !isSharedAiPoolTemporarilyExhausted()) { sharedFallbackAttempted = true; const sharedStatusTextEl = thinkingEl.querySelector('.amaes-ai-status-text'); if (sharedStatusTextEl) { sharedStatusTextEl.textContent = 'Your personal key is temporarily busy. Checking limited shared AI help...'; } setLog('[AI Assistant] Your personal key is temporarily busy. Checking limited shared AI help; this may take a few seconds.', "var(--accent-blue)"); try { const sharedResult = await callSharedAiFallback({ prompt: promptText, maxOutputTokens, signal: abortCtrl.signal }); if (sharedResult && sharedResult.text) { answerText = sharedResult.text; setLog('[AI Assistant] Shared AI help responded. The answer is still a suggestion—please review it.', "var(--accent-blue)"); break; } } catch (sharedError) { markSharedAiPoolExhausted(60); logDebug(`Shared AI fallback unavailable: ${sharedError.message}`); if (sharedStatusTextEl) { sharedStatusTextEl.textContent = 'Shared AI help is currently full. Returning to your personal-key options...'; } setLog('[AI Assistant] Shared AI help is currently full or unavailable. No charge was made by this fallback.', "var(--accent-amber)"); } } // Authentication, quota, and rate-limit errors are not // fixed by immediately repeating the same request. The // shared fallback above is the only alternate path. break; } if (attempt < maxAttempts) { await new Promise(r => setTimeout(r, 800)); } } } clearTimeout(timeoutId); thinkingEl.remove(); if (isAborted) return; if (answerText) { // Check for Short-Answer / Fill-in-the-Blank text inputs const textInputs = Array.from(que.querySelectorAll('input[type="text"].form-control, input.form-control, input[type="text"], input[type="number"], textarea')); if (textInputs.length > 0 && (qData.isShortAnswer || (!qData.choices || qData.choices.length === 0)) && que.querySelectorAll('select').length === 0) { let cleaned = answerText .replace(/^Answer:\s*/i, '') .replace(/^The correct answer is:\s*/i, '') .replace(/^[a-e][.)]\s*/i, '') .replace(/^["']|["']$/g, '') .replace(/[.;:]\s*$/, '') .trim(); const qTextLower = (qData.qText || '').toLowerCase(); const isCssClassQuestion = qTextLower.includes('class selector') || qTextLower.includes('css class') || qTextLower.includes('class name'); const isCommandQuestion = qTextLower.includes('command') || qTextLower.includes('method') || qTextLower.includes('function') || qTextLower.includes('property') || qTextLower.includes('jquery'); // Strip leading dots on commands/methods (e.g., ".hide()" -> "hide()", ".val()" -> "val()") if (cleaned.startsWith('.') && isCommandQuestion && !isCssClassQuestion) { cleaned = cleaned.replace(/^\.+/, '').trim(); } // Canonicalize capitalized command names to lowercase (e.g., "Val" -> "val", "Hide" -> "hide") if (isCommandQuestion && /^[A-Z][a-z0-9_]*(\(\))?$/.test(cleaned)) { cleaned = cleaned.charAt(0).toLowerCase() + cleaned.slice(1); } // Fix MaterializeCSS grid prefix confusion (AI returning Bootstrap "lg" instead of MaterializeCSS "l") if (qTextLower.includes('materialize') && qTextLower.includes('prefix') && (qTextLower.includes('desktop') || qTextLower.includes('large'))) { if (cleaned.toLowerCase() === 'lg') { cleaned = 'l'; } } if (cleaned) { let multiAnswers = []; if (/blank\s*\d+\s*:/i.test(cleaned)) { const parts = cleaned.split(/(?=blank\s*\d+\s*:)/i).filter(Boolean); parts.forEach(p => { multiAnswers.push(p.replace(/^blank\s*\d+\s*:\s*/i, '').trim()); }); } else if (textInputs.length > 1 && (cleaned.includes(',') || cleaned.includes('\n') || cleaned.includes(';') || cleaned.includes('|'))) { multiAnswers = cleaned.split(/[\n,;|]+/).map(s => s.trim()).filter(Boolean); } if (multiAnswers.length > 1 && textInputs.length > 1) { textInputs.forEach((ti, tIdx) => { const val = multiAnswers[tIdx] || ''; if (val) { ti.value = val; ti.dispatchEvent(new Event('input', { bubbles: true })); ti.dispatchEvent(new Event('change', { bubbles: true })); ti.dispatchEvent(new Event('blur', { bubbles: true })); applyAiTextHighlight(que, ti, val); } }); } else { const textInput = textInputs[0]; textInput.value = cleaned; textInput.dispatchEvent(new Event('input', { bubbles: true })); textInput.dispatchEvent(new Event('change', { bubbles: true })); textInput.dispatchEvent(new Event('blur', { bubbles: true })); applyAiTextHighlight(que, textInput, cleaned); } saveAiAnswerToCache(qData, { choiceText: cleaned }); recordAttemptAnswerEvidence(que, cleaned, 'ai_inference'); try { const courseInfo = detectCourseInfo(); const sCode = courseInfo.subjectCode || 'GENERAL'; if (cleaned && qData && qData.qText) { const aiEntry = { qRaw: qData.qText, qNorm: normalizeText(qData.qText), ansRaw: cleaned, ansNorm: normalizeChoice(cleaned), questionType: 'shortanswer', choices: [], wrongAnswers: Array.from(getEliminatedChoicesForQuestion(que, qData, sCode) || []), verified: false, isAiSuggestion: true, source: 'Google Gemini AI', confirmations: 1 }; mergeAnswersIntoCache(sCode, [aiEntry], 'Google Gemini AI'); if (typeof queueCommunityContribution === 'function') { queueCommunityContribution(sCode, [aiEntry], { source: 'gemini_ai_suggestion', isAiSuggestion: true }); } } } catch (_) {} setLog(`[AI Suggestion] Gemini suggested ${escapeHtml(cleaned)} for #${qData ? qData.qNum : ''}. (Paused for review)`, "var(--accent-purple)"); showToast(`Gemini suggested: "${cleaned}" for #${qData ? qData.qNum : ''}.`, 3000); if (typeof onSuccess === 'function') { await onSuccess({ choiceText: cleaned, input: textInputs[0] }); } return; } } if (qData.questionType === 'match' && Array.isArray(qData.matchPairs) && qData.matchPairs.length > 0) { const matchingRows = getMatchingAiRows(que); const suggestions = parseMatchingAiResponse(answerText, matchingRows); if (suggestions.length > 0) { const shouldAutoApply = Boolean(aiAutoSelect || autoPickQuiz || autoQuizMode); suggestions.forEach(suggestion => { suggestion.row.querySelectorAll('.amaes-ai-matching-suggestion').forEach(el => el.remove()); const badge = document.createElement('div'); badge.className = 'amaes-ai-matching-suggestion'; badge.setAttribute('role', 'status'); badge.style.cssText = 'display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin: 4px 0; color: #7e22ce; font-size: 11px; font-weight: 700;'; const label = document.createElement('span'); label.textContent = `AI suggestion (unverified): ${suggestion.option.text.trim()}`; const useButton = document.createElement('button'); useButton.type = 'button'; useButton.textContent = 'Use suggestion'; useButton.style.cssText = 'padding: 2px 7px; border: 1px solid #c084fc; border-radius: 4px; background: #faf5ff; color: #7e22ce; font-size: 10px; font-weight: 700; cursor: pointer;'; useButton.addEventListener('click', event => { if (event) { event.preventDefault(); event.stopPropagation(); } suggestion.select.value = suggestion.option.value; suggestion.select.dispatchEvent(new Event('input', { bubbles: true })); suggestion.select.dispatchEvent(new Event('change', { bubbles: true })); suggestion.select.dispatchEvent(new Event('blur', { bubbles: true })); suggestion.select.style.outline = '2px solid #a855f7'; suggestion.select.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; useButton.textContent = 'Selected'; useButton.disabled = true; }); badge.append(label, useButton); suggestion.select.insertAdjacentElement('afterend', badge); if (shouldAutoApply) { useButton.click(); } }); const summary = suggestions.map(item => `Row ${item.rowIndex + 1}: ${item.option.text.trim()}`).join('; '); const partial = suggestions.length < matchingRows.length; setLog(`[AI Suggestion] Gemini suggested ${suggestions.length} of ${matchingRows.length} matching rows${partial ? '; unmatched rows were left unchanged' : ''}.${shouldAutoApply ? ' Auto-selected choices.' : ' Review each unverified row before continuing.'}`, "var(--accent-purple)"); showToast(`Gemini suggested ${suggestions.length} matching answer${suggestions.length === 1 ? '' : 's'}${shouldAutoApply ? ' (auto-selected)' : '; review before continuing'}.`, 3500); if (shouldAutoApply) { que.querySelectorAll('.amaes-blockage-hud, .amaes-ai-fallback-bar, .amaes-unanswered-hint').forEach(el => el.remove()); setQuestionAiTag(que, true); } if (typeof onSuccess === 'function') { await onSuccess({ choiceText: summary, matchingRows: suggestions }); } return; } // If bulk matching output was unconfirmed or unparseable, do not blast 10 sequential API calls // which crashes into the 15 RPM rate limit. Prompt user to use per-row AI buttons on demand. setLog(`[AI Matching] Bulk AI parsing unconfirmed. Use '+ AI' beside each dropdown to solve on-demand.`, "var(--accent-purple)"); const mismatchReason = "Bulk AI could not resolve all matching rows. Click '+ AI' beside each dropdown to solve individually without rate limits."; showAiFallbackBar(que, qData, promptText, async () => { await handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); }, { reason: mismatchReason, isAuthError: false }); showToast(mismatchReason, 4500); if (typeof onFallback === 'function') onFallback(); return; } // Check for Dropdown / Select elements (gapselect) const selectInputs = Array.from(que.querySelectorAll('select')); if (qData.questionType !== 'match' && selectInputs.length > 0 && (!qData.choices || qData.choices.length === 0)) { const cleaned = answerText .replace(/^Answer:\s*/i, '') .replace(/^The correct answer is:\s*/i, '') .replace(/^[a-e][.)]\s*/i, '') .replace(/^["']|["']$/g, '') .trim(); let selectedCount = 0; selectInputs.forEach((sel, sIdx) => { const options = Array.from(sel.options); let targetText = cleaned; if (selectInputs.length > 1) { const lines = cleaned.split(/[\n,;]+/).map(s => s.trim()); for (const line of lines) { const matchPrefix = line.match(/(?:dropdown|blank)\s*(\d+)[:\-\s]+(.*)/i); if (matchPrefix && parseInt(matchPrefix[1], 10) === sIdx + 1) { targetText = matchPrefix[2].trim(); break; } } if (!targetText && lines[sIdx]) { targetText = lines[sIdx].replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); } } const matchOpt = options.find(opt => { if (!opt.value || opt.value === '0' || opt.text.toLowerCase().includes('choose')) return false; const optNorm = normalizeText(opt.text); const targetNorm = normalizeText(targetText); return optNorm === targetNorm || (targetNorm.length > 2 && optNorm.includes(targetNorm)) || (optNorm.length > 2 && targetNorm.includes(optNorm)); }); if (matchOpt) { sel.value = matchOpt.value; sel.dispatchEvent(new Event('input', { bubbles: true })); sel.dispatchEvent(new Event('change', { bubbles: true })); sel.dispatchEvent(new Event('blur', { bubbles: true })); selectedCount++; sel.style.outline = '2px solid #a855f7'; sel.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; } }); if (selectedCount > 0) { saveAiAnswerToCache(qData, { choiceText: cleaned }); recordAttemptAnswerEvidence(que, cleaned, 'ai_inference'); setLog(`[AI Suggestion] Gemini selected ${selectedCount} dropdown option(s) for #${qData ? qData.qNum : ''}.`, "var(--accent-purple)"); showToast(`Gemini selected dropdown option for #${qData ? qData.qNum : ''}.`, 3000); if (typeof onSuccess === 'function') { await onSuccess({ choiceText: cleaned, selects: selectInputs }); } return; } } const matched = matchAiAnswerToChoice(answerText, que, qData); if (matched && matched.row) { // Hard Safety Guard: Check if the AI returned a confirmed WRONG choice! if (isChoiceRowEliminated(matched.row)) { logDebug(`Gemini suggested eliminated choice "${matched.choiceText}". Guard rejecting.`); // Deduction: Check if only 1 uneliminated choice remains! const choiceRows = Array.from(que.querySelectorAll('.answer > div, .answer div.r0, .answer div.r1, .answer li, .answer tr, .answer label')); const validRows = choiceRows.filter(r => !isChoiceRowEliminated(r)); if (validRows.length === 1) { const deducedRow = validRows[0]; const deducedInput = deducedRow.querySelector('input[type="radio"], input[type="checkbox"]'); const deducedText = cleanDOMToAI(deducedRow.querySelector('label') || deducedRow); const deducedMatched = { choiceIndex: choiceRows.indexOf(deducedRow), choiceText: deducedText, row: deducedRow, input: deducedInput }; applyAiChoiceHighlight(deducedRow); saveAiAnswerToCache(qData, deducedMatched); setLog(`[AI Deduction] AI suggested confirmed wrong choice "${escapeHtml(matched.choiceText)}". Deduced remaining valid choice: ${escapeHtml(deducedText)}`, "var(--accent-purple)"); showToast('AI correction applied: one valid choice remained.', 3000); if (typeof onSuccess === 'function') { await onSuccess(deducedMatched); } return; } else { // More than 1 uneliminated choices remain; do NOT select the wrong answer! const wrongReason = `AI suggested "${matched.choiceText}", but it is confirmed INCORRECT by database.`; if (getAiAutoCopyOnFail()) { copyQuestionWithOptionalImage(que, promptText).catch(() => {}); } showAiFallbackBar(que, qData, promptText, async () => { await handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); }, { reason: wrongReason, isAuthError: false }); showToast(wrongReason, 4500); setLog(`[AI Wrong Answer Blocked] Question #${qData ? qData.qNum : ''}: ${wrongReason}`, "var(--accent-pink)"); if (typeof onFallback === 'function') onFallback(); return; } } // Valid answer that is NOT eliminated saveAiAnswerToCache(qData, matched); applyAiChoiceHighlight(matched.row); // Cache as unverified AI suggestion in course study bank try { const courseInfo = detectCourseInfo(); const sCode = courseInfo.subjectCode || 'GENERAL'; const rawAns = (matched.choiceText || '').replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (rawAns && qData && qData.qText) { const aiEntry = { qRaw: qData.qText, qNorm: normalizeText(qData.qText), ansRaw: rawAns, ansNorm: normalizeChoice(rawAns), questionType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : (Array.isArray(qData.choices) && qData.choices.length > 0 ? 'multichoice' : 'shortanswer')), choices: qData.choices || [], wrongAnswers: Array.from(getEliminatedChoicesForQuestion(que, qData, sCode) || []), verified: false, isAiSuggestion: true, source: 'Google Gemini AI', confirmations: 1 }; mergeAnswersIntoCache(sCode, [aiEntry], 'Google Gemini AI'); if (typeof queueCommunityContribution === 'function') { queueCommunityContribution(sCode, [aiEntry], { source: 'gemini_ai_suggestion', isAiSuggestion: true }); } } } catch (_) {} if (typeof onSuccess === 'function') { await onSuccess(matched); } return; } else { logDebug(`Gemini returned "${answerText}" but could not be mapped to choices.`); const cleanSnippet = answerText.trim().replace(/\s+/g, ' ').slice(0, 32); const mismatchReason = `AI suggested "${cleanSnippet}", but it couldn't be matched to any option.`; if (getAiAutoCopyOnFail()) { copyQuestionWithOptionalImage(que, promptText).catch(() => {}); } showAiFallbackBar(que, qData, promptText, async () => { await handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); }, { reason: mismatchReason, isAuthError: false }); showToast(mismatchReason, 4500); setLog(`[AI Choice Mismatch] Question #${qData ? qData.qNum : ''}: ${mismatchReason}`, "var(--accent-amber)"); if (typeof onFallback === 'function') onFallback(); return; } } // Categorize the true failure reason for clear feedback let failureReason = 'AI was unavailable.'; let isAuthError = false; let isRateLimit = false; let waitSeconds = 0; if (timedOut) { failureReason = 'AI took too long to respond (timed out after 8s).'; } else if (lastError) { const errMsg = lastError.message || ''; const errLower = errMsg.toLowerCase(); if (errLower.includes('unauthenticated') || errLower.includes('invalid authentication') || errLower.includes('api key not valid') || errLower.includes('api_key_service_blocked') || errLower.includes('access_token_type_unsupported')) { failureReason = 'Google rejected API key. Check key in Course Tools.'; isAuthError = true; } else if (errLower.includes('quota') || errLower.includes('429') || errLower.includes('rate limit') || errLower.includes('resource_exhausted')) { isRateLimit = true; const status = getAiRateLimitStatus(); waitSeconds = status.isLimited ? status.remainingSec : 20; failureReason = `AI Studio rate limit / quota exceeded. (Available in ${waitSeconds}s)`; } else if (errLower.includes('not found') || errLower.includes('no available gemini model') || errLower.includes('404')) { failureReason = 'Gemini model unavailable. Check key permissions.'; } else { failureReason = `AI Error: ${errMsg.slice(0, 48)}`; } } // Auto-copy question to clipboard if enabled on failure if (getAiAutoCopyOnFail()) { copyQuestionWithOptionalImage(que, promptText).then((res) => { showToast(res && res.withImage ? 'Visual snippet & question copied to clipboard for external AI solving.' : 'Question copied to clipboard for external AI solving.', 3500); }).catch(() => {}); } // Failure or timeout: inject fallback bar with clear reason and direct configure button if auth error showAiFallbackBar(que, qData, promptText, async () => { await handleGeminiQuestionInference({ que, qData, promptText, onSuccess, onFallback, allowAnswered }); }, { reason: failureReason, isAuthError: isAuthError, isRateLimit: isRateLimit, waitSeconds: waitSeconds }); showToast(failureReason, 4500); setLog(`[AI Fallback] Question #${qData ? qData.qNum : ''}: ${failureReason}`, "var(--accent-amber)"); if (typeof onFallback === 'function') { onFallback(); } } // Visual highlight & suggestion badge for Short-Answer / text input questions function applyAiTextHighlight(que, textInput, answerText) { if (!que || !textInput) return; textInput.style.borderColor = '#8b5cf6'; textInput.style.boxShadow = '0 0 0 2px rgba(139, 92, 246, 0.2)'; textInput.style.borderRadius = '5px'; que.querySelectorAll('.amaes-ai-text-badge').forEach(el => el.remove()); const badge = document.createElement('div'); badge.className = 'amaes-ai-text-badge'; badge.style.cssText = ` display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: rgba(139, 92, 246, 0.12); color: #7c3aed; border: 1px solid rgba(139, 92, 246, 0.3); `; badge.innerHTML = `${ICONS.gemini} AI Suggestion: ${escapeHtml(answerText)}`; const parent = textInput.parentElement || textInput; if (parent.nextSibling) { parent.parentNode.insertBefore(badge, parent.nextSibling); } else { parent.parentNode.appendChild(badge); } } // Manual on-demand AI solver trigger (invoked by clicking "Ask AI" or "Retry AI" on question card or blockage HUD) async function manualSolveWithAi(que, triggerBtn = null) { if (!que) return; setActiveQuestion(que, true); const keys = getGeminiApiKeys(); if (keys.length === 0) { if (!isSharedAiFallbackEnabled()) { showToast("Gemini AI is not configured. Please set your free Google AI Studio key.", 3500); showGeminiSetupModal("Gemini AI is not configured. Set up your free Google AI Studio key below to get started."); return; } if (isSharedAiPoolTemporarilyExhausted()) { showToast("Shared AI pool is temporarily busy. Add your own free key for instant answers.", 3500); showGeminiSetupModal("The community shared AI pool is currently busy or rate-limited. Please configure your own free Gemini key below to solve questions without waiting."); return; } } const qData = extractQuestionData(que); if (!qData) { showToast("Could not extract question content.", 3000); return; } que.dataset.amaesAiAttempted = 'true'; que.dataset.amaesAiRetryCount = String((Number(que.dataset.amaesAiRetryCount) || 0) + 1); // Update button text to loading state const cardAiBtn = que.querySelector('.amaes-ask-ai-card-btn'); const blockageAiBtn = que.querySelector('#btn-blockage-ask-ai'); if (cardAiBtn) cardAiBtn.innerHTML = `Asking AI...`; if (blockageAiBtn) blockageAiBtn.innerHTML = `Asking AI...`; const courseInfo = detectCourseInfo(); const courseCode = courseInfo.subjectCode || ''; if (qData.questionType === 'match') { setLog("[AI Matching] Analyzing matching rows with Gemini...", "var(--accent-purple)"); } const promptText = buildGeminiCompactPrompt(qData, courseCode, que); await handleGeminiQuestionInference({ que, qData, promptText, onSuccess: async (matched) => { if (matched && matched.choiceText) { recordAttemptAnswerEvidence(que, matched.choiceText, 'ai_inference'); } que.querySelectorAll('.amaes-blockage-hud, .amaes-ai-fallback-bar, .amaes-unanswered-hint').forEach(el => el.remove()); que.querySelectorAll('.amaes-que-top-toolbar').forEach(toolbar => { toolbar.style.display = 'flex'; }); que.style.outline = '2px solid rgba(139, 92, 246, 0.7)'; que.style.borderRadius = '8px'; setQuestionAiTag(que, true); if (matched && matched.input) { if (matched.input.type === 'radio' || matched.input.type === 'checkbox') { if (typeof safelySelectInput === 'function') { safelySelectInput(matched.input); } else { if (!matched.input.checked) { matched.input.click(); if (!matched.input.checked) matched.input.checked = true; matched.input.dispatchEvent(new Event('input', { bubbles: true })); matched.input.dispatchEvent(new Event('change', { bubbles: true })); } } } } else if (matched && Array.isArray(matched.matchingRows)) { matched.matchingRows.forEach(item => { if (item.select && item.option) { item.select.value = item.option.value; item.select.dispatchEvent(new Event('input', { bubbles: true })); item.select.dispatchEvent(new Event('change', { bubbles: true })); item.select.dispatchEvent(new Event('blur', { bubbles: true })); item.select.style.outline = '2px solid #a855f7'; item.select.style.backgroundColor = 'rgba(168, 85, 247, 0.1)'; } }); } if (cardAiBtn) cardAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; if (blockageAiBtn) blockageAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; showToast(`Gemini resolved Question #${qData ? qData.qNum : ''}!`, 3000); }, onFallback: () => { if (cardAiBtn) cardAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; if (blockageAiBtn) blockageAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; } }); if (cardAiBtn && cardAiBtn.innerHTML.includes('Asking AI...')) { cardAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; } if (blockageAiBtn && blockageAiBtn.innerHTML.includes('Asking AI...')) { blockageAiBtn.innerHTML = `${ICONS.gemini} Retry AI`; } } // Non-tech student setup modal for Google AI Studio API key function showGeminiSetupModal() { const initialNotice = arguments[0] || ''; let modal = document.getElementById('amaes-gemini-modal'); if (modal) modal.remove(); const currentKey = getGeminiApiKey(); modal = document.createElement('div'); modal.id = 'amaes-gemini-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px); z-index: 10000000; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; modal.innerHTML = `
AI Setup Free Google Gemini AI
${initialNotice ? `
${ICONS.alertTriangle}
${escapeHtml(initialNotice)}
` : ''}
Google Gemini API Key
Solves unknown quiz questions instantly for free.
${ICONS.external} Get Free Key
Plan:
Higher Limits with Multiple Keys: Add backup keys to automatically rotate between your keys and bypass rate limits.
Advanced Settings (TypeSafe Jev Classifier & Sharing)
TypeSafe Jev Fast Classifier
Get Key
${currentKey ? ` ` : ''}
`; document.body.appendChild(modal); const closeModal = () => modal.remove(); modal.querySelector('#amaes-gemini-modal-close').onclick = closeModal; modal.querySelector('#amaes-gemini-btn-cancel').onclick = closeModal; modal.onclick = (e) => { if (e.target === modal) closeModal(); }; // inputKey kept for backward compat (returns null since element removed from DOM) const inputKey = modal.querySelector('#amaes-gemini-input-key'); const feedback = modal.querySelector('#amaes-gemini-status-feedback'); const testSaveBtn = modal.querySelector('#amaes-gemini-btn-test-save'); const removeBtn = modal.querySelector('#amaes-gemini-btn-remove'); const sharedFallbackCheckbox = modal.querySelector('#amaes-shared-ai-fallback'); const contributorSharingCheckbox = modal.querySelector('#amaes-contributor-sharing'); // Feature 3: Multi-key UI const existingKeys = getGeminiApiKeys(); const keyRowsContainer = modal.querySelector('#amaes-gemini-key-rows'); const addKeyBtn = modal.querySelector('#amaes-gemini-btn-add-key'); const KEY_LABELS = ['Key #1 (Primary)', 'Key #2 (Backup)', 'Key #3 (Backup)', 'Key #4', 'Key #5']; const MAX_KEYS = 5; function renderKeyRowsWithVals(vals) { keyRowsContainer.innerHTML = ''; vals.forEach((val, idx) => { const row = document.createElement('div'); row.className = 'amaes-key-row'; row.style.cssText = 'display: flex; gap: 5px; align-items: center;'; row.innerHTML = ` ${KEY_LABELS[idx] || ('Key #' + (idx+1))} ${idx > 0 ? `` : ''} `; row.querySelector('.amaes-key-paste-btn').onclick = async () => { const inp = row.querySelector('input'); try { if (navigator.clipboard && navigator.clipboard.readText) { const t = await navigator.clipboard.readText(); if (t) inp.value = t.trim(); } else { inp.focus(); inp.select(); } } catch (_) { inp.focus(); } }; const testBtn = row.querySelector('.amaes-key-test-btn'); if (testBtn) { testBtn.onclick = async () => { const inpVal = row.querySelector('input').value.trim(); if (!inpVal) { showToast('Paste or type an API key first to test.', 2500); return; } testBtn.disabled = true; testBtn.textContent = 'Testing...'; try { const res = await callGeminiApi({ apiKey: inpVal, prompt: 'Ping', maxOutputTokens: 2 }); if (res && res.success) { testBtn.textContent = 'Valid'; testBtn.style.background = 'rgba(16, 185, 129, 0.2)'; testBtn.style.borderColor = 'rgba(16, 185, 129, 0.4)'; testBtn.style.color = '#34d399'; showToast(`${KEY_LABELS[idx] || ('Key #' + (idx+1))} is valid!`); } else { testBtn.textContent = 'Failed'; testBtn.style.background = 'rgba(239, 68, 68, 0.2)'; testBtn.style.borderColor = 'rgba(239, 68, 68, 0.4)'; testBtn.style.color = '#f87171'; const errMsg = res?.error || 'Validation error'; showToast(`Key error: ${errMsg.slice(0, 60)}`, 3500); } } catch (err) { testBtn.textContent = 'Failed'; testBtn.style.background = 'rgba(239, 68, 68, 0.2)'; testBtn.style.borderColor = 'rgba(239, 68, 68, 0.4)'; testBtn.style.color = '#f87171'; showToast(`Key error: ${(err.message || err).slice(0, 60)}`, 3500); } finally { setTimeout(() => { testBtn.disabled = false; }, 500); } }; } const removeRowBtn = row.querySelector('.amaes-key-remove-btn'); if (removeRowBtn) { removeRowBtn.onclick = () => { const current = Array.from(keyRowsContainer.querySelectorAll('.amaes-key-row')) .map(r => r.querySelector('input').value.trim()) .filter((_, i) => i !== idx); if (current.length === 0) current.push(''); renderKeyRowsWithVals(current); if (addKeyBtn) addKeyBtn.style.display = current.length >= MAX_KEYS ? 'none' : 'block'; }; } keyRowsContainer.appendChild(row); }); if (addKeyBtn) addKeyBtn.style.display = vals.length >= MAX_KEYS ? 'none' : 'block'; } // Initial render renderKeyRowsWithVals(existingKeys.length > 0 ? existingKeys : ['']); if (addKeyBtn) { addKeyBtn.onclick = () => { const currentVals = Array.from(keyRowsContainer.querySelectorAll('.amaes-key-row')) .map(r => r.querySelector('input').value.trim()); if (currentVals.length < MAX_KEYS) { currentVals.push(''); renderKeyRowsWithVals(currentVals); } }; } // Jev Decision Engine elements const jevInput = modal.querySelector('#amaes-jev-input-key'); const jevPasteBtn = modal.querySelector('#amaes-jev-btn-paste'); const jevTestBtn = modal.querySelector('#amaes-jev-btn-test'); const jevCheckbox = modal.querySelector('#amaes-jev-enable-checkbox'); const jevFeedback = modal.querySelector('#amaes-jev-status-feedback'); if (jevPasteBtn && jevInput) { jevPasteBtn.onclick = async () => { try { if (navigator.clipboard && navigator.clipboard.readText) { const t = await navigator.clipboard.readText(); if (t) jevInput.value = t.trim(); } else { jevInput.focus(); jevInput.select(); } } catch (_) { jevInput.focus(); } }; } if (jevTestBtn && jevInput && jevFeedback) { jevTestBtn.onclick = async () => { const testKey = jevInput.value.trim(); jevTestBtn.disabled = true; jevTestBtn.textContent = 'Testing...'; jevFeedback.style.display = 'block'; jevFeedback.style.background = 'rgba(59, 130, 246, 0.15)'; jevFeedback.style.color = '#60a5fa'; jevFeedback.style.border = '1px solid rgba(59, 130, 246, 0.3)'; jevFeedback.textContent = 'Testing TypeSafe Jev System One endpoint...'; try { const testRes = await testJevConnection(testKey || null); jevFeedback.style.background = 'rgba(16, 185, 129, 0.15)'; jevFeedback.style.color = '#34d399'; jevFeedback.style.border = '1px solid rgba(16, 185, 129, 0.3)'; jevFeedback.textContent = `Jev Connected! Decision: "${testRes.decision}" (${Math.round((testRes.confidence || 0.9) * 100)}% conf in ${testRes.durationMs}ms)`; } catch (e) { jevFeedback.style.background = 'rgba(239, 68, 68, 0.15)'; jevFeedback.style.color = '#f87171'; jevFeedback.style.border = '1px solid rgba(239, 68, 68, 0.3)'; jevFeedback.textContent = `Jev Error: ${e.message.slice(0, 100)}`; } finally { jevTestBtn.disabled = false; jevTestBtn.textContent = 'Test Jev'; } }; } if (removeBtn) { removeBtn.onclick = () => { deleteContributorKey().catch(() => {}); setGeminiApiKeys([]); if (typeof setJevApiKey === 'function') setJevApiKey(''); if (typeof setJevEnabled === 'function') setJevEnabled(false); showToast('AI API keys removed.'); closeModal(); }; } if (testSaveBtn && feedback) { testSaveBtn.onclick = async () => { const allKeyVals = Array.from(keyRowsContainer.querySelectorAll('.amaes-key-row input')) .map(inp => inp.value.trim()) .filter(Boolean); const rawKey = allKeyVals[0] || ''; const jevKeyVal = jevInput ? jevInput.value.trim() : ''; if (jevInput && typeof setJevApiKey === 'function') { setJevApiKey(jevKeyVal); } if (jevCheckbox && typeof setJevEnabled === 'function') { setJevEnabled(jevCheckbox.checked); } if (!rawKey) { if (jevKeyVal || (jevCheckbox && jevCheckbox.checked)) { feedback.style.display = 'block'; feedback.style.background = 'rgba(16, 185, 129, 0.15)'; feedback.style.color = '#34d399'; feedback.style.border = '1px solid rgba(16, 185, 129, 0.3)'; feedback.innerText = 'TypeSafe Jev settings saved successfully!'; showToast('TypeSafe Jev settings saved.'); updateAiAssistantUI(); setTimeout(closeModal, 1000); return; } feedback.style.display = 'block'; feedback.style.background = 'rgba(239, 68, 68, 0.15)'; feedback.style.color = '#f87171'; feedback.style.border = '1px solid rgba(239, 68, 68, 0.3)'; feedback.innerText = 'Please paste or type your Gemini API key or TypeSafe Jev key.'; return; } testSaveBtn.disabled = true; testSaveBtn.innerText = 'Testing Key...'; feedback.style.display = 'block'; feedback.style.background = 'rgba(59, 130, 246, 0.15)'; feedback.style.color = '#60a5fa'; feedback.style.border = '1px solid rgba(59, 130, 246, 0.3)'; feedback.innerText = 'Testing connection with Google Gemini...'; try { const res = await callGeminiApi({ apiKey: rawKey, prompt: 'Ping', maxOutputTokens: 2 }); if (res && res.success) { setGeminiApiKeys(allKeyVals); if (sharedFallbackCheckbox) { setSharedAiFallbackEnabled(sharedFallbackCheckbox.checked); } if (contributorSharingCheckbox) { let wantsContributorSharing = contributorSharingCheckbox.checked; const currentFingerprint = await contributorKeyFingerprint(rawKey); const registeredFingerprint = getContributorValue(CONTRIBUTOR_KEY_FINGERPRINT_STORAGE_KEY); if (wantsContributorSharing && isContributorSharingEnabled() && currentFingerprint !== registeredFingerprint) { await deleteContributorKey(); } if (wantsContributorSharing && !isContributorSharingEnabled()) { await registerContributorKey(rawKey); } else if (!wantsContributorSharing && isContributorSharingEnabled()) { await deleteContributorKey(); } } const planSelect = modal.querySelector('#amaes-gemini-plan-select'); if (planSelect) { setAiPlanTier(planSelect.value); const quizPlanSel = document.getElementById('sel-ai-plan-tier'); if (quizPlanSel) quizPlanSel.value = getAiPlanTier(); const coursePlanSel = document.getElementById('sel-course-ai-plan-tier'); if (coursePlanSel) coursePlanSel.value = getAiPlanTier(); } const modelName = res.modelUsed || 'Gemini Flash'; feedback.style.background = 'rgba(16, 185, 129, 0.15)'; feedback.style.color = '#34d399'; feedback.style.border = '1px solid rgba(16, 185, 129, 0.3)'; feedback.innerHTML = `API key verified and saved. Connected via ${modelName}.` + (isContributorSharingEnabled() ? ` Manage sharing` : ''); showToast(`Google Gemini connected (${modelName}).`); updateAiAssistantUI(); setTimeout(closeModal, 1200); } } catch (err) { testSaveBtn.disabled = false; testSaveBtn.innerText = 'Test & Save Key'; feedback.style.background = 'rgba(239, 68, 68, 0.15)'; feedback.style.color = '#f87171'; feedback.style.border = '1px solid rgba(239, 68, 68, 0.3)'; const msg = err.message || 'Invalid API key or network error.'; const msgLower = msg.toLowerCase(); if (msgLower.includes('api_key_service_blocked') || msgLower.includes('unauthenticated') || msgLower.includes('access_token_type_unsupported')) { feedback.innerText = `Validation Failed: Key was rejected by Google. In Google AI Studio, ensure you click "Create API key in new project" so the Generative Language API is automatically enabled.`; } else if (msgLower.includes('quota') || msgLower.includes('429') || msgLower.includes('rate limit') || msgLower.includes('resource_exhausted')) { setGeminiApiKeys(allKeyVals); logDebug(`Gemini validation quota response: ${msg}`); feedback.style.background = 'rgba(245, 158, 11, 0.15)'; feedback.style.color = '#fbbf24'; feedback.style.border = '1px solid rgba(245, 158, 11, 0.3)'; const retryMatch = msg.match(/retry in\s+([0-9]+(?:\.[0-9]+)?)s/i); const retryText = retryMatch ? ` Try again in about ${Math.ceil(Number(retryMatch[1]))} seconds.` : ''; feedback.innerText = `Your key was saved. Google is temporarily limiting AI requests.${retryText} You can continue using verified database answers while waiting.`; showToast('Gemini key saved; free-tier quota is temporarily exhausted.', 4500); updateAiAssistantUI(); } else { logDebug(`Gemini validation failed: ${msg}`); feedback.innerText = `We could not connect to Google with this key. Check that it was copied correctly, then try again.`; } } }; } } // ========================================== // Quiz Review Harvester & Answer Sharing // ========================================== // Intelligent Multi-Source Answer Cross-Referencing, Elimination & Consensus Engine let communityShareDebounceTimer = null; function getQuestionIdentity(item) { const qNorm = item.qNorm || normalizeText(item.qRaw || item.question); const choiceNorms = Array.isArray(item.choices) ? item.choices.map(choice => normalizeChoice(choice)).filter(Boolean).sort() : []; return `${qNorm}::${choiceNorms.join('|')}`; } function mergeAnswersIntoCache(subCode, newQuestions, sourceLabel = 'Imported') { let existing = getCachedAnswers(subCode) || []; if (!Array.isArray(newQuestions)) { throw new TypeError('Incoming answer data must be an array'); } let addedCount = 0; let confirmedCount = 0; let conflictCount = 0; let eliminatedCount = 0; newQuestions.forEach(newItem => { const qNorm = newItem.qNorm || normalizeText(newItem.qRaw || newItem.question); const ansRaw = newItem.ansRaw || newItem.answer || ''; const ansNorm = newItem.ansNorm || normalizeChoice(ansRaw); const incomingWrong = normalizeWrongAnswers(newItem.wrongAnswers); // If neither correct answer nor eliminated wrong answers exist, ignore if (!qNorm || (!ansRaw && incomingWrong.length === 0)) return; const incomingIdentity = getQuestionIdentity({ ...newItem, qNorm }); const idx = existing.findIndex(ex => { if (ex.qNorm !== qNorm) return false; // Legacy entries without choices remain compatible with the // question-text identity used by older cache versions. if (!Array.isArray(ex.choices) || ex.choices.length === 0 || !Array.isArray(newItem.choices) || newItem.choices.length === 0) { return true; } return getQuestionIdentity(ex) === incomingIdentity; }); if (idx === -1) { // Brand new question added to database const entry = { qRaw: newItem.qRaw || newItem.question, qNorm: qNorm, ansRaw: ansRaw, ansNorm: ansNorm, answers: Array.isArray(newItem.answers) && newItem.answers.length > 0 ? newItem.answers : undefined, choices: newItem.choices || [], verified: Boolean(newItem.verified), deduced: Boolean(newItem.deduced), isAiSuggestion: Boolean(newItem.isAiSuggestion), period: newItem.period || newItem.term || detectTermFromText(newItem.quizTitle || newItem.qRaw || '') || 'General', quizTitle: newItem.quizTitle || '', wrongAnswers: incomingWrong, wrongAnswerEvidence: Boolean(newItem.wrongAnswerEvidence || newItem.evidenceType === 'moodle_review_elimination' || sourceLabel === 'Review'), confirmations: ansRaw ? 1 : 0, source: newItem.source || sourceLabel, evidenceType: newItem.evidenceType || '', sources: [sourceLabel] }; // Deduction check: if choices exist and all but 1 are eliminated if (!entry.ansRaw && Array.isArray(entry.choices) && entry.choices.length > 1) { const wrongNorms = entry.wrongAnswers.map(w => w.norm); const remaining = entry.choices.filter(c => !wrongNorms.includes(normalizeChoice(c))); if (remaining.length === 1) { entry.ansRaw = remaining[0].replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); entry.ansNorm = normalizeChoice(entry.ansRaw); entry.verified = true; entry.deduced = true; entry.sources.push('Elimination Deduction'); } } existing.push(entry); if (ansRaw) addedCount++; if (incomingWrong.length > 0) eliminatedCount += incomingWrong.length; } else { // Question already exists: Cross-reference! const cur = existing[idx]; cur.sources = cur.sources || []; if (!cur.sources.includes(sourceLabel)) cur.sources.push(sourceLabel); if (newItem.evidenceType) cur.evidenceType = newItem.evidenceType; if ((!cur.period || cur.period === 'General') && newItem.period && newItem.period !== 'General') { cur.period = newItem.period; } if (!cur.quizTitle && newItem.quizTitle) cur.quizTitle = newItem.quizTitle; if (Array.isArray(newItem.answers) && newItem.answers.length > 0) { cur.answers = Array.from(new Set((cur.answers || []).concat(newItem.answers))); } if (!Array.isArray(cur.wrongAnswers)) { cur.wrongAnswers = normalizeWrongAnswers(cur.wrongAnswers); } if (newItem.wrongAnswerEvidence || newItem.evidenceType === 'moodle_review_elimination' || sourceLabel === 'Review') { cur.wrongAnswerEvidence = true; } // Merge incoming wrong answers with weighting incomingWrong.forEach(inW => { inW.sources = Array.isArray(inW.sources) ? inW.sources : []; if (!inW.sources.includes(sourceLabel)) inW.sources.push(sourceLabel); // Safety Guard: If incoming wrong answer matches current ansNorm, review/attempt proved cur.ansRaw was WRONG! if (cur.ansNorm && (inW.norm === cur.ansNorm || unscriptDigits(inW.norm) === unscriptDigits(cur.ansNorm))) { cur.ansRaw = ''; cur.ansNorm = ''; cur.verified = false; cur.isAiSuggestion = false; cur.confirmations = 0; } if (Array.isArray(cur.answers)) { cur.answers = cur.answers.filter(a => { const aNorm = normalizeChoice(a); return aNorm !== inW.norm && unscriptDigits(aNorm) !== unscriptDigits(inW.norm); }); if (cur.answers.length === 0) { cur.answers = undefined; } else { cur.ansRaw = cur.answers.join(', '); cur.ansNorm = normalizeChoice(cur.ansRaw); } } const existingW = cur.wrongAnswers.find(w => w.norm === inW.norm || unscriptDigits(w.norm) === unscriptDigits(inW.norm)); if (existingW) { existingW.count = (existingW.count || 1) + (inW.count || 1); if (!existingW.sources) existingW.sources = []; if (!existingW.sources.includes(sourceLabel)) existingW.sources.push(sourceLabel); } else { cur.wrongAnswers.push(inW); eliminatedCount++; } }); // Update choices if missing if (Array.isArray(newItem.choices) && newItem.choices.length > 0 && (!cur.choices || cur.choices.length === 0)) { cur.choices = newItem.choices; } // Handle correct answer const answerIsPreviouslyEliminated = ansRaw && cur.wrongAnswers.some(w => { const wrongNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); return wrongNorm === ansNorm || unscriptDigits(wrongNorm) === unscriptDigits(ansNorm); }); if (ansRaw && (!answerIsPreviouslyEliminated || newItem.verified)) { if (!cur.ansRaw) { cur.ansRaw = ansRaw; cur.ansNorm = ansNorm; cur.verified = Boolean(newItem.verified); if (newItem.deduced) cur.deduced = true; cur.isAiSuggestion = Boolean(newItem.isAiSuggestion); cur.confirmations = 1; if (newItem.source) cur.source = newItem.source; addedCount++; } else { const isSameAnswer = (cur.ansNorm === ansNorm || cur.ansRaw.toLowerCase() === ansRaw.toLowerCase()); if (isSameAnswer) { cur.confirmations = (cur.confirmations || 1) + 1; if (newItem.verified) { cur.verified = true; if (newItem.deduced) cur.deduced = true; cur.isAiSuggestion = false; } confirmedCount++; } else { cur.variations = cur.variations || []; const existingVar = cur.variations.find(v => v.ansNorm === ansNorm); if (existingVar) { existingVar.confirmations = (existingVar.confirmations || 1) + 1; } else { cur.variations.push({ ansRaw: ansRaw, ansNorm: ansNorm, choices: newItem.choices || [], verified: Boolean(newItem.verified), isAiSuggestion: Boolean(newItem.isAiSuggestion), confirmations: 1, source: sourceLabel }); } if (newItem.verified && (!cur.verified || sourceLabel === 'Review')) { cur.variations.push({ ansRaw: cur.ansRaw, ansNorm: cur.ansNorm, choices: cur.choices || [], verified: cur.verified || false, isAiSuggestion: cur.isAiSuggestion || false, source: cur.source || 'Previous' }); cur.ansRaw = ansRaw; cur.ansNorm = ansNorm; cur.verified = true; cur.isAiSuggestion = false; cur.source = 'Review'; } conflictCount++; } } } // Safety Guard: Purge confirmed answer from wrongAnswers! if (cur.ansNorm && Array.isArray(cur.wrongAnswers)) { cur.wrongAnswers = cur.wrongAnswers.filter(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); return wNorm !== cur.ansNorm && unscriptDigits(wNorm) !== unscriptDigits(cur.ansNorm); }); } // Deduction check for existing item if still missing verified answer (or holds unverified AI guess) if ((!cur.ansRaw || (!cur.verified && cur.isAiSuggestion)) && Array.isArray(cur.choices) && cur.choices.length > 1) { const wrongNorms = cur.wrongAnswers.map(w => w.norm); const remaining = cur.choices.filter(c => !wrongNorms.includes(normalizeChoice(c))); if (remaining.length === 1) { cur.ansRaw = remaining[0].replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); cur.ansNorm = normalizeChoice(cur.ansRaw); cur.verified = true; cur.deduced = true; cur.isAiSuggestion = false; if (!cur.sources.includes('Elimination Deduction')) cur.sources.push('Elimination Deduction'); } } } }); setCachedAnswers(subCode, existing); // Community Auto-Share: If user enabled auto-sharing (enabled by default), // and new/confirmed/eliminated data was saved to local database, // send anonymously to the community database relay so the world can benefit. // Avoid pinging if the source is already from cloud sync ('Cloud-Verified', 'Cloud-Fallback', 'Cloud-Amauoed'). try { const autoShareEnabled = localStorage.getItem('amaes_auto_community_share') !== 'false'; const isFromCloudSync = typeof sourceLabel === 'string' && sourceLabel.startsWith('Cloud-'); const isReviewSave = sourceLabel === 'Review'; if (autoShareEnabled && !isFromCloudSync && !isReviewSave && (addedCount > 0 || confirmedCount > 0 || eliminatedCount > 0)) { if (typeof dispatchCommunityContribution === 'function') { clearTimeout(communityShareDebounceTimer); communityShareDebounceTimer = setTimeout(() => { queueCommunityContribution(subCode, existing, { source: sourceLabel }); }, 1500); } } } catch (shareErr) { logDebug(`Community dispatch check note: ${shareErr.message}`); } return { total: existing.length, added: addedCount, confirmed: confirmedCount, conflicts: conflictCount, eliminated: eliminatedCount }; } // Direct GitHub REST API / Web Editor Answer Exporter & Contributor async function pushAnswersToGitHub(subCode, questionsToPush, options = {}) { const repoOwner = localStorage.getItem('amaes_github_owner') || 'Acads-Tools'; const repoName = localStorage.getItem('amaes_github_repo') || 'database'; const pat = localStorage.getItem('amaes_github_token'); const branch = localStorage.getItem('amaes_github_branch') || 'main'; const filePath = `data/${subCode}.json`; const apiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}`; // Ensure questions are valid if (!questionsToPush || questionsToPush.length === 0) { throw new Error('No verified questions to push'); } // If no Personal Access Token is configured, prompt user or guide to 1-click web edit if (!pat) { const fullPayload = exportAnswersAsJSON({ subjectCode: subCode, quizTitle: options.quizTitle || 'Community Contribution', gradeText: options.gradeText || 'Verified Answers', totalQuestions: questionsToPush.length, harvestedCount: questionsToPush.length, questions: questionsToPush }); await copyToClipboard(fullPayload); const userChoice = confirm( `PUSH TO GITHUB (Community Study Hub)\n\n` + `${questionsToPush.length} Verified Q&A entries have been COPIED to your clipboard!\n\n` + `Click OK to open the GitHub Web Editor:\n` + `Press Ctrl+A, Ctrl+V, and click 'Commit changes'.\n\n` + `Tip: To push automatically in 1 click without opening tabs, set your GitHub Token in 'Config'.` ); if (userChoice) { const editUrl = `https://github.com/${repoOwner}/${repoName}/edit/${branch}/${filePath}`; window.open(editUrl, '_blank'); return { success: true, mode: 'web_edit', count: questionsToPush.length }; } return { success: false, mode: 'cancelled' }; } // Direct GitHub REST API Commit (Zero traces of personal username, commits as Open LMS Contributor) const req = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; function doHttp(params) { return new Promise((resolve, reject) => { if (req) { req({ ...params, onload: (res) => resolve(res), onerror: (err) => reject(err) }); } else { fetch(params.url, { method: params.method, headers: params.headers, body: params.data }).then(async res => { const text = await res.text(); resolve({ status: res.status, responseText: text }); }).catch(reject); } }); } // 1. Fetch existing file to retrieve current SHA and merge contents let existingSha = null; let existingQuestions = []; try { const getRes = await doHttp({ method: 'GET', url: `${apiUrl}?ref=${branch}`, headers: { 'Authorization': `token ${pat}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'AMAES-Moodle-Toolkit' } }); if (getRes.status === 200) { const data = JSON.parse(getRes.responseText); existingSha = data.sha; if (data.content) { const decoded = decodeURIComponent(escape(atob(data.content.replace(/\s/g, '')))); const parsedExisting = JSON.parse(decoded); existingQuestions = parseIncomingAnswerPayload(parsedExisting, subCode); } } } catch (e) { console.warn('GitHub file not found or fetch error, creating new dataset:', e); } // 2. Intelligent Merge & Consensus const questionMap = new Map(); existingQuestions.forEach(q => { const key = q.qNorm || normalizeText(q.qRaw || q.question); if (key) { questionMap.set(key, { question: q.qRaw || q.question, answer: q.ansRaw || q.answer, choices: q.choices || [], verified: Boolean(q.verified), confirmations: q.confirmations || 1, source: q.source || 'community' }); } }); questionsToPush.forEach(q => { const key = q.qNorm || normalizeText(q.qRaw || q.question); if (!key) return; const text = q.qRaw || q.question; const ans = q.ansRaw || q.answer; const choices = q.choices || []; const isVer = Boolean(q.verified); if (questionMap.has(key)) { const cur = questionMap.get(key); if (normalizeChoice(cur.answer) === normalizeChoice(ans)) { cur.confirmations = (cur.confirmations || 1) + 1; cur.verified = true; } else if (isVer && !cur.verified) { cur.answer = ans; cur.verified = true; cur.confirmations = 1; } if (!cur.choices || cur.choices.length === 0) cur.choices = choices; } else { questionMap.set(key, { question: text, answer: ans, choices: choices, verified: isVer, confirmations: 1, source: options.quizTitle || 'toolkit-review' }); } }); const mergedList = Array.from(questionMap.values()); const finalPayload = { subjectCode: subCode, subjectName: options.courseTitle || subCode, updatedAt: new Date().toISOString(), totalQuestions: mergedList.length, questions: mergedList }; const jsonString = JSON.stringify(finalPayload, null, 2); const contentBase64 = btoa(unescape(encodeURIComponent(jsonString))); const commitBody = { message: `Update ${subCode} verified answers database (${mergedList.length} questions) via Toolkit`, content: contentBase64, branch: branch, committer: { name: "Open LMS Contributor", email: "academic-contributor@users.noreply.github.com" }, author: { name: "Open LMS Contributor", email: "academic-contributor@users.noreply.github.com" } }; if (existingSha) { commitBody.sha = existingSha; } const putRes = await doHttp({ method: 'PUT', url: apiUrl, headers: { 'Authorization': `token ${pat}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json', 'User-Agent': 'AMAES-Moodle-Toolkit' }, data: JSON.stringify(commitBody) }); if (putRes.status === 200 || putRes.status === 201) { // Merge into local cache too mergeAnswersIntoCache(subCode, mergedList, 'GitHub-Push'); return { success: true, count: mergedList.length, mode: 'api' }; } else { let errMsg = `HTTP ${putRes.status}`; try { const errData = JSON.parse(putRes.responseText); if (errData.message) errMsg = errData.message; } catch (_) {} throw new Error(`GitHub Commit Error: ${errMsg}`); } } // Multi-Tier Cloud Database Synchronization (Verified + AMAUOED Tiers) async function syncAnswersFromCloud(subCode, cloudUrl = null, courseTitle = '') { if (!subCode || subCode.toUpperCase() === 'DEFAULT' || subCode.toUpperCase() === 'GENERAL') { throw new Error('Please select or specify a valid subject code (e.g. CS6301, ITE6301)'); } const cleanSubCode = subCode.trim().toUpperCase(); let resolvedCourseTitle = String(courseTitle || '').trim(); if (!resolvedCourseTitle && typeof detectCourseInfo === 'function') { const info = detectCourseInfo(); if (String(info.subjectCode || '').toUpperCase() === cleanSubCode) { resolvedCourseTitle = info.subjectName || ''; } } const req = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; function fetchJsonUrl(url) { return new Promise((resolve, reject) => { if (!req) { fetch(url) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(resolve) .catch(reject); return; } req({ method: 'GET', url: url, headers: { 'Accept': 'application/json' }, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) { try { resolve(JSON.parse(resp.responseText)); } catch (e) { reject(new Error('Invalid JSON')); } } else { reject(new Error(`HTTP ${resp.status}`)); } }, onerror: () => reject(new Error('Network error connecting to database')) }); }); } const STUDY_GUIDE_CACHE_TTL = 30 * 24 * 60 * 60 * 1000; const studyGuideSessionLoads = new Map(); function studyGuideCacheKey(source, code) { return `amaes_study_guide_${source}_v1_${code}`; } function readStudyGuideLocalCache(source, code) { try { const cached = JSON.parse(localStorage.getItem(studyGuideCacheKey(source, code)) || 'null'); if (!cached || !Number.isFinite(cached.savedAt) || Date.now() - cached.savedAt < 0 || Date.now() - cached.savedAt >= STUDY_GUIDE_CACHE_TTL || !Array.isArray(cached.answers) || cached.answers.length > 5000 || cached.answers.some(answer => !answer || answer.verified !== false || answer.evidenceType !== 'study_guide_candidate' || answer.source !== source)) { return null; } return cached; } catch (error) { logDebug(`Study-guide ${source} cache read note for ${code}: ${error.message}`); return null; } } function writeStudyGuideLocalCache(source, code, sourceUrl, answers, savedAt = Date.now()) { const safeAnswers = answers.filter(answer => answer && answer.qNorm && answer.ansNorm) .map(answer => ({ ...answer, verified: false, confirmations: 1, source, evidenceType: 'study_guide_candidate', sourceUrl: answer.sourceUrl || sourceUrl })); if (!safeAnswers.length) return null; const snapshot = { savedAt, sourceUrl, answers: safeAnswers }; try { localStorage.setItem(studyGuideCacheKey(source, code), JSON.stringify(snapshot)); } catch (error) { logDebug(`Study-guide ${source} cache write note for ${code}: ${error.message}`); } return snapshot; } function parseStudyGuideTier(payload, code, source) { if (!payload || payload.subjectCode !== code || !Array.isArray(payload.questions)) return []; return payload.questions.map(question => { const qRaw = question.question || question.qRaw || ''; const ansRaw = question.answer || question.ansRaw || ''; return { ...question, qRaw, qNorm: normalizeText(qRaw), ansRaw, ansNorm: normalizeChoice(ansRaw), verified: false, confirmations: 1, source, evidenceType: 'study_guide_candidate', sourceUrl: question.sourceUrl || payload.sourceUrl || '' }; }).filter(question => question.qNorm && question.ansNorm); } function studyGuideTierIsFresh(payload) { const timestamp = Date.parse(payload && (payload.refreshedAt || payload.updatedAt) || ''); return Number.isFinite(timestamp) && Date.now() - timestamp >= 0 && Date.now() - timestamp < STUDY_GUIDE_CACHE_TTL; } async function fetchStudyGuideJson(url) { const req = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (!req) { const response = await fetch(url, { headers: { Accept: 'application/json' } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } return new Promise((resolve, reject) => { req({ method: 'GET', url, headers: { Accept: 'application/json' }, onload: response => { if (response.status < 200 || response.status >= 300) { reject(new Error(`HTTP ${response.status}`)); return; } try { resolve(JSON.parse(response.responseText)); } catch (_) { reject(new Error('Invalid study-guide JSON')); } }, onerror: () => reject(new Error('Study-guide database request failed')) }); }); } async function loadAmauoedStudyGuideForCourse(code, courseTitle) { const source = 'amauoed'; const local = readStudyGuideLocalCache(source, code); let shared = null; try { shared = await fetchStudyGuideJson(`${CLOUD_DB_AMAUOED_URL}${code}.json`); const answers = parseStudyGuideTier(shared, code, source); if (answers.length && studyGuideTierIsFresh(shared)) { const snapshot = writeStudyGuideLocalCache( source, code, shared.sourceUrl, answers, Date.parse(shared.refreshedAt || shared.updatedAt) ); mergeAnswersIntoCache(code, snapshot.answers, 'Study-Guide-Amauoed'); return snapshot.answers; } } catch (error) { if (!/HTTP 404/.test(error.message)) logDebug(`AMAUOED shared snapshot note for ${code}: ${error.message}`); } if (local) { queueStudyGuideRefresh(code); mergeAnswersIntoCache(code, local.answers, 'Study-Guide-Amauoed'); return local.answers; } try { const url = await autoFindAmauoedLink(code, courseTitle); if (url) { const scraped = await loadAllAmauoedAnswers(url); const candidates = scraped.map(answer => ({ ...answer, verified: false, source: 'amauoed', evidenceType: 'study_guide_candidate', sourceUrl: url })); const snapshot = writeStudyGuideLocalCache(source, code, url, candidates); if (snapshot) { queueStudyGuideRefresh(code); mergeAnswersIntoCache(code, snapshot.answers, 'Study-Guide-Amauoed'); return snapshot.answers; } } } catch (error) { logDebug(`AMAUOED live lookup note for ${code}: ${error.message}`); } queueStudyGuideRefresh(code); const stale = shared ? parseStudyGuideTier(shared, code, source) : []; const fallback = stale.length ? stale : (local ? local.answers : []); if (fallback.length) mergeAnswersIntoCache(code, fallback, 'Study-Guide-Amauoed'); return fallback; } async function refreshStudyGuideSourcesForCourse(code, courseTitle = '') { const cleanCode = String(code || '').toUpperCase().replace(/[^A-Z0-9_-]/g, ''); if (!cleanCode || cleanCode === 'GENERAL' || cleanCode === 'DEFAULT') return []; if (studyGuideSessionLoads.has(cleanCode)) return studyGuideSessionLoads.get(cleanCode); const load = (async () => { const results = await Promise.allSettled([ loadAmauoedStudyGuideForCourse(cleanCode, courseTitle), loadJennysonlineAnswersForCourse(cleanCode) ]); const answers = []; results.forEach((result, index) => { const source = index === 0 ? 'AMAUOED' : "Jenny's Online"; if (result.status === 'fulfilled') answers.push(...result.value); else logDebug(`${source} course lookup note for ${cleanCode}: ${result.reason.message}`); }); return answers; })(); studyGuideSessionLoads.set(cleanCode, load); try { return await load; } catch (error) { studyGuideSessionLoads.delete(cleanCode); throw error; } } let verifiedCount = 0; let communityCount = 0; let amauoedCount = 0; let jennysonlineCount = 0; // 1. Fetch Verified Tier (Audited Gold Standard) try { const verBase = cloudUrl || cloudDbBaseUrl; const targetVerUrl = verBase.endsWith('/') ? `${verBase}${cleanSubCode}.json` : `${verBase}/${cleanSubCode}.json`; logDebug(`Syncing verified answers from: ${targetVerUrl}`); const verData = await fetchJsonUrl(targetVerUrl); const parsedVer = parseIncomingAnswerPayload(verData, cleanSubCode).map(q => ({ ...q, source: q.source || 'verified_db', verified: true })); mergeAnswersIntoCache(cleanSubCode, parsedVer, 'Cloud-Verified'); verifiedCount = parsedVer.length; } catch (e) { // Fallback to legacy master data/{CODE}.json try { const fbUrl = `${CLOUD_DB_FALLBACK_URL}${cleanSubCode}.json`; logDebug(`Falling back to master dataset: ${fbUrl}`); const fbData = await fetchJsonUrl(fbUrl); const parsedFb = parseIncomingAnswerPayload(fbData, cleanSubCode); mergeAnswersIntoCache(cleanSubCode, parsedFb, 'Cloud-Fallback'); verifiedCount = parsedFb.length; } catch (err) { logDebug(`Verified tier note for ${cleanSubCode}: ${err.message}`); } } // 2. Fetch community tier, including unverified AI suggestions. // These entries are displayed as suggestions but never auto-selected // unless later promoted by review evidence or a 100% score. try { const communityUrl = `${communityDbBaseUrl}${cleanSubCode}.json`; const communityData = await fetchJsonUrl(communityUrl); const parsedCommunity = parseIncomingAnswerPayload(communityData, cleanSubCode).map(q => ({ ...q, source: q.source || 'community_cache', verified: Boolean(q.verified), isAiSuggestion: Boolean(q.isAiSuggestion || q.evidenceType === 'ai_inference') })); mergeAnswersIntoCache(cleanSubCode, parsedCommunity, 'Cloud-Community'); communityCount = parsedCommunity.length; } catch (e) { logDebug(`Community cache tier note for ${cleanSubCode}: ${e.message}`); } // Study-guide sources have independent local/shared freshness and remain unverified. const studyGuideAnswers = await refreshStudyGuideSourcesForCourse(cleanSubCode, resolvedCourseTitle); amauoedCount = studyGuideAnswers.filter(answer => answer.source === 'amauoed').length; jennysonlineCount = studyGuideAnswers.filter(answer => answer.source === 'jennysonline').length; const cachedCount = verifiedCount + communityCount + amauoedCount; const totalAvailable = cachedCount + jennysonlineCount; if (totalAvailable === 0) { throw new Error(`No answer databases found for ${cleanSubCode}`); } return { success: true, count: totalAvailable, cachedCount, verifiedCount, communityCount, amauoedCount, jennysonlineCount }; } async function autoFetchCloudAnswersIfMissing(code) { if (!code || code === 'DEFAULT' || code === 'GENERAL') return false; try { const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : null; const courseTitle = courseInfo && String(courseInfo.subjectCode || '').toUpperCase() === String(code).toUpperCase() ? courseInfo.subjectName || '' : ''; // 1. Check local cache: If answers already exist locally, avoid unnecessary scraping or network calls const existing = getCachedAnswers(code) || []; if (existing.length > 0) { try { await syncAnswersFromCloud(code, null, courseTitle); } catch (error) { logDebug(`Study-guide freshness check note for ${code}: ${error.message}`); } return true; } // 2. Fetch from Cloud / GitHub community database const res = await syncAnswersFromCloud(code, null, courseTitle); if (res && res.count > 0) { return true; } return false; } catch (e) { logDebug(`autoFetchCloudAnswersIfMissing note for ${code}: ${e.message}`); return false; } } function parseIncomingAnswerPayload(payload, defaultSubCode) { const incomingSchema = Number(payload && !Array.isArray(payload) ? payload.databaseSchema || payload.schemaVersion || 1 : 1); if (Number.isFinite(incomingSchema) && incomingSchema > ANSWER_DB_SCHEMA_VERSION) { throw new Error(`Unsupported answer database schema ${incomingSchema}; this toolkit supports schema ${ANSWER_DB_SCHEMA_VERSION}.`); } let list = []; if (Array.isArray(payload)) { list = payload; } else if (payload && Array.isArray(payload.questions)) { list = payload.questions; } return list.map(q => ({ qRaw: q.question || q.qRaw, qNorm: normalizeText(q.question || q.qRaw), ansRaw: q.answer || q.ansRaw, ansNorm: normalizeChoice(q.answer || q.ansRaw), wrongAnswers: Array.isArray(q.wrongAnswers) ? q.wrongAnswers : [], wrongAnswerEvidence: Boolean(q.wrongAnswerEvidence || q.evidenceType === 'moodle_review_elimination'), choices: q.choices || [], verified: q.verified !== false, isAiSuggestion: Boolean(q.isAiSuggestion || q.evidenceType === 'ai_inference'), evidenceType: q.evidenceType || '', confirmations: Number(q.confirmations) || 1, source: q.source || 'db' })); } // Parse Moodle question grade accurately for arbitrary numbers and decimals (e.g. 1.00 out of 1.00, 3.40 out of 5.00, 2.10 out of 3.00, 0.00 out of 2.00) function parseMoodleQuestionGrade(que) { let earned = null; let max = null; const gradeElem = que.querySelector('.info .grade'); if (gradeElem) { const match = gradeElem.innerText.match(/([0-9]+(?:\.[0-9]+)?)\s*out of\s*([0-9]+(?:\.[0-9]+)?)/i); if (match) { earned = parseFloat(match[1]); max = parseFloat(match[2]); } } const hasCorrectClass = que.classList.contains('correct'); const hasIncorrectClass = que.classList.contains('incorrect'); const hasPartialClass = que.classList.contains('partiallycorrect'); const isFullMark = Boolean( (max !== null && max > 0 && Math.abs(earned - max) < 0.001) || (hasCorrectClass && !hasPartialClass && !hasIncorrectClass) ); const isZeroMark = Boolean( (earned !== null && earned === 0) || (hasIncorrectClass && !hasPartialClass && !hasCorrectClass) ); const isPartialMark = Boolean( hasPartialClass || (earned !== null && max !== null && earned > 0 && earned < max && Math.abs(earned - max) >= 0.001) ); return { earned, max, isFullMark, isZeroMark, isPartialMark }; } function hasChoiceCheckmark(elem) { if (!elem) return false; // Priority guard: If choice has an explicit Moodle cross / incorrect marker, it is never a checkmark! if (hasChoiceCross(elem)) return false; if (elem.classList && (elem.classList.contains('correct') || elem.classList.contains('text-success'))) return true; if (elem.querySelector('.fa-check, .fa-check-circle, .feedbackimage[alt*="Correct" i], .feedbackimage[title*="Correct" i], img[src*="tick" i], img[src*="correct" i], img[src*="check" i], [title*="Correct" i], [aria-label*="Correct" i], .text-success, [class*="correct" i], svg[class*="check" i], svg[data-icon="check"], i[data-icon="check"]')) return true; // Clone and strip any toolkit-injected badges so toolkit's own check icons don't trigger false positives const clone = elem.cloneNode(true); clone.querySelectorAll('.amaes-verified-badge, .amaes-eliminated-badge, .amaes-active-focus-badge, .amaes-review-status-pill, .amaes-review-outcome-banner, .amaes-matching-row-badge, .amaes-card-btn-container, .amaes-card-ai-drawer, .amaes-web-ai-row, .amaes-que-top-toolbar, .amaes-probability-hint, .amaes-shortans-hint, .amaes-select-hint, .amaes-drag-hint, .amaes-unanswered-hint, .amaes-ai-suggested-badge, .amaes-ai-text-badge, .amaes-ai-question-tag').forEach(el => el.remove()); const text = (clone.innerText || clone.textContent || ''); if (/[✓✔]/.test(text)) return true; const html = clone.innerHTML || ''; return /fa-check|alt="Correct"|title="Correct"|aria-label="Correct"|correct\.svg|tick\.svg|feedbackimage/i.test(html); } function hasChoiceCross(elem) { if (!elem) return false; if (elem.classList && (elem.classList.contains('incorrect') || elem.classList.contains('text-danger'))) return true; if (elem.querySelector('.fa-remove, .fa-times, .fa-close, .fa-times-circle, .feedbackimage[alt*="Incorrect" i], .feedbackimage[title*="Incorrect" i], img[src*="cross" i], img[src*="incorrect" i], [title*="Incorrect" i], [aria-label*="Incorrect" i], .text-danger, [class*="incorrect" i], svg[class*="times" i], svg[class*="cross" i], svg[data-icon="times"], i[data-icon="times"]')) return true; // Clone and strip any toolkit-injected badges const clone = elem.cloneNode(true); clone.querySelectorAll('.amaes-verified-badge, .amaes-eliminated-badge, .amaes-active-focus-badge, .amaes-review-status-pill, .amaes-review-outcome-banner, .amaes-matching-row-badge, .amaes-card-btn-container, .amaes-card-ai-drawer, .amaes-web-ai-row, .amaes-que-top-toolbar, .amaes-probability-hint, .amaes-shortans-hint, .amaes-select-hint, .amaes-drag-hint, .amaes-unanswered-hint, .amaes-ai-suggested-badge, .amaes-ai-text-badge, .amaes-ai-question-tag').forEach(el => el.remove()); const text = (clone.innerText || clone.textContent || ''); if (/[✗✘✕✖]/.test(text)) return true; const html = clone.innerHTML || ''; return /fa-remove|fa-times|fa-close|alt="Incorrect"|title="Incorrect"|aria-label="Incorrect"|incorrect\.svg|cross\.svg/i.test(html); } function evaluateReviewPageReviewability(rootDoc = document) { if (!rootDoc) return { isReviewable: false, reason: 'no_document' }; const bodyText = (rootDoc.body ? rootDoc.body.innerText : (rootDoc.innerText || '')).toLowerCase(); if (bodyText.includes('you cannot review this attempt') || bodyText.includes('review is not permitted') || bodyText.includes('review of this attempt is not allowed')) { return { isReviewable: false, reason: 'moodle_explicit_restriction' }; } const queList = rootDoc.querySelectorAll('.que'); if (queList.length === 0) { return { isReviewable: false, reason: 'no_questions' }; } let hasGroundTruth = false; for (const que of queList) { if (que.querySelector('.rightanswer, .outcome .rightanswer')) { hasGroundTruth = true; break; } if (hasChoiceCheckmark(que) || hasChoiceCross(que)) { hasGroundTruth = true; break; } const gradeInfo = parseMoodleQuestionGrade(que); if (gradeInfo.earned !== null && gradeInfo.max !== null && (gradeInfo.isFullMark || gradeInfo.isZeroMark || gradeInfo.isPartialMark)) { hasGroundTruth = true; break; } } return { isReviewable: hasGroundTruth, reason: hasGroundTruth ? 'ground_truth_present' : 'feedback_hidden' }; } function harvestFromReviewDOM(rootDoc, subCode, quizTitle, courseTitle = '') { const queList = rootDoc.querySelectorAll('.que'); if (queList.length === 0) return { success: false, error: 'No questions found on review page', questions: [] }; // Reviewability Safety Guard: If feedback and marks are hidden, disable scraping! const reviewability = evaluateReviewPageReviewability(rootDoc); if (!reviewability.isReviewable) { return { success: false, unreviewable: true, reason: reviewability.reason, error: 'Moodle has hidden answer feedback and marks for this assessment. Answer scraping is safely paused.', questions: [] }; } let gradeText = ''; const gradeRow = rootDoc.querySelector('.quizreviewsummary tr:last-child, .quizreviewsummary .grade'); if (gradeRow) { gradeText = gradeRow.innerText.replace(/\s+/g, ' ').trim(); } const harvested = []; let correctCount = 0; let eliminatedTotal = 0; queList.forEach((que, idx) => { const qData = extractQuestionData(que); if (!qData || !qData.qText) return; const matchingRows = Array.from(que.querySelectorAll('.answer table tr, .answer tr')).filter(row => row.querySelector('select') && row.querySelector('td.text, td:first-child') ); const isMatchingQuestion = matchingRows.length > 0 || qData.questionType === 'match'; let rightAnswer = ''; let isVerified = false; const wrongAnswers = []; // 1. Check for explicit Moodle .rightanswer box (handles both singular and plural answers) const rightElem = que.querySelector('.rightanswer, .outcome .rightanswer'); if (rightElem && !isMatchingQuestion) { let raw = cleanDOMToAI(rightElem); raw = raw.replace(/^The correct answers? (is|are):?\s*['"]?/i, '').replace(/['"]?\s*$/i, '').trim(); if (raw) { rightAnswer = raw; isVerified = true; } } // 2. Extract choices from all input formats (Radio, Checkbox, Text input, Dropdown, Drag & Drop) const checkedInputs = que.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"]'); const checkedTexts = []; checkedInputs.forEach(inp => { const label = inp.closest('label') || inp.closest('div.r0, div.r1, [class*="r"]') || inp.parentElement; let text = cleanDOMToAI(label); text = text.replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (text && !checkedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) checkedTexts.push(text); }); if (checkedTexts.length === 0) { const selectedRows = que.querySelectorAll('.answer div.correct, .answer div.selected, .answer label.correct, .answer label.selected'); selectedRows.forEach(row => { let text = cleanDOMToAI(row).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (text && !checkedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) checkedTexts.push(text); }); } // Text inputs & textareas (e.g. cloze sentences, short answers) const textInputs = que.querySelectorAll('input[type="text"], textarea, input.form-control, input[type="number"], input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="button"]):not([type="reset"])'); const filledInputTexts = []; textInputs.forEach(inp => { const val = (inp.value || inp.getAttribute('value') || '').trim(); if (val) filledInputTexts.push(val); }); // Dropdowns (select elements in matching tables & gapselects) const selects = que.querySelectorAll('select'); const selectedDropdownTexts = []; selects.forEach(sel => { const opt = sel.options[sel.selectedIndex]; const optText = (opt && opt.value) ? (opt.text || opt.innerText).trim() : ''; if (optText && optText !== 'Choose...') { selectedDropdownTexts.push(optText); } }); // Drag and drop words into text (ddwtos, ddimageortext, ddmarker) const dropZones = que.querySelectorAll('.drop, .dropzone, span.droptarget'); const placedDropTexts = []; dropZones.forEach(dz => { const placedText = cleanDOMToAI(dz).replace(/^\[Blank\s*\d+[^\]]*\]/i, '').trim(); if (placedText && !placedText.startsWith('[Blank')) { placedDropTexts.push(placedText); } }); // Parse decimal and arbitrary score (1.00 out of 1.00, 3.40 out of 5.00, 2.10 out of 3.00, 0.00 out of 2.00) const gradeInfo = parseMoodleQuestionGrade(que); const isFullMark = gradeInfo.isFullMark; const isZeroMark = gradeInfo.isZeroMark; const isPartialMark = gradeInfo.isPartialMark; const matchingEvidence = []; if (isMatchingQuestion) { // Collect all matching option texts from the dropdowns const allMatchingOptions = new Set(); matchingRows.forEach(row => { const select = row.querySelector('select'); if (select) { Array.from(select.options || []).forEach(opt => { const optText = (opt.text || opt.innerText || '').trim(); if (optText && opt.value && !optText.toLowerCase().includes('choose')) { allMatchingOptions.add(optText); } }); } }); // First pass: identify per-row checkmarks, crosses, and full mark states const rowAnalyses = []; matchingRows.forEach((row, rIdx) => { const promptCell = row.querySelector('td.text, td:first-child'); const select = row.querySelector('select'); const selectedOption = select && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null; const selectedAnswer = selectedOption && selectedOption.value && !selectedOption.text.toLowerCase().includes('choose') ? String(selectedOption.text || selectedOption.innerText || '').trim() : ''; const prompt = promptCell ? cleanDOMToAI(promptCell).trim() : ''; if (!prompt) return; const rowIsWrong = hasChoiceCross(row) || row.classList.contains('incorrect') || Boolean(row.querySelector('.text-danger, .incorrect')); const rowIsCorrect = !rowIsWrong && ( hasChoiceCheckmark(row) || row.classList.contains('correct') || Boolean(row.querySelector('.text-success, .correct')) || isFullMark ); rowAnalyses.push({ row, index: idx + 1, rIdx, prompt, selectedAnswer, rowIsCorrect, rowIsWrong, wrongAnswers: rowIsWrong && selectedAnswer ? [selectedAnswer] : [], ansRaw: rowIsCorrect ? selectedAnswer : '', verified: rowIsCorrect, deduced: false }); }); // Second pass: Smart Elimination & Deduction // 1. Identify which choices are already verified as correct answers across the rows const verifiedChoiceNorms = new Set( rowAnalyses.filter(r => r.verified && r.ansRaw).map(r => normalizeChoice(r.ansRaw)) ); // 2. Identify remaining unassigned choices from allMatchingOptions let remainingChoices = Array.from(allMatchingOptions).filter( opt => !verifiedChoiceNorms.has(normalizeChoice(opt)) ); // 3. Iterative single-candidate deduction & 2-swap detection let deductionMade = true; while (deductionMade) { deductionMade = false; const unverifiedRows = rowAnalyses.filter(r => !r.verified); if (unverifiedRows.length === 0 || remainingChoices.length === 0) break; // Special case: 2-choice swap detection // If exactly 2 unverified rows remain, and remainingChoices has size 2, // and each row's wrong answer matches one of the remaining choices: if (unverifiedRows.length === 2 && remainingChoices.length === 2) { const r1 = unverifiedRows[0]; const r2 = unverifiedRows[1]; const r1WrongNorms = r1.wrongAnswers.map(w => normalizeChoice(w)); const r2WrongNorms = r2.wrongAnswers.map(w => normalizeChoice(w)); const c1 = remainingChoices[0]; const c2 = remainingChoices[1]; const c1Norm = normalizeChoice(c1); const c2Norm = normalizeChoice(c2); if (r1WrongNorms.includes(c1Norm) && r2WrongNorms.includes(c2Norm)) { r1.ansRaw = c2; r1.verified = true; r1.deduced = true; r2.ansRaw = c1; r2.verified = true; r2.deduced = true; remainingChoices = []; deductionMade = true; break; } else if (r1WrongNorms.includes(c2Norm) && r2WrongNorms.includes(c1Norm)) { r1.ansRaw = c1; r1.verified = true; r1.deduced = true; r2.ansRaw = c2; r2.verified = true; r2.deduced = true; remainingChoices = []; deductionMade = true; break; } } // General deduction: Any unverified row where exactly 1 choice from remainingChoices is not eliminated for (const uRow of unverifiedRows) { const uWrongNorms = new Set(uRow.wrongAnswers.map(w => normalizeChoice(w))); const possibleCandidates = remainingChoices.filter(c => !uWrongNorms.has(normalizeChoice(c))); if (possibleCandidates.length === 1) { const deducedChoice = possibleCandidates[0]; uRow.ansRaw = deducedChoice; uRow.verified = true; uRow.deduced = true; remainingChoices = remainingChoices.filter(c => normalizeChoice(c) !== normalizeChoice(deducedChoice)); deductionMade = true; break; } } } // Push processed rowAnalyses into matchingEvidence rowAnalyses.forEach(analysis => { if (!analysis.verified && analysis.wrongAnswers.length === 0) return; const isRowWrong = analysis.rowIsWrong && !analysis.verified; matchingEvidence.push({ index: analysis.index, qRaw: analysis.prompt, qNorm: normalizeText(analysis.prompt), ansRaw: analysis.verified ? analysis.ansRaw : '', ansNorm: analysis.verified ? normalizeChoice(analysis.ansRaw) : '', questionType: 'shortanswer', wrongAnswers: normalizeWrongAnswers(analysis.wrongAnswers), choices: Array.from(allMatchingOptions), verified: analysis.verified, deduced: analysis.deduced, evidenceType: analysis.deduced ? 'elimination_deduction' : (analysis.rowIsWrong ? 'moodle_review_elimination' : 'moodle_review'), // evidenceType: rowIsWrong ? 'moodle_review_elimination' : 'moodle_review' wrongAnswerEvidence: analysis.wrongAnswers.length > 0, period: detectTermFromText(quizTitle) || 'General', quizTitle, originType: (typeof detectAssessmentOrigin === 'function') ? detectAssessmentOrigin().originType : 'quiz', originLabel: (typeof detectAssessmentOrigin === 'function') ? detectAssessmentOrigin().originLabel : (quizTitle || 'Quiz') }); }); matchingEvidence.forEach(item => { if (item.ansRaw) correctCount++; eliminatedTotal += item.wrongAnswers.length; harvested.push(item); }); } // Direct per-choice checkmark and cross detection (supports nested fieldsets and all Moodle themes) const choiceRows = que.querySelectorAll('.answer div.r0, .answer div.r1, .answer div[class*="r"], .answer fieldset > div, .answer li, .answer tr, .answer label, .answer > div'); const checkmarkedTexts = []; const crossedTexts = []; choiceRows.forEach(row => { if (isMatchingQuestion) return; const label = row.querySelector('label') || row; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (!text) return; // Feature 4: Empty choice sanitizer — skip choices with insufficient text after prefix strip if (text.length < 2) return; if (hasChoiceCheckmark(row) || hasChoiceCheckmark(label)) { if (!checkmarkedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) { checkmarkedTexts.push(text); } } else if (hasChoiceCross(row) || hasChoiceCross(label)) { if (!crossedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) { crossedTexts.push(text); } } }); // If checkmarked choices exist (even on partial scores or multi-answer), harvest them as verified! if (checkmarkedTexts.length > 0) { rightAnswer = checkmarkedTexts.join(', '); isVerified = true; } // If user got full mark, the selected/entered choice(s) are verified correct! // Strict atomic question guard: Never harvest composite dropdowns, matching rows, or multi-input blanks as joined strings for the parent question. if (!rightAnswer && isFullMark) { if (checkedTexts.length > 0) { rightAnswer = checkedTexts.length === 1 ? checkedTexts[0] : checkedTexts.join(', '); isVerified = true; } else if (filledInputTexts.length === 1) { rightAnswer = filledInputTexts[0]; isVerified = true; } else if (selectedDropdownTexts.length === 1 && !isMatchingQuestion) { rightAnswer = selectedDropdownTexts[0]; isVerified = true; } else if (placedDropTexts.length === 1) { rightAnswer = placedDropTexts[0]; isVerified = true; } } // Confirmed wrong choices: // 1. Explicit red cross choices are always confirmed wrong crossedTexts.forEach(txt => { const norm = normalizeChoice(txt); if (norm && !wrongAnswers.some(w => normalizeChoice(w) === norm)) { wrongAnswers.push(txt); } }); // 2. If question was marked INCORRECT (0 marks): the checked/entered choice(s) are confirmed WRONG! if (isZeroMark && !isMatchingQuestion) { const submittedAnswers = [...checkedTexts, ...filledInputTexts, ...selectedDropdownTexts, ...placedDropTexts]; submittedAnswers.forEach(txt => { const norm = normalizeChoice(txt); if (norm && !wrongAnswers.some(w => normalizeChoice(w) === norm)) { wrongAnswers.push(txt); } }); // Auto-detect and report anomaly if student failed despite matching a verified candidate or if answer oscillates if (typeof detectAndReportQuestionAnomaly === 'function' && submittedAnswers.length > 0) { try { const cachedDb = typeof getCachedAnswers === 'function' ? getCachedAnswers(subCode) : []; const qNorm = normalizeText(qData.qText); const candidate = (cachedDb || []).find(c => { const cText = c.qText || c.question || c.qRaw || ''; if (typeof questionTextMatches === 'function') { return questionTextMatches(qData.qText, cText); } const cNorm = normalizeText(cText); return cNorm === qNorm || (cNorm.length > 25 && (cNorm.includes(qNorm) || qNorm.includes(cNorm))); }); if (candidate) { const candAns = candidate.answer || candidate.ansRaw || candidate.ansNorm || ''; const candNorm = candidate.ansNorm || normalizeChoice(candAns); const candVerified = Boolean(candidate.verified); const matchesSubmitted = submittedAnswers.some(s => { const sNorm = normalizeChoice(s); return sNorm === candNorm || unscriptDigits(sNorm) === unscriptDigits(candNorm); }); if (candVerified && matchesSubmitted) { detectAndReportQuestionAnomaly({ anomalyType: 'VERIFIED_ANSWER_FAILED', subjectCode: subCode, activityTitle: quizTitle, questionRaw: qData.qText, questionNorm: qNorm, domType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : 'unknown'), submittedAnswer: submittedAnswers.join(', '), markScored: gradeInfo.earned ?? 0, maxMark: gradeInfo.max ?? 1, attemptHistory: [...(candidate.wrongAnswers || []), ...wrongAnswers], dbCandidate: { answer: candAns, questionType: candidate.questionType, verified: candidate.verified, wrongAnswers: candidate.wrongAnswers || [] } }); } else if (Array.isArray(candidate.wrongAnswers) && candidate.wrongAnswers.length > 0) { detectAndReportQuestionAnomaly({ anomalyType: 'ANSWER_OSCILLATION', subjectCode: subCode, activityTitle: quizTitle, questionRaw: qData.qText, questionNorm: qNorm, domType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : 'unknown'), submittedAnswer: submittedAnswers.join(', '), markScored: gradeInfo.earned ?? 0, maxMark: gradeInfo.max ?? 1, attemptHistory: [...(candidate.wrongAnswers || []), ...wrongAnswers], dbCandidate: { answer: candAns, questionType: candidate.questionType, verified: candidate.verified, wrongAnswers: candidate.wrongAnswers || [] } }); } else if (matchesSubmitted) { detectAndReportQuestionAnomaly({ anomalyType: 'UNVERIFIED_CHOICE_FAILED', subjectCode: subCode, activityTitle: quizTitle, questionRaw: qData.qText, questionNorm: qNorm, domType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : 'unknown'), submittedAnswer: submittedAnswers.join(', '), markScored: gradeInfo.earned ?? 0, maxMark: gradeInfo.max ?? 1, attemptHistory: [...(candidate.wrongAnswers || []), ...wrongAnswers], dbCandidate: { answer: candAns, questionType: candidate.questionType, verified: candidate.verified, wrongAnswers: candidate.wrongAnswers || [] } }); } } else if (submittedAnswers.length > 0) { detectAndReportQuestionAnomaly({ anomalyType: 'UNINDEXED_QUESTION_FAILED', subjectCode: subCode, activityTitle: quizTitle, questionRaw: qData.qText, questionNorm: qNorm, domType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : 'unknown'), submittedAnswer: submittedAnswers.join(', '), markScored: gradeInfo.earned ?? 0, maxMark: gradeInfo.max ?? 1, attemptHistory: wrongAnswers, dbCandidate: null }); } } catch (err) { if (typeof logDebug === 'function') logDebug('Review anomaly detection note:', err.message); } } } // Also check Moodle's per-choice incorrect indicators on non-standard containers const incorrectChoiceElems = que.querySelectorAll('.answer div.incorrect, .answer tr.incorrect, .answer li.incorrect, .answer .fa-remove, .answer .fa-times, .drop.incorrect'); incorrectChoiceElems.forEach(el => { if (isMatchingQuestion) return; const row = el.closest('div.r0, div.r1, tr, li') || el; const label = row.querySelector('label') || row; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); const norm = normalizeChoice(text); if (norm && !wrongAnswers.some(w => normalizeChoice(w) === norm)) { wrongAnswers.push(text); } }); // Safety guard: Any verified rightAnswer or checkmarked choice can NEVER be in wrongAnswers! const verifiedSet = new Set(checkmarkedTexts.map(c => normalizeChoice(c))); if (rightAnswer) { verifiedSet.add(normalizeChoice(rightAnswer)); rightAnswer.split(/[,;&\n]+|\s+and\s+/i).forEach(s => verifiedSet.add(normalizeChoice(s))); } for (let i = wrongAnswers.length - 1; i >= 0; i--) { const wNorm = normalizeChoice(wrongAnswers[i]); if (verifiedSet.has(wNorm) || verifiedSet.has(unscriptDigits(wNorm))) { wrongAnswers.splice(i, 1); } } // 3. Real-time Deduction by Elimination on Review screen: // If answer is not yet known, but choices are available (e.g. True/False where 1 is wrong, or 4-choice where 3 are wrong) let isDeduced = false; if (!rightAnswer && wrongAnswers.length > 0 && Array.isArray(qData.choices) && qData.choices.length > 1) { const uneliminated = qData.choices.filter(c => { const normC = normalizeChoice(c); return !wrongAnswers.some(w => normalizeChoice(w) === normC); }); if (uneliminated.length === 1) { rightAnswer = uneliminated[0]; isVerified = true; isDeduced = true; } } if (!isMatchingQuestion && (rightAnswer || wrongAnswers.length > 0)) { if (rightAnswer) correctCount++; eliminatedTotal += wrongAnswers.length; const detectedPeriod = detectTermFromText(quizTitle) || 'General'; const rightAnswersList = rightAnswer ? rightAnswer.split(/[,;&\n]+|\s+and\s+/i).map(s => s.trim()).filter(Boolean) : []; harvested.push({ index: idx + 1, qRaw: qData.qText, qNorm: normalizeText(qData.qText), ansRaw: rightAnswer, ansNorm: normalizeChoice(rightAnswer), answers: rightAnswersList.length > 1 ? rightAnswersList : undefined, isMultiChoice: Boolean(qData.isMultiChoice), questionType: qData.questionType || (typeof identifyQuestionType === 'function' ? identifyQuestionType(que) : (Array.isArray(qData.choices) && qData.choices.length > 0 ? 'multichoice' : 'shortanswer')), wrongAnswers: normalizeWrongAnswers(wrongAnswers), choices: qData.choices, verified: isVerified, deduced: isDeduced, evidenceType: isVerified ? 'moodle_review' : (wrongAnswers.length > 0 ? 'moodle_review_elimination' : 'community_report'), wrongAnswerEvidence: wrongAnswers.length > 0, period: detectedPeriod, quizTitle: quizTitle, originType: (typeof detectAssessmentOrigin === 'function') ? detectAssessmentOrigin().originType : 'quiz', originLabel: (typeof detectAssessmentOrigin === 'function') ? detectAssessmentOrigin().originLabel : (quizTitle || 'Quiz') }); } }); return { success: true, course: courseTitle || subCode, subjectCode: subCode, quizTitle, gradeText, totalQuestions: queList.length, harvestedCount: correctCount, eliminatedCount: eliminatedTotal, questions: harvested }; } function harvestReviewAnswers() { const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; let quizTitle = courseInfo.currentActivityTitle || 'Quiz Review'; return harvestFromReviewDOM(document, subCode, quizTitle, courseInfo.fullTitle || subCode); } function exportAnswersAsJSON(data) { const payload = { tool: "AMAES Moodle Toolkit", version: SCRIPT_VERSION, exportedAt: new Date().toISOString(), subjectCode: data.subjectCode, quizTitle: data.quizTitle, grade: data.gradeText, totalQuestions: data.totalQuestions, answerCount: data.harvestedCount, eliminatedCount: data.eliminatedCount || 0, questions: data.questions.map(q => ({ question: q.qRaw, answer: q.ansRaw, choices: q.choices, wrongAnswers: Array.isArray(q.wrongAnswers) ? q.wrongAnswers.map(w => typeof w === 'string' ? w : w.text) : [] })) }; return JSON.stringify(payload, null, 2); } // Batched & Debounced Community Contribution Queue const pendingContributionBatches = {}; const pendingContributionTimers = {}; const pendingReviewShares = new Set(); function queueCommunityContribution(subCode, questions, options = {}) { if (!questions || questions.length === 0) return; const validQuestions = questions.filter(q => { if (!q) return false; const hasAns = Boolean(q.ansRaw || q.answer || q.correctAnswer); const hasElim = q.evidenceType === 'moodle_review_elimination' && Array.isArray(q.wrongAnswers) && q.wrongAnswers.length > 0; if (!hasAns && !hasElim) return false; // Guard: Never contribute composite comma-separated answers or unmapped match parent questions const ansText = String(q.ansRaw || q.answer || q.correctAnswer || '').trim(); if (q.questionType === 'match' && ansText.includes(',')) return false; return true; }); if (validQuestions.length === 0) return; if (!pendingContributionBatches[subCode]) { pendingContributionBatches[subCode] = new Map(); } validQuestions.forEach(q => { const key = (typeof getQuestionIdentity === 'function' ? getQuestionIdentity(q) : null) || normalizeText(q.qRaw || q.question || ""); if (key) { pendingContributionBatches[subCode].set(key, q); } }); if (pendingContributionTimers[subCode]) { clearTimeout(pendingContributionTimers[subCode]); } // Ultra-fast 2-second debounce: catches simultaneous page discoveries and shares immediately pendingContributionTimers[subCode] = setTimeout(() => { flushCommunityContributions(subCode, options) .catch(error => logDebug(`Community contribution queue note: ${error.message}`)); }, 2000); } async function flushCommunityContributions(subCode, options = {}) { if (pendingContributionTimers[subCode]) { clearTimeout(pendingContributionTimers[subCode]); delete pendingContributionTimers[subCode]; } const batchMap = pendingContributionBatches[subCode]; if (!batchMap || batchMap.size === 0) return; const questionsToFlush = Array.from(batchMap.values()); delete pendingContributionBatches[subCode]; return dispatchCommunityContribution(subCode, questionsToFlush, options); } // Dispatch Community Contribution silently in background async function dispatchCommunityContribution(subCode, questions, options = {}) { if (!questions || questions.length === 0) return; const validQuestions = questions.filter(q => { const raw = (q.ansRaw || q.answer || q.correctAnswer || '').trim(); const hasReviewElimination = q.evidenceType === 'moodle_review_elimination' && Array.isArray(q.wrongAnswers) && q.wrongAnswers.length > 0; if (!raw) return hasReviewElimination; // Proven Wrong Guard: If answer was confirmed wrong/eliminated, NEVER share it! const norm = normalizeChoice(raw); const wrongList = Array.isArray(q.wrongAnswers) ? q.wrongAnswers : []; const isProvenWrong = wrongList.some(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); return wNorm === norm || unscriptDigits(wNorm) === unscriptDigits(norm); }); return !isProvenWrong; }); if (validQuestions.length === 0) return; const contributionId = options.contributionId || `contribution-${Date.now()}-${Math.random().toString(36).slice(2)}`; const evidenceType = options.evidenceType || (options.source === 'review_screen' ? 'moodle_review' : 'community_report'); const detectedCourse = (typeof detectCourseInfo === 'function') ? detectCourseInfo() : null; const resolvedName = options.subjectName || (detectedCourse && (detectedCourse.subjectCode === subCode || detectedCourse.code === subCode) && detectedCourse.subjectName) || resolveKnownCourseName(subCode) || subCode; const payload = { subjectCode: subCode, subjectName: resolvedName, clientVersion: SCRIPT_VERSION.replace(/^v/i, ''), databaseSchema: ANSWER_DB_SCHEMA_VERSION, totalQuestions: validQuestions.length, source: options.source || "auto_harvester", evidenceType, contributionId, contributorId: getAnonymousContributorId(), submittedAt: new Date().toISOString(), questions: validQuestions.map(q => ({ question: q.qRaw || q.question || "", answer: q.ansRaw || q.answer || "", choices: q.choices || [], wrongAnswers: Array.isArray(q.wrongAnswers) ? q.wrongAnswers.map(w => typeof w === 'string' ? w : w.text) : [], wrongAnswerEvidence: Boolean(q.wrongAnswerEvidence || q.evidenceType === 'moodle_review_elimination'), verified: Boolean(q.verified), isAiSuggestion: Boolean(q.isAiSuggestion || (q.source && String(q.source).toLowerCase().includes('gemini'))), source: q.source || options.source || "auto_harvester", evidenceType: q.evidenceType || (q.isAiSuggestion || (q.source && String(q.source).toLowerCase().includes('gemini')) ? 'ai_inference' : evidenceType), originType: q.originType || (typeof detectAssessmentOrigin === 'function' ? detectAssessmentOrigin().originType : 'quiz'), originLabel: q.originLabel || q.quizTitle || (typeof detectAssessmentOrigin === 'function' ? detectAssessmentOrigin().originLabel : 'Quiz') })) }; logDebug(`Dispatching ${validQuestions.length} answer/evidence records to community relay...`); if (communityRelayUrl) { const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { return new Promise((resolve, reject) => { gmReq({ method: 'POST', url: communityRelayUrl, timeout: 15_000, headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, data: JSON.stringify(payload), onload: (res) => { if (res.status >= 200 && res.status < 300) { showToast(`Auto-shared ${validQuestions.length} answer/evidence records to Global Database!`); setLog(`Shared ${validQuestions.length} evidence records to Global Database via relay.`, "var(--accent-green)"); resolve({ success: true, mode: 'relay_gm', count: validQuestions.length }); } else { reject(new Error(`Community relay rejected the submission (${res.status}).`)); } }, onerror: (err) => { reject(new Error(`Community relay request failed: ${err && err.error ? err.error : 'network error'}.`)); }, ontimeout: () => { reject(new Error('Community relay request timed out.')); } }); }); } else { const resp = await fetch(communityRelayUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-AMAES-Client-Version': SCRIPT_VERSION.replace(/^v/i, '') }, body: JSON.stringify(payload) }); if (resp.status === 426) { const update = await resp.json().catch(() => ({})); showToast(`Update AMAES Toolkit to v${update.minimumVersion || 'the latest version'} to share answers.`, 7000); setLog(`Toolkit update required before community sharing. Install update.`, "var(--accent-amber)"); throw new Error('Update required before community sharing.'); } if (!resp.ok) { const result = await resp.json().catch(() => ({})); throw new Error(result.error || `Community relay rejected the submission (${resp.status}).`); } showToast(`Auto-shared ${validQuestions.length} answer/evidence records to Global Database!`); setLog(`Shared ${validQuestions.length} evidence records to Global Database via relay.`, "var(--accent-green)"); return { success: true, mode: 'relay', count: validQuestions.length }; } } throw new Error('Community relay is not configured; the review remains saved only in local storage.'); } // Background Grades Report Answer Harvester let isHarvestingInProgress = false; async function harvestQuizzesFromGradesDoc(gradesDoc, targetSubCode, courseTitle, statusCallback) { let subCode = targetSubCode; const table = gradesDoc.querySelector('.user-grade, table[summary="User report"], .generaltable'); if (!table) { return { success: false, error: 'No grade table found', count: 0 }; } if (!subCode || subCode === 'DEFAULT' || subCode === 'GENERAL') { const catHeaders = table.querySelectorAll('th.category, tr.category, h2, h3, .cat_1'); for (const h of catHeaders) { const m = (h.innerText || '').match(/(?:UGRD-|UGRD_)?([A-Z]{2,6}\d{3,4}[A-Z]?)/i); if (m) { subCode = m[1].toUpperCase(); break; } } } if (!subCode || subCode === 'DEFAULT') { subCode = 'GENERAL'; } // Scan all candidate rows across document or tables const candidateRows = Array.from(gradesDoc.querySelectorAll('.user-grade tr, .generaltable tr, table.table tr, tr')); const completedQuizzes = []; const seenUrls = new Set(); candidateRows.forEach(r => { const quizLink = r.querySelector('a[href*="/mod/quiz/"], a[href*="quiz"], a.gradeitemheader') || r.querySelector('.column-itemname a, th a, td:first-child a'); if (!quizLink) return; const href = quizLink.getAttribute('href') || quizLink.href || ''; const rawTitle = quizLink.innerText.trim(); if (!rawTitle || (!href.includes('quiz') && !r.innerText.toLowerCase().includes('quiz'))) return; // Prevent empty or unattempted rows const rowText = r.innerText || ''; if (rowText.includes('( Empty )') || rowText.includes('(Empty)') || rowText.includes('( empty )')) return; let fullQuizUrl = ''; try { fullQuizUrl = new URL(href, window.location.origin).href; } catch (e) { fullQuizUrl = href; } if (!fullQuizUrl || seenUrls.has(fullQuizUrl)) return; // Determine if the quiz has a completed grade const gradeCell = r.querySelector('.column-grade, [headers*="grade"], td.grade'); const pctCell = r.querySelector('.column-percentage, [headers*="percentage"]'); let hasGrade = false; let gradeStr = ''; if (gradeCell) { const gText = gradeCell.innerText.trim(); if (gText && gText !== '-' && gText !== '–' && /\d/.test(gText)) { hasGrade = true; gradeStr = gText; } } if (!hasGrade && pctCell) { const pText = pctCell.innerText.trim(); if (pText && pText !== '-' && pText !== '–' && !pText.includes('0.00') && /\d/.test(pText)) { hasGrade = true; gradeStr = pText; } } if (!hasGrade) { const cells = Array.from(r.querySelectorAll('td, th')); for (const c of cells) { if (c.contains(quizLink)) continue; const txt = c.innerText.trim(); if (txt && txt !== '-' && txt !== '–' && !txt.includes('( Empty )') && !txt.includes('0.00 %') && !txt.startsWith('0-') && !txt.startsWith('0–') && /\b\d+(\.\d+)?\b/.test(txt)) { hasGrade = true; gradeStr = txt; break; } } } if (hasGrade) { seenUrls.add(fullQuizUrl); const cleanTitle = rawTitle.replace(/^QUIZ\s+/i, '').replace(/\s+/g, ' ').trim(); completedQuizzes.push({ title: cleanTitle || rawTitle, url: fullQuizUrl, grade: gradeStr }); } }); if (completedQuizzes.length === 0) { return { success: false, count: 0, quizzes: 0, subCode }; } let totalHarvested = 0; let allQuestions = []; for (let i = 0; i < completedQuizzes.length; i++) { const qz = completedQuizzes[i]; if (statusCallback) statusCallback(i + 1, completedQuizzes.length, qz.title); setLog(`[${i + 1}/${completedQuizzes.length}] Opening ${qz.title} (${subCode} Score: ${qz.grade})...`, "var(--accent-cyan)", `Harvesting verified answer key`); try { const viewResp = await fetch(qz.url); if (!viewResp.ok) continue; const viewHtml = await viewResp.text(); const viewDoc = new DOMParser().parseFromString(viewHtml, 'text/html'); const reviewLinks = Array.from(viewDoc.querySelectorAll( 'a[href*="review.php"], a[href*="/mod/quiz/review.php"], a[href*="/mod/quiz/attempt.php"][href*="review"]' )); // Some Moodle themes put the review URL directly in the grade row. if (reviewLinks.length === 0 && /\/mod\/quiz\/review\.php/i.test(qz.url)) { reviewLinks.push({ getAttribute: () => qz.url, href: qz.url }); } if (reviewLinks.length === 0) continue; const seenReviewUrls = new Set(); const reviewQueue = reviewLinks.map(rLink => { const raw = rLink.getAttribute('href') || rLink.href; return raw ? new URL(raw, qz.url).href : ''; }).filter(Boolean); // Some Moodle themes ignore showall=1, so keep a queue for // pagination links discovered in each fetched review page. for (let queueIndex = 0; queueIndex < reviewQueue.length; queueIndex++) { const queuedUrl = reviewQueue[queueIndex]; const rLink = { getAttribute: () => queuedUrl, href: queuedUrl }; const rawHref = rLink.getAttribute('href') || rLink.href; if (!rawHref) continue; let reviewUrl = ''; try { reviewUrl = new URL(rawHref, qz.url).href; } catch (e) { reviewUrl = rawHref; } const reviewUrlObj = new URL(reviewUrl, qz.url); reviewUrlObj.searchParams.set('showall', '1'); reviewUrl = reviewUrlObj.href; if (seenReviewUrls.has(reviewUrl)) continue; seenReviewUrls.add(reviewUrl); const reviewResp = await fetch(reviewUrl); if (!reviewResp.ok) continue; const reviewHtml = await reviewResp.text(); const reviewDoc = new DOMParser().parseFromString(reviewHtml, 'text/html'); const res = harvestFromReviewDOM(reviewDoc, subCode, qz.title, courseTitle || subCode); if (res.success && res.questions.length > 0) { const knownQuestionKeys = new Set(allQuestions.map(getQuestionIdentity)); res.questions.forEach(question => { if (!question.qNorm) return; const questionKey = getQuestionIdentity(question); if (knownQuestionKeys.has(questionKey)) return; knownQuestionKeys.add(questionKey); allQuestions.push(question); totalHarvested += question.ansRaw ? 1 : 0; }); } // If showall is ignored, enqueue every review page link // found in the fetched document. reviewDoc.querySelectorAll('a[href*="review.php"], a[href*="/mod/quiz/review.php"]').forEach(pageLink => { const href = pageLink.getAttribute('href') || pageLink.href; if (!href) return; const pageUrl = new URL(href, qz.url); pageUrl.searchParams.delete('showall'); const pageHref = pageUrl.href; if (!reviewQueue.includes(pageHref)) reviewQueue.push(pageHref); }); } } catch (err) { console.warn(`Error harvesting ${qz.title}:`, err); } } if (allQuestions.length > 0) { mergeAnswersIntoCache(subCode, allQuestions, 'Grades-Harvester'); flushCommunityContributions(subCode, { source: 'grades_harvester' }).catch(() => {}); return { success: true, count: totalHarvested, quizzes: completedQuizzes.length, subCode }; } return { success: false, count: 0, quizzes: completedQuizzes.length, subCode }; } async function batchHarvestCourseReviews(targetCourseId, targetSubCode, statusCallback, isBackground = true) { if (isHarvestingInProgress) { if (!isBackground) showToast("Answer collection is already running in background...", 2500); return { success: false, inProgress: true }; } isHarvestingInProgress = true; try { const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const subCode = targetSubCode || courseInfo.subjectCode || 'GENERAL'; const courseId = targetCourseId || courseInfo.courseId || new URLSearchParams(window.location.search).get('id') || ''; setLog(`Auto-Harvest: Background review scanner inspecting ${escapeHtml(subCode)}...`, "var(--accent-blue)"); if (!isBackground) showToast(`Scanning reviews for ${subCode}...`, 2500); // 1. Gather all quiz links from course page DOM and/or grade report const quizLinksMap = new Map(); document.querySelectorAll('a[href*="/mod/quiz/view.php"], a[href*="/mod/quiz/"]').forEach(a => { const href = a.getAttribute('href') || a.href; if (!href || href.includes('attempt.php')) return; try { const fullUrl = new URL(href, window.location.origin).href; const title = (a.innerText || '').trim() || 'Quiz'; if (!quizLinksMap.has(fullUrl)) quizLinksMap.set(fullUrl, title); } catch (_) {} }); // Fallback/enrichment: also scan Grade Report if available let singleGradesUrl = null; const gradesLink = document.querySelector('a[href*="/grade/report/user/index.php"]'); if (gradesLink && gradesLink.href) { singleGradesUrl = gradesLink.href; } else if (courseId) { singleGradesUrl = `${window.location.origin}${getSemesterBasePath()}grade/report/user/index.php?id=${courseId}`; } if (singleGradesUrl) { try { const gResp = await fetch(singleGradesUrl); if (gResp.ok) { const gHtml = await gResp.text(); const gDoc = new DOMParser().parseFromString(gHtml, 'text/html'); gDoc.querySelectorAll('a[href*="/mod/quiz/view.php"], a[href*="/mod/quiz/"]').forEach(a => { const href = a.getAttribute('href') || a.href; if (!href || href.includes('attempt.php')) return; try { const fullUrl = new URL(href, window.location.origin).href; const title = (a.innerText || '').trim() || 'Quiz'; if (!quizLinksMap.has(fullUrl)) quizLinksMap.set(fullUrl, title); } catch (_) {} }); } } catch (_) {} } const quizList = Array.from(quizLinksMap.entries()).map(([url, title]) => ({ url, title })); if (quizList.length === 0) { if (!isBackground) showToast(`No quiz links found for ${subCode}.`); setLog(`Auto-Harvest: No quiz links found for ${escapeHtml(subCode)}.`, "var(--accent-amber)"); return { success: false, count: 0, quizzes: 0 }; } // 2. Discover review links for each quiz const reviewUrlsToHarvest = []; const seenReviewUrls = new Set(); for (let i = 0; i < quizList.length; i++) { const qz = quizList[i]; if (statusCallback) statusCallback(i + 1, quizList.length, `Checking ${qz.title}`); try { const qResp = await fetch(qz.url); if (!qResp.ok) continue; const qHtml = await qResp.text(); const qDoc = new DOMParser().parseFromString(qHtml, 'text/html'); qDoc.querySelectorAll('a[href*="review.php"], a[href*="/mod/quiz/review.php"]').forEach(rLink => { const raw = rLink.getAttribute('href') || rLink.href; if (!raw) return; try { const rUrl = new URL(raw, qz.url); rUrl.searchParams.set('showall', '1'); const finalUrl = rUrl.href; if (!seenReviewUrls.has(finalUrl)) { seenReviewUrls.add(finalUrl); reviewUrlsToHarvest.push({ url: finalUrl, title: qz.title }); } } catch (_) {} }); } catch (err) { console.warn(`Error inspecting quiz landing ${qz.url}:`, err); } } if (reviewUrlsToHarvest.length === 0) { if (!isBackground) showToast("No completed review attempts available for this course."); setLog(`Auto-Harvest: No completed review attempts available for ${escapeHtml(subCode)}.`, "var(--accent-amber)"); return { success: false, count: 0, reviews: 0 }; } setLog(`Auto-Harvest: Found ${reviewUrlsToHarvest.length} review attempt(s). Extracting verified answers in background...`, "var(--accent-cyan)"); if (!isBackground) showToast(`Harvesting ${reviewUrlsToHarvest.length} review attempt(s)...`, 3000); // 3. Fetch each review page and extract answers let totalHarvested = 0; const allQuestions = []; const knownQuestionKeys = new Set(); for (let rIdx = 0; rIdx < reviewUrlsToHarvest.length; rIdx++) { const rev = reviewUrlsToHarvest[rIdx]; if (statusCallback) statusCallback(rIdx + 1, reviewUrlsToHarvest.length, `Harvesting: ${rev.title}`); setLog(`[${rIdx + 1}/${reviewUrlsToHarvest.length}] Harvesting ${escapeHtml(rev.title)}...`, "var(--accent-cyan)"); try { const revResp = await fetch(rev.url); if (!revResp.ok) continue; const revHtml = await revResp.text(); const revDoc = new DOMParser().parseFromString(revHtml, 'text/html'); const res = harvestFromReviewDOM(revDoc, subCode, rev.title, courseInfo.fullTitle || subCode); if (res.success && res.questions && res.questions.length > 0) { res.questions.forEach(q => { if (!q.qNorm) return; const qKey = getQuestionIdentity(q); if (knownQuestionKeys.has(qKey)) return; knownQuestionKeys.add(qKey); allQuestions.push(q); totalHarvested += q.ansRaw ? 1 : 0; }); } } catch (err) { console.warn(`Error harvesting review ${rev.url}:`, err); } } if (allQuestions.length > 0) { mergeAnswersIntoCache(subCode, allQuestions, 'Batch-Course-Harvester'); flushCommunityContributions(subCode, { source: 'batch_course_harvester' }).catch(() => {}); setLog(`Auto-Harvest Complete: Saved ${totalHarvested} verified answers from ${reviewUrlsToHarvest.length} review(s) into ${escapeHtml(subCode)}.`, "var(--accent-green)"); showToast(`Auto-Harvest: Saved ${totalHarvested} verified answers into ${subCode} database!`, 4000); if (typeof injectCourseConfidenceCard === 'function') { injectCourseConfidenceCard(); } if (typeof syncAutoQuizUI === 'function') { syncAutoQuizUI(); } return { success: true, count: totalHarvested, reviews: reviewUrlsToHarvest.length, subCode }; } else { setLog(`Auto-Harvest finished. No new verified answers found in ${reviewUrlsToHarvest.length} review(s).`, "var(--accent-amber)"); if (!isBackground) showToast("All reviews checked, no new answers to harvest."); return { success: false, count: 0, reviews: reviewUrlsToHarvest.length, subCode }; } } catch (e) { setLog(`Auto-Harvest error: ${e.message}`, "var(--accent-pink)"); if (!isBackground) showToast("Batch harvest failed: " + e.message); return { success: false, error: e.message }; } finally { isHarvestingInProgress = false; } } async function executeGradesHarvester(statusCallback) { if (isHarvestingInProgress) { showToast("Answer collection is already running in background...", 2500); return { success: false, inProgress: true }; } isHarvestingInProgress = true; try { const isDirectGradesPage = window.location.pathname.includes('/grade/report/user/index.php'); const isDashboard = window.location.pathname.includes('/my/') || window.location.pathname.includes('courses.php'); const courseInfo = detectCourseInfo(); // Scenario 1: Directly on a Grade Report page if (isDirectGradesPage) { setLog("Scanning current Grade Report for completed quizzes...", "var(--accent-blue)", "Analyzing grade items"); const res = await harvestQuizzesFromGradesDoc(document, courseInfo.subjectCode, courseInfo.fullTitle, statusCallback); if (res.success && res.count > 0) { setLog(`Collection Complete! Loaded ${res.count} verified answers into ${res.subCode} DB.`, "var(--accent-green)", "Saved to local database"); showToast(`Collected ${res.count} verified answers! Saved to database.`, 4000); syncAutoQuizUI(); return res; } else { setLog("Grade report scan finished. No accessible reviews found or already cached.", "var(--accent-amber)", "Reviews may be restricted by instructor"); showToast("No new answers collected from this grade report."); return res; } } // Scenario 2: Inside a specific course or quiz page (courseId is known) let singleGradesUrl = null; const gradesLink = document.querySelector('a[href*="/grade/report/user/index.php"]'); if (gradesLink && gradesLink.href) { singleGradesUrl = gradesLink.href; } else if (courseInfo.courseId) { const semPath = getSemesterBasePath(); singleGradesUrl = `${window.location.origin}${semPath}grade/report/user/index.php?id=${courseInfo.courseId}`; } if (singleGradesUrl && !isDashboard) { setLog(`Fetching course Grade Report for ${courseInfo.subjectCode || 'course'}...`, "var(--accent-blue)", "Accessing grade history"); showToast("Fetching course Grade Report in background...", 2500); try { const resp = await fetch(singleGradesUrl); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const html = await resp.text(); const gradesDoc = new DOMParser().parseFromString(html, 'text/html'); const res = await harvestQuizzesFromGradesDoc(gradesDoc, courseInfo.subjectCode, courseInfo.fullTitle, statusCallback); if (res.success && res.count > 0) { setLog(`Collection Complete! Loaded ${res.count} verified answers into ${res.subCode} DB.`, "var(--accent-green)", "Saved to local database"); showToast(`Collected ${res.count} verified answers! Saved to database.`, 4000); syncAutoQuizUI(); return res; } else { setLog("Grade report scan complete. No new accessible reviews found.", "var(--accent-amber)", "Quizzes may not have review attempts available"); showToast("No accessible quiz reviews found to collect for this course."); return res; } } catch (e) { showToast("Could not fetch Grade Report: " + e.message); setLog(`Grade Report fetch error: ${e.message}`, "var(--accent-pink)", "Check course permissions"); return { success: false, error: e.message }; } } // Scenario 3: On Dashboard or Multi-Course View const dashCourses = detectDashboardCourses().filter(c => c.courseId || c.gradesUrl); if (dashCourses.length > 0) { setLog(`Scanning Grade Reports for ${dashCourses.length} enrolled courses...`, "var(--accent-blue)", "Batch collecting past semester quizzes"); showToast(`Scanning grade reports for ${dashCourses.length} enrolled courses...`, 3000); let totalHarvested = 0; let totalQuizzes = 0; for (let i = 0; i < dashCourses.length; i++) { const c = dashCourses[i]; const gradesUrl = c.gradesUrl || `${window.location.origin}${getSemesterBasePath()}grade/report/user/index.php?id=${c.courseId}`; if (statusCallback) statusCallback(i + 1, dashCourses.length, `Course: ${c.code}`); setLog(`[${i + 1}/${dashCourses.length}] Scanning ${c.code} Grade Report...`, "var(--accent-cyan)", `Checking completed quizzes for ${c.code}`); try { const resp = await fetch(gradesUrl); if (!resp.ok) continue; const html = await resp.text(); const gDoc = new DOMParser().parseFromString(html, 'text/html'); const res = await harvestQuizzesFromGradesDoc(gDoc, c.code, c.title, (curQ, totQ, qTitle) => { if (statusCallback) statusCallback(i + 1, dashCourses.length, `[${curQ}/${totQ}] ${qTitle}`); }); if (res.success && res.count > 0) { totalHarvested += res.count; totalQuizzes += res.quizzes || 0; } } catch (err) { logDebug(`Error scanning grades for ${c.code}: ${err.message}`); } } if (totalHarvested > 0) { setLog(`Collection Complete! Loaded ${totalHarvested} verified answers across ${dashCourses.length} courses.`, "var(--accent-green)", "All enrolled subjects updated in database"); showToast(`Collection complete! Loaded ${totalHarvested} verified answers across ${dashCourses.length} courses!`, 4500); injectDashboardCourseBadges(); syncAutoQuizUI(); return { success: true, count: totalHarvested, quizzes: totalQuizzes, courses: dashCourses.length }; } else { setLog(`Scanned ${dashCourses.length} courses. All available answers already cached or reviews restricted.`, "var(--accent-cyan)", "Answers ready in local database"); showToast(`Scan complete for ${dashCourses.length} courses. All up to date!`, 3500); injectDashboardCourseBadges(); return { success: true, count: 0, courses: dashCourses.length }; } } // Fallback: If no course can be identified showToast("Open a course or your Dashboard first to scan completed quizzes!", 3500); setLog("Collection Unavailable: Open a course or your Dashboard first.", "var(--accent-pink)", "Plan: Go to Dashboard or Course page to collect answers"); return { success: false, error: 'No course or dashboard found' }; } catch (e) { showToast("Collection error: " + e.message); setLog(`Collection error: ${e.message}`, "var(--accent-pink)", "Check network or permissions"); return { success: false, error: e.message }; } finally { isHarvestingInProgress = false; } } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function injectReviewQuestionMarkers(harvestedData = null) { if (!checkIsReviewPage()) return; const queList = document.querySelectorAll('.que'); if (queList.length === 0) return; const courseInfo = detectCourseInfo(); const subCode = (harvestedData && harvestedData.subjectCode) ? harvestedData.subjectCode : (courseInfo.subjectCode || 'CS6301'); const cachedDb = getCachedAnswers(subCode); const autoShareEnabled = localStorage.getItem('amaes_auto_community_share') !== 'false'; const autoPushEnabled = localStorage.getItem('amaes_auto_push_github') === 'true' && Boolean(localStorage.getItem('amaes_github_token')); const isCloudSharingOn = autoShareEnabled || autoPushEnabled; queList.forEach((que) => { const qData = extractQuestionData(que); if (!qData || !qData.qText) return; const qNorm = normalizeText(qData.qText); const isMatch = typeof identifyQuestionType === 'function' && identifyQuestionType(que) === 'match'; // Find matching item in harvested list or cached DB const isGenericMatchingHeader = isMatch && (qNorm.includes('matching type') || qNorm.includes('choose the correct answer')); const harvestedItem = (isGenericMatchingHeader) ? null : (harvestedData && harvestedData.questions ? harvestedData.questions.find(q => q.qNorm === qNorm || (qData.qText && qData.qText.includes(q.qRaw))) : null); const dbEntry = (isGenericMatchingHeader) ? null : cachedDb.find(q => { const normQ = normalizeText(q.qRaw || q.question || ''); if (normQ.includes('matching type') && (q.ansRaw || q.answer || '').includes(',')) return false; return q.qNorm === qNorm || (qData.qText && qData.qText.includes(q.qRaw)); }); const gradeInfo = parseMoodleQuestionGrade(que); const isFullMark = gradeInfo.isFullMark; const isZeroMark = gradeInfo.isZeroMark; const isPartialMark = gradeInfo.isPartialMark; const rightElem = que.querySelector('.rightanswer, .outcome .rightanswer'); const hasExplicitRightElem = Boolean(rightElem && rightElem.innerText.trim()); // Dedicated Matching Question Handling in Review Screen const matchingRows = Array.from(que.querySelectorAll('.answer table tr, .answer tr')).filter(row => row.querySelector('select') && row.querySelector('td.text, td:first-child') ); const isMatchQuestion = matchingRows.length > 0 || isMatch; if (isMatchQuestion && matchingRows.length > 0) { const infoCol = que.querySelector('.info'); let pill = que.querySelector('.amaes-review-status-pill'); if (!pill) { pill = document.createElement('div'); pill.className = 'amaes-review-status-pill'; const flagBox = que.querySelector('.info .questionflag, .info .state'); if (flagBox) { flagBox.parentNode.insertBefore(pill, flagBox.nextSibling); } else if (infoCol) { infoCol.appendChild(pill); } } let outcomeBox = que.querySelector('.outcome'); if (!outcomeBox) { const formulationBox = que.querySelector('.formulation, .content'); if (formulationBox) { outcomeBox = document.createElement('div'); outcomeBox.className = 'outcome clearfix'; formulationBox.appendChild(outcomeBox); } } let verifiedRowCount = 0; let deducedRowCount = 0; let eliminatedRowCount = 0; const totalMatchingRows = matchingRows.length; matchingRows.forEach((row) => { const promptCell = row.querySelector('td.text, td:first-child'); const prompt = promptCell ? cleanDOMToAI(promptCell).trim() : ''; const promptNorm = normalizeText(prompt); const select = row.querySelector('select'); const selectedOption = select && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null; const selectedAnswer = selectedOption && selectedOption.value && !selectedOption.text.toLowerCase().includes('choose') ? String(selectedOption.text || selectedOption.innerText || '').trim() : ''; // Match against harvestedData or cachedDb const mHarvested = (harvestedData && harvestedData.questions) ? harvestedData.questions.find(q => q.qNorm === promptNorm) : null; const mCached = cachedDb.find(q => (q.qNorm || normalizeText(q.qRaw || q.question || '')) === promptNorm); const isRowDeduced = Boolean((mHarvested && mHarvested.deduced) || (mCached && mCached.deduced)); const isRowVerified = Boolean(isRowDeduced || (mHarvested && mHarvested.verified) || (mCached && mCached.verified) || isFullMark || hasChoiceCheckmark(row)); const resolvedAnswer = (mHarvested && mHarvested.ansRaw) || (mCached && (mCached.ansRaw || mCached.answer)) || (isRowVerified ? selectedAnswer : ''); const isRowWrong = Boolean(!isRowVerified && (hasChoiceCross(row) || (mHarvested && mHarvested.wrongAnswers && mHarvested.wrongAnswers.length > 0) || (mCached && mCached.wrongAnswers && mCached.wrongAnswers.length > 0))); if (isRowVerified && resolvedAnswer) { verifiedRowCount++; if (isRowDeduced) deducedRowCount++; } else if (isRowWrong) { eliminatedRowCount++; } // Per-row visual badge injection const controlCell = row.querySelector('td.control, td:last-child'); if (controlCell) { controlCell.querySelectorAll('.amaes-matching-row-badge').forEach(b => b.remove()); const rowBadge = document.createElement('div'); rowBadge.className = 'amaes-matching-row-badge'; rowBadge.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; margin-top: 5px; padding: 2px 7px; border-radius: 4px; font-size: 11px; font-weight: 600; line-height: 1.3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;'; if (isRowDeduced && resolvedAnswer) { rowBadge.style.background = 'rgba(6, 182, 212, 0.12)'; rowBadge.style.border = '1px solid rgba(6, 182, 212, 0.35)'; rowBadge.style.color = '#0e7490'; rowBadge.innerHTML = `Deduced: ${escapeHtml(resolvedAnswer)} (Elimination)`; controlCell.appendChild(rowBadge); } else if (isRowVerified && resolvedAnswer) { rowBadge.style.background = 'rgba(16, 185, 129, 0.12)'; rowBadge.style.border = '1px solid rgba(16, 185, 129, 0.35)'; rowBadge.style.color = '#059669'; rowBadge.innerHTML = `Verified: ${escapeHtml(resolvedAnswer)}`; controlCell.appendChild(rowBadge); } else if (isRowWrong && selectedAnswer) { rowBadge.style.background = 'rgba(239, 68, 68, 0.12)'; rowBadge.style.border = '1px solid rgba(239, 68, 68, 0.35)'; rowBadge.style.color = '#b91c1c'; rowBadge.innerHTML = `Eliminated: ${escapeHtml(selectedAnswer)}`; controlCell.appendChild(rowBadge); } } }); const earnedStr = gradeInfo.earned !== null ? gradeInfo.earned : ''; const maxStr = gradeInfo.max !== null ? gradeInfo.max : ''; if (verifiedRowCount === totalMatchingRows && totalMatchingRows > 0) { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: ${isCloudSharingOn ? 'rgba(16, 185, 129, 0.14)' : 'rgba(59, 130, 246, 0.14)'}; color: ${isCloudSharingOn ? '#059669' : '#2563eb'}; border: 1px solid ${isCloudSharingOn ? 'rgba(16, 185, 129, 0.4)' : 'rgba(59, 130, 246, 0.4)'}; box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; const labelText = deducedRowCount > 0 ? `Saved (${totalMatchingRows - deducedRowCount} Verified, ${deducedRowCount} Deduced)` : 'Saved to Study Bank'; pill.title = `All ${totalMatchingRows} matching items resolved (${totalMatchingRows - deducedRowCount} verified directly, ${deducedRowCount} deduced by elimination) and saved to study database.`; pill.innerHTML = `${isCloudSharingOn ? (ICONS.cloudUpload || ICONS.cloud) : ICONS.database} ${labelText}`; if (outcomeBox && !outcomeBox.querySelector('.amaes-review-outcome-banner')) { const banner = document.createElement('div'); banner.className = 'amaes-review-outcome-banner'; banner.style.cssText = 'margin-top: 8px; padding: 7px 12px; border-radius: 6px; font-size: 11.5px; font-weight: 600; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.35); color: #065f46; display: flex; align-items: center; gap: 7px;'; banner.innerHTML = `${ICONS.check} All ${totalMatchingRows} matching items resolved (${totalMatchingRows - deducedRowCount} verified directly, ${deducedRowCount} deduced by elimination) and cataloged into study bank.`; outcomeBox.appendChild(banner); } } else if (verifiedRowCount > 0) { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(16, 185, 129, 0.12); color: #047857; border: 1px solid rgba(16, 185, 129, 0.35); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; pill.title = `${verifiedRowCount} verified row(s) saved to study bank, ${eliminatedRowCount} incorrect choice(s) eliminated.`; pill.innerHTML = `${ICONS.check} Partially Harvested (${verifiedRowCount} Verified, ${eliminatedRowCount} Eliminated)`; if (outcomeBox && !outcomeBox.querySelector('.amaes-review-matching-notice')) { const notice = document.createElement('div'); notice.className = 'amaes-review-matching-notice'; notice.style.cssText = 'margin-top: 8px; padding: 7px 12px; border-radius: 6px; font-size: 11.5px; font-weight: 600; background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.3); color: #065f46;'; notice.textContent = `Partial Score (${earnedStr}/${maxStr}): ${verifiedRowCount} of ${totalMatchingRows} matching rows verified correct and saved to study bank. ${eliminatedRowCount} wrong choice(s) recorded as eliminated.`; outcomeBox.appendChild(notice); } } else if (eliminatedRowCount > 0) { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(239, 68, 68, 0.12); color: #b91c1c; border: 1px solid rgba(239, 68, 68, 0.35); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; pill.title = `${eliminatedRowCount} incorrect choices eliminated from future attempts.`; pill.innerHTML = `${ICONS.xCircle || ICONS.alertTriangle} ${eliminatedRowCount} Choices Eliminated`; } else { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(245, 158, 11, 0.14); color: #b45309; border: 1px solid rgba(245, 158, 11, 0.4); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; pill.title = `Partial score (${earnedStr}/${maxStr}): Moodle does not specify which individual rows were correct or incorrect, so this matching question is not auto-harvested.`; pill.innerHTML = `${ICONS.alertTriangle || ICONS.help} Unconfirmed (Not Harvested)`; if (outcomeBox && !outcomeBox.querySelector('.amaes-review-matching-notice')) { const notice = document.createElement('div'); notice.className = 'amaes-review-matching-notice'; notice.style.cssText = 'margin-top: 8px; padding: 6px 12px; border-radius: 6px; font-size: 11.5px; font-weight: 600; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.35); color: #92400e;'; notice.textContent = `Partial Score (${earnedStr}/${maxStr}): Moodle does not provide row-by-row correctness indicators for this matching question. Rows remain unconfirmed and are not saved to the study bank.`; outcomeBox.appendChild(notice); } } return; } let ansText = (harvestedItem && harvestedItem.ansRaw) || (dbEntry && (dbEntry.ansRaw || dbEntry.answer)) || ''; // Check choice rows directly for checkmarks (Moodle review ground truth!) const choiceRows = que.querySelectorAll('.answer div.r0, .answer div.r1, .answer div[class*="r"], .answer fieldset > div, .answer li, .answer tr, .answer label, .answer > div'); const checkmarkedTexts = []; choiceRows.forEach(row => { const label = row.querySelector('label') || row; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (!text) return; if (hasChoiceCheckmark(row) || hasChoiceCheckmark(label)) { if (!checkmarkedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) { checkmarkedTexts.push(text); } } }); if (checkmarkedTexts.length > 0) { ansText = checkmarkedTexts.join(', '); } // If question scored full marks but ansText wasn't in cache, extract directly from live DOM! if (!ansText && isFullMark) { const textInput = que.querySelector('input[type="text"], textarea, .form-control'); if (textInput && (textInput.value || textInput.getAttribute('value'))) { ansText = (textInput.value || textInput.getAttribute('value')).trim(); } const checked = que.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"], .answer div.selected, .answer div.correct, .answer [class*="selected"], .answer [class*="checked"]'); if (!ansText && checked) { const lbl = checked.closest('label') || checked.closest('div.r0, div.r1, [class*="r"]') || checked.parentElement; if (lbl) ansText = cleanDOMToAI(lbl).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); } const selected = que.querySelector('select'); if (!ansText && selected && selected.selectedIndex >= 0) { const opt = selected.options[selected.selectedIndex]; if (opt && opt.value && !opt.text.toLowerCase().includes('choose')) { ansText = opt.text.trim(); } } } // Extract explicit right answer from outcome if present if (rightElem && !checkmarkedTexts.length) { let raw = cleanDOMToAI(rightElem); raw = raw.replace(/^The correct answers? (is|are):?\s*['"]?/i, '').replace(/['"]?\s*$/i, '').trim(); if (raw) { ansText = raw; } } const wrongList = (harvestedItem && harvestedItem.wrongAnswers) || (dbEntry && dbEntry.wrongAnswers) || []; const wrongNorms = wrongList.map(w => typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || ''))); // Detect any selected choice that was marked wrong (red cross or zero mark) const crossedTexts = []; choiceRows.forEach(row => { const label = row.querySelector('label') || row; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (!text) return; if (hasChoiceCross(row) || hasChoiceCross(label)) { if (!crossedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) { crossedTexts.push(text); } } }); if (isZeroMark) { const checkedInputs = que.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked, input[checked], [aria-checked="true"], .answer div.selected, .answer label.selected'); checkedInputs.forEach(inp => { const label = inp.closest('label') || inp.closest('div.r0, div.r1, [class*="r"]') || inp.parentElement; let text = cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); if (text && !crossedTexts.some(c => normalizeChoice(c) === normalizeChoice(text))) { crossedTexts.push(text); } }); } crossedTexts.forEach(txt => { const norm = normalizeChoice(txt); if (norm && !wrongNorms.includes(norm)) { wrongList.push(txt); wrongNorms.push(norm); } }); const ansNorm = normalizeChoice(ansText); const isConfirmedByMoodle = Boolean(isFullMark || hasExplicitRightElem || checkmarkedTexts.length > 0); let isDebunked = false; if (isConfirmedByMoodle && ansNorm) { isDebunked = false; // Moodle's grade / checkmark is absolute supreme truth! // Purge any contradictory wrongAnswer distractor from wrongList and cache! const verifiedNormsList = checkmarkedTexts.map(c => normalizeChoice(c)); verifiedNormsList.push(ansNorm); ansText.split(/[,;&\n]+|\s+and\s+/i).forEach(s => verifiedNormsList.push(normalizeChoice(s))); for (let i = wrongList.length - 1; i >= 0; i--) { const wNorm = typeof wrongList[i] === 'string' ? normalizeChoice(wrongList[i]) : (wrongList[i].norm || normalizeChoice(wrongList[i].text || '')); if (verifiedNormsList.some(v => v === wNorm || unscriptDigits(v) === unscriptDigits(wNorm))) { wrongList.splice(i, 1); } } if (harvestedItem && Array.isArray(harvestedItem.wrongAnswers)) { harvestedItem.wrongAnswers = harvestedItem.wrongAnswers.filter(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); return !verifiedNormsList.some(v => v === wNorm || unscriptDigits(v) === unscriptDigits(wNorm)); }); harvestedItem.ansRaw = ansText; harvestedItem.ansNorm = ansNorm; harvestedItem.verified = true; } if (dbEntry) { if (Array.isArray(dbEntry.wrongAnswers)) { dbEntry.wrongAnswers = dbEntry.wrongAnswers.filter(w => { const wNorm = typeof w === 'string' ? normalizeChoice(w) : (w.norm || normalizeChoice(w.text || '')); return !verifiedNormsList.some(v => v === wNorm || unscriptDigits(v) === unscriptDigits(wNorm)); }); } dbEntry.ansRaw = ansText; dbEntry.ansNorm = ansNorm; dbEntry.verified = true; if (cachedDb && cachedDb.length > 0) { setCachedAnswers(subCode, cachedDb); } } } else if (ansNorm && wrongNorms.some(w => w === ansNorm || unscriptDigits(w) === unscriptDigits(ansNorm))) { isDebunked = true; ansText = ''; if (dbEntry) { dbEntry.ansRaw = ''; dbEntry.ansNorm = ''; dbEntry.verified = false; dbEntry.wrongAnswers = wrongList; if (cachedDb && cachedDb.length > 0) { setCachedAnswers(subCode, cachedDb); } } if (harvestedItem) { harvestedItem.ansRaw = ''; harvestedItem.ansNorm = ''; harvestedItem.verified = false; harvestedItem.wrongAnswers = wrongList; } } // Ground-truth debunking: If question scored zero mark and Moodle revealed neither checkmarks nor right answer: if (isZeroMark && !isFullMark && checkmarkedTexts.length === 0 && !hasExplicitRightElem) { // If ansText matches any crossed choice or was the submitted choice, it is completely debunked if (ansNorm && (wrongNorms.some(w => w === ansNorm || unscriptDigits(w) === unscriptDigits(ansNorm)) || crossedTexts.some(c => normalizeChoice(c) === ansNorm))) { isDebunked = true; ansText = ''; if (dbEntry) { dbEntry.ansRaw = ''; dbEntry.ansNorm = ''; dbEntry.verified = false; dbEntry.wrongAnswers = wrongList; if (cachedDb && cachedDb.length > 0) { setCachedAnswers(subCode, cachedDb); } } if (harvestedItem) { harvestedItem.ansRaw = ''; harvestedItem.ansNorm = ''; harvestedItem.verified = false; harvestedItem.wrongAnswers = wrongList; } } } // Real-time Deduction by Elimination on Review screen let isDeduced = Boolean((harvestedItem && harvestedItem.deduced) || (dbEntry && dbEntry.deduced)); if (!ansText && wrongNorms.length > 0 && Array.isArray(qData.choices) && qData.choices.length > 1) { const uneliminated = qData.choices.filter(c => { const normC = normalizeChoice(c); return !wrongNorms.some(w => w === normC || unscriptDigits(w) === unscriptDigits(normC)); }); if (uneliminated.length === 1) { ansText = uneliminated[0].replace(/^[a-zA-Z0-9][.)]\s*/, '').trim(); isDeduced = true; isDebunked = false; if (dbEntry) { dbEntry.ansRaw = ansText; dbEntry.ansNorm = normalizeChoice(ansText); dbEntry.answer = ansText; dbEntry.verified = true; dbEntry.deduced = true; dbEntry.wrongAnswers = wrongList; if (cachedDb && cachedDb.length > 0) { setCachedAnswers(subCode, cachedDb); } } if (harvestedItem) { harvestedItem.ansRaw = ansText; harvestedItem.ansNorm = normalizeChoice(ansText); harvestedItem.verified = true; harvestedItem.deduced = true; harvestedItem.wrongAnswers = wrongList; } } } const hasMoodleOrDeducedProof = isConfirmedByMoodle || isDeduced; const isVerified = Boolean(!isDebunked && ansText && (hasMoodleOrDeducedProof || (!isZeroMark && ((harvestedItem && harvestedItem.verified) || (dbEntry && dbEntry.verified))))); const infoCol = que.querySelector('.info'); const contentCol = que.querySelector('.content'); // 1. Sidebar Info Badge (Left box under Grade / Flag question) let pill = que.querySelector('.amaes-review-status-pill'); if (!pill) { pill = document.createElement('div'); pill.className = 'amaes-review-status-pill'; const flagBox = que.querySelector('.info .questionflag, .info .state'); if (flagBox) { flagBox.parentNode.insertBefore(pill, flagBox.nextSibling); } else if (infoCol) { infoCol.appendChild(pill); } } if (isVerified && ansText) { if (isCloudSharingOn) { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(16, 185, 129, 0.14); color: #059669; border: 1px solid rgba(16, 185, 129, 0.4); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; pill.title = `Correct answer "${ansText}" saved to Study Bank for your next attempt and classmates!`; pill.innerHTML = `${ICONS.cloudUpload || ICONS.cloud} ${isDeduced ? 'Deduced & Saved' : 'Saved to Study Bank'}`; } else { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(59, 130, 246, 0.14); color: #2563eb; border: 1px solid rgba(59, 130, 246, 0.4); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; pill.title = `Correct answer "${ansText}" saved locally (Community sharing toggle is OFF)`; pill.innerHTML = `${ICONS.database} ${isDeduced ? 'Deduced Locally' : 'Saved Locally'}`; } // 2. In-Question Outcome Banner let outcomeBox = que.querySelector('.outcome'); if (!outcomeBox) { const formulationBox = que.querySelector('.formulation, .content'); if (formulationBox) { outcomeBox = document.createElement('div'); outcomeBox.className = 'outcome clearfix'; formulationBox.appendChild(outcomeBox); } } if (outcomeBox) { let banner = outcomeBox.querySelector('.amaes-review-outcome-banner'); if (!banner) { banner = document.createElement('div'); banner.className = 'amaes-review-outcome-banner'; outcomeBox.appendChild(banner); } banner.style.cssText = ` display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding: 6px 12px; border-radius: 6px; font-size: 11.5px; font-weight: 600; background: ${isCloudSharingOn ? 'rgba(16, 185, 129, 0.12)' : 'rgba(59, 130, 246, 0.12)'}; border: 1px solid ${isCloudSharingOn ? 'rgba(16, 185, 129, 0.35)' : 'rgba(59, 130, 246, 0.35)'}; color: ${isCloudSharingOn ? '#065f46' : '#1e40af'}; `; banner.innerHTML = `
${isCloudSharingOn ? (ICONS.cloudUpload || ICONS.cloud) : ICONS.database} ${isDeduced ? 'Deduced' : 'Verified'} Answer${checkmarkedTexts.length > 1 ? 's' : ''}: “${escapeHtml(ansText)}”${(isPartialMark && gradeInfo.earned !== null && gradeInfo.max !== null) ? ` (Partial: ${gradeInfo.earned}/${gradeInfo.max})` : ''}
${isCloudSharingOn ? 'Saved to Study Bank' : 'Saved Locally'} `; } } else if (isZeroMark && !isFullMark && checkmarkedTexts.length === 0) { pill.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin-top: 6px; border-radius: 6px; font-size: 11px; font-weight: 700; line-height: 1.25; background: rgba(239, 68, 68, 0.14); color: #dc2626; border: 1px solid rgba(239, 68, 68, 0.4); box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: default; `; const wrongText = wrongList.map(w => typeof w === 'string' ? w : w.text).filter(Boolean).join(', '); const isEmptyAnswer = !wrongText; // e.g. user cleared a fill-in-the-blank field const pillLabel = isEmptyAnswer ? 'No Answer / Incorrect' : 'Past response eliminated'; pill.title = isEmptyAnswer ? 'No answer was submitted or the answer field was cleared. Check the verified answer below.' : `Moodle keeps this submitted answer in the completed-attempt history. The toolkit saved "${wrongText}" as incorrect and excludes it from future suggestions on this browser.`; pill.innerHTML = `${ICONS.xCircle} ${pillLabel}`; // Try to get the correct answer from the .rightanswer element for display const rightAnswerForDisplay = (() => { const re = que.querySelector('.rightanswer, .outcome .rightanswer'); if (!re) return ''; return cleanDOMToAI(re).replace(/^The correct answers? (is|are):?\s*['"]/i, '').replace(/['"]?\s*$/i, '').trim(); })(); let outcomeBox = que.querySelector('.outcome'); if (!outcomeBox) { const formulationBox = que.querySelector('.formulation, .content'); if (formulationBox) { outcomeBox = document.createElement('div'); outcomeBox.className = 'outcome clearfix'; formulationBox.appendChild(outcomeBox); } } if (outcomeBox) { let banner = outcomeBox.querySelector('.amaes-review-outcome-banner'); if (!banner) { banner = document.createElement('div'); banner.className = 'amaes-review-outcome-banner'; outcomeBox.appendChild(banner); } banner.style.cssText = ` display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding: 6px 12px; border-radius: 6px; font-size: 11.5px; font-weight: 600; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.35); color: #991b1b; `; if (isEmptyAnswer) { banner.innerHTML = `
${ICONS.xCircle} Incorrect / No Answer Submitted${rightAnswerForDisplay ? ` — Correct: “${escapeHtml(rightAnswerForDisplay)}”` : ''}
Incorrect `; } else { banner.innerHTML = `
${ICONS.xCircle} Eliminated Choice: “${escapeHtml(wrongText)}” (Confirmed Incorrect)${rightAnswerForDisplay ? ` — Correct: “${escapeHtml(rightAnswerForDisplay)}”` : ''}
Eliminated `; } } // Highlight the eliminated choice row on the question card with badge & tint if (wrongList.length > 0) { const choiceRows = que.querySelectorAll('.answer > div, .answer > tr, .answer li'); choiceRows.forEach(row => { const label = row.querySelector('label') || row; const rowText = normalizeChoice(cleanDOMToAI(label).replace(/^[a-zA-Z0-9][.)]\s*/, '').trim()); const isThisWrong = wrongList.some(w => { const wText = typeof w === 'string' ? normalizeChoice(w) : normalizeChoice(w.text || ''); return wText && (rowText === wText || rowText.includes(wText) || wText.includes(rowText)); }); if (isThisWrong && !row.querySelector('.amaes-eliminated-badge')) { row.style.background = 'rgba(239, 68, 68, 0.08)'; row.style.borderRadius = '5px'; const elimBadge = document.createElement('span'); elimBadge.className = 'amaes-eliminated-badge'; elimBadge.style.cssText = ` font-size: 9.5px; font-weight: 700; padding: 2px 6px; border-radius: 4px; margin-left: 8px; background: rgba(239, 68, 68, 0.18); color: #dc2626; border: 1px solid rgba(239, 68, 68, 0.35); display: inline-flex; align-items: center; gap: 3px; `; elimBadge.innerHTML = `${ICONS.xCircle} Eliminated`; label.appendChild(elimBadge); } }); } } // ── Review Card Border Highlight ────────────────────────────────────── // Visually highlight the entire question card so users instantly see // which questions were wrong (red), partial (orange), or correct (green). // This prevents confusion especially for fill-in-the-blank where students // may have cleared their answer and can't see what they entered. que.style.transition = 'border-left 0.3s ease'; if (isFullMark) { que.style.borderLeft = '4px solid rgba(16, 185, 129, 0.75)'; que.style.borderRadius = '0 6px 6px 0'; } else if (isPartialMark) { que.style.borderLeft = '4px solid rgba(245, 158, 11, 0.85)'; que.style.borderRadius = '0 6px 6px 0'; } else if (isZeroMark) { que.style.borderLeft = '4px solid rgba(239, 68, 68, 0.8)'; que.style.borderRadius = '0 6px 6px 0'; } }); } let lastProcessedReviewAttempt = null; function getReviewQuestionStateSignature(que) { const qData = extractQuestionData(que); const qText = normalizeText(qData ? qData.qText : ''); // Extract native Moodle feedback, excluding toolkit's own injected banners/pills const feedbackEls = que.querySelectorAll('.info, .outcome, .rightanswer, .feedback'); const feedbackTexts = []; feedbackEls.forEach(el => { const clone = el.cloneNode(true); clone.querySelectorAll('[class*="amaes-"]').forEach(inj => inj.remove()); feedbackTexts.push(clone.innerText || ''); }); const feedback = normalizeText(feedbackTexts.join('|')); const inputs = Array.from(que.querySelectorAll('input, select, textarea')).map(input => { const selected = input.type === 'checkbox' || input.type === 'radio' ? (input.checked ? 'checked' : '') : input.value || ''; return `${input.type || input.tagName}:${selected}`; }).join('|'); const markedChoices = Array.from(que.querySelectorAll('.answer [class*="correct" i], .answer [class*="incorrect" i], .answer .fa-check, .answer .fa-times')) .filter(el => !el.closest('[class*="amaes-"]') && !(el.className && typeof el.className === 'string' && el.className.includes('amaes-'))) .map(el => `${el.className || ''}:${normalizeText(el.innerText || '')}`) .join('|'); return `${qText}::${feedback}::${inputs}::${markedChoices}`; } function getReviewShareKey(question) { const wrong = normalizeWrongAnswers(question.wrongAnswers) .map(item => item.norm) .sort() .join('|'); return `${getQuestionIdentity(question)}::${normalizeChoice(question.ansRaw || question.answer || '')}::${wrong}`; } function handleQuizReviewPageLoad() { if (!checkIsReviewPage()) return; const urlParams = new URLSearchParams(window.location.search); const attemptId = urlParams.get('attempt') || urlParams.get('id') || urlParams.get('cmid') || window.location.search || window.location.pathname; if (!sessionStorage.getItem(`amaes_review_ding_${attemptId}`)) { sessionStorage.setItem(`amaes_review_ding_${attemptId}`, '1'); playToolkitSound('quest_done'); if (typeof recordSessionQuizCompleted === 'function') { recordSessionQuizCompleted(); } } // Check for multi-page review pagination: expand to show all questions on one page if available const showAllLink = document.querySelector('a[href*="review.php"][href*="showall=1"], a[href*="showall=true"]'); if (showAllLink && !window.location.search.includes('showall=1') && !sessionStorage.getItem(`amaes_expanded_review_${attemptId}`)) { sessionStorage.setItem(`amaes_expanded_review_${attemptId}`, '1'); setLog("Multi-page Review: Expanding full quiz to review and harvest all questions on one page...", "var(--accent-blue)"); showToast("Expanding full quiz review for complete answer harvest...", 2000); window.location.href = showAllLink.href; return; } const harvested = harvestReviewAnswers(); // Non-Reviewable Assessment Guard: If feedback and marks are hidden, pause auto-harvesting if (harvested && harvested.unreviewable) { let banner = document.getElementById('amaes-unreviewable-page-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'amaes-unreviewable-page-banner'; banner.style.cssText = 'display:flex; align-items:center; gap:10px; margin:14px 0; padding:12px 16px; border:2px solid #f59e0b; border-radius:10px; background:#fffbeb; color:#78350f; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow:0 4px 12px rgba(245,158,11,0.15);'; banner.innerHTML = ` ${ICONS.lock}
Non-Reviewable Assessment — Auto-Harvesting Paused
Moodle has hidden answer feedback and marks for this assessment. Answer scraping is safely disabled to prevent unverified data pollution.
`; const mainArea = document.querySelector('#region-main, main, [role="main"]') || document.body; mainArea.prepend(banner); } setLog("Non-Reviewable Assessment: Answer feedback and marks are hidden by Moodle for this attempt. Answer scraping safely paused.", "var(--accent-amber)"); showToast("Non-reviewable assessment: answer scraping safely paused.", 4000); return; } // MutationObserver may see the review page before Moodle finishes // inserting all questions. Include the question signature in the guard // so a later multi-question DOM is harvested too. const questionSignature = Array.from(document.querySelectorAll('.que')) .map(getReviewQuestionStateSignature) .filter(Boolean) .join('|'); const processingKey = `${attemptId}:${questionSignature}`; // Always inject visual markers for questions on review page injectReviewQuestionMarkers(harvested); if (!harvested || !harvested.success || (harvested.harvestedCount === 0 && (harvested.eliminatedCount || 0) === 0)) { const retryKey = `amaes_review_retry_${attemptId}`; const retryCount = Number(sessionStorage.getItem(retryKey) || '0'); if (retryCount < 5) { sessionStorage.setItem(retryKey, String(retryCount + 1)); setTimeout(() => { if (checkIsReviewPage()) handleQuizReviewPageLoad(); }, 700); } else { sessionStorage.removeItem(retryKey); } return; } sessionStorage.removeItem(`amaes_review_retry_${attemptId}`); if (lastProcessedReviewAttempt === processingKey) return; lastProcessedReviewAttempt = processingKey; // Inject Reviewable Assessment Status Badge let reviewBadge = document.getElementById('amaes-reviewable-page-badge'); if (!reviewBadge) { const summaryTable = document.querySelector('.quizreviewsummary, table.quizreviewsummary, #region-main, main') || document.body; reviewBadge = document.createElement('div'); reviewBadge.id = 'amaes-reviewable-page-badge'; reviewBadge.style.cssText = 'display:inline-flex; align-items:center; gap:6px; margin:8px 0; padding:5px 12px; background:rgba(16,185,129,0.12); border:1px solid rgba(16,185,129,0.35); border-radius:6px; color:#059669; font-size:11.5px; font-weight:700; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;'; reviewBadge.innerHTML = `${ICONS.check} Reviewable Assessment: Ground truth available (${harvested.harvestedCount} verified answers, ${harvested.eliminatedCount || 0} wrong choices harvested)`; if (summaryTable.parentNode) { summaryTable.parentNode.insertBefore(reviewBadge, summaryTable); } } // Mount top quiz mover navigation right after review badge so user can immediately cycle to next quiz if (typeof injectQuizBottomNavigation === 'function') { injectQuizBottomNavigation(); } const topNav = document.getElementById('amaes-quiz-top-nav'); if (reviewBadge && reviewBadge.parentNode && topNav && topNav.previousElementSibling !== reviewBadge) { reviewBadge.parentNode.insertBefore(topNav, reviewBadge.nextSibling); } // Mount Retake Quiz action bar directly below review summary table if (typeof injectReviewRetakeButton === 'function') { injectReviewRetakeButton(); } // Check for 100% score and trigger auto-mark activity as Done if enabled if (typeof checkQuizAttemptIs100Percent === 'function' && checkQuizAttemptIs100Percent(harvested, document)) { const urlParams = new URLSearchParams(window.location.search); let cmid = urlParams.get('cmid') || urlParams.get('id') || ''; if (!cmid) { const idInput = document.querySelector('input[name="cmid"], input[name="id"]'); if (idInput && idInput.value) cmid = idInput.value.trim(); } if (!cmid) { const mBody = (document.body?.className || '').match(/\bcmid-(\d+)\b/); if (mBody) cmid = mBody[1]; } if (!cmid) { const viewLinks = document.querySelectorAll('a[href*="/mod/quiz/view.php?id="], a[href*="/mod/quiz/view.php?cmid="], .breadcrumb a[href*="id="], .breadcrumb-item a[href*="id="]'); for (const l of viewLinks) { const m = (l.getAttribute('href') || l.href || '').match(/[?&](?:id|cmid)=(\d+)/); if (m) { cmid = m[1]; break; } } } let rawTitle = (harvested && harvested.quizTitle) || ''; if (!rawTitle) { const h = document.querySelector('.page-header-headings h1, #region-main h2, h2, h3, .breadcrumb-item:last-child'); if (h) rawTitle = h.innerText.trim(); } if (!rawTitle && typeof detectCourseInfo === 'function') { rawTitle = detectCourseInfo().currentActivityTitle || ''; } const normTitle = rawTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); if (!cmid && normTitle && typeof getCourseQuizzesList === 'function') { const qList = getCourseQuizzesList(); const m = qList.find(q => { const qNorm = (q.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); return qNorm && (qNorm === normTitle || normTitle.includes(qNorm) || qNorm.includes(normTitle)); }); if (m && m.cmid) cmid = m.cmid; } if (cmid) { localStorage.setItem(`amaes_quiz_100_cmid_${cmid}`, 'true'); } if (normTitle) { localStorage.setItem(`amaes_quiz_100_title_${normTitle}`, 'true'); } // Immediately update and invalidate course grades cache with 100% passable score const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const courseId = courseInfo && courseInfo.courseId ? courseInfo.courseId : ''; if (courseId) { try { const cachedRaw = localStorage.getItem(`amaes_course_grades_cache_${courseId}`); const map = cachedRaw ? JSON.parse(cachedRaw) : {}; const gradeEntry = { title: rawTitle, normTitle: normTitle, cmid: cmid, hasGrade: true, percentage: 100, gradeStr: '100%', isPassable: true, source: 'review-100' }; if (normTitle) map[normTitle] = gradeEntry; if (cmid) map[`cmid_${cmid}`] = gradeEntry; localStorage.setItem(`amaes_course_grades_cache_${courseId}`, JSON.stringify(map)); localStorage.setItem(`amaes_course_grades_cache_time_${courseId}`, String(Date.now())); if (typeof window !== 'undefined') { window.__amaesCourseGradesCache = map; window.__amaesCourseGradesCacheTime = Date.now(); } } catch (_) {} } const autoMark100 = (typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_mark_done_100') !== 'false' : true); if (autoMark100 && typeof triggerManualCompletionOnPage === 'function') { triggerManualCompletionOnPage(); } } // Auto-save verified answers and eliminated wrong choices to local subject cache const cacheRes = mergeAnswersIntoCache(harvested.subjectCode, harvested.questions, 'Review'); // Re-highlight choices so newly deduced or eliminated choices display updated badges highlightQuizAnswers(getCachedAnswers(harvested.subjectCode), false); // Update markers with fresh cache state injectReviewQuestionMarkers(harvested); const autoShareEnabled = localStorage.getItem('amaes_auto_community_share') !== 'false'; const hasNewDiscoveries = Boolean(cacheRes && (cacheRes.added > 0 || cacheRes.confirmed > 0 || cacheRes.eliminated > 0 || cacheRes.conflicts > 0)); if (hasNewDiscoveries) { const newTotal = (cacheRes.added || 0) + (cacheRes.confirmed || 0); if (autoShareEnabled) { setLog(`Quiz Review Checked: Extracted ${harvested.harvestedCount} verified answers (${newTotal} updated) & ${harvested.eliminatedCount || 0} wrong choices for ${harvested.subjectCode}. Sharing queued.`, "var(--accent-green)"); showToast(`Review Checked: ${newTotal > 0 ? `Collected ${newTotal} verified answer${newTotal > 1 ? 's' : ''}` : `${cacheRes.eliminated} wrong choice${cacheRes.eliminated > 1 ? 's' : ''} eliminated`}; sharing queued.`, 4000); } else { setLog(`Quiz Review Checked: Extracted ${harvested.harvestedCount} verified answers (${newTotal} updated) for ${harvested.subjectCode} (Saved to Local DB, sharing is OFF).`, "var(--accent-blue)"); showToast(`Review Checked: ${newTotal > 0 ? `Collected ${newTotal} verified answer${newTotal > 1 ? 's' : ''}` : `${cacheRes.eliminated} wrong choice${cacheRes.eliminated > 1 ? 's' : ''} eliminated`} & saved locally!`, 4000); } } else { // Already cataloged: quiet debug log, no repetitive popup toast when reviewing past attempts logDebug(`Quiz Review: All ${harvested.harvestedCount} verified answers for ${harvested.subjectCode} are already cataloged.`); } const autoPushEnabled = localStorage.getItem('amaes_auto_push_github') === 'true'; const hasGithubToken = Boolean(localStorage.getItem('amaes_github_token')); // Prevent repeated auto-actions on refresh using sessionStorage attempt keys const pushKey = `amaes_autopush_${attemptId}`; const shareKey = `amaes_autoshare_${attemptId}_${harvested.subjectCode}`; // 2. Auto-Push to GitHub if configured if (autoPushEnabled && hasGithubToken && !sessionStorage.getItem(pushKey)) { sessionStorage.setItem(pushKey, '1'); setTimeout(async () => { try { const res = await pushAnswersToGitHub(harvested.subjectCode, harvested.questions, { quizTitle: harvested.quizTitle, gradeText: harvested.gradeText, courseTitle: harvested.course }); if (res && res.success && res.mode === 'api') { showToast(`Auto-pushed ${harvested.harvestedCount} answers directly to GitHub database!`); } } catch (e) { console.error('Auto-push to GitHub failed:', e); } }, 1000); } // 3. Auto-share review evidence. Persist deduplication keys only after relay acceptance, // so a failed request can be retried on the next review visit. if (autoShareEnabled && (harvested.harvestedCount > 0 || (harvested.eliminatedCount || 0) > 0)) { let sharedKeys = []; try { sharedKeys = JSON.parse(localStorage.getItem(shareKey) || sessionStorage.getItem(shareKey) || '[]'); if (!Array.isArray(sharedKeys)) sharedKeys = []; } catch (e) { sharedKeys = []; } const sharedSet = new Set(sharedKeys); const pendingQuestions = harvested.questions.filter(question => { if (!question.ansRaw && !(question.evidenceType === 'moodle_review_elimination' && Array.isArray(question.wrongAnswers) && question.wrongAnswers.length > 0)) return false; const key = getReviewShareKey(question); if (sharedSet.has(key) || pendingReviewShares.has(key)) return false; return true; }); if (pendingQuestions.length > 0) { const pendingKeys = pendingQuestions.map(getReviewShareKey); pendingKeys.forEach(key => pendingReviewShares.add(key)); setTimeout(async () => { try { const result = await dispatchCommunityContribution(harvested.subjectCode, pendingQuestions, { source: 'review_screen', evidenceType: 'moodle_review', contributionId: `review-${processingKey}` }); if (!result || !result.success || !String(result.mode).startsWith('relay')) { throw new Error('The relay did not confirm receipt.'); } let acceptedKeys = []; try { acceptedKeys = JSON.parse(localStorage.getItem(shareKey) || sessionStorage.getItem(shareKey) || '[]'); if (!Array.isArray(acceptedKeys)) acceptedKeys = []; } catch (e) { acceptedKeys = []; } const acceptedSet = new Set(acceptedKeys); pendingKeys.forEach(key => acceptedSet.add(key)); const serializedKeys = JSON.stringify(Array.from(acceptedSet)); localStorage.setItem(shareKey, serializedKeys); sessionStorage.setItem(shareKey, serializedKeys); } catch (err) { logDebug(`Review community share failed; it can be retried on the next review visit: ${err.message}`); setLog(`Review evidence was saved locally but not confirmed by the community relay: ${err.message}`, "var(--accent-amber)"); } finally { pendingKeys.forEach(key => pendingReviewShares.delete(key)); } }, 1200); } } } const injectReviewScreenBanner = handleQuizReviewPageLoad; // Helper to retrieve all locally cached subject databases function getAllSavedSubjectDatabases() { const dbs = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('amaes_amauoed_cache_')) { const code = key.replace('amaes_amauoed_cache_', '').toUpperCase(); try { const data = JSON.parse(localStorage.getItem(key)); if (Array.isArray(data) && data.length > 0) { dbs[code] = data; } } catch (e) {} } } return dbs; } // Community Database Contribution & Anti-Sabotage Submission Modal function showCommunityContributionModal(initialSubCode) { const existing = document.getElementById('amaes-contribute-modal'); if (existing) existing.remove(); const allDbs = getAllSavedSubjectDatabases(); const availableCodes = Object.keys(allDbs); // Sort availableCodes by question count descending availableCodes.sort((a, b) => (allDbs[b]?.length || 0) - (allDbs[a]?.length || 0)); let targetCode = (initialSubCode && allDbs[initialSubCode.toUpperCase()]) ? initialSubCode.toUpperCase() : (availableCodes[0] || (initialSubCode ? initialSubCode.toUpperCase() : 'GENERAL')); const modal = document.createElement('div'); modal.id = 'amaes-contribute-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(4px); z-index: 100003; display: flex; align-items: center; justify-content: center; padding: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-sizing: border-box; `; function generatePayload(code, qList) { // Only export questions that have a verified or deduced answer const validQuestions = qList.filter(q => Boolean(q.ansRaw || q.answer || q.correctAnswer)); const cInfo = (typeof courseInfo !== 'undefined' && courseInfo) ? courseInfo : (typeof detectedCourseInfo !== 'undefined' && detectedCourseInfo) ? detectedCourseInfo : (typeof detectCourseInfo === 'function' ? detectCourseInfo() : null); const resolvedName = (cInfo && (cInfo.subjectCode === code || cInfo.code === code) && cInfo.subjectName) ? cInfo.subjectName : resolveKnownCourseName(code); return { subjectCode: code, subjectName: resolvedName || code, contributor: "community", timestamp: new Date().toISOString(), totalQuestions: validQuestions.length, questions: validQuestions.map(q => ({ question: q.qRaw || q.question || q.qText || "", answer: q.ansRaw || q.answer || q.correctAnswer || "", choices: q.choices || [], wrongAnswers: Array.isArray(q.wrongAnswers) ? q.wrongAnswers.map(w => typeof w === 'string' ? w : (w && w.text ? w.text : w)) : [], confidence: typeof q.confidence === 'number' ? q.confidence : 1.0, period: q.period || q.term || 'General', quizTitle: q.quizTitle || '', source: q.source || "community_contribute" })) }; } function renderModal() { const questions = allDbs[targetCode] || []; const payload = generatePayload(targetCode, questions); const payloadStr = JSON.stringify(payload, null, 2); const verifiedCount = questions.filter(q => q.ansRaw || q.answer || q.correctAnswer).length; const eliminatedCount = questions.reduce((acc, q) => acc + (Array.isArray(q.wrongAnswers) ? q.wrongAnswers.length : 0), 0); const hasPatToken = Boolean(localStorage.getItem('amaes_github_token')); modal.innerHTML = `
${ICONS.upload} Share Database to Community Hub
"Solve once, share together, never guess twice."
${ICONS.shieldCheck} A community where each student answers what's missing
Submissions are processed automatically by GitHub Actions CI. Teacher keys and consensus answers are strictly protected from overwrites. Zero tokens or accounts required!
${availableCodes.length === 0 ? `
No Saved Answers in Local Storage Yet
Complete or review a quiz in Moodle to harvest verified answers, or scrape answers from amauoed.com. Once answers are saved in your local storage, you can share them here with 1 click!
` : `
Subject Database: ${availableCodes.length > 1 ? ` ` : `${targetCode}`}
${verifiedCount} Verified • ${eliminatedCount} Eliminated
Contribution Payload Preview (${questions.length} questions):
${payloadStr.length > 1000 ? payloadStr.slice(0, 1000) + '\n... (truncated for display)' : payloadStr}
How it works: Clicking "Submit via 1-Click Issue" opens a pre-filled GitHub issue in the open community repository. Click "Submit new issue" on GitHub and the automated merger bot incorporates the new questions within 60 seconds!
`}
${hasPatToken ? ` ` : ''}
`; const closeBtn = modal.querySelector('#amaes-contribute-close-btn'); if (closeBtn) closeBtn.onclick = () => modal.remove(); const selectEl = modal.querySelector('#amaes-contribute-sub-select'); if (selectEl) { selectEl.onchange = (e) => { targetCode = e.target.value; renderModal(); }; } const copyBtn = modal.querySelector('#amaes-contribute-copy-btn'); if (copyBtn) { copyBtn.onclick = () => { copyToClipboard(payloadStr).then(() => { showToast("Payload JSON copied to clipboard!"); copyBtn.innerHTML = `${ICONS.check} Copied!`; setTimeout(() => { copyBtn.innerHTML = `${ICONS.copy} Copy JSON`; }, 2000); }).catch(() => { showToast("Failed to copy JSON"); }); }; } const tokenPushBtn = modal.querySelector('#amaes-contribute-token-push-btn'); if (tokenPushBtn) { tokenPushBtn.onclick = async () => { tokenPushBtn.disabled = true; tokenPushBtn.innerHTML = `${ICONS.rotateCcw} Pushing...`; try { await pushAnswersToGitHub(targetCode, questions, { quizTitle: 'Community Contribution' }); showToast(`Successfully pushed ${questions.length} answers directly to GitHub!`); setLog(`Successfully pushed ${questions.length} answers directly to GitHub for ${targetCode}!`, "var(--accent-green)"); const feedbackContainer = modal.querySelector('#amaes-contribute-feedback-area'); if (feedbackContainer) { feedbackContainer.style.display = 'block'; feedbackContainer.innerHTML = `
${ICONS.checkCircle} Successfully Pushed to Cloud Database!
${questions.length} answers were committed directly to the official community repository.
`; } } catch (err) { alert(`Push failed: ${err.message}`); tokenPushBtn.disabled = false; tokenPushBtn.innerHTML = `${ICONS.github} Direct Push (via Token)`; } }; } const submitBtn = modal.querySelector('#amaes-contribute-submit-btn'); if (submitBtn) { submitBtn.onclick = () => { if (!questions || questions.length === 0) { alert("No questions found for this subject to submit."); return; } const issueTitle = `[Contribution] ${targetCode} (${questions.length} questions)`; let issueBody = `### Community Answer Contribution\n\n**Subject**: ${targetCode}\n**Total Questions**: ${questions.length}\n**Submitted At**: ${new Date().toISOString()}\n\n\`\`\`json\n${payloadStr}\n\`\`\`\n`; let autoCopied = false; // If URL query string exceeds safe browser/HTTP limits (~3500 chars), auto-copy full JSON and provide clean paste prompt if (encodeURIComponent(issueBody).length > 3500) { copyToClipboard(payloadStr); autoCopied = true; issueBody = `### Community Answer Contribution\n\n**Subject**: ${targetCode}\n**Total Questions**: ${questions.length}\n**Submitted At**: ${new Date().toISOString()}\n\n> [!NOTE]\n> The JSON payload (${questions.length} questions) was automatically copied to your clipboard!\n> Simply press **Ctrl + V** (or Cmd + V) below inside this text box to paste it, then click **"Submit new issue"**.\n\n\`\`\`json\nPASTE_HERE\n\`\`\`\n`; } const ghUrl = `https://github.com/Acads-Tools/database/issues/new?title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}&labels=community-contribution`; window.open(ghUrl, '_blank'); // Provide immediate, unmissable in-modal feedback const feedbackContainer = modal.querySelector('#amaes-contribute-feedback-area'); if (feedbackContainer) { feedbackContainer.style.display = 'block'; feedbackContainer.innerHTML = `
${ICONS.checkCircle} Submission Page Opened in New Tab!
${autoCopied ? `Payload Copied! Your ${questions.length} questions were auto-copied to your clipboard. Go to the new GitHub tab, press Ctrl + V in the box, and click "Submit new issue"!` : `Final Step: Go to the newly opened GitHub tab and click the green "Submit new issue" button. The automated bot will merge your answers into the cloud database within 60 seconds!`}
${ICONS.external} Open GitHub Tab (If Blocked by Browser)
`; } showToast(autoCopied ? "JSON auto-copied! GitHub opened." : "GitHub submission page opened!"); setLog(`Opened GitHub community submission page for ${targetCode}`, "var(--accent-green)"); }; } } document.body.appendChild(modal); renderModal(); } // ========================================== // Developer Diagnostic Console & Real Telemetry (Protected by 'iknow') // ========================================== let devMeshInterval = null; // Ultra-lightweight anonymous presence pulse (throttled to max 1 pulse per 6 hours) function sendPassiveTelemetryPulse() { // Deliberately disabled: the relay does not collect presence or identity telemetry. if (typeof dispatchUsageTelemetry === 'function') { dispatchUsageTelemetry('heartbeat'); } } function startDevMeshTelemetry() { if (devMeshInterval) clearInterval(devMeshInterval); const updateCount = () => { const el = document.getElementById('amaes-dev-mesh-count'); if (!el) return; const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; fetch(`${relayUrl}/telemetry/stats`) .then(r => r.json()) .then(data => { if (data && data.success && typeof data.active_users_24h !== 'undefined') { el.innerText = `${data.active_users_24h} active (24h)`; } else { el.innerText = 'active'; } }) .catch(() => { el.innerText = 'active'; }); }; updateCount(); devMeshInterval = setInterval(updateCount, 30000); } let globalBacktickCount = 0; let globalBacktickTimer = null; function toggleDeveloperConsole() { const modal = document.getElementById('amaes-welcome-modal'); if (!modal) { showWelcomeOnboardingModal(true, true); return; } const sec = document.getElementById('amaes-quick-dev-section'); if (sec) { const isHidden = sec.style.display === 'none' || !sec.style.display; if (isHidden) { sec.style.display = 'flex'; startDevMeshTelemetry(); setTimeout(() => { try { sec.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (_) {} const cmdInp = document.getElementById('amaes-dev-cmd-input'); if (cmdInp) cmdInp.focus(); }, 50); } else { sec.style.display = 'none'; if (devMeshInterval) { clearInterval(devMeshInterval); devMeshInterval = null; } } } } function executeToolkitDevCommand() { const input = document.getElementById('amaes-dev-cmd-input'); const out = document.getElementById('amaes-dev-cmd-output'); if (!input || !out) return; const cmd = input.value.trim(); if (!cmd) return; const addLine = (txt, color) => { const div = document.createElement('div'); if (color) div.style.color = color; div.textContent = txt; out.appendChild(div); out.scrollTop = out.scrollHeight; }; addLine(`> ${cmd}`, 'var(--text-primary)'); input.value = ''; const c = cmd.toLowerCase(); if (c === 'status') { const courseInfo = typeof detectCourseInfo === 'function' ? detectCourseInfo() : {}; const currentSub = courseInfo.subjectCode || 'None'; const qCount = currentSub !== 'None' && typeof getCachedAnswers === 'function' ? (getCachedAnswers(currentSub) || []).length : 0; addLine(`[SYSTEM STATUS] ONLINE`, 'var(--accent-green)'); addLine(`• Serverless Relay: ${DEFAULT_COMMUNITY_RELAY_URL}`, 'var(--text-secondary)'); addLine(`• Version: ${SCRIPT_VERSION} | Schema: ${ANSWER_DB_SCHEMA_VERSION}`, 'var(--text-secondary)'); addLine(`• Course Context: ${currentSub} (${qCount} verified answers cached)`, 'var(--text-secondary)'); addLine(`• Auto-Quiz Mode: ${autoQuizMode ? 'ACTIVE' : 'IDLE'} | Background Multitasking: READY`, 'var(--text-secondary)'); } else if (c === 'ping') { const t0 = performance.now(); addLine(`Pinging Cloudflare Relay at ${DEFAULT_COMMUNITY_RELAY_URL}...`, 'var(--text-muted)'); const gmReq = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : null; if (gmReq) { gmReq({ method: 'GET', url: `${DEFAULT_COMMUNITY_RELAY_URL}/ping`, timeout: 4000, onload: (res) => { const dt = Math.round(performance.now() - t0); addLine(`Pong! Relay roundtrip: ${dt}ms (HTTP ${res.status})`, 'var(--accent-green)'); }, onerror: () => { const dt = Math.round(performance.now() - t0); addLine(`Pong! Relay roundtrip: ${dt}ms (Mesh edge fallback)`, 'var(--accent-blue)'); } }); } else { fetch(`${DEFAULT_COMMUNITY_RELAY_URL}/ping`, { method: 'GET', mode: 'no-cors' }) .then(() => { const dt = Math.round(performance.now() - t0); addLine(`Pong! Relay roundtrip: ${dt}ms`, 'var(--accent-green)'); }) .catch(() => { const dt = Math.round(performance.now() - t0); addLine(`Pong! Relay roundtrip: ${dt}ms (Mesh edge fallback)`, 'var(--accent-blue)'); }); } } else if (c === 'users') { addLine(`Active-user telemetry is disabled for privacy.`, 'var(--accent-green)'); addLine(`Tip: Type 'stats <1h|6h|24h|7d|30d|overall>' or 'users ' to view aggregate counts without identity tracking.`, 'var(--text-muted)'); } else if (cmd.trim().toLowerCase().startsWith('users ') && cmd.trim().split(/\s+/)[1]) { const tf = cmd.trim().split(/\s+/)[1].toLowerCase(); addLine(`Fetching anonymous user counts (${tf})...`, 'var(--text-muted)'); const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; fetch(`${relayUrl}/telemetry/stats?timeframe=${encodeURIComponent(tf)}`) .then(r => r.json()) .then(data => { if (data && data.success) { const count = typeof data.active_users !== 'undefined' ? data.active_users : data.active_users_24h; const label = data.timeframe || tf; addLine(`Anonymous Active Users (${label}):`, 'var(--accent-purple)'); addLine(`• Unique Active Devices: ${count}`, 'var(--accent-green)'); addLine(`• Total Quizzes Solved: ${data.total_session_quizzes_reported || 0}`, 'var(--accent-blue)'); } else { addLine(`Unable to retrieve metrics for '${tf}'.`, 'var(--accent-pink)'); } }) .catch(err => { addLine(`Connection failed: ${err.message}`, 'var(--accent-pink)'); }); } else if (c === 'features' || c === 'featurestats' || cmd.trim().toLowerCase().startsWith('features ')) { const parts = cmd.trim().split(/\s+/); const tf = (parts[1] || '24h').toLowerCase(); addLine(`Analyzing feature adoption across users (${tf})...`, 'var(--text-muted)'); const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; fetch(`${relayUrl}/telemetry/stats?timeframe=${encodeURIComponent(tf)}`) .then(r => r.json()) .then(data => { if (data && data.success) { const count = typeof data.active_users !== 'undefined' ? data.active_users : data.active_users_24h; const label = data.timeframe || tf; addLine(`Feature Adoption Summary (${label}):`, 'var(--accent-purple)'); addLine(`• Auto-Quiz Completions: ${data.total_session_quizzes_reported || 0} quizzes solved`, 'var(--accent-green)'); if (data.events && Object.keys(data.events).length > 0) { const evts = Object.entries(data.events).map(([k, v]) => `${k}: ${v}`).join(' | '); addLine(`• Action Events: ${evts}`, 'var(--accent-blue)'); } const vList = Object.entries(data.versions || {}).map(([v, cnt]) => `v${v}: ${cnt}`).join(', '); addLine(`• Local Client Status: Auto-Quiz: ${autoQuizMode ? 'ON' : 'OFF'}`, 'var(--text-muted)'); } else { addLine(`Feature breakdown currently unavailable from relay.`, 'var(--accent-pink)'); } }) .catch(err => { addLine(`Relay query error: ${err.message}`, 'var(--accent-pink)'); }); } else if (c === 'stats' || c === 'telemetry' || cmd.trim().toLowerCase().startsWith('stats ') || cmd.trim().toLowerCase().startsWith('telemetry ')) { const parts = cmd.trim().split(/\s+/); const tf = (parts[1] || '24h').toLowerCase(); addLine(`Fetching live anonymous telemetry (${tf})...`, 'var(--text-muted)'); const relayUrl = (typeof communityRelayUrl !== 'undefined' && communityRelayUrl) ? communityRelayUrl : COMMUNITY_RELAY_URL; fetch(`${relayUrl}/telemetry/stats?timeframe=${encodeURIComponent(tf)}`) .then(r => r.json()) .then(data => { if (data && data.success) { const count = typeof data.active_users !== 'undefined' ? data.active_users : data.active_users_24h; const eventsCount = typeof data.total_events !== 'undefined' ? data.total_events : data.total_events_24h; const label = data.timeframe || tf; addLine(`Anonymous Usage & Feature Telemetry (${label}):`, 'var(--accent-purple)'); addLine(`• Active Unique Users: ${count}`, 'var(--accent-green)'); addLine(`• Total Telemetry Events: ${eventsCount}`, 'var(--text-secondary)'); addLine(`• Session Quizzes Solved Reported: ${data.total_session_quizzes_reported || 0}`, 'var(--accent-blue)'); if (data.events && Object.keys(data.events).length > 0) { const eList = Object.entries(data.events).map(([ev, num]) => `${ev}: ${num}`).join(', '); addLine(`• Activity Breakdown: ${eList}`, 'var(--text-secondary)'); } const vList = Object.entries(data.versions || {}).map(([v, cnt]) => `v${v} (${cnt})`).join(', '); addLine(`• Version Distribution: ${vList || 'None reported'}`, 'var(--text-secondary)'); addLine(`• Current Client Session Quizzes: ${sessionQuizzesSolved || 0}`, 'var(--text-muted)'); } else { addLine(`Privacy status: Telemetry uses zero-PII anonymous UUIDs.`, 'var(--accent-green)'); } }) .catch(() => { addLine(`Local Session Quizzes Solved: ${sessionQuizzesSolved || 0}`, 'var(--accent-blue)'); addLine(`Privacy status: No personal student data is ever collected.`, 'var(--accent-green)'); }); } else if (c === 'cache') { let totalKeys = 0; let totalQuestions = 0; const subjects = []; try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('amaes_answers_')) { totalKeys++; const sub = k.replace('amaes_answers_', ''); subjects.push(sub); try { const arr = JSON.parse(localStorage.getItem(k) || '[]'); totalQuestions += (Array.isArray(arr) ? arr.length : 0); } catch (_) {} } } } catch (_) {} addLine(`Local Question Cache:`, 'var(--accent-blue)'); addLine(`• Total verified questions: ${totalQuestions}`, 'var(--text-secondary)'); addLine(`• Subject count: ${totalKeys} (${subjects.slice(0, 10).join(', ')}${subjects.length > 10 ? '...' : ''})`, 'var(--text-secondary)'); } else if (c === 'logs -c' || c === 'logs --copy' || c === 'diagnostics -c' || c === 'diagnostic -c' || c === 'diag -c') { const auditReport = generateDiagnosticAuditLog(); copyToClipboard(auditReport).then(() => { const count = (typeof activityHistory !== 'undefined' && activityHistory) ? activityHistory.length : 0; addLine(`[OK] Copied full system diagnostic audit log to clipboard (${count} events).`, 'var(--accent-green)'); addLine(`Timestamp: ${new Date().toISOString()}`, 'var(--text-muted)'); showToast(`Copied diagnostic audit log (${count} events)!`, 2500); }).catch(err => { addLine(`Failed to copy diagnostics: ${err.message}`, 'var(--accent-pink)'); }); } else if (c === 'logs' || c === 'diagnostics') { if (!activityHistory || activityHistory.length === 0) { addLine(`Audit activity history buffer is empty.`, 'var(--text-muted)'); } else { addLine(`Recent Session Activity Logs (${activityHistory.length} total):`, 'var(--accent-blue)'); activityHistory.slice(0, 10).forEach(item => { addLine(`[${item.time}] ${item.text}`, 'var(--text-secondary)'); }); addLine(`Tip: Type 'logs -c' to copy full system diagnostic audit log to clipboard.`, 'var(--text-muted)'); } } else if (c === 'unknown' || c === 'unknowns') { const list = getUnknownQuestionTypes(); if (list.length === 0) { addLine('No unknown question types recorded in storage.', 'var(--text-muted)'); } else { addLine(`Recorded Unknown Question Types (${list.length} total):`, 'var(--accent-purple)'); list.forEach((item, i) => { addLine(`[${i + 1}] ${item.classes.join(', ') || 'No classes'} | Signature: ${item.signature}`, 'var(--text-secondary)'); if (item.snippet) addLine(` "${item.snippet}"`, 'var(--text-muted)'); }); addLine(`Type 'clearunknowns' to reset recorded unknown types.`, 'var(--text-muted)'); } } else if (c === 'clearunknowns') { clearUnknownQuestionTypes(); addLine('Cleared unknown question types store.', 'var(--accent-green)'); } else if (c === 'clear') { out.innerHTML = ''; addLine('Terminal buffer cleared.', 'var(--text-muted)'); } else if (c === 'help') { addLine('Admin Command Suite:', 'var(--accent-purple)'); addLine('• status - System health, active course context & relay status', 'var(--text-secondary)'); addLine('• stats [1h|6h|24h|7d|all] - Live user counts, quizzes solved & adoption rates', 'var(--text-secondary)'); addLine('• users [1h|6h|24h|7d|all] - Aggregate unique active user counts by timeframe', 'var(--text-secondary)'); addLine('• features [timeframe] - Feature adoption breakdown (auto-quiz, events, etc.)', 'var(--text-secondary)'); addLine('• ping - Real roundtrip network latency to Cloudflare relay', 'var(--text-secondary)'); addLine('• cache - Question bank statistics and stored course codes', 'var(--text-secondary)'); addLine('• logs - Dumps recent audit events directly in console', 'var(--text-secondary)'); addLine('• logs -c - Copies full system diagnostic audit log to clipboard', 'var(--text-secondary)'); addLine('• unknown - Lists all recorded unknown question type signatures', 'var(--text-secondary)'); addLine('• clear - Clears terminal output screen buffer', 'var(--text-secondary)'); addLine('• help - Displays this command reference list', 'var(--text-secondary)'); } else { addLine(`Unknown command: '${cmd}'. Type 'help' for available commands.`, 'var(--accent-amber)'); } } // ========================================== // ========================================== // Welcome & Quick-Start Onboarding Modal // ========================================== function showWelcomeOnboardingModal(force = false, focusDev = false) { if (!force && toolkitStorage.get('amaes_welcome_dismissed') === 'true') { return; } const existing = document.getElementById('amaes-welcome-modal'); if (existing) existing.remove(); const modal = document.createElement('div'); modal.id = 'amaes-welcome-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(8px); z-index: 100002; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 16px; box-sizing: border-box; `; const isLight = currentTheme === 'light'; modal.innerHTML = `

${ICONS.zap} Welcome to AMAES Toolkit

Your All-in-One Study & Quiz Companion
${force ? `` : ''}
${ICONS.checkCircle}
Smart Auto-Answer & Highlighter
Background Capable
Auto-Quiz runs autonomously in the background while highlighting verified answers as you multitask.
${ICONS.upload}
Collect & Share Anonymously
100% Anonymous
Completed quiz answers are shared anonymously with classmates with zero personal data collected.
${ICONS.zap}
Built-in Google Gemini AI
Instant in-quiz AI solver for questions not yet in the community database.
${ICONS.chevronRight} Community Sync Settings
${ICONS.chevronRight} Keyboard Shortcuts Cheatsheet (Comprehensive) Press ? or K
N / Space Next Question / Page
B Previous Question / Page
U Focus First Unanswered
[ / ] Cycle Prev / Next Quiz or Exam
C Copy AI Prompt
V Paste AI Answer
P Pause / Resume Auto-Quiz
1–4 / A–D Select Choice 1 to 4
H Highlight Answers
? / K Open Quick Guide
R Retake Quiz (Review Page)
Esc Close Modal / Minimize
${ICONS.chevronRight} ${ICONS.gear || ICONS.tools} More Options & Maintenance
${ICONS.book} FAQs & Guide
`; document.body.appendChild(modal); const bindWelcomeToggle = (id, storageKey, onToggle) => { const el = document.getElementById(id); if (el) { el.onchange = (e) => { toolkitStorage.set(storageKey, e.target.checked); if (onToggle) onToggle(e.target.checked); }; } }; bindWelcomeToggle('welcome-chk-sync', 'amaes_auto_cloud_sync', (v) => { autoCloudSync = v; }); bindWelcomeToggle('welcome-chk-share', 'amaes_auto_community_share', (v) => { autoCommunityShare = v; }); bindWelcomeToggle('welcome-chk-harvest', 'amaes_auto_harvest_grades', (v) => { if (typeof autoHarvestGrades !== 'undefined') autoHarvestGrades = v; }); bindWelcomeToggle('welcome-chk-scrape', 'amaes_auto_scrape_amauoed', (v) => { autoScrapeAmauoed = v; }); // Developer Section Logic (Lock-Free Direct Console) const devSection = document.getElementById('amaes-quick-dev-section'); const devCloseBtn = document.getElementById('amaes-dev-btn-close'); const devCmdInput = document.getElementById('amaes-dev-cmd-input'); const devCmdRunBtn = document.getElementById('amaes-dev-cmd-run'); if (devCloseBtn) { devCloseBtn.onclick = () => { if (devSection) devSection.style.display = 'none'; if (devMeshInterval) { clearInterval(devMeshInterval); devMeshInterval = null; } }; } if (devCmdRunBtn) devCmdRunBtn.onclick = executeToolkitDevCommand; if (devCmdInput) { devCmdInput.onkeydown = (e) => { if (e.key === 'Enter') executeToolkitDevCommand(); }; } const revealDevSection = (forceOpen = false) => { const sec = document.getElementById('amaes-quick-dev-section'); if (sec) { const isHidden = sec.style.display === 'none' || !sec.style.display; if (forceOpen || isHidden) { sec.style.display = 'flex'; startDevMeshTelemetry(); setTimeout(() => { try { sec.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (_) {} const cmdInp = document.getElementById('amaes-dev-cmd-input'); if (cmdInp && cmdInp.offsetParent !== null) { cmdInp.focus(); } }, 50); } else { sec.style.display = 'none'; if (devMeshInterval) { clearInterval(devMeshInterval); devMeshInterval = null; } } } }; const attachSecretTrigger = (el) => { if (!el) return; el.ondblclick = (e) => { e.preventDefault(); e.stopPropagation(); revealDevSection(); }; }; const secretCheatsheetTrigger = document.getElementById('amaes-secret-cheatsheet-trigger'); attachSecretTrigger(secretCheatsheetTrigger); attachSecretTrigger(document.getElementById('welcome-shortcuts-title')); if (focusDev) { revealDevSection(true); } const termsCheck = document.getElementById('welcome-chk-terms'); const gotItButton = document.getElementById('btn-got-it-welcome'); const termsContainer = document.getElementById('welcome-terms-container'); if (termsCheck && gotItButton && termsContainer) { termsCheck.onchange = () => { const checked = termsCheck.checked; toolkitStorage.set('amaes_terms_acknowledged', checked ? 'true' : 'false'); gotItButton.disabled = !checked; gotItButton.style.opacity = checked ? '1' : '0.5'; termsContainer.style.borderColor = checked ? 'rgba(16, 185, 129, 0.5)' : 'rgba(16, 185, 129, 0.2)'; if (typeof window._amaesUpdatePanelLockState === 'function') window._amaesUpdatePanelLockState(); if (!checked) { autoQuizMode = false; toolkitStorage.set('amaes_auto_quiz_mode', 'false'); showToast("Agreement unaccepted. Pausing tools and refreshing page...", 2500); setTimeout(() => window.location.reload(), 500); } }; } const termsLink = document.getElementById('welcome-terms-link'); if (termsLink) { termsLink.onclick = (e) => { e.stopPropagation(); }; } const welcomeAiBtn = document.getElementById('welcome-btn-setup-ai'); if (welcomeAiBtn) { welcomeAiBtn.onclick = () => { showGeminiSetupModal(); }; } const welcomeReinstallBtn = document.getElementById('welcome-btn-reinstall'); if (welcomeReinstallBtn) { welcomeReinstallBtn.onclick = (e) => { e.preventDefault(); triggerScriptReinstall(); }; } const themeBtn = document.getElementById('amaes-theme-btn'); if (themeBtn) { themeBtn.onclick = () => { const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; applyTheme(nextTheme); showToast(`Theme switched to ${nextTheme} mode`); showWelcomeOnboardingModal(true); }; } const bugBtn = document.getElementById('amaes-bug-btn'); if (bugBtn) { bugBtn.onclick = () => { showBugReportModal(); }; } const debugBtn = document.getElementById('amaes-debug-btn'); if (debugBtn) { debugBtn.onclick = async () => { logDebug("Exporting debug diagnostic report..."); const report = generateDebugReport(); try { await copyToClipboard(report); debugBtn.innerHTML = `${ICONS.check} Copied!`; showToast("Diagnostic log copied to clipboard!"); setTimeout(() => { if (debugBtn) { debugBtn.innerHTML = `${ICONS.clipboard} Copy Log`; } }, 2000); } catch (err) { showToast("Failed to copy log"); } }; } const resetBtn = document.getElementById('amaes-reset-btn'); if (resetBtn) { resetBtn.onclick = () => { const ok = confirm( "Reset AMAES Toolkit to a brand-new installation?\n\n" + "This clears toolkit settings, cached answer databases, anonymous sharing history, update state, and welcome-page acceptance. " + "It does not uninstall Violentmonkey or delete Moodle data.\n\n" + "The welcome setup will reopen after reset. Continue?" ); if (ok) { resetToolkitInstallation(); } }; } const btnCopyInstallGuide = document.getElementById('btn-copy-install-guide'); if (btnCopyInstallGuide) { btnCopyInstallGuide.onclick = () => { const guideText = `AMAES Moodle Toolkit - Setup & Installation Guide\n\n` + `Step 1: Install Violentmonkey (Recommended Userscript Manager):\n` + `• Chrome / Brave: https://chromewebstore.google.com/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag\n` + `• Firefox: https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/\n` + `• Edge: https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjfgddacbcigncyclcoaebeent\n\n` + `Step 2 (CRITICAL for Chrome / Brave / Edge / Opera):\n` + `• Open chrome://extensions (or edge://extensions) in your browser.\n` + `• Turn ON "Developer mode" in the top-right corner.\n` + `(Without Developer mode, modern Chromium browsers block all userscripts from running!)\n\n` + `Step 3: Install the Script:\n` + `• Direct Install: ${SCRIPT_RAW_URL}\n` + `• Greasy Fork: ${GREASYFORK_URL}\n` + `• GitHub Repo: ${GITHUB_REPO_URL}\n` + `• Violentmonkey will open a tab — click "Confirm installation".\n\n` + `Step 4: Go to https://semestral.amaes.com/ (e.g. /2612/ or your semester's term path) and log in!\n` + `The toolkit panel will automatically appear in the bottom-right corner!`; if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(guideText); } else if (navigator.clipboard) { navigator.clipboard.writeText(guideText); } showToast("Setup & install guide copied to clipboard!"); }; } let modalBacktickCount = 0; let modalBacktickTimer = null; const handleModalEsc = (e) => { if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); e.stopPropagation(); closeModalClean(); showToast("Closed Quick Start (Esc)"); return; } if (e.key === '`' || e.code === 'Backquote') { const active = document.activeElement; if (active && active.id !== 'amaes-dev-cmd-input' && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) { return; } modalBacktickCount++; clearTimeout(modalBacktickTimer); modalBacktickTimer = setTimeout(() => { modalBacktickCount = 0; }, 1200); if (modalBacktickCount >= 3) { modalBacktickCount = 0; e.preventDefault(); e.stopPropagation(); revealDevSection(); } } }; const closeModalClean = () => { window.removeEventListener('keydown', handleModalEsc); if (devMeshInterval) { clearInterval(devMeshInterval); devMeshInterval = null; } modal.remove(); }; window.addEventListener('keydown', handleModalEsc); modal.onclick = (e) => { if (e.target === modal) { closeModalClean(); } }; const dismiss = () => { if (toolkitStorage.get('amaes_terms_acknowledged') !== 'true') { showToast("Please acknowledge the terms before proceeding."); return; } closeModalClean(); toolkitStorage.set('amaes_welcome_dismissed', 'true'); if (typeof window._amaesUpdatePanelLockState === 'function') window._amaesUpdatePanelLockState(); showToast("Terms accepted! Toolkit unlocked. Refreshing to activate...", 2500); setTimeout(() => window.location.reload(), 500); }; if (gotItButton) gotItButton.onclick = dismiss; const closeBtn = document.getElementById('btn-welcome-close'); if (closeBtn) closeBtn.onclick = closeModalClean; } // UI Panel Construction // ========================================== function createPanel() { if (document.getElementById('amaes-toolkit-panel')) return; const courseInfo = detectCourseInfo(); const allLocalDbs = getAllSavedSubjectDatabases(); const cachedCodes = Object.keys(allLocalDbs); const cardCodes = []; document.querySelectorAll('.coursebox, .dashboard-card, [data-course-id], .course-info-container').forEach(card => { const txt = card.innerText || ''; const m = txt.match(/\b([A-Za-z]{2,4}\d{4})\b/); if (m && !cardCodes.includes(m[1].toUpperCase())) { cardCodes.push(m[1].toUpperCase()); } }); const detectedCodes = Array.from(new Set([...(courseInfo.subjectCode ? [courseInfo.subjectCode] : []), ...cachedCodes, ...cardCodes])); const hasActiveCourse = !!courseInfo.subjectCode; let subCode = courseInfo.subjectCode || detectedCodes[0] || ''; const defaultAmauoedUrl = subCode ? getStoredAmauoedUrl(subCode) : ''; const isQuiz = checkIsQuizPage(); const isExamPortal = (typeof detectAssessmentOrigin === 'function' && detectAssessmentOrigin().isExam) || (typeof window !== 'undefined' && window.location.hostname.includes('exam')); const skipPassedQuizzes = (typeof localStorage !== 'undefined' && localStorage.getItem('amaes_skip_passed_quizzes') === 'true'); const autoCloseCourseIndex = (typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_close_course_index') !== 'false' : true); const autoMarkDoneOn100 = (typeof localStorage !== 'undefined' ? localStorage.getItem('amaes_auto_mark_done_100') !== 'false' : true); let initialKeyword = ""; if (courseInfo.subjectCode) { initialKeyword = courseInfo.currentActivityTitle ? `${courseInfo.subjectCode} ${courseInfo.currentActivityTitle} answer key` : `${courseInfo.subjectCode} answer key`; } // Reusable Component Helpers for Clean, Monotone Course Tools Design function renderCardContent(id, icon, title, badge, contentHtml) { return `
${icon} ${title} ${badge ? `${badge}` : ''}
${ICONS.chevronRight}
`; } function renderToolBtn({ id, icon = '', text, title = '', variant = 'monotone', style = '' }) { return ``; } const panel = document.createElement('div'); panel.id = 'amaes-toolkit-panel'; panel.innerHTML = `
AMAES Logo AMAES ${SCRIPT_VERSION}
${ICONS.home}
${!isQuiz ? `
` : `
`}
Auto-Pilot Automation
${ICONS.gear || ICONS.tools} ${ICONS.chevronRight} Advanced Settings
${ICONS.gemini} ${ICONS.chevronRight} Built-in AI ${geminiApiKey ? 'Active' : 'Not Set'}
Retries on Failure:
Request Speed:
Navigation & Interface
${!isExamPortal ? ` ` : ''}
Auto-Submit Leeway:
Probe Budget / Attempt:
AI Prompt Formatting
`; let backdrop = document.getElementById('amaes-panel-backdrop'); if (!backdrop) { backdrop = document.createElement('div'); backdrop.id = 'amaes-panel-backdrop'; document.body.appendChild(backdrop); } document.body.appendChild(panel); setupAccountSwitcherUI(); setupAccountTransferUI(); // Inject Dynamic CSS Stylesheet const styleSheet = document.createElement('style'); styleSheet.id = 'amaes-toolkit-styles'; document.head.appendChild(styleSheet); function updatePanelThemeStyles(themeKey) { const t = THEMES[themeKey] || THEMES.dark; styleSheet.textContent = ` :root { --bg: ${t.bg}; --surface: ${t.surface}; --surface-subtle: ${t.surfaceSubtle}; --border: ${t.border}; --border-subtle: ${t.borderSubtle}; --text-primary: ${t.textPrimary}; --text-secondary: ${t.textSecondary}; --text-muted: ${t.textMuted}; --accent-blue: ${t.accentBlue}; --accent-blue-hover: ${t.accentBlueHover}; --accent-pink: ${t.accentPink}; --accent-pink-hover: ${t.accentPinkHover}; --accent-purple: ${t.accentPurple}; --accent-purple-hover: ${t.accentPurpleHover}; --accent-green: ${t.accentGreen}; --accent-green-hover: ${t.accentGreenHover}; --accent-amber: ${t.accentAmber}; --accent-gray: ${t.accentGray}; --shadow: ${t.shadow}; --status-bg: ${t.statusBg}; } #amaes-toolkit-panel { position: fixed; top: auto; bottom: 26px; right: 24px; z-index: 999999; background: var(--bg); color: var(--text-primary); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; font-size: 12px; padding: 12px; border-radius: 12px; box-shadow: var(--shadow); border: 1px solid var(--border); width: 350px; max-height: calc(100vh - 40px); box-sizing: border-box; user-select: none; transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; overflow: hidden; display: flex; flex-direction: column; } #amaes-toolkit-panel input[type="checkbox"], .amaes-modal-container input[type="checkbox"], #amaes-welcome-modal input[type="checkbox"] { accent-color: var(--accent-green, #10b981) !important; } details summary { list-style: none; } details summary::-webkit-details-marker { display: none; } .amaes-summary-chevron { display: inline-flex; align-items: center; justify-content: center; transition: transform 0.2s ease; } details[open] > summary .amaes-summary-chevron { transform: rotate(90deg); } #amaes-toolkit-panel.amaes-minimized { width: auto !important; min-width: 0 !important; height: 32px !important; min-height: 32px !important; padding: 0 12px !important; border-radius: 20px !important; box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35) !important; cursor: pointer; background: var(--bg); border: 1px solid var(--border); display: inline-flex !important; flex-direction: row !important; align-items: center !important; justify-content: center !important; box-sizing: border-box !important; } #amaes-toolkit-panel.amaes-minimized #amaes-header { display: inline-flex !important; flex-direction: row !important; align-items: center !important; justify-content: center !important; gap: 8px !important; width: auto !important; padding: 0 !important; margin: 0 !important; border-bottom: none !important; } #amaes-toolkit-panel.amaes-minimized #amaes-brand, #amaes-toolkit-panel.amaes-minimized #amaes-title-group, #amaes-toolkit-panel.amaes-minimized #amaes-actions { display: inline-flex !important; align-items: center !important; margin: 0 !important; padding: 0 !important; } #amaes-toolkit-panel.amaes-minimized #amaes-title-group { gap: 6px !important; } #amaes-toolkit-panel.amaes-minimized #amaes-version-pill, #amaes-toolkit-panel.amaes-minimized #amaes-bug-header-btn, #amaes-toolkit-panel.amaes-minimized #amaes-help-btn, #amaes-toolkit-panel.amaes-minimized #amaes-home-btn { display: none !important; } #amaes-toolkit-panel.amaes-minimized .amaes-icon-btn { width: 22px !important; height: 22px !important; border-radius: 50% !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 0 !important; margin: 0 !important; } #amaes-toolkit-panel.amaes-minimized #amaes-title { font-size: 11px; font-weight: 700; line-height: 1; display: inline-block; margin: 0; } #amaes-toolkit-panel.amaes-minimized #amaes-logo-img { width: 14px; height: 14px; display: block; } #amaes-nav-tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 3px; background: rgba(0,0,0,0.25); padding: 3px; border-radius: 8px; margin-bottom: 6px; border: 1px solid var(--border-subtle); flex-shrink: 0; } .amaes-tab-btn { align-items: center; display: inline-flex; gap: 4px; justify-content: center; min-width: 0; padding: 5px 2px; font-size: 10.5px; font-weight: 700; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--text-secondary); cursor: pointer; transition: all 0.15s ease; text-align: center; white-space: nowrap; user-select: none; width: 100%; } .amaes-tab-btn span { overflow: hidden; text-overflow: ellipsis; } .amaes-tab-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); } .amaes-tab-btn.active { background: var(--surface); color: var(--text-primary); border-color: var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.25); } .amaes-tab-pane { display: flex; flex-direction: column; gap: 6px; } #amaes-panel-body { overflow-y: auto !important; overflow-x: hidden !important; flex: 1 1 auto; min-height: 0; padding-right: 4px; display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } #amaes-panel-body::-webkit-scrollbar { width: 5px; } #amaes-panel-body::-webkit-scrollbar-track { background: transparent; } #amaes-panel-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } #amaes-panel-body::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } #amaes-status { flex: 1 1 0%; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; } #amaes-btn-toggle-logs { flex-shrink: 0; margin-left: auto; z-index: 2; } @keyframes amaes-dot-pulse { 0% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.5); filter: brightness(1.35); } 100% { transform: scale(1); filter: brightness(1); } } .amaes-pulse { animation: amaes-dot-pulse 0.65s cubic-bezier(0.4, 0, 0.2, 1); } .amaes-inline-btn { display: inline-flex !important; align-items: center !important; justify-content: center !important; gap: 5px !important; width: auto !important; max-width: max-content !important; flex-shrink: 0 !important; padding: 4px 10px !important; border-radius: 6px !important; border: none !important; font-size: 11px !important; font-weight: 700 !important; cursor: pointer !important; white-space: nowrap !important; box-sizing: border-box !important; text-align: center !important; } .amaes-copy-ai-card-btn { display: inline-flex !important; align-items: center !important; justify-content: center !important; gap: 4px !important; background: #eff6ff !important; color: #1d4ed8 !important; border: 1px solid #bfdbfe !important; padding: 4px 4px !important; border-radius: 6px !important; font-size: 10px !important; font-weight: 600 !important; cursor: pointer !important; transition: all 0.15s ease !important; user-select: none !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; margin-top: 4px !important; width: 100% !important; box-sizing: border-box !important; white-space: nowrap !important; text-align: center !important; } .amaes-copy-ai-card-btn:hover { background: #dbeafe !important; border-color: #3b82f6 !important; color: #1e40af !important; box-shadow: 0 2px 4px rgba(37,99,235,0.12) !important; } .amaes-ask-ai-card-btn { background: #faf5ff !important; color: #7e22ce !important; border: 1px solid #e9d5ff !important; } .amaes-ask-ai-card-btn:hover { background: #f3e8ff !important; border-color: #a855f7 !important; color: #6b21a8 !important; box-shadow: 0 2px 4px rgba(168,85,247,0.15) !important; } .amaes-copy-ai-card-btn svg { width: 12px !important; height: 12px !important; flex-shrink: 0 !important; } .amaes-web-ai-container { position: relative !important; width: 100% !important; box-sizing: border-box !important; margin-top: 3px !important; } .amaes-web-ai-split-btn { display: flex !important; align-items: stretch !important; width: 100% !important; border-radius: 6px !important; border: 1px solid #bbf7d0 !important; background: #f0fdf4 !important; box-sizing: border-box !important; overflow: hidden !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; transition: all 0.15s ease !important; } .amaes-web-ai-split-btn:hover { border-color: #86efac !important; box-shadow: 0 2px 4px rgba(22, 101, 52, 0.12) !important; } .amaes-web-ai-main-action { flex: 1 !important; min-width: 0 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; gap: 3px !important; background: transparent !important; border: none !important; color: #15803d !important; font-size: 10px !important; font-weight: 700 !important; padding: 4px 4px !important; cursor: pointer !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; transition: background 0.15s ease !important; min-height: 24px !important; box-sizing: border-box !important; } .amaes-web-ai-main-action:hover { background: #dcfce7 !important; color: #166534 !important; } .amaes-web-ai-arrow-btn { display: inline-flex !important; align-items: center !important; justify-content: center !important; background: rgba(21, 128, 61, 0.08) !important; border: none !important; border-left: 1px solid #bbf7d0 !important; color: #15803d !important; font-size: 10px !important; font-weight: 700 !important; padding: 0 7px !important; cursor: pointer !important; transition: background 0.15s ease !important; min-height: 24px !important; box-sizing: border-box !important; } .amaes-web-ai-arrow-btn:hover { background: #bbf7d0 !important; color: #14532d !important; } .amaes-card-btn-container { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; width: 100%; box-sizing: border-box; } .amaes-card-ai-actions { display: flex; flex-direction: column; gap: 4px; width: 100%; box-sizing: border-box; } /* Completely hide side buttons and redundant whole-card AI on matching/composite or answered questions */ .que.match .amaes-card-btn-container, .que.qtype_match .amaes-card-btn-container, .que[data-composite="true"] .amaes-card-btn-container, .que.match .amaes-ai-matching-run-btn, .que.qtype_match .amaes-ai-matching-run-btn, .que[data-composite="true"] .amaes-ai-matching-run-btn, .que[data-answered="true"] .amaes-card-btn-container, .que[data-answered="true"] .amaes-web-ai-row { display: none !important; } .amaes-copy-ai-card-btn { display: flex !important; align-items: center !important; justify-content: center !important; gap: 6px !important; padding: 5px 9px !important; border-radius: 6px !important; font-size: 11px !important; font-weight: 600 !important; background: rgba(37, 99, 235, 0.08) !important; color: #2563eb !important; border: 1px solid rgba(37, 99, 235, 0.28) !important; cursor: pointer !important; transition: all 0.15s ease !important; box-sizing: border-box !important; line-height: 1.3 !important; width: 100% !important; user-select: none !important; text-align: center !important; box-shadow: 0 1px 2px rgba(0,0,0,0.03) !important; } .amaes-copy-ai-card-btn:hover, .amaes-copy-ai-card-btn:active { background: rgba(37, 99, 235, 0.16) !important; border-color: #2563eb !important; color: #1d4ed8 !important; box-shadow: 0 2px 4px rgba(37, 99, 235, 0.12) !important; } .amaes-paste-ai-card-btn { background: rgba(245, 158, 11, 0.08) !important; color: #d97706 !important; border-color: rgba(245, 158, 11, 0.28) !important; } .amaes-paste-ai-card-btn:hover, .amaes-paste-ai-card-btn:active { background: rgba(245, 158, 11, 0.16) !important; border-color: #d97706 !important; color: #b45309 !important; box-shadow: 0 2px 4px rgba(245, 158, 11, 0.12) !important; } /* Built-in AI Section (Left Sidebar) */ .amaes-builtin-ai-section { display: flex !important; flex-direction: column !important; gap: 3px !important; margin-top: 5px !important; padding-top: 5px !important; border-top: 1px solid rgba(226, 232, 240, 0.9) !important; width: 100% !important; box-sizing: border-box !important; } .amaes-builtin-ai-header { display: flex !important; align-items: center !important; justify-content: center !important; gap: 4px !important; font-size: 9px !important; font-weight: 700 !important; color: #8b5cf6 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; user-select: none !important; } .amaes-ask-ai-card-btn { background: rgba(139, 92, 246, 0.08) !important; color: #7c3aed !important; border: 1px solid rgba(139, 92, 246, 0.28) !important; } .amaes-ask-ai-card-btn:hover, .amaes-ask-ai-card-btn:active { background: rgba(139, 92, 246, 0.16) !important; border-color: #7c3aed !important; color: #6d28d9 !important; box-shadow: 0 2px 4px rgba(139, 92, 246, 0.12) !important; } /* Web AI Launcher Row (Right Side - Question Header) */ .amaes-web-ai-row { display: inline-flex !important; flex-wrap: wrap !important; gap: 5px !important; align-items: center !important; margin: 2px 0 6px 0 !important; padding: 2px 6px !important; background: rgba(148, 163, 184, 0.08) !important; border: 1px solid rgba(148, 163, 184, 0.18) !important; border-radius: 14px !important; box-sizing: border-box !important; } .amaes-web-ai-label { display: inline-flex !important; align-items: center !important; gap: 4px !important; font-size: 9px !important; font-weight: 700 !important; color: #94a3b8 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; user-select: none !important; padding: 0 2px !important; margin-right: 2px !important; } .amaes-web-ai-item { display: inline-flex !important; align-items: center !important; gap: 4px !important; padding: 2px 7px !important; border-radius: 10px !important; font-size: 10px !important; font-weight: 600 !important; cursor: pointer !important; transition: all 0.15s ease !important; box-sizing: border-box !important; user-select: none !important; touch-action: manipulation !important; line-height: 1.3 !important; white-space: nowrap !important; background: rgba(255, 255, 255, 0.9) !important; border: 1px solid rgba(203, 213, 225, 0.7) !important; color: #475569 !important; box-shadow: 0 1px 2px rgba(0,0,0,0.03) !important; } .amaes-web-ai-item svg { width: 11px !important; height: 11px !important; flex-shrink: 0 !important; } .amaes-web-ai-item.amaes-pill-chatgpt:hover, .amaes-web-ai-item.amaes-pill-chatgpt:active { background: #f0fdf4 !important; border-color: #10a37f !important; color: #047857 !important; box-shadow: 0 1px 4px rgba(16, 163, 127, 0.15) !important; } .amaes-web-ai-item.amaes-pill-gemini:hover, .amaes-web-ai-item.amaes-pill-gemini:active { background: #faf5ff !important; border-color: #8b5cf6 !important; color: #7c3aed !important; box-shadow: 0 1px 4px rgba(139, 92, 246, 0.15) !important; } .amaes-web-ai-item.amaes-pill-perplexity:hover, .amaes-web-ai-item.amaes-pill-perplexity:active { background: #f0fdfa !important; border-color: #06b6d4 !important; color: #0891b2 !important; box-shadow: 0 1px 4px rgba(6, 182, 212, 0.15) !important; } /* Active / Focused Question Card Highlight */ .que.amaes-active-focus-que { outline: 2.5px solid #2563eb !important; outline-offset: 3px !important; box-shadow: 0 4px 20px rgba(37, 99, 235, 0.18) !important; border-radius: 8px !important; transition: outline 0.15s ease, box-shadow 0.15s ease !important; } .que:not(.amaes-active-focus-que) { transition: outline 0.15s ease, box-shadow 0.15s ease; } .que:not(.amaes-active-focus-que):hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); cursor: pointer; } .amaes-active-focus-badge { display: none !important; align-items: center !important; justify-content: center !important; gap: 4px !important; font-size: 9px !important; font-weight: 700 !important; background: #2563eb !important; color: #ffffff !important; padding: 3px 5px !important; border-radius: 5px !important; margin-bottom: 2px !important; width: 100% !important; box-sizing: border-box !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3) !important; user-select: none !important; } .amaes-active-focus-badge svg { width: 10px !important; height: 10px !important; } .que.amaes-active-focus-que .amaes-active-focus-badge { display: none !important; } .que.amaes-no-answer-que { border-left: 4px solid var(--accent-amber, #f59e0b) !important; } /* Choice Row Highlight Styling */ .amaes-highlighted-choice { border-radius: 6px !important; transition: all 0.2s ease !important; box-sizing: border-box !important; } .amaes-highlighted-choice p { display: inline !important; margin: 0 !important; padding: 0 !important; } .amaes-highlighted-choice label { display: inline-flex !important; align-items: center !important; gap: 6px !important; margin: 0 !important; cursor: pointer !important; background: transparent !important; outline: none !important; padding: 0 !important; } #amaes-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; border-bottom: 1px solid var(--border-subtle); gap: 8px; } #amaes-brand { display: flex; align-items: center; gap: 8px; min-width: 0; flex-shrink: 1; } #amaes-logo-link { display: inline-flex; align-items: center; text-decoration: none; cursor: pointer; flex-shrink: 0; border-radius: 4px; outline: none; } #amaes-logo-link:hover #amaes-logo-img, #amaes-logo-link:focus-visible #amaes-logo-img { transform: scale(1.08); filter: drop-shadow(0 2px 6px rgba(16, 185, 129, 0.45)); } #amaes-logo-img { height: 24px; width: auto; display: block; flex-shrink: 0; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.25)); transition: transform 0.15s ease, filter 0.15s ease; } #amaes-title-group { display: flex; align-items: center; gap: 5px; min-width: 0; } #amaes-title { font-weight: 700; font-size: 13px; letter-spacing: -0.2px; color: var(--text-primary); white-space: nowrap; } #amaes-version-pill { font-size: 9px; font-weight: 600; padding: 1px 4px; border-radius: 4px; background: var(--surface); color: var(--text-muted); border: 1px solid var(--border); white-space: nowrap; } #amaes-actions { display: flex; align-items: center; gap: 3px; flex-shrink: 0; min-width: max-content; } .amaes-icon-btn { width: 26px; height: 26px; background: var(--surface); color: var(--text-secondary); text-decoration: none; border-radius: 6px; font-size: 11px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--border); cursor: pointer; transition: all 0.15s ease; box-sizing: border-box; padding: 0; flex-shrink: 0; } .amaes-icon-btn:hover { color: var(--text-primary); border-color: var(--text-muted); } .amaes-debug-btn { color: var(--accent-amber); border-color: rgba(245, 158, 11, 0.4); } .amaes-card { border: 1px solid var(--border); border-radius: 8px; margin-top: 8px; margin-bottom: 8px; overflow: hidden; background: var(--surface-subtle); } .amaes-card-header { padding: 8px 10px; background: var(--surface); font-weight: 600; color: var(--text-primary); cursor: pointer; display: flex; justify-content: space-between; align-items: center; } .amaes-subcard { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--bg); } .amaes-subcard-header { padding: 6px 8px; background: var(--surface); font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; } .sub-label { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; white-space: nowrap; } .arrow-container { display: inline-flex; align-items: center; color: var(--text-muted); transition: transform 0.15s ease; flex-shrink: 0; } .amaes-btn { display: flex; align-items: center; gap: 6px; border: none; border-radius: 6px; padding: 6px 9px; font-size: 11px; font-weight: 500; cursor: pointer; transition: filter 0.15s ease, background 0.15s ease, transform 0.1s ease; text-align: left; width: 100%; box-sizing: border-box; } .amaes-btn:active { transform: scale(0.97); filter: brightness(0.92); } .amaes-btn-preview { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); font-weight: 600; justify-content: center; } .amaes-btn-preview:hover { border-color: var(--text-muted); } .amaes-btn-monotone { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); font-weight: 500; border-radius: 4px; } .amaes-btn-monotone:hover { background: var(--surface-subtle); border-color: var(--text-muted); color: var(--text-primary); } .amaes-btn-blue { background: var(--accent-blue); color: #ffffff; } .amaes-btn-blue:hover { background: var(--accent-blue-hover); } .amaes-btn-green { background: var(--accent-green); color: #ffffff; } .amaes-btn-green:hover { background: var(--accent-green-hover); } .amaes-btn-pink { background: var(--accent-pink); color: #ffffff; } .amaes-btn-pink:hover { background: var(--accent-pink-hover); } .amaes-btn-gray { background: var(--surface); color: var(--text-secondary); border: 1px solid var(--border); } .amaes-btn-gray:hover { color: var(--text-primary); } .amaes-btn-outline { background: var(--surface); color: var(--text-secondary); border: 1px solid var(--border); } .amaes-btn-outline:hover { color: var(--text-primary); } .amaes-text-blue { color: var(--accent-blue) !important; } .amaes-text-pink { color: var(--accent-pink) !important; } .amaes-text-purple { color: var(--accent-purple) !important; } .amaes-btn-stop { background: var(--accent-pink); color: #ffffff; font-weight: 600; justify-content: center; } .amaes-status-box { padding: 8px; background: var(--status-bg); border-radius: 6px; font-size: 11px; line-height: 1.4; color: var(--text-secondary); max-height: 80px; overflow-y: auto; border: 1px solid var(--border-subtle); } @keyframes amaes-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes amaes-ai-progress { 0% { width: 10%; transform: translateX(0); } 50% { width: 60%; transform: translateX(50%); } 100% { width: 10%; transform: translateX(120%); } } .amaes-ai-spin { animation: amaes-spin 1.2s linear infinite; display: inline-block; } .amaes-ai-thinking-indicator { box-sizing: border-box; width: 100%; } .amaes-ai-progress-bar { animation: amaes-ai-progress 2s ease-in-out infinite; } .amaes-ai-suggested-choice { outline: 2px solid rgba(139, 92, 246, 0.85) !important; background-color: rgba(139, 92, 246, 0.09) !important; box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.2) !important; border-radius: 6px !important; } .amaes-ai-suggested-badge { user-select: none; } .amaes-ai-question-tag { user-select: none; } /* ========================================================== */ /* RESPONSIVE DESIGN & MOBILE/TABLET OPTIMIZATIONS */ /* ========================================================== */ /* Global Touch Optimizations */ .amaes-btn, .amaes-icon-btn, .amaes-tab-btn, .amaes-copy-ai-card-btn, .amaes-web-ai-item, .amaes-que-stop-btn { touch-action: manipulation; } /* Mobile Bottom Sheet Backdrop */ #amaes-panel-backdrop { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.55); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); z-index: 999998; opacity: 0; pointer-events: none; transition: opacity 0.25s ease; } .amaes-sheet-handle { display: none; } /* Mobile Viewports (Phones & Small Screens <= 640px) */ @media (max-width: 640px) { #amaes-panel-backdrop.active { display: block !important; opacity: 1 !important; pointer-events: auto !important; } .amaes-sheet-handle { display: block !important; width: 36px !important; height: 4px !important; border-radius: 2px !important; background: var(--border, #475569) !important; margin: 0 auto 8px auto !important; flex-shrink: 0 !important; cursor: pointer !important; } #amaes-toolkit-panel.amaes-minimized .amaes-sheet-handle { display: none !important; } /* Expanded Panel -> Full Mobile Bottom Sheet */ #amaes-toolkit-panel:not(.amaes-minimized) { left: 0 !important; right: 0 !important; bottom: 0 !important; top: auto !important; width: 100% !important; max-width: 100vw !important; max-height: 80vh !important; max-height: calc(85dvh - env(safe-area-inset-bottom, 0px)) !important; border-radius: 16px 16px 0 0 !important; border-left: none !important; border-right: none !important; border-bottom: none !important; padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 12px)) 14px !important; box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.5) !important; z-index: 999999 !important; } /* Minimized Panel -> Compact Touch FAB Pill */ #amaes-toolkit-panel.amaes-minimized { bottom: calc(14px + env(safe-area-inset-bottom, 0px)) !important; right: calc(14px + env(safe-area-inset-right, 0px)) !important; left: auto !important; top: auto !important; height: 34px !important; min-height: 34px !important; padding: 0 10px !important; border-radius: 22px !important; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important; max-width: calc(100vw - 28px) !important; } /* Responsive Header & Action Buttons */ #amaes-header { padding-bottom: 6px !important; } .amaes-icon-btn { width: 28px !important; height: 28px !important; } /* Tabs in Bottom Sheet */ #amaes-nav-tabs { gap: 2px !important; padding: 2px !important; margin-bottom: 5px !important; } .amaes-tab-btn { padding: 6px 2px !important; font-size: 10px !important; } /* Quiz HUD Collision Prevention */ #amaes-quiz-hud { bottom: calc(52px + env(safe-area-inset-bottom, 10px)) !important; left: 10px !important; right: auto !important; max-width: calc(100vw - 20px) !important; padding: 4px 10px !important; font-size: 10px !important; gap: 6px !important; border-radius: 20px !important; } #amaes-quiz-hud #hud-question-progress { display: none !important; } /* Top Navigation Update Item */ #amaes-topnav-update-btn span, #amaes-topnav-update-item span { max-width: 130px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; font-size: 10px !important; padding: 2px 7px !important; } /* Toast Notification */ #amaes-toast-notification { top: calc(10px + env(safe-area-inset-top, 0px)) !important; left: 12px !important; right: 12px !important; max-width: calc(100vw - 24px) !important; font-size: 11px !important; justify-content: center !important; } /* Modal Dialogs */ #amaes-welcome-modal, #amaes-gemini-modal, #amaes-bug-modal, #amaes-contribute-modal, #amaes-full-qr-modal, #amaes-dev-modal { padding: 8px !important; } #amaes-welcome-card, #amaes-gemini-modal > div, #amaes-bug-modal > div, #amaes-contribute-modal > div, #amaes-full-qr-modal > div, #amaes-dev-modal > div { width: 100% !important; max-width: 100% !important; max-height: 94dvh !important; border-radius: 12px !important; } #amaes-welcome-header { padding: 12px 14px 10px !important; } #amaes-welcome-title { font-size: 14px !important; } #amaes-welcome-scroll-container { padding: 12px 14px !important; gap: 12px !important; } #btn-welcome-close, #amaes-gemini-modal-close, #amaes-bug-modal-close { min-width: 36px !important; min-height: 36px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; font-size: 22px !important; } } /* Tablets & Medium Screens (641px to 1024px) */ @media (min-width: 641px) and (max-width: 1024px) { #amaes-toolkit-panel:not(.amaes-minimized) { width: 320px !important; max-height: calc(100vh - 60px) !important; bottom: calc(18px + env(safe-area-inset-bottom, 0px)) !important; right: calc(18px + env(safe-area-inset-right, 0px)) !important; } #amaes-toolkit-panel.amaes-minimized { bottom: calc(18px + env(safe-area-inset-bottom, 0px)) !important; right: calc(18px + env(safe-area-inset-right, 0px)) !important; } } /* In-Question Card Responsive Layout (All Mobile & Tablet Screens <= 768px) */ @media (max-width: 768px) { .que .info { padding-bottom: 2px !important; margin-bottom: 4px !important; } .que.match .amaes-card-btn-container, .que.qtype_match .amaes-card-btn-container, .que[data-composite="true"] .amaes-card-btn-container { display: none !important; } .amaes-card-btn-container { display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; align-items: center !important; gap: 4px !important; margin-top: 3px !important; margin-bottom: 3px !important; width: 100% !important; } .amaes-card-ai-actions { display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; align-items: center !important; gap: 4px !important; width: 100% !important; } .amaes-copy-ai-card-btn, .amaes-paste-ai-card-btn, .amaes-ask-ai-card-btn { flex: 1 1 auto !important; width: auto !important; min-width: 70px !important; padding: 4px 8px !important; font-size: 10px !important; height: 28px !important; box-sizing: border-box !important; border-radius: 6px !important; } .amaes-builtin-ai-section { display: flex !important; flex-direction: row !important; align-items: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; border-top: none !important; flex: 1 1 auto !important; width: auto !important; } .amaes-builtin-ai-header { display: none !important; } .amaes-active-focus-badge { width: auto !important; padding: 2px 6px !important; font-size: 8.5px !important; margin-bottom: 0 !important; border-radius: 4px !important; } .amaes-web-ai-row { margin: 2px 0 4px 0 !important; padding: 2px 5px !important; gap: 3px !important; border-radius: 8px !important; } .amaes-web-ai-item { padding: 3px 6px !important; font-size: 9.5px !important; min-height: 24px !important; } .amaes-que-top-toolbar { margin-bottom: 4px !important; } } `; const themeBtn = document.getElementById('amaes-theme-btn'); if (themeBtn) { themeBtn.innerHTML = currentTheme === 'dark' ? `${ICONS.sun} Light Mode` : `${ICONS.moon} Dark Mode`; } } window._amaesUpdatePanelTheme = updatePanelThemeStyles; updatePanelThemeStyles(currentTheme); // UI References const statusEl = document.getElementById('amaes-status'); const stopBtn = document.getElementById('amaes-stop-btn'); const minBtn = document.getElementById('amaes-min-btn'); const bodyEl = document.getElementById('amaes-panel-body'); const debugBtn = document.getElementById('amaes-debug-btn'); const themeBtn = document.getElementById('amaes-theme-btn'); // Global setLog used // Helper: Accordion with automatic state persistence in localStorage function setupPersistentAccordion(headerId, bodyId, arrowId, storageKey, defaultOpen = false) { const header = document.getElementById(headerId); const body = document.getElementById(bodyId); const arrow = document.getElementById(arrowId); if (!header || !body) return; const saved = localStorage.getItem(storageKey); const isOpen = saved !== null ? (saved === 'open') : defaultOpen; body.style.display = isOpen ? 'flex' : 'none'; if (arrow) arrow.innerHTML = isOpen ? ICONS.chevronDown : ICONS.chevronRight; header.onclick = () => { const isCurrentlyHidden = body.style.display === 'none'; body.style.display = isCurrentlyHidden ? 'flex' : 'none'; if (arrow) arrow.innerHTML = isCurrentlyHidden ? ICONS.chevronDown : ICONS.chevronRight; localStorage.setItem(storageKey, isCurrentlyHidden ? 'open' : 'closed'); }; } // Tab Navigation Logic (Strict 3-Tab Grid: Quiz, DB, Course Tools) const tabBtns = document.querySelectorAll('.amaes-tab-btn'); const tabPanes = { quiz: document.getElementById('tab-pane-quiz'), db: document.getElementById('tab-pane-db'), course: document.getElementById('tab-pane-course'), accounts: document.getElementById('tab-pane-accounts') }; function switchTab(tabName) { tabBtns.forEach(btn => { if (btn.dataset.tab === tabName) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); Object.keys(tabPanes).forEach(name => { if (tabPanes[name]) { tabPanes[name].style.display = (name === tabName) ? 'flex' : 'none'; } }); localStorage.setItem('amaes_active_tab', tabName); } tabBtns.forEach(btn => { btn.onclick = () => switchTab(btn.dataset.tab); }); // Initial Tab Selection: Auto-open to 'quiz' let initialTab = localStorage.getItem('amaes_active_tab') || 'quiz'; switchTab(initialTab); const btnMasterAutoQuiz = document.getElementById('btn-master-auto-quiz'); const btnCopyCurrQ = document.getElementById('btn-copy-curr-q'); const btnCopyAllQ = document.getElementById('btn-copy-all-q'); const btnFillAllPage = document.getElementById('btn-fill-all-page'); const chkAutoPick = document.getElementById('chk-auto-pick'); const chkAutoNextVerified = document.getElementById('chk-auto-next-verified'); const chkAutoPickStudyGuideFallback = document.getElementById('chk-auto-pick-study-guide-fallback'); const chkAutoHlQuiz = document.getElementById('chk-auto-hl-quiz'); if (btnFillAllPage) { btnFillAllPage.onclick = () => { if (!checkIsQuizAttemptPage()) { showToast("Open a quiz attempt to fill verified answers!"); return; } const cached = getCachedAnswers(subCode); if (!cached || cached.length === 0) { showToast("No verified answers found in database for this subject."); return; } const res = highlightQuizAnswers(cached, true, true); showToast(`Filled ${res.matched || 0} verified answers!`, 2500); setLog(`1-Click Fill: Selected ${res.matched || 0} verified answers on page!`, "var(--accent-green)"); playToolkitSound('success'); }; } if (btnMasterAutoQuiz) { btnMasterAutoQuiz.onclick = () => { if (checkIsQuizSummaryPage()) { if (typeof hastenQuizSummaryAutoSubmit === 'function') { hastenQuizSummaryAutoSubmit(); return; } const submitBtn = findQuizSummarySubmitButton(); if (submitBtn) { submitBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); submitBtn.focus(); submitBtn.style.outline = '3px solid #10b981'; submitBtn.style.boxShadow = '0 0 15px rgba(16, 185, 129, 0.6)'; showToast("Click 'Submit all and finish' to submit your attempt.", 3000); return; } } if (!checkIsQuizAttemptPage()) { // Student is not on an active attempt page (e.g. view.php, dashboard, course) const isQuizLanding = window.location.pathname.includes('/mod/quiz/view.php'); if (!autoQuizMode && isQuizLanding) { const startBtn = document.querySelector('form[action*="attempt.php"] button, form[action*="attempt.php"] input[type="submit"], .quizstartbutton button, .quizstartbutton input[type="submit"], #region-main button.btn-primary, #region-main input.btn-primary'); if (startBtn) { toggleAutoQuizMode(true); showToast("Starting quiz attempt...", 2000); setLog("Starting quiz attempt from introduction page...", "var(--accent-green)"); startBtn.click(); return; } } toggleAutoQuizMode(); return; } if (checkIsQuizAttemptPage() && (isWaitingForUserAnswer || document.querySelector('.amaes-blockage-hud'))) { const currentFocus = userSelectedActiveQuestion || getActiveViewportQuestion(); const nextOnPage = findNextUnansweredOnCurrentPage(currentFocus); if (nextOnPage) { setActiveQuestion(nextOnPage, true); nextOnPage.scrollIntoView({ behavior: 'smooth', block: 'center' }); const qData = extractQuestionData(nextOnPage); showToast(`Next: Question #${qData ? qData.qNum : ''}`, 1500); setLog(`Advancing to Question #${qData ? qData.qNum : ''}...`, "var(--accent-blue)"); runAutoQuizSolver(true); return; } } toggleAutoQuizMode(); }; } if (chkAutoHlQuiz) { chkAutoHlQuiz.onchange = () => { autoHighlightQuiz = chkAutoHlQuiz.checked; localStorage.setItem('amaes_auto_highlight_quiz', autoHighlightQuiz); showToast(`Highlight Answers: ${autoHighlightQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Highlight Answers: ${autoHighlightQuiz ? 'ON' : 'OFF'}`, autoHighlightQuiz ? "var(--accent-green)" : "var(--accent-amber)", autoHighlightQuiz ? "Visual answer badges enabled on quizzes" : "Badges hidden"); if (autoHighlightQuiz && checkIsQuizAttemptPage()) { const cached = getCachedAnswers(subCode); if (cached && cached.length > 0) { highlightQuizAnswers(cached, false, true); } else if (typeof autoFetchCloudAnswersIfMissing === 'function') { autoFetchCloudAnswersIfMissing(subCode).then(ok => { if (ok) highlightQuizAnswers(getCachedAnswers(subCode), false, true); }); } } }; } const chkCopyConfidence = document.getElementById('chk-copy-confidence'); if (chkCopyConfidence) { chkCopyConfidence.onchange = () => { copyIncludeConfidence = chkCopyConfidence.checked; localStorage.setItem('amaes_copy_include_confidence', copyIncludeConfidence); showToast(`Include DB Hints: ${copyIncludeConfidence ? 'Enabled' : 'Disabled'}`); setLog(`Include DB Hints on Copy: ${copyIncludeConfidence ? 'ON' : 'OFF'}`, "var(--accent-blue)", copyIncludeConfidence ? "Prompt will include verified answer hints & confidence" : "Question & choices only"); }; } const chkAutoNext = document.getElementById('chk-auto-next'); if (chkAutoNext) { chkAutoNext.onchange = () => { autoNextQuiz = chkAutoNext.checked; localStorage.setItem('amaes_auto_next_quiz', autoNextQuiz); showToast(`Auto-Next (Manual): ${autoNextQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Next Navigation: ${autoNextQuiz ? 'ON (Auto-advancing)' : 'OFF (Safe Manual Review)'}`, autoNextQuiz ? "var(--accent-green)" : "var(--accent-amber)", autoNextQuiz ? "Advances automatically on manual choice selection" : "Stay on question until manual Next / N press"); if (autoNextQuiz && checkIsQuizAttemptPage()) runAutoQuizSolver(); }; } const chkSmartSkip = document.getElementById('chk-smart-skip'); if (chkSmartSkip) { chkSmartSkip.onchange = () => { smartSkipQuiz = chkSmartSkip.checked; localStorage.setItem('amaes_smart_skip_quiz', smartSkipQuiz); showToast(`Smart Skip: ${smartSkipQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Smart Skip Unverified: ${smartSkipQuiz ? 'ON (Pause on unverified)' : 'OFF'}`, smartSkipQuiz ? "var(--accent-green)" : "var(--accent-amber)", smartSkipQuiz ? "Pauses auto-next on unverified questions" : "Attempts all matching questions"); }; } const chkShowInQBtns = document.getElementById('chk-show-in-q-btns'); if (chkShowInQBtns) { chkShowInQBtns.onchange = (e) => { showInQuestionAiBtns = e.target.checked; localStorage.setItem('amaes_show_in_question_ai_btns', showInQuestionAiBtns); injectQuestionCopyButtons(); showToast(`In-question buttons: ${showInQuestionAiBtns ? 'Shown' : 'Hidden'}`); setLog(`In-Question Action Buttons: ${showInQuestionAiBtns ? 'ON' : 'OFF'}`, "var(--accent-blue)", showInQuestionAiBtns ? "Injected beside each question box" : "Buttons hidden"); }; } if (chkAutoPick) { chkAutoPick.onchange = () => { if (autoQuizMode) { chkAutoPick.checked = true; return; } autoPickQuiz = chkAutoPick.checked; localStorage.setItem('amaes_auto_pick_quiz', autoPickQuiz); showToast(`Auto-Pick: ${autoPickQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Pick Answers: ${autoPickQuiz ? 'ON (Auto-select verified)' : 'OFF (Companion Mode)'}`, autoPickQuiz ? "var(--accent-green)" : "var(--accent-amber)", autoPickQuiz ? "Will auto-select answers on quiz attempts" : "Highlights only; manual clicks required"); if (autoPickQuiz && checkIsQuizAttemptPage()) runAutoQuizSolver(); }; } if (chkAutoNextVerified) { chkAutoNextVerified.onchange = () => { if (autoQuizMode) { chkAutoNextVerified.checked = true; return; } autoNextVerified = chkAutoNextVerified.checked; localStorage.setItem('amaes_auto_next_verified', autoNextVerified); showToast(`Auto-Next (Verified): ${autoNextVerified ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Next on Verified Answers: ${autoNextVerified ? 'ON' : 'OFF'}`, autoNextVerified ? "var(--accent-green)" : "var(--accent-amber)"); if (autoNextVerified && checkIsQuizAttemptPage()) runAutoQuizSolver(); }; } if (chkAutoPickStudyGuideFallback) { chkAutoPickStudyGuideFallback.onchange = () => { if (autoQuizMode) { chkAutoPickStudyGuideFallback.checked = true; return; } autoPickStudyGuideFallback = chkAutoPickStudyGuideFallback.checked; localStorage.setItem('amaes_auto_pick_study_guide_fallback', String(autoPickStudyGuideFallback)); showToast(`Study-guide fallback Auto-Pick: ${autoPickStudyGuideFallback ? 'Enabled in Auto-Quiz' : 'Disabled'}`); setLog(`Study-guide fallback Auto-Pick: ${autoPickStudyGuideFallback ? 'ON (unverified suggestions)' : 'OFF'}`, autoPickStudyGuideFallback ? 'var(--accent-amber)' : 'var(--text-secondary)', 'Auto-Quiz only; Smart Next controls automatic advancement'); if (autoPickStudyGuideFallback && autoQuizMode && checkIsQuizAttemptPage()) runAutoQuizSolver(); }; } const chkAutoMinQuiz = document.getElementById('chk-auto-min-quiz'); if (chkAutoMinQuiz) { chkAutoMinQuiz.onchange = () => { autoMinimizeQuiz = chkAutoMinQuiz.checked; localStorage.setItem('amaes_auto_min_quiz', autoMinimizeQuiz); showToast(`Auto-minimize during quizzes: ${autoMinimizeQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Minimize During Quizzes: ${autoMinimizeQuiz ? 'ON' : 'OFF'}`, autoMinimizeQuiz ? "var(--accent-cyan)" : "var(--accent-amber)", autoMinimizeQuiz ? "Minimizes panel upon entering quiz" : "Panel remains open"); }; } const chkAutoSubmitQuiz = document.getElementById('chk-auto-submit-quiz'); if (chkAutoSubmitQuiz) { chkAutoSubmitQuiz.onchange = () => { autoSubmitQuiz = chkAutoSubmitQuiz.checked; localStorage.setItem('amaes_auto_submit_quiz', autoSubmitQuiz); showToast(`Auto-Submit on Summary: ${autoSubmitQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Submit on Summary Review: ${autoSubmitQuiz ? 'ON' : 'OFF'}`, autoSubmitQuiz ? "var(--accent-green)" : "var(--accent-amber)", autoSubmitQuiz ? "Auto-submits from summary page & plays completion chime" : "Safe manual submission"); }; } const chkAdaptiveProbe = document.getElementById('chk-adaptive-probe'); if (chkAdaptiveProbe) { chkAdaptiveProbe.onchange = () => { adaptiveProbeQuiz = chkAdaptiveProbe.checked; localStorage.setItem('amaes_adaptive_probe_quiz', adaptiveProbeQuiz); showToast(`Adaptive Probe: ${adaptiveProbeQuiz ? 'Enabled' : 'Disabled'}`); setLog(`Adaptive Probe (Unreviewed Quizzes): ${adaptiveProbeQuiz ? 'ON (Rotating unverified choices)' : 'OFF'}`, adaptiveProbeQuiz ? "var(--accent-purple)" : "var(--accent-amber)", adaptiveProbeQuiz ? "Rotates unverified choices across attempts to find 100%" : "Repeats static choices on re-attempts"); if (adaptiveProbeQuiz && checkIsQuizAttemptPage()) runAutoQuizSolver(); }; } const selAdaptiveProbeBudget = document.getElementById('sel-adaptive-probe-budget'); if (selAdaptiveProbeBudget) { selAdaptiveProbeBudget.onchange = () => { adaptiveProbeBudget = parseInt(selAdaptiveProbeBudget.value, 10) || 2; localStorage.setItem('amaes_adaptive_probe_budget', adaptiveProbeBudget); showToast(`Probe Budget: ${adaptiveProbeBudget} questions/attempt`); setLog(`Probe Budget: ${adaptiveProbeBudget} unverified questions rotated per attempt`, "var(--accent-purple)"); }; } const selAutoSubmitDelay = document.getElementById('sel-auto-submit-delay'); if (selAutoSubmitDelay) { selAutoSubmitDelay.onchange = () => { autoSubmitDelay = parseInt(selAutoSubmitDelay.value, 10); if (isNaN(autoSubmitDelay) || autoSubmitDelay < 0) autoSubmitDelay = 10; localStorage.setItem('amaes_auto_submit_delay', autoSubmitDelay); showToast(`Auto-Submit Leeway: ${autoSubmitDelay === 0 ? 'Instant' : autoSubmitDelay + 's'}`); setLog(`Auto-Submit Leeway: ${autoSubmitDelay === 0 ? 'Instant' : autoSubmitDelay + 's'} before auto-submitting on summary page`, "var(--accent-green)"); }; } const chkKeyboardShortcuts = document.getElementById('chk-keyboard-shortcuts'); if (chkKeyboardShortcuts) { chkKeyboardShortcuts.onchange = () => { enableKeyboardShortcuts = chkKeyboardShortcuts.checked; localStorage.setItem('amaes_enable_hotkeys', enableKeyboardShortcuts); showToast(`Keyboard Navigation: ${enableKeyboardShortcuts ? 'Enabled' : 'Disabled'}`); setLog(`Keyboard Navigation: ${enableKeyboardShortcuts ? 'ON' : 'OFF'}`, enableKeyboardShortcuts ? "var(--accent-blue)" : "var(--accent-amber)", enableKeyboardShortcuts ? "N, Space, [ ], 1-4, C, P, H active" : "Key navigation disabled"); }; } const chkSkipPassedQuizzes = document.getElementById('chk-skip-passed-quizzes'); if (chkSkipPassedQuizzes) { chkSkipPassedQuizzes.onchange = () => { const isSkip = chkSkipPassedQuizzes.checked; localStorage.setItem('amaes_skip_passed_quizzes', isSkip ? 'true' : 'false'); showToast(`Skip Passed Quizzes: ${isSkip ? 'Enabled (≥80%)' : 'Disabled (Cycle all)'}`); setLog(`Quiz Cycling: ${isSkip ? 'Skip Passed (≥80%)' : 'All Quizzes'}`, isSkip ? "var(--accent-blue)" : "var(--text-secondary)", isSkip ? "Keyboard cycling [ and ] jumps over completed/passing quizzes" : "Keyboard cycling advances to every quiz in sequence"); if (typeof injectQuizBottomNavigation === 'function') { injectQuizBottomNavigation(); } }; } const chkAutoCloseCourseIndex = document.getElementById('chk-auto-close-course-index'); if (chkAutoCloseCourseIndex) { chkAutoCloseCourseIndex.onchange = () => { const isAuto = chkAutoCloseCourseIndex.checked; localStorage.setItem('amaes_auto_close_course_index', isAuto ? 'true' : 'false'); showToast(`Auto-Close Course Index: ${isAuto ? 'Enabled' : 'Disabled'}`); setLog(`Course Index Drawer: ${isAuto ? 'Auto-Close ON' : 'Keep Open'}`, isAuto ? "var(--accent-blue)" : "var(--text-secondary)", isAuto ? "Course index drawer automatically closes on course pages" : "Course index drawer remains in default state"); if (isAuto && typeof autoCloseCourseIndexDrawer === 'function') { autoCloseCourseIndexDrawer(); } }; } const chkAutoMarkDone100 = document.getElementById('chk-auto-mark-done-100'); if (chkAutoMarkDone100) { chkAutoMarkDone100.onchange = () => { autoMarkDoneOn100 = chkAutoMarkDone100.checked; localStorage.setItem('amaes_auto_mark_done_100', autoMarkDoneOn100 ? 'true' : 'false'); showToast(`Auto-Mark 100% Done: ${autoMarkDoneOn100 ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Mark Done on 100%: ${autoMarkDoneOn100 ? 'ON' : 'OFF'}`, autoMarkDoneOn100 ? "var(--accent-green)" : "var(--accent-amber)", autoMarkDoneOn100 ? "Automatically marks Moodle activity as Done upon achieving a 100% quiz score" : "Requires manual activity completion click"); }; } const chkAudioAlerts = document.getElementById('chk-audio-alerts'); if (chkAudioAlerts) { chkAudioAlerts.onchange = () => { enableAudioAlerts = chkAudioAlerts.checked; localStorage.setItem('amaes_enable_audio_alerts', enableAudioAlerts ? 'true' : 'false'); showToast(`Audio Notifications: ${enableAudioAlerts ? 'Enabled' : 'Disabled'}`); setLog(`Audio Notifications: ${enableAudioAlerts ? 'ON' : 'OFF'}`, enableAudioAlerts ? "var(--accent-green)" : "var(--accent-amber)"); if (enableAudioAlerts) { playToolkitSound('quest_done'); } }; } const chkAiQuizEnabled = document.getElementById('chk-ai-quiz-enabled'); if (chkAiQuizEnabled) { chkAiQuizEnabled.onchange = () => { aiQuizEnabled = chkAiQuizEnabled.checked; localStorage.setItem('amaes_ai_quiz_enabled', aiQuizEnabled); showToast(`AI Quiz Solver: ${aiQuizEnabled ? 'Enabled' : 'Disabled'}`); setLog(`Gemini AI Quiz Solver: ${aiQuizEnabled ? 'ON' : 'OFF'}`, aiQuizEnabled ? "var(--accent-purple)" : "var(--accent-amber)"); }; } const chkAiAutoSelect = document.getElementById('chk-ai-auto-select'); if (chkAiAutoSelect) { chkAiAutoSelect.onchange = () => { aiAutoSelect = chkAiAutoSelect.checked; localStorage.setItem('amaes_ai_auto_select', aiAutoSelect); showToast(`AI Auto-Select: ${aiAutoSelect ? 'Auto-Select ON' : 'Highlight Only'}`); setLog(`AI Auto-Select Answers: ${aiAutoSelect ? 'ON (Auto-select AI answers)' : 'OFF (Highlight only)'}`, aiAutoSelect ? "var(--accent-purple)" : "var(--accent-amber)"); }; } const selAiRetryCount = document.getElementById('sel-ai-retry-count'); if (selAiRetryCount) { selAiRetryCount.onchange = () => { setAiRetryCount(selAiRetryCount.value); const courseSel = document.getElementById('sel-course-ai-retry-count'); if (courseSel) courseSel.value = String(getAiRetryCount()); showToast(`AI Retry Attempts set to ${getAiRetryCount()}`); }; } const selCourseAiRetryCount = document.getElementById('sel-course-ai-retry-count'); if (selCourseAiRetryCount) { selCourseAiRetryCount.onchange = () => { setAiRetryCount(selCourseAiRetryCount.value); const quizSel = document.getElementById('sel-ai-retry-count'); if (quizSel) quizSel.value = String(getAiRetryCount()); showToast(`AI Retry Attempts set to ${getAiRetryCount()}`); }; } const selAiPlanTier = document.getElementById('sel-ai-plan-tier'); if (selAiPlanTier) { selAiPlanTier.onchange = () => { setAiPlanTier(selAiPlanTier.value); const courseSel = document.getElementById('sel-course-ai-plan-tier'); if (courseSel) courseSel.value = getAiPlanTier(); showToast(`AI plan set to ${getAiPlanTier() === 'free' ? 'Free plan' : 'Paid plan'}`); }; } const selCourseAiPlanTier = document.getElementById('sel-course-ai-plan-tier'); if (selCourseAiPlanTier) { selCourseAiPlanTier.onchange = () => { setAiPlanTier(selCourseAiPlanTier.value); const quizSel = document.getElementById('sel-ai-plan-tier'); if (quizSel) quizSel.value = getAiPlanTier(); showToast(`AI plan set to ${getAiPlanTier() === 'free' ? 'Free plan' : 'Paid plan'}`); }; } const chkAiAutoCopyOnFail = document.getElementById('chk-ai-auto-copy-on-fail'); if (chkAiAutoCopyOnFail) { chkAiAutoCopyOnFail.onchange = () => { setAiAutoCopyOnFail(chkAiAutoCopyOnFail.checked); const courseChk = document.getElementById('chk-course-ai-auto-copy-on-fail'); if (courseChk) courseChk.checked = getAiAutoCopyOnFail(); showToast(`Auto-Copy on AI Failure: ${getAiAutoCopyOnFail() ? 'ON' : 'OFF'}`); }; } const chkCourseAiAutoCopyOnFail = document.getElementById('chk-course-ai-auto-copy-on-fail'); if (chkCourseAiAutoCopyOnFail) { chkCourseAiAutoCopyOnFail.onchange = () => { setAiAutoCopyOnFail(chkCourseAiAutoCopyOnFail.checked); const quizChk = document.getElementById('chk-ai-auto-copy-on-fail'); if (quizChk) quizChk.checked = getAiAutoCopyOnFail(); showToast(`Auto-Copy on AI Failure: ${getAiAutoCopyOnFail() ? 'ON' : 'OFF'}`); }; } if (btnCopyCurrQ) { btnCopyCurrQ.onclick = async () => { const que = getActiveViewportQuestion(); if (!que) { showToast("No quiz questions found on this page! Enter a quiz attempt first.", 3500); setLog("No questions found on this page. Open a quiz attempt first.", "var(--accent-pink)"); return; } const qData = extractQuestionData(que); const willIncludeContext = shouldInjectAiContext(qData ? qData.qNum : null); const text = formatQuestionForAI(que, aiPromptHint); try { await copyToClipboard(text); btnCopyCurrQ.innerHTML = `${ICONS.check} Copied!`; const qLabel = qData && qData.qNum ? `Question #${qData.qNum}` : 'Question'; showToast(willIncludeContext ? `${qLabel} copied with Course AI Context!` : `${qLabel} & choices copied!`); setLog(willIncludeContext ? `Copied ${qLabel} with Course AI Context!` : `Copied ${qLabel} & choices!`, "var(--accent-green)"); setTimeout(() => { btnCopyCurrQ.innerHTML = `${ICONS.copy} Copy Question (C)`; }, 1800); } catch (err) { showToast("Failed to copy question to clipboard."); setLog("Failed to copy to clipboard.", "var(--accent-pink)"); } }; } const btnPasteAiAns = document.getElementById('btn-paste-ai-ans'); if (btnPasteAiAns) { btnPasteAiAns.onclick = () => { autoSelectFromAiClipboard(); }; } if (btnCopyAllQ) { btnCopyAllQ.onclick = async () => { const queList = document.querySelectorAll('.que'); if (queList.length === 0) { showToast("No quiz questions found on this page! Enter a quiz attempt first.", 3500); setLog("No questions found on this page. Open a quiz attempt first.", "var(--accent-pink)"); return; } const text = formatAllQuestionsForAI(aiPromptHint); try { await copyToClipboard(text); btnCopyAllQ.innerHTML = `${ICONS.check} Copied!`; showToast(`All ${queList.length} questions copied for AI!`); setLog(`Copied all ${queList.length} questions for AI!`, "var(--accent-green)"); setTimeout(() => { btnCopyAllQ.innerHTML = `${ICONS.copy} Copy All`; }, 1800); } catch (err) { showToast("Failed to copy questions to clipboard."); setLog("Failed to copy to clipboard.", "var(--accent-pink)"); } }; } // --- MODULE 1: Web Scraper Answers & Controls --- const btnCopyAmauoedLink = document.getElementById('btn-copy-amauoed-link'); const urlMatchBadge = document.getElementById('amauoed-url-match-badge'); const hlAnswersBtn = document.getElementById('btn-hl-answers'); const selectAnswersBtn = document.getElementById('btn-select-answers'); async function updateAmauoedDisplay(targetCode) { const code = targetCode || subCode; const linkDisplay = document.getElementById('amauoed-link-display'); const copyBtn = document.getElementById('btn-copy-amauoed-link'); if (!linkDisplay) return; let url = getStoredAmauoedUrl(code); if (url) { renderLink(url); } else if (code && code !== 'GENERAL' && code !== 'DEFAULT' && autoScrapeAmauoed) { linkDisplay.textContent = 'Auto-detecting link...'; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (urlMatchBadge) urlMatchBadge.style.display = 'none'; try { url = await autoFindAmauoedLink(code, courseInfo.subjectName || courseInfo.currentActivityTitle || ''); if (url) { renderLink(url); const existing = getCachedAnswers(code); if ((!existing || existing.length === 0) && autoScrapeAmauoed) { setLog(`Auto-scraping AMAUOED answers for ${code}...`, 'var(--accent-blue)'); const scraped = await loadAllAmauoedAnswers(url); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(code, scraped, 'AMAUOED'); updateTermCoverageUI(code); setLog(`Auto-scraped ${scraped.length} answers from AMAUOED for ${code}!`, 'var(--accent-green)'); if (checkIsQuizPage()) { highlightQuizAnswers(getCachedAnswers(code), false); } } } } else { linkDisplay.textContent = 'No study guide link detected'; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (urlMatchBadge) urlMatchBadge.style.display = 'none'; } } catch (e) { linkDisplay.textContent = 'No study guide link detected'; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (urlMatchBadge) urlMatchBadge.style.display = 'none'; } } else { linkDisplay.textContent = 'No study guide link detected'; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (urlMatchBadge) urlMatchBadge.style.display = 'none'; } function renderLink(linkUrl) { linkDisplay.textContent = linkUrl; linkDisplay.href = linkUrl; linkDisplay.title = linkUrl; if (copyBtn) copyBtn.disabled = false; if (urlMatchBadge) { const activeCourseContext = { ...courseInfo, subjectCode: code || subCode || (courseInfo && courseInfo.subjectCode) }; const res = checkUrlCourseMatch(linkUrl, activeCourseContext); const isInCourse = (typeof checkIsCoursePage === 'function' && checkIsCoursePage()) || (typeof checkIsQuizPage === 'function' && checkIsQuizPage()) || Boolean(courseInfo && courseInfo.subjectCode && !document.getElementById('amaes-select-active-course')); if (res.status === 'match') { urlMatchBadge.style.display = 'block'; urlMatchBadge.innerHTML = res.html; urlMatchBadge.style.background = 'rgba(16, 185, 129, 0.12)'; urlMatchBadge.style.border = '1px solid rgba(16, 185, 129, 0.3)'; urlMatchBadge.style.color = 'var(--accent-green)'; } else if (res.status === 'mismatch' && isInCourse) { urlMatchBadge.style.display = 'block'; urlMatchBadge.innerHTML = res.html; urlMatchBadge.style.background = 'rgba(244, 63, 94, 0.12)'; urlMatchBadge.style.border = '1px solid rgba(244, 63, 94, 0.3)'; urlMatchBadge.style.color = 'var(--accent-pink)'; const fixBtn = document.getElementById('btn-fix-amauoed-url'); if (fixBtn) { fixBtn.onclick = (e) => { e.preventDefault(); const correctUrl = getStoredAmauoedUrl(subCode); if (correctUrl) { setStoredAmauoedUrl(subCode, correctUrl); renderLink(correctUrl); setLog(`Switched to detected ${subCode} URL!`, 'var(--accent-green)'); } }; } } else if (res.status === 'invalid' && isInCourse) { urlMatchBadge.style.display = 'block'; urlMatchBadge.innerHTML = res.html; urlMatchBadge.style.background = 'rgba(244, 63, 94, 0.12)'; urlMatchBadge.style.border = '1px solid rgba(244, 63, 94, 0.3)'; urlMatchBadge.style.color = 'var(--accent-pink)'; } else { urlMatchBadge.style.display = 'none'; } } } updateJennysonlineDisplay(code); } async function updateJennysonlineDisplay(targetCode) { const code = targetCode || subCode; const linkDisplay = document.getElementById('jennysonline-link-display'); const copyBtn = document.getElementById('btn-copy-jennysonline-link'); const badge = document.getElementById('jennysonline-match-badge'); const statusPill = document.getElementById('jennysonline-status-pill'); if (!linkDisplay) return; if (!code || code === 'GENERAL' || code === 'DEFAULT') { linkDisplay.textContent = 'No course selected'; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (badge) badge.style.display = 'none'; if (statusPill) statusPill.textContent = ''; return; } let cached = typeof readJennysonlinePersistentCache === 'function' ? readJennysonlinePersistentCache(code) : null; let url = ''; let count = 0; if (Array.isArray(cached) && cached.length > 0) { count = cached.length; url = cached[0].sourceUrl || (typeof CLOUD_DB_JENNYSONLINE_URL !== 'undefined' ? `${CLOUD_DB_JENNYSONLINE_URL}${code}.json` : ''); } if (!url && typeof loadJennysonlineAnswersForCourse === 'function' && autoScrapeAmauoed) { linkDisplay.textContent = "Checking Jenny's Online..."; try { const answers = await loadJennysonlineAnswersForCourse(code); if (Array.isArray(answers) && answers.length > 0) { count = answers.length; url = answers[0].sourceUrl || (typeof CLOUD_DB_JENNYSONLINE_URL !== 'undefined' ? `${CLOUD_DB_JENNYSONLINE_URL}${code}.json` : ''); } } catch (_) {} } if (url) { linkDisplay.textContent = url; linkDisplay.href = url; linkDisplay.title = url; if (copyBtn) copyBtn.disabled = false; if (badge) { badge.style.display = 'block'; badge.style.background = 'rgba(168, 85, 247, 0.12)'; badge.style.border = '1px solid rgba(168, 85, 247, 0.3)'; badge.style.color = 'var(--accent-purple, #c084fc)'; badge.innerHTML = `Jenny's Online: ${count} unconfirmed study guide candidates available`; } if (statusPill) statusPill.textContent = `${count} Qs`; } else { linkDisplay.textContent = `No Jenny's Online guide available for ${code}`; linkDisplay.removeAttribute('href'); if (copyBtn) copyBtn.disabled = true; if (badge) badge.style.display = 'none'; if (statusPill) statusPill.textContent = 'Not in snapshot'; } } function updateTermCoverageUI(code) { const cardEl = document.getElementById('amaes-term-coverage-card'); const targetCode = code !== undefined ? code : (courseInfo.subjectCode || ''); if (!targetCode) { if (cardEl) cardEl.style.display = 'none'; return; } if (cardEl) cardEl.style.display = 'flex'; const summaryEl = document.getElementById('amaes-term-summary'); const pillsEl = document.getElementById('amaes-term-pills'); const titleEl = document.getElementById('amaes-coverage-title'); const badgeEl = document.getElementById('amaes-coverage-badge'); if (titleEl) titleEl.innerText = `Course Coverage (${targetCode}):`; if (badgeEl) badgeEl.innerText = targetCode; if (!pillsEl || !summaryEl) return; const questions = getCachedAnswers(targetCode) || []; const total = questions.length; let verifiedCount = 0; let communityCount = 0; let amauoedCount = 0; let eliminatedCount = 0; questions.forEach(q => { const s = (q.source || '').toLowerCase(); const sources = Array.isArray(q.sources) ? q.sources.map(x => (x || '').toLowerCase()) : []; const isStudyGuide = s.includes('amauoed') || s.includes('jennysonline') || sources.some(x => x.includes('amauoed') || x.includes('jennysonline')); if (isStudyGuide) { amauoedCount++; } else { verifiedCount++; } if (Array.isArray(q.wrongAnswers)) { eliminatedCount += q.wrongAnswers.length; } }); const statVer = document.getElementById('amaes-stat-verified'); const statComm = document.getElementById('amaes-stat-community'); const statAmau = document.getElementById('amaes-stat-amauoed'); const statElim = document.getElementById('amaes-stat-eliminated'); if (statVer) statVer.innerText = String(verifiedCount); if (statComm) statComm.innerText = '0'; if (statAmau) statAmau.innerText = String(amauoedCount); if (statElim) statElim.innerText = String(eliminatedCount); const stats = getSubjectTermBreakdown(targetCode); if (total === 0) { summaryEl.innerText = "No Qs Saved"; summaryEl.style.color = "rgba(255, 255, 255, 0.45)"; } else { const covered = [stats.prelim > 0, stats.midterm > 0, stats.prefi > 0, stats.final > 0].filter(Boolean).length; summaryEl.innerText = `${total} Qs Total • ${covered}/4 Terms Ready`; summaryEl.style.color = covered === 4 ? "#34d399" : covered > 0 ? "#60a5fa" : "rgba(255, 255, 255, 0.7)"; } const terms = [ { name: 'Prelim', count: stats.prelim }, { name: 'Midterm', count: stats.midterm }, { name: 'Prefi', count: stats.prefi }, { name: 'Final', count: stats.final } ]; pillsEl.innerHTML = terms.map(t => { if (t.count > 0) { return `
${t.name}
${t.count} Qs
`; } else { return `
${t.name}
${t.count} Qs
`; } }).join(''); } updateAmauoedDisplay(courseInfo.subjectCode ? subCode : ''); updateTermCoverageUI(courseInfo.subjectCode ? subCode : ''); const selectActiveCourse = document.getElementById('amaes-select-active-course'); if (selectActiveCourse) { selectActiveCourse.onchange = () => { let val = selectActiveCourse.value; if (val === '_custom') { const custom = prompt("Enter Subject Code (e.g. CS6301, ITE6301):", ""); if (custom && custom.trim()) { val = custom.trim().toUpperCase(); const opt = document.createElement('option'); opt.value = val; opt.textContent = `${val} (0 Qs)`; opt.selected = true; selectActiveCourse.insertBefore(opt, selectActiveCourse.lastElementChild); } else { selectActiveCourse.value = subCode; return; } } subCode = val; updateAmauoedDisplay(subCode); updateTermCoverageUI(subCode); const curAnswers = getCachedAnswers(subCode); setLog(`Active subject switched to ${subCode} (${(curAnswers || []).length} cached answers).`, "var(--accent-blue)"); }; } if (btnCopyAmauoedLink) { btnCopyAmauoedLink.onclick = async () => { const linkDisplay = document.getElementById('amauoed-link-display'); const url = (linkDisplay && linkDisplay.getAttribute('href') && linkDisplay.getAttribute('href') !== '#') ? linkDisplay.getAttribute('href') : (getStoredAmauoedUrl(subCode) || ''); if (!url) { showToast("No study guide link to copy!"); return; } try { await navigator.clipboard.writeText(url); btnCopyAmauoedLink.innerHTML = `${ICONS.check} Copied!`; showToast("Study guide link copied to clipboard!"); setLog(`Copied study guide link to clipboard: ${url}`, "var(--accent-green)"); setTimeout(() => { btnCopyAmauoedLink.innerHTML = `${ICONS.copy} Copy Link`; }, 1800); } catch (err) { showToast("Failed to copy link to clipboard."); } }; } const btnCopyJennysonlineLink = document.getElementById('btn-copy-jennysonline-link'); if (btnCopyJennysonlineLink) { btnCopyJennysonlineLink.onclick = async () => { const linkDisplay = document.getElementById('jennysonline-link-display'); const url = (linkDisplay && linkDisplay.getAttribute('href') && linkDisplay.getAttribute('href') !== '#') ? linkDisplay.getAttribute('href') : ''; if (!url) { showToast("No Jenny's Online link to copy!"); return; } try { await navigator.clipboard.writeText(url); btnCopyJennysonlineLink.innerHTML = `${ICONS.check} Copied!`; showToast("Jenny's Online link copied to clipboard!"); setLog(`Copied Jenny's Online link to clipboard: ${url}`, "var(--accent-green)"); setTimeout(() => { btnCopyJennysonlineLink.innerHTML = `${ICONS.copy} Copy Link`; }, 1800); } catch (err) { showToast("Failed to copy link to clipboard."); } }; } setupPersistentAccordion('mod-quiz-header', 'mod-quiz-body', 'mod-quiz-arrow', 'amaes_pref_mod_amauoed', true); if (chkAutoHlQuiz) chkAutoHlQuiz.onchange = () => { autoHighlightQuiz = chkAutoHlQuiz.checked; localStorage.setItem('amaes_auto_highlight_quiz', autoHighlightQuiz); }; // Highlight Button if (hlAnswersBtn) hlAnswersBtn.onclick = () => { const cached = getCachedAnswers(subCode); const isExamSite = typeof isExamPortalDomain === 'function' && isExamPortalDomain(); const term = isExamSite ? 'exam' : 'quiz'; if (!cached) { setLog(`No answers cached yet for this ${term}!`, "var(--accent-pink)"); return; } const res = highlightQuizAnswers(cached, false); if (res.error) { setLog(res.error, "var(--accent-pink)"); } else if (res.matched === res.total && res.total > 0) { setLog(`Highlighted all ${res.total} answers!`, "var(--accent-green)"); } else if (res.matched === 0 && res.total > 0) { setLog(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`, "var(--accent-amber)"); } else { setLog(`Highlighted ${res.matched}/${res.total} answers. (${res.total - res.matched} question(s) have no saved answer in database)`, "var(--accent-green)"); } }; // Auto-Select Button if (selectAnswersBtn) selectAnswersBtn.onclick = () => { const cached = getCachedAnswers(subCode); const isExamSite = typeof isExamPortalDomain === 'function' && isExamPortalDomain(); const term = isExamSite ? 'exam' : 'quiz'; if (!cached) { setLog(`No answers cached yet for this ${term}!`, "var(--accent-pink)"); return; } const res = highlightQuizAnswers(cached, true, true); if (res.error) { setLog(res.error, "var(--accent-pink)"); } else if (res.matched === res.total && res.total > 0) { setLog(`Auto-selected all ${res.total} answers!`, "var(--accent-green)"); } else if (res.matched === 0 && res.total > 0) { setLog(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`, "var(--accent-amber)"); } else { setLog(`Auto-selected ${res.matched}/${res.total} answers. (${res.total - res.matched} question(s) have no saved answer in database)`, "var(--accent-green)"); } }; // Cloud Database Sync Handlers const btnCloudSync = document.getElementById('btn-cloud-sync'); const btnConfigCloud = document.getElementById('btn-config-cloud'); if (btnCloudSync) { btnCloudSync.onclick = async () => { btnCloudSync.disabled = true; btnCloudSync.classList.add('amaes-pulse'); btnCloudSync.innerHTML = `${ICONS.cloud} Connecting...`; showToast("Connecting to Cloud Database...", 2000); setLog("Starting Cloud Sync...", "var(--accent-blue)", "Connecting to Community Cloud"); let targetCode = subCode; const dashCourses = detectDashboardCourses(); // Dashboard batch sync if no specific course is selected if ((!targetCode || targetCode === 'DEFAULT' || targetCode === 'GENERAL') && dashCourses.length > 0) { btnCloudSync.innerHTML = `${ICONS.cloud} Syncing ${dashCourses.length} Courses...`; showToast(`Syncing community & AMAUOED databases for ${dashCourses.length} courses...`, 3000); setLog(`Syncing Cloud & AMAUOED databases for ${dashCourses.length} courses...`, "var(--accent-blue)", "Connecting to Community Cloud and amauoed.com"); let totalSynced = 0; let coursesProcessed = 0; for (let i = 0; i < dashCourses.length; i++) { const c = dashCourses[i]; btnCloudSync.innerHTML = `${ICONS.cloud} [${i + 1}/${dashCourses.length}] ${c.code}...`; setLog(`[${i + 1}/${dashCourses.length}] Syncing ${c.code} from Cloud Hub...`, "var(--accent-cyan)", `Checking database for ${c.code}`); try { let res = null; try { res = await syncAnswersFromCloud(c.code, null, c.title || ''); } catch (e) { res = null; } if (res && res.count > 0) { totalSynced += res.count; coursesProcessed++; } else { const amauoedLink = await autoFindAmauoedLink(c.code, c.title || ''); if (amauoedLink) { setLog(`[${i + 1}/${dashCourses.length}] Scraping AMAUOED for ${c.code}...`, "var(--accent-purple)", `Crawling questions from ${amauoedLink}`); const scraped = await loadAllAmauoedAnswers(amauoedLink); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(c.code, scraped, 'AMAUOED'); totalSynced += scraped.length; coursesProcessed++; } } } } catch (err) { logDebug(`Cloud sync error for ${c.code}: ${err.message}`); } } showToast(`Cloud Sync Complete! Found ${totalSynced} answer entries and study-guide suggestions across ${dashCourses.length} courses.`, 4000); setLog(`Cloud Sync Complete! Found ${totalSynced} answer entries and study-guide suggestions across ${dashCourses.length} courses.`, "var(--accent-green)", "Guide suggestions remain unconfirmed and session-only"); btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `${ICONS.check} Synced ${totalSynced} Qs!`; injectDashboardCourseBadges(); setTimeout(() => { btnCloudSync.disabled = false; btnCloudSync.innerHTML = `${ICONS.cloudDownload} Cloud Sync`; }, 3500); return; } if (!targetCode || targetCode === 'DEFAULT' || targetCode === 'GENERAL') { btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.disabled = false; btnCloudSync.innerHTML = `${ICONS.cloudDownload} Cloud Sync`; const entered = prompt("Enter Subject Code to sync from Cloud (e.g. CS6301, ITE6301):", (detectedCodes && detectedCodes[0]) || ""); if (!entered || !entered.trim()) { setLog("Cloud sync cancelled (no subject code specified).", "var(--accent-amber)", "Select a subject code to sync"); showToast("Cloud sync cancelled."); return; } targetCode = entered.trim().toUpperCase(); subCode = targetCode; btnCloudSync.disabled = true; btnCloudSync.classList.add('amaes-pulse'); } btnCloudSync.innerHTML = `${ICONS.cloud} Syncing ${targetCode}...`; showToast(`Connecting to Cloud Database for ${targetCode}...`, 2500); setLog(`Connecting to community cloud database for ${targetCode}...`, "var(--accent-blue)", `Checking GitHub repository for ${targetCode}`); try { let res = null; try { res = await syncAnswersFromCloud(targetCode, null, (detectCourseInfo().subjectName || '')); } catch (cloudErr) { res = null; } if (res && res.count > 0) { const finalDb = getCachedAnswers(targetCode) || []; const jennyCount = res.jennysonlineCount || 0; const jennyAnswers = jennyCount > 0 ? await loadJennysonlineAnswersForCourse(targetCode) : []; const cachedCount = res.cachedCount || 0; showToast(`Cloud Sync Success! (${cachedCount} cached; ${jennyCount} unconfirmed Jenny suggestions)`); setLog(`Synced ${cachedCount} cached entries and ${jennyCount} locally cached Jenny suggestions for ${targetCode}.`, "var(--accent-green)", "Jenny guide suggestions are unconfirmed"); btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `${ICONS.check} Synced ${res.count} Qs!`; if (fetchBtnLabel) { fetchBtnLabel.innerText = `Refresh Answers (${finalDb.length} cached)`; } updateTermCoverageUI(targetCode); if (checkIsQuizPage()) { highlightQuizAnswers(finalDb.concat(jennyAnswers), false); } } else { // Fallback to AMAUOED auto-discovery! setLog(`Cloud repo has no answers for ${targetCode} yet. Checking AMAUOED...`, "var(--accent-purple)", `Searching amauoed.com catalog for ${targetCode}`); showToast(`No cloud answers found for ${targetCode}. Checking AMAUOED...`, 2500); btnCloudSync.innerHTML = `${ICONS.rotateCcw} Checking AMAUOED...`; const amauoedUrl = await autoFindAmauoedLink(targetCode, detectCourseInfo().subjectName || ''); if (amauoedUrl) { btnCloudSync.innerHTML = `${ICONS.rotateCcw} Scraping AMAUOED...`; setLog(`Auto-scraping AMAUOED study guide for ${targetCode}...`, "var(--accent-blue)", `Crawling ${amauoedUrl}`); const scraped = await loadAllAmauoedAnswers(amauoedUrl); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(targetCode, scraped, 'AMAUOED'); const finalDb = getCachedAnswers(targetCode) || []; showToast(`AMAUOED Fallback: Loaded ${scraped.length} questions for ${targetCode}!`, 4000); setLog(`Scraped ${scraped.length} questions from AMAUOED for ${targetCode}!`, "var(--accent-green)", "Cached in local database"); btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `${ICONS.check} Scraped ${scraped.length} Qs!`; if (fetchBtnLabel) fetchBtnLabel.innerText = `Refresh Answers (${finalDb.length} cached)`; updateTermCoverageUI(targetCode); if (checkIsQuizPage()) highlightQuizAnswers(finalDb, false); } else { showToast(`No questions found on AMAUOED for ${targetCode}. Will harvest when you take quizzes!`, 4000); setLog(`No questions found for ${targetCode} on AMAUOED.`, "var(--accent-amber)", "Answers will auto-harvest as you complete quizzes"); btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `No Answers Yet`; } } else { showToast(`Course ${targetCode} is not in Cloud Hub yet. It will auto-harvest as you quiz!`, 4000); setLog(`No answers found on Cloud or AMAUOED for ${targetCode}.`, "var(--accent-amber)", "Answers will auto-harvest from quiz attempts and reviews"); btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `No Answers Yet`; } } } catch (err) { showToast(`Cloud Sync error: ${err.message}`); setLog(`Cloud Sync Note: ${err.message}`, "var(--accent-amber)", "You can set custom database link via Config"); } finally { setTimeout(() => { btnCloudSync.disabled = false; btnCloudSync.classList.remove('amaes-pulse'); btnCloudSync.innerHTML = `${ICONS.cloudDownload} Cloud Sync`; }, 3500); } }; } const btnHarvestGradesDb = document.getElementById('btn-harvest-grades-db'); if (btnHarvestGradesDb) { btnHarvestGradesDb.onclick = () => { btnHarvestGradesDb.disabled = true; btnHarvestGradesDb.classList.add('amaes-pulse'); btnHarvestGradesDb.innerHTML = `${ICONS.clock} Scanning Quizzes...`; showToast("Scanning completed quizzes for verified answers...", 2500); setLog("Starting quiz answer collection & share...", "var(--accent-blue)", "Scanning completed attempts for verified answers"); executeGradesHarvester((curr, total, name) => { btnHarvestGradesDb.innerHTML = `${ICONS.clock} [${curr}/${total}] ${name}...`; }).then(res => { btnHarvestGradesDb.classList.remove('amaes-pulse'); if (res && res.success && res.count > 0) { btnHarvestGradesDb.innerHTML = `${ICONS.check} Collected ${res.count} Qs!`; } else if (res && res.inProgress) { btnHarvestGradesDb.innerHTML = `${ICONS.clock} In Progress`; showToast("Auto-collector is currently scanning past quizzes in background. Please wait...", 3000); setLog("Collector is actively scanning enrolled courses in background...", "var(--accent-blue)", "Scanning grade items for completed quizzes"); } else if (res && res.success && res.count === 0) { btnHarvestGradesDb.innerHTML = `${ICONS.check} All Up to Date`; showToast("Grade report scan complete. All completed quizzes are already in database!", 3500); setLog("Grade report scan complete. All available quizzes already cached.", "var(--accent-cyan)", "Answers ready in database"); } else { btnHarvestGradesDb.innerHTML = `Finished`; } setTimeout(() => { btnHarvestGradesDb.disabled = false; btnHarvestGradesDb.classList.remove('amaes-pulse'); btnHarvestGradesDb.innerHTML = `${ICONS.download} Collect & Share Answers`; }, 4000); }); }; } if (btnConfigCloud) { btnConfigCloud.onclick = () => { const currentUrl = localStorage.getItem('amaes_cloud_db_url') || cloudDbBaseUrl; const currentToken = localStorage.getItem('amaes_github_token') || ''; const choice = prompt( `CLOUD DATABASE & GITHUB CONFIGURATION\n\n` + `1. Base URL for Cloud Sync:\n${currentUrl}\n\n` + `2. GitHub Token (PAT) for 1-Click Push:\n${currentToken ? '••••••••' + currentToken.slice(-4) : '(Not set - Web editor fallback)'}\n\n` + `Commands:\n` + `• 'token ' -> Set GitHub Personal Access Token\n` + `• 'url ' -> Set raw database URL\n` + `• 'clear' -> Remove GitHub Token\n\n` + `Enter command or leave blank:`, currentToken ? `token ${currentToken}` : `url ${currentUrl}` ); if (choice) { const trimmed = choice.trim(); if (trimmed.startsWith('token ')) { const token = trimmed.replace(/^token\s+/, '').trim(); localStorage.setItem('amaes_github_token', token); showToast("GitHub Token saved!"); setLog("GitHub Token configured for 1-click commits!", "var(--accent-green)"); } else if (trimmed.startsWith('url ')) { const url = trimmed.replace(/^url\s+/, '').trim(); localStorage.setItem('amaes_cloud_db_url', url); cloudDbBaseUrl = url; showToast("Cloud DB URL updated!"); setLog(`Cloud Database URL updated to ${url}`, "var(--accent-blue)"); } else if (trimmed === 'clear') { localStorage.removeItem('amaes_github_token'); showToast("GitHub Token cleared!"); setLog("GitHub Token cleared.", "var(--text-muted)"); } } }; } const btnContributeDb = document.getElementById('btn-contribute-db'); if (btnContributeDb) { btnContributeDb.onclick = () => { showCommunityContributionModal(subCode); }; } const syncSettingsCheckbox = (id, checked) => { const welcomeId = { 'chk-auto-harvest-grades': 'welcome-chk-harvest', 'chk-auto-cloud-sync': 'welcome-chk-sync', 'chk-auto-community-share': 'welcome-chk-share', 'chk-auto-scrape-amauoed': 'welcome-chk-scrape' }[id]; const welcome = welcomeId && document.getElementById(welcomeId); if (welcome) welcome.checked = checked; }; const chkAutoHarvestGrades = document.getElementById('chk-auto-harvest-grades'); if (chkAutoHarvestGrades) { chkAutoHarvestGrades.onchange = (e) => { autoHarvestGrades = e.target.checked; localStorage.setItem('amaes_auto_harvest_grades', autoHarvestGrades); syncSettingsCheckbox('chk-auto-harvest-grades', autoHarvestGrades); showToast(`Auto-harvest past quizzes: ${autoHarvestGrades ? 'Enabled' : 'Disabled'}`); setLog(`Auto-Harvest past quizzes from Grades: ${autoHarvestGrades ? 'ON' : 'OFF'}`, autoHarvestGrades ? "var(--accent-green)" : "var(--accent-amber)", autoHarvestGrades ? "Scans completed quizzes for verified teacher answers" : "Manual harvest only"); }; } const chkAutoCloudSync = document.getElementById('chk-auto-cloud-sync'); if (chkAutoCloudSync) { chkAutoCloudSync.onchange = (e) => { autoCloudSync = e.target.checked; localStorage.setItem('amaes_auto_cloud_sync', autoCloudSync); syncSettingsCheckbox('chk-auto-cloud-sync', autoCloudSync); showToast(`Auto-pull community answers: ${autoCloudSync ? 'Enabled' : 'Disabled'}`); setLog(`Auto-pull community answers on course open: ${autoCloudSync ? 'ON' : 'OFF'}`, autoCloudSync ? "var(--accent-green)" : "var(--accent-amber)", autoCloudSync ? "Auto-fetches answers upon opening any enrolled course" : "Manual sync only"); }; } const chkAutoCommunityShare = document.getElementById('chk-auto-community-share'); if (chkAutoCommunityShare) { chkAutoCommunityShare.onchange = (e) => { autoCommunityShare = e.target.checked; localStorage.setItem('amaes_auto_community_share', autoCommunityShare); syncSettingsCheckbox('chk-auto-community-share', autoCommunityShare); showToast(`Auto-share to Community Hub: ${autoCommunityShare ? 'Enabled' : 'Disabled'}`); setLog(`Auto-share verified answers on Review: ${autoCommunityShare ? 'ON' : 'OFF'}`, autoCommunityShare ? "var(--accent-green)" : "var(--accent-amber)", autoCommunityShare ? "Anonymously contributes new teacher keys on review" : "Sharing disabled"); if (checkIsReviewPage()) { document.querySelectorAll('.amaes-review-status-pill, .amaes-review-outcome-banner').forEach(el => el.remove()); injectReviewQuestionMarkers(); } }; } const chkAutoScrapeAmauoed = document.getElementById('chk-auto-scrape-amauoed'); const chkAutoScrapeAmauoedQuiz = document.getElementById('chk-auto-scrape-amauoed-quiz'); function handleAutoScrapeToggle(enabled) { autoScrapeAmauoed = enabled; localStorage.setItem('amaes_auto_scrape_amauoed', autoScrapeAmauoed); syncSettingsCheckbox('chk-auto-scrape-amauoed', autoScrapeAmauoed); if (chkAutoScrapeAmauoed) chkAutoScrapeAmauoed.checked = autoScrapeAmauoed; if (chkAutoScrapeAmauoedQuiz) chkAutoScrapeAmauoedQuiz.checked = autoScrapeAmauoed; showToast(`Auto-fetch AMAUOED: ${autoScrapeAmauoed ? 'Enabled' : 'Disabled'}`); setLog(`Auto-fetch AMAUOED when missing: ${autoScrapeAmauoed ? 'ON' : 'OFF'}`, autoScrapeAmauoed ? "var(--accent-purple)" : "var(--accent-amber)", autoScrapeAmauoed ? "Discovers and crawls online study guides when missing" : "AMAUOED scraping disabled"); if (enabled) { updateAmauoedDisplay(subCode); } } if (chkAutoScrapeAmauoed) { chkAutoScrapeAmauoed.onchange = (e) => handleAutoScrapeToggle(e.target.checked); } if (chkAutoScrapeAmauoedQuiz) { chkAutoScrapeAmauoedQuiz.onchange = (e) => handleAutoScrapeToggle(e.target.checked); } // If on quiz attempt page, the unified runAutoQuizSolver handles highlighting, auto-picking & auto-next. // Never highlight over graded review pages. const cachedQuestions = subCode ? getCachedAnswers(subCode) : null; if (autoHighlightQuiz && isQuiz && cachedQuestions && !checkIsQuizAttemptPage() && !checkIsReviewPage()) { setTimeout(() => { const res = highlightQuizAnswers(cachedQuestions, false); if (res.matched > 0) { setLog(`Auto-highlighted ${res.matched}/${res.total} answers from database.`, "var(--accent-green)"); } }, 600); } // --- MODULE 2: Highlighter Module Elements --- setupPersistentAccordion('mod-highlighter-header', 'mod-highlighter-body', 'mod-highlighter-arrow', 'amaes_pref_mod_hl', false); const triggerHighlight = (cat, label) => { const isGrades = typeof window !== 'undefined' && window.location.pathname.includes('/grade/report/user/index.php'); if (isGrades && cat !== 'quiz') { showToast("Open a course page to highlight lecture items."); setLog("Notice: Open a course subject page to highlight lecture items.", "var(--accent-amber)"); return; } const res = highlightItems(cat); if (res.error) { if (isGrades) { showToast("Open a course page to highlight lecture items."); setLog("Notice: Open a course subject page to highlight lecture items.", "var(--accent-amber)"); } else { showToast("Open a course page first to highlight items."); setLog("Action Blocked: Open a course subject first.", "var(--accent-pink)"); } } else if (res.count === 0) { showToast(`No ${label} items found to highlight on this page.`); setLog(`No ${label} items found on this page.`, "var(--accent-amber)"); } else { showToast(`Highlighted ${res.count} ${label} items!`); setLog(`Highlighted ${res.count} ${label} items on page.`, "var(--accent-green)"); } }; const _el__btn_hl_quiz_ = document.getElementById('btn-hl-quiz'); if (_el__btn_hl_quiz_) _el__btn_hl_quiz_.onclick = () => triggerHighlight('quiz', 'Quiz/Exam'); const _el__btn_hl_lec_ = document.getElementById('btn-hl-lec'); if (_el__btn_hl_lec_) _el__btn_hl_lec_.onclick = () => triggerHighlight('lecture', 'Lecture'); const _el__btn_hl_vid_ = document.getElementById('btn-hl-vid'); if (_el__btn_hl_vid_) _el__btn_hl_vid_.onclick = () => triggerHighlight('video', 'Video'); const _el__btn_hl_all_ = document.getElementById('btn-hl-all'); if (_el__btn_hl_all_) _el__btn_hl_all_.onclick = () => triggerHighlight('all', 'total'); const _el__btn_hl_clear_ = document.getElementById('btn-hl-clear'); if (_el__btn_hl_clear_) _el__btn_hl_clear_.onclick = () => { clearAllHighlights(); showToast("Cleared all highlights."); setLog("Cleared all highlights.", "var(--text-muted)"); }; const handleMissingQuizzesClick = async () => { showToast("Checking quiz grades and attempt statuses...", 1500); const res = await highlightMissingOrUnansweredQuizzes(); if (res.count > 0) { showToast(`Highlighted ${res.count} missing/re-attemptable quizzes!`); setLog(`Found and highlighted ${res.count} quizzes needing completion/re-attempt (<80%).`, "var(--accent-amber)"); } else if (res.message) { showToast(res.message); setLog(res.message, "var(--accent-green)"); } else { showToast("All quizzes achieved passing grade (≥80%)! None pending."); setLog("All quizzes achieved passing grade (≥80%). No pending or missing quizzes!", "var(--accent-green)"); } }; const btnHlMissingQuizzes = document.getElementById('btn-hl-missing-quizzes'); if (btnHlMissingQuizzes) { btnHlMissingQuizzes.onclick = handleMissingQuizzesClick; } const btnHlMissingQuizzesCourse = document.getElementById('btn-hl-missing-quizzes-course'); if (btnHlMissingQuizzesCourse) { btnHlMissingQuizzesCourse.onclick = handleMissingQuizzesClick; } const btnTriggerHlAnswers = document.getElementById('btn-trigger-hl-answers'); if (btnTriggerHlAnswers) { btnTriggerHlAnswers.onclick = () => { const courseInfo = detectCourseInfo(); const subCode = courseInfo.subjectCode || 'CS6301'; const cached = getCachedAnswers(subCode); const isExamSite = typeof isExamPortalDomain === 'function' && isExamPortalDomain(); const term = isExamSite ? 'exam' : 'quiz'; if (cached && cached.length > 0) { const res = highlightQuizAnswers(cached, false, true); if (res.matched === res.total && res.total > 0) { showToast(`Highlighted all ${res.total} answers!`); setLog(`Highlighted all ${res.total} answers!`, "var(--accent-green)"); } else if (res.matched === 0 && res.total > 0) { showToast(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`); setLog(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`, "var(--accent-amber)"); } else { showToast(`Highlighted ${res.matched}/${res.total} answers. (${res.total - res.matched} with no saved answer)`); setLog(`Highlighted ${res.matched}/${res.total} answers. (${res.total - res.matched} question(s) have no saved answer in database)`, "var(--accent-green)"); } } else if (typeof autoFetchCloudAnswersIfMissing === 'function') { showToast("Checking cloud database for answers..."); autoFetchCloudAnswersIfMissing(subCode).then(ok => { if (ok) { const fresh = getCachedAnswers(subCode); const res = highlightQuizAnswers(fresh, false, true); if (res.matched === res.total && res.total > 0) { showToast(`Highlighted all ${res.total} answers from cloud!`); setLog(`Highlighted all ${res.total} answers from cloud!`, "var(--accent-green)"); } else if (res.matched === 0 && res.total > 0) { showToast(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`); setLog(`No saved answers in database for this ${term}. Answer manually or use Gemini AI (C/V) to solve.`, "var(--accent-amber)"); } else { showToast(`Highlighted ${res.matched}/${res.total} answers from cloud. (${res.total - res.matched} with no saved answer)`); setLog(`Highlighted ${res.matched}/${res.total} answers from cloud. (${res.total - res.matched} question(s) have no saved answer in database)`, "var(--accent-green)"); } } else { showToast(`No answers found in database for ${subCode}`); } }); } else { showToast(`No answers cached for ${subCode}`); } }; } // --- MODULE 3: Search Module Elements --- const keywordInput = document.getElementById('search-keyword-input'); const copyKeywordBtn = document.getElementById('btn-copy-keyword'); const openGoogleBtn = document.getElementById('btn-open-google'); setupPersistentAccordion('mod-search-header', 'mod-search-body', 'mod-search-arrow', 'amaes_pref_mod_search', false); if (copyKeywordBtn) copyKeywordBtn.onclick = async () => { const query = keywordInput ? keywordInput.value.trim() : ''; if (!query) { showToast("Enter a search term first!"); return; } try { await copyToClipboard(query); setLog(`Copied: "${query}"`, "var(--accent-green)"); copyKeywordBtn.innerHTML = `${ICONS.check} Copied!`; showToast(`Copied search query: "${query}"`); setTimeout(() => { copyKeywordBtn.innerHTML = `${ICONS.copy} Copy`; }, 2000); } catch (e) { showToast("Failed to copy search keyword."); setLog("Failed to copy search keyword.", "var(--accent-pink)"); } }; if (openGoogleBtn) openGoogleBtn.onclick = async () => { const query = keywordInput ? keywordInput.value.trim() : ''; if (!query) { showToast("Enter a search term first!"); return; } try { await copyToClipboard(query); } catch (e) {} const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`; window.open(url, '_blank'); showToast(`Opened Google search for "${query}"`); setLog(`Opened Google search for: "${query}"`, "var(--accent-blue)"); }; // --- MODULE 4: Smart AI Assistant (Google Gemini) --- setupPersistentAccordion('mod-ai-header', 'mod-ai-body', 'mod-ai-arrow', 'amaes_pref_mod_ai', false); const btnOpenGeminiSetup = document.getElementById('btn-open-gemini-setup'); if (btnOpenGeminiSetup) { btnOpenGeminiSetup.onclick = () => { showGeminiSetupModal(); }; } const btnQuizConfigAi = document.getElementById('btn-quiz-config-ai'); if (btnQuizConfigAi) { btnQuizConfigAi.onclick = () => { showGeminiSetupModal(); }; } // Header Reset Settings Button Handler const resetBtn = document.getElementById('amaes-reset-btn'); if (resetBtn) { resetBtn.onclick = () => { const ok = confirm( "Reset AMAES Toolkit to a brand-new installation?\n\n" + "This clears toolkit settings, cached answer databases, anonymous sharing history, update state, and welcome-page acceptance. " + "It does not uninstall Violentmonkey or delete Moodle data.\n\n" + "The welcome setup will reopen after reset. Continue?" ); if (ok) { resetToolkitInstallation(); } }; } // Header Home / Dashboard Navigation Button Handler const homeBtn = document.getElementById('amaes-home-btn'); if (homeBtn) { homeBtn.onclick = (e) => { e.preventDefault(); showToast("Navigating to My Courses..."); window.location.href = getSemesterCoursesUrl(); }; } // Theme Toggle Handler if (themeBtn) themeBtn.onclick = () => { const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; applyTheme(nextTheme); showToast(`Theme switched to ${nextTheme} mode`); }; // Header Version Pill Click -> Direct Update if available, else quick check const versionPill = document.getElementById('amaes-version-pill'); if (versionPill) { versionPill.onclick = () => { const cachedLatest = localStorage.getItem('amaes_latest_version_seen'); if (cachedLatest && isNewerVersion(cachedLatest, SCRIPT_VERSION)) { window.open(SCRIPT_RAW_URL, '_blank'); } else { showToast("Checking GitHub for updates..."); checkForScriptUpdates(true); } }; } const welcomeUpdateNow = document.getElementById('btn-welcome-update-now'); if (welcomeUpdateNow) { welcomeUpdateNow.onclick = (e) => { e.preventDefault(); checkForScriptUpdates(true); }; } // --- MODULE 4: Marker Module Collapse --- setupPersistentAccordion('mod-marker-header', 'mod-marker-body', 'mod-marker-arrow', 'amaes_pref_mod_marker', !isQuiz); setupPersistentAccordion('sub-done-header', 'sub-done-body', 'sub-done-arrow', 'amaes_pref_sub_done', false); setupPersistentAccordion('sub-undo-header', 'sub-undo-body', 'sub-undo-arrow', 'amaes_pref_sub_undo', false); // --- MODULE 5: Accounts Module Cards --- setupPersistentAccordion('mod-accounts-switcher-header', 'mod-accounts-switcher-body', 'mod-accounts-switcher-arrow', 'amaes_pref_mod_accounts_switcher', true); setupPersistentAccordion('mod-accounts-transfer-header', 'mod-accounts-transfer-body', 'mod-accounts-transfer-arrow', 'amaes_pref_mod_accounts_transfer', false); // Lock State & Terms Acceptance Management const lockOverlay = document.getElementById('amaes-panel-lock-overlay'); const lockChk = document.getElementById('amaes-lock-chk-terms'); const lockContainer = document.getElementById('amaes-lock-terms-label'); const viewTermsBtn = document.getElementById('amaes-btn-lock-view-terms'); const updatePanelLockState = () => { const isAccepted = localStorage.getItem('amaes_terms_acknowledged') === 'true'; const isMin = localStorage.getItem('amaes_pref_minimized') === 'true'; if (!isAccepted) { if (lockOverlay) lockOverlay.style.display = isMin ? 'none' : 'flex'; if (bodyEl) bodyEl.style.display = 'none'; if (lockChk) lockChk.checked = false; if (lockContainer) lockContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)'; } else { if (lockOverlay) lockOverlay.style.display = 'none'; if (bodyEl) bodyEl.style.display = isMin ? 'none' : 'flex'; if (lockChk) lockChk.checked = true; } }; window._amaesUpdatePanelLockState = updatePanelLockState; if (lockChk) { lockChk.onchange = () => { if (lockChk.checked) { if (lockContainer) lockContainer.style.borderColor = 'rgba(16, 185, 129, 0.5)'; localStorage.setItem('amaes_terms_acknowledged', 'true'); showToast("Terms accepted! Toolkit unlocked. Refreshing to activate...", 2500); updatePanelLockState(); const welcomeTerms = document.getElementById('welcome-chk-terms'); const welcomeBtn = document.getElementById('btn-got-it-welcome'); if (welcomeTerms) welcomeTerms.checked = true; if (welcomeBtn) { welcomeBtn.disabled = false; welcomeBtn.style.opacity = '1'; } setTimeout(() => { window.location.reload(); }, 500); } else { localStorage.setItem('amaes_terms_acknowledged', 'false'); autoQuizMode = false; localStorage.setItem('amaes_auto_quiz_mode', 'false'); showToast("Agreement unaccepted. Pausing all tools and refreshing page...", 2500); updatePanelLockState(); setTimeout(() => { window.location.reload(); }, 500); } }; } if (viewTermsBtn) { viewTermsBtn.onclick = () => { showWelcomeOnboardingModal(true); }; } const bigLockIcon = document.getElementById('amaes-lock-big-icon'); if (bigLockIcon) { bigLockIcon.onclick = () => { showWelcomeOnboardingModal(true); }; } // Panel Minimize State Persistence & Responsive Sync const panelEl = document.getElementById('amaes-toolkit-panel'); const updateHudPauseBtnVisibility = () => { const hudPauseBtn = document.getElementById('btn-hud-toggle-quiz'); if (hudPauseBtn && panelEl) { const isMin = panelEl.classList.contains('amaes-minimized') || (bodyEl && bodyEl.style.display === 'none'); hudPauseBtn.style.display = isMin ? 'inline-flex' : 'none'; } }; const updatePanelMinState = (isMin) => { const backdropEl = document.getElementById('amaes-panel-backdrop'); const hudEl = document.getElementById('amaes-quiz-hud'); const root = document.documentElement; if (root) { root.classList.toggle('amaes-panel-is-minimized', isMin); } if (backdropEl) { backdropEl.classList.toggle('active', !isMin); } if (hudEl) { const isMobileScreen = (typeof window !== 'undefined' ? (window.innerWidth || 0) : 0) <= 640; if (isMobileScreen) { hudEl.style.display = isMin ? 'flex' : 'none'; } else { hudEl.style.display = 'flex'; } } updateHudPauseBtnVisibility(); }; const deviceState = (typeof detectDeviceMode === 'function') ? detectDeviceMode() : { isMobile: false, isTablet: false }; const prefMinimized = localStorage.getItem('amaes_pref_minimized'); const savedMinimized = prefMinimized !== null ? (prefMinimized === 'true') : deviceState.isMobile; if (savedMinimized) { if (bodyEl) bodyEl.style.display = 'none'; if (lockOverlay) lockOverlay.style.display = 'none'; if (panelEl) panelEl.classList.add('amaes-minimized'); minBtn.innerHTML = ` `; updatePanelMinState(true); } else { updatePanelMinState(false); } if (minBtn) minBtn.onclick = (e) => { if (e) e.stopPropagation(); const isLocked = localStorage.getItem('amaes_terms_acknowledged') !== 'true'; const targetEl = (isLocked && lockOverlay) ? lockOverlay : bodyEl; if (targetEl && targetEl.style.display === 'none') { targetEl.style.display = isLocked ? 'flex' : 'block'; if (panelEl) panelEl.classList.remove('amaes-minimized'); minBtn.innerHTML = ICONS.minimize; localStorage.setItem('amaes_pref_minimized', 'false'); updatePanelMinState(false); } else if (targetEl) { targetEl.style.display = 'none'; if (panelEl) panelEl.classList.add('amaes-minimized'); minBtn.innerHTML = ` `; localStorage.setItem('amaes_pref_minimized', 'true'); updatePanelMinState(true); } }; // Click anywhere on minimized pill to expand if (panelEl) { panelEl.addEventListener('click', (e) => { if (panelEl.classList.contains('amaes-minimized') && !e.target.closest('#amaes-min-btn')) { const isLocked = localStorage.getItem('amaes_terms_acknowledged') !== 'true'; const targetEl = (isLocked && lockOverlay) ? lockOverlay : bodyEl; if (targetEl) { targetEl.style.display = isLocked ? 'flex' : 'block'; panelEl.classList.remove('amaes-minimized'); minBtn.innerHTML = ICONS.minimize; localStorage.setItem('amaes_pref_minimized', 'false'); updatePanelMinState(false); } } }); } // Backdrop click to minimize bottom sheet on mobile const backdropEl = document.getElementById('amaes-panel-backdrop'); if (backdropEl) { backdropEl.onclick = (e) => { e.stopPropagation(); if (panelEl && !panelEl.classList.contains('amaes-minimized') && minBtn) { minBtn.click(); } }; } // Pull handle click & swipe-down touch gestures on mobile const sheetHandle = panelEl ? panelEl.querySelector('.amaes-sheet-handle') : null; if (sheetHandle) { sheetHandle.onclick = (e) => { e.stopPropagation(); if (panelEl && !panelEl.classList.contains('amaes-minimized') && minBtn) { minBtn.click(); } }; } let touchStartY = 0; let touchDiffY = 0; const panelDragZone = sheetHandle || (panelEl ? panelEl.querySelector('#amaes-header') : null); if (panelDragZone) { panelDragZone.addEventListener('touchstart', (e) => { if (panelEl && panelEl.classList.contains('amaes-minimized')) return; touchStartY = e.touches[0].clientY; touchDiffY = 0; }, { passive: true }); panelDragZone.addEventListener('touchmove', (e) => { if (panelEl && panelEl.classList.contains('amaes-minimized')) return; const currentY = e.touches[0].clientY; touchDiffY = currentY - touchStartY; }, { passive: true }); panelDragZone.addEventListener('touchend', () => { if (panelEl && panelEl.classList.contains('amaes-minimized')) return; if (touchDiffY > 45 && minBtn) { minBtn.click(); } touchDiffY = 0; }, { passive: true }); } // Initialize lock state updatePanelLockState(); const helpBtn = document.getElementById('amaes-help-btn'); if (helpBtn) { if (helpBtn) helpBtn.onclick = () => { showWelcomeOnboardingModal(true); }; } const bugBtn = document.getElementById('amaes-bug-btn'); if (bugBtn) { bugBtn.onclick = () => { showBugReportModal(); }; } const headerBugBtn = document.getElementById('amaes-bug-header-btn'); if (headerBugBtn) { headerBugBtn.onclick = () => { showBugReportModal(); }; } // Debug Copy Handler if (debugBtn) { if (debugBtn) debugBtn.onclick = async () => { logDebug("Exporting debug diagnostic report..."); const report = generateDebugReport(); try { await copyToClipboard(report); debugBtn.innerHTML = ICONS.check; setLog("Debug report copied to clipboard. You can paste it directly to AI.", "var(--accent-green)"); setTimeout(() => { debugBtn.innerHTML = ICONS.debug; }, 2500); } catch (err) { console.error("Clipboard write error:", err); setLog("Failed to copy automatically. Check browser console.", "var(--accent-pink)"); } }; } // Activity Log Feed Toggle & Clear Handlers const btnToggleLogs = document.getElementById('amaes-btn-toggle-logs'); const activityFeed = document.getElementById('amaes-activity-feed'); if (btnToggleLogs && activityFeed) { btnToggleLogs.onclick = (e) => { e.preventDefault(); const isHidden = activityFeed.style.display === 'none'; activityFeed.style.display = isHidden ? 'flex' : 'none'; if (isHidden) renderActivityLogs(); localStorage.setItem('amaes_pref_show_logs', isHidden ? 'true' : 'false'); }; const savedShowLogs = localStorage.getItem('amaes_pref_show_logs') === 'true'; if (savedShowLogs) { activityFeed.style.display = 'flex'; renderActivityLogs(); } } const btnClearLogs = document.getElementById('amaes-btn-clear-logs'); if (btnClearLogs) { btnClearLogs.onclick = (e) => { e.preventDefault(); activityHistory.length = 0; renderActivityLogs(); const countBadge = document.getElementById('amaes-log-count-badge'); if (countBadge) countBadge.innerText = 'Log (0)'; }; } const btnCopyLogs = document.getElementById('amaes-btn-copy-logs'); if (btnCopyLogs) { btnCopyLogs.onclick = async (e) => { e.preventDefault(); if (activityHistory.length === 0) { showToast("No logs recorded yet to copy!"); return; } const lines = activityHistory.map(item => `[${item.time}] ${item.text}`).reverse(); const logText = lines.join('\n'); try { await copyToClipboard(logText); showToast(`Copied ${activityHistory.length} log entries!`); setLog(`Copied ${activityHistory.length} activity log entries to clipboard.`, "var(--accent-blue)"); } catch (err) { showToast("Failed to copy logs to clipboard."); } }; } // Batch Execution Engine for Auto-Marker const runBatch = async (goal, category) => { if (isRunning) return; if (typeof isFeatureDisabledByAdmin === 'function' && isFeatureDisabledByAdmin('autoMarker')) { showToast("Activity Auto-Marker is temporarily disabled by administrator."); setLog("[Admin Notice] Activity Auto-Marker is temporarily disabled by administrator.", "var(--accent-amber)"); return; } if (!checkIsCoursePage()) { const isGrades = typeof window !== 'undefined' && window.location.pathname.includes('/grade/report/user/index.php'); if (isGrades) { showToast("Open the Course page to mark lecture items as done."); setLog("Notice: Completion checkboxes only live on Course pages. Open the Course page to mark lectures as done.", "var(--accent-amber)"); } else { showToast("Open a course page first to use Activity Auto-Marker!"); setLog("Action Blocked: You are not on a course page. Open a course subject first.", "var(--accent-pink)"); } return; } const isExamPortal = (typeof detectAssessmentOrigin === 'function' && detectAssessmentOrigin().isExam) || (typeof window !== 'undefined' && window.location.hostname.includes('exam')); if (isExamPortal && category === 'lecture') { showToast("Lectures are not hosted on the Exam Portal."); setLog("Exam Portal Mode: Lectures and reading materials are not hosted on the Exam Portal. Use Exam Auto-Marker instead.", "var(--accent-amber)"); return; } isRunning = true; shouldStop = false; stopBtn.style.display = 'flex'; let gradesMap = null; if (goal === 'mark_done' && (category === 'quiz' || category === 'all')) { setLog(isExamPortal ? "Verifying course grades: Only exams with a passable grade (≥80%) will be marked..." : "Verifying course grades: Only quizzes with a passable grade (≥80%) will be marked...", "var(--accent-blue)"); if (typeof fetchCourseGradesMap === 'function') { gradesMap = await fetchCourseGradesMap(); } } const actionLabel = goal === 'mark_done' ? 'Marking' : 'Undoing'; setLog(`Searching for ${category} items to ${goal === 'mark_done' ? 'complete' : 'undo'}...`); const items = findButtons(goal, category, gradesMap); if (items.length === 0) { if (goal === 'mark_done' && (category === 'quiz' || (isExamPortal && category === 'all'))) { showToast(isExamPortal ? "No exams with passable grade (≥80%) found to mark." : "No quizzes with passable grade (≥80%) found to mark."); setLog(isExamPortal ? "Zero Matching Exams: Only exams with a passing grade (≥80%) are marked. Incomplete, locked, or failing exams were left untouched." : "Zero Matching Quizzes: Only quizzes with a passing grade (≥80%) are marked. Incomplete, locked, or failing quizzes were left untouched.", "var(--accent-amber)"); } else { showToast(`No uncompleted ${category} items found to ${goal === 'mark_done' ? 'mark' : 'undo'}!`); setLog(`No matching items found for: ${category} (${goal})!`, "var(--accent-green)"); } finish(); return; } showToast(`Found ${items.length} ${category} items to ${goal === 'mark_done' ? 'mark' : 'undo'}. Processing...`, 2500); // Partition items into Fast Track (manual clicks / undo) vs Safe Track (auto-view network fetches) const fastItems = []; const autoViewItems = []; for (const item of items) { if (item.isAutoView && item.autoViewUrl) { autoViewItems.push(item); } else if (item.button) { fastItems.push(item); } } const totalCount = items.length; showToast(`Found ${totalCount} ${category} items to ${goal === 'mark_done' ? 'mark' : 'undo'}. Processing...`, 2500); let processedCount = 0; // Track 1: Fast Micro-Stagger Queue for Manual Buttons and Undo (Lectures, Passed Quizzes, Undo All) if (fastItems.length > 0 && !shouldStop) { setLog(`Processing ${fastItems.length} manual ${actionLabel.toLowerCase()} actions in high-speed batch...`); for (let i = 0; i < fastItems.length; i++) { if (shouldStop) break; const item = fastItems[i]; processedCount++; // Throttled UI logging to avoid log feed DOM flooding on large lists if (i === 0 || i === fastItems.length - 1 || i % 3 === 0) { const gradeSuffix = item.gradePct !== null && item.gradePct !== undefined ? ` [${item.gradePct}%]` : ''; setLog(`[${processedCount}/${totalCount}] ${actionLabel}: ${item.title.substring(0, 22)}...${gradeSuffix}`); } // Reliable pointer dispatch without jarring smooth-scroll if (typeof triggerClick === 'function') { triggerClick(item.button); } else { item.button.click(); } if (goal === 'mark_done') { try { item.button.classList.remove('btn-outline-secondary'); item.button.classList.add('btn-outline-success'); item.button.style.opacity = '0.8'; } catch (_) {} } // Yield to browser event loop: keeps UI 60fps responsive, Stop button functional, and avoids server session queue deadlocks await new Promise(resolve => setTimeout(resolve, 40)); } } // Track 2: Safe Sequential Queue for Activities Requiring Page Loads ("To do: View") if (autoViewItems.length > 0 && !shouldStop) { setLog(`Opening ${autoViewItems.length} lecture activities requiring view verification...`, "var(--accent-blue)"); for (let i = 0; i < autoViewItems.length; i++) { if (shouldStop) break; const item = autoViewItems[i]; processedCount++; setLog(`[${processedCount}/${totalCount}] Viewing: ${item.title.substring(0, 22)}...`); if (typeof autoViewActivity === 'function') { await autoViewActivity(item.autoViewUrl, item.badgeElem, item.container); } // Safe network cadence await new Promise(resolve => setTimeout(resolve, 250)); } } if (shouldStop) { showToast(`Stopped. Processed ${processedCount} of ${totalCount} items.`); setLog(`Stopped by user. Processed ${processedCount} of ${totalCount} items.`, "var(--accent-amber)"); } else { if (typeof playToolkitSound === 'function') { playToolkitSound('quest_done'); } showToast(`Finished ${actionLabel.toLowerCase()} ${processedCount} items!`); if (goal === 'mark_done' && (category === 'quiz' || category === 'all')) { setLog(`Successfully finished ${actionLabel.toLowerCase()} ${processedCount} items! Any ${isExamPortal ? 'exams' : 'quizzes'} without a passable grade (≥80%) remained untouched.`, "var(--accent-green)"); } else { setLog(`Successfully finished ${actionLabel.toLowerCase()} ${processedCount} items!`, "var(--accent-green)"); } } finish(); }; const finish = () => { isRunning = false; stopBtn.style.display = 'none'; }; if (stopBtn) stopBtn.onclick = () => { shouldStop = true; showToast("Stopping after current item..."); setLog('Stopping after current item...', "var(--accent-amber)"); }; // Attach Clicks const _el__btn_mark_lec_ = document.getElementById('btn-mark-lec'); if (_el__btn_mark_lec_) _el__btn_mark_lec_.onclick = () => runBatch('mark_done', 'lecture'); const _el__btn_mark_quiz_ = document.getElementById('btn-mark-quiz'); if (_el__btn_mark_quiz_) _el__btn_mark_quiz_.onclick = () => runBatch('mark_done', 'quiz'); const _el__btn_mark_all_ = document.getElementById('btn-mark-all'); if (_el__btn_mark_all_) _el__btn_mark_all_.onclick = () => runBatch('mark_done', 'all'); const _el__btn_undo_lec_ = document.getElementById('btn-undo-lec'); if (_el__btn_undo_lec_) _el__btn_undo_lec_.onclick = () => runBatch('undo', 'lecture'); const _el__btn_undo_quiz_ = document.getElementById('btn-undo-quiz'); if (_el__btn_undo_quiz_) _el__btn_undo_quiz_.onclick = () => runBatch('undo', 'quiz'); const _el__btn_undo_all_ = document.getElementById('btn-undo-all'); if (_el__btn_undo_all_) _el__btn_undo_all_.onclick = () => runBatch('undo', 'all'); // Quiz Automation will be initialized once when document is ready } async function initializeToolkit() { await handleAccountSwitcherNavigation(); if (isUserLoggedIn()) { await completeAccountSwitcherLoginCapture(); if (await completeAccountSwitcherReturn()) return; } else { await setupAccountSwitcherLoginCapture(); logDebug("User not logged in; skipping UI mounting."); return; } if (!(await verifyClientCompatibility())) return; checkPendingUpdateInstallation(); setupPendingUpdateFocusListener(); document.getElementById('amaes-topbar-version-badge')?.remove(); const cachedLatest = localStorage.getItem('amaes_latest_version_seen'); if (cachedLatest && isNewerVersion(cachedLatest, SCRIPT_VERSION)) { injectTopNavUpdateNotification(cachedLatest); } else { localStorage.removeItem('amaes_latest_version_seen'); document.getElementById('amaes-topnav-update-item')?.remove(); } createPanel(); checkForScriptUpdates(false); // Strict Terms Acceptance Guard: if not acknowledged, pause all tools except update checking const isTermsAccepted = toolkitStorage.get('amaes_terms_acknowledged') === 'true'; if (!isTermsAccepted) { autoQuizMode = false; toolkitStorage.set('amaes_auto_quiz_mode', 'false'); showWelcomeOnboardingModal(false); logDebug("Terms not acknowledged. Toolkit paused in locked state."); return; } startCapabilityTips(); setupQuizAutomation(); setupQuizKeyboardShortcuts(); if (typeof decorateNativeActivityNavigation === 'function') { decorateNativeActivityNavigation(); } if (typeof injectQuizBottomNavigation === 'function') { injectQuizBottomNavigation(); } setupBreadcrumbListeners(); if (typeof setupAutoViewBadges === 'function') { setupAutoViewBadges(); if (typeof scanAndAutoMark100PercentQuizzesOnCoursePage === 'function') { scanAndAutoMark100PercentQuizzesOnCoursePage(); } if (typeof checkIsCoursePage === 'function' && checkIsCoursePage()) { let courseViewDebounce = null; const courseViewObs = new MutationObserver(() => { if (courseViewDebounce) clearTimeout(courseViewDebounce); courseViewDebounce = setTimeout(() => { setupAutoViewBadges(); if (typeof scanAndAutoMark100PercentQuizzesOnCoursePage === 'function') { scanAndAutoMark100PercentQuizzesOnCoursePage(); } }, 400); }); const mainTarget = document.getElementById('region-main') || document.querySelector('.course-content') || document.body; if (mainTarget) { courseViewObs.observe(mainTarget, { childList: true, subtree: true }); } } } showWelcomeOnboardingModal(false); injectDashboardCourseBadges(); if (typeof autoCloseCourseIndexDrawer === 'function') { autoCloseCourseIndexDrawer(); } if (typeof injectCourseConfidenceCard === 'function') { injectCourseConfidenceCard(); } else { injectCourseReviewabilityNotice(); } sendPassiveTelemetryPulse(); if (checkIsQuizViewPage() || checkIsQuizSummaryPage()) { promoteAttemptEvidenceFromScore(); setTimeout(() => { try { promoteAttemptEvidenceFromScore(); } catch (_) {} }, 800); } if (checkIsReviewPage()) { if (typeof injectReviewRetakeButton === 'function') { injectReviewRetakeButton(); } } if (checkIsQuizViewPage()) { if (typeof checkAutoStartAttemptOnLanding === 'function') { checkAutoStartAttemptOnLanding(); } try { const landingData = parseQuizLandingData(); if (landingData && landingData.cmid) { localStorage.setItem('amaes_quiz_attempts_' + landingData.cmid, JSON.stringify(landingData)); } } catch (_) {} if (typeof injectQuizBottomNavigation === 'function') { injectQuizBottomNavigation(); setTimeout(() => { try { injectQuizBottomNavigation(); } catch (_) {} }, 600); } } // Auto-Harvest past quizzes: scan Grade Report once per session per course or all courses on dashboard if (autoHarvestGrades) { try { const isGradesPage = window.location.pathname.includes('/grade/report/user/index.php'); const isCoursePage = window.location.pathname.includes('/course/view.php'); const isDashboard = window.location.pathname.includes('/my/') || window.location.pathname.includes('courses.php'); if (isDashboard) { if (!sessionStorage.getItem('amaes_dash_harvested')) { sessionStorage.setItem('amaes_dash_harvested', '1'); setTimeout(() => { setLog("Auto-Harvest: Scanning all enrolled courses in background...", "var(--accent-blue)", "Auto-populating database with past verified answers"); executeGradesHarvester(); }, 2500); } } else if (isGradesPage || isCoursePage) { const cInfo = detectCourseInfo(); const courseKey = (cInfo && cInfo.subjectCode && cInfo.subjectCode !== 'DEFAULT' && cInfo.subjectCode !== 'GENERAL') ? cInfo.subjectCode : (cInfo?.courseId || new URLSearchParams(window.location.search).get('id') || 'grades'); const lastScan = parseInt(sessionStorage.getItem(`amaes_auto_harvest_time_${courseKey}`) || '0', 10); const shouldScan = (Date.now() - lastScan > 60000); if (shouldScan) { sessionStorage.setItem(`amaes_auto_harvest_time_${courseKey}`, String(Date.now())); setTimeout(() => { if (isCoursePage && typeof batchHarvestCourseReviews === 'function') { setLog(`Auto-Harvest: Scanning all available reviews for ${courseKey} in background...`, "var(--accent-blue)"); batchHarvestCourseReviews(cInfo?.courseId, cInfo?.subjectCode, null, true); } else { setLog(`Auto-Harvest: Scanning past quizzes for ${courseKey} in background...`, "var(--accent-blue)"); executeGradesHarvester(); } }, 1200); } } } catch (e) { logDebug(`Auto grades harvest error: ${e.message}`); } } // Auto Cloud Sync: sync answers from community repository once per session in background if (autoCloudSync) { try { const cInfo = detectCourseInfo(); const sc = cInfo ? cInfo.subjectCode : null; if (sc && sc !== 'GENERAL' && sc !== 'DEFAULT' && !sessionStorage.getItem(`amaes_cloud_synced_${sc}`)) { sessionStorage.setItem(`amaes_cloud_synced_${sc}`, '1'); setLog(`Auto-syncing community database for ${sc}...`, "var(--accent-blue)"); syncAnswersFromCloud(sc, null, cInfo.subjectName || '').then(async (res) => { if (res && res.count > 0) { const jennyCount = res.jennysonlineCount || 0; showToast(`Loaded ${res.cachedCount || 0} cached entries and ${jennyCount} unconfirmed Jenny suggestions for ${sc}.`); setLog(`Loaded ${res.cachedCount || 0} cached entries and ${jennyCount} locally cached, unconfirmed Jenny suggestions for ${sc}.`, "var(--accent-green)"); const fresh = getCachedAnswers(sc); const lbl = document.getElementById('fetch-btn-label'); if (lbl && fresh) lbl.innerText = `Refresh Answers (${fresh.length} cached)`; if (checkIsQuizPage()) { const jennyAnswers = jennyCount > 0 ? await loadJennysonlineAnswersForCourse(sc) : []; highlightQuizAnswers((fresh || []).concat(jennyAnswers), false); } } else if (autoScrapeAmauoed) { const link = await autoFindAmauoedLink(sc, cInfo.subjectName || ''); if (link) { const scraped = await loadAllAmauoedAnswers(link); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(sc, scraped, 'AMAUOED'); showToast(`Auto-scraped ${scraped.length} answers from AMAUOED for ${sc}!`); setLog(`Auto-scraped ${scraped.length} answers for ${sc} from AMAUOED.`, "var(--accent-green)"); const fresh = getCachedAnswers(sc); const lbl = document.getElementById('fetch-btn-label'); if (lbl && fresh) lbl.innerText = `Refresh Answers (${fresh.length} cached)`; if (checkIsQuizPage()) { highlightQuizAnswers(fresh, false); } } } } }).catch(async (err) => { logDebug(`Auto cloud sync note for ${sc}: ${err.message}`); if (autoScrapeAmauoed) { const link = await autoFindAmauoedLink(sc, cInfo.subjectName || ''); if (link) { const scraped = await loadAllAmauoedAnswers(link); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(sc, scraped, 'AMAUOED'); showToast(`Auto-scraped ${scraped.length} answers from AMAUOED for ${sc}!`); setLog(`Auto-scraped ${scraped.length} answers for ${sc} from AMAUOED.`, "var(--accent-green)"); const fresh = getCachedAnswers(sc); const lbl = document.getElementById('fetch-btn-label'); if (lbl && fresh) lbl.innerText = `Refresh Answers (${fresh.length} cached)`; if (checkIsQuizPage()) { highlightQuizAnswers(fresh, false); } } } } }); } } catch (e) { logDebug(`Auto cloud sync error: ${e.message}`); } // Dashboard Auto-Sync: automatically sync all detected courses visible on dashboard try { const isDashboard = window.location.pathname.includes('/my/') || window.location.pathname.includes('courses.php') || window.location.pathname === '/' || window.location.pathname.endsWith('/index.php'); if (isDashboard) { setTimeout(() => { const dashCourses = detectDashboardCourses(); dashCourses.forEach(c => { if (c.code && !sessionStorage.getItem(`amaes_cloud_synced_${c.code}`)) { sessionStorage.setItem(`amaes_cloud_synced_${c.code}`, '1'); syncAnswersFromCloud(c.code, null, c.title || '').then(res => { if (res && res.count > 0) { injectDashboardCourseBadges(); } }).catch(async (e) => { logDebug(`Dashboard auto-sync note for ${c.code}: ${e.message}`); if (autoScrapeAmauoed) { const link = await autoFindAmauoedLink(c.code, c.title || ''); if (link) { const scraped = await loadAllAmauoedAnswers(link); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(c.code, scraped, 'AMAUOED'); injectDashboardCourseBadges(); } } } }); } }); }, 1200); } } catch (e) { logDebug(`Dashboard auto-sync error: ${e.message}`); } } // Observe DOM mutations on dashboard to tag dynamically loaded course cards & auto-sync if (window.location.pathname.includes('/my/') || window.location.pathname.includes('courses.php')) { let debounceTimer = null; const obs = new MutationObserver(() => { injectDashboardCourseBadges(); if (autoCloudSync) { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const dashCourses = detectDashboardCourses(); dashCourses.forEach(c => { if (c.code && !sessionStorage.getItem(`amaes_cloud_synced_${c.code}`)) { sessionStorage.setItem(`amaes_cloud_synced_${c.code}`, '1'); syncAnswersFromCloud(c.code, null, c.title || '').then(res => { if (res && res.count > 0) { injectDashboardCourseBadges(); } }).catch(async (e) => { logDebug(`Dashboard observer auto-sync note for ${c.code}: ${e.message}`); if (autoScrapeAmauoed) { const link = await autoFindAmauoedLink(c.code, c.title || ''); if (link) { const scraped = await loadAllAmauoedAnswers(link); if (scraped && scraped.length > 0) { mergeAnswersIntoCache(c.code, scraped, 'AMAUOED'); injectDashboardCourseBadges(); } } } }); } }); }, 1500); } }); obs.observe(document.body, { childList: true, subtree: true }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeToolkit); } else { initializeToolkit(); } })();