diff --git a/README.md b/README.md index 336c42a..f2f79ac 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,16 @@ ## Features -- 🤖 **Dual AI Model System** for different question types: - - **Simple Model**: For general questions, trivia, and non-technical content - - **Advanced Model**: For programming, mathematics, and technical questions -- 🔥 **8 OpenRouter AI Models** including: - - Claude Sonnet 4.5, GPT-5 Pro, GPT-5 Codex - - Qwen3 Coder Plus, DeepSeek, Grok 4 Fast, GLM 4.6 -- ⚡ **Legacy Provider Support**: - - **Groq** (Fast & Free with Llama 3.3) - - **Google Gemini** (Free tier available) +- 🤖 **Dual AI Provider Support**: + - **OpenAI GPT-4o Mini** - Fast and accurate for all question types + - **Google Gemini 2.5 Flash** - Free tier available, great performance - 🎯 Automatic question and option extraction from Netacad quizzes +- ✅ **Multiple-answer support** - Works with checkbox questions (choose two, choose three, etc.) - ✨ Visual highlighting of suggested correct answers - 🔐 Secure API key storage -- 🎨 Clean and intuitive dual-button UI -- 💡 Smart model selection for optimal accuracy +- 🎨 Clean and intuitive dual-button UI (GPT and Gemini buttons) +- ⚡ Fast response times with both models +- 💡 Choose the best model for your needs ## Installation @@ -38,65 +34,62 @@ ## Setup -### 1. Get API Keys (FREE options available!) +### 1. Get API Keys -**Option A: OpenRouter** (⭐ Recommended - Access to 8+ Models) -- Sign up at [OpenRouter](https://openrouter.ai/) -- Get your free API key from [OpenRouter Keys](https://openrouter.ai/keys) -- Access Claude Sonnet 4.5, GPT-5 Pro, and more models +**Option A: OpenAI** (GPT-4o Mini) +- Sign up at [OpenAI Platform](https://platform.openai.com/) +- Get your API key from [OpenAI API Keys](https://platform.openai.com/api-keys) +- Very affordable pay-as-you-go pricing -**Option B: Legacy Providers** (Still Supported) -- **Groq** (Fast & Free): [Groq Console](https://console.groq.com/keys) -- **Google Gemini** (Free tier): [Google AI Studio](https://makersuite.google.com/app/apikey) +**Option B: Google Gemini** (⭐ FREE - Recommended to start) +- Sign up at [Google AI Studio](https://makersuite.google.com/app/apikey) +- Get your free API key +- Generous free tier with excellent performance + +**You can use one or both API keys!** ### 2. Configure the Extension 1. Click the extension icon in your browser toolbar -2. **Select models for each use case:** - - **Simple Questions Model**: Choose for general/trivia questions - - Options: Groq, Gemini, DeepSeek, Qwen3 Coder Flash - - **Advanced (Coding/Math) Model**: Choose for technical questions - - Options: Claude Sonnet 4.5, GPT-5 Pro, GPT-5 Codex, Qwen3 Coder Plus, Grok 4 Fast, GLM 4.6 -3. **Enter API key(s):** - - OpenRouter API Key (if using OpenRouter models) - - Legacy Provider API Key (if using Groq/Gemini) -4. Click "Save Settings" - -### Why Use OpenRouter? 🔥 - -**OpenRouter** gives you access to multiple state-of-the-art models: -- ✅ Access to Claude, GPT-5, Qwen3, and more -- 🧠 Choose specialized coding models for technical questions -- 💡 Use simpler models for basic questions to save costs -- 🎯 Higher accuracy for programming and math problems -- 🆓 Free tier available with generous limits +2. Enter your API key(s): + - **OpenAI API Key** (for GPT-4o Mini) + - **Google Gemini API Key** (for Gemini 2.5 Flash) +3. Click "Save Settings" + +**Note:** You need at least one API key to use the extension. Both providers work great! ## Usage 1. Navigate to a Netacad quiz page 2. You'll see **two buttons** on the page once the quiz iframe loads: - - **🤖 Get AI Answer** (Green) - For general questions - - **🔥 Advanced AI (Code/Math)** (Red) - For technical questions -3. Choose the appropriate button based on the question type: - - Use the **green button** for general knowledge, trivia, theory questions - - Use the **red button** for programming, mathematics, statistics, or complex technical questions -4. The extension will highlight the AI-suggested correct answer + - **🤖 Get Answer from GPT** (Blue) - Uses OpenAI GPT-4o Mini + - **✨ Get Answer from Gemini** (Purple) - Uses Google Gemini 2.5 Flash +3. Click the button for the AI provider you want to use +4. The extension will highlight the AI-suggested correct answer(s) + - For single-answer questions (radio buttons): One option is highlighted + - For multiple-answer questions (checkboxes): Multiple options are highlighted 5. Review the suggestion and make your selection -### Which Button to Use? 🤔 +### Multiple-Answer Questions + +The extension automatically detects when a question requires multiple answers (checkboxes instead of radio buttons) and will: +- Analyze the question text to determine how many answers are needed (e.g., "choose two", "select three") +- Request the correct number of answers from the AI +- Highlight all suggested correct answers in green + +### Which Provider to Use? 🤔 + +Both models work great for all types of questions! Here are some considerations: -- **Green Button (Simple Model)**: - - General networking concepts - - Theory and definitions - - Basic troubleshooting scenarios - - Non-technical questions +- **GPT-4o Mini**: + - Very accurate and reliable + - Pay-as-you-go pricing (affordable) + - Great for all question types -- **Red Button (Advanced Model)**: - - Code analysis and debugging - - Mathematical calculations - - Complex algorithms - - Statistical problems - - Technical programming questions +- **Gemini 2.5 Flash**: + - Free tier available (generous limits) + - Excellent performance + - Perfect for starting out or high-volume use ## Screenshots @@ -110,11 +103,11 @@ Below are some screenshots demonstrating the extension in action: --- -### 2. "Get AI Answer" Button on Netacad Quiz +### 2. AI Provider Buttons on Netacad Quiz -![Get AI Answer Button](screenshots/quiz-button.png) +![AI Provider Buttons](screenshots/quiz-button.png) -*The "🤖 Get AI Answer" button appears on Netacad quiz pages.* +*Two buttons appear on Netacad quiz pages: "🤖 Get Answer from GPT" and "✨ Get Answer from Gemini".* --- @@ -155,10 +148,11 @@ Below are some screenshots demonstrating the extension in action: 1. **Content Script** (`content.js`) runs on Netacad pages and detects quiz elements using Shadow DOM 2. Netacad uses Shadow DOM to encapsulate quiz content, so the script accesses the `mcq-view` element's shadow root 3. The script extracts question text and options from inside the shadow DOM -4. User clicks either the **green button** (simple model) or **red button** (advanced model) -5. **Background Script** (`background.js`) sends the question to the selected AI model via OpenRouter or legacy provider -6. The AI responds with only the correct option letter (A, B, C, or D) -7. The extension highlights the corresponding option with inline styles (since CSS doesn't penetrate Shadow DOM) +4. The script detects if the question requires multiple answers (checkbox vs radio button) +5. User clicks either the **blue button** (GPT) or **purple button** (Gemini) +6. **Background Script** (`background.js`) sends the question to the selected AI provider (OpenAI or Google) with info about whether multiple answers are needed +7. The AI responds with the correct option letter(s) (e.g., "A" for single answer, or "A,B" for multiple answers) +8. The extension highlights the corresponding option(s) with inline styles (since CSS doesn't penetrate Shadow DOM) ## Files @@ -172,41 +166,40 @@ Below are some screenshots demonstrating the extension in action: ## Privacy & Security - API keys are stored locally in Chrome's sync storage -- No data is sent to any server except the AI provider you choose +- No data is sent to any server except the AI provider you choose (OpenAI or Google) - The extension only runs on `netacad.com` domains +- Your quiz data is only sent to the AI provider when you click a button ## Limitations - AI suggestions may not always be correct - always verify answers -- Requires a valid API key (OpenRouter, Groq, or Gemini) -- **Free options available** (OpenRouter, Groq, and Gemini offer free tiers) -- API usage may incur costs for premium models -- Only works on multiple-choice questions -- For best results, use the appropriate button (simple vs advanced) based on question type +- Requires at least one API key (OpenAI or Gemini) +- **Affordable/FREE options**: + - **Gemini**: Free tier with generous limits (⭐ Recommended to start) + - **OpenAI**: Very affordable pay-as-you-go pricing +- Works with multiple-choice questions (single and multiple answers) +- Both models work well for all question types +- The extension tries to auto-detect the number of required answers, but may default to 2 if unclear ## Troubleshooting **Extension buttons not appearing** - Make sure you're on a Netacad quiz page with multiple choice questions - Refresh the page after installing the extension -- Check that both green and red buttons are visible +- Check that both blue (GPT) and purple (Gemini) buttons are visible **API errors** -- Verify your API key is correct (OpenRouter or Legacy provider) +- Verify your API key is correct in the extension popup +- For GPT button: Make sure you entered a valid OpenAI API key +- For Gemini button: Make sure you entered a valid Google Gemini API key - Check that you have API credits/quota remaining -- Ensure you selected models that match your API key -- For OpenRouter models, make sure you entered the OpenRouter API key +- Gemini has a generous free tier - start with that if you're unsure **No answer highlighted** - Check the browser console for errors (F12) -- Verify the question format is supported -- Try switching between simple and advanced models -- Ensure the correct model is configured for the button you clicked - -**Wrong model being used** -- Check which button you clicked (green = simple, red = advanced) -- Verify model selections in the extension popup -- Make sure the appropriate API key is configured +- Verify the question format is supported (must be multiple choice) +- Try the other provider button if one doesn't work +- Make sure you saved your API key(s) in the settings ## Disclaimer diff --git a/background.js b/background.js index 4149392..ea9ae1d 100644 --- a/background.js +++ b/background.js @@ -1,96 +1,65 @@ // Background service worker for handling AI API requests const SYSTEM_PROMPT = `SYSTEM: -You are an AI assistant that answers multiple-choice programming or math questions. -You must always return only the correct option letter (A, B, C, or D) — nothing else. - -Rules: -1. Think carefully before answering; simulate the code or calculate the math internally. -2. Do not explain or include reasoning. -3. Output only one character: A, B, C, or D. -4. Never include punctuation, words, or extra spaces — only the letter. - +You are an AI assistant that answers multiple-choice questions with extreme precision. + +CRITICAL RULES - YOU MUST FOLLOW THESE EXACTLY: +1. Think carefully before answering; simulate code or calculate math internally +2. ONLY output the letter(s) specified in the prompt (e.g., A, B, C, D, E, F) +3. If asked for ONE letter, provide EXACTLY ONE letter +4. If asked for N letters, provide EXACTLY N letters separated by commas +5. NEVER include explanations, reasoning, punctuation, or extra text +6. NEVER output more or fewer letters than requested +7. Follow the EXACT format specified in the user prompt + +Your response must contain ONLY the requested letter(s) and nothing else. End. ` const temperature = 0; const top_p = 1.0; -const max_tokens = 500; +const max_tokens = 2000; // Increased for Gemini compatibility const presence_penalty = 0; const frequency_penalty = 0; -// OpenRouter Model Mapping -const OPENROUTER_MODELS = { - "DeepSeek": "deepseek/deepseek-v3.2-exp", - "GPT-5 Pro": "openai/gpt-5-pro", - "Claude Sonnet 4.5": "anthropic/claude-sonnet-4.5", - "Qwen3 Coder Plus": "qwen/qwen3-coder-plus", - "GLM": "z-ai/glm-4.6", - "Grok 4 Fast": "x-ai/grok-4-fast", - "GPT-5 Codex": "openai/gpt-5-codex", - "Qwen3 Coder Flash": "qwen/qwen3-coder-flash" -}; - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'getAnswer') { - handleGetAnswer(request.question, request.options, request.modelType) + handleGetAnswer( + request.question, + request.options, + request.modelType, + request.isMultipleAnswer, + request.requiredAnswers + ) .then(result => sendResponse(result)) .catch(error => sendResponse({ success: false, error: error.message })); return true; // Keep message channel open for async response } }); -async function handleGetAnswer(question, options, modelType = 'simple') { +async function handleGetAnswer(question, options, modelType, isMultipleAnswer = false, requiredAnswers = 1) { try { // Get settings from storage const settings = await chrome.storage.sync.get([ - 'simpleModel', - 'codingModel', - 'openRouterApiKey', - 'aiProvider', - 'apiKey' + 'geminiApiKey', + 'openAiApiKey' ]); - // Determine which model to use based on modelType - let provider, modelId, apiKey; - - if (modelType === 'simple') { - modelId = settings.simpleModel; - } else if (modelType === 'coding') { - modelId = settings.codingModel; - } + let answerIndex; - // Check if it's an OpenRouter model - if (modelId && OPENROUTER_MODELS[modelId]) { - provider = 'openrouter'; - apiKey = settings.openRouterApiKey; + if (modelType === 'gpt') { + const apiKey = settings.openAiApiKey; if (!apiKey) { - throw new Error('OpenRouter API key not configured. Please set it in the extension popup.'); + throw new Error('OpenAI API key not configured. Please set it in the extension popup.'); } - } else if (modelId === 'groq' || modelId === 'gemini') { - // Legacy provider support - provider = modelId; - apiKey = settings.apiKey; + answerIndex = await getAnswerFromOpenAI(question, options, apiKey, isMultipleAnswer, requiredAnswers); + } else if (modelType === 'gemini') { + const apiKey = settings.geminiApiKey; if (!apiKey) { - throw new Error('API key not configured. Please set it in the extension popup.'); + throw new Error('Gemini API key not configured. Please set it in the extension popup.'); } + answerIndex = await getAnswerFromGemini(question, options, apiKey, isMultipleAnswer, requiredAnswers); } else { - // Fallback to old provider system if no model selected - provider = settings.aiProvider || 'groq'; - apiKey = settings.apiKey; - if (!apiKey) { - throw new Error('API key not configured. Please set it in the extension popup.'); - } - } - - let answerIndex; - if (provider === 'gemini') { - answerIndex = await getAnswerFromGemini(question, options, apiKey); - } else if (provider === 'groq') { - answerIndex = await getAnswerFromGroq(question, options, apiKey); - } else if (provider === 'openrouter') { - answerIndex = await getAnswerFromOpenRouter(question, options, modelId, apiKey); - } else { - throw new Error('Unknown AI provider: ' + provider); + throw new Error('Unknown model type: ' + modelType); } return { success: true, answerIndex: answerIndex }; @@ -100,22 +69,49 @@ async function handleGetAnswer(question, options, modelType = 'simple') { } } -async function getAnswerFromGemini(question, options, apiKey) { +async function getAnswerFromGemini(question, options, apiKey, isMultipleAnswer = false, requiredAnswers = 1) { const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; - // Format options with letters + // Format options with letters (dynamically handle any number of options) const formattedOptions = options.map((opt, idx) => `${String.fromCharCode(65 + idx)}. ${opt}` ).join('\n'); - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + // Get the available option letters dynamically + const availableLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(', '); + + let prompt; + if (isMultipleAnswer) { + prompt = `IMPORTANT: This is a multiple-answer question. You MUST select EXACTLY ${requiredAnswers} correct answer(s). Not more, not less. + +CRITICAL RULES: +1. You MUST provide EXACTLY ${requiredAnswers} letters +2. Separate letters with commas (e.g., "A,B" or "A,C,D") +3. Only use available letters: ${availableLetters} +4. No explanation, no extra text, no reasoning +5. ONLY output the ${requiredAnswers} correct letter(s) + +Question: ${question} + +Options: +${formattedOptions} + +Answer with EXACTLY ${requiredAnswers} letter(s) separated by commas:`; + } else { + prompt = `Answer this question with ONLY ONE letter from the available options: ${availableLetters} + +CRITICAL RULES: +1. Output ONLY ONE letter +2. No explanation, no extra text +3. Only use available letters: ${availableLetters} Question: ${question} Options: ${formattedOptions} -Answer with only the letter:`; +Answer with only ONE letter:`; + } const requestBody = { contents: [{ @@ -125,10 +121,8 @@ Answer with only the letter:`; }], generationConfig: { temperature: temperature, - top_p: top_p, - max_tokens: max_tokens, - presence_penalty: presence_penalty, - frequency_penalty: frequency_penalty, + topP: top_p, + maxOutputTokens: max_tokens }, safetySettings: [ { @@ -219,38 +213,98 @@ Answer with only the letter:`; console.log('Gemini answer text:', answerText); - // Extract letter from response (handles "A", "A.", "Answer: A", etc.) - const letterMatch = answerText.match(/[ABCD]/); - if (!letterMatch) { - throw new Error('Invalid answer format from Gemini: ' + answerText); - } + // Get valid letters based on number of options + const maxOptionIndex = options.length - 1; + const validLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(''); + const validLetterPattern = new RegExp(`[${validLetters}]`, 'g'); + + if (isMultipleAnswer) { + // Extract multiple letters from response (handles "A,B", "A, B", "A,C,D", etc.) + const letterMatches = answerText.match(validLetterPattern); + if (!letterMatches || letterMatches.length === 0) { + throw new Error(`Invalid answer format from Gemini. Expected letters from ${validLetters}, got: ${answerText}`); + } + + // Convert letters to indices and remove duplicates + const answerIndices = [...new Set(letterMatches)].map(letter => letter.charCodeAt(0) - 65); + + // Validate we have the correct number of answers + if (answerIndices.length !== requiredAnswers) { + console.warn(`⚠️ Gemini returned ${answerIndices.length} answers but ${requiredAnswers} were required. Trying to adjust...`); + + // If we have too many, take the first N + if (answerIndices.length > requiredAnswers) { + answerIndices.splice(requiredAnswers); + console.log(`✂️ Trimmed to first ${requiredAnswers} answers:`, answerIndices); + } else { + // If we have too few, warn but continue + console.warn(`⚠️ Using ${answerIndices.length} answers instead of ${requiredAnswers}`); + } + } - const answerLetter = letterMatch[0]; - const answerIndex = answerLetter.charCodeAt(0) - 65; // Convert A->0, B->1, etc. + console.log('Gemini answers:', letterMatches.join(','), 'Indices:', answerIndices); + return answerIndices; + } else { + // Extract single letter from response (handles "A", "A.", "Answer: A", etc.) + const letterMatch = answerText.match(validLetterPattern); + if (!letterMatch) { + throw new Error(`Invalid answer format from Gemini. Expected one letter from ${validLetters}, got: ${answerText}`); + } + + const answerLetter = letterMatch[0]; + const answerIndex = answerLetter.charCodeAt(0) - 65; // Convert A->0, B->1, etc. - console.log('Gemini answer:', answerLetter, 'Index:', answerIndex); - return answerIndex; + console.log('Gemini answer:', answerLetter, 'Index:', answerIndex); + return answerIndex; + } } -async function getAnswerFromOpenRouter(question, options, modelName, apiKey) { - const url = 'https://openrouter.ai/api/v1/chat/completions'; +async function getAnswerFromOpenAI(question, options, apiKey, isMultipleAnswer = false, requiredAnswers = 1) { + const url = 'https://api.openai.com/v1/chat/completions'; - // Format options with letters + // Format options with letters (dynamically handle any number of options) const formattedOptions = options.map((opt, idx) => `${String.fromCharCode(65 + idx)}. ${opt}` ).join('\n'); - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + // Get the available option letters dynamically + const availableLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(', '); + + let prompt; + if (isMultipleAnswer) { + prompt = `IMPORTANT: This is a multiple-answer question. You MUST select EXACTLY ${requiredAnswers} correct answer(s). Not more, not less. + +CRITICAL RULES: +1. You MUST provide EXACTLY ${requiredAnswers} letters +2. Separate letters with commas (e.g., "A,B" or "A,C,D") +3. Only use available letters: ${availableLetters} +4. No explanation, no extra text, no reasoning +5. ONLY output the ${requiredAnswers} correct letter(s) + +Question: ${question} + +Options: +${formattedOptions} + +Answer with EXACTLY ${requiredAnswers} letter(s) separated by commas:`; + } else { + prompt = `Answer this question with ONLY ONE letter from the available options: ${availableLetters} + +CRITICAL RULES: +1. Output ONLY ONE letter +2. No explanation, no extra text +3. Only use available letters: ${availableLetters} Question: ${question} Options: ${formattedOptions} -Answer with only the letter:`; +Answer with only ONE letter:`; + } const requestBody = { - model: OPENROUTER_MODELS[modelName], + model: 'gpt-4o-mini', messages: [ { role: 'system', @@ -279,31 +333,24 @@ Answer with only the letter:`; const data = await response.json(); - console.log('OpenRouter full response:', JSON.stringify(data, null, 2)); + console.log('OpenAI full response:', JSON.stringify(data, null, 2)); console.log('Response status:', response.status, response.statusText); if (!response.ok) { - console.error('OpenRouter API error response:', data); - throw new Error(`OpenRouter API error: ${data.error?.message || response.statusText}`); + console.error('OpenAI API error response:', data); + throw new Error(`OpenAI API error: ${data.error?.message || response.statusText}`); } - // Check response structure - console.log('data.choices:', data.choices); - console.log('data.choices[0]:', data.choices?.[0]); - console.log('data.choices[0].message:', data.choices?.[0]?.message); - console.log('data.choices[0].message.content:', data.choices?.[0]?.message?.content); - const rawContent = data.choices?.[0]?.message?.content; const answerText = rawContent?.trim().toUpperCase(); if (!answerText) { - console.error('Empty or undefined answer text from OpenRouter'); + console.error('Empty or undefined answer text from OpenAI'); console.error('Raw content:', rawContent); console.error('Finish reason:', data.choices?.[0]?.finish_reason); console.error('Full response data:', JSON.stringify(data, null, 2)); - // More detailed error message - let errorDetails = 'No answer received from OpenRouter. '; + let errorDetails = 'No answer received from OpenAI. '; if (!data.choices) { errorDetails += 'Response has no choices array. '; } else if (data.choices.length === 0) { @@ -316,98 +363,59 @@ Answer with only the letter:`; errorDetails += 'Message content is empty/whitespace. '; } - // Check if token limit was hit if (data.choices[0]?.finish_reason === 'length') { - errorDetails += 'Response was cut off due to token limit (increase max_tokens). '; + errorDetails += 'Response was cut off due to token limit. '; } errorDetails += 'Full response: ' + JSON.stringify(data); - throw new Error(errorDetails); } - console.log('OpenRouter raw answer:', answerText); - - // Extract letter from response - const letterMatch = answerText.match(/[ABCD]/); - if (!letterMatch) { - throw new Error('Invalid answer format from OpenRouter: ' + answerText); - } - - const answerLetter = letterMatch[0]; - const answerIndex = answerLetter.charCodeAt(0) - 65; // Convert A->0, B->1, etc. - - console.log('OpenRouter answer:', answerLetter, 'Index:', answerIndex, 'Model:', modelName); - return answerIndex; -} - -async function getAnswerFromGroq(question, options, apiKey) { - const url = 'https://api.groq.com/openai/v1/chat/completions'; - - // Format options with letters - const formattedOptions = options.map((opt, idx) => - `${String.fromCharCode(65 + idx)}. ${opt}` - ).join('\n'); + console.log('OpenAI raw answer:', answerText); - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + // Get valid letters based on number of options + const maxOptionIndex = options.length - 1; + const validLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(''); + const validLetterPattern = new RegExp(`[${validLetters}]`, 'g'); -Question: ${question} + if (isMultipleAnswer) { + // Extract multiple letters from response (handles "A,B", "A, B", "A,C,D", etc.) + const letterMatches = answerText.match(validLetterPattern); + if (!letterMatches || letterMatches.length === 0) { + throw new Error(`Invalid answer format from OpenAI. Expected letters from ${validLetters}, got: ${answerText}`); + } -Options: -${formattedOptions} + // Convert letters to indices and remove duplicates + const answerIndices = [...new Set(letterMatches)].map(letter => letter.charCodeAt(0) - 65); -Answer with only the letter:`; + // Validate we have the correct number of answers + if (answerIndices.length !== requiredAnswers) { + console.warn(`⚠️ OpenAI returned ${answerIndices.length} answers but ${requiredAnswers} were required. Trying to adjust...`); - const requestBody = { - model: 'llama-3.3-70b-versatile', // Fast and accurate model - messages: [ - { - role: 'system', - content: SYSTEM_PROMPT - }, - { - role: 'user', - content: prompt + // If we have too many, take the first N + if (answerIndices.length > requiredAnswers) { + answerIndices.splice(requiredAnswers); + console.log(`✂️ Trimmed to first ${requiredAnswers} answers:`, answerIndices); + } else { + // If we have too few, warn but continue + console.warn(`⚠️ Using ${answerIndices.length} answers instead of ${requiredAnswers}`); } - ], - temperature: temperature, - top_p: top_p, - max_tokens: max_tokens, - presence_penalty: presence_penalty, - frequency_penalty: frequency_penalty, - }; - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }, - body: JSON.stringify(requestBody) - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(`Groq API error: ${data.error?.message || response.statusText}`); - } + } - const answerText = data.choices[0]?.message?.content?.trim().toUpperCase(); + console.log('OpenAI answers:', letterMatches.join(','), 'Indices:', answerIndices, 'Model: gpt-4o-mini'); + return answerIndices; + } else { + // Extract single letter from response + const letterMatch = answerText.match(validLetterPattern); + if (!letterMatch) { + throw new Error(`Invalid answer format from OpenAI. Expected one letter from ${validLetters}, got: ${answerText}`); + } - if (!answerText) { - throw new Error('No answer received from Groq'); - } + const answerLetter = letterMatch[0]; + const answerIndex = answerLetter.charCodeAt(0) - 65; - // Extract letter from response - const letterMatch = answerText.match(/[ABCD]/); - if (!letterMatch) { - throw new Error('Invalid answer format from Groq: ' + answerText); + console.log('OpenAI answer:', answerLetter, 'Index:', answerIndex, 'Model: gpt-4o-mini'); + return answerIndex; } - - const answerLetter = letterMatch[0]; - const answerIndex = answerLetter.charCodeAt(0) - 65; - - console.log('Groq answer:', answerLetter, 'Index:', answerIndex); - return answerIndex; } diff --git a/content.css b/content.css index ca4059e..6a57743 100644 --- a/content.css +++ b/content.css @@ -16,28 +16,28 @@ text-align: center; } -/* Simple Button (Green) */ -.ai-helper-button-simple { +/* GPT Button (Blue) */ +.ai-helper-button-gpt { top: 80px; - background: linear-gradient(135deg, #6abf4b 0%, #5aa93d 100%); + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); } -.ai-helper-button-simple:hover { +.ai-helper-button-gpt:hover { transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(106, 191, 75, 0.6); - background: linear-gradient(135deg, #7ed15f 0%, #6abf4b 100%); + box-shadow: 0 6px 16px rgba(59, 130, 246, 0.6); + background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); } -/* Advanced Button (Red) */ -.ai-helper-button-advanced { +/* Gemini Button (Purple) */ +.ai-helper-button-gemini { top: 140px; - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); } -.ai-helper-button-advanced:hover { +.ai-helper-button-gemini:hover { transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(239, 68, 68, 0.6); - background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); + box-shadow: 0 6px 16px rgba(139, 92, 246, 0.6); + background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%); } .ai-helper-button:active { @@ -110,12 +110,12 @@ font-size: 13px !important; min-width: 160px; } - - .ai-helper-button-simple { + + .ai-helper-button-gpt { top: 70px; } - - .ai-helper-button-advanced { + + .ai-helper-button-gemini { top: 120px; } } diff --git a/content.js b/content.js index ccab86c..fe71fde 100644 --- a/content.js +++ b/content.js @@ -249,11 +249,68 @@ async function extractQuestionData() { }; }); + // Detect if this is a multiple-answer question (checkbox) + // Check for checkbox input type + const firstOption = mcqView.querySelector('.mcq__item'); + let isMultipleAnswer = false; + let requiredAnswers = 1; + + if (firstOption) { + // Look for input type (checkbox vs radio) + const inputElement = firstOption.querySelector('input[type="checkbox"]'); + if (inputElement) { + isMultipleAnswer = true; + console.log('✅ Detected CHECKBOX question (multiple answers possible)'); + } else { + console.log('✅ Detected RADIO question (single answer)'); + } + } + + // Try to detect number of required answers from question text + if (isMultipleAnswer) { + const questionLower = questionText.toLowerCase(); + + // Match patterns like "choose two", "select three", "choose 2", etc. + const patterns = [ + /choose\s+(two|three|four|five|2|3|4|5)/i, + /select\s+(two|three|four|five|2|3|4|5)/i, + /pick\s+(two|three|four|five|2|3|4|5)/i, + /identify\s+(two|three|four|five|2|3|4|5)/i + ]; + + const numberMap = { + 'two': 2, '2': 2, + 'three': 3, '3': 3, + 'four': 4, '4': 4, + 'five': 5, '5': 5 + }; + + for (const pattern of patterns) { + const match = questionLower.match(pattern); + if (match && match[1]) { + const num = numberMap[match[1].toLowerCase()]; + if (num) { + requiredAnswers = num; + console.log(`✅ Detected ${requiredAnswers} required answers from question text`); + break; + } + } + } + + // If still couldn't detect, default to 2 for checkbox questions + if (requiredAnswers === 1) { + requiredAnswers = 2; + console.log('⚠️ Could not detect number of answers, defaulting to 2'); + } + } + console.log('=== Extraction Complete ===\n'); return { question: questionText, - options: options + options: options, + isMultipleAnswer: isMultipleAnswer, + requiredAnswers: requiredAnswers }; } catch (error) { @@ -262,10 +319,14 @@ async function extractQuestionData() { } } -// Function to highlight the correct answer -function highlightCorrectAnswer(correctOptionIndex) { +// Function to highlight the correct answer(s) +// correctOptionIndices can be a single index or an array of indices +function highlightCorrectAnswer(correctOptionIndices) { console.log('=== Starting Highlight ==='); - console.log('Highlighting option index:', correctOptionIndex); + + // Normalize to array + const indices = Array.isArray(correctOptionIndices) ? correctOptionIndices : [correctOptionIndices]; + console.log('Highlighting option indices:', indices); try { // Find ALL mcq-view elements and pick the visible/active one (same as extraction) @@ -357,28 +418,30 @@ function highlightCorrectAnswer(correctOptionIndex) { }); }); - // Highlight the correct answer - if (correctOptionIndex >= 0 && correctOptionIndex < optionElements.length) { - const correctElement = optionElements[correctOptionIndex]; - correctElement.classList.add('ai-correct-answer'); - - // Apply inline styles - green background with white text - correctElement.style.backgroundColor = '#22c55e'; - correctElement.style.border = '3px solid #16a34a'; - correctElement.style.borderRadius = '8px'; - correctElement.style.boxShadow = '0 0 0 4px rgba(34, 197, 94, 0.2)'; - correctElement.style.color = 'white'; - - // Make sure all text inside is white - const textElements = correctElement.querySelectorAll('*'); - textElements.forEach(el => { - el.style.color = 'white'; - }); - - console.log(`✅ Highlighted option ${correctOptionIndex} as correct`); - } else { - console.log(`❌ Invalid option index: ${correctOptionIndex} (total options: ${optionElements.length})`); - } + // Highlight all correct answers + indices.forEach((correctOptionIndex) => { + if (correctOptionIndex >= 0 && correctOptionIndex < optionElements.length) { + const correctElement = optionElements[correctOptionIndex]; + correctElement.classList.add('ai-correct-answer'); + + // Apply inline styles - green background with white text + correctElement.style.backgroundColor = '#22c55e'; + correctElement.style.border = '3px solid #16a34a'; + correctElement.style.borderRadius = '8px'; + correctElement.style.boxShadow = '0 0 0 4px rgba(34, 197, 94, 0.2)'; + correctElement.style.color = 'white'; + + // Make sure all text inside is white + const textElements = correctElement.querySelectorAll('*'); + textElements.forEach(el => { + el.style.color = 'white'; + }); + + console.log(`✅ Highlighted option ${correctOptionIndex} as correct`); + } else { + console.log(`❌ Invalid option index: ${correctOptionIndex} (total options: ${optionElements.length})`); + } + }); console.log('=== Highlight Complete ===\n'); @@ -387,44 +450,44 @@ function highlightCorrectAnswer(correctOptionIndex) { } } -// Function to create the helper buttons (simple and advanced) +// Function to create the helper buttons (GPT and Gemini) function createHelperButton(targetDocument = document) { // Check if buttons already exist - if (document.getElementById('netacad-ai-helper-btn-simple')) { + if (document.getElementById('netacad-ai-helper-btn-gpt')) { console.log('Buttons already exist in main document'); return; } - if (targetDocument !== document && targetDocument.getElementById('netacad-ai-helper-btn-simple')) { + if (targetDocument !== document && targetDocument.getElementById('netacad-ai-helper-btn-gpt')) { console.log('Buttons already exist in iframe'); return; } - // Create Simple Button (Green) - const simpleButton = targetDocument.createElement('button'); - simpleButton.id = 'netacad-ai-helper-btn-simple'; - simpleButton.innerHTML = '🤖 Get AI Answer'; - simpleButton.className = 'ai-helper-button ai-helper-button-simple'; - - // Create Advanced Button (Red) - const advancedButton = targetDocument.createElement('button'); - advancedButton.id = 'netacad-ai-helper-btn-advanced'; - advancedButton.innerHTML = '🔥 Advanced AI (Code/Math)'; - advancedButton.className = 'ai-helper-button ai-helper-button-advanced'; - - // Simple button click handler - simpleButton.addEventListener('click', async () => { - await handleButtonClick(simpleButton, 'simple', '🤖 Get AI Answer'); + // Create GPT Button (Blue) + const gptButton = targetDocument.createElement('button'); + gptButton.id = 'netacad-ai-helper-btn-gpt'; + gptButton.innerHTML = '🤖 Get Answer from GPT'; + gptButton.className = 'ai-helper-button ai-helper-button-gpt'; + + // Create Gemini Button (Purple) + const geminiButton = targetDocument.createElement('button'); + geminiButton.id = 'netacad-ai-helper-btn-gemini'; + geminiButton.innerHTML = '✨ Get Answer from Gemini'; + geminiButton.className = 'ai-helper-button ai-helper-button-gemini'; + + // GPT button click handler + gptButton.addEventListener('click', async () => { + await handleButtonClick(gptButton, 'gpt', '🤖 Get Answer from GPT'); }); - // Advanced button click handler - advancedButton.addEventListener('click', async () => { - await handleButtonClick(advancedButton, 'coding', '🔥 Advanced AI (Code/Math)'); + // Gemini button click handler + geminiButton.addEventListener('click', async () => { + await handleButtonClick(geminiButton, 'gemini', '✨ Get Answer from Gemini'); }); // Add buttons to the target document body - targetDocument.body.appendChild(simpleButton); - targetDocument.body.appendChild(advancedButton); + targetDocument.body.appendChild(gptButton); + targetDocument.body.appendChild(geminiButton); console.log('AI helper buttons added to', targetDocument === document ? 'main page' : 'iframe'); } @@ -443,19 +506,28 @@ async function handleButtonClick(button, modelType, originalText) { } try { - // Send message to background script with model type + // Send message to background script with model type and multiple-answer info const response = await chrome.runtime.sendMessage({ action: 'getAnswer', question: questionData.question, options: questionData.options.map(opt => opt.text), - modelType: modelType + modelType: modelType, + isMultipleAnswer: questionData.isMultipleAnswer, + requiredAnswers: questionData.requiredAnswers }); console.log('AI Response received:', response); if (response.success) { - highlightCorrectAnswer(response.answerIndex); - button.innerHTML = '✅ Answer Highlighted'; + // Handle both single answer (number) and multiple answers (array) + const answerIndices = Array.isArray(response.answerIndex) ? response.answerIndex : [response.answerIndex]; + highlightCorrectAnswer(answerIndices); + + const answerText = questionData.isMultipleAnswer + ? `✅ ${answerIndices.length} Answers Highlighted` + : '✅ Answer Highlighted'; + + button.innerHTML = answerText; setTimeout(() => { button.innerHTML = originalText; button.disabled = false; @@ -483,7 +555,7 @@ function checkForQuiz() { const appRoot = document.querySelector('app-root'); console.log('app-root element:', appRoot); - const buttonsExist = document.getElementById('netacad-ai-helper-btn-simple'); + const buttonsExist = document.getElementById('netacad-ai-helper-btn-gpt'); console.log('Buttons exist:', buttonsExist); if (appRoot && !buttonsExist) { @@ -504,7 +576,7 @@ function tryCheckForQuiz() { checkForQuiz(); - if (checkAttempts < maxAttempts && !document.getElementById('netacad-ai-helper-btn-simple')) { + if (checkAttempts < maxAttempts && !document.getElementById('netacad-ai-helper-btn-gpt')) { setTimeout(tryCheckForQuiz, 500); } } @@ -519,7 +591,7 @@ function initialize() { // Also observe for dynamic content changes (for SPA navigation) const observer = new MutationObserver((mutations) => { // Only check if buttons don't exist - if (!document.getElementById('netacad-ai-helper-btn-simple')) { + if (!document.getElementById('netacad-ai-helper-btn-gpt')) { const appRoot = document.querySelector('app-root'); if (appRoot) { console.log('app-root detected via mutation observer'); diff --git a/popup.html b/popup.html index d67f485..a8321ca 100644 --- a/popup.html +++ b/popup.html @@ -93,51 +93,18 @@

