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 = ` -
Processing...
-
- `; - - // Add the animation - const style = document.createElement('style'); - style.textContent = ` - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - `; - document.head.appendChild(style); + popup.style.backgroundColor = '#202124'; + popup.style.color = '#e8eaed'; + } + + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + spinner.style.borderRadius = '50%'; + spinner.style.width = '24px'; + spinner.style.height = '24px'; + spinner.style.margin = '0 auto 12px'; + spinner.style.border = '3px solid rgba(0, 0, 0, 0.1)'; + spinner.style.borderTopColor = '#1a73e8'; + spinner.style.animation = 'spin 1s linear infinite'; + + const spinnerStyle = document.createElement('style'); + spinnerStyle.textContent = '@keyframes spin { to { transform: rotate(360deg); } }'; + document.head.appendChild(spinnerStyle); + + const message = document.createElement('div'); + message.textContent = 'Translating...'; + + popup.appendChild(spinner); + popup.appendChild(message); + + // Remove any existing popups before creating a new one + const existingPopup = document.getElementById('translationLoadingPopup'); + if (existingPopup) { + existingPopup.remove(); + } document.body.appendChild(popup); } // Function to show translation popup -function showTranslationPopup(originalText, translatedText) { - // Remove loading popup if it exists +function showTranslationPopup(original, translated) { + // Remove loading popup if exists const loadingPopup = document.getElementById('translationLoadingPopup'); if (loadingPopup) { - document.body.removeChild(loadingPopup); + loadingPopup.remove(); } - + + // Create translation popup const popup = document.createElement('div'); - popup.className = 'hinglish-popup'; + popup.id = 'translationResultPopup'; popup.style.position = 'fixed'; popup.style.zIndex = '9999'; popup.style.borderRadius = '8px'; @@ -306,276 +769,256 @@ function showTranslationPopup(originalText, translatedText) { popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; + popup.style.backgroundColor = '#ffffff'; + popup.style.color = '#202124'; - // 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'; - - popup.innerHTML = ` -
-
Original Text:
-
${originalText}
-
Translation:
-
${translatedText}
-
-
- -
- `; - } else { - popup.style.backgroundColor = '#ffffff'; - popup.style.color = '#333333'; - popup.style.border = '1px solid #ddd'; - - popup.innerHTML = ` -
-
Original Text:
-
${originalText}
-
Translation:
-
${translatedText}
-
-
- -
- `; + popup.style.backgroundColor = '#202124'; + popup.style.color = '#e8eaed'; } + + const originalHeader = document.createElement('h3'); + originalHeader.textContent = 'Original:'; + originalHeader.style.margin = '0 0 5px 0'; + originalHeader.style.fontSize = '14px'; + originalHeader.style.fontWeight = 'normal'; + originalHeader.style.color = '#5f6368'; - document.body.appendChild(popup); + const originalText = document.createElement('div'); + originalText.textContent = original; + originalText.style.marginBottom = '15px'; - // Close button functionality - const closeButton = popup.querySelector('#closePopup'); - closeButton.addEventListener('click', () => { - document.body.removeChild(popup); - }); + const translationHeader = document.createElement('h3'); + translationHeader.textContent = 'Translation:'; + translationHeader.style.margin = '0 0 5px 0'; + translationHeader.style.fontSize = '14px'; + translationHeader.style.fontWeight = 'normal'; + translationHeader.style.color = '#5f6368'; - // Hover effect for close button - closeButton.addEventListener('mouseenter', () => { - closeButton.style.background = '#0d5bc1'; - }); - closeButton.addEventListener('mouseleave', () => { - closeButton.style.background = '#1a73e8'; + const translationText = document.createElement('div'); + translationText.textContent = translated; + translationText.style.marginBottom = '15px'; + + const actions = document.createElement('div'); + actions.style.display = 'flex'; + actions.style.justifyContent = 'space-between'; + actions.style.marginTop = '15px'; + + const copyButton = document.createElement('button'); + copyButton.textContent = 'Copy Translation'; + copyButton.style.padding = '8px 12px'; + copyButton.style.backgroundColor = '#1a73e8'; + copyButton.style.color = 'white'; + copyButton.style.border = 'none'; + copyButton.style.borderRadius = '4px'; + copyButton.style.cursor = 'pointer'; + copyButton.addEventListener('click', () => { + navigator.clipboard.writeText(translated).then(() => { + const originalText = copyButton.textContent; + copyButton.textContent = 'Copied!'; + setTimeout(() => { + copyButton.textContent = originalText; + }, 2000); + }); }); - // Close when clicking outside - document.addEventListener('click', function outsideClick(e) { - if (!popup.contains(e.target)) { - document.body.removeChild(popup); - document.removeEventListener('click', outsideClick); - } + const closeButton = document.createElement('button'); + closeButton.textContent = 'Close'; + closeButton.style.padding = '8px 12px'; + closeButton.style.backgroundColor = 'transparent'; + closeButton.style.color = '#5f6368'; + closeButton.style.border = '1px solid #dadce0'; + closeButton.style.borderRadius = '4px'; + closeButton.style.cursor = 'pointer'; + closeButton.style.marginLeft = '10px'; + closeButton.addEventListener('click', () => { + popup.remove(); }); + + actions.appendChild(copyButton); + actions.appendChild(closeButton); + + popup.appendChild(originalHeader); + popup.appendChild(originalText); + popup.appendChild(translationHeader); + popup.appendChild(translationText); + popup.appendChild(actions); + + document.body.appendChild(popup); } // Function to show explanation popup -function showExplanationPopup(originalText, explanation) { - // Remove loading popup if it exists +function showExplanationPopup(original, explanation) { + // Remove loading popup if exists const loadingPopup = document.getElementById('translationLoadingPopup'); if (loadingPopup) { - document.body.removeChild(loadingPopup); + loadingPopup.remove(); } - + + // Create explanation popup const popup = document.createElement('div'); - popup.className = 'hinglish-popup'; + popup.id = 'explanationResultPopup'; popup.style.position = 'fixed'; popup.style.zIndex = '9999'; popup.style.borderRadius = '8px'; popup.style.padding = '20px'; popup.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; - popup.style.maxWidth = '500px'; + popup.style.maxWidth = '450px'; popup.style.fontFamily = 'Arial, sans-serif'; popup.style.fontSize = '14px'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; + popup.style.backgroundColor = '#ffffff'; + popup.style.color = '#202124'; + popup.style.maxHeight = '70vh'; + popup.style.overflow = 'auto'; - // 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'; - - popup.innerHTML = ` -
-
Original Text:
-
${originalText}
-
AI Explanation:
-
${explanation}
-
-
- -
- `; - } else { - popup.style.backgroundColor = '#ffffff'; - popup.style.color = '#333333'; - popup.style.border = '1px solid #ddd'; - - popup.innerHTML = ` -
-
Original Text:
-
${originalText}
-
AI Explanation:
-
${explanation}
-
-
- -
- `; + popup.style.backgroundColor = '#202124'; + popup.style.color = '#e8eaed'; } + + const originalHeader = document.createElement('h3'); + originalHeader.textContent = 'Original Text:'; + originalHeader.style.margin = '0 0 5px 0'; + originalHeader.style.fontSize = '14px'; + originalHeader.style.fontWeight = 'normal'; + originalHeader.style.color = '#5f6368'; - document.body.appendChild(popup); + const originalText = document.createElement('div'); + originalText.textContent = original; + originalText.style.marginBottom = '15px'; + originalText.style.padding = '10px'; + originalText.style.backgroundColor = 'rgba(0,0,0,0.05)'; + originalText.style.borderRadius = '4px'; - // Close button functionality - const closeButton = popup.querySelector('#closePopup'); - closeButton.addEventListener('click', () => { - document.body.removeChild(popup); - }); + const explanationHeader = document.createElement('h3'); + explanationHeader.textContent = 'Explanation:'; + explanationHeader.style.margin = '0 0 5px 0'; + explanationHeader.style.fontSize = '14px'; + explanationHeader.style.fontWeight = 'normal'; + explanationHeader.style.color = '#5f6368'; - // Hover effect for close button - closeButton.addEventListener('mouseenter', () => { - closeButton.style.background = '#0d5bc1'; - }); - closeButton.addEventListener('mouseleave', () => { - closeButton.style.background = '#1a73e8'; + const explanationText = document.createElement('div'); + explanationText.innerHTML = explanation.replace(/\n/g, '
'); + explanationText.style.marginBottom = '15px'; + explanationText.style.lineHeight = '1.5'; + + const actions = document.createElement('div'); + actions.style.display = 'flex'; + actions.style.justifyContent = 'space-between'; + actions.style.marginTop = '15px'; + + const copyButton = document.createElement('button'); + copyButton.textContent = 'Copy Explanation'; + copyButton.style.padding = '8px 12px'; + copyButton.style.backgroundColor = '#1a73e8'; + copyButton.style.color = 'white'; + copyButton.style.border = 'none'; + copyButton.style.borderRadius = '4px'; + copyButton.style.cursor = 'pointer'; + copyButton.addEventListener('click', () => { + navigator.clipboard.writeText(explanation).then(() => { + const originalText = copyButton.textContent; + copyButton.textContent = 'Copied!'; + setTimeout(() => { + copyButton.textContent = originalText; + }, 2000); + }); }); - // Close when clicking outside - document.addEventListener('click', function outsideClick(e) { - if (!popup.contains(e.target)) { - document.body.removeChild(popup); - document.removeEventListener('click', outsideClick); - } + const closeButton = document.createElement('button'); + closeButton.textContent = 'Close'; + closeButton.style.padding = '8px 12px'; + closeButton.style.backgroundColor = 'transparent'; + closeButton.style.color = '#5f6368'; + closeButton.style.border = '1px solid #dadce0'; + closeButton.style.borderRadius = '4px'; + closeButton.style.cursor = 'pointer'; + closeButton.style.marginLeft = '10px'; + closeButton.addEventListener('click', () => { + popup.remove(); }); + + actions.appendChild(copyButton); + actions.appendChild(closeButton); + + popup.appendChild(originalHeader); + popup.appendChild(originalText); + popup.appendChild(explanationHeader); + popup.appendChild(explanationText); + popup.appendChild(actions); + + document.body.appendChild(popup); } // Function to show error popup function showErrorPopup(errorMessage) { - // Remove loading popup if it exists + // Remove loading popup if exists const loadingPopup = document.getElementById('translationLoadingPopup'); if (loadingPopup) { - document.body.removeChild(loadingPopup); + loadingPopup.remove(); } - + + // Create error popup const popup = document.createElement('div'); - popup.className = 'hinglish-popup'; + popup.id = 'errorPopup'; popup.style.position = 'fixed'; popup.style.zIndex = '9999'; popup.style.borderRadius = '8px'; popup.style.padding = '20px'; popup.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; - popup.style.maxWidth = '300px'; + popup.style.maxWidth = '350px'; popup.style.fontFamily = 'Arial, sans-serif'; popup.style.fontSize = '14px'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; + popup.style.backgroundColor = '#ffffff'; + popup.style.color = '#202124'; + popup.style.border = '1px solid #f28b82'; - // 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'; - - popup.innerHTML = ` -
-
Error:
-
${errorMessage}
-
-
- -
- `; - } else { - popup.style.backgroundColor = '#ffffff'; - popup.style.color = '#333333'; - popup.style.border = '1px solid #ddd'; - - popup.innerHTML = ` -
-
Error:
-
${errorMessage}
-
-
- -
- `; + popup.style.backgroundColor = '#202124'; + popup.style.color = '#e8eaed'; } - document.body.appendChild(popup); + const errorIcon = document.createElement('div'); + errorIcon.innerHTML = '⚠'; + errorIcon.style.fontSize = '24px'; + errorIcon.style.color = '#d93025'; + errorIcon.style.marginBottom = '10px'; + + const errorTitle = document.createElement('h3'); + errorTitle.textContent = 'Error'; + errorTitle.style.margin = '0 0 10px 0'; + errorTitle.style.color = '#d93025'; - // Close button functionality - const closeButton = popup.querySelector('#closePopup'); + const errorText = document.createElement('div'); + errorText.textContent = errorMessage; + errorText.style.marginBottom = '15px'; + + const closeButton = document.createElement('button'); + closeButton.textContent = 'Close'; + closeButton.style.padding = '8px 12px'; + closeButton.style.backgroundColor = '#d93025'; + closeButton.style.color = 'white'; + closeButton.style.border = 'none'; + closeButton.style.borderRadius = '4px'; + closeButton.style.cursor = 'pointer'; + closeButton.style.width = '100%'; closeButton.addEventListener('click', () => { - document.body.removeChild(popup); + popup.remove(); }); - // Hover effect for close button - closeButton.addEventListener('mouseenter', () => { - closeButton.style.background = '#c5221f'; - }); - closeButton.addEventListener('mouseleave', () => { - closeButton.style.background = '#d93025'; - }); + popup.appendChild(errorIcon); + popup.appendChild(errorTitle); + popup.appendChild(errorText); + popup.appendChild(closeButton); - // Auto close after 5 seconds - setTimeout(() => { - if (document.body.contains(popup)) { - document.body.removeChild(popup); - } - }, 5000); -} \ No newline at end of file + document.body.appendChild(popup); +} diff --git a/content.js b/content.js index cf158c9..2c9ed01 100644 --- a/content.js +++ b/content.js @@ -41,11 +41,36 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { setTimeout(() => loadingIndicator.remove(), 2000); } } - - // Translate paragraph by paragraph + // Translate paragraph by paragraph with batch processing async function translateParagraphs() { + // Create progress indicator + const progressIndicator = document.createElement('div'); + progressIndicator.id = 'translationProgressIndicator'; + progressIndicator.style.position = 'fixed'; + progressIndicator.style.bottom = '20px'; + progressIndicator.style.right = '20px'; + progressIndicator.style.padding = '10px 15px'; + progressIndicator.style.backgroundColor = '#1a73e8'; + progressIndicator.style.color = 'white'; + progressIndicator.style.borderRadius = '20px'; + progressIndicator.style.zIndex = '9999'; + progressIndicator.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; + progressIndicator.style.display = 'flex'; + progressIndicator.style.alignItems = 'center'; + progressIndicator.style.justifyContent = 'center'; + progressIndicator.style.fontFamily = 'Arial, sans-serif'; + progressIndicator.style.transition = 'opacity 0.3s'; + + // Add progress bar + const progressText = document.createElement('div'); + progressText.textContent = 'Processing: 0%'; + progressIndicator.appendChild(progressText); + + document.body.appendChild(progressIndicator); + + // Collect paragraphs to translate const paragraphs = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, span, div'); - let translatedCount = 0; + const elementsToTranslate = []; for (let i = 0; i < paragraphs.length; i++) { const element = paragraphs[i]; @@ -53,27 +78,74 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE && !element.classList.contains('hinglish-translated')) { + elementsToTranslate.push(element); + } + } + + // Listen for progress updates + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'updateProgress' && request.progress >= 0) { + progressText.textContent = `Processing: ${request.progress}%`; + } else if (request.action === 'updateProgress' && request.progress === -1) { + // Hide progress indicator + progressIndicator.style.opacity = '0'; + setTimeout(() => progressIndicator.remove(), 300); + } + }); + + try { + // Process in batches of 5 + const batchSize = 5; + let translatedCount = 0; + + for (let i = 0; i < elementsToTranslate.length; i += batchSize) { + const batch = elementsToTranslate.slice(i, i + batchSize); + const promises = batch.map(element => { + return new Promise(async (resolve) => { + try { + const response = await chrome.runtime.sendMessage({ + action: "translateText", + text: element.textContent + }); + + if (response && !response.startsWith("Translation error:")) { + element.textContent = response; + element.classList.add('hinglish-translated'); + translatedCount++; + } + } catch (error) { + console.error('Translation error:', error); + } + resolve(); + }); + }); - const originalText = element.textContent; + await Promise.all(promises); - try { - const response = await chrome.runtime.sendMessage({ - action: "translateText", - text: originalText - }); - - if (response && response !== "Please configure your API key first") { - element.textContent = response; - element.classList.add('hinglish-translated'); - translatedCount++; - } - } catch (error) { - console.error('Translation error:', error); - } + // Update progress + const progress = Math.min(100, Math.round((i + batchSize) * 100 / elementsToTranslate.length)); + progressText.textContent = `Processing: ${progress}%`; } + + // Complete + progressText.textContent = 'Translation Complete!'; + progressIndicator.style.backgroundColor = '#0b8043'; + setTimeout(() => { + progressIndicator.style.opacity = '0'; + setTimeout(() => progressIndicator.remove(), 300); + }, 2000); + + return translatedCount; + } catch (error) { + console.error('Batch translation error:', error); + progressText.textContent = 'Translation Failed'; + progressIndicator.style.backgroundColor = '#d93025'; + setTimeout(() => { + progressIndicator.style.opacity = '0'; + setTimeout(() => progressIndicator.remove(), 300); + }, 2000); + return 0; } - - return translatedCount; } // Translate all text nodes (more aggressive approach) diff --git a/manifest.json b/manifest.json index 34da0d4..00b00f8 100644 --- a/manifest.json +++ b/manifest.json @@ -19,10 +19,8 @@ ], "host_permissions": [ "https://api.groq.com/*" - ], - "background": { - "service_worker": "background.js", - "type": "module" + ], "background": { + "service_worker": "background.js" }, "action": { "default_popup": "popup/welcome.html", @@ -31,13 +29,18 @@ "32": "icons/icon32.png", "48": "icons/icon48.png" } - }, - "content_scripts": [ + }, "content_scripts": [ { "matches": [""], "js": ["content.js"], "css": ["styles/content.css"], "run_at": "document_idle" } + ], + "web_accessible_resources": [ + { + "resources": ["utils/securityHelper.js", "utils/apiRequestManager.js"], + "matches": [""] + } ] } \ No newline at end of file diff --git a/popup/popup.css b/popup/popup.css index 36a70a5..efd1332 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -12,6 +12,8 @@ --error-bg: #fce8e6; --error-color: #d93025; --label-color: #5f6368; + --button-transition: all 0.2s ease; + --hover-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @media (prefers-color-scheme: dark) { @@ -29,6 +31,7 @@ --error-bg: #3c1a1a; --error-color: #f28b82; --label-color: #9aa0a6; + --hover-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } } @@ -70,16 +73,29 @@ h2 { display: flex; gap: 8px; margin-bottom: 10px; + max-width: 350px; /* Limit the width of the input group */ + margin-left: auto; + margin-right: auto; } .input-group input { flex-grow: 1; - padding: 8px; + padding: 8px 10px; /* Smaller vertical padding */ border: 1px solid var(--input-border); border-radius: 4px; font-family: monospace; background: var(--bg-color); color: var(--text-color); + transition: border-color 0.3s ease, box-shadow 0.3s ease; + font-size: 13px; + letter-spacing: 0.5px; + height: 20px; /* Set a fixed height */ +} + +.input-group input:focus { + outline: none; + border-color: var(--button-bg); + box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2); } .button-group { @@ -89,22 +105,33 @@ h2 { } button { - padding: 8px 16px; + padding: 6px 14px; background: var(--button-bg); color: white; border: none; border-radius: 4px; cursor: pointer; - font-size: 14px; - transition: background-color 0.2s; + font-size: 13px; + transition: var(--button-transition); + height: 32px; /* Fixed height for consistency */ } button:hover { background: var(--button-hover); + box-shadow: var(--hover-shadow); + transform: translateY(-1px); } button:active { background: var(--button-active); + transform: translateY(0); +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; + box-shadow: none; + transform: none; } #saveApiKey { @@ -131,6 +158,14 @@ button:active { .setting-group { margin-bottom: 15px; + transition: transform 0.2s ease, box-shadow 0.2s ease; + border-radius: 4px; + padding: 8px; +} + +.setting-group:hover { + background-color: rgba(26, 115, 232, 0.05); + transform: translateY(-1px); } .setting-group label { @@ -148,6 +183,18 @@ select { background: var(--bg-color); color: var(--text-color); font-size: 14px; + cursor: pointer; + transition: border-color 0.2s ease; +} + +select:hover { + border-color: var(--button-hover); +} + +select:focus { + border-color: var(--button-bg); + outline: none; + box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.3); } .status { @@ -222,4 +269,264 @@ li:last-child { transform: translate(-50%, 0); opacity: 1; } +} + +/* Add loading spinner animation */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255,255,255,0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.8s linear infinite; + margin-right: 8px; + vertical-align: text-bottom; +} + +/* Add feedback message styles */ +.success-message { + color: var(--success-color); + background: var(--success-bg); + padding: 8px; + border-radius: 4px; + margin: 8px 0; + font-size: 14px; + text-align: center; +} + +.error-message { + color: var(--error-color); + background: var(--error-bg); + padding: 8px; + border-radius: 4px; + margin: 8px 0; + font-size: 14px; + text-align: center; +} + +/* Add smooth transitions for containers */ +.api-key-container { + transition: opacity 0.3s ease, height 0.3s ease; + overflow: hidden; + background-color: var(--section-bg); + padding: 12px; + border-radius: 8px; + margin-bottom: 12px; + border: 1px solid var(--border-color); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + max-width: 360px; + margin-left: auto; + margin-right: auto; +} + +.api-key-container.hidden { + opacity: 0; + height: 0; + padding: 0; + margin: 0; +} + +/* Improve feedback for action states */ +.success-message { + color: var(--success-color); + background: var(--success-bg); + padding: 8px; + border-radius: 4px; + margin: 8px 0; + font-size: 14px; +} + +.error-message { + color: var(--error-color); + background: var(--error-bg); + padding: 8px; + border-radius: 4px; + margin: 8px 0; + font-size: 14px; +} + +/* Additional styles for API usage section */ +.api-usage-section { + margin-top: 20px; + padding: 15px; + background-color: var(--section-bg); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.usage-stats { + margin: 10px 0; +} + +.stat { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + padding: 5px 0; + border-bottom: 1px solid var(--border-color); +} + +.usage-actions { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +#lastApiStatus { + font-weight: bold; +} + +.success-status { + color: var(--success-color); +} + +.error-status { + color: var(--error-color); +} + +.session-option, .checkbox-container { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0; +} + +.checkbox-container input[type="checkbox"] { + width: 16px; + height: 16px; +} + +/* Progress bar styles */ +.progress-container { + width: 100%; + background-color: var(--input-border); + border-radius: 4px; + height: 8px; + margin: 10px 0; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background-color: var(--button-bg); + width: 0%; + transition: width 0.3s; +} + +/* Status indicators */ +.status-indicator { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + margin-left: 5px; +} + +.success-indicator { + background-color: var(--success-color); + color: #ffffff; +} + +.error-indicator { + background-color: var(--error-color); + color: #ffffff; +} + +/* Styles for welcome page session checkbox */ +.setup .session-option { + display: flex; + align-items: center; + gap: 8px; + margin: 15px 0; +} + +.setup input[type="checkbox"] { + width: 16px; + height: 16px; +} + +.setup label { + font-size: 14px; + color: var(--text-color); +} + +/* Style for the icon button */ +.icon-button { + padding: 3px 6px; + background-color: var(--section-bg); + border: 1px solid var(--input-border); + border-radius: 4px; + font-size: 11px; + cursor: pointer; + margin-left: 4px; + color: var(--button-bg); + transition: var(--button-transition); + height: 32px; /* Match the height of the input */ + min-width: 45px; +} + +.icon-button:hover { + background-color: var(--button-bg); + color: white; +} + +/* Improve API key status styling */ +.api-key-section { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 15px; +} + +.api-key-status { + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; + text-align: center; + font-weight: bold; + margin-bottom: 8px; + transition: background-color 0.3s ease; + color: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.api-key-actions { + display: flex; + gap: 8px; + justify-content: center; +} + +/* Style for the API key help link */ +.key-help { + font-size: 12px; + display: flex; + align-items: center; +} + +.save-key-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; +} + +.api-key-options { + margin: 6px 0; + font-size: 12px; +} + +.checkbox-container.compact { + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 2px; } \ No newline at end of file diff --git a/popup/popup.html b/popup/popup.html index fdc02d7..10db085 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -15,17 +15,24 @@

Hinglish Translator

- - - - -
+
+
+ + Session Only +
+
+
+
+ Get key +
+ +
+

Translation Settings

@@ -45,7 +52,24 @@

Translation Settings

- + +
+ +
+

API Usage Statistics

+
+
+ API Calls Used: + 0 +
+
+ Last Status: + - +
+
+
+ +
diff --git a/popup/popup.js b/popup/popup.js index 90955e7..a48c82d 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,146 +1,375 @@ +// Use the background script for handling API keys to avoid CSP issues + document.addEventListener('DOMContentLoaded', async () => { - // Get DOM elements - const apiKeyInput = document.getElementById('apiKey'); - const apiKeyContainer = document.getElementById('apiKeyContainer'); - const apiKeyStatus = document.getElementById('apiKeyStatus'); - const toggleApiKey = document.getElementById('toggleApiKey'); - const saveApiKey = document.getElementById('saveApiKey'); - const changeApiKey = document.getElementById('changeApiKey'); - const removeApiKey = document.getElementById('removeApiKey'); - const translationStyle = document.getElementById('translationStyle'); - const languageLevel = document.getElementById('languageLevel'); - const saveSettings = document.getElementById('saveSettings'); + // Get DOM elements + const translateBtn = document.getElementById('translateBtn'); + const translateInput = document.getElementById('translateInput'); + const translateResult = document.getElementById('translateResult'); + const explainBtn = document.getElementById('explainBtn'); + const explainInput = document.getElementById('explainInput'); + const explainResult = document.getElementById('explainResult'); + const translatePageBtn = document.getElementById('translatePageBtn'); + const apiKeyStatus = document.getElementById('apiKeyStatus'); + const apiKeyContainer = document.getElementById('apiKeyContainer'); + const changeApiKeyBtn = document.getElementById('changeApiKey'); + const removeApiKeyBtn = document.getElementById('removeApiKey'); + const apiKeyInput = document.getElementById('apiKey'); + const saveApiKeyBtn = document.getElementById('saveApiKey'); + const toggleApiKeyBtn = document.getElementById('toggleApiKey'); + const translationStyle = document.getElementById('translationStyle'); + const languageLevel = document.getElementById('languageLevel'); + const translationMode = document.getElementById('translationMode'); + const sessionOnlyCheckbox = document.getElementById('sessionOnly'); + const apiUsageCount = document.getElementById('apiCallCount'); // Match the ID in HTML + const apiUsageStatus = document.getElementById('lastApiStatus'); // Match the ID in HTML + const resetApiCountBtn = document.getElementById('resetCounter'); // Match the ID in HTML + + // Helper function to show loading state on buttons + function setButtonLoading(button, isLoading, originalText) { + if (isLoading) { + button.disabled = true; + const spinner = document.createElement('span'); + spinner.className = 'spinner'; + button.setAttribute('data-original-text', button.textContent); + button.textContent = ' ' + originalText; + button.prepend(spinner); + } else { + button.disabled = false; + button.textContent = button.getAttribute('data-original-text') || originalText; + } + } + + // Helper function for showing messages + function showMessage(message, isError = false) { + const messageElement = document.createElement('div'); + messageElement.className = isError ? 'error-message' : 'success-message'; + messageElement.textContent = message; + + // Remove any existing messages + document.querySelectorAll('.error-message, .success-message').forEach(el => el.remove()); - // Check if API key exists - const { groqApiKey } = await chrome.storage.local.get('groqApiKey'); - if (!groqApiKey) { - window.location.href = 'welcome.html'; - return; + // Add the new message at the top of the container + const container = document.querySelector('.container'); + if (container) { + container.insertBefore(messageElement, container.firstChild); + + // Auto-remove after 4 seconds + setTimeout(() => messageElement.remove(), 4000); } - - // Show API key is configured - apiKeyStatus.textContent = '✓ API Key Configured'; - apiKeyStatus.style.color = '#4CAF50'; - - // Load existing translation settings - const { translationSettings } = await chrome.storage.local.get('translationSettings'); - if (translationSettings) { - translationStyle.value = translationSettings.style || 'hinglish'; - languageLevel.value = translationSettings.level || 'balanced'; + } + // Check if API key exists + let apiKeyExists = false; + chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { + apiKeyExists = response.hasKey; + if (apiKeyExists) { + const storageTypeText = response.storageType === 'session' ? + "API key configured (Session only)" : + "API key configured"; + apiKeyStatus.textContent = storageTypeText; + apiKeyStatus.style.backgroundColor = "#0b8043"; + + // Set the checkbox to match stored preference if editing API key + if (sessionOnlyCheckbox) { + sessionOnlyCheckbox.checked = response.storageType === 'session'; + } + } else { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; } - - // Toggle API key visibility - toggleApiKey.addEventListener('click', () => { - if (apiKeyInput.type === 'password') { - apiKeyInput.type = 'text'; - toggleApiKey.textContent = '🙈'; - } else { - apiKeyInput.type = 'password'; - toggleApiKey.textContent = '👁️'; + }); + + // Load API usage stats + function updateApiUsageStats() { + chrome.runtime.sendMessage({action: "getApiUsage"}, (stats) => { + if (stats && apiUsageCount) { + apiUsageCount.textContent = `${stats.callCount || 0} calls`; + if (stats.lastStatus && apiUsageStatus) { + let statusColor = "#1a73e8"; // Default blue + let statusText = stats.lastStatus || "Unknown"; + + // Convert status to more user-friendly text + if (stats.lastStatus === "success") { + statusColor = "#0b8043"; // Green + statusText = "Success"; + } else if (stats.lastStatus === "failed") { + statusColor = "#d93025"; // Red + statusText = "Failed"; + } else if (stats.lastStatus === "retrying") { + statusColor = "#f29900"; // Orange + statusText = "Retrying..."; + } + + apiUsageStatus.textContent = statusText; + apiUsageStatus.style.color = statusColor; + } } }); + } - // Save API key - saveApiKey.addEventListener('click', async () => { - const apiKey = apiKeyInput.value.trim(); - if (!apiKey) { - showError('Please enter your API key'); - return; - } + // Initialize UI and load saved settings + async function initializeUI() { + // Load translation settings + const settings = await chrome.storage.local.get([ + 'translationStyle', + 'languageLevel', + 'translationMode', + 'sessionOnly' + ]); + + if (settings.translationStyle && translationStyle) { + translationStyle.value = settings.translationStyle; + } + + if (settings.languageLevel && languageLevel) { + languageLevel.value = settings.languageLevel; + } + + if (settings.translationMode && translationMode) { + translationMode.value = settings.translationMode; + } + + if (settings.sessionOnly !== undefined && sessionOnlyCheckbox) { + sessionOnlyCheckbox.checked = settings.sessionOnly; + } + + updateApiUsageStats(); + } + // Call initialization + initializeUI(); + + // Translation button click handler + if (translateBtn) { + translateBtn.addEventListener('click', async () => { + const text = translateInput.value.trim(); + if (!text) return; + + setButtonLoading(translateBtn, true, "Translating..."); + try { - // Save API key first - await chrome.storage.local.set({ groqApiKey: apiKey }); - - // Test the API key - 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: "You are a helpful assistant." - }, { - role: "user", - content: "Hello" - }], - model: "meta-llama/llama-4-scout-17b-16e-instruct", - temperature: 0.7, - max_tokens: 10 - }) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `API error: ${response.status}`); - } - - showSuccess('API key saved successfully'); - apiKeyInput.value = ''; - apiKeyContainer.style.display = 'none'; - apiKeyStatus.textContent = '✓ API Key Configured'; - apiKeyStatus.style.color = '#4CAF50'; + chrome.runtime.sendMessage( + {action: "translateText", text: text}, + (response) => { + setButtonLoading(translateBtn, false); + + if (typeof response === 'string') { + translateResult.textContent = response; + // Update API usage stats after successful call + updateApiUsageStats(); + } else { + translateResult.textContent = "Error: Could not translate text"; + } + } + ); } catch (error) { - console.error('API key validation error:', error); - await chrome.storage.local.remove('groqApiKey'); - showError(error.message || 'Failed to validate API key'); + setButtonLoading(translateBtn, false); + translateResult.textContent = "Error: " + error.message; } }); - - // Change API key - changeApiKey.addEventListener('click', () => { - apiKeyContainer.style.display = 'block'; - }); - - // Remove API key - removeApiKey.addEventListener('click', async () => { + } + + // Explain button click handler + if (explainBtn) { + explainBtn.addEventListener('click', async () => { + const text = explainInput.value.trim(); + if (!text) return; + + setButtonLoading(explainBtn, true, "Generating explanation..."); + try { - await chrome.storage.local.remove('groqApiKey'); - window.location.href = 'welcome.html'; + chrome.runtime.sendMessage( + {action: "explainText", text: text}, + (response) => { + setButtonLoading(explainBtn, false); + + if (typeof response === 'string') { + explainResult.textContent = response; + // Update API usage stats after successful call + updateApiUsageStats(); + } else { + explainResult.textContent = "Error: Could not generate explanation"; + } + } + ); } catch (error) { - console.error('Error removing API key:', error); - showError('Failed to remove API key'); + setButtonLoading(explainBtn, false); + explainResult.textContent = "Error: " + error.message; } }); - - // Save settings - saveSettings.addEventListener('click', async () => { + + // Add keyboard shortcut (Ctrl+Enter) for explanation + explainInput.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + explainBtn.click(); + e.preventDefault(); + } + }); + } + + // Translate page button click handler + if (translatePageBtn) { + translatePageBtn.addEventListener('click', async () => { try { - const settings = { - style: translationStyle.value, - level: languageLevel.value - }; + const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); - await chrome.storage.local.set({ translationSettings: settings }); - showSuccess('Settings saved successfully'); - } catch (error) { - console.error('Error saving settings:', error); - showError('Failed to save settings'); + if (tab) { + setButtonLoading(translatePageBtn, true, "Translating..."); + + // Send message to content script + await chrome.tabs.sendMessage(tab.id, {action: "translatePage"}); + // Update UI + setTimeout(() => { + setButtonLoading(translatePageBtn, false, "Translate This Page"); + showMessage("Page translation completed!"); + // Update API usage stats + updateApiUsageStats(); + }, 1000); + } + } catch (error) { console.error("Error translating page:", error); + setButtonLoading(translatePageBtn, false, "Translate This Page"); + showMessage("Page translation failed: " + error.message, true); } }); - }); - - // Function to show success message - function showSuccess(message) { - const successDiv = document.createElement('div'); - successDiv.className = 'success-message'; - successDiv.textContent = message; - document.body.appendChild(successDiv); - setTimeout(() => { - successDiv.remove(); - }, 3000); + } + + // Change API key button click handler + if (changeApiKeyBtn) { changeApiKeyBtn.addEventListener('click', () => { + // Use the smooth transition class + apiKeyContainer.classList.remove('hidden'); + apiKeyContainer.style.display = 'flex'; + }); + } + + // Remove API key button click handler + if (removeApiKeyBtn) { removeApiKeyBtn.addEventListener('click', async () => { + if (confirm('Are you sure you want to remove your API key?')) { + setButtonLoading(removeApiKeyBtn, true, "Removing..."); + chrome.runtime.sendMessage({action: "clearApiKey"}, (response) => { + setButtonLoading(removeApiKeyBtn, false, "Remove Key"); + if (response.success) { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; + apiKeyContainer.classList.remove('hidden'); + apiKeyContainer.style.display = 'flex'; + showMessage("API key removed successfully"); + } else { + showMessage("Failed to remove API key: " + (response.error || "Unknown error"), true); + } + }); + } + }); + } + + // Save API key button click handler + if (saveApiKeyBtn) { + saveApiKeyBtn.addEventListener('click', async () => { const apiKey = apiKeyInput.value.trim(); + + if (!apiKey) { + showMessage("Please enter an API key", true); + return; + } + + // Simple validation + if (!apiKey.startsWith('gsk_')) { + showMessage("This doesn't look like a valid Groq API key. It should start with 'gsk_'", true); + return; + } + + setButtonLoading(saveApiKeyBtn, true, "Saving..."); + + const sessionOnly = sessionOnlyCheckbox.checked; + + try { + chrome.runtime.sendMessage( + { + action: "saveApiKey", + apiKey: apiKey, + sessionOnly: sessionOnly + }, + (response) => { setButtonLoading(saveApiKeyBtn, false, "Save API Key"); + if (response.success) { + apiKeyStatus.textContent = "API key configured"; + apiKeyStatus.style.backgroundColor = "#0b8043"; + + // Use smooth transition + apiKeyContainer.classList.add('hidden'); + setTimeout(() => { + apiKeyContainer.style.display = 'none'; + apiKeyContainer.classList.remove('hidden'); + }, 300); + + apiKeyInput.value = ''; + showMessage("API key saved successfully!"); + } else { + showMessage("Failed to save API key: " + (response.error || "Unknown error"), true); + } + } + ); + } catch (error) { setButtonLoading(saveApiKeyBtn, false, "Save API Key"); + showMessage("Error saving API key: " + error.message, true); + } + }); + } + + // Toggle API key visibility button click handler + if (toggleApiKeyBtn) { toggleApiKeyBtn.addEventListener('click', () => { + if (apiKeyInput.type === "password") { + apiKeyInput.type = "text"; + toggleApiKeyBtn.textContent = "Hide"; + } else { + apiKeyInput.type = "password"; + toggleApiKeyBtn.textContent = "Show"; + } + }); + } + + // Save translation settings when they change + if (translationStyle) { translationStyle.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationStyle': translationStyle.value + }); + showMessage(`Translation style updated to: ${translationStyle.options[translationStyle.selectedIndex].text}`); + }); + } + + if (languageLevel) { languageLevel.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'languageLevel': languageLevel.value + }); + showMessage(`Language level updated to: ${languageLevel.options[languageLevel.selectedIndex].text}`); + }); + } + + if (translationMode) { translationMode.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationMode': translationMode.value + }); + showMessage(`Translation mode updated to: ${translationMode.options[translationMode.selectedIndex].text}`); + }); } - // Function to show error message - function showError(message) { - const errorDiv = document.createElement('div'); - errorDiv.className = 'error-message'; - errorDiv.textContent = message; - document.body.appendChild(errorDiv); - setTimeout(() => { - errorDiv.remove(); - }, 3000); - } \ No newline at end of file + if (sessionOnlyCheckbox) { sessionOnlyCheckbox.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'sessionOnly': sessionOnlyCheckbox.checked + }); + showMessage(`API key storage set to: ${sessionOnlyCheckbox.checked ? "Session Only" : "Persistent"}`); + }); + } + // Reset API counter + if (resetApiCountBtn) { + resetApiCountBtn.addEventListener('click', () => { + if (confirm("Reset API call counter to zero?")) { + setButtonLoading(resetApiCountBtn, true, "Resetting..."); + chrome.runtime.sendMessage({action: "resetApiCount"}, (response) => { + setButtonLoading(resetApiCountBtn, false, "Reset Counter"); + if (response && response.success) { + showMessage("API call counter reset successfully"); + } else { + showMessage("Failed to reset API call counter", true); + } + updateApiUsageStats(); + }); + } + }); + } +}); diff --git a/popup/welcome.html b/popup/welcome.html index 6b94e00..18e0333 100644 --- a/popup/welcome.html +++ b/popup/welcome.html @@ -3,16 +3,76 @@ Setup Groq API Key +

Welcome to Hinglish Translator

+ +
+

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:

+ +
+ + + (key will be deleted when browser closes) +
+ + +

Your key will be stored securely and only used for translation requests.

+

Don't have a key? Click here

diff --git a/popup/welcome.js b/popup/welcome.js index 20a7786..fe418c8 100644 --- a/popup/welcome.js +++ b/popup/welcome.js @@ -1,59 +1,155 @@ -document.addEventListener('DOMContentLoaded', async () => { - // Check if API key exists - const { groqApiKey } = await chrome.storage.local.get(['groqApiKey']); - if (groqApiKey) { - window.location.href = 'popup.html'; - return; - } +// Welcome page for the Hinglish AI Translator Extension +// Handles API key setup and validation - const apiKeyInput = document.getElementById('apiKeyInput'); - const saveButton = document.getElementById('saveApiKey'); +document.addEventListener('DOMContentLoaded', () => { + console.log("Welcome page loaded"); + + // Create error message container const errorMessage = document.createElement('div'); + errorMessage.id = 'errorMessage'; errorMessage.style.color = '#d93025'; - errorMessage.style.marginTop = '10px'; + errorMessage.style.marginTop = '15px'; + errorMessage.style.padding = '8px'; + errorMessage.style.borderRadius = '4px'; + errorMessage.style.fontWeight = '500'; + errorMessage.style.display = 'none'; document.querySelector('.setup').appendChild(errorMessage); - - saveButton.addEventListener('click', async () => { + + // Create success message container + const successMessage = document.createElement('div'); + successMessage.id = 'successMessage'; + successMessage.style.color = '#0b8043'; + successMessage.style.backgroundColor = 'rgba(11, 128, 67, 0.1)'; + successMessage.style.marginTop = '15px'; + successMessage.style.padding = '8px'; + successMessage.style.borderRadius = '4px'; + successMessage.style.fontWeight = '500'; + successMessage.style.display = 'none'; + document.querySelector('.setup').appendChild(successMessage); + + // Get form elements + const apiKeyInput = document.getElementById('apiKeyInput'); + const saveButton = document.getElementById('saveApiKey'); + const sessionOnlyCheckbox = document.getElementById('sessionOnlyStorage'); + // Check if API key exists - if it does, redirect to main popup + chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { + console.log("API key exists check:", response); + + if (response && response.hasKey === true) { + const storageTypeText = response.storageType === 'session' ? + "API key already configured! (Session only)" : + "API key already configured!"; + + successMessage.textContent = storageTypeText; + successMessage.style.display = 'block'; + + // Redirect with small delay to show the success message + setTimeout(() => { + window.location.href = 'popup.html'; + }, 1000); + } + }); + // API key save button click handler + saveButton.addEventListener('click', () => { + // Hide previous messages + errorMessage.style.display = 'none'; + successMessage.style.display = 'none'; + + // Get API key from input const apiKey = apiKeyInput.value.trim(); + + // Validate input exists if (!apiKey) { errorMessage.textContent = 'Please enter your API key'; + errorMessage.style.display = 'block'; + apiKeyInput.focus(); return; } - + + // Validate API key format + if (!apiKey.startsWith('gsk_')) { + errorMessage.textContent = 'This doesn\'t look like a valid Groq API key. It should start with "gsk_"'; + errorMessage.style.display = 'block'; + apiKeyInput.focus(); + return; + } + + // Update button state + const sessionOnly = sessionOnlyCheckbox.checked; + saveButton.disabled = true; + saveButton.textContent = 'Validating...'; + + // First attempt to validate the API key with a simple test request to Groq API + validateApiKey(apiKey).then(isValid => { + if (isValid) { + // API key is valid, save it + saveApiKey(apiKey, sessionOnly); + } else { + // API key validation failed + saveButton.disabled = false; + saveButton.textContent = 'Save Key'; + errorMessage.textContent = 'Invalid API key. Please check your key and try again.'; + errorMessage.style.display = 'block'; + } + }).catch(error => { + // Handle API validation error + saveButton.disabled = false; + saveButton.textContent = 'Save Key'; + errorMessage.textContent = 'Error validating API key: ' + error.message; + errorMessage.style.display = 'block'; + console.error('API key validation error:', error); + }); + }); + + // Function to validate API key by making a test request + async function validateApiKey(apiKey) { try { - // Save API key first - await chrome.storage.local.set({ groqApiKey: apiKey }); - - // Test the API key with a simple request - const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { - method: 'POST', + const response = await fetch('https://api.groq.com/openai/v1/models', { + method: 'GET', headers: { - 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` - }, - body: JSON.stringify({ - messages: [{ - role: "user", - content: "Hello" - }], - model: "meta-llama/llama-4-scout-17b-16e-instruct", - temperature: 0.7, - max_tokens: 10 - }) + } }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `API error: ${response.status}`); - } - - // If we get here, the API key is valid - window.location.href = 'popup.html'; + + return response.ok; } catch (error) { - console.error('API Key validation error:', error); - // Remove invalid key - await chrome.storage.local.remove(['groqApiKey']); - errorMessage.textContent = error.message || 'Invalid API key. Please try again.'; + console.error('API validation error:', error); + return false; } - }); -}); \ No newline at end of file + } + + // Function to save API key after validation + function saveApiKey(apiKey, sessionOnly) { + chrome.runtime.sendMessage( + { + action: 'saveApiKey', + apiKey: apiKey, + sessionOnly: sessionOnly + }, + (response) => { + saveButton.disabled = false; + saveButton.textContent = 'Save Key'; + + if (response && response.success) { // Show success message + const storageText = sessionOnly ? 'API key saved successfully! (Session only)' : 'API key saved successfully!'; + successMessage.textContent = storageText; + successMessage.style.display = 'block'; + + // Clear input field + apiKeyInput.value = ''; + + // Redirect to popup after short delay + setTimeout(() => { + window.location.href = 'popup.html'; + }, 1500); + } else { + // Show error message + const errorMsg = response && response.error ? response.error : 'Unknown error occurred'; + errorMessage.textContent = 'Failed to save API key: ' + errorMsg; + errorMessage.style.display = 'block'; + console.error('Error saving API key:', response); + } + } + ); + } +}); diff --git a/reload.bat b/reload.bat new file mode 100644 index 0000000..6adb512 Binary files /dev/null and b/reload.bat differ diff --git a/utils/apiRequestManager.js b/utils/apiRequestManager.js new file mode 100644 index 0000000..629c4bf --- /dev/null +++ b/utils/apiRequestManager.js @@ -0,0 +1,168 @@ +// 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 + await this.incrementApiCallCount(); + this.lastApiStatus = 'success'; + await chrome.storage.local.set({ lastApiStatus: 'success' }); + + resolve(result); + } catch (error) { + console.error('API request error:', error); + + // Check if it's a rate limiting error (HTTP 429) + if (error.status === 429 && retryCount < this.options.maxRetries) { + const nextRetry = this.requestQueue.length; + const backoffDelay = this.options.initialBackoffDelay * Math.pow(2, retryCount); + + console.log(`Rate limited. Retrying after ${backoffDelay}ms (Attempt ${retryCount + 1}/${this.options.maxRetries})`); + + setTimeout(() => { + // Re-add this request to the queue with incremented retry count + this.requestQueue.splice(nextRetry, 0, { + fn, + resolve, + reject, + retryCount: retryCount + 1 + }); + }, backoffDelay); + + this.lastApiStatus = 'rate-limited'; + } else { + this.lastApiStatus = 'error'; + await chrome.storage.local.set({ lastApiStatus: 'error' }); + reject(error); + } + } + + // Wait for throttle delay before processing next request + setTimeout(() => { + this.processQueue(); + }, this.options.throttleDelay); + } + + // Load the current API call count from storage + async loadCallCount() { + try { + const { apiCallCount = 0 } = await chrome.storage.local.get('apiCallCount'); + this.apiCallCount = apiCallCount; + + const { lastApiStatus = null } = await chrome.storage.local.get('lastApiStatus'); + this.lastApiStatus = lastApiStatus; + } catch (error) { + console.error('Error loading API call count:', error); + } + } + + // Increment the API call count + async incrementApiCallCount() { + try { + this.apiCallCount++; + await chrome.storage.local.set({ apiCallCount: this.apiCallCount }); + } catch (error) { + console.error('Error incrementing API call count:', error); + } + } + + // Reset the API call counter + async resetApiCallCount() { + try { + this.apiCallCount = 0; + await chrome.storage.local.set({ apiCallCount: 0 }); + } catch (error) { + console.error('Error resetting API call count:', error); + } + } + + // Get the current API call count + getApiCallCount() { + return this.apiCallCount; + } + + // Get the last API status + getLastApiStatus() { + return this.lastApiStatus; + } + + // Batch process an array of items with the given processor function + async batchProcess(items, processorFn, batchSize = 5) { + const results = []; + const batches = []; + + // Split items into batches + for (let i = 0; i < items.length; i += batchSize) { + batches.push(items.slice(i, i + batchSize)); + } + + // Process each batch sequentially + for (let i = 0; i < batches.length; i++) { + const batchResults = await Promise.all( + batches[i].map(item => this.addRequest(() => processorFn(item))) + ); + results.push(...batchResults); + + // Report progress after each batch + const progress = Math.min(100, Math.round((i + 1) * 100 / batches.length)); + chrome.runtime.sendMessage({ action: 'updateProgress', progress }); + } + + // Reset progress when done + chrome.runtime.sendMessage({ action: 'updateProgress', progress: 100 }); + setTimeout(() => { + chrome.runtime.sendMessage({ action: 'updateProgress', progress: -1 }); + }, 1000); + + return results; + } +} + +// Make available to other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = ApiRequestManager; +} diff --git a/utils/securityHelper.js b/utils/securityHelper.js new file mode 100644 index 0000000..1bcdd5c --- /dev/null +++ b/utils/securityHelper.js @@ -0,0 +1,92 @@ +// 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 + sessionStorage.setItem('groqApiKey', encryptedKey); + // Clear from local storage if it was stored there before + await chrome.storage.local.remove('groqApiKey'); + // Set a flag to indicate we're using session storage + await chrome.storage.local.set({ 'apiKeyInSession': true }); + } else { + // Store in Chrome's local storage for persistence + await chrome.storage.local.set({ 'groqApiKey': encryptedKey }); + // Clear session storage if it was stored there before + sessionStorage.removeItem('groqApiKey'); + // Clear the session flag + await chrome.storage.local.remove('apiKeyInSession'); + } + return true; + } catch (error) { + console.error('Error storing API key:', error); + return false; + } + } + + // Retrieve API key from storage + static async getApiKey() { + try { + // Check if we're using session storage + const { apiKeyInSession } = await chrome.storage.local.get('apiKeyInSession'); + + if (apiKeyInSession) { + // Get from session storage + const encryptedKey = sessionStorage.getItem('groqApiKey'); + if (!encryptedKey) return null; + return this.decryptApiKey(encryptedKey); + } else { + // Get from local storage + const { groqApiKey } = await chrome.storage.local.get('groqApiKey'); + if (!groqApiKey) return null; + return this.decryptApiKey(groqApiKey); + } + } catch (error) { + console.error('Error retrieving API key:', error); + return null; + } + } + + // Remove API key from all storage options + static async removeApiKey() { + try { + await chrome.storage.local.remove(['groqApiKey', 'apiKeyInSession']); + sessionStorage.removeItem('groqApiKey'); + return true; + } catch (error) { + console.error('Error removing API key:', error); + return false; + } + } +} + +// Make available to other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = SecurityHelper; +} \ No newline at end of file