AI-Powered Quiz Assistant (Chrome Extension)
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
# 🎓 Quiz Helper - AI-Powered Quiz Assistant
|
||||||
|
|
||||||
|
An intelligent Chrome extension that uses ChatGPT to help you with online quizzes by displaying helpful hint bubbles with [somewhat] correct answers.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🤖 **AI-Powered**: Uses ChatGPT (GPT-3.5-turbo) for intelligent answer analysis
|
||||||
|
- 💡 **Smart Hints**: Highlights answers in green
|
||||||
|
- 🎯 **Accurate**: ChatGPT sees actual answer text for better context
|
||||||
|
- ⚡ **Auto-Detection**: Automatically detects single vs. multiple answer questions
|
||||||
|
- 🔒 **Private**: All data stays between you and OpenAI
|
||||||
|
- 💰 **Affordable**: ~$0.001 per question (100 questions ≈ $0.10-0.20)
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- Google Chrome or Chromium-based browser (Edge, Brave, etc.)
|
||||||
|
- OpenAI API key ([get one free here](https://platform.openai.com/api-keys))
|
||||||
|
- Internet connection
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
1. **Download** this repository:
|
||||||
|
- Click the green "Code" button above
|
||||||
|
- Select "Download ZIP"
|
||||||
|
- Extract the ZIP file
|
||||||
|
|
||||||
|
2. **Load in Chrome**:
|
||||||
|
- Open Chrome and navigate to `chrome://extensions/`
|
||||||
|
- Enable "Developer mode" (toggle in top-right corner)
|
||||||
|
- Click "Load unpacked"
|
||||||
|
- Select the `extension` folder from the extracted files
|
||||||
|
|
||||||
|
3. **Add API Key**:
|
||||||
|
- Click the extension icon in your toolbar
|
||||||
|
- Paste your OpenAI API key
|
||||||
|
- Click "Save API Key"
|
||||||
|
|
||||||
|
## 🎯 How to Use
|
||||||
|
|
||||||
|
1. **Navigate to a quiz** (works best with Microsoft Learn practice assessments)
|
||||||
|
2. **Click the extension icon** in your toolbar
|
||||||
|
3. **Toggle the switch to "On"**
|
||||||
|
4. **Wait a moment** - Correct answer(s) will be marked in green
|
||||||
|
5. **Select your answers** based on the hints
|
||||||
|
6. **Toggle off** when done to remove hints
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
The extension requires an OpenAI API key to function. To get one:
|
||||||
|
|
||||||
|
1. Visit [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
|
||||||
|
2. Sign up or log in
|
||||||
|
3. Click "Create new secret key"
|
||||||
|
4. Copy the key (starts with `sk-...`)
|
||||||
|
5. Paste it in the extension popup
|
||||||
|
|
||||||
|
**Note**: New OpenAI accounts get $5 in free credits!
|
||||||
|
|
||||||
|
## 💰 Costs
|
||||||
|
|
||||||
|
- Model used: `gpt-3.5-turbo`
|
||||||
|
- Cost per question: ~$0.001-0.002 (1/10th of a penny)
|
||||||
|
- 50 questions: ~$0.05-0.10
|
||||||
|
- 100 questions: ~$0.10-0.20
|
||||||
|
|
||||||
|
Very affordable for exam preparation!
|
||||||
|
|
||||||
|
## 🛠️ Technical Details
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Question Detection**: Scans the page for quiz questions using multiple selectors
|
||||||
|
2. **Answer Identification**: Finds associated answer choices (radio buttons, checkboxes)
|
||||||
|
3. **Smart Prompting**: Sends the question and answer choices to ChatGPT
|
||||||
|
4. **Response Matching**: Matches ChatGPT's response to actual answer text
|
||||||
|
5. **Hint Display**: Shows a styled hint bubble with the correct answer(s)
|
||||||
|
|
||||||
|
### Files Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
extension/
|
||||||
|
├── manifest.json # Extension configuration
|
||||||
|
├── content.js # Main logic (question detection, ChatGPT API)
|
||||||
|
├── popup.html # Popup interface
|
||||||
|
├── popup.js # Popup functionality
|
||||||
|
├── background.js # Background service worker
|
||||||
|
├── icon16.png # Icon (16x16)
|
||||||
|
├── icon48.png # Icon (48x48)
|
||||||
|
└── icon128.png # Icon (128x128)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Privacy & Security
|
||||||
|
|
||||||
|
- Your API key is stored locally in Chrome's sync storage
|
||||||
|
- No data is collected or sent anywhere except directly to OpenAI
|
||||||
|
- The extension only runs when you explicitly enable it
|
||||||
|
- All communication is encrypted (HTTPS)
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
**For Educational Use Only**
|
||||||
|
|
||||||
|
This extension is designed for learning and study purposes. Use responsibly and in accordance with your institution's academic integrity policies. The accuracy of answers depends on ChatGPT's knowledge and may not always be 100% correct.
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
**Extension won't load:**
|
||||||
|
- Make sure all files are in the `extension` folder
|
||||||
|
- Check that `manifest.json` has no syntax errors
|
||||||
|
- Ensure all three icon files are present
|
||||||
|
|
||||||
|
**No hints appearing:**
|
||||||
|
- Check that you've added a valid OpenAI API key
|
||||||
|
- Open browser console (F12) and look for error messages
|
||||||
|
- Make sure the extension toggle is "On"
|
||||||
|
- Try refreshing the quiz page
|
||||||
|
|
||||||
|
**API errors:**
|
||||||
|
- Verify your API key is correct (starts with `sk-`)
|
||||||
|
- Check you have credits in your OpenAI account
|
||||||
|
- Look for rate limit messages in console
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Feel free to:
|
||||||
|
- Report bugs
|
||||||
|
- Suggest features
|
||||||
|
- Submit pull requests
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - feel free to use and modify as needed.
|
||||||
|
|
||||||
|
**Made with ❤️ for students and learners everywhere**
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Background service worker for the extension
|
||||||
|
// Handles installation and updates
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('Quiz Helper extension installed');
|
||||||
|
|
||||||
|
// Set default state
|
||||||
|
chrome.storage.sync.set({ enabled: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for messages from content scripts or popup
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === 'getState') {
|
||||||
|
chrome.storage.sync.get(['enabled'], (result) => {
|
||||||
|
sendResponse({ enabled: result.enabled || false });
|
||||||
|
});
|
||||||
|
return true; // Indicates async response
|
||||||
|
}
|
||||||
|
});
|
||||||
+633
@@ -0,0 +1,633 @@
|
|||||||
|
let extensionEnabled = false;
|
||||||
|
let isProcessing = false;
|
||||||
|
let processedQuestions = new Set();
|
||||||
|
let openaiApiKey = null;
|
||||||
|
let styleInjected = false;
|
||||||
|
|
||||||
|
// Inject custom CSS for highlighting
|
||||||
|
function injectCustomStyles() {
|
||||||
|
if (styleInjected) return;
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'quiz-helper-styles';
|
||||||
|
style.textContent = `
|
||||||
|
.quiz-helper-correct {
|
||||||
|
background-color: #90EE90 !important;
|
||||||
|
border: 3px solid #28a745 !important;
|
||||||
|
box-shadow: 0 0 10px rgba(40, 167, 69, 0.6) !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-helper-correct * {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-helper-unknown {
|
||||||
|
background-color: #FFB6C1 !important;
|
||||||
|
border: 3px solid #dc3545 !important;
|
||||||
|
box-shadow: 0 0 10px rgba(220, 53, 69, 0.6) !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-helper-unknown * {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
styleInjected = true;
|
||||||
|
console.log('✓ Custom CSS styles injected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize extension state
|
||||||
|
chrome.storage.sync.get(['enabled', 'openaiApiKey'], function(result) {
|
||||||
|
extensionEnabled = result.enabled || false;
|
||||||
|
openaiApiKey = result.openaiApiKey || null;
|
||||||
|
|
||||||
|
if (openaiApiKey) {
|
||||||
|
console.log('✓ OpenAI API key loaded');
|
||||||
|
} else {
|
||||||
|
console.log('⚠ No OpenAI API key found. Please add one in the extension popup.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionEnabled) {
|
||||||
|
injectCustomStyles();
|
||||||
|
waitForContentAndScan();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for toggle messages
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === 'toggleExtension') {
|
||||||
|
extensionEnabled = request.enabled;
|
||||||
|
|
||||||
|
if (extensionEnabled) {
|
||||||
|
if (!openaiApiKey) {
|
||||||
|
console.error('❌ Cannot enable: No OpenAI API key set. Please add one in the extension popup.');
|
||||||
|
} else {
|
||||||
|
injectCustomStyles();
|
||||||
|
waitForContentAndScan();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeHighlights();
|
||||||
|
processedQuestions.clear();
|
||||||
|
}
|
||||||
|
} else if (request.action === 'apiKeyUpdated') {
|
||||||
|
openaiApiKey = request.apiKey;
|
||||||
|
console.log('✓ OpenAI API key updated');
|
||||||
|
processedQuestions.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for dynamic content to load before scanning
|
||||||
|
function waitForContentAndScan() {
|
||||||
|
console.log('Waiting for quiz content to load...');
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 40;
|
||||||
|
|
||||||
|
const checkContent = setInterval(() => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const hasContent = document.body.innerText.length > 500;
|
||||||
|
const hasInteractiveElements = document.querySelectorAll('button, input, label').length > 5;
|
||||||
|
const mainContent = document.querySelector('[data-main-column]');
|
||||||
|
const hasMainContent = mainContent && mainContent.innerText.length > 100;
|
||||||
|
const hasQuizElements = document.querySelectorAll('input[type="radio"], input[type="checkbox"]').length > 0;
|
||||||
|
|
||||||
|
console.log(`Attempt ${attempts}: content=${hasContent}, interactive=${hasInteractiveElements}, main=${hasMainContent}, quiz=${hasQuizElements}`);
|
||||||
|
|
||||||
|
if ((hasContent && hasInteractiveElements && hasMainContent) || hasQuizElements) {
|
||||||
|
clearInterval(checkContent);
|
||||||
|
console.log('✓ Quiz content detected! Starting scan in 2 seconds...');
|
||||||
|
setTimeout(() => scanAndHighlight(), 2000);
|
||||||
|
} else if (attempts >= maxAttempts) {
|
||||||
|
clearInterval(checkContent);
|
||||||
|
console.log('✗ Timeout: Quiz content did not load. Try refreshing the page.');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to identify potential questions
|
||||||
|
function findQuestions() {
|
||||||
|
const questions = [];
|
||||||
|
const seenTexts = new Set();
|
||||||
|
|
||||||
|
console.log('DEBUG: Total body text length:', document.body.innerText.length);
|
||||||
|
console.log('DEBUG: Sample text:', document.body.innerText.substring(0, 500));
|
||||||
|
|
||||||
|
const selectors = [
|
||||||
|
'[role="group"]',
|
||||||
|
'[class*="question"]',
|
||||||
|
'[class*="quiz"]',
|
||||||
|
'[class*="assessment"]',
|
||||||
|
'[data-test*="question"]',
|
||||||
|
'fieldset',
|
||||||
|
'h1, h2, h3, h4, h5, h6',
|
||||||
|
'p',
|
||||||
|
'div[class*="text"]',
|
||||||
|
'div',
|
||||||
|
'span',
|
||||||
|
'label'
|
||||||
|
];
|
||||||
|
|
||||||
|
const questionPatterns = [
|
||||||
|
/\?$/,
|
||||||
|
/\?/,
|
||||||
|
/^(what|when|where|who|why|how|which|is|are|does|do|did|can|could|would|should|will|shall)/i,
|
||||||
|
/\d+\.\s*[A-Z]/,
|
||||||
|
/^(true|false)/i,
|
||||||
|
/characteristics/i,
|
||||||
|
/deployment model/i
|
||||||
|
];
|
||||||
|
|
||||||
|
selectors.forEach(selector => {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
console.log(`DEBUG: Found ${elements.length} elements for selector "${selector}"`);
|
||||||
|
|
||||||
|
elements.forEach(el => {
|
||||||
|
const text = el.textContent.trim();
|
||||||
|
|
||||||
|
if (text.length < 10 || text.length > 1000 || seenTexts.has(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of questionPatterns) {
|
||||||
|
if (pattern.test(text)) {
|
||||||
|
seenTexts.add(text);
|
||||||
|
questions.push({
|
||||||
|
text: text,
|
||||||
|
element: el
|
||||||
|
});
|
||||||
|
console.log('Found question:', text.substring(0, 100));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questions.length === 0) {
|
||||||
|
console.log('DEBUG: No questions found. Logging first 10 p elements:');
|
||||||
|
const pElements = document.querySelectorAll('p');
|
||||||
|
Array.from(pElements).slice(0, 10).forEach((el, i) => {
|
||||||
|
console.log(` p[${i}]:`, el.textContent.substring(0, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DEBUG: Logging first 10 divs with text:');
|
||||||
|
const divs = document.querySelectorAll('div');
|
||||||
|
let count = 0;
|
||||||
|
for (const div of divs) {
|
||||||
|
const text = div.textContent.trim();
|
||||||
|
if (text.length > 20 && text.length < 200) {
|
||||||
|
console.log(` div[${count}]:`, text.substring(0, 100));
|
||||||
|
count++;
|
||||||
|
if (count >= 10) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to find potential answer choices
|
||||||
|
function findAnswerChoices(questionElement) {
|
||||||
|
const answers = [];
|
||||||
|
const seenTexts = new Set();
|
||||||
|
let inputType = null; // 'radio' or 'checkbox'
|
||||||
|
|
||||||
|
let container = questionElement.closest('[role="group"], fieldset, form, [class*="question"], div[class*="options"]');
|
||||||
|
if (!container) {
|
||||||
|
container = questionElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 3 && container && answers.length === 0; i++) {
|
||||||
|
container = container.parentElement;
|
||||||
|
if (!container) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container) container = document.body;
|
||||||
|
|
||||||
|
const answerSelectors = [
|
||||||
|
'input[type="radio"]',
|
||||||
|
'input[type="checkbox"]',
|
||||||
|
'button[role="radio"]',
|
||||||
|
'button[role="checkbox"]',
|
||||||
|
'[role="option"]',
|
||||||
|
'[class*="answer"]',
|
||||||
|
'[class*="option"]',
|
||||||
|
'[class*="choice"]',
|
||||||
|
'[data-test*="answer"]',
|
||||||
|
'[data-test*="option"]',
|
||||||
|
'label',
|
||||||
|
'li'
|
||||||
|
];
|
||||||
|
|
||||||
|
const excludePatterns = [
|
||||||
|
/^question\s+\d+\s+of\s+\d+$/i,
|
||||||
|
/^page\s+\d+/i,
|
||||||
|
/^next$/i,
|
||||||
|
/^previous$/i,
|
||||||
|
/^submit$/i,
|
||||||
|
/^skip$/i,
|
||||||
|
/^back$/i,
|
||||||
|
/^continue$/i,
|
||||||
|
/^finish$/i,
|
||||||
|
/^review$/i,
|
||||||
|
/^\d+\s*\/\s*\d+$/,
|
||||||
|
/^score:/i,
|
||||||
|
/^time:/i,
|
||||||
|
/^remaining:/i
|
||||||
|
];
|
||||||
|
|
||||||
|
answerSelectors.forEach(selector => {
|
||||||
|
const elements = container.querySelectorAll(selector);
|
||||||
|
|
||||||
|
elements.forEach(el => {
|
||||||
|
let text = el.textContent.trim();
|
||||||
|
|
||||||
|
// Detect input type from actual input elements
|
||||||
|
if (el.tagName === 'INPUT') {
|
||||||
|
if (el.type === 'radio') {
|
||||||
|
inputType = 'radio';
|
||||||
|
} else if (el.type === 'checkbox') {
|
||||||
|
inputType = 'checkbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = container.querySelector(`label[for="${el.id}"]`);
|
||||||
|
if (label) {
|
||||||
|
text = label.textContent.trim();
|
||||||
|
} else {
|
||||||
|
const parentLabel = el.closest('label');
|
||||||
|
if (parentLabel) {
|
||||||
|
text = parentLabel.textContent.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text || text.length < 2 || text.length > 300 || seenTexts.has(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludePatterns.some(pattern => pattern.test(text))) {
|
||||||
|
console.log(`Excluding UI element: "${text}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordCount = text.split(/\s+/).length;
|
||||||
|
const numberCount = (text.match(/\d+/g) || []).length;
|
||||||
|
if (numberCount > wordCount / 2) {
|
||||||
|
console.log(`Excluding numeric element: "${text}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenTexts.add(text);
|
||||||
|
answers.push({
|
||||||
|
text: text,
|
||||||
|
element: el,
|
||||||
|
parentLabel: el.closest('label') || el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${answers.length} answer choices (type: ${inputType || 'unknown'})`);
|
||||||
|
answers.forEach((a, i) => {
|
||||||
|
console.log(` [${i}] "${a.text.substring(0, 50)}" - Element: ${a.element.tagName}.${a.element.className || 'no-class'}, ID: ${a.element.id || 'no-id'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { answers, inputType };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to determine expected number of answers from question text
|
||||||
|
function getExpectedAnswerCount(questionText, inputType) {
|
||||||
|
if (inputType === 'radio') {
|
||||||
|
return 1; // Radio buttons always mean single answer
|
||||||
|
}
|
||||||
|
|
||||||
|
// For checkboxes, try to find the expected count in the question
|
||||||
|
const lowerQuestion = questionText.toLowerCase();
|
||||||
|
|
||||||
|
// Look for explicit numbers
|
||||||
|
const numberWords = {
|
||||||
|
'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5,
|
||||||
|
'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for patterns like "select two", "choose three", "two factors", etc.
|
||||||
|
for (const [word, num] of Object.entries(numberWords)) {
|
||||||
|
const patterns = [
|
||||||
|
new RegExp(`\\b${word}\\s+(answers?|options?|choices?|factors?|characteristics?|items?|reasons?|ways?|methods?|types?)\\b`, 'i'),
|
||||||
|
new RegExp(`\\bselect\\s+${word}\\b`, 'i'),
|
||||||
|
new RegExp(`\\bchoose\\s+${word}\\b`, 'i'),
|
||||||
|
new RegExp(`\\bpick\\s+${word}\\b`, 'i'),
|
||||||
|
new RegExp(`\\bwhich\\s+${word}\\b`, 'i')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.test(lowerQuestion)) {
|
||||||
|
console.log(`📊 Detected ${num} expected answers from question text`);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for digit numbers (e.g., "2 answers", "3 characteristics")
|
||||||
|
const digitMatch = lowerQuestion.match(/\b(\d+)\s+(answers?|options?|choices?|factors?|characteristics?|items?|reasons?|ways?|methods?|types?)\b/i);
|
||||||
|
if (digitMatch) {
|
||||||
|
const count = parseInt(digitMatch[1]);
|
||||||
|
console.log(`📊 Detected ${count} expected answers from question text`);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "Each correct answer" or "select all that apply" - means multiple but unknown count
|
||||||
|
if (/each correct answer|select all that apply|all that apply|select all|choose all/i.test(lowerQuestion)) {
|
||||||
|
console.log(`📊 Detected "select all" question - expecting multiple answers`);
|
||||||
|
return -1; // Special value meaning "multiple, unknown count"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default for checkboxes: assume multiple but we don't know how many
|
||||||
|
console.log(`📊 Checkbox question with no specific count mentioned`);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get answer using OpenAI
|
||||||
|
async function getAnswerFromOpenAI(question, answerChoices, inputType, expectedCount) {
|
||||||
|
if (!openaiApiKey) {
|
||||||
|
console.error('❌ No API key available');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🤖 Asking ChatGPT...');
|
||||||
|
|
||||||
|
const choicesText = answerChoices.map((c, i) => `${String.fromCharCode(65 + i)}. ${c.text}`).join('\n');
|
||||||
|
|
||||||
|
let instructions = '';
|
||||||
|
if (inputType === 'radio') {
|
||||||
|
instructions = '- This is a SINGLE-ANSWER question (radio buttons). Provide exactly ONE letter.';
|
||||||
|
} else if (expectedCount > 0) {
|
||||||
|
instructions = `- This is a MULTIPLE-ANSWER question (checkboxes). Provide exactly ${expectedCount} letter(s), separated by commas.`;
|
||||||
|
} else if (expectedCount === -1) {
|
||||||
|
instructions = '- This is a MULTIPLE-ANSWER question (checkboxes). Provide ALL correct answers, separated by commas.';
|
||||||
|
} else {
|
||||||
|
instructions = '- Determine if this requires one or multiple answers based on the question wording.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `You are helping with a Microsoft Azure certification exam. Answer the following multiple-choice question by selecting the correct answer(s).
|
||||||
|
|
||||||
|
Question: ${question}
|
||||||
|
|
||||||
|
Answer choices:
|
||||||
|
${choicesText}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
${instructions}
|
||||||
|
- Respond ONLY with the letter(s) of the correct answer(s) (e.g., "A" or "A,C,D")
|
||||||
|
- Do not include any explanation, reasoning, or additional text
|
||||||
|
- Do not include spaces after commas
|
||||||
|
|
||||||
|
Correct answer(s):`;
|
||||||
|
|
||||||
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${openaiApiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'You are an expert on Microsoft Azure and cloud computing. You provide only the letter(s) of correct answers to multiple-choice questions, without any explanation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
temperature: 0.1,
|
||||||
|
max_tokens: 20
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('❌ OpenAI API error:', error);
|
||||||
|
if (error.error && error.error.message) {
|
||||||
|
console.error('Error message:', error.error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const answer = data.choices[0].message.content.trim();
|
||||||
|
|
||||||
|
console.log('🤖 ChatGPT says:', answer);
|
||||||
|
|
||||||
|
const correctLetters = answer.replace(/\s/g, '').split(',').map(l => l.trim().toUpperCase());
|
||||||
|
const correctIndices = correctLetters.map(l => l.charCodeAt(0) - 65).filter(i => i >= 0 && i < answerChoices.length);
|
||||||
|
|
||||||
|
// Validate answer count for radio buttons
|
||||||
|
if (inputType === 'radio' && correctIndices.length > 1) {
|
||||||
|
console.log(`⚠️ Warning: Radio button question but got ${correctIndices.length} answers. Using only the first one.`);
|
||||||
|
return [correctIndices[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate answer count for checkboxes with specific count
|
||||||
|
if (expectedCount > 0 && correctIndices.length !== expectedCount) {
|
||||||
|
console.log(`⚠️ Warning: Expected ${expectedCount} answers but got ${correctIndices.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return correctIndices;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error calling OpenAI:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to highlight matching answers
|
||||||
|
function highlightAnswer(element, isCorrect = true) {
|
||||||
|
const colorType = isCorrect ? 'correct' : 'unknown';
|
||||||
|
console.log(` 🎨 Attempting to highlight as ${colorType}`);
|
||||||
|
console.log(` Original element:`, element);
|
||||||
|
|
||||||
|
let targetElement = element;
|
||||||
|
|
||||||
|
if (element.tagName === 'INPUT') {
|
||||||
|
console.log(` Element is INPUT, looking for label...`);
|
||||||
|
const label = document.querySelector(`label[for="${element.id}"]`) || element.closest('label');
|
||||||
|
if (label) {
|
||||||
|
targetElement = label;
|
||||||
|
console.log(` Found label:`, label);
|
||||||
|
} else {
|
||||||
|
targetElement = element.parentElement;
|
||||||
|
console.log(` No label found, using parent:`, targetElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any existing quiz helper classes
|
||||||
|
targetElement.classList.remove('quiz-helper-correct', 'quiz-helper-unknown');
|
||||||
|
targetElement.removeAttribute('data-quiz-helper-highlight');
|
||||||
|
|
||||||
|
// Add the appropriate class
|
||||||
|
const className = isCorrect ? 'quiz-helper-correct' : 'quiz-helper-unknown';
|
||||||
|
targetElement.classList.add(className);
|
||||||
|
targetElement.setAttribute('data-quiz-helper-highlight', 'true');
|
||||||
|
targetElement.setAttribute('data-quiz-helper-type', colorType);
|
||||||
|
|
||||||
|
console.log(` ✓ Applied ${colorType} class to:`, targetElement.textContent.substring(0, 50));
|
||||||
|
console.log(` Classes:`, targetElement.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to remove all highlights
|
||||||
|
function removeHighlights() {
|
||||||
|
const highlighted = document.querySelectorAll('[data-quiz-helper-highlight="true"]');
|
||||||
|
highlighted.forEach(el => {
|
||||||
|
el.classList.remove('quiz-helper-correct', 'quiz-helper-unknown');
|
||||||
|
el.removeAttribute('data-quiz-helper-highlight');
|
||||||
|
el.removeAttribute('data-quiz-helper-type');
|
||||||
|
});
|
||||||
|
console.log(`Removed ${highlighted.length} highlights`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main scanning function
|
||||||
|
async function scanAndHighlight() {
|
||||||
|
if (isProcessing || !extensionEnabled) return;
|
||||||
|
|
||||||
|
if (!openaiApiKey) {
|
||||||
|
console.error('❌ Cannot scan: No OpenAI API key. Please add one in the extension popup.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
console.log('Starting scan...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const questions = findQuestions();
|
||||||
|
console.log(`Found ${questions.length} potential questions`);
|
||||||
|
|
||||||
|
if (questions.length === 0) {
|
||||||
|
console.log('No questions found. Page might still be loading or uses unsupported format.');
|
||||||
|
isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionsToProcess = questions.slice(0, 10);
|
||||||
|
|
||||||
|
for (const question of questionsToProcess) {
|
||||||
|
if (processedQuestions.has(question.text)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedQuestions.add(question.text);
|
||||||
|
|
||||||
|
const { answers: answerChoices, inputType } = findAnswerChoices(question.element);
|
||||||
|
|
||||||
|
if (answerChoices.length === 0) {
|
||||||
|
console.log('No answer choices found for question');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedCount = getExpectedAnswerCount(question.text, inputType);
|
||||||
|
|
||||||
|
console.log(`\n📝 Question: ${question.text.substring(0, 100)}...`);
|
||||||
|
console.log(` Input type: ${inputType || 'unknown'}`);
|
||||||
|
console.log(` Expected answers: ${expectedCount === -1 ? 'multiple (unknown count)' : expectedCount}`);
|
||||||
|
console.log(` Answer choices (${answerChoices.length}):`);
|
||||||
|
answerChoices.forEach((c, i) => {
|
||||||
|
console.log(` ${String.fromCharCode(65 + i)}. ${c.text.substring(0, 50)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const correctIndices = await getAnswerFromOpenAI(question.text, answerChoices, inputType, expectedCount);
|
||||||
|
|
||||||
|
if (correctIndices && correctIndices.length > 0) {
|
||||||
|
console.log(`✅ ChatGPT identified ${correctIndices.length} correct answer(s):`);
|
||||||
|
|
||||||
|
let highlightedCount = 0;
|
||||||
|
correctIndices.forEach(index => {
|
||||||
|
if (index >= 0 && index < answerChoices.length) {
|
||||||
|
const choice = answerChoices[index];
|
||||||
|
console.log(` ✓ ${String.fromCharCode(65 + index)}. ${choice.text}`);
|
||||||
|
console.log(` Element type: ${choice.element.tagName}, class: ${choice.element.className}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
highlightAnswer(choice.element, true);
|
||||||
|
highlightedCount++;
|
||||||
|
console.log(` ✓ Successfully highlighted`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ❌ Failed to highlight:`, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(` ❌ Invalid index ${index} (max: ${answerChoices.length - 1})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (highlightedCount === 0) {
|
||||||
|
console.error('⚠️ WARNING: ChatGPT provided answers but NONE were highlighted!');
|
||||||
|
console.log('⚠️ Falling back to RED highlighting (user must guess)');
|
||||||
|
answerChoices.forEach((choice, index) => {
|
||||||
|
console.log(` ⚠️ ${String.fromCharCode(65 + index)}. ${choice.text} (HIGHLIGHT FAILED)`);
|
||||||
|
try {
|
||||||
|
highlightAnswer(choice.element, false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ❌ Failed to highlight in red:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (highlightedCount < correctIndices.length) {
|
||||||
|
console.warn(`⚠️ WARNING: Only ${highlightedCount} of ${correctIndices.length} answers were highlighted`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log('❌ ChatGPT failed to provide an answer - highlighting all choices in RED');
|
||||||
|
console.log('⚠️ USER MUST GUESS - ChatGPT could not determine the correct answer');
|
||||||
|
// Highlight all answers in red to indicate user needs to make a guess
|
||||||
|
answerChoices.forEach((choice, index) => {
|
||||||
|
console.log(` ⚠️ ${String.fromCharCode(65 + index)}. ${choice.text} (UNKNOWN)`);
|
||||||
|
try {
|
||||||
|
highlightAnswer(choice.element, false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ❌ Failed to highlight:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ Scan complete');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in scanAndHighlight:', error);
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for dynamic content changes
|
||||||
|
let mutationTimeout;
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
if (!extensionEnabled || isProcessing) return;
|
||||||
|
|
||||||
|
const hasNewContent = mutations.some(mutation => {
|
||||||
|
return mutation.addedNodes.length > 0 &&
|
||||||
|
Array.from(mutation.addedNodes).some(node =>
|
||||||
|
node.nodeType === 1 && node.textContent.length > 50
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasNewContent) {
|
||||||
|
clearTimeout(mutationTimeout);
|
||||||
|
mutationTimeout = setTimeout(() => {
|
||||||
|
console.log('Content changed, rescanning...');
|
||||||
|
scanAndHighlight();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 965 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Quiz Helper",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Automatically searches for quiz answers and highlights correct ones",
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"storage",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icon16.png",
|
||||||
|
"48": "icon48.png",
|
||||||
|
"128": "icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "icon16.png",
|
||||||
|
"48": "icon48.png",
|
||||||
|
"128": "icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
width: 320px;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.toggle-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.api-section {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.api-section label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.api-section input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.api-section button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #0078d4;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.api-section button:hover {
|
||||||
|
background: #106ebe;
|
||||||
|
}
|
||||||
|
.api-status {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.api-status.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.api-status.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.warning strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Quiz Helper</h3>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>⚠️ API Key Required</strong>
|
||||||
|
This extension requires an OpenAI API key to work. Add your key below to get started.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-section">
|
||||||
|
<label>OpenAI API Key (Required):</label>
|
||||||
|
<input type="text" id="apiKey" placeholder="sk-..." />
|
||||||
|
<button id="saveApiKey">Save API Key</button>
|
||||||
|
<div id="apiStatus" class="api-status" style="display: none;"></div>
|
||||||
|
<div class="info">
|
||||||
|
Get a free API key at <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a>.
|
||||||
|
New accounts get $5 in free credits. Cost per question: ~$0.001 (1/10th of a cent).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-container">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="toggleSwitch">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span id="toggleLabel">Off</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
When enabled, ChatGPT will analyze quiz questions and highlight answers:
|
||||||
|
<ul style="margin: 5px 0; padding-left: 20px; font-size: 11px;">
|
||||||
|
<li><strong style="color: #28a745;">Green</strong> = ChatGPT identified correct answer(s)</li>
|
||||||
|
<li><strong style="color: #dc3545;">Red</strong> = ChatGPT failed, you need to guess</li>
|
||||||
|
</ul>
|
||||||
|
Check the browser console (F12) for detailed logs.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Load saved state
|
||||||
|
chrome.storage.sync.get(['enabled', 'openaiApiKey'], function(result) {
|
||||||
|
const enabled = result.enabled || false;
|
||||||
|
document.getElementById('toggleSwitch').checked = enabled;
|
||||||
|
document.getElementById('toggleLabel').textContent = enabled ? 'On' : 'Off';
|
||||||
|
|
||||||
|
// Load API key if saved
|
||||||
|
if (result.openaiApiKey) {
|
||||||
|
document.getElementById('apiKey').value = result.openaiApiKey;
|
||||||
|
showApiStatus('API key loaded', 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle toggle change
|
||||||
|
document.getElementById('toggleSwitch').addEventListener('change', function(e) {
|
||||||
|
const enabled = e.target.checked;
|
||||||
|
|
||||||
|
// Save state
|
||||||
|
chrome.storage.sync.set({ enabled: enabled });
|
||||||
|
|
||||||
|
// Update label
|
||||||
|
document.getElementById('toggleLabel').textContent = enabled ? 'On' : 'Off';
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
document.getElementById('status').textContent = enabled ?
|
||||||
|
'Extension is now active' : 'Extension is now disabled';
|
||||||
|
|
||||||
|
// Send message to content script (with error handling)
|
||||||
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
|
if (tabs[0]) {
|
||||||
|
// Check if we can send messages to this tab
|
||||||
|
const url = tabs[0].url;
|
||||||
|
|
||||||
|
// Can't inject content scripts into chrome:// pages or other special URLs
|
||||||
|
if (url.startsWith('chrome://') || url.startsWith('chrome-extension://') || url.startsWith('edge://')) {
|
||||||
|
document.getElementById('status').textContent = 'Extension cannot run on this page';
|
||||||
|
document.getElementById('status').style.color = '#d13438';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('status').textContent = '';
|
||||||
|
document.getElementById('status').style.color = '';
|
||||||
|
}, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send message
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, {
|
||||||
|
action: 'toggleExtension',
|
||||||
|
enabled: enabled
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('Could not send message to content script:', error);
|
||||||
|
// This is okay - the content script will pick up the state from storage when it loads
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear status after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('status').textContent = '';
|
||||||
|
document.getElementById('status').style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle API key save
|
||||||
|
document.getElementById('saveApiKey').addEventListener('click', function() {
|
||||||
|
const apiKey = document.getElementById('apiKey').value.trim();
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
showApiStatus('Please enter an API key', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey.startsWith('sk-')) {
|
||||||
|
showApiStatus('Invalid API key format', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save API key
|
||||||
|
chrome.storage.sync.set({ openaiApiKey: apiKey }, function() {
|
||||||
|
showApiStatus('API key saved successfully!', 'success');
|
||||||
|
|
||||||
|
// Notify content script to reload
|
||||||
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, {
|
||||||
|
action: 'apiKeyUpdated',
|
||||||
|
apiKey: apiKey
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showApiStatus(message, type) {
|
||||||
|
const statusEl = document.getElementById('apiStatus');
|
||||||
|
statusEl.textContent = message;
|
||||||
|
statusEl.className = `api-status ${type}`;
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
statusEl.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user