From c1d853de4ef757bbd563529c68b1d076ca8d71f3 Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Wed, 7 Jan 2026 17:07:04 -0700 Subject: [PATCH] AI-Powered Quiz Assistant (Chrome Extension) --- README.md | 138 +++++++++++ background.js | 19 ++ content.js | 633 ++++++++++++++++++++++++++++++++++++++++++++++++++ icon128.png | Bin 0 -> 49538 bytes icon16.png | Bin 0 -> 965 bytes icon48.png | Bin 0 -> 7141 bytes manifest.json | 37 +++ popup.html | 172 ++++++++++++++ popup.js | 102 ++++++++ 9 files changed, 1101 insertions(+) create mode 100644 README.md create mode 100644 background.js create mode 100644 content.js create mode 100644 icon128.png create mode 100644 icon16.png create mode 100644 icon48.png create mode 100644 manifest.json create mode 100644 popup.html create mode 100644 popup.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..df19e04 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# šŸŽ“ Quiz Helper - AI-Powered Quiz Assistant + +An intelligent Chrome extension that uses ChatGPT to help you with online quizzes by displaying helpful hint bubbles with [somewhat] correct answers. + +![Chrome Extension](https://img.shields.io/badge/Chrome-Extension-green) +![License](https://img.shields.io/badge/license-MIT-blue) +![AI Powered](https://img.shields.io/badge/AI-ChatGPT-orange) + +## ✨ Features + +- šŸ¤– **AI-Powered**: Uses ChatGPT (GPT-3.5-turbo) for intelligent answer analysis +- šŸ’” **Smart Hints**: Highlights answers in green +- šŸŽÆ **Accurate**: ChatGPT sees actual answer text for better context +- ⚔ **Auto-Detection**: Automatically detects single vs. multiple answer questions +- šŸ”’ **Private**: All data stays between you and OpenAI +- šŸ’° **Affordable**: ~$0.001 per question (100 questions ā‰ˆ $0.10-0.20) + +## šŸ“‹ Requirements + +- Google Chrome or Chromium-based browser (Edge, Brave, etc.) +- OpenAI API key ([get one free here](https://platform.openai.com/api-keys)) +- Internet connection + +## šŸš€ Installation + +1. **Download** this repository: + - Click the green "Code" button above + - Select "Download ZIP" + - Extract the ZIP file + +2. **Load in Chrome**: + - Open Chrome and navigate to `chrome://extensions/` + - Enable "Developer mode" (toggle in top-right corner) + - Click "Load unpacked" + - Select the `extension` folder from the extracted files + +3. **Add API Key**: + - Click the extension icon in your toolbar + - Paste your OpenAI API key + - Click "Save API Key" + +## šŸŽÆ How to Use + +1. **Navigate to a quiz** (works best with Microsoft Learn practice assessments) +2. **Click the extension icon** in your toolbar +3. **Toggle the switch to "On"** +4. **Wait a moment** - Correct answer(s) will be marked in green +5. **Select your answers** based on the hints +6. **Toggle off** when done to remove hints + +## šŸ”§ Configuration + +The extension requires an OpenAI API key to function. To get one: + +1. Visit [platform.openai.com/api-keys](https://platform.openai.com/api-keys) +2. Sign up or log in +3. Click "Create new secret key" +4. Copy the key (starts with `sk-...`) +5. Paste it in the extension popup + +**Note**: New OpenAI accounts get $5 in free credits! + +## šŸ’° Costs + +- Model used: `gpt-3.5-turbo` +- Cost per question: ~$0.001-0.002 (1/10th of a penny) +- 50 questions: ~$0.05-0.10 +- 100 questions: ~$0.10-0.20 + +Very affordable for exam preparation! + +## šŸ› ļø Technical Details + +### How It Works + +1. **Question Detection**: Scans the page for quiz questions using multiple selectors +2. **Answer Identification**: Finds associated answer choices (radio buttons, checkboxes) +3. **Smart Prompting**: Sends the question and answer choices to ChatGPT +4. **Response Matching**: Matches ChatGPT's response to actual answer text +5. **Hint Display**: Shows a styled hint bubble with the correct answer(s) + +### Files Structure + +``` +extension/ +ā”œā”€ā”€ manifest.json # Extension configuration +ā”œā”€ā”€ content.js # Main logic (question detection, ChatGPT API) +ā”œā”€ā”€ popup.html # Popup interface +ā”œā”€ā”€ popup.js # Popup functionality +ā”œā”€ā”€ background.js # Background service worker +ā”œā”€ā”€ icon16.png # Icon (16x16) +ā”œā”€ā”€ icon48.png # Icon (48x48) +└── icon128.png # Icon (128x128) +``` + +## šŸ”’ Privacy & Security + +- Your API key is stored locally in Chrome's sync storage +- No data is collected or sent anywhere except directly to OpenAI +- The extension only runs when you explicitly enable it +- All communication is encrypted (HTTPS) + +## āš ļø Disclaimer + +**For Educational Use Only** + +This extension is designed for learning and study purposes. Use responsibly and in accordance with your institution's academic integrity policies. The accuracy of answers depends on ChatGPT's knowledge and may not always be 100% correct. + +## šŸ› Troubleshooting + +**Extension won't load:** +- Make sure all files are in the `extension` folder +- Check that `manifest.json` has no syntax errors +- Ensure all three icon files are present + +**No hints appearing:** +- Check that you've added a valid OpenAI API key +- Open browser console (F12) and look for error messages +- Make sure the extension toggle is "On" +- Try refreshing the quiz page + +**API errors:** +- Verify your API key is correct (starts with `sk-`) +- Check you have credits in your OpenAI account +- Look for rate limit messages in console + +## šŸ¤ Contributing + +Contributions are welcome! Feel free to: +- Report bugs +- Suggest features +- Submit pull requests + +## šŸ“„ License + +MIT License - feel free to use and modify as needed. + +**Made with ā¤ļø for students and learners everywhere** diff --git a/background.js b/background.js new file mode 100644 index 0000000..61cdbac --- /dev/null +++ b/background.js @@ -0,0 +1,19 @@ +// Background service worker for the extension +// Handles installation and updates + +chrome.runtime.onInstalled.addListener(() => { + console.log('Quiz Helper extension installed'); + + // Set default state + chrome.storage.sync.set({ enabled: false }); +}); + +// Listen for messages from content scripts or popup +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'getState') { + chrome.storage.sync.get(['enabled'], (result) => { + sendResponse({ enabled: result.enabled || false }); + }); + return true; // Indicates async response + } +}); diff --git a/content.js b/content.js new file mode 100644 index 0000000..d0b011f --- /dev/null +++ b/content.js @@ -0,0 +1,633 @@ +let extensionEnabled = false; +let isProcessing = false; +let processedQuestions = new Set(); +let openaiApiKey = null; +let styleInjected = false; + +// Inject custom CSS for highlighting +function injectCustomStyles() { + if (styleInjected) return; + + const style = document.createElement('style'); + style.id = 'quiz-helper-styles'; + style.textContent = ` + .quiz-helper-correct { + background-color: #90EE90 !important; + border: 3px solid #28a745 !important; + box-shadow: 0 0 10px rgba(40, 167, 69, 0.6) !important; + padding: 8px !important; + border-radius: 4px !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); + styleInjected = true; + console.log('āœ“ Custom CSS styles injected'); +} + +// Initialize extension state +chrome.storage.sync.get(['enabled', 'openaiApiKey'], function(result) { + extensionEnabled = result.enabled || false; + openaiApiKey = result.openaiApiKey || null; + + if (openaiApiKey) { + console.log('āœ“ OpenAI API key loaded'); + } else { + console.log('⚠ No OpenAI API key found. Please add one in the extension popup.'); + } + + if (extensionEnabled) { + injectCustomStyles(); + waitForContentAndScan(); + } +}); + +// Listen for toggle messages +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'toggleExtension') { + extensionEnabled = request.enabled; + + if (extensionEnabled) { + if (!openaiApiKey) { + console.error('āŒ Cannot enable: No OpenAI API key set. Please add one in the extension popup.'); + } else { + injectCustomStyles(); + waitForContentAndScan(); + } + } else { + removeHighlights(); + processedQuestions.clear(); + } + } else if (request.action === 'apiKeyUpdated') { + openaiApiKey = request.apiKey; + console.log('āœ“ OpenAI API key updated'); + processedQuestions.clear(); + } +}); + +// Wait for dynamic content to load before scanning +function waitForContentAndScan() { + console.log('Waiting for quiz content to load...'); + + let attempts = 0; + const maxAttempts = 40; + + const checkContent = setInterval(() => { + attempts++; + + const hasContent = document.body.innerText.length > 500; + const hasInteractiveElements = document.querySelectorAll('button, input, label').length > 5; + const mainContent = document.querySelector('[data-main-column]'); + const hasMainContent = mainContent && mainContent.innerText.length > 100; + const hasQuizElements = document.querySelectorAll('input[type="radio"], input[type="checkbox"]').length > 0; + + console.log(`Attempt ${attempts}: content=${hasContent}, interactive=${hasInteractiveElements}, main=${hasMainContent}, quiz=${hasQuizElements}`); + + if ((hasContent && hasInteractiveElements && hasMainContent) || hasQuizElements) { + clearInterval(checkContent); + console.log('āœ“ Quiz content detected! Starting scan in 2 seconds...'); + setTimeout(() => scanAndHighlight(), 2000); + } else if (attempts >= maxAttempts) { + clearInterval(checkContent); + console.log('āœ— Timeout: Quiz content did not load. Try refreshing the page.'); + } + }, 500); +} + +// Function to identify potential questions +function findQuestions() { + const questions = []; + const seenTexts = new Set(); + + console.log('DEBUG: Total body text length:', document.body.innerText.length); + console.log('DEBUG: Sample text:', document.body.innerText.substring(0, 500)); + + const selectors = [ + '[role="group"]', + '[class*="question"]', + '[class*="quiz"]', + '[class*="assessment"]', + '[data-test*="question"]', + 'fieldset', + 'h1, h2, h3, h4, h5, h6', + 'p', + 'div[class*="text"]', + 'div', + 'span', + 'label' + ]; + + const questionPatterns = [ + /\?$/, + /\?/, + /^(what|when|where|who|why|how|which|is|are|does|do|did|can|could|would|should|will|shall)/i, + /\d+\.\s*[A-Z]/, + /^(true|false)/i, + /characteristics/i, + /deployment model/i + ]; + + selectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + console.log(`DEBUG: Found ${elements.length} elements for selector "${selector}"`); + + elements.forEach(el => { + const text = el.textContent.trim(); + + if (text.length < 10 || text.length > 1000 || seenTexts.has(text)) { + return; + } + + for (const pattern of questionPatterns) { + if (pattern.test(text)) { + seenTexts.add(text); + questions.push({ + text: text, + element: el + }); + console.log('Found question:', text.substring(0, 100)); + break; + } + } + }); + }); + + if (questions.length === 0) { + console.log('DEBUG: No questions found. Logging first 10 p elements:'); + const pElements = document.querySelectorAll('p'); + Array.from(pElements).slice(0, 10).forEach((el, i) => { + console.log(` p[${i}]:`, el.textContent.substring(0, 100)); + }); + + console.log('DEBUG: Logging first 10 divs with text:'); + const divs = document.querySelectorAll('div'); + let count = 0; + for (const div of divs) { + const text = div.textContent.trim(); + if (text.length > 20 && text.length < 200) { + console.log(` div[${count}]:`, text.substring(0, 100)); + count++; + if (count >= 10) break; + } + } + } + + return questions; +} + +// Function to find potential answer choices +function findAnswerChoices(questionElement) { + const answers = []; + const seenTexts = new Set(); + let inputType = null; // 'radio' or 'checkbox' + + let container = questionElement.closest('[role="group"], fieldset, form, [class*="question"], div[class*="options"]'); + if (!container) { + container = questionElement.parentElement; + } + + for (let i = 0; i < 3 && container && answers.length === 0; i++) { + container = container.parentElement; + if (!container) break; + } + + if (!container) container = document.body; + + const answerSelectors = [ + 'input[type="radio"]', + 'input[type="checkbox"]', + 'button[role="radio"]', + 'button[role="checkbox"]', + '[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$/i, + /^previous$/i, + /^submit$/i, + /^skip$/i, + /^back$/i, + /^continue$/i, + /^finish$/i, + /^review$/i, + /^\d+\s*\/\s*\d+$/, + /^score:/i, + /^time:/i, + /^remaining:/i + ]; + + answerSelectors.forEach(selector => { + const elements = container.querySelectorAll(selector); + + elements.forEach(el => { + let text = el.textContent.trim(); + + // Detect input type from actual input elements + if (el.tagName === 'INPUT') { + if (el.type === 'radio') { + inputType = 'radio'; + } else if (el.type === 'checkbox') { + inputType = 'checkbox'; + } + + const label = container.querySelector(`label[for="${el.id}"]`); + if (label) { + text = label.textContent.trim(); + } else { + const parentLabel = el.closest('label'); + if (parentLabel) { + text = parentLabel.textContent.trim(); + } + } + } + + if (!text || text.length < 2 || text.length > 300 || seenTexts.has(text)) { + return; + } + + if (excludePatterns.some(pattern => pattern.test(text))) { + console.log(`Excluding UI element: "${text}"`); + return; + } + + const wordCount = text.split(/\s+/).length; + const numberCount = (text.match(/\d+/g) || []).length; + if (numberCount > wordCount / 2) { + console.log(`Excluding numeric element: "${text}"`); + return; + } + + seenTexts.add(text); + answers.push({ + text: text, + element: el, + parentLabel: el.closest('label') || el + }); + }); + }); + + console.log(`Found ${answers.length} answer choices (type: ${inputType || 'unknown'})`); + answers.forEach((a, i) => { + console.log(` [${i}] "${a.text.substring(0, 50)}" - Element: ${a.element.tagName}.${a.element.className || 'no-class'}, ID: ${a.element.id || 'no-id'}`); + }); + + return { answers, inputType }; +} + +// Function to determine expected number of answers from question text +function getExpectedAnswerCount(questionText, inputType) { + if (inputType === 'radio') { + return 1; // Radio buttons always mean single answer + } + + // For checkboxes, try to find the expected count in the question + const lowerQuestion = questionText.toLowerCase(); + + // Look for explicit numbers + const numberWords = { + 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, + 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10 + }; + + // Check for patterns like "select two", "choose three", "two factors", etc. + for (const [word, num] of Object.entries(numberWords)) { + const patterns = [ + new RegExp(`\\b${word}\\s+(answers?|options?|choices?|factors?|characteristics?|items?|reasons?|ways?|methods?|types?)\\b`, 'i'), + new RegExp(`\\bselect\\s+${word}\\b`, 'i'), + new RegExp(`\\bchoose\\s+${word}\\b`, 'i'), + new RegExp(`\\bpick\\s+${word}\\b`, 'i'), + new RegExp(`\\bwhich\\s+${word}\\b`, 'i') + ]; + + for (const pattern of patterns) { + if (pattern.test(lowerQuestion)) { + console.log(`šŸ“Š Detected ${num} expected answers from question text`); + return num; + } + } + } + + // Check for digit numbers (e.g., "2 answers", "3 characteristics") + const digitMatch = lowerQuestion.match(/\b(\d+)\s+(answers?|options?|choices?|factors?|characteristics?|items?|reasons?|ways?|methods?|types?)\b/i); + if (digitMatch) { + const count = parseInt(digitMatch[1]); + console.log(`šŸ“Š Detected ${count} expected answers from question text`); + return count; + } + + // Check for "Each correct answer" or "select all that apply" - means multiple but unknown count + if (/each correct answer|select all that apply|all that apply|select all|choose all/i.test(lowerQuestion)) { + console.log(`šŸ“Š Detected "select all" question - expecting multiple answers`); + return -1; // Special value meaning "multiple, unknown count" + } + + // Default for checkboxes: assume multiple but we don't know how many + console.log(`šŸ“Š Checkbox question with no specific count mentioned`); + return -1; +} + +// Function to get answer using OpenAI +async function getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount) { + if (!openaiApiKey) { + console.error('āŒ No API key available'); + return null; + } + + try { + console.log('šŸ¤– Asking ChatGPT...'); + + const choicesText = answerChoices.map((c, i) => `${String.fromCharCode(65 + i)}. ${c.text}`).join('\n'); + + let instructions = ''; + if (inputType === 'radio') { + instructions = '- This is a SINGLE-ANSWER question (radio buttons). Provide exactly ONE letter.'; + } else if (expectedCount > 0) { + instructions = `- This is a MULTIPLE-ANSWER question (checkboxes). Provide exactly ${expectedCount} letter(s), separated by commas.`; + } else if (expectedCount === -1) { + instructions = '- This is a MULTIPLE-ANSWER question (checkboxes). Provide ALL correct answers, separated by commas.'; + } else { + instructions = '- Determine if this requires one or multiple answers based on the question wording.'; + } + + const prompt = `You are helping with a Microsoft Azure certification exam. Answer the following multiple-choice question by selecting the correct answer(s). + +Question: ${question} + +Answer choices: +${choicesText} + +Instructions: +${instructions} +- Respond ONLY with the letter(s) of the correct answer(s) (e.g., "A" or "A,C,D") +- Do not include any explanation, reasoning, or additional text +- Do not include spaces after commas + +Correct answer(s):`; + + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${openaiApiKey}` + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: 'You are an expert on Microsoft Azure and cloud computing. You provide only the letter(s) of correct answers to multiple-choice questions, without any explanation.' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.1, + max_tokens: 20 + }) + }); + + if (!response.ok) { + const error = await response.json(); + console.error('āŒ OpenAI API error:', error); + if (error.error && error.error.message) { + console.error('Error message:', error.error.message); + } + return null; + } + + const data = await response.json(); + const answer = data.choices[0].message.content.trim(); + + console.log('šŸ¤– ChatGPT says:', answer); + + const correctLetters = answer.replace(/\s/g, '').split(',').map(l => l.trim().toUpperCase()); + const correctIndices = correctLetters.map(l => l.charCodeAt(0) - 65).filter(i => i >= 0 && i < answerChoices.length); + + // Validate answer count for radio buttons + if (inputType === 'radio' && correctIndices.length > 1) { + console.log(`āš ļø Warning: Radio button question but got ${correctIndices.length} answers. Using only the first one.`); + return [correctIndices[0]]; + } + + // Validate answer count for checkboxes with specific count + if (expectedCount > 0 && correctIndices.length !== expectedCount) { + console.log(`āš ļø Warning: Expected ${expectedCount} answers but got ${correctIndices.length}`); + } + + return correctIndices; + + } catch (error) { + console.error('āŒ Error calling OpenAI:', error); + return null; + } +} + +// Function to highlight matching answers +function highlightAnswer(element, isCorrect = true) { + const colorType = isCorrect ? 'correct' : 'unknown'; + console.log(` šŸŽØ Attempting to highlight as ${colorType}`); + console.log(` Original element:`, element); + + let targetElement = element; + + if (element.tagName === 'INPUT') { + console.log(` Element is INPUT, looking for label...`); + const label = document.querySelector(`label[for="${element.id}"]`) || element.closest('label'); + if (label) { + targetElement = label; + console.log(` Found label:`, label); + } else { + targetElement = element.parentElement; + console.log(` No label found, using parent:`, targetElement); + } + } + + // Remove any existing quiz helper classes + targetElement.classList.remove('quiz-helper-correct', 'quiz-helper-unknown'); + targetElement.removeAttribute('data-quiz-helper-highlight'); + + // Add the appropriate class + const className = isCorrect ? 'quiz-helper-correct' : 'quiz-helper-unknown'; + targetElement.classList.add(className); + targetElement.setAttribute('data-quiz-helper-highlight', 'true'); + targetElement.setAttribute('data-quiz-helper-type', colorType); + + console.log(` āœ“ Applied ${colorType} class to:`, targetElement.textContent.substring(0, 50)); + console.log(` Classes:`, targetElement.className); +} + +// Function to remove all highlights +function removeHighlights() { + const highlighted = document.querySelectorAll('[data-quiz-helper-highlight="true"]'); + highlighted.forEach(el => { + el.classList.remove('quiz-helper-correct', 'quiz-helper-unknown'); + el.removeAttribute('data-quiz-helper-highlight'); + el.removeAttribute('data-quiz-helper-type'); + }); + console.log(`Removed ${highlighted.length} highlights`); +} + +// Main scanning function +async function scanAndHighlight() { + if (isProcessing || !extensionEnabled) return; + + if (!openaiApiKey) { + console.error('āŒ Cannot scan: No OpenAI API key. Please add one in the extension popup.'); + return; + } + + isProcessing = true; + console.log('Starting scan...'); + + try { + const questions = findQuestions(); + console.log(`Found ${questions.length} potential questions`); + + if (questions.length === 0) { + console.log('No questions found. Page might still be loading or uses unsupported format.'); + isProcessing = false; + return; + } + + const questionsToProcess = questions.slice(0, 10); + + for (const question of questionsToProcess) { + if (processedQuestions.has(question.text)) { + continue; + } + + processedQuestions.add(question.text); + + const { answers: answerChoices, inputType } = findAnswerChoices(question.element); + + if (answerChoices.length === 0) { + console.log('No answer choices found for question'); + continue; + } + + const expectedCount = getExpectedAnswerCount(question.text, inputType); + + console.log(`\nšŸ“ Question: ${question.text.substring(0, 100)}...`); + console.log(` Input type: ${inputType || 'unknown'}`); + console.log(` Expected answers: ${expectedCount === -1 ? 'multiple (unknown count)' : expectedCount}`); + console.log(` Answer choices (${answerChoices.length}):`); + answerChoices.forEach((c, i) => { + console.log(` ${String.fromCharCode(65 + i)}. ${c.text.substring(0, 50)}`); + }); + + const correctIndices = await getAnswerFromOpenAI(question.text, answerChoices, inputType, expectedCount); + + if (correctIndices && correctIndices.length > 0) { + console.log(`āœ… ChatGPT identified ${correctIndices.length} correct answer(s):`); + + let highlightedCount = 0; + correctIndices.forEach(index => { + if (index >= 0 && index < answerChoices.length) { + const choice = answerChoices[index]; + console.log(` āœ“ ${String.fromCharCode(65 + index)}. ${choice.text}`); + console.log(` Element type: ${choice.element.tagName}, class: ${choice.element.className}`); + + try { + highlightAnswer(choice.element, true); + highlightedCount++; + console.log(` āœ“ Successfully highlighted`); + } catch (e) { + console.error(` āŒ Failed to highlight:`, e); + } + } else { + console.error(` āŒ Invalid index ${index} (max: ${answerChoices.length - 1})`); + } + }); + + if (highlightedCount === 0) { + console.error('āš ļø WARNING: ChatGPT provided answers but NONE were highlighted!'); + console.log('āš ļø Falling back to RED highlighting (user must guess)'); + answerChoices.forEach((choice, index) => { + console.log(` āš ļø ${String.fromCharCode(65 + index)}. ${choice.text} (HIGHLIGHT FAILED)`); + try { + highlightAnswer(choice.element, false); + } catch (e) { + console.error(` āŒ Failed to highlight in red:`, e); + } + }); + } else if (highlightedCount < correctIndices.length) { + console.warn(`āš ļø WARNING: Only ${highlightedCount} of ${correctIndices.length} answers were highlighted`); + } + + } else { + console.log('āŒ ChatGPT failed to provide an answer - highlighting all choices in RED'); + console.log('āš ļø USER MUST GUESS - ChatGPT could not determine the correct answer'); + // Highlight all answers in red to indicate user needs to make a guess + answerChoices.forEach((choice, index) => { + console.log(` āš ļø ${String.fromCharCode(65 + index)}. ${choice.text} (UNKNOWN)`); + try { + highlightAnswer(choice.element, false); + } catch (e) { + console.error(` āŒ Failed to highlight:`, e); + } + }); + } + + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + console.log('\nāœ… Scan complete'); + } catch (error) { + console.error('Error in scanAndHighlight:', error); + } finally { + isProcessing = false; + } +} + +// Watch for dynamic content changes +let mutationTimeout; +const observer = new MutationObserver((mutations) => { + if (!extensionEnabled || isProcessing) return; + + const hasNewContent = mutations.some(mutation => { + return mutation.addedNodes.length > 0 && + Array.from(mutation.addedNodes).some(node => + node.nodeType === 1 && node.textContent.length > 50 + ); + }); + + if (hasNewContent) { + clearTimeout(mutationTimeout); + mutationTimeout = setTimeout(() => { + console.log('Content changed, rescanning...'); + scanAndHighlight(); + }, 2000); + } +}); + +observer.observe(document.body, { + childList: true, + subtree: true +}); diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..a798b9987939c22d94cbb457ebfadebfc1379249 GIT binary patch literal 49538 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%0Awy~!xMg(esi(g6=9@WR)UyOy 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 literal 0 HcmV?d00001 diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..5647c45b1ac41523ae8c96c5ecd36a12af60f644 GIT binary patch literal 965 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a z5n0T@z%2~Ij105pNB{-dOFVsD*EaloA<8Jg{QeIk5CByX z4?qql3D8~x3#NNoTU(Wtm4V`S@7}Ghu0~V9jOOY7{{HCb=q+2eyn6LAH8ovUR%YkU zohW)h!Gr{!J$trtXhOnm+N zHPA7SA3p{%fLt^M=$^iC;Q~zAXj_3`njxgN@xNA!=qx; literal 0 HcmV?d00001 diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..b850bb0753b040abd541fdeec7684adbb5633688 GIT binary patch literal 7141 zcma)AOK4r!6+QP^e_Qb&traRT9h6QSsN$%pNvnc`6Mw*=H68c^#laE6lM+Zf3D^#H zu&F)hKngUUvF%ZzkYW;pBCzEqmR+mRK~+t&``)M5=yG-5 z+Iz3P_CDvHckV-f{>{rhS2-V?OQhgZg*$a z*L&@YtX4xSbr9o)&n#I0^k5`5#|j3Mpj08}@_ZHNW)r@PV76m9e78s7d64%q6W{BU<7! z^mT#GI~C+Hyu;%nq!zYBD%h}8!^8Zcv)fS z^X9~@oZCZa&5pEO+;;;Bs?aILNX;xnVV{$}Ht>O8jzkY{2v98%YBcjEm`!{`b(Xxw*OP*KfdyP^#m)ap{1(@e$oF6lxh>XYQ_;K9um@y?ZAo zCm%g}^tj1|5D2?>?_ORRC9-(M(hfv&hIOUO3^Gb8Y=X&_Ju*CU|Ni|Zos`_54G#}D zRfDD#%^OH&pm&)xwxUMOJbU(Rspbj{q!i+*YMgmeMnk!2B^nxHm-PYE|4vI=_F3Z~6+oq?cJ%4FwsjsiEU?$~Q ziHVh8F+TPx-vkJ%5$jI=+hYy^`!7My@@L=Ta5$6gW9i4-NgXw&; zNPZCIxG0_X$S=RzOmk)X7cXA))1952E#_K!WbO%O*_U75a#{5Y7cLYl>I(@IRRoku|m!S^l7RPDzxr3Yb?tOs0vawb4zs%8NC!Ps09M6r3F7)=#f7MYhqQyStxJ2 z5PvJjE2X5*VhJo9H+7=A$vGYo}XXXv13P`i~KBLRXm}Bksiq`L6DSH zg>9sF8EcD5AZM1HIdi65qmDJ$w{M>>Gd_N)%#l?;efo6HR0&&!*o~_KoVJ1j0yYDw zbar;&Ype$EB!(b!_TF76@wnmsRs@Fh5L;>0)K zd9yl!y;z&w2DaQ;@$vg5~( zFD~9ad>Ec$mlTs!H=Ma~;u>(-mLK)(5N;&eQ@2FKX!wK}*|6d1&p!K4M2vWm5~oaZ5J4RFZ(X7^y3D2b#a_O`=*1wS8x>WP)!grQXP=OSz&c z7XX*}Q$r##8K$(vCICSplIbrw%_U6%^8LtM=dp&E;M2&1$2%Ko5~?uiS23xDHEkz&PC(<2&9Hla|( zgkSt--GHO4#7XB6b)`-qHZ#RZ$x)729&tQSXN^EKgzR<=QC&Hr5_rGiya(^6f|~Ot iXceaP#fPUJ{=H%CFa9;u^?VF(fAQrle|us7(eD6VaX6^} literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..69a5184 --- /dev/null +++ b/manifest.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "Quiz Helper", + "version": "1.0", + "description": "Automatically searches for quiz answers and highlights correct ones", + "permissions": [ + "activeTab", + "storage", + "scripting" + ], + "host_permissions": [ + "" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + } + }, + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..41036a8 --- /dev/null +++ b/popup.html @@ -0,0 +1,172 @@ + + + + + + + +

