ExamFX Test Answer Assistant

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})();