Initial commit: LinkedIn Analyzer with Gemini 2.5 Flash and PHP Backend

This commit is contained in:
Hamza-Ayed
2026-05-17 01:28:17 +03:00
commit 8445affc78
18 changed files with 3323 additions and 0 deletions

176
background.js Normal file
View File

@@ -0,0 +1,176 @@
// background.js — Service Worker
// Handles all Gemini API calls (avoids CORS issues from content scripts)
const GEMINI_MODEL = 'gemini-2.5-flash';
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 }) {
// Rate limit check
const canProceed = await checkRateLimit();
if (!canProceed) {
throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.');
}
// Check cache (keyed by tab + prompt hash — different jobs produce different hashes)
const cacheKey = `cache_${tab}_${hashString(prompt)}`;
const cached = await getCached(cacheKey);
if (cached) {
return { text: cached, fromCache: true };
}
// Truncate prompt if too long (free tier has strict TPM limits)
const maxPromptChars = 6000;
const trimmedPrompt = prompt.length > maxPromptChars
? prompt.substring(0, maxPromptChars) + '\n\n[Description truncated for length]'
: prompt;
// Retry logic (up to 3 attempts with LONG backoff for free tier)
const MAX_RETRIES = 3;
const RETRY_DELAYS = [2000, 20000, 30000]; // 2s, 20s, 30s
let lastError = '';
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
await delay(RETRY_DELAYS[attempt]);
try {
const response = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: trimmedPrompt }] }],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 2048,
topP: 0.9
}
})
});
if (response.ok) {
const data = await response.json();
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
lastError = 'Empty response from Gemini.';
continue;
}
await incrementUsage();
await setCached(cacheKey, text);
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));
}