From d547decc6083ff29fb423e0e7324f695a8bc976d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 17 May 2026 01:38:08 +0300 Subject: [PATCH] Auto-deploy: 2026-05-17 01:38:08 --- background.js | 57 +++++++------ content.js | 69 +++++++++++++++- server/generate_cv.php | 183 +++++++++++++++++++++-------------------- 3 files changed, 194 insertions(+), 115 deletions(-) diff --git a/background.js b/background.js index 124da02..55a5a01 100644 --- a/background.js +++ b/background.js @@ -17,59 +17,66 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // ─── Core API call ─────────────────────────────────────────────────────────── -async function handleGeminiRequest({ apiKey, prompt, tab }) { +async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '' }) { // 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)}`; + // Check cache (keyed by tab + prompt hash) + const cacheStr = prompt || jobDescription; + const cacheKey = `cache_${tab}_${action}_${hashString(cacheStr)}`; const cached = await getCached(cacheKey); if (cached) { - return { text: cached, fromCache: true }; + return cached; // returns either text object or pdf object } - // 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]' + // Truncate text + const maxChars = 6000; + const trimmedPrompt = prompt && prompt.length > maxChars + ? prompt.substring(0, maxChars) + '\n\n[Truncated]' : 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 + const RETRY_DELAYS = [2000, 20000, 30000]; let lastError = ''; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { await delay(RETRY_DELAYS[attempt]); try { - const response = await fetch(`${GEMINI_URL}?key=${apiKey}`, { + const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - contents: [{ parts: [{ text: trimmedPrompt }] }], - generationConfig: { - temperature: 0.7, - maxOutputTokens: 2048, - topP: 0.9 - } + action: action, + apiKey: apiKey, + prompt: trimmedPrompt, + jobDescription: jobDescription }) }); 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; + + if (action === 'generatePdf') { + if (!data.pdf) throw new Error('Empty PDF response from server.'); + await incrementUsage(); + const result = { pdf: data.pdf, filename: data.filename, fromCache: false }; + await setCached(cacheKey, result); + return result; + } else { + const text = data.candidates?.[0]?.content?.parts?.[0]?.text; + if (!text) { + lastError = 'Empty response from API.'; + continue; + } + await incrementUsage(); + const result = { text, fromCache: false }; + await setCached(cacheKey, result); + return result; } - - await incrementUsage(); - await setCached(cacheKey, text); - return { text, fromCache: false }; } const status = response.status; diff --git a/content.js b/content.js index 434c5c8..cb9094c 100644 --- a/content.js +++ b/content.js @@ -339,9 +339,10 @@ -
- - +
+ + +
@@ -474,6 +475,68 @@ await runAnalysis(settings, jobData, currentTab, results, root, loading, loadingText, copyBtn); }); + // ── Generate PDF Action + const pdfBtn = root.querySelector('#lja-pdf-btn'); + if (pdfBtn) { + pdfBtn.addEventListener('click', async () => { + if (!jobData.description && !jobData.jobTitle) { + showPanelToast(root, '⚠️ No job detected.'); + return; + } + + const settings = await getSettings(); + if (!settings.apiKey) { + showPanelToast(root, '⚠️ Set your API key first!'); + return; + } + + try { + pdfBtn.textContent = '⏳ Generating...'; + pdfBtn.disabled = true; + showPanelToast(root, 'Generating ATS CV PDF via Server...', 'info'); + + const response = await new Promise(resolve => { + chrome.runtime.sendMessage({ + type: 'GEMINI_REQUEST', + payload: { + apiKey: settings.apiKey, + jobDescription: jobData.description, + action: 'generatePdf' + } + }, resolve); + }); + + if (response && response.success && response.data && response.data.pdf) { + // Convert base64 to Blob and trigger download + const binaryString = window.atob(response.data.pdf); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const blob = new Blob([bytes], { type: 'application/pdf' }); + const url = window.URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = response.data.filename || 'Hamza_Ayed_CV.pdf'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + + showPanelToast(root, '✅ PDF Downloaded Successfully!', 'success'); + } else { + throw new Error(response?.error || 'Failed to generate PDF from server'); + } + } catch (e) { + showPanelToast(root, '❌ PDF Error: ' + e.message, 'error'); + } finally { + pdfBtn.textContent = '📥 Get ATS PDF'; + pdfBtn.disabled = false; + } + }); + } + // ── Copy button copyBtn.addEventListener('click', () => { if (results[currentTab]) { diff --git a/server/generate_cv.php b/server/generate_cv.php index d03f0bb..20c844a 100644 --- a/server/generate_cv.php +++ b/server/generate_cv.php @@ -1,9 +1,8 @@ "Vendor folder not found. Please run 'composer install' in the server directory."]); + echo json_encode(["error" => "Vendor folder not found. Please run 'composer install'."]); exit; } @@ -25,24 +23,28 @@ require_once $autoloadPath; use Dompdf\Dompdf; use Dompdf\Options; -// 1. Get POST Data $rawData = file_get_contents('php://input'); $data = json_decode($rawData, true); -$jobDescription = $data['jobDescription'] ?? ''; +$action = $data['action'] ?? 'generateText'; $apiKey = $data['apiKey'] ?? ''; -if (empty($jobDescription) || empty($apiKey)) { +if (empty($apiKey)) { http_response_code(400); - echo json_encode(["error" => "Missing jobDescription or apiKey in POST payload."]); + echo json_encode(["error" => "Missing apiKey"]); exit; } -// 2. Build Gemini API Request $model = "gemini-2.5-flash"; $geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey; -$prompt = "You are an expert ATS CV tailor. Read the following job description and generate tailored content for my CV to maximize my chances of getting an interview. +// ========================================== +// ACTION 1: Generate ATS PDF CV +// ========================================== +if ($action === 'generatePdf') { + $jobDescription = $data['jobDescription'] ?? ''; + + $prompt = "You are an expert ATS CV tailor. Read the following job description and generate tailored content for my CV to maximize my chances of getting an interview. Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'. The 'headline' should be a 5-6 word professional title relevant to the job. The 'summary' should be a 3-sentence powerful paragraph highlighting skills relevant to the job. @@ -52,83 +54,90 @@ Do NOT use markdown blocks like ```json, just return raw JSON text. Job Description: " . substr($jobDescription, 0, 4000); -$payload = json_encode([ - "contents" => [ - ["parts" => [["text" => $prompt]]] - ], - "generationConfig" => [ - "temperature" => 0.2, // Low temperature for consistent JSON - "responseMimeType" => "application/json" // Force JSON output - ] -]); - -// 3. Call Google Gemini via cURL -$ch = curl_init($geminiUrl); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); -$response = curl_exec($ch); -$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); - -if ($httpCode !== 200) { - http_response_code(500); - echo json_encode(["error" => "Gemini API Error", "statusCode" => $httpCode, "details" => json_decode($response)]); - exit; -} - -// 4. Parse Gemini Response -$responseData = json_decode($response, true); -$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '{}'; - -// Clean markdown if Gemini still wrapped it in ```json ... ``` -$aiText = str_replace(['```json', '```'], '', $aiText); -$aiText = trim($aiText); - -$parsedJson = json_decode($aiText, true); - -$headline = $parsedJson['headline'] ?? "Solutions Architect & Technical Leader"; -$summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering and system architecture."; -$skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design"; - -// 5. Load HTML Template and Inject Data -// Note: Put cv_template.html in the same directory as this script on your server -$templatePath = __DIR__ . '/cv_template.html'; -if (!file_exists($templatePath)) { - http_response_code(500); - echo json_encode(["error" => "cv_template.html not found on server."]); - exit; -} - -$html = file_get_contents($templatePath); -$html = str_replace('{{JOB_HEADLINE}}', htmlspecialchars($headline), $html); -$html = str_replace('{{TAILORED_SUMMARY}}', htmlspecialchars($summary), $html); -$html = str_replace('{{DYNAMIC_SKILLS}}', htmlspecialchars($skills), $html); - -// 6. Generate PDF via Dompdf -try { - $options = new Options(); - $options->set('isHtml5ParserEnabled', true); - $options->set('defaultFont', 'Helvetica'); - $options->set('isRemoteEnabled', true); - - $dompdf = new Dompdf($options); - $dompdf->loadHtml($html); - $dompdf->setPaper('A4', 'portrait'); - $dompdf->render(); - - // 7. Return PDF as Base64 encoded string to the frontend - $pdfOutput = $dompdf->output(); - $base64Pdf = base64_encode($pdfOutput); - - echo json_encode([ - "success" => true, - "pdf" => $base64Pdf, - "filename" => "Hamza_Ayed_Tailored_CV.pdf" + $payload = json_encode([ + "contents" => [["parts" => [["text" => $prompt]]]], + "generationConfig" => ["temperature" => 0.2, "responseMimeType" => "application/json"] ]); -} catch (Exception $e) { - http_response_code(500); - echo json_encode(["error" => "PDF Generation Failed", "details" => $e->getMessage()]); + $ch = curl_init($geminiUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + http_response_code(500); + echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]); + exit; + } + + $responseData = json_decode($response, true); + $aiText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '{}'; + $aiText = str_replace(['```json', '```'], '', $aiText); + $parsedJson = json_decode(trim($aiText), true); + + $headline = $parsedJson['headline'] ?? "Solutions Architect & Technical Leader"; + $summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering."; + $skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design"; + + $templatePath = __DIR__ . '/cv_template.html'; + $html = file_get_contents($templatePath); + $html = str_replace('{{JOB_HEADLINE}}', htmlspecialchars($headline), $html); + $html = str_replace('{{TAILORED_SUMMARY}}', htmlspecialchars($summary), $html); + $html = str_replace('{{DYNAMIC_SKILLS}}', htmlspecialchars($skills), $html); + + try { + $options = new Options(); + $options->set('isHtml5ParserEnabled', true); + $options->set('defaultFont', 'Helvetica'); + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->render(); + + $pdfOutput = $dompdf->output(); + echo json_encode([ + "success" => true, + "pdf" => base64_encode($pdfOutput), + "filename" => "Tailored_CV.pdf" + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(["error" => "PDF Generation Failed", "details" => $e->getMessage()]); + } + exit; +} + +// ========================================== +// ACTION 2: Standard Proxy (Text generation) +// ========================================== +if ($action === 'generateText') { + $prompt = $data['prompt'] ?? ''; + + $payload = json_encode([ + "contents" => [["parts" => [["text" => $prompt]]]], + "generationConfig" => ["temperature" => 0.7, "maxOutputTokens" => 2048] + ]); + + $ch = curl_init($geminiUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + http_response_code($httpCode); + echo $response; + exit; + } + + // Pass the exact Gemini response back to the extension + echo $response; + exit; }