Quiz Helper

+ +
+ āš ļø API Key Required + This extension requires an OpenAI API key to work. Add your key below to get started. +
+ +
+ + + + +
+ Get a free API key at platform.openai.com. + New accounts get $5 in free credits. Cost per question: ~$0.001 (1/10th of a cent). +
+
+ +
+ + Off +
+ +
+ +
+ When enabled, ChatGPT will analyze quiz questions and highlight answers: +
    +
  • Green = ChatGPT identified correct answer(s)
  • +
  • Red = ChatGPT failed, you need to guess
  • +
+ Check the browser console (F12) for detailed logs. +
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..80912e1 --- /dev/null +++ b/popup.js @@ -0,0 +1,102 @@ +// Load saved state +chrome.storage.sync.get(['enabled', 'openaiApiKey'], function(result) { + const enabled = result.enabled || false; + document.getElementById('toggleSwitch').checked = enabled; + document.getElementById('toggleLabel').textContent = enabled ? 'On' : 'Off'; + + // Load API key if saved + if (result.openaiApiKey) { + document.getElementById('apiKey').value = result.openaiApiKey; + showApiStatus('API key loaded', 'success'); + } +}); + +// Handle toggle change +document.getElementById('toggleSwitch').addEventListener('change', function(e) { + const enabled = e.target.checked; + + // Save state + chrome.storage.sync.set({ enabled: enabled }); + + // Update label + document.getElementById('toggleLabel').textContent = enabled ? 'On' : 'Off'; + + // Update status + document.getElementById('status').textContent = enabled ? + 'Extension is now active' : 'Extension is now disabled'; + + // Send message to content script (with error handling) + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + if (tabs[0]) { + // Check if we can send messages to this tab + const url = tabs[0].url; + + // Can't inject content scripts into chrome:// pages or other special URLs + 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'; + setTimeout(() => { + document.getElementById('status').textContent = ''; + document.getElementById('status').style.color = ''; + }, 3000); + return; + } + + // Try to send message + chrome.tabs.sendMessage(tabs[0].id, { + action: 'toggleExtension', + enabled: enabled + }).catch(error => { + console.log('Could not send message to content script:', error); + // This is okay - the content script will pick up the state from storage when it loads + }); + } + }); + + // Clear status after 2 seconds + setTimeout(() => { + document.getElementById('status').textContent = ''; + document.getElementById('status').style.color = ''; + }, 2000); +}); + +// Handle API key save +document.getElementById('saveApiKey').addEventListener('click', function() { + const apiKey = document.getElementById('apiKey').value.trim(); + + if (!apiKey) { + showApiStatus('Please enter an API key', 'error'); + return; + } + + if (!apiKey.startsWith('sk-')) { + showApiStatus('Invalid API key format', 'error'); + return; + } + + // Save API key + chrome.storage.sync.set({ openaiApiKey: apiKey }, function() { + showApiStatus('API key saved successfully!', 'success'); + + // Notify content script to reload + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + if (tabs[0]) { + chrome.tabs.sendMessage(tabs[0].id, { + action: 'apiKeyUpdated', + apiKey: apiKey + }).catch(() => {}); + } + }); + }); +}); + +function showApiStatus(message, type) { + const statusEl = document.getElementById('apiStatus'); + statusEl.textContent = message; + statusEl.className = `api-status ${type}`; + statusEl.style.display = 'block'; + + setTimeout(() => { + statusEl.style.display = 'none'; + }, 3000); +}