306 lines
12 KiB
PHP
306 lines
12 KiB
PHP
<?php
|
|
// ============================================================================
|
|
// Dynamic ATS-Optimized CV Generator & AI Proxy (Backend)
|
|
// ============================================================================
|
|
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
http_response_code(200);
|
|
exit;
|
|
}
|
|
|
|
$autoloadPath = __DIR__ . '/vendor/autoload.php';
|
|
if (!file_exists($autoloadPath)) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Vendor folder not found. Please run 'composer install'."]);
|
|
exit;
|
|
}
|
|
|
|
require_once $autoloadPath;
|
|
use Dompdf\Dompdf;
|
|
use Dompdf\Options;
|
|
use Dotenv\Dotenv;
|
|
|
|
// Path to the .env file located outside the document root for security
|
|
// Assuming script is in /home/user/htdocs/domain.com/cv/server/
|
|
// and .env is in /home/user/
|
|
$envPath = realpath(__DIR__ . '/../../../..');
|
|
|
|
if ($envPath && file_exists($envPath . '/.env')) {
|
|
$dotenv = Dotenv::createImmutable($envPath);
|
|
$dotenv->load();
|
|
}
|
|
|
|
$rawData = file_get_contents('php://input');
|
|
$data = json_decode($rawData, true);
|
|
|
|
$action = $data['action'] ?? 'generateText';
|
|
|
|
// Prioritize API key from .env over frontend payload
|
|
$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: ($data['apiKey'] ?? '');
|
|
|
|
if (empty($apiKey)) {
|
|
http_response_code(400);
|
|
echo json_encode(["error" => "Missing apiKey. Please set GEMINI_API_KEY in .env or pass it in request."]);
|
|
exit;
|
|
}
|
|
|
|
// Standardized on gemini-flash-lite-latest due to quota limits
|
|
$model = "gemini-flash-lite-latest";
|
|
$geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey;
|
|
|
|
// ==========================================
|
|
// ACTION 1: Generate ATS PDF CV
|
|
// ==========================================
|
|
if ($action === 'generatePdf') {
|
|
$jobDescription = $data['jobDescription'] ?? '';
|
|
$template = $data['template'] ?? 'default'; // 'amman' or 'default'
|
|
|
|
// --- Amman Market Prompt (Senior Backend Engineer & Technical Lead) ---
|
|
if ($template === 'amman') {
|
|
$prompt = "You are an expert ATS CV tailor for the Jordan/Amman local tech market. Read the following job description and generate tailored content.
|
|
|
|
STRICT INTEGRITY RULES:
|
|
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
|
|
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
|
|
3. My title MUST be aligned with 'Senior Backend Engineer', 'Technical Lead', or 'Lead Backend Engineer'. NEVER use 'Solutions Architect', 'AI Developer', or 'CTO'.
|
|
|
|
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
|
|
The 'headline' should be a clean, confident title based on my TRUE skills — matching Amman market expectations (Senior/Lead Backend Engineer).
|
|
The 'summary' should open with a hook about building and scaling production systems in the MENA region, emphasizing delivery, cost optimization, and hands-on engineering. Connect my real achievements to the job requirements. Keep it professional and direct — NO academic fluff, NO 'I leverage my expertise' phrases.
|
|
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job. Prioritize backend, API, database, and infrastructure skills over GIS.
|
|
Do NOT use markdown blocks like ```json, just return raw JSON text.
|
|
|
|
Job Description:
|
|
" . substr($jobDescription, 0, 4000);
|
|
}
|
|
// --- Default Prompt (Current — Enterprise/GCC) ---
|
|
else {
|
|
$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.
|
|
|
|
STRICT INTEGRITY RULES:
|
|
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
|
|
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
|
|
3. My title MUST be aligned with 'Solutions Architect', 'Senior Backend Engineer', or 'Senior Mobile Engineer'. NEVER title me 'Senior AI Developer'.
|
|
|
|
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
|
|
The 'headline' should be a clean, confident title based on my TRUE skills.
|
|
The 'summary' MUST open with exactly this hook: 'Built two production ride-hailing platforms from zero to thousands of users, on proprietary infrastructure, in high-complexity and emerging markets.' Then use the next 2 sentences to seamlessly tie my relevant TRUE skills to the job description requirements.
|
|
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job.
|
|
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, "responseMimeType" => "application/json"]
|
|
]);
|
|
|
|
$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);
|
|
|
|
$defaultHeadline = ($template === 'amman')
|
|
? "Senior Backend Engineer & Technical Lead"
|
|
: "Solutions Architect & Technical Leader";
|
|
$headline = $parsedJson['headline'] ?? $defaultHeadline;
|
|
$summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering.";
|
|
$skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design";
|
|
|
|
$templateFile = ($template === 'amman')
|
|
? __DIR__ . '/cv_template_amman.html'
|
|
: __DIR__ . '/cv_template.html';
|
|
$html = file_get_contents($templateFile);
|
|
$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();
|
|
$rawJobTitle = $data['jobTitle'] ?? 'Job';
|
|
$safeJobTitle = preg_replace('/[^a-zA-Z0-9\-_]/', '_', $rawJobTitle);
|
|
$safeJobTitle = trim($safeJobTitle, '_');
|
|
$fileName = "Hamza_Ayed - {$safeJobTitle}.pdf";
|
|
|
|
echo json_encode([
|
|
"success" => true,
|
|
"pdf" => base64_encode($pdfOutput),
|
|
"filename" => $fileName
|
|
]);
|
|
} 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" => 8192]
|
|
]);
|
|
|
|
$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;
|
|
}
|
|
|
|
// ==========================================
|
|
// ACTION 3: Smart Comment Generator
|
|
// ==========================================
|
|
if ($action === 'generateComment') {
|
|
$postText = substr($data['postText'] ?? '', 0, 3000);
|
|
|
|
if (empty($postText)) {
|
|
http_response_code(400);
|
|
echo json_encode(["error" => "postText is required."]);
|
|
exit;
|
|
}
|
|
|
|
$promptFile = __DIR__ . '/prompts/comment_prompt.txt';
|
|
if (!file_exists($promptFile)) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Comment prompt file not found on server."]);
|
|
exit;
|
|
}
|
|
|
|
$promptTemplate = file_get_contents($promptFile);
|
|
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
|
|
|
|
$payload = json_encode([
|
|
"contents" => [["parts" => [["text" => $prompt]]]],
|
|
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 256]
|
|
]);
|
|
|
|
$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);
|
|
$commentText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
|
|
|
|
if (empty($commentText)) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Empty comment from AI."]);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode(["success" => true, "comment" => $commentText]);
|
|
exit;
|
|
}
|
|
|
|
// ==========================================
|
|
// ACTION 4: Repurpose Post Generator
|
|
// ==========================================
|
|
if ($action === 'repurposePost') {
|
|
$postText = substr($data['postText'] ?? '', 0, 3000);
|
|
|
|
if (empty($postText)) {
|
|
http_response_code(400);
|
|
echo json_encode(["error" => "postText is required."]);
|
|
exit;
|
|
}
|
|
|
|
$promptFile = __DIR__ . '/prompts/repurpose_prompt.txt';
|
|
if (!file_exists($promptFile)) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Repurpose prompt file not found on server."]);
|
|
exit;
|
|
}
|
|
|
|
$promptTemplate = file_get_contents($promptFile);
|
|
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
|
|
|
|
$payload = json_encode([
|
|
"contents" => [["parts" => [["text" => $prompt]]]],
|
|
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 800]
|
|
]);
|
|
|
|
$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);
|
|
$resultText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
|
|
|
|
if (empty($resultText)) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Empty result from AI."]);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode(["success" => true, "result" => $resultText]);
|
|
exit;
|
|
}
|