Auto-deploy: 2026-08-05 16:17:53
This commit is contained in:
+43
-146
@@ -345,6 +345,7 @@
|
||||
<!-- Tabs -->
|
||||
<div id="lja-tabs">
|
||||
<button class="lja-tab active" data-tab="analysis">📊</button>
|
||||
<button class="lja-tab" data-tab="ats">🎯</button>
|
||||
<button class="lja-tab" data-tab="coverletter">✉️</button>
|
||||
<button class="lja-tab" data-tab="cvtips">📝</button>
|
||||
<button class="lja-tab" data-tab="qa">❓</button>
|
||||
@@ -353,6 +354,7 @@
|
||||
|
||||
<div id="lja-tab-labels">
|
||||
<span class="lja-tab-label active" data-tab="analysis">Analysis</span>
|
||||
<span class="lja-tab-label" data-tab="ats">ATS Score</span>
|
||||
<span class="lja-tab-label" data-tab="coverletter">Cover Letter</span>
|
||||
<span class="lja-tab-label" data-tab="cvtips">CV Tips</span>
|
||||
<span class="lja-tab-label" data-tab="qa">Q&A</span>
|
||||
@@ -361,7 +363,7 @@
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div id="lja-content">
|
||||
${['analysis', 'coverletter', 'cvtips', 'qa', 'benefits'].map(tab => `
|
||||
${['analysis', 'ats', 'coverletter', 'cvtips', 'qa', 'benefits'].map(tab => `
|
||||
<div class="lja-pane" id="lja-pane-${tab}" ${tab !== 'analysis' ? 'style="display:none"' : ''}>
|
||||
<div class="lja-pane-empty">
|
||||
<div class="lja-empty-icon">${tabIcon(tab)}</div>
|
||||
@@ -388,7 +390,7 @@
|
||||
}
|
||||
|
||||
function tabIcon(tab) {
|
||||
const icons = { analysis: '📊', coverletter: '✉️', cvtips: '📝', qa: '❓', benefits: '⭐' };
|
||||
const icons = { analysis: '📊', ats: '🎯', coverletter: '✉️', cvtips: '📝', qa: '❓', benefits: '⭐' };
|
||||
return icons[tab] || '🔍';
|
||||
}
|
||||
|
||||
@@ -536,7 +538,11 @@
|
||||
jobDescription: jobData.description,
|
||||
jobTitle: jobData.jobTitle || 'Job',
|
||||
action: 'generatePdf',
|
||||
template: 'amman'
|
||||
// Jordan-based postings get the conservative local framing;
|
||||
// everything else gets the GCC/international framing.
|
||||
template: /jordan|amman|الأردن|عمّان|عمان/i.test(jobData.location || '')
|
||||
? 'amman'
|
||||
: 'default'
|
||||
}
|
||||
}, resolve);
|
||||
});
|
||||
@@ -558,8 +564,20 @@
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
showPanelToast(root, '✅ PDF Downloaded Successfully!', 'success');
|
||||
|
||||
// Surface what the tailoring actually achieved, and warn loudly if
|
||||
// the server had to fall back because the model tried to fabricate.
|
||||
const ats = response.data.ats || {};
|
||||
let msg = '✅ PDF downloaded';
|
||||
if (ats.score) msg += ` — est. ATS match ${ats.score}%`;
|
||||
if (ats.tailored === false) msg += ' (untailored fallback)';
|
||||
if (ats.violations && ats.violations.length) {
|
||||
msg += ` ⚠️ blocked fabricated claims: ${ats.violations.join(', ')}`;
|
||||
}
|
||||
if (ats.missing && ats.missing.length) {
|
||||
console.warn('[LJA] Job requirements not claimable from profile:', ats.missing);
|
||||
}
|
||||
showPanelToast(root, msg, 'success');
|
||||
} else {
|
||||
throw new Error(response?.error || 'Failed to generate PDF from server');
|
||||
}
|
||||
@@ -588,6 +606,7 @@
|
||||
const loadingMsgs = {
|
||||
analysis: 'Analyzing job fit...',
|
||||
coverletter: 'Writing cover letter...',
|
||||
ats: 'Scoring CV against ATS...',
|
||||
cvtips: 'Generating CV tips...',
|
||||
qa: 'Preparing Q&A answers...',
|
||||
benefits: 'Summarizing benefits...'
|
||||
@@ -669,142 +688,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Prompt Builder ──────────────────────────────────────────────────────
|
||||
|
||||
function buildPrompt(tab, job, userProfile, language) {
|
||||
const langInstruction = language === 'arabic'
|
||||
? 'Respond entirely in Arabic.'
|
||||
: language === 'english'
|
||||
? 'Respond entirely in English.'
|
||||
: 'Respond in the same language as the job posting (Arabic or English).';
|
||||
|
||||
const jobContext = `
|
||||
Job Title: ${job.jobTitle || 'Not specified'}
|
||||
Company: ${job.company || 'Not specified'}
|
||||
Location: ${job.location || 'Not specified'}
|
||||
Description:
|
||||
${job.description || 'No description available'}
|
||||
${job.skills.length ? `\nRequired Skills: ${job.skills.join(', ')}` : ''}`.trim();
|
||||
|
||||
const profileBlock = `MY PROFILE:\n${userProfile}`;
|
||||
|
||||
const prompts = {
|
||||
analysis: `You are a senior career advisor specializing in tech roles in the Middle East.
|
||||
${langInstruction}
|
||||
Analyze this job posting against my profile. Be direct and honest.
|
||||
|
||||
${profileBlock}
|
||||
|
||||
JOB POSTING:
|
||||
${jobContext}
|
||||
|
||||
Provide in this exact format:
|
||||
## MATCH SCORE: X/100
|
||||
Brief explanation.
|
||||
|
||||
## ✅ STRONG MATCHES
|
||||
- bullet points of matching skills/experience
|
||||
|
||||
## ⚠️ GAPS
|
||||
- bullet points of missing requirements or weak areas
|
||||
|
||||
## 🎯 RECOMMENDATION
|
||||
Apply / Apply with preparation / Skip — with clear reason
|
||||
|
||||
## 💡 KEY SELLING POINTS
|
||||
- what to emphasize when applying
|
||||
|
||||
## 🏷️ BEST CV VARIANT
|
||||
Which of my CV variants (Solutions Architect / Flutter Developer / GIS Developer / FinTech Engineer) best fits and why`,
|
||||
|
||||
coverletter: `Write a compelling, personalized cover letter for this position.
|
||||
${langInstruction}
|
||||
|
||||
${profileBlock}
|
||||
|
||||
JOB:
|
||||
${jobContext}
|
||||
|
||||
Requirements:
|
||||
- 3 paragraphs maximum
|
||||
- Reference specific requirements from the job description
|
||||
- Highlight my most relevant achievements with numbers
|
||||
- Professional but confident tone — not generic
|
||||
- End with a strong call to action
|
||||
- Do NOT use placeholder brackets — write a complete ready-to-send letter
|
||||
- Address to "Hiring Team" if no specific name is available
|
||||
- Sign as "Hamza Ayed"`,
|
||||
|
||||
cvtips: `Based on this job posting, provide specific CV optimization advice.
|
||||
${langInstruction}
|
||||
|
||||
${profileBlock}
|
||||
|
||||
JOB:
|
||||
${jobContext}
|
||||
|
||||
Provide:
|
||||
## 📌 SUGGESTED HEADLINE
|
||||
A LinkedIn-style headline optimized for this specific role (max 120 characters)
|
||||
|
||||
## 📋 SUGGESTED SUMMARY
|
||||
3-4 sentence professional summary tailored for this role
|
||||
|
||||
## 🔑 KEYWORDS TO ADD
|
||||
- specific keywords from the job to add to CV
|
||||
|
||||
## ⬆️ EXPERIENCE TO PRIORITIZE
|
||||
- which of my experiences to move to top or emphasize
|
||||
|
||||
## ✂️ WHAT TO DE-EMPHASIZE
|
||||
- what to remove or minimize for this application
|
||||
|
||||
## 🎯 SKILLS SECTION
|
||||
- specific skills to list for this role`,
|
||||
|
||||
qa: `I am applying for this job and need to answer application questions.
|
||||
${langInstruction}
|
||||
Answer each question based on my profile. Be specific, professional, and concise.
|
||||
|
||||
${profileBlock}
|
||||
|
||||
JOB:
|
||||
${jobContext}
|
||||
|
||||
${job.questions.length > 0
|
||||
? 'QUESTIONS TO ANSWER:\n' + job.questions.map((q, i) => `${i + 1}. [${q.type}] ${q.question}`).join('\n')
|
||||
: 'Generate answers for these common application questions:\n1. Why are you interested in this role?\n2. What is your relevant experience?\n3. What are your salary expectations?\n4. When can you start?\n5. Do you require visa sponsorship?\n6. What is your current notice period?'}
|
||||
|
||||
For each question provide a clear, professional answer ready to paste. Keep answers concise (2-4 sentences for text fields).`,
|
||||
|
||||
benefits: `Analyze this job posting and extract all benefits and advantages.
|
||||
${langInstruction}
|
||||
|
||||
JOB:
|
||||
${jobContext}
|
||||
|
||||
Provide:
|
||||
## 💰 COMPENSATION & BENEFITS
|
||||
- salary, bonuses, equity if mentioned; otherwise state "Not specified"
|
||||
|
||||
## 🏢 WORK ARRANGEMENT
|
||||
- remote/hybrid/onsite, flexibility, hours
|
||||
|
||||
## 📈 GROWTH OPPORTUNITIES
|
||||
- career development, training, advancement potential
|
||||
|
||||
## 🌟 COMPANY PERKS
|
||||
- insurance, vacation, equipment, culture perks etc.
|
||||
|
||||
## ⚠️ POTENTIAL CONCERNS
|
||||
- red flags, very demanding requirements, unclear aspects
|
||||
|
||||
## 📊 OVERALL ATTRACTIVENESS: X/10
|
||||
Brief honest assessment of this opportunity for my profile`
|
||||
};
|
||||
|
||||
return prompts[tab] || prompts.analysis;
|
||||
}
|
||||
|
||||
// ─── Markdown Renderer ───────────────────────────────────────────────────
|
||||
|
||||
@@ -933,14 +816,28 @@ Brief honest assessment of this opportunity for my profile`
|
||||
|
||||
// ─── Utilities ───────────────────────────────────────────────────────────
|
||||
|
||||
// Markers that identify a profile saved before the CV was rewritten around
|
||||
// the Senior Mobile Architect positioning. A profile containing any of these
|
||||
// is describing a person who no longer matches the master CV, so every prompt
|
||||
// built from it would contradict the PDF that actually gets sent.
|
||||
const STALE_PROFILE_MARKERS = [
|
||||
'CTO & Technical Architect',
|
||||
'Senior Backend Engineer & Systems Architect',
|
||||
'IntaleqMaps',
|
||||
'hamzaayed.dev@gmail.com'
|
||||
];
|
||||
|
||||
function isStaleProfile(text) {
|
||||
if (!text) return true;
|
||||
return STALE_PROFILE_MARKERS.some(marker => text.includes(marker));
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], (data) => {
|
||||
if (data.userProfile && (data.userProfile.includes('hamzaayed@intaleqapp.com') || data.userProfile.includes('hamzaayedflutter@gmail.com') || data.userProfile.includes('hamzaayed@tripz-egypt.com'))) {
|
||||
data.userProfile = data.userProfile
|
||||
.replace(/hamzaayed@intaleqapp\.com/g, 'hamzaayed.dev@gmail.com')
|
||||
.replace(/hamzaayedflutter@gmail\.com/g, 'hamzaayed.dev@gmail.com')
|
||||
.replace(/hamzaayed@tripz-egypt\.com/g, 'hamzaayed.dev@gmail.com');
|
||||
if (isStaleProfile(data.userProfile) && typeof renderProfileText === 'function') {
|
||||
console.warn('[LJA] Stored profile is out of date with the master CV — migrating to profile_data.js.');
|
||||
data.userProfile = renderProfileText();
|
||||
chrome.storage.sync.set({ userProfile: data.userProfile });
|
||||
}
|
||||
resolve(data);
|
||||
|
||||
Reference in New Issue
Block a user