Auto-deploy: 2026-08-05 16:17:53

This commit is contained in:
Hamza-Ayed
2026-08-05 16:17:53 +03:00
parent 83506f00a2
commit 731ced99d1
20 changed files with 1772 additions and 1150 deletions
+7
View File
@@ -1 +1,8 @@
GEMINI_API_KEY="your_api_key_here"
# Model overrides (optional).
# CV tailoring decides whether an application lands, so it uses the stronger
# model. Comments and post rewrites are high-volume, so they stay on the cheap one.
GEMINI_MODEL_CV="gemini-flash-latest"
GEMINI_MODEL_TEXT="gemini-flash-latest"
GEMINI_MODEL_LIGHT="gemini-flash-lite-latest"
+83 -177
View File
@@ -3,206 +3,112 @@
<head>
<meta charset="UTF-8">
<style>
/*
* CSS Optimized for Dompdf / mPDF / TCPDF
* Avoids flexbox, uses standard block/inline styling for flawless PDF generation.
*/
body {
font-family: Arial, Helvetica, sans-serif;
/* ==========================================================================
* ATS-SAFE CV TEMPLATE
* Rules enforced by this stylesheet:
* - Single column. No floats, no tables, no absolute positioning.
* Multi-column layouts are the #1 cause of scrambled ATS text extraction.
* - No headers/footers, no text boxes, no images, no icons.
* - Standard, machine-recognisable section headings.
* - Standard fonts only (Helvetica/Arial), ligatures disabled so that
* Dompdf does not emit glyphs that break text copy-out.
* - Contact details and URLs are PLAIN TEXT, not anchor-wrapped labels:
* many parsers read the visible text, not the href.
* ========================================================================== */
body {
font-family: Helvetica, Arial, sans-serif;
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "clig" 0;
letter-spacing: 0px;
letter-spacing: 0;
text-rendering: geometricPrecision;
font-size: 13px;
color: #222;
line-height: 1.4;
margin: 0;
padding: 15px 30px;
}
.header {
text-align: center;
border-bottom: 2px solid #1a237e;
padding-bottom: 10px;
margin-bottom: 15px;
}
h1 {
font-size: 28px;
color: #1a237e;
margin: 0 0 5px 0;
text-transform: uppercase;
letter-spacing: 1px;
}
.headline {
font-size: 16px;
font-weight: bold;
color: #424242;
margin-bottom: 5px;
}
.contact {
font-size: 12px;
color: #555;
font-size: 10.5pt;
color: #1a1a1a;
line-height: 1.42;
margin: 0;
padding: 34px 40px;
}
.section-title {
font-size: 15px;
font-weight: bold;
color: #1a237e;
border-bottom: 1px solid #ddd;
margin-top: 15px;
margin-bottom: 6px;
padding-bottom: 4px;
text-transform: uppercase;
h1 {
font-size: 20pt;
color: #111;
margin: 0 0 4px 0;
letter-spacing: 0.5px;
}
p { margin: 0 0 6px 0; text-align: justify; }
.job-block { margin-bottom: 10px; }
.job-header {
width: 100%;
margin-bottom: 5px;
display: table; /* هذا يحل مشاكل Dompdf */
}
.job-title {
font-weight: bold;
font-size: 13.5px; /* تصغير طفيف جداً */
color: #222;
float: left;
width: 70%; /* إجبار العنوان على عدم تجاوز 70% من السطر */
line-height: 1.3;
}
.job-meta {
font-size: 11px;
color: #1a237e;
font-weight: bold;
float: right;
width: 28%; /* تخصيص مساحة آمنة للتاريخ */
text-align: right;
}
.clear { clear: both; }
.headline {
font-size: 11.5pt;
font-weight: bold;
color: #333;
margin: 0 0 6px 0;
}
.contact {
font-size: 9.5pt;
color: #333;
line-height: 1.55;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1.5px solid #222;
}
ul { margin: 4px 0 8px 0; padding-left: 20px; }
li { margin-bottom: 4px; text-align: justify; }
h2 {
font-size: 11pt;
font-weight: bold;
color: #111;
border-bottom: 1px solid #bbb;
margin: 16px 0 7px 0;
padding-bottom: 3px;
text-transform: uppercase;
letter-spacing: 0.6px;
}
p { margin: 0 0 6px 0; text-align: left; }
.job { margin-bottom: 11px; }
/* Each field on its own line, in reading order. No float, no table.
A parser walking the DOM gets: title, then employer/location, then dates. */
.job-title { font-weight: bold; font-size: 10.5pt; color: #111; margin: 0; }
.job-company { font-size: 10pt; color: #222; margin: 1px 0 0 0; }
.job-dates { font-size: 9.5pt; color: #444; margin: 1px 0 3px 0; }
ul { margin: 3px 0 7px 0; padding-left: 17px; }
li { margin-bottom: 3px; text-align: left; }
.skill-line { margin: 0 0 4px 0; }
.label { font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>Hamza Ayed</h1>
<!-- PHP WILL INJECT THE AI-GENERATED HEADLINE HERE -->
<div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact">
Amman, Jordan | +962 79 858 3052 | <a href="mailto:hamzaayed.dev@gmail.com" style="color: #555; text-decoration: none;">hamzaayed.dev@gmail.com</a><br>
<a href="https://linkedin.com/in/hamza-ayed" style="color: #555; text-decoration: none; font-weight: bold;">linkedin.com/in/hamza-ayed</a> |
<a href="https://github.com/Hamza-Ayed" style="color: #555; text-decoration: none; font-weight: bold;">github.com/Hamza-Ayed</a> |
<a href="https://intaleqapp.com/hamza.html" style="color: #1a237e; text-decoration: underline; font-weight: bold;">intaleqapp.com/hamza.html (Portfolio)</a>
</div>
</div>
<h1>{{FULL_NAME}}</h1>
<div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact">{{CONTACT_BLOCK}}</div>
<div class="section-title">Professional Summary</div>
<!-- PHP WILL INJECT THE AI-GENERATED SUMMARY HERE -->
<h2>Professional Summary</h2>
<p>{{TAILORED_SUMMARY}}</p>
<h2>Key Achievements</h2>
{{ACHIEVEMENTS_BLOCK}}
<h2>Technical Skills</h2>
<p class="skill-line"><span class="label">Core Focus for This Role:</span> {{DYNAMIC_SKILLS}}</p>
{{SKILLS_BLOCK}}
<div class="section-title">Technical Skills</div>
<!-- PHP WILL INJECT ATS KEYWORDS HERE -->
<p><strong>Core Focus:</strong> {{DYNAMIC_SKILLS}}</p>
<p><strong>Backend & Architecture:</strong> PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Event-Driven Architecture</p>
<p><strong>Infrastructure & Cloud:</strong> Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS</p>
<p><strong>Databases & Geospatial:</strong> PostgreSQL/PostGIS, MySQL, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries</p>
<p><strong>Mobile:</strong> Flutter/Dart, GetX, BLoC/Cubit</p>
<h2>Professional Experience</h2>
{{EXPERIENCE_BLOCK}}
<div class="section-title">Selected Technical Highlights</div>
<ul>
<li>Built proprietary OSM-based mapping infrastructure replacing Google Maps APIs</li>
<li>Engineered WebSocket-based real-time ride tracking systems</li>
<li>Developed offline-first mapping SDKs published on pub.dev and npm</li>
<li>Designed async AI document-processing pipelines using vision models</li>
<li>Optimized routing infrastructure using GraphHopper and PostGIS</li>
</ul>
<h2>Education</h2>
{{EDUCATION_BLOCK}}
<div class="section-title">Professional Experience</div>
<h2>Certifications</h2>
{{CERTIFICATIONS_BLOCK}}
<div class="job-block">
<div class="job-header">
<div class="job-title">CTO & Technical Architect | Systems & Backend Focus — Intaleq &nbsp;|&nbsp;</div>
<div class="job-meta">January 2024 – Present | Remote (MENA Region)</div>
<div class="clear"></div>
</div>
<ul>
<li>Led the full-stack architecture of a smart transportation ecosystem supporting 1,800+ drivers and 2,500+ riders, building the core backend with PHP/Workerman.</li>
<li>Architected the proprietary mapping platform (IntaleqMaps), orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, saving $10,000+/month in operational costs.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Founding Engineer & Lead Backend Architect — Tripz Egypt &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2023 – Present | Cairo / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Co-founded and architected Egypt's homegrown ride-hailing platform, managing the entire distributed ecosystem with real-time tracking for 4,318 drivers and 2,464 riders.</li>
<li>Implemented robust event-driven architecture for real-time driver/rider matching and WebSockets-based route optimization.</li>
<li>Integrated local digital payment gateways and driver payout systems, supporting an 8% commission structure that drove a $0.78 Customer Acquisition Cost.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Senior Systems Architect — Mobile & Backend (Freelance) &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2017 – Dec 2023 | Jordan / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Delivered 25+ production enterprise applications across GIS, FinTech, and utilities, leading full lifecycle architecture for clients in the MENA region.</li>
<li>Developed Nabih (AI Smart Responder): designed and built an intelligent virtual assistant system connecting a Flutter mobile application (GetX/Cubit) with custom PHP backend APIs.</li>
<li>Developed Musadaq (AI-Powered Invoice Processing): engineered an automated KYC and invoice document processor using AI vision models and asynchronous processing pipelines.</li>
</ul>
</div>
<div class="section-title">Notable Projects</div>
<ul>
<li><strong>IntaleqMaps Engine:</strong> Engineered a proprietary mapping platform orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer, replacing Google Maps API across the MENA region.</li>
<li><strong>Nabih AI Virtual Assistant:</strong> Architected an automated customer support solution connecting a Flutter application (GetX/Cubit) to custom high-concurrency PHP backend APIs.</li>
<li><strong>Musadaq AI Document Processor:</strong> Engineered an automated KYC and invoice document processing platform utilizing AI vision models and async processing pipelines.</li>
<li><strong>Meta Ads Manager & SaaS Gateways:</strong> Deployed multi-tenant platforms using NestJS/React (Meta Ads Manager) and high-concurrency Node.js (WhatsApp bridge managing headless Puppeteer sessions in Docker) to automate operations.</li>
</ul>
<div class="section-title">Open Source Contributions</div>
<ul>
<li><strong>intaleq_maps (Flutter SDK):</strong> Published on <a href="https://pub.dev/packages/intaleq_maps" style="color: #1a237e; text-decoration: none; font-weight: bold;">pub.dev/packages/intaleq_maps</a>. Custom map rendering, offline caching, and route plotting package optimized for low-bandwidth environments.</li>
<li><strong>intaleq-maps-gl (NPM Library):</strong> Published on <a href="https://libraries.io/npm/intaleq-maps-gl" style="color: #1a237e; text-decoration: none; font-weight: bold;">npmjs.com/package/intaleq-maps-gl</a>. Web-based Mapbox GL compatible library optimized for custom OSM tiles and routing integration.</li>
</ul>
<div class="section-title">Education & Certifications</div>
<ul>
<li><strong>BS Mathematics (Applied Computing & Algorithms)</strong>, Mutah University (2003–2007)</li>
<li><strong>Google Data Analytics</strong> Professional Certificate (Coursera)</li>
<li><strong>IBM Data Analyst</strong> Professional Certificate (Coursera)</li>
<li><strong>Meta APIs & Django Web Framework</strong> Course Certificates (Coursera)</li>
<li><strong>AWS Cloud Practitioner & Cloud Architecture Fundamentals</strong></li>
</ul>
<div class="section-title">Languages</div>
<p><strong>Arabic:</strong> Native | <strong>English:</strong> Professional Working Proficiency | <strong>Turkish:</strong> Limited Working Proficiency</p>
<div class="section-title">Availability & Work Authorization</div>
<p style="text-align: center; font-weight: bold; color: #1a237e; margin-top: 10px;">
Available Immediately | Open to Remote, Hybrid & Relocation (GCC/Europe/MENA) | Valid Passport
</p>
<h2>Additional Information</h2>
<p class="skill-line"><span class="label">Languages:</span> {{LANGUAGES}}</p>
<p class="skill-line"><span class="label">Location:</span> {{LOCATION}}</p>
<p class="skill-line"><span class="label">Availability:</span> {{AVAILABILITY}}</p>
</body>
</html>
-200
View File
@@ -1,200 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "clig" 0;
letter-spacing: 0px;
text-rendering: geometricPrecision;
font-size: 13px;
color: #222;
line-height: 1.4;
margin: 0;
padding: 15px 30px;
}
.header {
text-align: center;
border-bottom: 2px solid #1a237e;
padding-bottom: 10px;
margin-bottom: 15px;
}
h1 {
font-size: 28px;
color: #1a237e;
margin: 0 0 5px 0;
text-transform: uppercase;
letter-spacing: 1px;
}
.headline {
font-size: 16px;
font-weight: bold;
color: #424242;
margin-bottom: 5px;
}
.contact {
font-size: 12px;
color: #555;
}
.section-title {
font-size: 15px;
font-weight: bold;
color: #1a237e;
border-bottom: 1px solid #ddd;
margin-top: 15px;
margin-bottom: 6px;
padding-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
p { margin: 0 0 6px 0; text-align: justify; }
.job-block { margin-bottom: 10px; }
.job-header {
width: 100%;
margin-bottom: 5px;
display: table;
}
.job-title {
font-weight: bold;
font-size: 13.5px;
color: #222;
float: left;
width: 70%;
line-height: 1.3;
}
.job-meta {
font-size: 11px;
color: #1a237e;
font-weight: bold;
float: right;
width: 28%;
text-align: right;
}
.clear { clear: both; }
ul { margin: 4px 0 8px 0; padding-left: 20px; }
li { margin-bottom: 4px; text-align: justify; }
</style>
</head>
<body>
<div class="header">
<h1>Hamza Ayed</h1>
<div class="headline">{{JOB_HEADLINE}}</div>
<div class="contact">
Amman, Jordan | +962 79 858 3052 | <a href="mailto:hamzaayed.dev@gmail.com" style="color: #555; text-decoration: none;">hamzaayed.dev@gmail.com</a><br>
<a href="https://linkedin.com/in/hamza-ayed" style="color: #555; text-decoration: none; font-weight: bold;">linkedin.com/in/hamza-ayed</a> |
<a href="https://github.com/Hamza-Ayed" style="color: #555; text-decoration: none; font-weight: bold;">github.com/Hamza-Ayed</a> |
<a href="https://intaleqapp.com/hamza.html" style="color: #1a237e; text-decoration: underline; font-weight: bold;">intaleqapp.com/hamza.html (Portfolio)</a>
</div>
</div>
<div class="section-title">Professional Summary</div>
<p>{{TAILORED_SUMMARY}}</p>
<div class="section-title">Technical Skills</div>
<p><strong>Core Focus:</strong> {{DYNAMIC_SKILLS}}</p>
<p><strong>Backend &amp; Architecture:</strong> PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), REST APIs, WebSockets, Microservices</p>
<p><strong>Infrastructure &amp; Cloud:</strong> Docker, Linux, Nginx, Redis, CI/CD (GitHub Actions), AWS</p>
<p><strong>Databases:</strong> PostgreSQL, MySQL, Redis, Database Design &amp; Query Optimization</p>
<p><strong>Geospatial (Supplemental):</strong> PostGIS, OpenStreetMap (OSM), GraphHopper, MapLibre GL, Spatial Queries</p>
<p><strong>Mobile:</strong> Flutter/Dart, GetX, BLoC/Cubit</p>
<div class="section-title">Impact &amp; Delivery</div>
<ul>
<li>Built and scaled platforms serving 6,000+ active users across two markets with zero downtime deployments</li>
<li>Engineered proprietary OSM-based mapping infrastructure replacing Google Maps APIs across the MENA region — saving $10,000+/month in operational costs</li>
<li>Designed high-concurrency WebSocket systems handling real-time operations at scale</li>
<li>Delivered 25+ production applications across healthcare, logistics, fintech, and utilities for MENA clients</li>
<li>Published open-source mapping SDKs on pub.dev and npm, used in production environments</li>
</ul>
<div class="section-title">Professional Experience</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Technical Lead &amp; Backend Engineer — Intaleq &nbsp;|&nbsp;</div>
<div class="job-meta">January 2024 – Present | Amman, Jordan</div>
<div class="clear"></div>
</div>
<ul>
<li>Built full-stack transportation platform from zero to production: PHP/Workerman backend, Flutter mobile apps, WebSocket-based real-time dispatcher — serving 1,800+ active drivers.</li>
<li>Developed proprietary mapping platform (IntaleqMaps) orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer — reducing mapping API costs by $10,000+/month.</li>
<li>Designed event-driven architecture for real-time driver/rider matching, live tracking, and automated fare calculation.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Lead Backend Engineer — Tripz Egypt &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2023 – Present | Cairo / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Built complete ride-hailing platform: PHP backend system, Flutter rider and driver apps, real-time WebSocket communication layer, and local payment gateway integrations.</li>
<li>Engineered event-driven driver/rider matching system serving 4,318 drivers with a $0.78 customer acquisition cost — the lowest in the Egyptian market.</li>
<li>Integrated local digital payment gateways and designed automated driver payout infrastructure supporting an 8% commission model.</li>
</ul>
</div>
<div class="job-block">
<div class="job-header">
<div class="job-title">Senior Backend Engineer — Freelance &nbsp;|&nbsp;</div>
<div class="job-meta">Jan 2017 – Dec 2023 | Jordan / Remote</div>
<div class="clear"></div>
</div>
<ul>
<li>Delivered 25+ applications for MENA clients across healthcare, logistics, HR technology, sports platforms, and utility management systems — full lifecycle from architecture to deployment.</li>
<li>Built automated customer support platform: designed Flutter application (GetX/Cubit) connected to high-concurrency PHP backend APIs handling real-time query resolution.</li>
<li>Developed document processing system: engineered async processing pipelines using vision models for automated KYC and invoice data extraction.</li>
</ul>
</div>
<div class="section-title">Notable Projects</div>
<ul>
<li><strong>IntaleqMaps Engine:</strong> Proprietary mapping platform orchestrating GraphHopper routing, OSM tile serving, and PostGIS spatial queries via a NestJS API layer — replacing Google Maps API across the MENA region.</li>
<li><strong>Tripz Egypt Platform:</strong> End-to-end ride-hailing system with 8 ride types, real-time matching, custom payment infrastructure, and AI-powered KYC verification.</li>
<li><strong>Customer Support Automation Platform:</strong> Built an automated query resolution system connecting a Flutter application (GetX/Cubit) to high-concurrency PHP backend APIs.</li>
<li><strong>Document Processing Platform:</strong> Engineered automated KYC and invoice processing using vision models and asynchronous processing pipelines.</li>
<li><strong>Meta Ads Manager (SaaS):</strong> Deployed multi-tenant platform using NestJS/React for automated ad campaign management.</li>
<li><strong>WhatsApp Bridge:</strong> High-concurrency Node.js service managing headless Puppeteer sessions in Docker for automated messaging operations.</li>
</ul>
<div class="section-title">Open Source Contributions</div>
<ul>
<li><strong>intaleq_maps (Flutter SDK):</strong> Published on <a href="https://pub.dev/packages/intaleq_maps" style="color: #1a237e; text-decoration: none; font-weight: bold;">pub.dev/packages/intaleq_maps</a>. Custom map rendering, offline caching, and route plotting package optimized for low-bandwidth environments.</li>
<li><strong>intaleq-maps-gl (NPM Library):</strong> Published on <a href="https://libraries.io/npm/intaleq-maps-gl" style="color: #1a237e; text-decoration: none; font-weight: bold;">npmjs.com/package/intaleq-maps-gl</a>. Web-based Mapbox GL compatible library optimized for custom OSM tiles and routing integration.</li>
</ul>
<div class="section-title">Education &amp; Certifications</div>
<ul>
<li><strong>BS Mathematics (Applied Computing &amp; Algorithms)</strong>, Mutah University (2003–2007)</li>
<li><strong>Google Data Analytics</strong> Professional Certificate (Coursera)</li>
<li><strong>IBM Data Analyst</strong> Professional Certificate (Coursera)</li>
<li><strong>Meta APIs &amp; Django Web Framework</strong> Course Certificates (Coursera)</li>
<li><strong>AWS Cloud Practitioner &amp; Cloud Architecture Fundamentals</strong></li>
</ul>
<div class="section-title">Languages</div>
<p><strong>Arabic:</strong> Native | <strong>English:</strong> Professional Working Proficiency | <strong>Turkish:</strong> Limited Working Proficiency</p>
<div class="section-title">Availability</div>
<p style="text-align: center; font-weight: bold; color: #1a237e; margin-top: 10px;">
Based in Amman, Jordan — Available Immediately | Open to On-site, Hybrid &amp; Remote Roles in Jordan &amp; MENA
</p>
</body>
</html>
+395 -194
View File
@@ -1,6 +1,10 @@
<?php
// ============================================================================
// Dynamic ATS-Optimized CV Generator & AI Proxy (Backend)
// ----------------------------------------------------------------------------
// All CV facts come from profile.json, which is generated from profile_data.js
// (run `node sync_profile.js` after editing the profile). Nothing in this file
// hardcodes a job title, a metric, a date or a contact detail.
// ============================================================================
header('Access-Control-Allow-Origin: *');
@@ -24,282 +28,479 @@ 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/
// .env lives outside the document root.
$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);
$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."]);
echo json_encode(["error" => "Missing apiKey. Set GEMINI_API_KEY in .env or pass it in the 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;
// ── Model selection ─────────────────────────────────────────────────────────
// CV tailoring is the one call where output quality directly determines whether
// an application lands, so it gets the stronger model. Comments and post
// rewrites are high-volume and low-stakes, so they stay on the cheap model.
$MODELS = [
'generatePdf' => $_ENV['GEMINI_MODEL_CV'] ?? 'gemini-flash-latest',
'generateText' => $_ENV['GEMINI_MODEL_TEXT'] ?? 'gemini-flash-latest',
'generateComment' => $_ENV['GEMINI_MODEL_LIGHT'] ?? 'gemini-flash-lite-latest',
'repurposePost' => $_ENV['GEMINI_MODEL_LIGHT'] ?? 'gemini-flash-lite-latest',
];
// ==========================================
// ACTION 1: Generate ATS PDF CV
// ==========================================
if ($action === 'generatePdf') {
$jobDescription = $data['jobDescription'] ?? '';
$template = $data['template'] ?? 'default'; // 'amman' or 'default'
function geminiUrl(string $model, string $apiKey): string {
return "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey;
}
// --- 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);
function callGemini(string $url, array $payload): array {
$ch = curl_init($url);
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);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
return ['body' => $response, 'code' => $httpCode, 'error' => $curlErr];
}
if ($httpCode !== 200) {
function loadProfile(): array {
$path = __DIR__ . '/profile.json';
if (!file_exists($path)) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
echo json_encode(["error" => "profile.json missing. Run 'node sync_profile.js' and redeploy."]);
exit;
}
$profile = json_decode(file_get_contents($path), true);
if (!is_array($profile)) {
http_response_code(500);
echo json_encode(["error" => "profile.json is not valid JSON."]);
exit;
}
return $profile;
}
function e(?string $s): string {
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// ============================================================================
// ACTION 1: Generate ATS PDF CV
// ============================================================================
if ($action === 'generatePdf') {
$jobDescription = trim($data['jobDescription'] ?? '');
$jobTitle = trim($data['jobTitle'] ?? '');
// 'amman' biases toward the local Jordanian market; 'default' toward
// GCC / international / remote. It changes EMPHASIS ONLY — never facts.
$market = ($data['template'] ?? 'default') === 'amman' ? 'amman' : 'default';
if ($jobDescription === '') {
http_response_code(400);
echo json_encode(["error" => "jobDescription is required to tailor the CV."]);
exit;
}
$responseData = json_decode($response, true);
$profile = loadProfile();
$id = $profile['identity'];
$bounds = $profile['boundaries'];
// ── Build the reference blocks the model must work from ────────────────
$skillsRef = '';
foreach ($profile['skills'] as $group => $items) {
$skillsRef .= "- {$group}: " . implode(', ', $items) . "\n";
}
$experienceRef = '';
foreach ($profile['experience'] as $ri => $role) {
$experienceRef .= "ROLE INDEX {$ri}: {$role['title']} — {$role['company']} ({$role['start']} – {$role['end']}, {$role['location']})\n";
foreach ($role['bullets'] as $bi => $bullet) {
$experienceRef .= " [{$ri}.{$bi}] {$bullet['text']}\n";
}
}
$marketGuidance = $market === 'amman'
? "TARGET MARKET: Jordan / Amman local tech market. Titles here are conservative — " .
"prefer 'Senior Mobile Engineer', 'Lead Mobile Engineer' or 'Mobile Technical Lead' " .
"over 'Architect' unless the posting itself uses 'Architect'. Emphasise hands-on " .
"delivery, cost control and breadth. State availability for on-site/hybrid in Amman."
: "TARGET MARKET: GCC / international / remote. 'Senior Mobile Architect' and " .
"'Founding Engineer' land well here. Emphasise 0-to-1 platform ownership, scale " .
"metrics, real-time systems and infrastructure cost reduction. Note openness to " .
"relocation and remote work.";
$prompt = <<<PROMPT
You are an expert ATS résumé tailor. Rewrite my CV content so it scores as highly
as possible against the job description below WITHOUT stating anything untrue.
{$marketGuidance}
=== ATS METHODOLOGY — APPLY ALL OF IT ===
1. EXACT-TERM MIRRORING: ATS matching is literal. If the posting says "React Native"
and I have "Flutter", do NOT write "React Native". But if the posting says
"Flutter" or "Dart" or "WebSocket", use THAT EXACT WORD, not a synonym.
2. DUAL FORM FOR ACRONYMS: e.g. "Continuous Integration / Continuous Deployment (CI/CD)".
3. TITLE ALIGNMENT: the headline should mirror the posting's title when that title
honestly describes my record and appears in ALLOWED TITLES.
4. KEYWORD FREQUENCY: a critical keyword should appear 2-3 times across summary,
skills and at least one experience bullet — always inside a real sentence.
5. BULLET FORMULA: "Accomplished [X] as measured by [Y] by doing [Z]." Strong
past-tense verb first. Outcome before task. A number beats an adjective.
6. HARD SKILLS OVER SOFT SKILLS.
=== STRICT INTEGRITY RULES — VIOLATION INVALIDATES THE OUTPUT ===
A. NEVER invent a skill, employer, metric, date or certification.
B. ALLOWED TITLES ONLY: {allowed}
C. FORBIDDEN TITLES: {forbidden}
D. NEVER CLAIM (no production experience): {never}
E. Every number must be copied exactly from MY REAL DATA. No rounding, no new numbers.
F. When rewriting an experience bullet you may change the WORDING to carry the
job's keywords, but the FACTS, NUMBERS and SCOPE must remain identical to the
original bullet you were given.
=== MY REAL DATA ===
NAME: {$id['fullName']}
CURRENT POSITIONING: {$id['primaryTitle']} — {$id['positioningLine']}
MASTER SUMMARY: {$profile['summary']}
SKILLS (you may only select from these):
{$skillsRef}
EXPERIENCE (you may only rewrite these exact bullets):
{$experienceRef}
HONEST LIMITS: {limits}
=== TARGET JOB ===
Title: {$jobTitle}
Description:
{jobdesc}
=== RETURN FORMAT ===
Return ONLY raw JSON (no markdown fences) with EXACTLY these keys:
{
"headline": "One line. Title mirroring the job + 3-4 exact hard skills I truly have.",
"summary": "3-4 sentences. Opens with what I built and the numbers. Mirrors the job's exact terminology.",
"core_skills": "Comma-separated list of exactly 10 keywords copied verbatim from the job posting that I genuinely have.",
"roles": [
{
"role_index": 0,
"bullet_ids": ["0.0", "0.2"],
"bullets": ["Rewritten text of bullet 0.0", "Rewritten text of bullet 0.2"]
}
],
"ats_keywords_matched": ["exact terms from the posting that now appear in the CV"],
"ats_keywords_missing": ["requirements from the posting I genuinely cannot claim"],
"estimated_ats_score": 0
}
RULES FOR "roles":
- Include EVERY role index that exists in MY REAL DATA, in the same order.
- For each role select the 3-4 bullets MOST relevant to this job (fewer for the
oldest role). "bullets" must align 1:1 with "bullet_ids".
- Each rewritten bullet must stay factually identical to its original.
PROMPT;
$prompt = str_replace(
['{allowed}', '{forbidden}', '{never}', '{limits}', '{jobdesc}'],
[
implode(', ', $profile['allowedTitles']),
implode(', ', $profile['forbiddenTitles']),
implode(', ', $bounds['neverClaim']),
implode(' | ', $bounds['limitedIn']),
mb_substr($jobDescription, 0, 6000)
],
$prompt
);
$res = callGemini(geminiUrl($MODELS['generatePdf'], $apiKey), [
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => [
"temperature" => 0.25,
"maxOutputTokens" => 4096,
"responseMimeType" => "application/json"
]
]);
if ($res['code'] !== 200) {
http_response_code(502);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($res['body'])]);
exit;
}
$responseData = json_decode($res['body'], true);
$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '{}';
$aiText = str_replace(['```json', '```'], '', $aiText);
$parsedJson = json_decode(trim($aiText), true);
$aiText = trim(str_replace(['```json', '```'], '', $aiText));
$ai = json_decode($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";
// ── Server-side integrity guard ─────────────────────────────────────────
// The model is instructed not to fabricate, but instruction-following is not
// a guarantee. Anything it produces is scanned for terms I have no right to
// claim; a hit means we fall back to the untailored master content rather
// than ship a CV that will fail a technical screen.
$violations = [];
$haystack = strtolower(json_encode($ai, JSON_UNESCAPED_UNICODE));
foreach (array_merge($bounds['neverClaim'], $profile['forbiddenTitles']) as $banned) {
if (strpos($haystack, strtolower($banned)) !== false) {
$violations[] = $banned;
}
}
$integrityClean = empty($violations);
$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);
$headline = ($integrityClean && !empty($ai['headline']))
? $ai['headline']
: $id['primaryTitle'] . ' — ' . $id['positioningLine'];
$summary = ($integrityClean && !empty($ai['summary']))
? $ai['summary']
: $profile['summary'];
if ($integrityClean && !empty($ai['core_skills'])) {
$coreSkills = $ai['core_skills'];
} else {
// Fallback spreads across every skill group. Taking the first N of the
// flattened list would just duplicate the "Mobile & Architecture" line
// verbatim, wasting the most valuable slot on the page.
$picked = [];
foreach ($profile['skills'] as $items) {
$picked = array_merge($picked, array_slice($items, 0, 3));
}
$coreSkills = implode(', ', array_slice($picked, 0, 10));
}
// ── Render blocks ───────────────────────────────────────────────────────
// Contact details sit on separate block-level lines with explicit
// separators. A bare <br> can collapse during text extraction and glue the
// email to the LinkedIn URL, which loses the recruiter both.
$contactBlock =
'<div>' . e($id['location']) . ' &nbsp;|&nbsp; ' . e($id['phone'])
. ' &nbsp;|&nbsp; ' . e($id['email']) . '</div>'
. '<div>' . e($id['linkedin']) . ' &nbsp;|&nbsp; ' . e($id['portfolio'])
. ' &nbsp;|&nbsp; ' . e($id['productSite']) . '</div>';
// Metrics are rendered as one linear list. The master CV shows them as a
// four-column band, which looks good to a human and extracts as garbage in
// an ATS — this is the parser-safe equivalent of the same information.
$achievements = '<ul>';
foreach ($profile['headlineMetrics'] as $m) {
$achievements .= '<li><strong>' . e($m['value']) . '</strong> — ' . e($m['label']) . '</li>';
}
foreach ($profile['achievementsAtAGlance'] as $a) {
$achievements .= '<li>' . e($a) . '</li>';
}
$achievements .= '</ul>';
$skillsBlock = '';
foreach ($profile['skills'] as $group => $items) {
$skillsBlock .= '<p class="skill-line"><span class="label">' . e($group) . ':</span> '
. e(implode(', ', $items)) . '</p>';
}
// Index the AI's per-role selections so we can fall back role-by-role.
$aiRoles = [];
if ($integrityClean && !empty($ai['roles']) && is_array($ai['roles'])) {
foreach ($ai['roles'] as $r) {
if (isset($r['role_index'])) {
$aiRoles[(int)$r['role_index']] = $r;
}
}
}
$experienceBlock = '';
foreach ($profile['experience'] as $ri => $role) {
$experienceBlock .= '<div class="job">';
$experienceBlock .= '<p class="job-title">' . e($role['title']) . '</p>';
$experienceBlock .= '<p class="job-company">' . e($role['company']) . ' — ' . e($role['location']) . '</p>';
$experienceBlock .= '<p class="job-dates">' . e($role['start']) . ' – ' . e($role['end']) . '</p>';
// Prefer the tailored rewrite; fall back to the master bullets whenever
// the model returned nothing usable for this role.
$bullets = [];
if (isset($aiRoles[$ri]['bullets']) && is_array($aiRoles[$ri]['bullets'])) {
foreach ($aiRoles[$ri]['bullets'] as $b) {
$b = trim((string)$b);
if ($b !== '') $bullets[] = $b;
}
}
if (empty($bullets)) {
foreach ($role['bullets'] as $b) {
$bullets[] = $b['text'];
}
}
$experienceBlock .= '<ul>';
foreach ($bullets as $b) {
$experienceBlock .= '<li>' . e($b) . '</li>';
}
$experienceBlock .= '</ul></div>';
}
$edu = $profile['education'];
$educationBlock = '<p>' . e($edu['degree']) . ' — ' . e($edu['institution']) . '. ' . e($edu['note']) . '</p>';
$certBlock = '<ul>';
foreach ($profile['certifications'] as $c) {
$certBlock .= '<li>' . e($c) . '</li>';
}
$certBlock .= '</ul>';
$availability = $market === 'amman'
? 'Based in Amman, Jordan. Available immediately for on-site, hybrid and remote roles.'
: $id['availability'];
$html = file_get_contents(__DIR__ . '/cv_template.html');
$html = strtr($html, [
'{{FULL_NAME}}' => e($id['fullName']),
'{{JOB_HEADLINE}}' => e($headline),
'{{CONTACT_BLOCK}}' => $contactBlock,
'{{TAILORED_SUMMARY}}' => e($summary),
'{{ACHIEVEMENTS_BLOCK}}' => $achievements,
'{{DYNAMIC_SKILLS}}' => e($coreSkills),
'{{SKILLS_BLOCK}}' => $skillsBlock,
'{{EXPERIENCE_BLOCK}}' => $experienceBlock,
'{{EDUCATION_BLOCK}}' => $educationBlock,
'{{CERTIFICATIONS_BLOCK}}' => $certBlock,
'{{LANGUAGES}}' => e($id['languages']),
'{{LOCATION}}' => e($id['location']),
'{{AVAILABILITY}}' => e($availability),
]);
try {
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('defaultFont', 'Helvetica');
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->loadHtml($html, 'UTF-8');
$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";
// ATS parsers key on the filename too — "Name - Role.pdf" reads cleanly.
$safeName = preg_replace('/[^a-zA-Z0-9\-_ ]/', '', $id['fullName']);
$safeTitle = preg_replace('/[^a-zA-Z0-9\-_ ]/', '', $jobTitle ?: 'CV');
$safeTitle = trim(preg_replace('/\s+/', ' ', $safeTitle));
$fileName = trim("{$safeName} - {$safeTitle}") . '.pdf';
echo json_encode([
"success" => true,
"pdf" => base64_encode($pdfOutput),
"filename" => $fileName
"success" => true,
"pdf" => base64_encode($pdfOutput),
"filename" => $fileName,
"ats" => [
"score" => $ai['estimated_ats_score'] ?? null,
"matched" => $ai['ats_keywords_matched'] ?? [],
"missing" => $ai['ats_keywords_missing'] ?? [],
"tailored" => $integrityClean && !empty($aiRoles),
"violations" => $violations,
]
]);
} catch (Exception $e) {
} catch (Exception $ex) {
http_response_code(500);
echo json_encode(["error" => "PDF Generation Failed", "details" => $e->getMessage()]);
echo json_encode(["error" => "PDF Generation Failed", "details" => $ex->getMessage()]);
}
exit;
}
// ==========================================
// ACTION 2: Standard Proxy (Text generation)
// ==========================================
// ============================================================================
// 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;
if (trim($prompt) === '') {
http_response_code(400);
echo json_encode(["error" => "prompt is required."]);
exit;
}
// Pass the exact Gemini response back to the extension
echo $response;
$res = callGemini(geminiUrl($MODELS['generateText'], $apiKey), [
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.6, "maxOutputTokens" => 8192]
]);
if ($res['code'] !== 200) {
http_response_code($res['code'] ?: 502);
echo $res['body'] ?: json_encode(["error" => $res['error'] ?: 'Upstream failure']);
exit;
}
echo $res['body'];
exit;
}
// ==========================================
// ACTION 3: Smart Comment Generator
// ==========================================
if ($action === 'generateComment') {
$postText = substr($data['postText'] ?? '', 0, 3000);
// ============================================================================
// ACTION 3 & 4: Prompt-file backed generators (comment / repurpose)
// ============================================================================
$fileBacked = [
'generateComment' => ['file' => 'comment_prompt.txt', 'key' => 'comment', 'tokens' => 700, 'temp' => 0.8],
'repurposePost' => ['file' => 'repurpose_prompt.txt', 'key' => 'result', 'tokens' => 1200, 'temp' => 0.85],
];
if (empty($postText)) {
if (isset($fileBacked[$action])) {
$cfg = $fileBacked[$action];
$postText = mb_substr($data['postText'] ?? '', 0, 3000);
if (trim($postText) === '') {
http_response_code(400);
echo json_encode(["error" => "postText is required."]);
exit;
}
$promptFile = __DIR__ . '/prompts/comment_prompt.txt';
$promptFile = __DIR__ . '/prompts/' . $cfg['file'];
if (!file_exists($promptFile)) {
http_response_code(500);
echo json_encode(["error" => "Comment prompt file not found on server."]);
echo json_encode(["error" => "Prompt file {$cfg['file']} not found on server."]);
exit;
}
// Give the writer prompts the real identity instead of a stale copy baked
// into the text file. {{PROFILE}} is optional in the template.
$profile = loadProfile();
$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]
$prompt = strtr($promptTemplate, [
'{{POST_TEXT}}' => $postText,
'{{PROFILE}}' => $profile['profileText'] ?? '',
'{{VOICE}}' => $profile['voice']['tone'] ?? '',
'{{BANNED}}' => implode(' / ', $profile['voice']['banned'] ?? []),
]);
$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;
$genConfig = [
"temperature" => $cfg['temp'],
"maxOutputTokens" => $cfg['tokens']
];
// The comment generator must return strict JSON; the repurposer returns prose.
if ($action === 'generateComment') {
$genConfig['responseMimeType'] = 'application/json';
}
$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([
$res = callGemini(geminiUrl($MODELS[$action], $apiKey), [
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 800]
"generationConfig" => $genConfig
]);
$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)]);
if ($res['code'] !== 200) {
http_response_code(502);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($res['body'])]);
exit;
}
$responseData = json_decode($response, true);
$resultText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
$responseData = json_decode($res['body'], true);
$text = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (empty($resultText)) {
http_response_code(500);
if ($text === '') {
http_response_code(502);
echo json_encode(["error" => "Empty result from AI."]);
exit;
}
echo json_encode(["success" => true, "result" => $resultText]);
echo json_encode(["success" => true, $cfg['key'] => $text]);
exit;
}
http_response_code(400);
echo json_encode(["error" => "Unknown action: " . $action]);
+344
View File
File diff suppressed because one or more lines are too long
+73 -16
View File
@@ -1,21 +1,78 @@
You are a Solutions Architect and Engineering Leader. Read the following LinkedIn post and generate a JSON response.
You are writing LinkedIn comments as Hamza Ayed. Read the post below and return a JSON response.
STRICT RULES FOR THE COMMENT:
- Max 3 sentences. It MUST flow naturally as a single, coherent, human-written comment. Do NOT output disconnected, bullet-point-like statements.
- Tone: Calm, authoritative, direct, and operator-level. Avoid corporate jargon, fluff, ego, preaching, or generic praise clichés (never start with "Great post", "Well said", etc.).
- INDUSTRY RELEVANCE (CRITICAL): Do NOT mention ride-hailing, mobility, GIS, or transportation unless the post is explicitly and directly about maps, routing, logistics, or transportation. For general posts (like team management, system design, workplace culture, innovation), comment from a general systems architect/tech lead perspective.
- TONE & STYLE: Write a comment that encourages professional discussion and attracts profile visits.
- LANGUAGE: Match the language of the post. If the post is in Arabic, write the comment in Arabic. If the post is in English, write the comment in English.
- Output the comment exactly as plain text in the JSON string (no markdown formatting inside).
=== WHO YOU ARE ===
{{PROFILE}}
STRICT RULES FOR THE ARABIC SUMMARY & CREDIBILITY CHECK:
- Analyze the post objectively. No flattery. No automatic agreement.
- Detect any exaggerated metrics, vanity metrics, logical flaws, or emotional manipulation (e.g., VC/startup hustle culture tropes).
- Provide a 2-3 sentence Arabic response. The first sentence should summarize the post. The following sentences MUST be a brutal, honest credibility assessment pointing out risks or flaws in the author's logic. (Note: The summary and credibility check MUST ALWAYS be in Arabic, regardless of the post's language).
=== VOICE ===
{{VOICE}}
Return ONLY a valid JSON object with EXACTLY two keys:
1. "arabic_summary": The objective summary and credibility assessment in Arabic.
2. "comment": The comment following the rules above (matching the post's language: Arabic if the post is Arabic, English if the post is English).
You have shipped things. You are not selling, not networking, not performing.
You comment because you have something specific to add — or because something in
the post is wrong and worth naming politely.
POST TEXT:
=== THE COMMENT — HARD RULES ===
1. LENGTH: 2-3 sentences. One coherent paragraph that reads like a person typed
it in one go. Never a list, never disconnected statements, never a mini-essay.
2. IT MUST ADD SOMETHING. Every comment has to do exactly one of these:
(a) contribute a concrete detail, constraint or trade-off the post missed;
(b) share a specific thing you saw when you did this yourself;
(c) ask a sharp, genuine question that moves the discussion forward;
(d) respectfully complicate a claim that is too clean to be true.
If you cannot do any of these honestly, set "comment" to "" (empty string) and
explain why in the Arabic analysis. A skipped comment is better than a generic
one — generic comments actively damage a technical reputation.
3. NEVER OPEN WITH PRAISE. Banned openers include: "Great post", "Well said",
"Couldn't agree more", "This resonates", "Spot on", "So true", "Love this",
"Thanks for sharing", "كلام سليم", "أحسنت", "بالضبط", "مقال رائع".
Open on the substance instead.
4. NO SELF-PROMOTION. Do not name Tripz, Intaleq, your job title, your metrics or
your portfolio. Your credibility must come from the precision of what you say,
not from a credential drop. At most you may say "when I built dispatch systems"
as context — never as a boast, and only when directly relevant.
5. DOMAIN DISCIPLINE (critical): Do NOT bring up ride-hailing, mobility, mapping,
GIS, dispatch or transportation UNLESS the post is explicitly about routing,
logistics, maps or transport. For general posts (management, hiring, system
design, culture, career advice), comment as an experienced engineer, full stop.
6. LANGUAGE: Match the post exactly. Arabic post → Arabic comment (natural,
professional, conversational Arabic — not translated-English Arabic).
English post → English comment.
7. BANNED PHRASING (instant fail): {{BANNED}}
Also banned: em-dash-heavy rhetorical constructions, "it's not X, it's Y"
parallelism, rhetorical questions used as filler, and any sentence that would
survive unchanged under a different post.
8. NO hashtags. NO emojis. NO markdown. Plain text only.
9. NO FABRICATION. Never invent a statistic, a benchmark, a study or an anecdote
that did not happen.
=== THE ARABIC ANALYSIS — HARD RULES ===
Always in Arabic, regardless of the post's language. 3-4 sentences total:
- Sentence 1: what the post actually claims, stripped of rhetoric.
- Sentences 2+: a blunt credibility assessment. Name specifically:
* unverifiable or vanity metrics
* survivorship bias, or a sample of one presented as a rule
* hustle-culture / VC-narrative framing
* logical gaps, false dichotomies, correlation sold as causation
* whether this is a disguised sales pitch or recruitment ad
- If the post is genuinely solid, say so plainly and say why — do not manufacture
criticism. Honesty runs in both directions.
- Close with a one-word engagement call: "يستاهل تعليق" or "تجاهله".
=== OUTPUT ===
Return ONLY a valid JSON object with EXACTLY these two keys:
{
"arabic_summary": "التحليل والمصداقية بالعربية",
"comment": "the comment in the post's language, or empty string if not worth commenting"
}
=== POST TEXT ===
{{POST_TEXT}}
+61 -17
View File
@@ -1,21 +1,65 @@
You are Hamza Ayed, a Senior Solutions Architect and technical leader.
Your task is to REPURPOSE an existing LinkedIn post into an entirely new, original post written in your unique voice.
You are Hamza Ayed. Take the post below and write an entirely NEW, original post
in your own voice on the same underlying topic.
YOUR BACKGROUND & PHILOSOPHY:
- Built "Intaleq" and "Tripz" platforms from scratch.
- Created "Intaleq Maps" to bypass Google Maps, saving massive costs and achieving independence.
- You believe in building independent infrastructure, cost-efficiency, and tech sovereignty rather than relying entirely on 3rd parties.
=== WHO YOU ARE ===
{{PROFILE}}
STRICT RULES:
1. CORE CONCEPT: Extract the main idea from the provided post, but write a COMPLETELY NEW post. Do not just summarize. Reframe it from your Architect/Leadership perspective.
2. LANGUAGE: Match the language of the original post. If Arabic, use conversational, engaging, professional Arabic (AVOID AI clichés like "في العصر الرقمي", "مما لا شك فيه", "يعد", "ختاماً"). Write naturally like a seasoned expert.
3. STRUCTURE:
- Hook: A strong opening statement (no emojis in the very first sentence).
- Body: Your perspective on the topic. Subtly weave in your philosophy if relevant (without revealing secrets).
- Conclusion: End with a strong closing thought or an open question to encourage engagement.
4. IMAGE PROMPT: At the very end of your response, leave two blank lines, then output EXACTLY this header: "--- IMAGE PROMPT ---", followed by a highly detailed, descriptive prompt in English for an AI image generator (like Midjourney or DALL-E) to create a stunning, professional accompanying visual for your post.
5. NO EMOJIS in the image prompt section. You may use a few appropriate emojis in the main post text.
6. OUTPUT FORMAT: Return the new post text, then the image prompt section. Nothing else.
=== VOICE ===
{{VOICE}}
POST TO REPURPOSE:
=== YOUR PHILOSOPHY (weave in only when genuinely relevant) ===
- You build independent infrastructure instead of renting it. Self-hosted maps,
routing and CI/CD were cost decisions before they were ideological ones.
- You launch in constrained markets — unstable networks, missing payment APIs,
low-end devices. Constraints are the interesting part of the engineering.
- You are suspicious of advice that has never survived contact with production.
- You measure things. A claim without a number is an opinion.
=== HARD RULES ===
1. ORIGINAL, NOT A SUMMARY. Extract the underlying idea, then write something new
from your own angle. If you find yourself restating the original's structure,
start over. Someone who read both posts should not feel they read the same one.
2. NO FABRICATION. You may only reference achievements, numbers and projects that
appear in WHO YOU ARE above. Never invent a metric, a client, a benchmark or a
war story. If the topic needs an example you do not have, write about the
principle instead of faking the anecdote.
3. LANGUAGE: Match the original post. Arabic → natural, professional,
conversational Arabic. Write like a working engineer talking, not like a
translated press release.
4. BANNED PHRASING: {{BANNED}}
Also avoid: opening with a one-word sentence for drama, "Here's the thing",
"Let that sink in", numbered listicles of obvious advice, and any closing line
that asks for engagement in a transparently manufactured way.
5. STRUCTURE:
- HOOK: a concrete, specific opening statement. No emoji in the first sentence.
A real observation beats a provocation.
- BODY: your actual position, with the reasoning visible. Name the trade-off,
not just the conclusion. Short paragraphs — LinkedIn is read on phones.
- CLOSE: a genuine question or a sharp closing thought. It should be a question
you would actually want answered.
6. LENGTH: 120-220 words. Long enough to say something, short enough to be read.
7. NO hashtags. At most 1-2 emoji in the body, and only if they earn their place.
8. NEVER reveal proprietary implementation details, client names, or anything
under NDA.
=== IMAGE PROMPT ===
After the post, leave two blank lines, then output EXACTLY this header:
--- IMAGE PROMPT ---
Then a detailed English prompt for an AI image generator: a professional,
editorial-quality visual that matches the post's idea. Describe subject,
composition, lighting, colour palette and mood. No text or lettering in the
image. No emojis in this section.
=== OUTPUT ===
The new post text, then the image prompt section. Nothing else.
=== POST TO REPURPOSE ===
{{POST_TEXT}}