1/* Quiz catalogue and page adapter for the AI & ML Tutor app. 2 * 3 * The original quiz was driven by js/quiz.js, which held a `categories` array 4 * mapping a topic name to a /server_data/*.json file and rendered questions 5 * into a bespoke #game panel. Three things were wrong with it: 6 * 7 * 1. It only understood banks with flat A/B/C/D keys. Two files use 8 * `Choices`/`Answers` instead, so 338 records rendered with no options. 9 * 2. Several topics pointed at files that do not exist (gcp_ml_mock_exam) or 10 * that contain no real questions (ml_computer_vision: 59 blank records). 11 * 3. Duplicate topics pointed at the same bank under different spellings. 12 * 13 * Banks are now schema v2 in /quizzes/, and rendering is handled by 14 * quiz-engine.js -- the same engine the Djamgatech app uses. This file is the 15 * glue: it owns the topic list and starts the engine when a topic is chosen. 16 */ 17(function () { 18 'use strict'; 19 20 /* topic label -> schema-v2 bank id in /quizzes/. 21 Only banks that exist and contain real questions appear here. 22 23 `retired` marks a bank whose target exam no longer exists. We keep these 24 banks -- the underlying ML theory is still good study material -- but they 25 are grouped separately, labelled with the retirement date, and show a 26 migration notice pointing at the replacement bank. Silently serving them 27 as current "certification prep" would mislead someone paying for an exam. 28 29 `successor` is the bank id a retired exam's candidates should move to. */ 30 var CATALOGUE = [ 31 { id: 'machine_learning_basics', label: 'Machine Learning Basics', group: 'Foundations' }, 32 { id: 'machine_learning_advanced', label: 'Machine Learning Advanced', group: 'Foundations' }, 33 { id: 'ai_basics', label: 'AI Basics', group: 'Foundations' }, 34 { id: 'ai_advanced', label: 'AI Advanced', group: 'Foundations' }, 35 { id: 'deep_learning', label: 'Deep Learning', group: 'Foundations' }, 36 { id: 'neural_networks', label: 'Neural Networks', group: 'Foundations' }, 37 { id: 'ml_nlp', label: 'Natural Language Processing', group: 'Specialisms' }, 38 { id: 'generative_ai', label: 'Generative AI', group: 'Specialisms' }, 39 { id: 'explainable_ai', label: 'Explainable AI', group: 'Specialisms' }, 40 { id: 'ml_ops_on_aws_azure_gcp', label: 'MLOps on AWS, Azure and GCP', group: 'Specialisms' }, 41 { id: 'ai_ops_on_aws_azure_gcp', label: 'AIOps on AWS, Azure and GCP', group: 'Specialisms' }, 42 { id: 'ml_implementation_and_operations', label: 'ML Implementation and Operations', group: 'Specialisms' }, 43 44 { id: 'aws_ai_practitioner', label: 'AWS Certified AI Practitioner', group: 'Certification prep â AWS' }, 45 { id: 'aws_ml_engineer_associate', label: 'AWS Certified ML Engineer â Associate (MLA-C02)', group: 'Certification prep â AWS' }, 46 { id: 'aws_genai_developer_professional', label: 'AWS Certified GenAI Developer â Professional (AIP-C01)', group: 'Certification prep â AWS' }, 47 { id: 'aws_data_engineer_exam_questions', label: 'AWS Certified Data Engineer Associate', group: 'Certification prep â AWS' }, 48 { id: 'formatted_aws_data_engineer_exam', label: 'AWS Data Engineer â Extra Set', group: 'Certification prep â AWS' }, 49 { id: 'azure_ai_apps_agents_ai103', label: 'Azure AI Apps & Agents Developer (AI-103)', group: 'Certification prep â Microsoft' }, 50 51 { id: 'azure_ai_mock_exam', label: 'Microsoft Azure AI Fundamentals (AI-901)', group: 'Certification prep â Microsoft' }, 52 53 { id: 'gcp_professional_ml_engineer', label: 'Google Professional ML Engineer', group: 'Certification prep â Google Cloud' }, 54 { id: 'gcp_generative_ai_leader', label: 'Google Cloud Generative AI Leader', group: 'Certification prep â Google Cloud' }, 55 { id: 'gcp_ml_implementation_and_operations', label: 'GCP ML: Implementation and Operations', group: 'Certification prep â Google Cloud' }, 56 57 { id: 'nvidia_nca_genl', label: 'NVIDIA Certified Associate: Generative AI LLMs (NCA-GENL)', group: 'Certification prep â Other vendors' }, 58 { id: 'databricks_genai_engineer_associate', label: 'Databricks Certified Generative AI Engineer Associate', group: 'Certification prep â Other vendors' }, 59 60 /* AWS ML Specialty (MLS-C01) retired 31 March 2026. Questions remain 61 useful as general AWS ML study material. */ 62 { id: 'aws_machine_learning_mock_exam', label: 'AWS ML Specialty (MLS-C01) â retired Mar 2026', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' }, 63 { id: 'aws_machine_learning_specialty_practice_exam2', label: 'AWS ML Specialty Practice Exam 2 â retired Mar 2026', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' }, 64 { id: 'aws_ml_data_engineering', label: 'AWS ML: Data Engineering (MLS-C01 domain)', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' }, 65 { id: 'aws_ml_exploratory_data_analysis', label: 'AWS ML: Exploratory Data Analysis (MLS-C01 domain)', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' }, 66 { id: 'aws_ml_modeling', label: 'AWS ML: Modeling (MLS-C01 domain)', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' }, 67 { id: 'aws_ml_implementation_and_operations', label: 'AWS ML: Implementation & Operations (MLS-C01 domain)', group: 'Legacy / retired exams', retired: 'MLS-C01 retired 31 March 2026' } 68 ]; 69 70 /* Order groups deliberately: current certification prep first, retired last. */ 71 var GROUP_ORDER = [ 72 'Certification prep â AWS', 73 'Certification prep â Microsoft', 74 'Certification prep â Google Cloud', 75 'Certification prep â Other vendors', 76 'Foundations', 77 'Specialisms', 78 'Legacy / retired exams' 79 ]; 80 81 function entryFor(bankId) { 82 for (var i = 0; i < CATALOGUE.length; i++) { 83 if (CATALOGUE[i].id === bankId) return CATALOGUE[i]; 84 } 85 return null; 86 } 87 88 /* Legacy topic labels that used to appear in the <select>, so old bookmarks 89 and any cached markup still resolve. Left-hand side is what the old page 90 used; right-hand side is the bank we now serve. 91 'GCP ML' pointed at gcp_ml_mock_exam.json, which has never existed in this 92 repository -- it is redirected to the Google ML Engineer bank. */ 93 var ALIASES = { 94 'Machine Learning Basics': 'machine_learning_basics', 95 'Sciences - Machine Learning Advanced': 'machine_learning_advanced', 96 'Machine Learning Advanced': 'machine_learning_advanced', 97 'ML NLP Natural Language Processing': 'ml_nlp', 98 'AWS ML NLP': 'ml_nlp', 99 'ML OPS on AWS Azure GCP': 'ml_ops_on_aws_azure_gcp', 100 'Machine Learning OPS on AWS Azure GCP': 'ml_ops_on_aws_azure_gcp', 101 'AI OPS on AWS Azure GCP': 'ai_ops_on_aws_azure_gcp', 102 'AI Basics': 'ai_basics', 103 'AI Advanced': 'ai_advanced', 104 'AWS AI Practitioner': 'aws_ai_practitioner', 105 'AWS Data Engineer': 'aws_data_engineer_exam_questions', 106 'Deep Learning': 'deep_learning', 107 'Explainable AI': 'explainable_ai', 108 'Generative AI': 'generative_ai', 109 'Neural Networks': 'neural_networks', 110 'AWS Machine Learning exam Prep': 'aws_machine_learning_mock_exam', 111 'AWS Machine Learning Specialty': 'aws_machine_learning_mock_exam', 112 'Azure AI': 'azure_ai_mock_exam', 113 'Azure AI Fundamentals': 'azure_ai_mock_exam', 114 'Google Professional Machine Learning Engineer': 'gcp_professional_ml_engineer', 115 'GCP ML': 'gcp_professional_ml_engineer', 116 'AWS ML Exploratory Data Analysis': 'aws_ml_exploratory_data_analysis', 117 'AWS ML Data Engineering': 'aws_ml_data_engineering', 118 'AWS ML Modelling': 'aws_ml_modeling', 119 'AWS ML Modeling': 'aws_ml_modeling' 120 }; 121 122 function resolve(value) { 123 if (!value) return null;
124 if (ALIASES[value]) return ALIASES[value]; 125 for (var i = 0; i < CATALOGUE.length; i++) { 126 if (CATALOGUE[i].id === value || CATALOGUE[i].label === value) { 127 return CATALOGUE[i].id; 128 } 129 } 130 return null; 131 } 132 133 /* The old renderer owned these nodes. The engine draws everything inside 134 #qz-root, so leaving them visible shows a stale question and a dead timer 135 above the real quiz. */ 136 var LEGACY_NODES = ['game', 'currentanswerbutton', 'currentanswer']; 137 138 /* The quiz sits inside <div id="app" class="workorder-card">, which has an 139 opaque WHITE background while the rest of the page is near-black. On a 140 phone that renders as a white slab around the quiz and reads as a broken 141 layout. Tag the ancestors while a quiz is running so the theme stylesheet 142 can make them transparent, and untag on exit so the rest of the page keeps 143 its normal card styling. */ 144 function markHostChain(on) { 145 var n = document.getElementById('quiz-wrapper'); 146 while (n && n !== document.body) { 147 if (on) { n.classList.add('qz-host'); } else { n.classList.remove('qz-host'); } 148 n = n.parentElement; 149 } 150 } 151 152 function hideLegacy() { 153 LEGACY_NODES.forEach(function (id) { 154 var el = document.getElementById(id); 155 if (el) el.style.display = 'none'; 156 }); 157 } 158 159 function showLegacy() { 160 LEGACY_NODES.forEach(function (id) { 161 var el = document.getElementById(id); 162 if (el) el.style.display = ''; 163 }); 164 } 165 166 function ensureWrapper() { 167 var wrap = document.getElementById('quiz-wrapper'); 168 if (wrap) return wrap; 169 wrap = document.createElement('section'); 170 wrap.id = 'quiz-wrapper'; 171 var anchor = document.getElementById('game'); 172 if (anchor && anchor.parentNode) { 173 anchor.parentNode.insertBefore(wrap, anchor.nextSibling); 174 } else { 175 (document.getElementById('app') || document.body).appendChild(wrap); 176 } 177 return wrap; 178 } 179 180 /* Show an honest, dismissible notice above a retired exam's quiz so nobody 181 studies for an exam they can no longer sit without knowing it. */ 182 function renderRetirementNotice(bankId) { 183 var existing = document.getElementById('qz-retired-notice'); 184 if (existing) existing.remove(); 185 186 var entry = entryFor(bankId); 187 if (!entry || !entry.retired) return; 188 189 var box = document.createElement('div'); 190 box.id = 'qz-retired-notice'; 191 box.style.cssText = 'background:rgba(249,115,22,0.10);border:1px solid rgba(249,115,22,0.45);' + 192 'border-left:4px solid #F97316;border-radius:10px;padding:1rem 1.25rem;margin:0 0 1rem;'; 193 box.innerHTML = 194 '<p style="margin:0 0 .4rem;font-weight:700;color:#F97316;font-size:.95rem;">' + 195 '⚠️ This exam has retired' + 196 '</p>' + 197 '<p style="margin:0;color:rgba(255,255,255,0.85);font-size:.9rem;line-height:1.5;">' +
198 entry.retired + '. These questions are still solid AWS machine-learning study ' + 199 'material, but they no longer map to a bookable exam. If you are certifying now, ' + 200 'AWS points candidates to <a href="/quiz/aws_ml_engineer_associate.html" ' + 201 'style="color:#F97316;font-weight:600;">Certified Machine Learning Engineer – ' + 202 'Associate (MLA-C02)</a> instead.' + 203 '</p>'; 204 205 var wrap = ensureWrapper(); 206 wrap.insertBefore(box, wrap.firstChild); 207 } 208 209 function start(bankId, opts) { 210 if (!bankId) return; 211 var scroll = !opts || opts.scroll !== false; 212 ensureWrapper(); 213 hideLegacy(); 214 markHostChain(true); 215 renderRetirementNotice(bankId); 216 if (!window.loadQuiz || !window.startQuiz) { 217 console.error('[quiz-catalogue] quiz-engine.js has not loaded'); 218 return; 219 } 220 window.loadQuiz(bankId).then(function () { 221 window.startQuiz(); 222 if (window.__qzBackBtn) window.__qzBackBtn.style.display = ''; 223 var wrap = document.getElementById('quiz-wrapper'); 224 if (wrap && scroll) wrap.scrollIntoView({ behavior: 'smooth', block: 'start' }); 225 }).catch(function (err) { 226 // A newer selection already superseded this one -- that request's own 227 // .then() (or a still-pending one) owns the screen now, so leave it 228 // alone rather than stomping it with an error card. 229 if (err && err.superseded) return; 230 console.error('[quiz-catalogue] could not load bank', bankId, err); 231 var wrap = ensureWrapper(); 232 wrap.innerHTML = '<div class="qz-card"><p>Sorry â that quiz could not be ' + 233 'loaded. Please pick another topic.</p></div>'; 234 }); 235 } 236 237 /* Rebuild the topic <select> from the catalogue, so it can never again offer 238 a topic whose bank is missing or empty. */ 239 function buildPicker() { 240 var sel = document.getElementById('Category') || document.getElementById('Categories'); 241 if (!sel) return; 242 var groups = {}; 243 CATALOGUE.forEach(function (c) { 244 (groups[c.group] = groups[c.group] || []).push(c); 245 }); 246 sel.innerHTML = '<option value="">Choose a topicâ¦</option>'; 247 // Render groups in a deliberate order (current certs first, retired last) 248 // rather than whatever order the object keys happen to come back in. 249 var ordered = GROUP_ORDER.filter(function (g) { return groups[g]; }); 250 Object.keys(groups).forEach(function (g) { 251 if (ordered.indexOf(g) === -1) ordered.push(g); 252 }); 253 ordered.forEach(function (g) { 254 var og = document.createElement('optgroup'); 255 og.label = g; 256 groups[g].forEach(function (c) { 257 var o = document.createElement('option'); 258 o.value = c.id; 259 o.textContent = c.label; 260 og.appendChild(o); 261 }); 262 sel.appendChild(og); 263 }); 264 sel.addEventListener('change', function () { start(resolve(sel.value)); }); 265 } 266 267 /* Pick a random current (non-retired) bank so a first-time visitor sees a 268 working quiz immediately instead of a bare, unlabelled dropdown -- but 269 never surprise them with a retired exam as their first impression. */ 270 function randomDefault() { 271 var current = CATALOGUE.filter(function (c) { return !c.retired; }); 272 if (!current.length) return null; 273 return current[Math.floor(Math.random() * current.length)].id; 274 } 275 276 function init() { 277 buildPicker(); 278 279 // Deep link: /?quiz=<bank id> starts that quiz directly. 280 var params = new URLSearchParams(window.location.search); 281 var deep = resolve(params.get('quiz') || params.get('cert') || params.get('topic')); 282 if (deep) { 283 start(deep); 284 } else { 285 var pick = randomDefault(); 286 if (pick) { 287 var sel = document.getElementById('Category') || document.getElementById('Categories'); 288 if (sel) sel.value = pick; 289 start(pick, { scroll: false }); 290 } 291 } 292 293 // Give the user a way out of the quiz. The engine has no Back control of 294 // its own -- in the Djamgatech app that button belongs to the host page -- 295 // so provide one here and restore the original panel when it is used. 296 var back = document.createElement('button'); 297 back.type = 'button'; 298 back.id = 'qz-back'; 299 back.className = 'qz-btn'; 300 back.textContent = 'â Choose another topic'; 301 back.style.cssText = 'margin:0 0 1rem;display:none;'; 302 back.addEventListener('click', function () { 303 var root = document.getElementById('qz-root'); 304 if (root) root.innerHTML = ''; 305 var notice = document.getElementById('qz-retired-notice'); 306 if (notice) notice.remove(); 307 if (window.quizTimer) clearInterval(window.quizTimer); 308 back.style.display = 'none'; 309 markHostChain(false); 310 showLegacy(); 311 var sel = document.getElementById('Category') || document.getElementById('Categories'); 312 if (sel) { sel.value = ''; sel.scrollIntoView({ behavior: 'smooth', block: 'center' }); } 313 }); 314 ensureWrapper().appendChild(back); 315 window.__qzBackBtn = back; 316 } 317 318 window.QuizCatalogue = { 319 list: CATALOGUE, 320 groupOrder: GROUP_ORDER, 321 resolve: resolve, 322 entryFor: entryFor, 323 start: start 324 }; 325 326 if (document.readyState === 'loading') { 327 document.addEventListener('DOMContentLoaded', init); 328 } else { 329 init(); 330 } 331})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.