🤖 netAIcad | AI Powered Netacad Quiz Helper

- - -
For general questions, trivia, and non-technical content
-
- -
- - -
For programming, mathematics, and technical questions
-
- -
- - + +
- Get your free API key from: OpenRouter + Get your API key from: OpenAI Platform
- - -
- Groq: Get Key | - Gemini: Get Key + + +
+ Get your free API key from: Google AI Studio
@@ -147,12 +114,11 @@

🤖 netAIcad | AI Powered Netacad Quiz Helper

How to use:
- 1. Select models for simple and advanced questions
- 2. Enter the required API key(s)
- 3. Click Save Settings
- 4. Go to a Netacad quiz page
- 5. Use the green button for general questions
- 6. Use the red button for coding/math questions + 1. Enter your OpenAI and/or Gemini API keys
+ 2. Click Save Settings
+ 3. Go to a Netacad quiz page
+ 4. Click "Get Answer from GPT" to use OpenAI
+ 5. Click "Get Answer from Gemini" to use Google Gemini
Created by Muhammad Zaid 😎
diff --git a/popup.js b/popup.js index 45dff35..5443559 100644 --- a/popup.js +++ b/popup.js @@ -1,83 +1,39 @@ // Popup script for configuration document.addEventListener('DOMContentLoaded', async () => { - const simpleModelSelect = document.getElementById('simpleModel'); - const codingModelSelect = document.getElementById('codingModel'); - const openRouterApiKeyInput = document.getElementById('openRouterApiKey'); - const apiKeyInput = document.getElementById('apiKey'); + const openAiApiKeyInput = document.getElementById('openAiApiKey'); + const geminiApiKeyInput = document.getElementById('geminiApiKey'); const saveBtn = document.getElementById('saveBtn'); const statusDiv = document.getElementById('status'); // Load saved settings const settings = await chrome.storage.sync.get([ - 'simpleModel', - 'codingModel', - 'openRouterApiKey', - 'apiKey', - 'aiProvider' // For backward compatibility + 'openAiApiKey', + 'geminiApiKey' ]); - if (settings.simpleModel) { - simpleModelSelect.value = settings.simpleModel; - } else if (settings.aiProvider) { - // Migrate old settings - simpleModelSelect.value = settings.aiProvider; + if (settings.openAiApiKey) { + openAiApiKeyInput.value = settings.openAiApiKey; } - if (settings.codingModel) { - codingModelSelect.value = settings.codingModel; - } - - if (settings.openRouterApiKey) { - openRouterApiKeyInput.value = settings.openRouterApiKey; - } - - if (settings.apiKey) { - apiKeyInput.value = settings.apiKey; + if (settings.geminiApiKey) { + geminiApiKeyInput.value = settings.geminiApiKey; } // Save settings saveBtn.addEventListener('click', async () => { - const simpleModel = simpleModelSelect.value; - const codingModel = codingModelSelect.value; - const openRouterApiKey = openRouterApiKeyInput.value.trim(); - const apiKey = apiKeyInput.value.trim(); - - // Validation - if (!simpleModel && !codingModel) { - showStatus('Please select at least one model', 'error'); - return; - } - - // Check if OpenRouter models are selected but no OpenRouter API key - const openRouterModels = [ - 'DeepSeek', 'GPT-5 Pro', 'Claude Sonnet 4.5', 'Qwen3 Coder Plus', - 'GLM', 'Grok 4 Fast', 'GPT-5 Codex', 'Qwen3 Coder Flash' - ]; - - const needsOpenRouterKey = openRouterModels.includes(simpleModel) || - openRouterModels.includes(codingModel); - - if (needsOpenRouterKey && !openRouterApiKey) { - showStatus('OpenRouter API key required for selected models', 'error'); - return; - } - - // Check if legacy providers are selected but no legacy API key - const needsLegacyKey = simpleModel === 'groq' || simpleModel === 'gemini' || - codingModel === 'groq' || codingModel === 'gemini'; + const openAiApiKey = openAiApiKeyInput.value.trim(); + const geminiApiKey = geminiApiKeyInput.value.trim(); - if (needsLegacyKey && !apiKey) { - showStatus('API key required for Groq/Gemini', 'error'); + // Validation - at least one API key is required + if (!openAiApiKey && !geminiApiKey) { + showStatus('Please enter at least one API key', 'error'); return; } try { await chrome.storage.sync.set({ - simpleModel: simpleModel, - codingModel: codingModel, - openRouterApiKey: openRouterApiKey, - apiKey: apiKey, - aiProvider: simpleModel // For backward compatibility + openAiApiKey: openAiApiKey, + geminiApiKey: geminiApiKey }); showStatus('Settings saved successfully!', 'success');