// background.js — Service Worker // Handles all Gemini API calls (avoids CORS issues from content scripts) const GEMINI_MODEL = 'gemini-flash-lite-latest'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; // ─── Message listener ──────────────────────────────────────────────────────── chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'GEMINI_REQUEST') { handleGeminiRequest(message.payload) .then(result => sendResponse({ success: true, data: result })) .catch(err => sendResponse({ success: false, error: err.message })); return true; // Keep message channel open for async } }); // ─── Core API call ─────────────────────────────────────────────────────────── async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '', jobTitle = '', postText = '' }) { // Rate limit check const canProceed = await checkRateLimit(); if (!canProceed) { throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.'); } // Truncate text — 12k chars to fit list_analysis prompts const maxChars = 12000; const trimmedPrompt = prompt && prompt.length > maxChars ? prompt.substring(0, maxChars) + '\n\n[Truncated]' : prompt; const MAX_RETRIES = 3; const RETRY_DELAYS = [0, 15000, 30000]; // No delay on first attempt let lastError = ''; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { if (RETRY_DELAYS[attempt] > 0) await delay(RETRY_DELAYS[attempt]); try { const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action, apiKey: apiKey, prompt: trimmedPrompt, jobDescription: jobDescription, jobTitle: jobTitle, postText: postText }) }); if (response.ok) { const data = await response.json(); if (action === 'generatePdf') { if (!data.pdf) throw new Error('Empty PDF response from server.'); await incrementUsage(); return { pdf: data.pdf, filename: data.filename, fromCache: false }; } else if (action === 'generateComment') { const comment = data.comment; if (!comment) { console.error('[LJA-BG] Empty comment. Full response:', JSON.stringify(data).substring(0, 300)); lastError = 'Empty comment from server.'; continue; } await incrementUsage(); return { comment, fromCache: false }; } else { const text = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!text) { console.error('[LJA-BG] Empty text. Full response:', JSON.stringify(data).substring(0, 500)); lastError = 'Empty response from API. Server returned: ' + JSON.stringify(data).substring(0, 200); continue; } await incrementUsage(); return { text, fromCache: false }; } } const status = response.status; const errData = await response.json().catch(() => ({})); const errMsg = errData.error?.message || ''; if (status === 429 || status === 503) { lastError = `API quota limit (${status}): ${errMsg || 'Too many tokens'}. Waiting before retry...`; continue; } else if (status === 400) { throw new Error('Request error: ' + (errMsg || 'Bad request')); } else if (status === 403) { throw new Error('Access denied. Check your API key in settings.'); } else { throw new Error(`API Error (${status}): ${errMsg || 'Unknown'}`); } } catch (e) { if (e.message.startsWith('Request error') || e.message.startsWith('Access denied') || e.message.startsWith('API Error')) { throw e; } lastError = e.message || 'Network error'; if (attempt === MAX_RETRIES - 1) throw new Error(`Failed after ${MAX_RETRIES} attempts: ${lastError}`); } } throw new Error(`API busy. ${lastError}. Please wait 1-2 minutes and try again.`); } // ─── Rate Limiting ─────────────────────────────────────────────────────────── async function checkRateLimit() { const today = new Date().toDateString(); const data = await storageGet(['usageDate', 'usageCount']); if (data.usageDate !== today) return true; // New day, reset return (data.usageCount || 0) < 1000; } async function incrementUsage() { const today = new Date().toDateString(); const data = await storageGet(['usageDate', 'usageCount']); if (data.usageDate !== today) { await storageSet({ usageDate: today, usageCount: 1 }); } else { await storageSet({ usageCount: (data.usageCount || 0) + 1 }); } } // ─── Cache ─────────────────────────────────────────────────────────────────── async function getCached(key) { const data = await storageGet(['analysisCache']); const cache = data.analysisCache || {}; const entry = cache[key]; if (!entry) return null; // Expire after 24 hours if (Date.now() - entry.timestamp > 24 * 60 * 60 * 1000) { return null; } return entry.value; } async function setCached(key, value) { const data = await storageGet(['analysisCache']); const cache = data.analysisCache || {}; // Keep max 50 entries const keys = Object.keys(cache); if (keys.length >= 50) { // Remove oldest const oldest = keys.sort((a, b) => cache[a].timestamp - cache[b].timestamp)[0]; delete cache[oldest]; } cache[key] = { value, timestamp: Date.now() }; await storageSet({ analysisCache: cache }); } // ─── Utilities ─────────────────────────────────────────────────────────────── function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function hashString(str) { let hash = 0; for (let i = 0; i < Math.min(str.length, 200); i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } function storageGet(keys) { return new Promise(resolve => chrome.storage.local.get(keys, resolve)); } function storageSet(data) { return new Promise(resolve => chrome.storage.local.set(data, resolve)); }