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;
}