Group multi-line questions into single container instead of treating each line as separate question

This commit is contained in:
justin committed 2026-01-08 10:15:47 -07:00
1 parent 4b1b694715
commit f79629e1b6
2 files changed
+176 -186

No files matched your search

+171 -179
View File
@@ -23,19 +23,6 @@ function injectCustomStyles() {
.quiz-helper-correct * { .quiz-helper-correct * {
background-color: transparent !important; 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;
@@ -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,11 +160,12 @@ 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
@@ -183,113 +174,108 @@ function findQuestions() {
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];
const questionPatterns = [
/\?$/, // Ends with question mark
/\?\s*$/, // Ends with question mark and whitespace
/^(what|when|where|who|why|how|which|is|are|does|do|did|can|could|would|should|will|shall|must)/i,
/you need to/i,
/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 => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => { containers.forEach(container => {
const text = el.textContent.trim(); // 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
// Skip very short or very long text, or duplicates // Find the question text part (everything before the answer choices)
if (text.length < 15 || text.length > 1500 || seenTexts.has(text)) { let questionText = '';
return; const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
} let node;
const answerContainer = inputs[0].closest('label, [class*="answer"], [class*="option"]')?.parentElement;
// Skip elements that are likely navigation or UI while (node = walker.nextNode()) {
if (/^(next|previous|submit|skip|back|continue|finish|review|question \d+ of \d+)$/i.test(text)) { const parent = node.parentElement;
return; // 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;
// Check against question patterns
for (const pattern of questionPatterns) { const text = node.textContent.trim();
if (pattern.test(text)) { if (text.length > 2) {
seenTexts.add(text); questionText += text + ' ';
questions.push({
text: text,
element: el
});
console.log('✓ Found question:', text.substring(0, 120) + '...');
break;
} }
} }
questionText = questionText.trim();
if (questionText.length < 15 || seenTexts.has(questionText)) return;
if (/^(next|previous|submit|skip)$/i.test(questionText)) return;
seenTexts.add(questionText);
questions.push({
text: cleanQuestionText(questionText),
element: container
});
console.log('✓ Found question:', questionText.substring(0, 120) + '...');
}); });
}); }
// 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
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
let container = questionElement.closest('[role="group"], fieldset, form, [class*="question"], [class*="answer"], [class*="option"], div'); let container = questionElement.closest('[role="group"], fieldset, form, [class*="question"], [class*="answer"], [class*="option"], div');
// Expand search scope if needed
if (!container || container === questionElement) { if (!container || container === questionElement) {
container = questionElement.parentElement; container = questionElement.parentElement;
} }
// Search up to 4 levels if we haven't found inputs yet
for (let i = 0; i < 4 && container; i++) { for (let i = 0; i < 4 && container; i++) {
const inputsInContainer = container.querySelectorAll('input[type="radio"], input[type="checkbox"]'); const inputsInContainer = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
if (inputsInContainer.length > 0) { if (inputsInContainer.length > 0) {
@@ -301,7 +287,6 @@ function findAnswerChoices(questionElement) {
if (!container) container = document.body; if (!container) container = document.body;
// Also check siblings
let nextSibling = questionElement.nextElementSibling; let nextSibling = questionElement.nextElementSibling;
let siblingAttempts = 0; let siblingAttempts = 0;
const siblingContainers = [container]; const siblingContainers = [container];
@@ -345,7 +330,6 @@ function findAnswerChoices(questionElement) {
elements.forEach(el => { elements.forEach(el => {
let text = el.textContent.trim(); let text = el.textContent.trim();
// Handle input elements specially
if (el.tagName === 'INPUT') { if (el.tagName === 'INPUT') {
if (el.type === 'radio') { if (el.type === 'radio') {
inputType = 'radio'; inputType = 'radio';
@@ -353,7 +337,6 @@ function findAnswerChoices(questionElement) {
inputType = 'checkbox'; inputType = 'checkbox';
} }
// Find associated label
const label = searchContainer.querySelector(`label[for="${el.id}"]`); const label = searchContainer.querySelector(`label[for="${el.id}"]`);
if (label) { if (label) {
text = label.textContent.trim(); text = label.textContent.trim();
@@ -362,7 +345,6 @@ function findAnswerChoices(questionElement) {
if (parentLabel) { if (parentLabel) {
text = parentLabel.textContent.trim(); text = parentLabel.textContent.trim();
} else { } else {
// Try to find text near the input
const parent = el.parentElement; const parent = el.parentElement;
if (parent) { if (parent) {
text = parent.textContent.trim(); text = parent.textContent.trim();
@@ -371,7 +353,6 @@ function findAnswerChoices(questionElement) {
} }
} }
// Validation checks
if (!text || text.length < 2 || text.length > 800 || seenTexts.has(text)) { if (!text || text.length < 2 || text.length > 800 || seenTexts.has(text)) {
return; return;
} }
@@ -380,7 +361,6 @@ function findAnswerChoices(questionElement) {
return; return;
} }
// Exclude if too many numbers (likely not answer text)
const wordCount = text.split(/\s+/).length; const wordCount = text.split(/\s+/).length;
const numberCount = (text.match(/\d+/g) || []).length; const numberCount = (text.match(/\d+/g) || []).length;
if (wordCount > 2 && numberCount > wordCount / 2) { if (wordCount > 2 && numberCount > wordCount / 2) {
@@ -454,7 +434,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 +442,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: Respond with only "ANSWER: " followed by the letter(s) separated by commas (e.g., "ANSWER: B" or "ANSWER: A,C,D")`;
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,8 +522,9 @@ 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
}) })
}); });
@@ -551,17 +537,20 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
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 +576,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 +584,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,7 +601,7 @@ Format: ANSWER: [letter(s) only - e.g., "B" or "A,C,D"]`;
} }
} }
// Retry wrapper // Retry wrapper with better backoff
async function getAnswerWithRetry(question, answerChoices, inputType, expectedCount, retries = 2) { async function getAnswerWithRetry(question, answerChoices, inputType, expectedCount, retries = 2) {
for (let i = 0; i <= retries; i++) { for (let i = 0; i <= retries; i++) {
try { try {
@@ -614,8 +610,8 @@ async function getAnswerWithRetry(question, answerChoices, inputType, expectedCo
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 +631,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 +640,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 +654,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 +703,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!');
+5 -7
View File
@@ -144,7 +144,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>
@@ -159,12 +159,10 @@
<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>