From 7a8b1ea7d387321d9f03dc58cb1d7f6ee07e4d11 Mon Sep 17 00:00:00 2001 From: charan22640 Date: Mon, 16 Jun 2025 13:17:47 +0530 Subject: [PATCH 1/2] inital version --- background.js | 1108 +++++++++++++++++++++++++----------- background.js.new | 604 ++++++++++++++++++++ content.js | 112 +++- manifest.json | 15 +- popup/popup.css | 107 ++++ popup/popup.html | 30 +- popup/popup.js | 423 ++++++++++---- popup/popup.js.new | 312 ++++++++++ popup/welcome.html | 64 ++- popup/welcome.js | 181 ++++-- popup/welcome.js.new | 77 +++ reload.bat | Bin 0 -> 772 bytes utils/apiRequestManager.js | 168 ++++++ utils/securityHelper.js | 92 +++ 14 files changed, 2756 insertions(+), 537 deletions(-) create mode 100644 background.js.new create mode 100644 popup/popup.js.new create mode 100644 popup/welcome.js.new create mode 100644 reload.bat create mode 100644 utils/apiRequestManager.js create mode 100644 utils/securityHelper.js diff --git a/background.js b/background.js index 3488114..036f19c 100644 --- a/background.js +++ b/background.js @@ -1,3 +1,297 @@ +// --- 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; + } + + // 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 +308,171 @@ 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") { + SecurityHelper.hasApiKey() + .then(hasKey => { + console.log("API key check result:", hasKey); + sendResponse({ hasKey }); + }) + .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 +564,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 +623,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 +699,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 +760,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/background.js.new b/background.js.new new file mode 100644 index 0000000..49eed31 --- /dev/null +++ b/background.js.new @@ -0,0 +1,604 @@ +// Import our utility classes +importScripts('./utils/securityHelper.js'); +importScripts('./utils/apiRequestManager.js'); + +// 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({ + id: "translateToHinglish", + title: "Translate to Hinglish", + contexts: ["selection"] + }); + chrome.contextMenus.create({ + id: "explainInHinglish", + title: "Explain in Hinglish", + contexts: ["selection"] + }); +}); + +// Handle messages from content script and popup +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "translateText") { + translateText(request.text) + .then(sendResponse) + .catch(error => { + console.error("Translation error:", error); + sendResponse("Translation error: " + error.message); + }); + return true; // Required for async sendResponse + } + if (request.action === "explainText") { + explainText(request.text) + .then(sendResponse) + .catch(error => { + console.error("Explanation error:", error); + sendResponse("Explanation error: " + error.message); + }); + return true; // Required for async sendResponse + } + if (request.action === "getApiUsage") { + // Get API usage statistics + Promise.resolve({ + callCount: apiRequestManager.getApiCallCount(), + lastStatus: apiRequestManager.getLastApiStatus() + }).then(sendResponse); + return true; + } + if (request.action === "resetApiCallCount") { + apiRequestManager.resetApiCallCount() + .then(() => sendResponse({ success: true })) + .catch(error => { + console.error("Error resetting API call count:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } +}); + +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId === "translateToHinglish" && info.selectionText) { + try { + // Show loading popup + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showLoadingPopup, + args: [] + }); + + const translatedText = await translateText(info.selectionText); + + // Remove loading popup and show translation + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showTranslationPopup, + args: [info.selectionText, translatedText] + }); + } catch (error) { + console.error("Context menu translation error:", error); + // Show error in popup + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showErrorPopup, + args: [error.message] + }); + } + } else if (info.menuItemId === "explainInHinglish" && info.selectionText) { + try { + // Show loading popup + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showLoadingPopup, + args: [] + }); + + const explanation = await explainText(info.selectionText); + + // Remove loading popup and show explanation + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showExplanationPopup, + args: [info.selectionText, explanation] + }); + } catch (error) { + console.error("Context menu explanation error:", error); + // Show error in popup + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: showErrorPopup, + args: [error.message] + }); + } + } +}); + +// Function to get translation prompt based on style and level +function getTranslationPrompt(style, level) { + const prompts = { + hinglish: { + balanced: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", + moreHindi: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use more Hindi words than English. Only respond with the translated text, no explanations.", + moreEnglish: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use more English words than Hindi. Only respond with the translated text, no explanations." + }, + hindi: { + balanced: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use a balanced mix of formal and colloquial Hindi. Only respond with the translated text, no explanations.", + moreHindi: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use more formal Hindi words. Only respond with the translated text, no explanations.", + moreEnglish: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use more colloquial Hindi words. Only respond with the translated text, no explanations." + }, + roman: { + balanced: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use a balanced mix of formal and colloquial words. Only respond with the translated text, no explanations.", + moreHindi: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use more formal words. Only respond with the translated text, no explanations.", + moreEnglish: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use more colloquial words. Only respond with the translated text, no explanations." + }, + formal: { + balanced: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", + moreHindi: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use more Hindi words than English. Only respond with the translated text, no explanations.", + moreEnglish: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use more English words than Hindi. Only respond with the translated text, no explanations." + }, + casual: { + balanced: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", + moreHindi: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use more Hindi words than English. Only respond with the translated text, no explanations.", + moreEnglish: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use more English words than Hindi. Only respond with the translated text, no explanations." + } + }; + + return prompts[style][level] || prompts.hinglish.balanced; +} + +// Function to translate text using Groq API +async function translateText(text) { + try { + // Get API key using our security helper + const apiKey = await SecurityHelper.getApiKey(); + if (!apiKey) { + throw new Error("Please configure your API key first"); + } + + // 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); + + // 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; + }); + } catch (error) { + console.error("Translation error:", error); + throw error; + } +} + +// Function to explain text using Groq API +async function explainText(text) { + try { + // Get API key using our security helper + const apiKey = await SecurityHelper.getApiKey(); + if (!apiKey) { + throw new Error("Please configure your API key first"); + } + + // 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.`; + + // 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; + }); + } catch (error) { + console.error("Explanation error:", error); + throw error; + } +} + +// Function to show loading popup +function showLoadingPopup() { + const popup = document.createElement('div'); + popup.id = 'translationLoadingPopup'; + 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.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.textAlign = 'center'; + + // Check if dark mode is enabled + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + 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(original, translated) { + // Remove loading popup if exists + const loadingPopup = document.getElementById('translationLoadingPopup'); + if (loadingPopup) { + loadingPopup.remove(); + } + + // Create translation popup + const popup = document.createElement('div'); + popup.id = 'translationResultPopup'; + 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 = '400px'; + 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'; + + // Check if dark mode is enabled + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + 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'; + + const originalText = document.createElement('div'); + originalText.textContent = original; + originalText.style.marginBottom = '15px'; + + 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'; + + 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); + }); + }); + + 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(original, explanation) { + // Remove loading popup if exists + const loadingPopup = document.getElementById('translationLoadingPopup'); + if (loadingPopup) { + loadingPopup.remove(); + } + + // Create explanation popup + const popup = document.createElement('div'); + 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 = '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'; + + // Check if dark mode is enabled + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + 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'; + + 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'; + + 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'; + + 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); + }); + }); + + 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 exists + const loadingPopup = document.getElementById('translationLoadingPopup'); + if (loadingPopup) { + loadingPopup.remove(); + } + + // Create error popup + const popup = document.createElement('div'); + 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 = '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'; + + // Check if dark mode is enabled + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + popup.style.backgroundColor = '#202124'; + popup.style.color = '#e8eaed'; + } + + 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'; + + 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', () => { + popup.remove(); + }); + + popup.appendChild(errorIcon); + popup.appendChild(errorTitle); + popup.appendChild(errorText); + popup.appendChild(closeButton); + + 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..bfbedf6 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -222,4 +222,111 @@ li:last-child { transform: translate(-50%, 0); opacity: 1; } +} + +/* 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); } \ No newline at end of file diff --git a/popup/popup.html b/popup/popup.html index fdc02d7..7118c6b 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -23,9 +23,7 @@

