AI-powered assistant that provides answers to test questions
Size
22.6 KB
Version
1.0.1
Created
Feb 13, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name ExamFX Test Answer Assistant
3// @description AI-powered assistant that provides answers to test questions
4// @version 1.0.1
5// @match https://*.learning.examfx.com/*
6// ==/UserScript==
7(function() {
8 'use strict';
9
10 console.log('ExamFX Test Answer Assistant loaded');
11
12 // Debounce function to prevent excessive calls
13 function debounce(func, wait) {
14 let timeout;
15 return function executedFunction(...args) {
16 const later = () => {
17 clearTimeout(timeout);
18 func(...args);
19 };
20 clearTimeout(timeout);
21 timeout = setTimeout(later, wait);
22 };
23 }
24
25 // Add custom styles for the assistant UI
26 TM_addStyle(`
27 #exam-assistant-panel {
28 position: fixed;
29 top: 20px;
30 right: 20px;
31 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32 color: white;
33 padding: 20px;
34 border-radius: 12px;
35 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
36 z-index: 999999;
37 min-width: 300px;
38 max-width: 400px;
39 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
40 }
41
42 #exam-assistant-panel h3 {
43 margin: 0 0 15px 0;
44 font-size: 18px;
45 font-weight: 600;
46 display: flex;
47 align-items: center;
48 gap: 8px;
49 }
50
51 #exam-assistant-panel button {
52 background: white;
53 color: #667eea;
54 border: none;
55 padding: 12px 24px;
56 border-radius: 8px;
57 cursor: pointer;
58 font-weight: 600;
59 font-size: 14px;
60 width: 100%;
61 margin-bottom: 10px;
62 transition: all 0.3s ease;
63 }
64
65 #exam-assistant-panel button:hover {
66 transform: translateY(-2px);
67 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
68 }
69
70 #exam-assistant-panel button:disabled {
71 opacity: 0.6;
72 cursor: not-allowed;
73 transform: none;
74 }
75
76 #exam-assistant-status {
77 margin-top: 15px;
78 padding: 12px;
79 background: rgba(255, 255, 255, 0.2);
80 border-radius: 8px;
81 font-size: 13px;
82 line-height: 1.5;
83 }
84
85 .exam-assistant-close {
86 position: absolute;
87 top: 10px;
88 right: 10px;
89 background: rgba(255, 255, 255, 0.3);
90 border: none;
91 color: white;
92 width: 24px;
93 height: 24px;
94 border-radius: 50%;
95 cursor: pointer;
96 font-size: 16px;
97 line-height: 1;
98 padding: 0;
99 }
100
101 .exam-assistant-close:hover {
102 background: rgba(255, 255, 255, 0.5);
103 }
104
105 .answer-highlight {
106 background-color: #90EE90 !important;
107 border: 3px solid #32CD32 !important;
108 box-shadow: 0 0 10px rgba(50, 205, 50, 0.5) !important;
109 }
110
111 .question-container {
112 border: 2px solid #667eea;
113 padding: 10px;
114 margin: 10px 0;
115 border-radius: 8px;
116 background: rgba(102, 126, 234, 0.05);
117 }
118 `);
119
120 // Main initialization function
121 async function init() {
122 console.log('Initializing ExamFX Test Answer Assistant...');
123
124 // Wait for page to be fully loaded
125 await waitForPageLoad();
126
127 // Create the assistant panel
128 createAssistantPanel();
129
130 // Set up observers for dynamic content
131 setupObservers();
132
133 console.log('ExamFX Test Answer Assistant initialized successfully');
134 }
135
136 // Wait for page to be fully loaded
137 function waitForPageLoad() {
138 return new Promise((resolve) => {
139 if (document.readyState === 'complete') {
140 resolve();
141 } else {
142 window.addEventListener('load', resolve);
143 }
144 });
145 }
146
147 // Create the assistant control panel
148 function createAssistantPanel() {
149 // Check if panel already exists
150 if (document.getElementById('exam-assistant-panel')) {
151 return;
152 }
153
154 const panel = document.createElement('div');
155 panel.id = 'exam-assistant-panel';
156 panel.innerHTML = `
157 <button class="exam-assistant-close" id="exam-assistant-close">×</button>
158 <h3>🤖 AI Test Assistant</h3>
159 <button id="find-answers-btn">Find Answers</button>
160 <button id="auto-answer-btn">Auto-Answer Current Question</button>
161 <button id="scan-page-btn">Scan Page Structure</button>
162 <div id="exam-assistant-status">Ready to assist! Click a button to start.</div>
163 `;
164
165 document.body.appendChild(panel);
166
167 // Add event listeners
168 document.getElementById('exam-assistant-close').addEventListener('click', () => {
169 panel.style.display = 'none';
170 });
171
172 document.getElementById('find-answers-btn').addEventListener('click', findAndHighlightAnswers);
173 document.getElementById('auto-answer-btn').addEventListener('click', autoAnswerCurrentQuestion);
174 document.getElementById('scan-page-btn').addEventListener('click', scanPageStructure);
175
176 console.log('Assistant panel created');
177 }
178
179 // Update status message
180 function updateStatus(message, isError = false) {
181 const statusDiv = document.getElementById('exam-assistant-status');
182 if (statusDiv) {
183 statusDiv.textContent = message;
184 statusDiv.style.background = isError ? 'rgba(255, 100, 100, 0.3)' : 'rgba(255, 255, 255, 0.2)';
185 }
186 console.log('Status:', message);
187 }
188
189 // Scan page structure to identify questions and answers
190 async function scanPageStructure() {
191 updateStatus('Scanning page structure...');
192
193 try {
194 // Look for common test question patterns
195 const possibleQuestions = [];
196
197 // Strategy 1: Look for question text patterns
198 const textElements = document.querySelectorAll('p, div, span, h1, h2, h3, h4, label');
199 textElements.forEach((el, index) => {
200 const text = el.textContent.trim();
201 // Check if it looks like a question
202 if (text.length > 20 && (text.includes('?') || /^\d+\./.test(text) || text.match(/^(what|which|who|where|when|why|how)/i))) {
203 possibleQuestions.push({
204 element: el,
205 text: text,
206 index: index
207 });
208 }
209 });
210
211 // Strategy 2: Look for form elements (radio buttons, checkboxes, inputs)
212 const radioGroups = {};
213 const radios = document.querySelectorAll('input[type="radio"]');
214 radios.forEach(radio => {
215 const name = radio.name;
216 if (!radioGroups[name]) {
217 radioGroups[name] = [];
218 }
219 radioGroups[name].push(radio);
220 });
221
222 const checkboxes = document.querySelectorAll('input[type="checkbox"]');
223 const textInputs = document.querySelectorAll('input[type="text"], textarea');
224
225 // Strategy 3: Look for iframes (many test platforms use iframes)
226 const iframes = document.querySelectorAll('iframe');
227
228 let report = `📊 Page Structure Report:\n\n`;
229 report += `Questions found: ${possibleQuestions.length}\n`;
230 report += `Radio button groups: ${Object.keys(radioGroups).length}\n`;
231 report += `Checkboxes: ${checkboxes.length}\n`;
232 report += `Text inputs: ${textInputs.length}\n`;
233 report += `Iframes: ${iframes.length}\n\n`;
234
235 if (possibleQuestions.length > 0) {
236 report += `First question preview: "${possibleQuestions[0].text.substring(0, 100)}..."`;
237 }
238
239 if (iframes.length > 0) {
240 report += `\n\n⚠️ Detected ${iframes.length} iframe(s). Test content may be inside iframe.`;
241 }
242
243 updateStatus(report);
244
245 // Store findings for later use
246 await GM.setValue('lastScan', {
247 questions: possibleQuestions.map(q => ({ text: q.text, selector: getSelector(q.element) })),
248 radioGroups: Object.keys(radioGroups),
249 timestamp: Date.now()
250 });
251
252 console.log('Scan complete:', { possibleQuestions, radioGroups, checkboxes, textInputs, iframes });
253
254 } catch (error) {
255 console.error('Error scanning page:', error);
256 updateStatus('Error scanning page: ' + error.message, true);
257 }
258 }
259
260 // Get a unique selector for an element
261 function getSelector(element) {
262 if (element.id) return `#${element.id}`;
263 if (element.className) return `${element.tagName.toLowerCase()}.${element.className.split(' ')[0]}`;
264 return element.tagName.toLowerCase();
265 }
266
267 // Find and highlight answers using AI
268 async function findAndHighlightAnswers() {
269 const btn = document.getElementById('find-answers-btn');
270 btn.disabled = true;
271 updateStatus('🔍 Analyzing questions and finding answers...');
272
273 try {
274 // First, scan for questions and answers
275 const questions = await extractQuestionsAndAnswers();
276
277 if (questions.length === 0) {
278 updateStatus('No questions found on this page. Try clicking "Scan Page Structure" first.', true);
279 btn.disabled = false;
280 return;
281 }
282
283 updateStatus(`Found ${questions.length} question(s). Analyzing with AI...`);
284
285 // Process each question with AI
286 for (let i = 0; i < questions.length; i++) {
287 const q = questions[i];
288 updateStatus(`Processing question ${i + 1}/${questions.length}...`);
289
290 try {
291 const answer = await getAIAnswer(q);
292
293 if (answer && answer.correctAnswer) {
294 highlightCorrectAnswer(q, answer);
295 updateStatus(`✅ Processed ${i + 1}/${questions.length} questions`);
296 }
297 } catch (error) {
298 console.error('Error processing question:', error);
299 }
300
301 // Small delay between questions
302 await new Promise(resolve => setTimeout(resolve, 500));
303 }
304
305 updateStatus(`✅ Complete! Processed ${questions.length} question(s). Correct answers are highlighted in green.`);
306
307 } catch (error) {
308 console.error('Error finding answers:', error);
309 updateStatus('Error: ' + error.message, true);
310 } finally {
311 btn.disabled = false;
312 }
313 }
314
315 // Auto-answer the current visible question
316 async function autoAnswerCurrentQuestion() {
317 const btn = document.getElementById('auto-answer-btn');
318 btn.disabled = true;
319 updateStatus('🤖 Finding current question...');
320
321 try {
322 const questions = await extractQuestionsAndAnswers();
323
324 if (questions.length === 0) {
325 updateStatus('No questions found. The test may be in an iframe or use a different structure.', true);
326 btn.disabled = false;
327 return;
328 }
329
330 // Find the first unanswered question or the most visible one
331 const currentQuestion = questions[0];
332
333 updateStatus('Analyzing question with AI...');
334 const answer = await getAIAnswer(currentQuestion);
335
336 if (answer && answer.correctAnswer) {
337 highlightCorrectAnswer(currentQuestion, answer);
338 updateStatus(`✅ Answer found: ${answer.correctAnswer}\n\nExplanation: ${answer.explanation || 'N/A'}`);
339 } else {
340 updateStatus('Could not determine the answer. Please try again.', true);
341 }
342
343 } catch (error) {
344 console.error('Error auto-answering:', error);
345 updateStatus('Error: ' + error.message, true);
346 } finally {
347 btn.disabled = false;
348 }
349 }
350
351 // Extract questions and their answer options from the page
352 async function extractQuestionsAndAnswers() {
353 const questions = [];
354
355 // Try to find questions in the main document
356 let questionElements = findQuestionElements(document);
357
358 // If no questions found, try iframes
359 if (questionElements.length === 0) {
360 const iframes = document.querySelectorAll('iframe');
361 for (const iframe of iframes) {
362 try {
363 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
364 const iframeQuestions = findQuestionElements(iframeDoc);
365 questionElements = questionElements.concat(iframeQuestions);
366 } catch (e) {
367 console.log('Cannot access iframe:', e);
368 }
369 }
370 }
371
372 return questionElements;
373 }
374
375 // Find question elements in a document
376 function findQuestionElements(doc) {
377 const questions = [];
378
379 // Look for radio button groups (most common in multiple choice tests)
380 const radioGroups = {};
381 const radios = doc.querySelectorAll('input[type="radio"]');
382
383 radios.forEach(radio => {
384 const name = radio.name;
385 if (!radioGroups[name]) {
386 radioGroups[name] = {
387 name: name,
388 options: [],
389 questionText: ''
390 };
391 }
392
393 // Get the label or nearby text for this option
394 let optionText = '';
395 const label = doc.querySelector(`label[for="${radio.id}"]`);
396 if (label) {
397 optionText = label.textContent.trim();
398 } else {
399 // Look for nearby text
400 const parent = radio.closest('div, li, td, label');
401 if (parent) {
402 optionText = parent.textContent.trim();
403 }
404 }
405
406 radioGroups[name].options.push({
407 element: radio,
408 text: optionText,
409 value: radio.value
410 });
411 });
412
413 // For each radio group, try to find the question text
414 Object.values(radioGroups).forEach(group => {
415 if (group.options.length > 0) {
416 // Try to find question text by looking at elements before the first radio button
417 const firstRadio = group.options[0].element;
418 let questionText = findQuestionTextNearElement(firstRadio, doc);
419
420 group.questionText = questionText;
421 questions.push(group);
422 }
423 });
424
425 // Also look for text inputs and textareas
426 const textInputs = doc.querySelectorAll('input[type="text"], textarea');
427 textInputs.forEach(input => {
428 const questionText = findQuestionTextNearElement(input, doc);
429 if (questionText) {
430 questions.push({
431 type: 'text',
432 questionText: questionText,
433 inputElement: input
434 });
435 }
436 });
437
438 return questions;
439 }
440
441 // Find question text near an element
442 function findQuestionTextNearElement(element, doc) {
443 let questionText = '';
444
445 // Strategy 1: Look for associated label
446 if (element.id) {
447 const label = doc.querySelector(`label[for="${element.id}"]`);
448 if (label) {
449 questionText = label.textContent.trim();
450 }
451 }
452
453 // Strategy 2: Look at parent containers
454 if (!questionText) {
455 let parent = element.closest('div, fieldset, section, form');
456 if (parent) {
457 // Look for heading or paragraph before the input
458 const headings = parent.querySelectorAll('h1, h2, h3, h4, h5, h6, p, legend, label');
459 for (const heading of headings) {
460 const text = heading.textContent.trim();
461 if (text.length > 10 && (text.includes('?') || /^\d+\./.test(text))) {
462 questionText = text;
463 break;
464 }
465 }
466 }
467 }
468
469 // Strategy 3: Look at previous siblings
470 if (!questionText) {
471 let sibling = element.previousElementSibling;
472 let attempts = 0;
473 while (sibling && attempts < 5) {
474 const text = sibling.textContent.trim();
475 if (text.length > 10 && (text.includes('?') || /^\d+\./.test(text))) {
476 questionText = text;
477 break;
478 }
479 sibling = sibling.previousElementSibling;
480 attempts++;
481 }
482 }
483
484 return questionText;
485 }
486
487 // Get AI answer for a question
488 async function getAIAnswer(question) {
489 try {
490 let prompt = '';
491
492 if (question.options) {
493 // Multiple choice question
494 const optionsText = question.options.map((opt, idx) =>
495 `${String.fromCharCode(65 + idx)}. ${opt.text}`
496 ).join('\n');
497
498 prompt = `You are a test-taking assistant. Answer this multiple choice question accurately.
499
500Question: ${question.questionText}
501
502Options:
503${optionsText}
504
505Provide the correct answer.`;
506
507 const response = await RM.aiCall(prompt, {
508 type: "json_schema",
509 json_schema: {
510 name: "test_answer",
511 schema: {
512 type: "object",
513 properties: {
514 correctAnswer: {
515 type: "string",
516 description: "The letter (A, B, C, D, etc.) of the correct answer"
517 },
518 explanation: {
519 type: "string",
520 description: "Brief explanation of why this is correct"
521 },
522 confidence: {
523 type: "number",
524 description: "Confidence level from 0 to 1"
525 }
526 },
527 required: ["correctAnswer", "explanation"]
528 }
529 }
530 });
531
532 return response;
533
534 } else if (question.inputElement) {
535 // Text input question
536 prompt = `You are a test-taking assistant. Answer this question concisely and accurately.
537
538Question: ${question.questionText}
539
540Provide a brief, accurate answer.`;
541
542 const response = await RM.aiCall(prompt);
543 return {
544 correctAnswer: response,
545 explanation: 'AI-generated answer'
546 };
547 }
548
549 } catch (error) {
550 console.error('Error getting AI answer:', error);
551 throw error;
552 }
553 }
554
555 // Highlight the correct answer
556 function highlightCorrectAnswer(question, answer) {
557 try {
558 if (question.options && answer.correctAnswer) {
559 // Convert letter to index (A=0, B=1, etc.)
560 const letterMatch = answer.correctAnswer.match(/[A-Z]/i);
561 if (letterMatch) {
562 const index = letterMatch[0].toUpperCase().charCodeAt(0) - 65;
563
564 if (index >= 0 && index < question.options.length) {
565 const correctOption = question.options[index];
566
567 // Highlight the radio button's parent container
568 const container = correctOption.element.closest('div, li, label, td');
569 if (container) {
570 container.classList.add('answer-highlight');
571 container.scrollIntoView({ behavior: 'smooth', block: 'center' });
572 }
573
574 // Optionally auto-select the answer
575 // correctOption.element.checked = true;
576
577 console.log('Highlighted answer:', correctOption.text);
578 }
579 }
580 } else if (question.inputElement && answer.correctAnswer) {
581 // For text inputs, show the answer nearby
582 const answerDiv = document.createElement('div');
583 answerDiv.style.cssText = `
584 background: #90EE90;
585 border: 2px solid #32CD32;
586 padding: 10px;
587 margin: 10px 0;
588 border-radius: 8px;
589 font-weight: bold;
590 `;
591 answerDiv.textContent = `Suggested answer: ${answer.correctAnswer}`;
592 question.inputElement.parentNode.insertBefore(answerDiv, question.inputElement.nextSibling);
593
594 // Optionally auto-fill
595 // question.inputElement.value = answer.correctAnswer;
596 }
597 } catch (error) {
598 console.error('Error highlighting answer:', error);
599 }
600 }
601
602 // Setup observers for dynamic content
603 function setupObservers() {
604 // Observe DOM changes to detect new questions
605 const observer = new MutationObserver(debounce(() => {
606 console.log('Page content changed, questions may have updated');
607 }, 1000));
608
609 observer.observe(document.body, {
610 childList: true,
611 subtree: true
612 });
613 }
614
615 // Start the extension
616 TM_runBody(init);
617
618})();