Auto-deploy: 2026-05-17 01:38:08
This commit is contained in:
@@ -17,59 +17,66 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
// ─── Core API call ───────────────────────────────────────────────────────────
|
// ─── Core API call ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function handleGeminiRequest({ apiKey, prompt, tab }) {
|
async function handleGeminiRequest({ apiKey, prompt, tab, action = 'generateText', jobDescription = '' }) {
|
||||||
// Rate limit check
|
// Rate limit check
|
||||||
const canProceed = await checkRateLimit();
|
const canProceed = await checkRateLimit();
|
||||||
if (!canProceed) {
|
if (!canProceed) {
|
||||||
throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.');
|
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)
|
// Check cache (keyed by tab + prompt hash)
|
||||||
const cacheKey = `cache_${tab}_${hashString(prompt)}`;
|
const cacheStr = prompt || jobDescription;
|
||||||
|
const cacheKey = `cache_${tab}_${action}_${hashString(cacheStr)}`;
|
||||||
const cached = await getCached(cacheKey);
|
const cached = await getCached(cacheKey);
|
||||||
if (cached) {
|
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)
|
// Truncate text
|
||||||
const maxPromptChars = 6000;
|
const maxChars = 6000;
|
||||||
const trimmedPrompt = prompt.length > maxPromptChars
|
const trimmedPrompt = prompt && prompt.length > maxChars
|
||||||
? prompt.substring(0, maxPromptChars) + '\n\n[Description truncated for length]'
|
? prompt.substring(0, maxChars) + '\n\n[Truncated]'
|
||||||
: prompt;
|
: prompt;
|
||||||
|
|
||||||
// Retry logic (up to 3 attempts with LONG backoff for free tier)
|
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const RETRY_DELAYS = [2000, 20000, 30000]; // 2s, 20s, 30s
|
const RETRY_DELAYS = [2000, 20000, 30000];
|
||||||
let lastError = '';
|
let lastError = '';
|
||||||
|
|
||||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
await delay(RETRY_DELAYS[attempt]);
|
await delay(RETRY_DELAYS[attempt]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
|
const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
contents: [{ parts: [{ text: trimmedPrompt }] }],
|
action: action,
|
||||||
generationConfig: {
|
apiKey: apiKey,
|
||||||
temperature: 0.7,
|
prompt: trimmedPrompt,
|
||||||
maxOutputTokens: 2048,
|
jobDescription: jobDescription
|
||||||
topP: 0.9
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
||||||
if (!text) {
|
if (action === 'generatePdf') {
|
||||||
lastError = 'Empty response from Gemini.';
|
if (!data.pdf) throw new Error('Empty PDF response from server.');
|
||||||
continue;
|
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;
|
const status = response.status;
|
||||||
|
|||||||
69
content.js
69
content.js
@@ -339,9 +339,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div id="lja-actions">
|
<div id="lja-actions" style="display: flex; gap: 8px;">
|
||||||
<button id="lja-analyze-btn">⚡ Analyze This Job</button>
|
<button id="lja-pdf-btn" style="background: #1a237e; border: 1px solid #3949ab; color: white; padding: 10px; border-radius: 6px; font-weight: 600; cursor: pointer; flex: 1; transition: 0.2s;">📥 Get ATS PDF</button>
|
||||||
<button id="lja-copy-btn" style="display:none">📋 Copy</button>
|
<button id="lja-analyze-btn" style="flex: 1.5;">⚡ Analyze Job</button>
|
||||||
|
<button id="lja-copy-btn" style="display:none; flex: 0.5;">📋 Copy</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
@@ -474,6 +475,68 @@
|
|||||||
await runAnalysis(settings, jobData, currentTab, results, root, loading, loadingText, copyBtn);
|
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
|
// ── Copy button
|
||||||
copyBtn.addEventListener('click', () => {
|
copyBtn.addEventListener('click', () => {
|
||||||
if (results[currentTab]) {
|
if (results[currentTab]) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Dynamic ATS-Optimized CV Generator (Backend)
|
// Dynamic ATS-Optimized CV Generator & AI Proxy (Backend)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Allow CORS from browser extension
|
|
||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type');
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
@@ -13,11 +12,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure composer dependencies are installed
|
|
||||||
$autoloadPath = __DIR__ . '/vendor/autoload.php';
|
$autoloadPath = __DIR__ . '/vendor/autoload.php';
|
||||||
if (!file_exists($autoloadPath)) {
|
if (!file_exists($autoloadPath)) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(["error" => "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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,24 +23,28 @@ require_once $autoloadPath;
|
|||||||
use Dompdf\Dompdf;
|
use Dompdf\Dompdf;
|
||||||
use Dompdf\Options;
|
use Dompdf\Options;
|
||||||
|
|
||||||
// 1. Get POST Data
|
|
||||||
$rawData = file_get_contents('php://input');
|
$rawData = file_get_contents('php://input');
|
||||||
$data = json_decode($rawData, true);
|
$data = json_decode($rawData, true);
|
||||||
|
|
||||||
$jobDescription = $data['jobDescription'] ?? '';
|
$action = $data['action'] ?? 'generateText';
|
||||||
$apiKey = $data['apiKey'] ?? '';
|
$apiKey = $data['apiKey'] ?? '';
|
||||||
|
|
||||||
if (empty($jobDescription) || empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Missing jobDescription or apiKey in POST payload."]);
|
echo json_encode(["error" => "Missing apiKey"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Build Gemini API Request
|
|
||||||
$model = "gemini-2.5-flash";
|
$model = "gemini-2.5-flash";
|
||||||
$geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey;
|
$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'.
|
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 '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.
|
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:
|
Job Description:
|
||||||
" . substr($jobDescription, 0, 4000);
|
" . substr($jobDescription, 0, 4000);
|
||||||
|
|
||||||
$payload = json_encode([
|
$payload = json_encode([
|
||||||
"contents" => [
|
"contents" => [["parts" => [["text" => $prompt]]]],
|
||||||
["parts" => [["text" => $prompt]]]
|
"generationConfig" => ["temperature" => 0.2, "responseMimeType" => "application/json"]
|
||||||
],
|
|
||||||
"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"
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
$ch = curl_init($geminiUrl);
|
||||||
http_response_code(500);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
echo json_encode(["error" => "PDF Generation Failed", "details" => $e->getMessage()]);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user