Hinglish Translator

- - -
+

Translation Settings

@@ -45,8 +43,34 @@

Translation Settings

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

API Usage Statistics

+
+
+ API Calls Used: + 0 +
+
+ Last Status: + - +
+
+
+ +
+

How to Use

diff --git a/popup/popup.js b/popup/popup.js index 90955e7..f0537bb 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,146 +1,325 @@ +// 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'); - - // Check if API key exists - const { groqApiKey } = await chrome.storage.local.get('groqApiKey'); - if (!groqApiKey) { - window.location.href = 'welcome.html'; - return; + // 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 + + // Check if API key exists + let apiKeyExists = false; + chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { + apiKeyExists = response.hasKey; + if (apiKeyExists) { + apiKeyStatus.textContent = "API key configured"; + apiKeyStatus.style.backgroundColor = "#0b8043"; + } else { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; } + }); + // Load API usage stats + function updateApiUsageStats() { + console.log("Updating API usage stats"); + chrome.runtime.sendMessage({action: "getApiUsage"}, (stats) => { + console.log("Received API usage stats:", stats); + + if (stats && apiUsageCount) { + apiUsageCount.textContent = `${stats.callCount || 0}`; + console.log("Updated apiUsageCount:", apiUsageCount.textContent); + + if (stats.lastStatus && apiUsageStatus) { + let statusColor = "#1a73e8"; // Default blue + if (stats.lastStatus === "success") statusColor = "#0b8043"; // Green + else if (stats.lastStatus === "failed") statusColor = "#d93025"; // Red + else if (stats.lastStatus === "retrying") statusColor = "#f29900"; // Orange + + apiUsageStatus.textContent = stats.lastStatus || "Unknown"; + apiUsageStatus.style.color = statusColor; + console.log("Updated apiUsageStatus:", apiUsageStatus.textContent, statusColor); + } + } else { + console.warn("Missing stats or DOM elements:", { + stats: stats, + apiUsageCount: apiUsageCount, + apiUsageStatus: apiUsageStatus + }); + } + }); + } - // 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'; + // 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(); + } - // Toggle API key visibility - toggleApiKey.addEventListener('click', () => { - if (apiKeyInput.type === 'password') { - apiKeyInput.type = 'text'; - toggleApiKey.textContent = '🙈'; - } else { - apiKeyInput.type = 'password'; - toggleApiKey.textContent = '👁️'; + // Call initialization + initializeUI(); + + // Translation button click handler + if (translateBtn) { + translateBtn.addEventListener('click', async () => { + const text = translateInput.value.trim(); + if (!text) return; + + translateBtn.disabled = true; + translateResult.textContent = "Translating..."; + + try { + chrome.runtime.sendMessage( + {action: "translateText", text: text}, + (response) => { + translateBtn.disabled = 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) { + translateBtn.disabled = false; + translateResult.textContent = "Error: " + error.message; } }); - - // Save API key - saveApiKey.addEventListener('click', async () => { - const apiKey = apiKeyInput.value.trim(); - if (!apiKey) { - showError('Please enter your API key'); - return; + } + + // Explain button click handler + if (explainBtn) { + explainBtn.addEventListener('click', async () => { + const text = explainInput.value.trim(); + if (!text) return; + + explainBtn.disabled = true; + explainResult.textContent = "Generating explanation..."; + + try { + chrome.runtime.sendMessage( + {action: "explainText", text: text}, + (response) => { + explainBtn.disabled = 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) { + explainBtn.disabled = false; + explainResult.textContent = "Error: " + error.message; } - + }); + } + + // Translate page button click handler + if (translatePageBtn) { + translatePageBtn.addEventListener('click', async () => { try { - // Save API key first - await chrome.storage.local.set({ groqApiKey: apiKey }); + const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); - // 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}`); + if (tab) { + translatePageBtn.textContent = "Translating..."; + translatePageBtn.disabled = true; + + // Send message to content script + await chrome.tabs.sendMessage(tab.id, {action: "translatePage"}); + + // Update UI + setTimeout(() => { + translatePageBtn.textContent = "Translate This Page"; + translatePageBtn.disabled = false; + // Update API usage stats + updateApiUsageStats(); + }, 1000); } - - showSuccess('API key saved successfully'); - apiKeyInput.value = ''; - apiKeyContainer.style.display = 'none'; - apiKeyStatus.textContent = '✓ API Key Configured'; - apiKeyStatus.style.color = '#4CAF50'; } catch (error) { - console.error('API key validation error:', error); - await chrome.storage.local.remove('groqApiKey'); - showError(error.message || 'Failed to validate API key'); + console.error("Error translating page:", error); + translatePageBtn.textContent = "Translate This Page"; + translatePageBtn.disabled = false; } }); - - // Change API key - changeApiKey.addEventListener('click', () => { + } + + // Change API key button click handler + if (changeApiKeyBtn) { + changeApiKeyBtn.addEventListener('click', () => { apiKeyContainer.style.display = 'block'; }); - - // Remove API key - removeApiKey.addEventListener('click', async () => { - try { - await chrome.storage.local.remove('groqApiKey'); - window.location.href = 'welcome.html'; - } catch (error) { - console.error('Error removing API key:', error); - showError('Failed to remove API key'); + } + + // Remove API key button click handler + if (removeApiKeyBtn) { + removeApiKeyBtn.addEventListener('click', async () => { + if (confirm('Are you sure you want to remove your API key?')) { + chrome.runtime.sendMessage({action: "clearApiKey"}, (response) => { + if (response.success) { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; + apiKeyContainer.style.display = 'block'; + } else { + alert("Failed to remove API key: " + (response.error || "Unknown error")); + } + }); } }); - - // Save settings - saveSettings.addEventListener('click', async () => { + } + + // Save API key button click handler + if (saveApiKeyBtn) { + saveApiKeyBtn.addEventListener('click', async () => { + const apiKey = apiKeyInput.value.trim(); + + if (!apiKey) { + alert("Please enter an API key"); + return; + } + + // Simple validation + if (!apiKey.startsWith('gsk_')) { + alert("This doesn't look like a valid Groq API key. It should start with 'gsk_'"); + return; + } + + saveApiKeyBtn.textContent = "Saving..."; + saveApiKeyBtn.disabled = true; + + const sessionOnly = sessionOnlyCheckbox.checked; + try { - const settings = { - style: translationStyle.value, - level: languageLevel.value - }; - - await chrome.storage.local.set({ translationSettings: settings }); - showSuccess('Settings saved successfully'); + chrome.runtime.sendMessage( + { + action: "saveApiKey", + apiKey: apiKey, + sessionOnly: sessionOnly + }, + (response) => { + saveApiKeyBtn.textContent = "Save API Key"; + saveApiKeyBtn.disabled = false; + + if (response.success) { + apiKeyStatus.textContent = "API key configured"; + apiKeyStatus.style.backgroundColor = "#0b8043"; + apiKeyContainer.style.display = 'none'; + apiKeyInput.value = ''; + } else { + alert("Failed to save API key: " + (response.error || "Unknown error")); + } + } + ); } catch (error) { - console.error('Error saving settings:', error); - showError('Failed to save settings'); + saveApiKeyBtn.textContent = "Save API Key"; + saveApiKeyBtn.disabled = false; + alert("Error saving API key: " + error.message); } }); - }); - - // 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); + } + + // Toggle API key visibility button click handler + if (toggleApiKeyBtn) { + toggleApiKeyBtn.addEventListener('click', () => { + if (apiKeyInput.type === "password") { + apiKeyInput.type = "text"; + toggleApiKeyBtn.textContent = "🙈"; + } else { + apiKeyInput.type = "password"; + toggleApiKeyBtn.textContent = "👁️"; + } + }); + } + + // Save translation settings when they change + if (translationStyle) { + translationStyle.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationStyle': translationStyle.value + }); + }); + } + + if (languageLevel) { + languageLevel.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'languageLevel': languageLevel.value + }); + }); + } + + if (translationMode) { + translationMode.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationMode': translationMode.value + }); + }); } - // 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 + }); + }); + } + // Reset API counter + if (resetApiCountBtn) { + resetApiCountBtn.addEventListener('click', () => { + if (confirm("Reset API call counter to zero?")) { + chrome.runtime.sendMessage({action: "resetApiCallCount"}, (response) => { + if (response && response.success) { + console.log("API call counter reset successfully"); + } else { + console.error("Failed to reset API call counter", response && response.error); + } + updateApiUsageStats(); + }); + } + }); + } +}); diff --git a/popup/popup.js.new b/popup/popup.js.new new file mode 100644 index 0000000..92135a8 --- /dev/null +++ b/popup/popup.js.new @@ -0,0 +1,312 @@ +// Use the background script for handling API keys to avoid CSP issues + +document.addEventListener('DOMContentLoaded', async () => { + // 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('apiUsageCount'); + const apiUsageStatus = document.getElementById('apiUsageStatus'); + const resetApiCountBtn = document.getElementById('resetApiCount'); + + // Check if API key exists + let apiKeyExists = false; + chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { + apiKeyExists = response.hasKey; + if (apiKeyExists) { + apiKeyStatus.textContent = "API key configured"; + apiKeyStatus.style.backgroundColor = "#0b8043"; + } else { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; + } + }); + + // 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 + if (stats.lastStatus === "success") statusColor = "#0b8043"; // Green + else if (stats.lastStatus === "failed") statusColor = "#d93025"; // Red + else if (stats.lastStatus === "retrying") statusColor = "#f29900"; // Orange + + apiUsageStatus.textContent = stats.lastStatus || "Unknown"; + apiUsageStatus.style.color = statusColor; + } + } + }); + } + + // 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; + + translateBtn.disabled = true; + translateResult.textContent = "Translating..."; + + try { + chrome.runtime.sendMessage( + {action: "translateText", text: text}, + (response) => { + translateBtn.disabled = 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) { + translateBtn.disabled = false; + translateResult.textContent = "Error: " + error.message; + } + }); + } + + // Explain button click handler + if (explainBtn) { + explainBtn.addEventListener('click', async () => { + const text = explainInput.value.trim(); + if (!text) return; + + explainBtn.disabled = true; + explainResult.textContent = "Generating explanation..."; + + try { + chrome.runtime.sendMessage( + {action: "explainText", text: text}, + (response) => { + explainBtn.disabled = 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) { + explainBtn.disabled = false; + explainResult.textContent = "Error: " + error.message; + } + }); + } + + // Translate page button click handler + if (translatePageBtn) { + translatePageBtn.addEventListener('click', async () => { + try { + const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); + + if (tab) { + translatePageBtn.textContent = "Translating..."; + translatePageBtn.disabled = true; + + // Send message to content script + await chrome.tabs.sendMessage(tab.id, {action: "translatePage"}); + + // Update UI + setTimeout(() => { + translatePageBtn.textContent = "Translate This Page"; + translatePageBtn.disabled = false; + // Update API usage stats + updateApiUsageStats(); + }, 1000); + } + } catch (error) { + console.error("Error translating page:", error); + translatePageBtn.textContent = "Translate This Page"; + translatePageBtn.disabled = false; + } + }); + } + + // Change API key button click handler + if (changeApiKeyBtn) { + changeApiKeyBtn.addEventListener('click', () => { + apiKeyContainer.style.display = 'block'; + }); + } + + // Remove API key button click handler + if (removeApiKeyBtn) { + removeApiKeyBtn.addEventListener('click', async () => { + if (confirm('Are you sure you want to remove your API key?')) { + chrome.runtime.sendMessage({action: "clearApiKey"}, (response) => { + if (response.success) { + apiKeyStatus.textContent = "No API key configured"; + apiKeyStatus.style.backgroundColor = "#d93025"; + apiKeyContainer.style.display = 'block'; + } else { + alert("Failed to remove API key: " + (response.error || "Unknown error")); + } + }); + } + }); + } + + // Save API key button click handler + if (saveApiKeyBtn) { + saveApiKeyBtn.addEventListener('click', async () => { + const apiKey = apiKeyInput.value.trim(); + + if (!apiKey) { + alert("Please enter an API key"); + return; + } + + // Simple validation + if (!apiKey.startsWith('gsk_')) { + alert("This doesn't look like a valid Groq API key. It should start with 'gsk_'"); + return; + } + + saveApiKeyBtn.textContent = "Saving..."; + saveApiKeyBtn.disabled = true; + + const sessionOnly = sessionOnlyCheckbox.checked; + + try { + chrome.runtime.sendMessage( + { + action: "saveApiKey", + apiKey: apiKey, + sessionOnly: sessionOnly + }, + (response) => { + saveApiKeyBtn.textContent = "Save API Key"; + saveApiKeyBtn.disabled = false; + + if (response.success) { + apiKeyStatus.textContent = "API key configured"; + apiKeyStatus.style.backgroundColor = "#0b8043"; + apiKeyContainer.style.display = 'none'; + apiKeyInput.value = ''; + } else { + alert("Failed to save API key: " + (response.error || "Unknown error")); + } + } + ); + } catch (error) { + saveApiKeyBtn.textContent = "Save API Key"; + saveApiKeyBtn.disabled = false; + alert("Error saving API key: " + error.message); + } + }); + } + + // Toggle API key visibility button click handler + if (toggleApiKeyBtn) { + toggleApiKeyBtn.addEventListener('click', () => { + if (apiKeyInput.type === "password") { + apiKeyInput.type = "text"; + toggleApiKeyBtn.textContent = "🙈"; + } else { + apiKeyInput.type = "password"; + toggleApiKeyBtn.textContent = "👁️"; + } + }); + } + + // Save translation settings when they change + if (translationStyle) { + translationStyle.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationStyle': translationStyle.value + }); + }); + } + + if (languageLevel) { + languageLevel.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'languageLevel': languageLevel.value + }); + }); + } + + if (translationMode) { + translationMode.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'translationMode': translationMode.value + }); + }); + } + + if (sessionOnlyCheckbox) { + sessionOnlyCheckbox.addEventListener('change', async () => { + await chrome.storage.local.set({ + 'sessionOnly': sessionOnlyCheckbox.checked + }); + }); + } + + // Reset API counter + if (resetApiCountBtn) { + resetApiCountBtn.addEventListener('click', () => { + if (confirm("Reset API call counter to zero?")) { + chrome.runtime.sendMessage({action: "resetApiCount"}, () => { + updateApiUsageStats(); + }); + } + }); + } +}); diff --git a/popup/welcome.html b/popup/welcome.html index 6b94e00..08b6741 100644 --- a/popup/welcome.html +++ b/popup/welcome.html @@ -3,16 +3,70 @@ 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:

