From 58b81d6f27a8bb114bbbde031c23ef602281a5b5 Mon Sep 17 00:00:00 2001 From: Muhammad Zaid Date: Tue, 25 Nov 2025 18:07:06 +0500 Subject: [PATCH 1/3] [*] Simple Version with Gemini & OpenAI model --- README.md | 133 +++++++++++++++++----------------------- background.js | 167 ++++++++------------------------------------------ content.css | 32 +++++----- content.js | 52 ++++++++-------- popup.html | 58 ++++-------------- popup.js | 74 +++++----------------- 6 files changed, 150 insertions(+), 366 deletions(-) diff --git a/README.md b/README.md index 336c42a..9f39df4 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,15 @@ ## 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 - ✨ 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 +33,53 @@ ## 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 + - **🤖 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 5. Review the suggestion and make your selection -### Which Button to Use? 🤔 +### 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 +93,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,8 +138,8 @@ 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 +4. User clicks either the **blue button** (GPT) or **purple button** (Gemini) +5. **Background Script** (`background.js`) sends the question to the selected AI provider (OpenAI or Google) 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) @@ -172,41 +155,39 @@ 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 +- 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 - Only works on multiple-choice questions -- For best results, use the appropriate button (simple vs advanced) based on question type +- Both models work well for all question types ## 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..1238aa7 100644 --- a/background.js +++ b/background.js @@ -18,18 +18,6 @@ const max_tokens = 500; 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) @@ -39,58 +27,30 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } }); -async function handleGetAnswer(question, options, modelType = 'simple') { +async function handleGetAnswer(question, options, modelType) { 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 (!apiKey) { - throw new Error('OpenRouter 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; + if (modelType === 'gpt') { + const apiKey = settings.openAiApiKey; if (!apiKey) { - throw new Error('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 { - // Fallback to old provider system if no model selected - provider = settings.aiProvider || 'groq'; - apiKey = settings.apiKey; + answerIndex = await getAnswerFromOpenAI(question, options, apiKey); + } 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.'); } - } - - 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 }; @@ -232,8 +192,8 @@ Answer with only the letter:`; return answerIndex; } -async function getAnswerFromOpenRouter(question, options, modelName, apiKey) { - const url = 'https://openrouter.ai/api/v1/chat/completions'; +async function getAnswerFromOpenAI(question, options, apiKey) { + const url = 'https://api.openai.com/v1/chat/completions'; // Format options with letters const formattedOptions = options.map((opt, idx) => @@ -250,7 +210,7 @@ ${formattedOptions} Answer with only the letter:`; const requestBody = { - model: OPENROUTER_MODELS[modelName], + model: 'gpt-4o-mini', messages: [ { role: 'system', @@ -279,31 +239,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 +269,26 @@ 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'); - - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. - -Question: ${question} - -Options: -${formattedOptions} - -Answer with only the letter:`; - - const requestBody = { - model: 'llama-3.3-70b-versatile', // Fast and accurate model - messages: [ - { - role: 'system', - content: SYSTEM_PROMPT - }, - { - role: 'user', - content: prompt - } - ], - 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(); - - if (!answerText) { - throw new Error('No answer received from Groq'); - } + console.log('OpenAI raw answer:', answerText); // Extract letter from response const letterMatch = answerText.match(/[ABCD]/); if (!letterMatch) { - throw new Error('Invalid answer format from Groq: ' + answerText); + throw new Error('Invalid answer format from OpenAI: ' + answerText); } const answerLetter = letterMatch[0]; const answerIndex = answerLetter.charCodeAt(0) - 65; - console.log('Groq answer:', answerLetter, 'Index:', answerIndex); + console.log('OpenAI answer:', answerLetter, 'Index:', answerIndex, 'Model: gpt-4o-mini'); 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..a217897 100644 --- a/content.js +++ b/content.js @@ -387,44 +387,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'); } @@ -483,7 +483,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 +504,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 +519,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
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'); From 848c5ea6b259809a28f30732cef185adf6ad4c5d Mon Sep 17 00:00:00 2001 From: Muhammad Zaid Date: Tue, 25 Nov 2025 18:46:47 +0500 Subject: [PATCH 2/3] Fixed Gemini response --- README.md | 24 ++++++--- background.js | 122 ++++++++++++++++++++++++++++++++++------------ content.js | 132 ++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 211 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 9f39df4..f2f79ac 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - **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 (GPT and Gemini buttons) @@ -64,9 +65,18 @@ - **🤖 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 +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 +### 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: @@ -138,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 **blue button** (GPT) or **purple button** (Gemini) -5. **Background Script** (`background.js`) sends the question to the selected AI provider (OpenAI or Google) -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 @@ -166,8 +177,9 @@ Below are some screenshots demonstrating the extension in action: - **Affordable/FREE options**: - **Gemini**: Free tier with generous limits (⭐ Recommended to start) - **OpenAI**: Very affordable pay-as-you-go pricing -- Only works on multiple-choice questions +- 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 diff --git a/background.js b/background.js index 1238aa7..74270ba 100644 --- a/background.js +++ b/background.js @@ -14,20 +14,26 @@ 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; 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) { +async function handleGetAnswer(question, options, modelType, isMultipleAnswer = false, requiredAnswers = 1) { try { // Get settings from storage const settings = await chrome.storage.sync.get([ @@ -42,13 +48,13 @@ async function handleGetAnswer(question, options, modelType) { if (!apiKey) { throw new Error('OpenAI API key not configured. Please set it in the extension popup.'); } - answerIndex = await getAnswerFromOpenAI(question, options, apiKey); + answerIndex = await getAnswerFromOpenAI(question, options, apiKey, isMultipleAnswer, requiredAnswers); } else if (modelType === 'gemini') { const apiKey = settings.geminiApiKey; if (!apiKey) { throw new Error('Gemini API key not configured. Please set it in the extension popup.'); } - answerIndex = await getAnswerFromGemini(question, options, apiKey); + answerIndex = await getAnswerFromGemini(question, options, apiKey, isMultipleAnswer, requiredAnswers); } else { throw new Error('Unknown model type: ' + modelType); } @@ -60,7 +66,7 @@ async function handleGetAnswer(question, options, modelType) { } } -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 @@ -68,7 +74,20 @@ async function getAnswerFromGemini(question, options, apiKey) { `${String.fromCharCode(65 + idx)}. ${opt}` ).join('\n'); - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + let prompt; + if (isMultipleAnswer) { + prompt = `This is a multiple-answer question. You must select exactly ${requiredAnswers} correct answer(s). + +Answer with ONLY the letters separated by commas (e.g., "A,B" or "A, C, D"). No explanation, no extra text. + +Question: ${question} + +Options: +${formattedOptions} + +Answer with only the ${requiredAnswers} correct letter(s) separated by commas:`; + } else { + prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. Question: ${question} @@ -76,6 +95,7 @@ Options: ${formattedOptions} Answer with only the letter:`; + } const requestBody = { contents: [{ @@ -85,10 +105,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: [ { @@ -179,20 +197,34 @@ 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); - } + if (isMultipleAnswer) { + // Extract multiple letters from response (handles "A,B", "A, B", "A,C,D", etc.) + const letterMatches = answerText.match(/[ABCD]/g); + if (!letterMatches || letterMatches.length === 0) { + throw new Error('Invalid answer format from Gemini: ' + answerText); + } - const answerLetter = letterMatch[0]; - const answerIndex = answerLetter.charCodeAt(0) - 65; // Convert A->0, B->1, etc. + // Convert letters to indices and remove duplicates + const answerIndices = [...new Set(letterMatches)].map(letter => letter.charCodeAt(0) - 65); - console.log('Gemini answer:', answerLetter, 'Index:', answerIndex); - return answerIndex; + 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(/[ABCD]/); + if (!letterMatch) { + throw new Error('Invalid answer format from Gemini: ' + 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; + } } -async function getAnswerFromOpenAI(question, options, apiKey) { +async function getAnswerFromOpenAI(question, options, apiKey, isMultipleAnswer = false, requiredAnswers = 1) { const url = 'https://api.openai.com/v1/chat/completions'; // Format options with letters @@ -200,7 +232,20 @@ async function getAnswerFromOpenAI(question, options, apiKey) { `${String.fromCharCode(65 + idx)}. ${opt}` ).join('\n'); - const prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + let prompt; + if (isMultipleAnswer) { + prompt = `This is a multiple-answer question. You must select exactly ${requiredAnswers} correct answer(s). + +Answer with ONLY the letters separated by commas (e.g., "A,B" or "A, C, D"). No explanation, no extra text. + +Question: ${question} + +Options: +${formattedOptions} + +Answer with only the ${requiredAnswers} correct letter(s) separated by commas:`; + } else { + prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. Question: ${question} @@ -208,6 +253,7 @@ Options: ${formattedOptions} Answer with only the letter:`; + } const requestBody = { model: 'gpt-4o-mini', @@ -279,16 +325,30 @@ Answer with only the letter:`; console.log('OpenAI raw answer:', answerText); - // Extract letter from response - const letterMatch = answerText.match(/[ABCD]/); - if (!letterMatch) { - throw new Error('Invalid answer format from OpenAI: ' + answerText); - } + if (isMultipleAnswer) { + // Extract multiple letters from response (handles "A,B", "A, B", "A,C,D", etc.) + const letterMatches = answerText.match(/[ABCD]/g); + if (!letterMatches || letterMatches.length === 0) { + throw new Error('Invalid answer format from OpenAI: ' + answerText); + } - const answerLetter = letterMatch[0]; - const answerIndex = answerLetter.charCodeAt(0) - 65; + // Convert letters to indices and remove duplicates + const answerIndices = [...new Set(letterMatches)].map(letter => letter.charCodeAt(0) - 65); - console.log('OpenAI answer:', answerLetter, 'Index:', answerIndex, 'Model: gpt-4o-mini'); - return answerIndex; + console.log('OpenAI answers:', letterMatches.join(','), 'Indices:', answerIndices, 'Model: gpt-4o-mini'); + return answerIndices; + } else { + // Extract single letter from response + const letterMatch = answerText.match(/[ABCD]/); + if (!letterMatch) { + throw new Error('Invalid answer format from OpenAI: ' + answerText); + } + + const answerLetter = letterMatch[0]; + const answerIndex = answerLetter.charCodeAt(0) - 65; + + console.log('OpenAI answer:', answerLetter, 'Index:', answerIndex, 'Model: gpt-4o-mini'); + return answerIndex; + } } diff --git a/content.js b/content.js index a217897..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'); @@ -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; From 580a0682cfc616ed02ed498c36481092dd0abefb Mon Sep 17 00:00:00 2001 From: Muhammad Zaid Date: Tue, 25 Nov 2025 20:47:41 +0500 Subject: [PATCH 3/3] fixed invalid option issues --- background.js | 125 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 29 deletions(-) diff --git a/background.js b/background.js index 74270ba..ea9ae1d 100644 --- a/background.js +++ b/background.js @@ -1,15 +1,18 @@ // 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; @@ -69,32 +72,45 @@ async function handleGetAnswer(question, options, modelType, isMultipleAnswer = 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'); + // Get the available option letters dynamically + const availableLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(', '); + let prompt; if (isMultipleAnswer) { - prompt = `This is a multiple-answer question. You must select exactly ${requiredAnswers} correct answer(s). + prompt = `IMPORTANT: This is a multiple-answer question. You MUST select EXACTLY ${requiredAnswers} correct answer(s). Not more, not less. -Answer with ONLY the letters separated by commas (e.g., "A,B" or "A, C, D"). No explanation, no extra text. +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 only the ${requiredAnswers} correct letter(s) separated by commas:`; +Answer with EXACTLY ${requiredAnswers} letter(s) separated by commas:`; } else { - prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + 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 = { @@ -197,23 +213,42 @@ Answer with only the letter:`; console.log('Gemini answer text:', 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(/[ABCD]/g); + const letterMatches = answerText.match(validLetterPattern); if (!letterMatches || letterMatches.length === 0) { - throw new Error('Invalid answer format from Gemini: ' + answerText); + 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}`); + } + } + 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(/[ABCD]/); + const letterMatch = answerText.match(validLetterPattern); if (!letterMatch) { - throw new Error('Invalid answer format from Gemini: ' + answerText); + throw new Error(`Invalid answer format from Gemini. Expected one letter from ${validLetters}, got: ${answerText}`); } const answerLetter = letterMatch[0]; @@ -227,32 +262,45 @@ Answer with only the letter:`; 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'); + // Get the available option letters dynamically + const availableLetters = options.map((_, idx) => String.fromCharCode(65 + idx)).join(', '); + let prompt; if (isMultipleAnswer) { - prompt = `This is a multiple-answer question. You must select exactly ${requiredAnswers} correct answer(s). + prompt = `IMPORTANT: This is a multiple-answer question. You MUST select EXACTLY ${requiredAnswers} correct answer(s). Not more, not less. -Answer with ONLY the letters separated by commas (e.g., "A,B" or "A, C, D"). No explanation, no extra text. +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 only the ${requiredAnswers} correct letter(s) separated by commas:`; +Answer with EXACTLY ${requiredAnswers} letter(s) separated by commas:`; } else { - prompt = `Answer this question with ONLY the letter (A, B, C, or D). No explanation. + 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 = { @@ -325,23 +373,42 @@ Answer with only the letter:`; console.log('OpenAI raw answer:', 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(/[ABCD]/g); + const letterMatches = answerText.match(validLetterPattern); if (!letterMatches || letterMatches.length === 0) { - throw new Error('Invalid answer format from OpenAI: ' + answerText); + throw new Error(`Invalid answer format from OpenAI. 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(`⚠️ OpenAI 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}`); + } + } + console.log('OpenAI answers:', letterMatches.join(','), 'Indices:', answerIndices, 'Model: gpt-4o-mini'); return answerIndices; } else { // Extract single letter from response - const letterMatch = answerText.match(/[ABCD]/); + const letterMatch = answerText.match(validLetterPattern); if (!letterMatch) { - throw new Error('Invalid answer format from OpenAI: ' + answerText); + throw new Error(`Invalid answer format from OpenAI. Expected one letter from ${validLetters}, got: ${answerText}`); } const answerLetter = letterMatch[0];