diff --git a/background.js b/background.js index 3488114..68632e1 100644 --- a/background.js +++ b/background.js @@ -1,3 +1,307 @@ +// --- BEGIN SecurityHelper --- +// Security utility for handling API key storage and encryption +class SecurityHelper { + // Simple obfuscation using base64 encoding + static encryptApiKey(apiKey) { + try { + return btoa(apiKey); + } catch (error) { + console.error('Error encrypting API key:', error); + return null; + } + } + + // Decrypt API key from storage + static decryptApiKey(encryptedKey) { + try { + return atob(encryptedKey); + } catch (error) { + console.error('Error decrypting API key:', error); + return null; + } + } + + // Store API key securely + static async storeApiKey(apiKey, sessionOnly = false) { + try { + const encryptedKey = this.encryptApiKey(apiKey); + if (!encryptedKey) { + throw new Error('Failed to encrypt API key'); + } + + if (sessionOnly) { + // Use sessionStorage for session-only storage (not available in background script) + // Mark it for session-only in storage + await chrome.storage.local.set({ + 'groqApiKey': encryptedKey, + 'apiKeyInSession': true + }); + } else { + // Store in Chrome's local storage for persistence + await chrome.storage.local.set({ + 'groqApiKey': encryptedKey, + 'apiKeyInSession': false + }); + } + return true; + } catch (error) { + console.error('Error storing API key:', error); + return false; + } + } + // Retrieve the stored API key + static async getApiKey() { + try { + // First check if we've marked the API key as session-only + const { apiKeyInSession, groqApiKey } = await chrome.storage.local.get(['apiKeyInSession', 'groqApiKey']); + + if (groqApiKey) { + return this.decryptApiKey(groqApiKey); + } + + return null; + } catch (error) { + console.error('Error retrieving API key:', error); + return null; + } + } + + // Check if the API key exists + static async hasApiKey() { + const { groqApiKey } = await chrome.storage.local.get(['groqApiKey']); + return !!groqApiKey; + } + + // Get the API key storage type (session-only or persistent) + static async getApiKeyStorageType() { + try { + const { apiKeyInSession } = await chrome.storage.local.get(['apiKeyInSession']); + return apiKeyInSession ? 'session' : 'persistent'; + } catch (error) { + console.error('Error getting API key storage type:', error); + return null; + } + } + + // Clear the stored API key + static async clearApiKey() { + try { + await chrome.storage.local.remove(['groqApiKey', 'apiKeyInSession']); + return true; + } catch (error) { + console.error('Error clearing API key:', error); + return false; + } + } +} +// --- END SecurityHelper --- + +// --- BEGIN ApiRequestManager --- +// Utility for managing API requests with throttling and rate limiting +class ApiRequestManager { + constructor(options = {}) { + // Default settings + this.options = { + throttleDelay: 500, // 500ms between requests + maxRetries: 5, + initialBackoffDelay: 1000, // 1s + ...options + }; + + this.requestQueue = []; + this.isProcessingQueue = false; + this.apiCallCount = 0; + this.lastApiStatus = null; + + // Load existing call count + this.loadCallCount(); + } + + // Add a request to the queue + async addRequest(requestFn) { + return new Promise((resolve, reject) => { + this.requestQueue.push({ + fn: requestFn, + resolve, + reject, + retryCount: 0 + }); + + if (!this.isProcessingQueue) { + this.processQueue(); + } + }); + } + + // Process the request queue with throttling + async processQueue() { + if (this.requestQueue.length === 0) { + this.isProcessingQueue = false; + return; + } + + this.isProcessingQueue = true; + const { fn, resolve, reject, retryCount } = this.requestQueue.shift(); + + try { + const result = await fn(); + + // Increment successful API call count + this.apiCallCount++; + this.lastApiStatus = 'success'; + this.saveCallCount(); + + resolve(result); + } catch (error) { + // Apply exponential backoff on failure + if (retryCount < this.options.maxRetries) { + const backoffDelay = this.options.initialBackoffDelay * Math.pow(2, retryCount); + console.log(`API request failed, retrying in ${backoffDelay}ms...`, error); + + // Push back into queue with increased retry count + this.requestQueue.unshift({ + fn, + resolve, + reject, + retryCount: retryCount + 1 + }); + + this.lastApiStatus = 'retrying'; + + setTimeout(() => { + this.processNextRequest(); + }, backoffDelay); + } else { + console.error('API request failed after max retries:', error); + this.lastApiStatus = 'failed'; + reject(error); + } + } + + // Apply throttling delay before processing the next request + setTimeout(() => { + this.processNextRequest(); + }, this.options.throttleDelay); + } + + // Process the next request in the queue + processNextRequest() { + if (this.requestQueue.length > 0) { + this.processQueue(); + } else { + this.isProcessingQueue = false; + } + } + // Process batch of requests in parallel with progress tracking + async processBatch(items, processFn, batchSize = 5, progressCallback = null) { + if (!items || !Array.isArray(items) || items.length === 0) { + console.warn("ProcessBatch called with empty or invalid items array"); + if (progressCallback) progressCallback(-1); // Signal completion with empty batch + return []; + } + + const total = items.length; + let processed = 0; + let results = []; + let errors = []; + + try { + // Process in batches + for (let i = 0; i < total; i += batchSize) { + const batch = items.slice(i, i + batchSize); + const batchPromises = batch.map(item => + this.addRequest(() => processFn(item)) + ); + + // Wait for the current batch to complete + const batchResults = await Promise.allSettled(batchPromises); + + // Update progress + processed += batch.length; + if (progressCallback) { + const progress = Math.round((processed / total) * 100); + progressCallback(progress); + } + + // Collect results and track errors + batchResults.forEach((result, index) => { + if (result.status === 'fulfilled') { + results.push(result.value); + } else { + console.error(`Error processing item ${i + index}:`, result.reason); + errors.push({ + itemIndex: i + index, + error: result.reason?.message || "Unknown error" + }); + // Add null placeholder for failed items to maintain array index correlation + results.push(null); + } + }); + + // Allow a short break between batches to avoid overwhelming the browser + await new Promise(resolve => setTimeout(resolve, 10)); + } + } catch (error) { + console.error("Batch processing encountered an error:", error); + } finally { + // Signal completion + if (progressCallback) { + progressCallback(-1); // -1 signals completion + } + + // Log a summary of errors if any occurred + if (errors.length > 0) { + console.warn(`Batch processing completed with ${errors.length} errors out of ${total} items`); + } + } + + return results; + } + + // Save the API call count to storage + async saveCallCount() { + try { + await chrome.storage.local.set({ 'apiCallCount': this.apiCallCount }); + } catch (error) { + console.error('Error saving API call count:', error); + } + } + + // Load the API call count from storage + async loadCallCount() { + try { + const { apiCallCount } = await chrome.storage.local.get(['apiCallCount']); + this.apiCallCount = apiCallCount || 0; + } catch (error) { + console.error('Error loading API call count:', error); + } + } + + // Reset API call counter + async resetApiCallCount() { + this.apiCallCount = 0; + await this.saveCallCount(); + } + + // Get the current API call count + getApiCallCount() { + return this.apiCallCount; + } + + // Get the last API status + getLastApiStatus() { + return this.lastApiStatus; + } +} +// --- END ApiRequestManager --- + +// Initialize the API request manager for throttling and rate limiting +const apiRequestManager = new ApiRequestManager({ + throttleDelay: 500, + maxRetries: 5, + initialBackoffDelay: 1000 +}); + // Handle context menu for highlighted text translation chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ @@ -14,26 +318,170 @@ chrome.runtime.onInstalled.addListener(() => { // Handle messages from content script and popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + console.log("Background received message:", request.action); + + // Translation handler if (request.action === "translateText") { translateText(request.text) - .then(sendResponse) + .then(result => { + console.log("Translation completed successfully"); + sendResponse(result); + }) .catch(error => { console.error("Translation error:", error); sendResponse("Translation error: " + error.message); }); return true; // Required for async sendResponse } + + // Explanation handler if (request.action === "explainText") { explainText(request.text) - .then(sendResponse) + .then(result => { + console.log("Explanation completed successfully"); + sendResponse(result); + }) .catch(error => { console.error("Explanation error:", error); sendResponse("Explanation error: " + error.message); }); return true; // Required for async sendResponse } + + // API usage statistics handler + if (request.action === "getApiUsage") { + try { + const stats = { + callCount: apiRequestManager.getApiCallCount(), + lastStatus: apiRequestManager.getLastApiStatus() + }; + console.log("Returning API usage stats:", stats); + sendResponse(stats); + } catch (error) { + console.error("Error getting API usage stats:", error); + sendResponse({ callCount: 0, lastStatus: "unknown", error: error.message }); + } + return true; + } + + // Reset API call counter handler + if (request.action === "resetApiCallCount") { + apiRequestManager.resetApiCallCount() + .then(() => { + console.log("API call count reset successfully"); + sendResponse({ success: true }); + }) + .catch(error => { + console.error("Error resetting API call count:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } + // Check if API key exists handler + if (request.action === "checkApiKey") { + Promise.all([SecurityHelper.hasApiKey(), SecurityHelper.getApiKeyStorageType()]) + .then(([hasKey, storageType]) => { + console.log("API key check result:", hasKey, "Storage type:", storageType); + sendResponse({ hasKey, storageType }); + }) + .catch(error => { + console.error("Error checking API key:", error); + sendResponse({ hasKey: false, error: error.message }); + }); + return true; + } + + // Save API key handler + if (request.action === "saveApiKey") { + console.log("Saving API key (session-only:", request.sessionOnly, ")"); + + if (!request.apiKey) { + console.error("Missing API key in request"); + sendResponse({ success: false, error: "No API key provided" }); + return true; + } + + SecurityHelper.storeApiKey(request.apiKey, request.sessionOnly) + .then(success => { + console.log("API key save result:", success); + if (success) { + // Mark that API key has been set + chrome.storage.local.set({ 'apiKeySet': true }); + sendResponse({ success: true }); + } else { + sendResponse({ success: false, error: "Failed to store API key" }); + } + }) + .catch(error => { + console.error("Error saving API key:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } + + // Clear API key handler + if (request.action === "clearApiKey") { + SecurityHelper.clearApiKey() + .then(success => { + console.log("API key cleared:", success); + if (success) { + chrome.storage.local.remove('apiKeySet'); + sendResponse({ success: true }); + } else { + sendResponse({ success: false, error: "Failed to clear API key" }); + } + }) + .catch(error => { + console.error("Error clearing API key:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } + + // Batch translation handler + if (request.action === "translateBatch") { + processBatchTranslation(request.texts, request.tabId) + .then(results => { + console.log("Batch translation complete", { count: results.length }); + sendResponse({ success: true, results }); + }) + .catch(error => { + console.error("Batch translation error:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } + + // Unknown action handler + console.warn("Unhandled message action:", request.action); + sendResponse({ success: false, error: "Unknown action" }); + return true; }); +// Handle batch translation for page content +async function processBatchTranslation(texts, tabId) { + if (!texts || !texts.length) { + console.warn("Empty texts array provided to batch translation"); + return []; + } + + // Process texts in batches with progress updates + return await apiRequestManager.processBatch( + texts, + translateText, + 5, + (progress) => { + // Send progress updates to content script + if (tabId) { + chrome.tabs.sendMessage(tabId, { + action: 'updateProgress', + progress: progress + }).catch(err => console.error("Error sending progress update:", err)); + } + } + ); +} + chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "translateToHinglish" && info.selectionText) { try { @@ -125,50 +573,57 @@ function getTranslationPrompt(style, level) { // Function to translate text using Groq API async function translateText(text) { - const { groqApiKey, translationSettings } = await chrome.storage.local.get(['groqApiKey', 'translationSettings']); - - if (!groqApiKey) { - throw new Error("Please configure your API key first"); - } + try { + // Get API key using our security helper + const apiKey = await SecurityHelper.getApiKey(); + if (!apiKey) { + throw new Error("Please configure your API key first"); + } - const style = translationSettings?.style || 'hinglish'; - const level = translationSettings?.level || 'balanced'; - const prompt = getTranslationPrompt(style, level); + // Get translation settings + const { translationSettings } = await chrome.storage.local.get(['translationSettings']); + const style = translationSettings?.style || 'hinglish'; + const level = translationSettings?.level || 'balanced'; + const prompt = getTranslationPrompt(style, level); - try { - const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${groqApiKey}` - }, - body: JSON.stringify({ - messages: [{ - role: "system", - content: prompt - }, { - role: "user", - content: text - }], - model: "meta-llama/llama-4-scout-17b-16e-instruct", - temperature: 0.7, - max_tokens: 1000 - }) + // Queue the API request with our request manager + return await apiRequestManager.addRequest(async () => { + const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + messages: [{ + role: "system", + content: prompt + }, { + role: "user", + content: text + }], + model: "meta-llama/llama-4-scout-17b-16e-instruct", + temperature: 0.7, + max_tokens: 1000 + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const error = new Error(errorData.error?.message || `API error: ${response.status}`); + error.status = response.status; + throw error; + } + + const data = await response.json(); + const translatedText = data.choices[0].message.content.trim(); + + if (!translatedText) { + throw new Error("Empty translation received"); + } + + return translatedText; }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `API error: ${response.status}`); - } - - const data = await response.json(); - const translatedText = data.choices[0].message.content.trim(); - - if (!translatedText) { - throw new Error("Empty translation received"); - } - - return translatedText; } catch (error) { console.error("Translation error:", error); throw error; @@ -177,54 +632,61 @@ async function translateText(text) { // Function to explain text using Groq API async function explainText(text) { - const { groqApiKey, translationSettings } = await chrome.storage.local.get(['groqApiKey', 'translationSettings']); - - if (!groqApiKey) { - throw new Error("Please configure your API key first"); - } + try { + // Get API key using our security helper + const apiKey = await SecurityHelper.getApiKey(); + if (!apiKey) { + throw new Error("Please configure your API key first"); + } - const style = translationSettings?.style || 'hinglish'; - const level = translationSettings?.level || 'balanced'; - const prompt = `You are an AI assistant that explains concepts in ${style === 'hindi' ? 'Hindi' : 'Hinglish'}. - Provide a clear and detailed explanation of the given text. - Make it easy to understand and use ${level === 'moreHindi' ? 'more Hindi words' : level === 'moreEnglish' ? 'more English words' : 'a balanced mix of Hindi and English words'}. - Format your response in a clear, structured way with bullet points or short paragraphs. - Only respond with the explanation, no additional text.`; + // Get translation settings + const { translationSettings } = await chrome.storage.local.get(['translationSettings']); + const style = translationSettings?.style || 'hinglish'; + const level = translationSettings?.level || 'balanced'; + const prompt = `You are an AI assistant that explains concepts in ${style === 'hindi' ? 'Hindi' : 'Hinglish'}. + Provide a clear and detailed explanation of the given text. + Make it easy to understand and use ${level === 'moreHindi' ? 'more Hindi words' : level === 'moreEnglish' ? 'more English words' : 'a balanced mix of Hindi and English words'}. + Format your response in a clear, structured way with bullet points or short paragraphs. + Only respond with the explanation, no additional text.`; - try { - const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${groqApiKey}` - }, - body: JSON.stringify({ - messages: [{ - role: "system", - content: prompt - }, { - role: "user", - content: text - }], - model: "meta-llama/llama-4-scout-17b-16e-instruct", - temperature: 0.7, - max_tokens: 1000 - }) + // Queue the API request with our request manager + return await apiRequestManager.addRequest(async () => { + const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + messages: [{ + role: "system", + content: prompt + }, { + role: "user", + content: text + }], + model: "meta-llama/llama-4-scout-17b-16e-instruct", + temperature: 0.7, + max_tokens: 1000 + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const error = new Error(errorData.error?.message || `API error: ${response.status}`); + error.status = response.status; + throw error; + } + + const data = await response.json(); + const explanation = data.choices[0].message.content.trim(); + + if (!explanation) { + throw new Error("Empty explanation received"); + } + + return explanation; }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `API error: ${response.status}`); - } - - const data = await response.json(); - const explanation = data.choices[0].message.content.trim(); - - if (!explanation) { - throw new Error("Empty explanation received"); - } - - return explanation; } catch (error) { console.error("Explanation error:", error); throw error; @@ -246,55 +708,56 @@ function showLoadingPopup() { popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; + popup.style.backgroundColor = '#ffffff'; + popup.style.color = '#202124'; popup.style.textAlign = 'center'; - - // Dark mode detection and styling + + // Check if dark mode is enabled if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - popup.style.backgroundColor = '#2d2d2d'; - popup.style.color = '#ffffff'; - popup.style.border = '1px solid #444'; - } else { - popup.style.backgroundColor = '#ffffff'; - popup.style.color = '#333333'; - popup.style.border = '1px solid #ddd'; - } - - popup.innerHTML = ` -
To use this extension, you need a Groq API key. Your API key allows the extension to translate text between English and Hinglish.
+Please enter your Groq API key to enable translations:
- - -Your key will be stored locally in Chrome and only used for translation requests.
-Don't have a key? Get one from Groq
+Please enter your Groq API key:
+ +Your key will be stored securely and only used for translation requests.
+Don't have a key? Click here