+ + +
+ + +
+ + + +

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

+

Don't have a key? Get one from Groq

diff --git a/popup/welcome.js b/popup/welcome.js index 20a7786..19e5d14 100644 --- a/popup/welcome.js +++ b/popup/welcome.js @@ -1,59 +1,152 @@ -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) { + successMessage.textContent = "API key already configured!"; + 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 + successMessage.textContent = 'API key saved successfully!'; + 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/popup/welcome.js.new b/popup/welcome.js.new new file mode 100644 index 0000000..25ff97b --- /dev/null +++ b/popup/welcome.js.new @@ -0,0 +1,77 @@ +// Use the background script for handling API keys to avoid CSP issues + +document.addEventListener('DOMContentLoaded', async () => { + console.log("Welcome page loaded"); + + // Check if API key exists using background service + chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { + console.log("API key exists:", response.hasKey); + + if (response.hasKey) { + window.location.href = 'popup.html'; + return; + } + }); + + const apiKeyInput = document.getElementById('apiKeyInput'); + const saveButton = document.getElementById('saveApiKey'); + const sessionOnlyCheckbox = document.getElementById('sessionOnlyStorage'); + + const errorMessage = document.createElement('div'); + errorMessage.style.color = '#d93025'; + errorMessage.style.marginTop = '10px'; + document.querySelector('.setup').appendChild(errorMessage); + + saveButton.addEventListener('click', async () => { + const apiKey = apiKeyInput.value.trim(); + if (!apiKey) { + errorMessage.textContent = 'Please enter your API key'; + return; + } + + // Simple validation - check if it looks like a Groq API key (gsk_ prefix) + if (!apiKey.startsWith('gsk_')) { + errorMessage.textContent = 'This doesn\'t look like a valid Groq API key. It should start with "gsk_"'; + return; + } + + const sessionOnly = sessionOnlyCheckbox.checked; + saveButton.disabled = true; + saveButton.textContent = 'Validating...'; + errorMessage.textContent = ''; + + try { + // Call background script to save and validate API key + chrome.runtime.sendMessage( + { + action: "saveApiKey", + apiKey: apiKey, + sessionOnly: sessionOnly + }, + (response) => { + saveButton.disabled = false; + saveButton.textContent = 'Save Key'; + + if (response.success) { + console.log('API key saved successfully'); + errorMessage.textContent = ''; + + // Redirect to the main popup + window.location.href = 'popup.html'; + } else { + errorMessage.textContent = 'Failed to save API key'; + if (response.error) { + console.error('Error detail:', response.error); + errorMessage.textContent = 'Error: ' + response.error; + } + } + } + ); + } catch (error) { + saveButton.disabled = false; + saveButton.textContent = 'Save Key'; + console.error('Error saving API key:', error); + errorMessage.textContent = 'Error: ' + error.message; + } + }); +}); diff --git a/reload.bat b/reload.bat new file mode 100644 index 0000000000000000000000000000000000000000..6adb512cf3994361770d683caec9b46b6d91aa11 GIT binary patch literal 772 zcmZva%}&EW41|3~;vM$b9su!o=?xVS91tA(0wGD0h>}DJr93_Gjklp{gQ~7})??4u z&d+yaOMb_8WVxkQTI2T@TY(enIRo(qyyDz|K49hBNDs=b1?j!v?7(6smi=)9Z(q)= zVOB(In~gNj?UamXs^!Fk&C%)LzJmkxYOh$hO4O3yfibf)eq)`U*Te2ETwp1;@uTiY}u7NjFX6+w{}8K1*3!;m}LX5z0!2#;L;u3!;f7J zl^*;>DixC|sdHnGoC|7c0zHw$y~y9V+rv(;Txag}QhOt3P|}{3RMjR+X0*Sz%Kd8# z6K)mJ)Yh&o-2|+;Z)^(EHg|Q$o-_+(WTPouySoMYn(=>`^47SrzNUST8+@r7`O(+8 Y^AKmG&f+FY7&V*Gizd{9*W7bQzaO%M$N&HU literal 0 HcmV?d00001 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 From d601c78b8d64dc2311bd61ed3e6274a9bfcbb394 Mon Sep 17 00:00:00 2001 From: charan22640 Date: Mon, 16 Jun 2025 15:24:10 +0530 Subject: [PATCH 2/2] Improved UI and removed temporary .new files --- background.js | 23 +- background.js.new | 604 ------------------------------------------- popup/popup.css | 208 ++++++++++++++- popup/popup.html | 34 +-- popup/popup.js | 202 +++++++++------ popup/popup.js.new | 312 ---------------------- popup/welcome.html | 36 +-- popup/welcome.js | 15 +- popup/welcome.js.new | 77 ------ 9 files changed, 393 insertions(+), 1118 deletions(-) delete mode 100644 background.js.new delete mode 100644 popup/popup.js.new delete mode 100644 popup/welcome.js.new diff --git a/background.js b/background.js index 036f19c..68632e1 100644 --- a/background.js +++ b/background.js @@ -49,7 +49,6 @@ class SecurityHelper { return false; } } - // Retrieve the stored API key static async getApiKey() { try { @@ -72,6 +71,17 @@ class SecurityHelper { 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() { @@ -367,13 +377,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); return true; } - - // Check if API key exists handler + // Check if API key exists handler if (request.action === "checkApiKey") { - SecurityHelper.hasApiKey() - .then(hasKey => { - console.log("API key check result:", hasKey); - sendResponse({ hasKey }); + 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); diff --git a/background.js.new b/background.js.new deleted file mode 100644 index 49eed31..0000000 --- a/background.js.new +++ /dev/null @@ -1,604 +0,0 @@ -// Import our utility classes -importScripts('./utils/securityHelper.js'); -importScripts('./utils/apiRequestManager.js'); - -// 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({ - id: "translateToHinglish", - title: "Translate to Hinglish", - contexts: ["selection"] - }); - chrome.contextMenus.create({ - id: "explainInHinglish", - title: "Explain in Hinglish", - contexts: ["selection"] - }); -}); - -// Handle messages from content script and popup -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === "translateText") { - translateText(request.text) - .then(sendResponse) - .catch(error => { - console.error("Translation error:", error); - sendResponse("Translation error: " + error.message); - }); - return true; // Required for async sendResponse - } - if (request.action === "explainText") { - explainText(request.text) - .then(sendResponse) - .catch(error => { - console.error("Explanation error:", error); - sendResponse("Explanation error: " + error.message); - }); - return true; // Required for async sendResponse - } - if (request.action === "getApiUsage") { - // Get API usage statistics - Promise.resolve({ - callCount: apiRequestManager.getApiCallCount(), - lastStatus: apiRequestManager.getLastApiStatus() - }).then(sendResponse); - return true; - } - if (request.action === "resetApiCallCount") { - apiRequestManager.resetApiCallCount() - .then(() => sendResponse({ success: true })) - .catch(error => { - console.error("Error resetting API call count:", error); - sendResponse({ success: false, error: error.message }); - }); - return true; - } -}); - -chrome.contextMenus.onClicked.addListener(async (info, tab) => { - if (info.menuItemId === "translateToHinglish" && info.selectionText) { - try { - // Show loading popup - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showLoadingPopup, - args: [] - }); - - const translatedText = await translateText(info.selectionText); - - // Remove loading popup and show translation - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showTranslationPopup, - args: [info.selectionText, translatedText] - }); - } catch (error) { - console.error("Context menu translation error:", error); - // Show error in popup - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showErrorPopup, - args: [error.message] - }); - } - } else if (info.menuItemId === "explainInHinglish" && info.selectionText) { - try { - // Show loading popup - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showLoadingPopup, - args: [] - }); - - const explanation = await explainText(info.selectionText); - - // Remove loading popup and show explanation - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showExplanationPopup, - args: [info.selectionText, explanation] - }); - } catch (error) { - console.error("Context menu explanation error:", error); - // Show error in popup - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: showErrorPopup, - args: [error.message] - }); - } - } -}); - -// Function to get translation prompt based on style and level -function getTranslationPrompt(style, level) { - const prompts = { - hinglish: { - balanced: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", - moreHindi: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use more Hindi words than English. Only respond with the translated text, no explanations.", - moreEnglish: "You are a translator that converts English text to Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural in Hinglish. Use more English words than Hindi. Only respond with the translated text, no explanations." - }, - hindi: { - balanced: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use a balanced mix of formal and colloquial Hindi. Only respond with the translated text, no explanations.", - moreHindi: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use more formal Hindi words. Only respond with the translated text, no explanations.", - moreEnglish: "You are a translator that converts English text to Hindi (Devanagari script). Keep the meaning exactly the same but make it sound natural in Hindi. Use more colloquial Hindi words. Only respond with the translated text, no explanations." - }, - roman: { - balanced: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use a balanced mix of formal and colloquial words. Only respond with the translated text, no explanations.", - moreHindi: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use more formal words. Only respond with the translated text, no explanations.", - moreEnglish: "You are a translator that converts Hindi text to Romanized Hindi (Hindi written in English letters). Keep the meaning exactly the same but make it sound natural. Use more colloquial words. Only respond with the translated text, no explanations." - }, - formal: { - balanced: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", - moreHindi: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use more Hindi words than English. Only respond with the translated text, no explanations.", - moreEnglish: "You are a translator that converts English text to formal Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound professional and formal. Use more English words than Hindi. Only respond with the translated text, no explanations." - }, - casual: { - balanced: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use a balanced mix of Hindi and English words. Only respond with the translated text, no explanations.", - moreHindi: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use more Hindi words than English. Only respond with the translated text, no explanations.", - moreEnglish: "You are a translator that converts English text to casual Hinglish (Hindi written in English letters). Keep the meaning exactly the same but make it sound casual and conversational. Use more English words than Hindi. Only respond with the translated text, no explanations." - } - }; - - return prompts[style][level] || prompts.hinglish.balanced; -} - -// Function to translate text using Groq API -async function translateText(text) { - try { - // Get API key using our security helper - const apiKey = await SecurityHelper.getApiKey(); - if (!apiKey) { - throw new Error("Please configure your API key first"); - } - - // 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); - - // 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; - }); - } catch (error) { - console.error("Translation error:", error); - throw error; - } -} - -// Function to explain text using Groq API -async function explainText(text) { - try { - // Get API key using our security helper - const apiKey = await SecurityHelper.getApiKey(); - if (!apiKey) { - throw new Error("Please configure your API key first"); - } - - // 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.`; - - // 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; - }); - } catch (error) { - console.error("Explanation error:", error); - throw error; - } -} - -// Function to show loading popup -function showLoadingPopup() { - const popup = document.createElement('div'); - popup.id = 'translationLoadingPopup'; - 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.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.textAlign = 'center'; - - // Check if dark mode is enabled - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - 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(original, translated) { - // Remove loading popup if exists - const loadingPopup = document.getElementById('translationLoadingPopup'); - if (loadingPopup) { - loadingPopup.remove(); - } - - // Create translation popup - const popup = document.createElement('div'); - popup.id = 'translationResultPopup'; - 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 = '400px'; - 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'; - - // Check if dark mode is enabled - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - 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'; - - const originalText = document.createElement('div'); - originalText.textContent = original; - originalText.style.marginBottom = '15px'; - - 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'; - - 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); - }); - }); - - 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(original, explanation) { - // Remove loading popup if exists - const loadingPopup = document.getElementById('translationLoadingPopup'); - if (loadingPopup) { - loadingPopup.remove(); - } - - // Create explanation popup - const popup = document.createElement('div'); - 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 = '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'; - - // Check if dark mode is enabled - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - 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'; - - 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'; - - 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'; - - 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); - }); - }); - - 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 exists - const loadingPopup = document.getElementById('translationLoadingPopup'); - if (loadingPopup) { - loadingPopup.remove(); - } - - // Create error popup - const popup = document.createElement('div'); - 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 = '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'; - - // Check if dark mode is enabled - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - popup.style.backgroundColor = '#202124'; - popup.style.color = '#e8eaed'; - } - - 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'; - - 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', () => { - popup.remove(); - }); - - popup.appendChild(errorIcon); - popup.appendChild(errorTitle); - popup.appendChild(errorText); - popup.appendChild(closeButton); - - document.body.appendChild(popup); -} diff --git a/popup/popup.css b/popup/popup.css index bfbedf6..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 { @@ -224,6 +271,86 @@ li:last-child { } } +/* 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; @@ -329,4 +456,77 @@ li:last-child { .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 7118c6b..10db085 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -15,15 +15,24 @@

Hinglish Translator

- - -
+
+
+ Get key +
+ +
+

Translation Settings

@@ -43,16 +52,7 @@

Translation Settings

- -
- -
- - Session Only (key will be deleted when browser closes) -
-
- - +
diff --git a/popup/popup.js b/popup/popup.js index f0537bb..a48c82d 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -15,7 +15,8 @@ document.addEventListener('DOMContentLoaded', async () => { 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 toggleApiKeyBtn = document.getElementById('toggleApiKey'); + const translationStyle = document.getElementById('translationStyle'); const languageLevel = document.getElementById('languageLevel'); const translationMode = document.getElementById('translationMode'); const sessionOnlyCheckbox = document.getElementById('sessionOnly'); @@ -23,44 +24,84 @@ document.addEventListener('DOMContentLoaded', async () => { 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()); + + // 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); + } + } // Check if API key exists let apiKeyExists = false; chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { apiKeyExists = response.hasKey; if (apiKeyExists) { - apiKeyStatus.textContent = "API key configured"; + 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"; } }); + // Load API usage stats function updateApiUsageStats() { - console.log("Updating API usage stats"); chrome.runtime.sendMessage({action: "getApiUsage"}, (stats) => { - console.log("Received API usage stats:", stats); - if (stats && apiUsageCount) { - apiUsageCount.textContent = `${stats.callCount || 0}`; - console.log("Updated apiUsageCount:", apiUsageCount.textContent); - - if (stats.lastStatus && apiUsageStatus) { + apiUsageCount.textContent = `${stats.callCount || 0} calls`; + if (stats.lastStatus && apiUsageStatus) { let statusColor = "#1a73e8"; // Default blue - if (stats.lastStatus === "success") statusColor = "#0b8043"; // Green - else if (stats.lastStatus === "failed") statusColor = "#d93025"; // Red - else if (stats.lastStatus === "retrying") statusColor = "#f29900"; // Orange + 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 = stats.lastStatus || "Unknown"; + apiUsageStatus.textContent = statusText; apiUsageStatus.style.color = statusColor; - console.log("Updated apiUsageStatus:", apiUsageStatus.textContent, statusColor); } - } else { - console.warn("Missing stats or DOM elements:", { - stats: stats, - apiUsageCount: apiUsageCount, - apiUsageStatus: apiUsageStatus - }); } }); } @@ -103,14 +144,13 @@ document.addEventListener('DOMContentLoaded', async () => { const text = translateInput.value.trim(); if (!text) return; - translateBtn.disabled = true; - translateResult.textContent = "Translating..."; + setButtonLoading(translateBtn, true, "Translating..."); try { chrome.runtime.sendMessage( {action: "translateText", text: text}, (response) => { - translateBtn.disabled = false; + setButtonLoading(translateBtn, false); if (typeof response === 'string') { translateResult.textContent = response; @@ -122,7 +162,7 @@ document.addEventListener('DOMContentLoaded', async () => { } ); } catch (error) { - translateBtn.disabled = false; + setButtonLoading(translateBtn, false); translateResult.textContent = "Error: " + error.message; } }); @@ -134,14 +174,13 @@ document.addEventListener('DOMContentLoaded', async () => { const text = explainInput.value.trim(); if (!text) return; - explainBtn.disabled = true; - explainResult.textContent = "Generating explanation..."; + setButtonLoading(explainBtn, true, "Generating explanation..."); try { chrome.runtime.sendMessage( {action: "explainText", text: text}, (response) => { - explainBtn.disabled = false; + setButtonLoading(explainBtn, false); if (typeof response === 'string') { explainResult.textContent = response; @@ -153,10 +192,18 @@ document.addEventListener('DOMContentLoaded', async () => { } ); } catch (error) { - explainBtn.disabled = false; + setButtonLoading(explainBtn, false); explainResult.textContent = "Error: " + error.message; } }); + + // 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 @@ -166,46 +213,47 @@ document.addEventListener('DOMContentLoaded', async () => { const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); if (tab) { - translatePageBtn.textContent = "Translating..."; - translatePageBtn.disabled = true; + setButtonLoading(translatePageBtn, true, "Translating..."); // Send message to content script await chrome.tabs.sendMessage(tab.id, {action: "translatePage"}); - - // Update UI + // Update UI setTimeout(() => { - translatePageBtn.textContent = "Translate This Page"; - translatePageBtn.disabled = false; + setButtonLoading(translatePageBtn, false, "Translate This Page"); + showMessage("Page translation completed!"); // Update API usage stats updateApiUsageStats(); }, 1000); } - } catch (error) { - console.error("Error translating page:", error); - translatePageBtn.textContent = "Translate This Page"; - translatePageBtn.disabled = false; + } catch (error) { console.error("Error translating page:", error); + setButtonLoading(translatePageBtn, false, "Translate This Page"); + showMessage("Page translation failed: " + error.message, true); } }); } // Change API key button click handler - if (changeApiKeyBtn) { - changeApiKeyBtn.addEventListener('click', () => { - apiKeyContainer.style.display = 'block'; + 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 (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.style.display = 'block'; + apiKeyContainer.classList.remove('hidden'); + apiKeyContainer.style.display = 'flex'; + showMessage("API key removed successfully"); } else { - alert("Failed to remove API key: " + (response.error || "Unknown error")); + showMessage("Failed to remove API key: " + (response.error || "Unknown error"), true); } }); } @@ -214,22 +262,20 @@ document.addEventListener('DOMContentLoaded', async () => { // Save API key button click handler if (saveApiKeyBtn) { - saveApiKeyBtn.addEventListener('click', async () => { - const apiKey = apiKeyInput.value.trim(); + saveApiKeyBtn.addEventListener('click', async () => { const apiKey = apiKeyInput.value.trim(); if (!apiKey) { - alert("Please enter an API key"); + showMessage("Please enter an API key", true); return; } // Simple validation if (!apiKey.startsWith('gsk_')) { - alert("This doesn't look like a valid Groq API key. It should start with 'gsk_'"); + showMessage("This doesn't look like a valid Groq API key. It should start with 'gsk_'", true); return; } - saveApiKeyBtn.textContent = "Saving..."; - saveApiKeyBtn.disabled = true; + setButtonLoading(saveApiKeyBtn, true, "Saving..."); const sessionOnly = sessionOnlyCheckbox.checked; @@ -240,82 +286,86 @@ document.addEventListener('DOMContentLoaded', async () => { apiKey: apiKey, sessionOnly: sessionOnly }, - (response) => { - saveApiKeyBtn.textContent = "Save API Key"; - saveApiKeyBtn.disabled = false; - + (response) => { setButtonLoading(saveApiKeyBtn, false, "Save API Key"); if (response.success) { apiKeyStatus.textContent = "API key configured"; apiKeyStatus.style.backgroundColor = "#0b8043"; - apiKeyContainer.style.display = 'none'; + + // 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 { - alert("Failed to save API key: " + (response.error || "Unknown error")); + showMessage("Failed to save API key: " + (response.error || "Unknown error"), true); } } ); - } catch (error) { - saveApiKeyBtn.textContent = "Save API Key"; - saveApiKeyBtn.disabled = false; - alert("Error saving API key: " + error.message); + } 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 (toggleApiKeyBtn) { toggleApiKeyBtn.addEventListener('click', () => { if (apiKeyInput.type === "password") { apiKeyInput.type = "text"; - toggleApiKeyBtn.textContent = "🙈"; + toggleApiKeyBtn.textContent = "Hide"; } else { apiKeyInput.type = "password"; - toggleApiKeyBtn.textContent = "👁️"; + toggleApiKeyBtn.textContent = "Show"; } }); } // Save translation settings when they change - if (translationStyle) { - translationStyle.addEventListener('change', async () => { + 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 () => { + 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 () => { + if (translationMode) { translationMode.addEventListener('change', async () => { await chrome.storage.local.set({ 'translationMode': translationMode.value }); + showMessage(`Translation mode updated to: ${translationMode.options[translationMode.selectedIndex].text}`); }); } - if (sessionOnlyCheckbox) { - sessionOnlyCheckbox.addEventListener('change', async () => { + 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?")) { - chrome.runtime.sendMessage({action: "resetApiCallCount"}, (response) => { + setButtonLoading(resetApiCountBtn, true, "Resetting..."); + chrome.runtime.sendMessage({action: "resetApiCount"}, (response) => { + setButtonLoading(resetApiCountBtn, false, "Reset Counter"); if (response && response.success) { - console.log("API call counter reset successfully"); + showMessage("API call counter reset successfully"); } else { - console.error("Failed to reset API call counter", response && response.error); + showMessage("Failed to reset API call counter", true); } updateApiUsageStats(); }); diff --git a/popup/popup.js.new b/popup/popup.js.new deleted file mode 100644 index 92135a8..0000000 --- a/popup/popup.js.new +++ /dev/null @@ -1,312 +0,0 @@ -// Use the background script for handling API keys to avoid CSP issues - -document.addEventListener('DOMContentLoaded', async () => { - // 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('apiUsageCount'); - const apiUsageStatus = document.getElementById('apiUsageStatus'); - const resetApiCountBtn = document.getElementById('resetApiCount'); - - // Check if API key exists - let apiKeyExists = false; - chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { - apiKeyExists = response.hasKey; - if (apiKeyExists) { - apiKeyStatus.textContent = "API key configured"; - apiKeyStatus.style.backgroundColor = "#0b8043"; - } else { - apiKeyStatus.textContent = "No API key configured"; - apiKeyStatus.style.backgroundColor = "#d93025"; - } - }); - - // 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 - if (stats.lastStatus === "success") statusColor = "#0b8043"; // Green - else if (stats.lastStatus === "failed") statusColor = "#d93025"; // Red - else if (stats.lastStatus === "retrying") statusColor = "#f29900"; // Orange - - apiUsageStatus.textContent = stats.lastStatus || "Unknown"; - apiUsageStatus.style.color = statusColor; - } - } - }); - } - - // 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; - - translateBtn.disabled = true; - translateResult.textContent = "Translating..."; - - try { - chrome.runtime.sendMessage( - {action: "translateText", text: text}, - (response) => { - translateBtn.disabled = 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) { - translateBtn.disabled = false; - translateResult.textContent = "Error: " + error.message; - } - }); - } - - // Explain button click handler - if (explainBtn) { - explainBtn.addEventListener('click', async () => { - const text = explainInput.value.trim(); - if (!text) return; - - explainBtn.disabled = true; - explainResult.textContent = "Generating explanation..."; - - try { - chrome.runtime.sendMessage( - {action: "explainText", text: text}, - (response) => { - explainBtn.disabled = 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) { - explainBtn.disabled = false; - explainResult.textContent = "Error: " + error.message; - } - }); - } - - // Translate page button click handler - if (translatePageBtn) { - translatePageBtn.addEventListener('click', async () => { - try { - const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); - - if (tab) { - translatePageBtn.textContent = "Translating..."; - translatePageBtn.disabled = true; - - // Send message to content script - await chrome.tabs.sendMessage(tab.id, {action: "translatePage"}); - - // Update UI - setTimeout(() => { - translatePageBtn.textContent = "Translate This Page"; - translatePageBtn.disabled = false; - // Update API usage stats - updateApiUsageStats(); - }, 1000); - } - } catch (error) { - console.error("Error translating page:", error); - translatePageBtn.textContent = "Translate This Page"; - translatePageBtn.disabled = false; - } - }); - } - - // Change API key button click handler - if (changeApiKeyBtn) { - changeApiKeyBtn.addEventListener('click', () => { - apiKeyContainer.style.display = 'block'; - }); - } - - // Remove API key button click handler - if (removeApiKeyBtn) { - removeApiKeyBtn.addEventListener('click', async () => { - if (confirm('Are you sure you want to remove your API key?')) { - chrome.runtime.sendMessage({action: "clearApiKey"}, (response) => { - if (response.success) { - apiKeyStatus.textContent = "No API key configured"; - apiKeyStatus.style.backgroundColor = "#d93025"; - apiKeyContainer.style.display = 'block'; - } else { - alert("Failed to remove API key: " + (response.error || "Unknown error")); - } - }); - } - }); - } - - // Save API key button click handler - if (saveApiKeyBtn) { - saveApiKeyBtn.addEventListener('click', async () => { - const apiKey = apiKeyInput.value.trim(); - - if (!apiKey) { - alert("Please enter an API key"); - return; - } - - // Simple validation - if (!apiKey.startsWith('gsk_')) { - alert("This doesn't look like a valid Groq API key. It should start with 'gsk_'"); - return; - } - - saveApiKeyBtn.textContent = "Saving..."; - saveApiKeyBtn.disabled = true; - - const sessionOnly = sessionOnlyCheckbox.checked; - - try { - chrome.runtime.sendMessage( - { - action: "saveApiKey", - apiKey: apiKey, - sessionOnly: sessionOnly - }, - (response) => { - saveApiKeyBtn.textContent = "Save API Key"; - saveApiKeyBtn.disabled = false; - - if (response.success) { - apiKeyStatus.textContent = "API key configured"; - apiKeyStatus.style.backgroundColor = "#0b8043"; - apiKeyContainer.style.display = 'none'; - apiKeyInput.value = ''; - } else { - alert("Failed to save API key: " + (response.error || "Unknown error")); - } - } - ); - } catch (error) { - saveApiKeyBtn.textContent = "Save API Key"; - saveApiKeyBtn.disabled = false; - alert("Error saving API key: " + error.message); - } - }); - } - - // Toggle API key visibility button click handler - if (toggleApiKeyBtn) { - toggleApiKeyBtn.addEventListener('click', () => { - if (apiKeyInput.type === "password") { - apiKeyInput.type = "text"; - toggleApiKeyBtn.textContent = "🙈"; - } else { - apiKeyInput.type = "password"; - toggleApiKeyBtn.textContent = "👁️"; - } - }); - } - - // Save translation settings when they change - if (translationStyle) { - translationStyle.addEventListener('change', async () => { - await chrome.storage.local.set({ - 'translationStyle': translationStyle.value - }); - }); - } - - if (languageLevel) { - languageLevel.addEventListener('change', async () => { - await chrome.storage.local.set({ - 'languageLevel': languageLevel.value - }); - }); - } - - if (translationMode) { - translationMode.addEventListener('change', async () => { - await chrome.storage.local.set({ - 'translationMode': translationMode.value - }); - }); - } - - if (sessionOnlyCheckbox) { - sessionOnlyCheckbox.addEventListener('change', async () => { - await chrome.storage.local.set({ - 'sessionOnly': sessionOnlyCheckbox.checked - }); - }); - } - - // Reset API counter - if (resetApiCountBtn) { - resetApiCountBtn.addEventListener('click', () => { - if (confirm("Reset API call counter to zero?")) { - chrome.runtime.sendMessage({action: "resetApiCount"}, () => { - updateApiUsageStats(); - }); - } - }); - } -}); diff --git a/popup/welcome.html b/popup/welcome.html index 08b6741..18e0333 100644 --- a/popup/welcome.html +++ b/popup/welcome.html @@ -22,20 +22,22 @@ font-size: 0.85rem; color: #666; margin: 10px 0; - } - input[type="password"], input[type="text"] { + } input[type="password"], input[type="text"] { width: 100%; - padding: 10px; + padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; - font-size: 1rem; - margin-bottom: 15px; + font-size: 13px; + margin-bottom: 12px; + height: 20px; } .session-option { margin: 15px 0; - } - #saveApiKey { - min-width: 120px; + } #saveApiKey { + min-width: 100px; + height: 32px; + font-size: 13px; + padding: 6px 14px; } .info-block { background-color: rgba(26, 115, 232, 0.1); @@ -44,6 +46,11 @@ margin: 15px 0; border-radius: 4px; } + .note-inline { + font-size: 0.8rem; + color: #666; + margin-left: 5px; + } @@ -56,17 +63,16 @@

Welcome to Hinglish Translator

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? Get one from Groq

+

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 19e5d14..fe418c8 100644 --- a/popup/welcome.js +++ b/popup/welcome.js @@ -31,13 +31,16 @@ document.addEventListener('DOMContentLoaded', () => { 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 + // 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) { - successMessage.textContent = "API key already configured!"; + 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 @@ -127,9 +130,9 @@ document.addEventListener('DOMContentLoaded', () => { saveButton.disabled = false; saveButton.textContent = 'Save Key'; - if (response && response.success) { - // Show success message - successMessage.textContent = 'API key saved successfully!'; + 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 diff --git a/popup/welcome.js.new b/popup/welcome.js.new deleted file mode 100644 index 25ff97b..0000000 --- a/popup/welcome.js.new +++ /dev/null @@ -1,77 +0,0 @@ -// Use the background script for handling API keys to avoid CSP issues - -document.addEventListener('DOMContentLoaded', async () => { - console.log("Welcome page loaded"); - - // Check if API key exists using background service - chrome.runtime.sendMessage({action: "checkApiKey"}, (response) => { - console.log("API key exists:", response.hasKey); - - if (response.hasKey) { - window.location.href = 'popup.html'; - return; - } - }); - - const apiKeyInput = document.getElementById('apiKeyInput'); - const saveButton = document.getElementById('saveApiKey'); - const sessionOnlyCheckbox = document.getElementById('sessionOnlyStorage'); - - const errorMessage = document.createElement('div'); - errorMessage.style.color = '#d93025'; - errorMessage.style.marginTop = '10px'; - document.querySelector('.setup').appendChild(errorMessage); - - saveButton.addEventListener('click', async () => { - const apiKey = apiKeyInput.value.trim(); - if (!apiKey) { - errorMessage.textContent = 'Please enter your API key'; - return; - } - - // Simple validation - check if it looks like a Groq API key (gsk_ prefix) - if (!apiKey.startsWith('gsk_')) { - errorMessage.textContent = 'This doesn\'t look like a valid Groq API key. It should start with "gsk_"'; - return; - } - - const sessionOnly = sessionOnlyCheckbox.checked; - saveButton.disabled = true; - saveButton.textContent = 'Validating...'; - errorMessage.textContent = ''; - - try { - // Call background script to save and validate API key - chrome.runtime.sendMessage( - { - action: "saveApiKey", - apiKey: apiKey, - sessionOnly: sessionOnly - }, - (response) => { - saveButton.disabled = false; - saveButton.textContent = 'Save Key'; - - if (response.success) { - console.log('API key saved successfully'); - errorMessage.textContent = ''; - - // Redirect to the main popup - window.location.href = 'popup.html'; - } else { - errorMessage.textContent = 'Failed to save API key'; - if (response.error) { - console.error('Error detail:', response.error); - errorMessage.textContent = 'Error: ' + response.error; - } - } - } - ); - } catch (error) { - saveButton.disabled = false; - saveButton.textContent = 'Save Key'; - console.error('Error saving API key:', error); - errorMessage.textContent = 'Error: ' + error.message; - } - }); -});