Compare commits
10
Commits
4b1b694715
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba1f28cfd | ||
|
|
bd7ec8cd06 | ||
|
|
2d9883de35 | ||
|
|
e0ffda56fa | ||
|
|
8f54ba762f | ||
|
|
b24bd48ee6 | ||
|
|
7b7fdc7c2c | ||
|
|
9b0a63aa37 | ||
|
|
75ebd38060 | ||
|
|
f79629e1b6 |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# 🎓 Quiz Helper - AI-Powered Quiz Assistant
|
# 🎓 Quiz Helper - AI-Powered Quiz Assistant
|
||||||
|
|
||||||
A web browser extension for Chrome which uses AI to help with online quizzes by highlighting correct answers in green (Proof of Concept. ~80% Final Score Accuracy on Practice Assessments).
|
A web browser extension for Chrome which uses AI to help with online quizzes by highlighting correct answers in green (~100% Final Score Accuracy on Microsoft Practice Assessments).
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -32,7 +32,7 @@ A web browser extension for Chrome which uses AI to help with online quizzes by
|
|||||||
- Open Chrome and navigate to `chrome://extensions/`
|
- Open Chrome and navigate to `chrome://extensions/`
|
||||||
- Enable "Developer mode" (toggle in top-right corner)
|
- Enable "Developer mode" (toggle in top-right corner)
|
||||||
- Click "Load unpacked"
|
- Click "Load unpacked"
|
||||||
- Select the `extension` folder from the extracted files
|
- Select the `extension/unpacked` folder from the extracted files
|
||||||
|
|
||||||
3. **Add API Key**:
|
3. **Add API Key**:
|
||||||
- Click the extension icon in your toolbar
|
- Click the extension icon in your toolbar
|
||||||
@@ -44,9 +44,8 @@ A web browser extension for Chrome which uses AI to help with online quizzes by
|
|||||||
1. **Navigate to a quiz** (works best with Microsoft Learn practice assessments)
|
1. **Navigate to a quiz** (works best with Microsoft Learn practice assessments)
|
||||||
2. **Click the extension icon** in your toolbar
|
2. **Click the extension icon** in your toolbar
|
||||||
3. **Toggle the switch to "On"**
|
3. **Toggle the switch to "On"**
|
||||||
4. **Refresh Page** - Correct answer(s) will be highlighted in green after a brief moment
|
4. **Select your answers** based on the hints
|
||||||
5. **Select your answers** based on the hints
|
5. **Toggle off** when done to remove hints
|
||||||
6. **Toggle off** when done to remove hints
|
|
||||||
|
|
||||||
## 🔧 Configuration
|
## 🔧 Configuration
|
||||||
|
|
||||||
@@ -103,9 +102,10 @@ This extension is designed for learning and study purposes. Use responsibly and
|
|||||||
- Ensure all three icon files are present
|
- Ensure all three icon files are present
|
||||||
|
|
||||||
**No hints appearing:**
|
**No hints appearing:**
|
||||||
|
- Try clicking "Retry Scan" on extnsion popup
|
||||||
|
- Make sure the extension toggle is "On"
|
||||||
- Check that you've added a valid OpenAI API key
|
- Check that you've added a valid OpenAI API key
|
||||||
- Open browser console (F12) and look for error messages
|
- Open browser console (F12) and look for error messages
|
||||||
- Make sure the extension toggle is "On"
|
|
||||||
- Try refreshing the quiz page
|
- Try refreshing the quiz page
|
||||||
|
|
||||||
**API errors:**
|
**API errors:**
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 78 KiB |
File renamed without changes.
+281
-265
@@ -12,30 +12,11 @@ function injectCustomStyles() {
|
|||||||
style.id = 'quiz-helper-styles';
|
style.id = 'quiz-helper-styles';
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.quiz-helper-correct {
|
.quiz-helper-correct {
|
||||||
background-color: #90EE90 !important;
|
|
||||||
border: 3px solid #28a745 !important;
|
border: 3px solid #28a745 !important;
|
||||||
box-shadow: 0 0 10px rgba(40, 167, 69, 0.6) !important;
|
box-shadow: 0 0 8px rgba(40, 167, 69, 0.5) !important;
|
||||||
padding: 8px !important;
|
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
transition: all 0.3s ease !important;
|
transition: all 0.3s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quiz-helper-correct * {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiz-helper-unknown {
|
|
||||||
background-color: #FFB6C1 !important;
|
|
||||||
border: 3px solid #dc3545 !important;
|
|
||||||
box-shadow: 0 0 10px rgba(220, 53, 69, 0.6) !important;
|
|
||||||
padding: 8px !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiz-helper-unknown * {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
styleInjected = true;
|
styleInjected = true;
|
||||||
@@ -79,6 +60,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
openaiApiKey = request.apiKey;
|
openaiApiKey = request.apiKey;
|
||||||
console.log('✓ OpenAI API key updated');
|
console.log('✓ OpenAI API key updated');
|
||||||
processedQuestions.clear();
|
processedQuestions.clear();
|
||||||
|
} else if (request.action === 'retryScan') {
|
||||||
|
console.log('🔄 Manual retry initiated from popup');
|
||||||
|
processedQuestions.clear();
|
||||||
|
isProcessing = false;
|
||||||
|
removeHighlights();
|
||||||
|
waitForContentAndScan();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,24 +74,17 @@ function waitForContentAndScan() {
|
|||||||
console.log('🔍 Waiting for Microsoft Learn quiz content to load...');
|
console.log('🔍 Waiting for Microsoft Learn quiz content to load...');
|
||||||
|
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 60; // Increased for slower loading
|
const maxAttempts = 60;
|
||||||
|
|
||||||
const checkContent = setInterval(() => {
|
const checkContent = setInterval(() => {
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
// Microsoft Learn specific selectors
|
|
||||||
const mainColumn = document.querySelector('[data-main-column]');
|
const mainColumn = document.querySelector('[data-main-column]');
|
||||||
const hasMainContent = mainColumn && mainColumn.innerText.length > 100;
|
const hasMainContent = mainColumn && mainColumn.innerText.length > 100;
|
||||||
|
|
||||||
// Look for Microsoft Learn quiz elements
|
|
||||||
const hasQuizElements = document.querySelectorAll('input[type="radio"], input[type="checkbox"]').length > 0;
|
const hasQuizElements = document.querySelectorAll('input[type="radio"], input[type="checkbox"]').length > 0;
|
||||||
const hasQuestionText = document.body.innerText.length > 500;
|
const hasQuestionText = document.body.innerText.length > 500;
|
||||||
|
|
||||||
// Look for common Microsoft Learn assessment structures
|
|
||||||
const hasAssessmentContainer = document.querySelector('[class*="assessment"], [class*="question"], [role="group"]');
|
const hasAssessmentContainer = document.querySelector('[class*="assessment"], [class*="question"], [role="group"]');
|
||||||
const hasInteractiveElements = document.querySelectorAll('button, input, label').length > 5;
|
const hasInteractiveElements = document.querySelectorAll('button, input, label').length > 5;
|
||||||
|
|
||||||
// Check for React root (Microsoft Learn uses React)
|
|
||||||
const reactRoot = document.querySelector('#root, [data-reactroot], [data-react-app]');
|
const reactRoot = document.querySelector('#root, [data-reactroot], [data-react-app]');
|
||||||
const hasReactContent = reactRoot && reactRoot.innerText.length > 200;
|
const hasReactContent = reactRoot && reactRoot.innerText.length > 200;
|
||||||
|
|
||||||
@@ -112,7 +92,7 @@ function waitForContentAndScan() {
|
|||||||
(hasQuestionText && hasQuizElements) ||
|
(hasQuestionText && hasQuizElements) ||
|
||||||
(hasReactContent && hasQuizElements);
|
(hasReactContent && hasQuizElements);
|
||||||
|
|
||||||
console.log(`Attempt ${attempts}/${maxAttempts}: main=${hasMainContent}, quiz=${hasQuizElements}, assessment=${!!hasAssessmentContainer}, react=${hasReactContent}`);
|
console.log(`Attempt ${attempts}/${maxAttempts}: main=${hasMainContent}, quiz=${hasQuizElements}, react=${hasReactContent}`);
|
||||||
|
|
||||||
if (contentLoaded) {
|
if (contentLoaded) {
|
||||||
clearInterval(checkContent);
|
clearInterval(checkContent);
|
||||||
@@ -121,27 +101,38 @@ function waitForContentAndScan() {
|
|||||||
} else if (attempts >= maxAttempts) {
|
} else if (attempts >= maxAttempts) {
|
||||||
clearInterval(checkContent);
|
clearInterval(checkContent);
|
||||||
console.log('⚠️ Timeout: Quiz content did not load within expected time.');
|
console.log('⚠️ Timeout: Quiz content did not load within expected time.');
|
||||||
console.log('💡 Try: 1) Refresh the page, 2) Wait for quiz to fully load, 3) Click "Start" if needed');
|
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to clean and extract pure question text
|
||||||
|
function cleanQuestionText(text) {
|
||||||
|
// Remove common prefixes that don't add value
|
||||||
|
let cleaned = text
|
||||||
|
.replace(/^Question \d+\s*[:.]?\s*/i, '')
|
||||||
|
.replace(/^\d+\.\s*/, '')
|
||||||
|
.replace(/^Q\d+\s*[:.]?\s*/i, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to get full question context including scenario descriptions
|
// Function to get full question context including scenario descriptions
|
||||||
function getFullQuestionContext(questionElement) {
|
function getFullQuestionContext(questionElement) {
|
||||||
let fullText = questionElement.textContent.trim();
|
let questionText = questionElement.textContent.trim();
|
||||||
|
questionText = cleanQuestionText(questionText);
|
||||||
|
|
||||||
|
const contextParts = [];
|
||||||
|
|
||||||
// Check previous siblings for context (scenario descriptions, requirements)
|
// Check previous siblings for context (scenario descriptions, requirements)
|
||||||
let prevSibling = questionElement.previousElementSibling;
|
let prevSibling = questionElement.previousElementSibling;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const contextParts = [];
|
|
||||||
|
|
||||||
while (prevSibling && attempts < 5) {
|
while (prevSibling && attempts < 5) {
|
||||||
const text = prevSibling.textContent.trim();
|
const text = prevSibling.textContent.trim();
|
||||||
|
|
||||||
// If it's a substantial paragraph that adds context
|
|
||||||
if (text.length > 30 && text.length < 2000 && !text.match(/question \d+|^\d+\s*of\s*\d+/i)) {
|
if (text.length > 30 && text.length < 2000 && !text.match(/question \d+|^\d+\s*of\s*\d+/i)) {
|
||||||
// Check if it looks like contextual information
|
const hasContextKeywords = /scenario|requirement|company|organization|need|must|should|environment|architecture|implement|deploy|configure|planning|contoso|fabrikam|adatum|you have|you are|you plan|you create/i.test(text);
|
||||||
const hasContextKeywords = /scenario|requirement|company|organization|need|must|should|environment|architecture|implement|deploy|configure|planning|contoso|fabrikam|adatum/i.test(text);
|
|
||||||
|
|
||||||
if (hasContextKeywords || text.length > 100) {
|
if (hasContextKeywords || text.length > 100) {
|
||||||
contextParts.unshift(text);
|
contextParts.unshift(text);
|
||||||
@@ -152,14 +143,13 @@ function getFullQuestionContext(questionElement) {
|
|||||||
attempts++;
|
attempts++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check parent containers for context
|
// Check parent containers for broader context
|
||||||
let parent = questionElement.parentElement;
|
let parent = questionElement.parentElement;
|
||||||
attempts = 0;
|
attempts = 0;
|
||||||
while (parent && attempts < 3) {
|
while (parent && attempts < 3) {
|
||||||
const parentText = parent.textContent.trim();
|
const parentText = parent.textContent.trim();
|
||||||
if (parentText.length > fullText.length + 50 && parentText.length < 3000) {
|
if (parentText.length > questionText.length + 50 && parentText.length < 3000) {
|
||||||
// This parent might have additional context
|
const extraContext = parentText.replace(questionText, '').trim();
|
||||||
const extraContext = parentText.replace(fullText, '').trim();
|
|
||||||
if (extraContext.length > 50 && /scenario|requirement|company|organization/i.test(extraContext)) {
|
if (extraContext.length > 50 && /scenario|requirement|company|organization/i.test(extraContext)) {
|
||||||
contextParts.unshift(extraContext.substring(0, 500));
|
contextParts.unshift(extraContext.substring(0, 500));
|
||||||
break;
|
break;
|
||||||
@@ -170,130 +160,123 @@ function getFullQuestionContext(questionElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (contextParts.length > 0) {
|
if (contextParts.length > 0) {
|
||||||
fullText = contextParts.join('\n\n') + '\n\n' + fullText;
|
const fullContext = contextParts.join('\n\n') + '\n\n' + questionText;
|
||||||
console.log('📋 Added context from surrounding elements');
|
console.log('📋 Added context from surrounding elements');
|
||||||
|
return fullContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullText;
|
return questionText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced question finding for Microsoft Learn
|
// Enhanced question finding for Microsoft Learn - Groups multi-line questions
|
||||||
function findQuestions() {
|
function findQuestions() {
|
||||||
const questions = [];
|
const questions = [];
|
||||||
const seenTexts = new Set();
|
const seenTexts = new Set();
|
||||||
|
|
||||||
console.log('🔍 Scanning page structure...');
|
console.log('🔍 Scanning page structure...');
|
||||||
console.log(' Total page text length:', document.body.innerText.length);
|
|
||||||
|
|
||||||
// Microsoft Learn specific selectors (prioritized)
|
// Look for question CONTAINERS first, not individual text elements
|
||||||
const microsoftLearnSelectors = [
|
const containerSelectors = [
|
||||||
'[role="group"]',
|
'[role="group"]',
|
||||||
|
'[class*="question-container"]',
|
||||||
|
'[class*="question-block"]',
|
||||||
'[class*="assessment"]',
|
'[class*="assessment"]',
|
||||||
'[class*="question"]',
|
|
||||||
'[data-test*="question"]',
|
|
||||||
'fieldset',
|
'fieldset',
|
||||||
'form'
|
'[data-test*="question"]'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Generic selectors as fallback
|
// Try to find question containers that hold the full multi-line question
|
||||||
const genericSelectors = [
|
for (const selector of containerSelectors) {
|
||||||
'h1, h2, h3, h4, h5, h6',
|
const containers = document.querySelectorAll(selector);
|
||||||
'p',
|
|
||||||
'div[class*="text"]',
|
|
||||||
'div[class*="content"]',
|
|
||||||
'div',
|
|
||||||
'span',
|
|
||||||
'label'
|
|
||||||
];
|
|
||||||
|
|
||||||
const allSelectors = [...microsoftLearnSelectors, ...genericSelectors];
|
containers.forEach(container => {
|
||||||
|
// Get ALL text content from the container as ONE question
|
||||||
|
const inputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
|
||||||
|
if (inputs.length === 0) return; // Skip if no answer choices
|
||||||
|
|
||||||
const questionPatterns = [
|
// Find the question text part (everything before the answer choices)
|
||||||
/\?$/, // Ends with question mark
|
let questionText = '';
|
||||||
/\?\s*$/, // Ends with question mark and whitespace
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
|
||||||
/^(what|when|where|who|why|how|which|is|are|does|do|did|can|could|would|should|will|shall|must)/i,
|
let node;
|
||||||
/you need to/i,
|
const answerContainer = inputs[0].closest('label, [class*="answer"], [class*="option"]')?.parentElement;
|
||||||
/which of the following/i,
|
|
||||||
/select.*correct/i,
|
|
||||||
/choose.*answer/i,
|
|
||||||
/characteristics/i,
|
|
||||||
/deployment model/i,
|
|
||||||
/recommend/i,
|
|
||||||
/solution/i,
|
|
||||||
/ensure that/i,
|
|
||||||
/minimize/i,
|
|
||||||
/configure/i,
|
|
||||||
/implement/i
|
|
||||||
];
|
|
||||||
|
|
||||||
allSelectors.forEach(selector => {
|
while (node = walker.nextNode()) {
|
||||||
const elements = document.querySelectorAll(selector);
|
const parent = node.parentElement;
|
||||||
|
// Stop collecting text once we hit the answers section
|
||||||
|
if (answerContainer && answerContainer.contains(parent)) continue;
|
||||||
|
if (parent.closest('label[for], [class*="answer"], [class*="option"]')) continue;
|
||||||
|
|
||||||
elements.forEach(el => {
|
const text = node.textContent.trim();
|
||||||
const text = el.textContent.trim();
|
if (text.length > 2) {
|
||||||
|
questionText += text + ' ';
|
||||||
// Skip very short or very long text, or duplicates
|
}
|
||||||
if (text.length < 15 || text.length > 1500 || seenTexts.has(text)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip elements that are likely navigation or UI
|
questionText = questionText.trim();
|
||||||
if (/^(next|previous|submit|skip|back|continue|finish|review|question \d+ of \d+)$/i.test(text)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check against question patterns
|
if (questionText.length < 15 || seenTexts.has(questionText)) return;
|
||||||
for (const pattern of questionPatterns) {
|
if (/^(next|previous|submit|skip)$/i.test(questionText)) return;
|
||||||
if (pattern.test(text)) {
|
// Skip page metadata, scripts, and non-question content
|
||||||
seenTexts.add(text);
|
if (/var\s+\w+\s*=\s*\{|function\s*\(|microsoft learn|practice assessment/i.test(questionText)) return;
|
||||||
|
|
||||||
|
seenTexts.add(questionText);
|
||||||
questions.push({
|
questions.push({
|
||||||
text: text,
|
text: cleanQuestionText(questionText),
|
||||||
element: el
|
element: container
|
||||||
|
});
|
||||||
|
console.log('✓ Found question:', questionText.substring(0, 120) + '...');
|
||||||
});
|
});
|
||||||
console.log('✓ Found question:', text.substring(0, 120) + '...');
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug logging if no questions found
|
// Fallback: if no containers found, group by proximity to radio/checkbox inputs
|
||||||
if (questions.length === 0) {
|
if (questions.length === 0) {
|
||||||
console.log('⚠️ No questions detected. Debugging page structure...');
|
const inputs = document.querySelectorAll('input[type="radio"], input[type="checkbox"]');
|
||||||
console.log(' Main column:', document.querySelector('[data-main-column]')?.innerText.substring(0, 200));
|
const processedContainers = new Set();
|
||||||
console.log(' Input elements:', document.querySelectorAll('input').length);
|
|
||||||
console.log(' Radio/Checkbox:', document.querySelectorAll('input[type="radio"], input[type="checkbox"]').length);
|
|
||||||
|
|
||||||
const allText = document.body.innerText;
|
inputs.forEach(input => {
|
||||||
const lines = allText.split('\n').filter(l => l.trim().length > 20);
|
let container = input.closest('[role="group"], fieldset, form, [class*="question"]');
|
||||||
console.log(' First 10 substantial text lines:');
|
if (!container) container = input.parentElement?.parentElement?.parentElement;
|
||||||
lines.slice(0, 10).forEach((line, i) => {
|
if (!container || processedContainers.has(container)) return;
|
||||||
console.log(` ${i + 1}. ${line.substring(0, 100)}`);
|
|
||||||
|
processedContainers.add(container);
|
||||||
|
|
||||||
|
// Collect all non-answer text as the question
|
||||||
|
let questionText = '';
|
||||||
|
const textElements = container.querySelectorAll('p, h1, h2, h3, h4, h5, h6, div, span');
|
||||||
|
|
||||||
|
textElements.forEach(el => {
|
||||||
|
if (el.closest('label') || el.querySelector('input')) return;
|
||||||
|
const text = el.textContent.trim();
|
||||||
|
if (text.length > 10 && !seenTexts.has(text)) {
|
||||||
|
questionText += text + ' ';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
questionText = questionText.trim();
|
||||||
|
if (questionText.length > 15 && !seenTexts.has(questionText)) {
|
||||||
|
seenTexts.add(questionText);
|
||||||
|
questions.push({
|
||||||
|
text: cleanQuestionText(questionText),
|
||||||
|
element: container
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return questions;
|
return questions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced answer choice finding for Microsoft Learn
|
// Enhanced answer choice finding - strictly finds only radio/checkbox options
|
||||||
function findAnswerChoices(questionElement) {
|
function findAnswerChoices(questionElement) {
|
||||||
const answers = [];
|
const answers = [];
|
||||||
const seenTexts = new Set();
|
const seenTexts = new Set();
|
||||||
let inputType = null;
|
let inputType = null;
|
||||||
|
|
||||||
// Start with the question element's container
|
// Find the container with radio/checkbox inputs
|
||||||
let container = questionElement.closest('[role="group"], fieldset, form, [class*="question"], [class*="answer"], [class*="option"], div');
|
let container = questionElement;
|
||||||
|
for (let i = 0; i < 6 && container; i++) {
|
||||||
// Expand search scope if needed
|
const inputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
|
||||||
if (!container || container === questionElement) {
|
if (inputs.length > 0) {
|
||||||
container = questionElement.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search up to 4 levels if we haven't found inputs yet
|
|
||||||
for (let i = 0; i < 4 && container; i++) {
|
|
||||||
const inputsInContainer = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
|
|
||||||
if (inputsInContainer.length > 0) {
|
|
||||||
console.log(` Found ${inputsInContainer.length} inputs at level ${i}`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
container = container.parentElement;
|
container = container.parentElement;
|
||||||
@@ -301,99 +284,66 @@ function findAnswerChoices(questionElement) {
|
|||||||
|
|
||||||
if (!container) container = document.body;
|
if (!container) container = document.body;
|
||||||
|
|
||||||
// Also check siblings
|
// Get all radio/checkbox inputs - these are the ONLY valid answer choices
|
||||||
let nextSibling = questionElement.nextElementSibling;
|
const inputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
|
||||||
let siblingAttempts = 0;
|
console.log(` Found ${inputs.length} radio/checkbox inputs`);
|
||||||
const siblingContainers = [container];
|
|
||||||
|
|
||||||
while (nextSibling && siblingAttempts < 8) {
|
inputs.forEach((input, idx) => {
|
||||||
siblingContainers.push(nextSibling);
|
if (input.type === 'radio') inputType = 'radio';
|
||||||
nextSibling = nextSibling.nextElementSibling;
|
else if (input.type === 'checkbox') inputType = 'checkbox';
|
||||||
siblingAttempts++;
|
|
||||||
|
let text = '';
|
||||||
|
let labelElement = null;
|
||||||
|
|
||||||
|
// Strategy 1: Find label with matching 'for' attribute
|
||||||
|
if (input.id) {
|
||||||
|
labelElement = document.querySelector(`label[for="${input.id}"]`);
|
||||||
|
if (labelElement) {
|
||||||
|
text = labelElement.textContent.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const answerSelectors = [
|
// Strategy 2: Find parent label
|
||||||
'input[type="radio"]',
|
if (!text) {
|
||||||
'input[type="checkbox"]',
|
labelElement = input.closest('label');
|
||||||
'button[role="radio"]',
|
if (labelElement) {
|
||||||
'button[role="checkbox"]',
|
text = labelElement.textContent.trim();
|
||||||
'[role="option"]',
|
}
|
||||||
'[class*="answer"]',
|
|
||||||
'[class*="option"]',
|
|
||||||
'[class*="choice"]',
|
|
||||||
'[data-test*="answer"]',
|
|
||||||
'[data-test*="option"]',
|
|
||||||
'label',
|
|
||||||
'li'
|
|
||||||
];
|
|
||||||
|
|
||||||
const excludePatterns = [
|
|
||||||
/^question\s+\d+\s+of\s+\d+$/i,
|
|
||||||
/^page\s+\d+/i,
|
|
||||||
/^(next|previous|submit|skip|back|continue|finish|review)$/i,
|
|
||||||
/^\d+\s*\/\s*\d+$/,
|
|
||||||
/^score:/i,
|
|
||||||
/^time:/i,
|
|
||||||
/^remaining:/i,
|
|
||||||
/^points?:/i
|
|
||||||
];
|
|
||||||
|
|
||||||
siblingContainers.forEach(searchContainer => {
|
|
||||||
answerSelectors.forEach(selector => {
|
|
||||||
const elements = searchContainer.querySelectorAll(selector);
|
|
||||||
|
|
||||||
elements.forEach(el => {
|
|
||||||
let text = el.textContent.trim();
|
|
||||||
|
|
||||||
// Handle input elements specially
|
|
||||||
if (el.tagName === 'INPUT') {
|
|
||||||
if (el.type === 'radio') {
|
|
||||||
inputType = 'radio';
|
|
||||||
} else if (el.type === 'checkbox') {
|
|
||||||
inputType = 'checkbox';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find associated label
|
// Strategy 3: Find adjacent sibling text
|
||||||
const label = searchContainer.querySelector(`label[for="${el.id}"]`);
|
if (!text) {
|
||||||
if (label) {
|
const parent = input.parentElement;
|
||||||
text = label.textContent.trim();
|
|
||||||
} else {
|
|
||||||
const parentLabel = el.closest('label');
|
|
||||||
if (parentLabel) {
|
|
||||||
text = parentLabel.textContent.trim();
|
|
||||||
} else {
|
|
||||||
// Try to find text near the input
|
|
||||||
const parent = el.parentElement;
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
text = parent.textContent.trim();
|
// Clone and remove the input to get just the text
|
||||||
}
|
const clone = parent.cloneNode(true);
|
||||||
}
|
const inputClone = clone.querySelector('input');
|
||||||
|
if (inputClone) inputClone.remove();
|
||||||
|
text = clone.textContent.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation checks
|
// Strategy 4: Check next sibling
|
||||||
if (!text || text.length < 2 || text.length > 800 || seenTexts.has(text)) {
|
if (!text && input.nextSibling) {
|
||||||
|
text = input.nextSibling.textContent?.trim() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no text found or duplicate
|
||||||
|
if (!text || text.length < 1) {
|
||||||
|
console.log(` Skipping input ${idx} - no text found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (excludePatterns.some(pattern => pattern.test(text))) {
|
if (seenTexts.has(text)) {
|
||||||
return;
|
console.log(` Skipping input ${idx} - duplicate text`);
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude if too many numbers (likely not answer text)
|
|
||||||
const wordCount = text.split(/\s+/).length;
|
|
||||||
const numberCount = (text.match(/\d+/g) || []).length;
|
|
||||||
if (wordCount > 2 && numberCount > wordCount / 2) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
seenTexts.add(text);
|
seenTexts.add(text);
|
||||||
answers.push({
|
answers.push({
|
||||||
text: text,
|
text: text,
|
||||||
element: el,
|
element: input,
|
||||||
parentLabel: el.closest('label') || el
|
parentLabel: labelElement || input.parentElement
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -454,7 +404,7 @@ function getExpectedAnswerCount(questionText, inputType) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced OpenAI call with better prompting
|
// ENHANCED OpenAI call with improved prompting and reasoning
|
||||||
async function getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount) {
|
async function getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount) {
|
||||||
if (!openaiApiKey) {
|
if (!openaiApiKey) {
|
||||||
console.error('❌ No API key available');
|
console.error('❌ No API key available');
|
||||||
@@ -462,68 +412,73 @@ async function getAnswerFromOpenAI(question, answerChoices, inputType, expectedC
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🤖 Querying GPT-4o...');
|
console.log('🤖 Querying GPT-4o with enhanced prompt...');
|
||||||
|
|
||||||
const choicesText = answerChoices.map((c, i) => `${String.fromCharCode(65 + i)}. ${c.text}`).join('\n');
|
const choicesText = answerChoices.map((c, i) => `${String.fromCharCode(65 + i)}. ${c.text}`).join('\n');
|
||||||
|
|
||||||
let instructions = '';
|
let instructions = '';
|
||||||
|
let answerCount = '';
|
||||||
|
|
||||||
if (inputType === 'radio') {
|
if (inputType === 'radio') {
|
||||||
instructions = `CRITICAL: This is a SINGLE-ANSWER question (radio buttons).
|
instructions = `This is a SINGLE-ANSWER question (radio buttons).`;
|
||||||
- You MUST select EXACTLY ONE option
|
answerCount = 'You MUST select EXACTLY ONE option.';
|
||||||
- Even if multiple seem correct, choose the MOST correct one
|
|
||||||
- Provide only ONE letter in your answer`;
|
|
||||||
} else if (expectedCount > 0) {
|
} else if (expectedCount > 0) {
|
||||||
instructions = `CRITICAL: This is a MULTIPLE-ANSWER question requiring EXACTLY ${expectedCount} answer(s).
|
instructions = `This is a MULTIPLE-ANSWER question requiring EXACTLY ${expectedCount} answer(s).`;
|
||||||
- You MUST select EXACTLY ${expectedCount} option(s), no more, no less
|
answerCount = `You MUST select EXACTLY ${expectedCount} option(s), no more, no less.`;
|
||||||
- Evaluate each option independently
|
|
||||||
- Provide exactly ${expectedCount} letter(s) separated by commas`;
|
|
||||||
} else if (expectedCount === -1) {
|
} else if (expectedCount === -1) {
|
||||||
instructions = `CRITICAL: This is a "SELECT ALL THAT APPLY" question.
|
instructions = `This is a "SELECT ALL THAT APPLY" question.`;
|
||||||
- Carefully evaluate EACH option independently
|
answerCount = 'Select ALL correct options. There are typically 2-4 correct answers.';
|
||||||
- Include ALL options that are correct (commonly 2-4 options)
|
|
||||||
- It's better to include a correct answer than to miss one
|
|
||||||
- Provide all correct letters separated by commas`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = `You are a Microsoft Azure certification expert with deep knowledge of:
|
// ENHANCED system prompt with more specific Azure knowledge
|
||||||
- Azure core services (Compute, Storage, Networking, Databases)
|
const systemPrompt = `You are a Microsoft Azure expert preparing someone for Azure certification exams. You have deep, practical knowledge of:
|
||||||
- Azure identity and access management (Entra ID, RBAC)
|
|
||||||
- Azure governance and compliance (Policy, Blueprints, Cost Management)
|
|
||||||
- Azure security (Security Center, Sentinel, Key Vault)
|
|
||||||
- Azure monitoring (Monitor, Log Analytics, Application Insights)
|
|
||||||
- Azure architecture patterns and best practices
|
|
||||||
- Real-world Azure implementation and troubleshooting
|
|
||||||
|
|
||||||
Your expertise comes from years of hands-on Azure experience and Microsoft certification training.
|
AZURE FUNDAMENTALS (AZ-900):
|
||||||
|
- Cloud concepts: IaaS, PaaS, SaaS, public/private/hybrid cloud
|
||||||
|
- Core Azure services: Compute (VMs, App Service, Functions, AKS), Storage (Blob, Files, Disk, Archive), Networking (VNet, VPN, ExpressRoute), Databases (SQL DB, Cosmos DB)
|
||||||
|
- Azure management: Portal, CLI, PowerShell, ARM templates, Azure Resource Manager
|
||||||
|
- Security: Azure AD/Entra ID, RBAC, MFA, Security Center, Key Vault, NSGs
|
||||||
|
- Pricing: Subscriptions, resource groups, cost management, TCO calculator, pricing calculator
|
||||||
|
- Governance: Azure Policy, Blueprints, Management Groups, Tags
|
||||||
|
- Compliance: Trust Center, compliance offerings, data residency
|
||||||
|
|
||||||
|
DECISION PRINCIPLES:
|
||||||
|
1. Microsoft ALWAYS recommends PaaS over IaaS when possible (more managed, less overhead)
|
||||||
|
2. For serverless: Azure Functions > Logic Apps for code-based logic
|
||||||
|
3. For storage: Blob for unstructured data, Azure Files for SMB shares, Disk for VMs
|
||||||
|
4. For databases: SQL Database for relational, Cosmos DB for globally distributed NoSQL
|
||||||
|
5. For identity: Azure AD/Entra ID is the central identity service
|
||||||
|
6. For security: Defense in depth - use multiple layers (NSG, firewalls, encryption)
|
||||||
|
7. For cost optimization: Reserved instances, spot VMs, auto-scaling, Azure Advisor recommendations
|
||||||
|
8. For high availability: Availability Zones > Availability Sets > single VMs
|
||||||
|
9. For hybrid: ExpressRoute > VPN for predictable performance and higher bandwidth
|
||||||
|
10. For compliance: Choose regions based on data residency requirements
|
||||||
|
|
||||||
When answering:
|
When answering:
|
||||||
1. Consider Microsoft's official recommendations and best practices
|
- Consider the Well-Architected Framework: Cost Optimization, Operational Excellence, Performance Efficiency, Reliability, Security
|
||||||
2. Think about the Azure Well-Architected Framework (cost, security, reliability, performance, operations)
|
- Think about what Microsoft would recommend in official documentation
|
||||||
3. For scenario questions, identify key requirements, constraints, and priorities
|
- For "select all" questions, be thorough - multiple answers are usually correct
|
||||||
4. Choose the MOST Azure-native and recommended solution
|
- Pay attention to key words: "cheapest" = lower tier, "most secure" = multiple layers, "fastest" = premium tiers`;
|
||||||
5. If multiple answers seem viable, pick the one Microsoft would recommend in their official docs
|
|
||||||
|
|
||||||
Example:
|
const userPrompt = `${instructions}
|
||||||
Q: Which Azure service provides serverless compute?
|
${answerCount}
|
||||||
Options: A. Azure Virtual Machines B. Azure Functions C. Azure App Service D. Azure Batch
|
|
||||||
Analysis: Looking for serverless - Azure Functions is the correct answer as it's fully serverless (no VM management)
|
|
||||||
Answer: B`;
|
|
||||||
|
|
||||||
const userPrompt = `Question:
|
QUESTION:
|
||||||
${question}
|
${question}
|
||||||
|
|
||||||
Options:
|
OPTIONS:
|
||||||
${choicesText}
|
${choicesText}
|
||||||
|
|
||||||
${instructions}
|
ANALYSIS STEPS:
|
||||||
|
1. Identify the core requirement (cost, security, performance, compliance, etc.)
|
||||||
|
2. Eliminate obviously wrong answers
|
||||||
|
3. Consider Microsoft best practices and official recommendations
|
||||||
|
4. For each remaining option, evaluate against Azure principles
|
||||||
|
5. Choose the answer(s) that Microsoft would recommend
|
||||||
|
|
||||||
Think step-by-step:
|
Think carefully and provide your answer.
|
||||||
1. Identify what the question is really asking
|
|
||||||
2. Consider Azure best practices and official guidance
|
|
||||||
3. Evaluate each option against the requirements
|
|
||||||
4. Choose the answer(s) that Microsoft would recommend
|
|
||||||
|
|
||||||
Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
|
FORMAT: Always respond with "ANSWER: " followed by the LETTER(s) of the correct option(s) (A, B, C, D, etc.), separated by commas if multiple. Never respond with numbers - always use letters to indicate which option is correct.`;
|
||||||
|
|
||||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -537,31 +492,42 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
|
|||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: userPrompt }
|
{ role: 'user', content: userPrompt }
|
||||||
],
|
],
|
||||||
temperature: 0.3,
|
temperature: 0.2, // Lowered for more consistent, confident answers
|
||||||
max_tokens: 600
|
max_tokens: 800,
|
||||||
|
top_p: 0.95 // Added for better quality
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
console.error('❌ OpenAI API error:', error);
|
console.error('❌ OpenAI API error:', error);
|
||||||
|
|
||||||
|
// Check for rate limit error (status 429 or error code)
|
||||||
|
if (response.status === 429 || error?.error?.type === 'rate_limit_exceeded' || error?.error?.code === 'rate_limit_exceeded') {
|
||||||
|
console.log('⚠️ Rate limit detected, will retry with backoff');
|
||||||
|
return 'RATE_LIMITED';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const fullResponse = data.choices[0].message.content.trim();
|
const fullResponse = data.choices[0].message.content.trim();
|
||||||
|
|
||||||
console.log('🤖 GPT-4o response:', fullResponse.substring(0, 300));
|
console.log('🤖 GPT-4o response:', fullResponse.substring(0, 400));
|
||||||
|
|
||||||
// Extract answer
|
// Extract answer with multiple strategies
|
||||||
let answer = null;
|
let answer = null;
|
||||||
|
|
||||||
|
// Strategy 1: Look for "ANSWER:" format
|
||||||
const answerMatch = fullResponse.match(/ANSWER:\s*([A-Z](?:,\s*[A-Z])*)/i);
|
const answerMatch = fullResponse.match(/ANSWER:\s*([A-Z](?:,\s*[A-Z])*)/i);
|
||||||
if (answerMatch) {
|
if (answerMatch) {
|
||||||
answer = answerMatch[1];
|
answer = answerMatch[1];
|
||||||
} else {
|
} else {
|
||||||
// Fallback extraction
|
// Strategy 2: Look for common answer patterns
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/(?:correct|answer|select).*?:\s*([A-Z](?:,\s*[A-Z])*)/i,
|
/(?:correct|answer|select).*?:\s*([A-Z](?:,\s*[A-Z])*)/i,
|
||||||
|
/(?:therefore|thus|so).*?([A-Z](?:,\s*[A-Z])*)\s*$/i,
|
||||||
/\b([A-Z](?:,\s*[A-Z])*)\s*$/,
|
/\b([A-Z](?:,\s*[A-Z])*)\s*$/,
|
||||||
/\b([A-Z](?:,\s*[A-Z])*)\b/
|
/\b([A-Z](?:,\s*[A-Z])*)\b/
|
||||||
];
|
];
|
||||||
@@ -587,7 +553,7 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
|
|||||||
.map(l => l.charCodeAt(0) - 65)
|
.map(l => l.charCodeAt(0) - 65)
|
||||||
.filter(i => i >= 0 && i < answerChoices.length);
|
.filter(i => i >= 0 && i < answerChoices.length);
|
||||||
|
|
||||||
// Validation
|
// Strict validation
|
||||||
if (inputType === 'radio' && correctIndices.length > 1) {
|
if (inputType === 'radio' && correctIndices.length > 1) {
|
||||||
console.log(`⚠️ Radio button but got ${correctIndices.length} answers - using first only`);
|
console.log(`⚠️ Radio button but got ${correctIndices.length} answers - using first only`);
|
||||||
return [correctIndices[0]];
|
return [correctIndices[0]];
|
||||||
@@ -595,6 +561,13 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
|
|||||||
|
|
||||||
if (expectedCount > 0 && correctIndices.length !== expectedCount) {
|
if (expectedCount > 0 && correctIndices.length !== expectedCount) {
|
||||||
console.log(`⚠️ Expected ${expectedCount} answers but got ${correctIndices.length}`);
|
console.log(`⚠️ Expected ${expectedCount} answers but got ${correctIndices.length}`);
|
||||||
|
// If GPT gave wrong count for a specific-count question, this might indicate uncertainty
|
||||||
|
// But we'll still use what it gave us
|
||||||
|
}
|
||||||
|
|
||||||
|
if (correctIndices.length === 0) {
|
||||||
|
console.error('❌ No valid answer indices extracted');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return correctIndices;
|
return correctIndices;
|
||||||
@@ -605,17 +578,25 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry wrapper
|
// Retry wrapper with exponential backoff for rate limits
|
||||||
async function getAnswerWithRetry(question, answerChoices, inputType, expectedCount, retries = 2) {
|
async function getAnswerWithRetry(question, answerChoices, inputType, expectedCount, retries = 5) {
|
||||||
for (let i = 0; i <= retries; i++) {
|
for (let i = 0; i <= retries; i++) {
|
||||||
try {
|
try {
|
||||||
const result = await getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount);
|
const result = await getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount);
|
||||||
|
|
||||||
|
if (result === 'RATE_LIMITED') {
|
||||||
|
const backoffMs = Math.min(1000 * Math.pow(2, i), 60000); // Max 60 seconds
|
||||||
|
console.log(`⏳ Rate limited. Waiting ${backoffMs / 1000}s before retry ${i + 1}/${retries}...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (result && result.length > 0) {
|
if (result && result.length > 0) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (i < retries) {
|
if (i < retries) {
|
||||||
console.log(`⚠️ Attempt ${i + 1} failed, retrying...`);
|
console.log(`⚠️ Attempt ${i + 1} failed or returned no answer, retrying...`);
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000 * (i + 1)));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Attempt ${i + 1} error:`, error);
|
console.error(`❌ Attempt ${i + 1} error:`, error);
|
||||||
@@ -635,9 +616,8 @@ function highlightAnswer(element, isCorrect = true) {
|
|||||||
targetElement = label || element.parentElement;
|
targetElement = label || element.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetElement.classList.remove('quiz-helper-correct', 'quiz-helper-unknown');
|
targetElement.classList.remove('quiz-helper-correct');
|
||||||
const className = isCorrect ? 'quiz-helper-correct' : 'quiz-helper-unknown';
|
targetElement.classList.add('quiz-helper-correct');
|
||||||
targetElement.classList.add(className);
|
|
||||||
targetElement.setAttribute('data-quiz-helper-highlight', 'true');
|
targetElement.setAttribute('data-quiz-helper-highlight', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +625,7 @@ function highlightAnswer(element, isCorrect = true) {
|
|||||||
function removeHighlights() {
|
function removeHighlights() {
|
||||||
const highlighted = document.querySelectorAll('[data-quiz-helper-highlight="true"]');
|
const highlighted = document.querySelectorAll('[data-quiz-helper-highlight="true"]');
|
||||||
highlighted.forEach(el => {
|
highlighted.forEach(el => {
|
||||||
el.classList.remove('quiz-helper-correct', 'quiz-helper-unknown');
|
el.classList.remove('quiz-helper-correct');
|
||||||
el.removeAttribute('data-quiz-helper-highlight');
|
el.removeAttribute('data-quiz-helper-highlight');
|
||||||
});
|
});
|
||||||
console.log(`Removed ${highlighted.length} highlights`);
|
console.log(`Removed ${highlighted.length} highlights`);
|
||||||
@@ -659,7 +639,8 @@ async function scanAndHighlight() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
console.log('🚀 Starting enhanced Microsoft Learn scan...');
|
console.log('🚀 Starting enhanced scan with GPT-4o...');
|
||||||
|
console.log('📊 Improvements: Lower temperature, enhanced Azure knowledge, better answer extraction');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const questions = findQuestions();
|
const questions = findQuestions();
|
||||||
@@ -707,15 +688,11 @@ async function scanAndHighlight() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ AI could not determine answer - marking all RED');
|
console.log('⚠️ AI could not determine answer - no highlighting applied');
|
||||||
answerChoices.forEach(choice => {
|
|
||||||
try {
|
|
||||||
highlightAnswer(choice.element, false);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
// Slightly longer delay to avoid rate limits and allow for higher quality responses
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3500));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✅ Scan complete!');
|
console.log('\n✅ Scan complete!');
|
||||||
@@ -726,7 +703,7 @@ async function scanAndHighlight() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutation observer for dynamic content
|
// Mutation observer for dynamic content - ignores self-triggered changes
|
||||||
let mutationTimeout;
|
let mutationTimeout;
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!extensionEnabled || isProcessing) return;
|
if (!extensionEnabled || isProcessing) return;
|
||||||
@@ -734,7 +711,10 @@ const observer = new MutationObserver((mutations) => {
|
|||||||
const hasNewContent = mutations.some(mutation => {
|
const hasNewContent = mutations.some(mutation => {
|
||||||
return mutation.addedNodes.length > 0 &&
|
return mutation.addedNodes.length > 0 &&
|
||||||
Array.from(mutation.addedNodes).some(node =>
|
Array.from(mutation.addedNodes).some(node =>
|
||||||
node.nodeType === 1 && node.textContent.length > 50
|
node.nodeType === 1 &&
|
||||||
|
node.textContent.length > 50 &&
|
||||||
|
!node.classList?.contains('quiz-helper-correct') &&
|
||||||
|
!node.hasAttribute?.('data-quiz-helper-highlight')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -751,3 +731,39 @@ observer.observe(document.body, {
|
|||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for page navigation (Next button clicks)
|
||||||
|
let lastUrl = location.href;
|
||||||
|
const urlObserver = new MutationObserver(() => {
|
||||||
|
if (location.href !== lastUrl) {
|
||||||
|
lastUrl = location.href;
|
||||||
|
console.log('🔄 Page navigation detected, resetting and rescanning...');
|
||||||
|
processedQuestions.clear();
|
||||||
|
isProcessing = false;
|
||||||
|
setTimeout(() => waitForContentAndScan(), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
urlObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Also listen for popstate (browser back/forward)
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
console.log('🔄 Browser navigation detected, resetting and rescanning...');
|
||||||
|
processedQuestions.clear();
|
||||||
|
isProcessing = false;
|
||||||
|
setTimeout(() => waitForContentAndScan(), 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for Next button clicks
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (target.tagName === 'SPAN' && target.textContent.trim() === 'Next') {
|
||||||
|
console.log('🔄 Next button clicked, scanning in 1 second...');
|
||||||
|
setTimeout(() => {
|
||||||
|
processedQuestions.clear();
|
||||||
|
isProcessing = false;
|
||||||
|
removeHighlights();
|
||||||
|
waitForContentAndScan();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -127,6 +127,22 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
.retry-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
.retry-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -144,7 +160,7 @@
|
|||||||
<div id="apiStatus" class="api-status" style="display: none;"></div>
|
<div id="apiStatus" class="api-status" style="display: none;"></div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
Get a free API key at <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a>.
|
Get a free API key at <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a>.
|
||||||
New accounts get $5 in free credits. Cost per question: ~$0.001 (1/10th of a cent).
|
New accounts get $5 in free credits. Cost per question: ~$0.002 with GPT-4o.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,17 +170,16 @@
|
|||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span id="toggleLabel">Off</span>
|
<span id="toggleLabel">Off</span>
|
||||||
|
<button id="retryBtn" class="retry-btn">Retry Scan</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
When enabled, ChatGPT will analyze quiz questions and highlight answers:
|
When enabled, GPT-4o will analyze quiz questions and highlight correct answers in <strong style="color: #28a745;">green</strong>.
|
||||||
<ul style="margin: 5px 0; padding-left: 20px; font-size: 11px;">
|
Questions without highlighting mean the AI couldn't determine the answer with confidence.
|
||||||
<li><strong style="color: #28a745;">Green</strong> = ChatGPT identified correct answer(s)</li>
|
<br><br>
|
||||||
<li><strong style="color: #dc3545;">Red</strong> = ChatGPT failed, you need to guess</li>
|
Check the browser console (F12) for detailed logs and reasoning.
|
||||||
</ul>
|
|
||||||
Check the browser console (F12) for detailed logs.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
@@ -60,6 +60,47 @@ document.getElementById('toggleSwitch').addEventListener('change', function(e) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Retry button click
|
||||||
|
document.getElementById('retryBtn').addEventListener('click', function() {
|
||||||
|
const btn = document.getElementById('retryBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Scanning...';
|
||||||
|
|
||||||
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
|
if (tabs[0]) {
|
||||||
|
const url = tabs[0].url;
|
||||||
|
|
||||||
|
if (url.startsWith('chrome://') || url.startsWith('chrome-extension://') || url.startsWith('edge://')) {
|
||||||
|
document.getElementById('status').textContent = 'Extension cannot run on this page';
|
||||||
|
document.getElementById('status').style.color = '#d13438';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Retry Scan';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('status').textContent = '';
|
||||||
|
document.getElementById('status').style.color = '';
|
||||||
|
}, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, {
|
||||||
|
action: 'retryScan'
|
||||||
|
}).then(() => {
|
||||||
|
window.close();
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('Could not send retry message:', error);
|
||||||
|
document.getElementById('status').textContent = 'Could not initiate scan - refresh the page';
|
||||||
|
document.getElementById('status').style.color = '#d13438';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Retry Scan';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('status').textContent = '';
|
||||||
|
document.getElementById('status').style.color = '';
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Handle API key save
|
// Handle API key save
|
||||||
document.getElementById('saveApiKey').addEventListener('click', function() {
|
document.getElementById('saveApiKey').addEventListener('click', function() {
|
||||||
const apiKey = document.getElementById('apiKey').value.trim();
|
const apiKey = document.getElementById('apiKey').value.trim();
|
||||||
Reference in new issue
Block a user