// content.js — LinkedIn page scraper + overlay launcher
(function () {
'use strict';
// Prevent double injection
if (window.__linkedinAnalyzerLoaded) return;
window.__linkedinAnalyzerLoaded = true;
// ─── Job Data Extraction ─────────────────────────────────────────────────
function extractJobData() {
const data = {
jobTitle: '',
company: '',
location: '',
jobType: '',
seniority: '',
description: '',
skills: [],
questions: [],
url: window.location.href,
detectedAt: Date.now()
};
// ── Helper: try selectors until one works
function trySelectors(selectors, minLength = 1) {
for (const sel of selectors) {
try {
const el = document.querySelector(sel);
if (el && el.textContent.trim().length >= minLength) {
return el.textContent.trim();
}
} catch(e) { /* skip invalid selectors */ }
}
return '';
}
function trySelectorsInner(selectors, minLength = 1) {
for (const sel of selectors) {
try {
const el = document.querySelector(sel);
if (el && el.innerText.trim().length >= minLength) {
return el.innerText.trim();
}
} catch(e) {}
}
return '';
}
// ── Job Title (many fallbacks)
data.jobTitle = trySelectors([
'.job-details-jobs-unified-top-card__job-title h1',
'.job-details-jobs-unified-top-card__job-title a',
'.job-details-jobs-unified-top-card__job-title',
'.jobs-unified-top-card__job-title h1',
'.jobs-unified-top-card__job-title a',
'.jobs-unified-top-card__job-title',
'h1.t-24',
'h1.t-18',
'h1[class*="job-title"]',
'h2[class*="job-title"]',
'.t-24.t-bold',
'.top-card-layout__title',
'a[class*="topcard__title"]',
'.topcard__title',
]);
// LinkedIn renders screen-reader-only headings throughout its chrome
// ("0 notifications", "1 new message"). They are real
elements with
// real text, so any document-wide heading scan will happily return one as
// the job title. Require the element to be visibly rendered and to not look
// like navigation furniture.
function isVisible(el) {
if (!el || !el.offsetParent) return false;
if (el.closest('[aria-hidden="true"]')) return false;
const cls = el.className && el.className.baseVal !== undefined
? el.className.baseVal
: String(el.className || '');
return !/visually-hidden|sr-only|a11y-text/.test(cls);
}
const NAV_NOISE = /notification|message|invitation|my network|feed update|search|premium|^\d+\s|jobs|people|similar|featured|recommended/i;
function plausibleTitle(text) {
return text.length > 4 && text.length < 100 && !NAV_NOISE.test(text);
}
// Fallback: a heading inside the job detail panel only — never page-wide.
if (!data.jobTitle) {
const jobPanel = document.querySelector(
'.scaffold-layout__detail-column, .jobs-search__job-details, .job-view-layout, ' +
'.jobs-details, [class*="jobs-unified-top-card"], main'
);
if (jobPanel) {
for (const h of jobPanel.querySelectorAll('h1, h2')) {
const text = h.textContent.trim();
if (isVisible(h) && plausibleTitle(text)) {
data.jobTitle = text;
break;
}
}
}
}
// Last resort: the browser tab title, which LinkedIn formats as
// "(N) Job Title | Company | LinkedIn". Still better than a nav label.
if (!data.jobTitle) {
const m = document.title.match(/^(?:\(\d+\)\s*)?(.+?)\s*\|/);
if (m && plausibleTitle(m[1].trim())) data.jobTitle = m[1].trim();
}
// ── Company Name
data.company = trySelectors([
'.job-details-jobs-unified-top-card__company-name a',
'.job-details-jobs-unified-top-card__company-name',
'.jobs-unified-top-card__company-name a',
'.jobs-unified-top-card__company-name',
'.topcard__org-name-link',
'.top-card-layout__card-subtitle a',
'.top-card-layout__second-subline a',
'a[data-tracking-control-name*="company"]',
'a[href*="/company/"]',
'.jobs-poster__name',
]);
// Fallback: look for company link in job details panel
if (!data.company) {
const jobPanel = document.querySelector('.scaffold-layout__detail-column, .jobs-search__job-details, .job-view-layout');
if (jobPanel) {
const companyLink = jobPanel.querySelector('a[href*="/company/"]');
if (companyLink) data.company = companyLink.textContent.trim();
}
}
// ── Location + metadata
const metaText = trySelectors([
'.job-details-jobs-unified-top-card__primary-description-container',
'.job-details-jobs-unified-top-card__primary-description',
'.jobs-unified-top-card__primary-description',
'.jobs-unified-top-card__subtitle',
'.top-card-layout__card-subtitle',
'.topcard__flavor--bullet',
]);
if (metaText) {
const parts = metaText.split('·').map(p => p.trim()).filter(Boolean);
if (parts.length > 0) data.location = parts[0];
}
// ── Job Type & Seniority from pills/tags
const tagSelectors = [
'.job-details-jobs-unified-top-card__job-insight span',
'.jobs-unified-top-card__job-insight span',
'.ui-label',
'.job-criteria__text',
'li.job-criteria__item',
];
for (const sel of tagSelectors) {
document.querySelectorAll(sel).forEach(el => {
const text = el.textContent.trim();
if (text.match(/full.time|part.time|contract|freelance|internship|temporary/i)) data.jobType = text;
if (text.match(/entry|associate|mid.senior|senior|director|executive|manager|intern/i)) data.seniority = text;
});
}
// Fallback: look for pill-style elements near the top
if (!data.jobType) {
document.querySelectorAll('span, li').forEach(el => {
const text = el.textContent.trim();
if (text === 'Full-time' || text === 'Part-time' || text === 'Contract' || text === 'Hybrid' ||
text === 'On-site' || text === 'Remote') {
if (!data.jobType) data.jobType = text;
if (!data.location && (text === 'Hybrid' || text === 'On-site' || text === 'Remote')) {
data.jobType = text;
}
}
});
}
// ── Job Description (many fallbacks)
data.description = trySelectorsInner([
'.jobs-description__content .jobs-box__html-content',
'#job-details',
'.jobs-description-content__text',
'.jobs-description__content',
'.jobs-description',
'.job-view-layout .description__text',
'.description__text--rich',
'.show-more-less-html__markup',
'[class*="jobs-description"]',
'[class*="description__text"]',
], 50);
// Fallback: find "About the job" section and grab everything after it
if (!data.description || data.description.length < 50) {
const allSections = document.querySelectorAll('section, div, article');
for (const section of allSections) {
const header = section.querySelector('h2, h3');
if (header && header.textContent.trim().match(/about the job|job description|description|responsibilities/i)) {
const text = section.innerText.trim();
if (text.length > 100) {
data.description = text.substring(0, 6000);
break;
}
}
}
}
// Last resort: grab largest text block from the detail column
if (!data.description || data.description.length < 50) {
const detailCol = document.querySelector('.scaffold-layout__detail-column, .jobs-search__job-details, .job-view-layout, main');
if (detailCol) {
let longestText = '';
detailCol.querySelectorAll('div, section, article').forEach(el => {
const text = el.innerText.trim();
if (text.length > longestText.length && text.length > 200) {
longestText = text;
}
});
if (longestText) {
data.description = longestText.substring(0, 6000);
}
}
}
// Cap description
if (data.description) data.description = data.description.substring(0, 6000);
// ── Skills
const skillSelectors = [
'.job-details-how-you-match__skills-item-subtitle',
'.job-details-skill-match-status-list li',
'.jobs-pref-match-skill-wrapper',
'.job-details-how-you-match-card__pill',
'.job-details-skill-match-status-list span',
];
const skillSet = new Set();
for (const sel of skillSelectors) {
document.querySelectorAll(sel).forEach(el => {
const skill = el.textContent.trim();
if (skill && skill.length < 60) skillSet.add(skill);
});
}
data.skills = Array.from(skillSet).slice(0, 20);
// ── Application Questions (Easy Apply)
data.questions = extractApplicationQuestions();
return data;
}
function extractApplicationQuestions() {
const questions = [];
const seen = new Set();
// LinkedIn wraps each form field in .fb-dash-form-element or [data-test-form-element]
// Search the whole document to avoid getting trapped in the wrong dialog (like the messaging pane).
const formElements = document.querySelectorAll(
'.jobs-easy-apply-modal .fb-dash-form-element, .jobs-easy-apply-modal [data-test-form-element], .fb-dash-form-element, [data-test-form-element]'
);
formElements.forEach(el => {
const label = el.querySelector('label');
if (!label) return;
// Prefer the aria-hidden span text to avoid duplication from visually-hidden span
const ariaSpan = label.querySelector('span[aria-hidden="true"]');
let text = (ariaSpan ? ariaSpan.textContent : label.textContent)
.replace(/\*/g, '')
.replace(/required/gi, '')
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!text || text.length < 5 || text.length > 300 || seen.has(text.toLowerCase())) return;
seen.add(text.toLowerCase());
// Detect input type from this form element
let inputType = 'text';
if (el.querySelector('select')) inputType = 'select';
else if (el.querySelector('input[type="radio"]')) inputType = 'radio';
else if (el.querySelector('textarea')) inputType = 'textarea';
else if (el.querySelector('input[type="checkbox"]')) inputType = 'checkbox';
questions.push({ question: text, type: inputType });
});
// Fallback: if the precise approach found nothing, scan broader inside modals
if (questions.length === 0) {
const activeModals = document.querySelectorAll('.jobs-easy-apply-modal, .artdeco-modal, [role="dialog"]');
activeModals.forEach(modal => {
const allLabels = modal.querySelectorAll('label');
allLabels.forEach(label => {
const ariaSpan = label.querySelector('span[aria-hidden="true"]');
let text = (ariaSpan ? ariaSpan.textContent : label.textContent)
.replace(/\*/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return;
const looksLikeQuestion = (
text.endsWith('?') ||
/^(how many|what|do you|are you|have you|will you|describe|tell us)/i.test(text) ||
/salary|experience|education|degree|visa|relocat|notice|certif/i.test(text)
);
if (!looksLikeQuestion) return;
seen.add(text.toLowerCase());
questions.push({ question: text, type: 'text' });
});
});
}
console.log('[LJA] Scraped questions:', questions);
return questions.slice(0, 15);
}
// ─── Overlay Injection ───────────────────────────────────────────────────
function injectOverlay() {
if (document.getElementById('lja-root')) return;
const root = document.createElement('div');
root.id = 'lja-root';
document.body.appendChild(root);
const jobData = extractJobData();
root.innerHTML = buildOverlayHTML(jobData);
initOverlayLogic(root, jobData);
}
function buildOverlayHTML(job) {
const hasJob = !!(job.jobTitle || job.description);
return `
🔍
${job.jobTitle || 'Unknown Position'}
${job.company || ''}${job.location ? ' · ' + job.location : ''}
${job.skills.length ? `
${job.skills.slice(0, 6).map(s => `${s}`).join('')}
` : ''}
📋 Paste job description manually
Analysis
ATS Score
Cover Letter
CV Tips
Q&A
Benefits
${['analysis', 'ats', 'coverletter', 'cvtips', 'qa', 'benefits'].map(tab => `
${tabIcon(tab)}
Click Analyze to generate
`).join('')}
`;
}
function tabIcon(tab) {
const icons = { analysis: '📊', ats: '🎯', coverletter: '✉️', cvtips: '📝', qa: '❓', benefits: '⭐' };
return icons[tab] || '🔍';
}
// ─── Overlay Logic ───────────────────────────────────────────────────────
function initOverlayLogic(root, jobData) {
let currentTab = 'analysis';
let isOpen = false;
let isDragging = false;
let dragStartX, dragStartY, panelStartX, panelStartY;
const results = {};
const toggle = root.querySelector('#lja-toggle');
const panel = root.querySelector('#lja-panel');
const analyzeBtn = root.querySelector('#lja-analyze-btn');
const copyBtn = root.querySelector('#lja-copy-btn');
const loading = root.querySelector('#lja-loading');
const loadingText = root.querySelector('#lja-loading-text');
const minimizeBtn = root.querySelector('#lja-minimize');
const manualToggle = root.querySelector('#lja-manual-toggle');
const manualSection = root.querySelector('#lja-manual-section');
const manualInput = root.querySelector('#lja-manual-input');
const useManualBtn = root.querySelector('#lja-use-manual');
// Status dot colour
const statusDot = root.querySelector('#lja-status-dot');
statusDot.style.background = jobData.jobTitle ? '#00d67e' : '#ffb347';
// ── Toggle panel open/close
toggle.addEventListener('click', () => {
isOpen = !isOpen;
panel.style.display = isOpen ? 'flex' : 'none';
toggle.style.display = isOpen ? 'none' : 'flex';
});
minimizeBtn.addEventListener('click', () => {
isOpen = false;
panel.style.display = 'none';
toggle.style.display = 'flex';
});
// ── Drag panel
const header = root.querySelector('#lja-header');
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
const rect = panel.getBoundingClientRect();
dragStartX = e.clientX;
dragStartY = e.clientY;
panelStartX = rect.right;
panelStartY = rect.bottom;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = dragStartX - e.clientX;
const dy = dragStartY - e.clientY;
const newRight = Math.max(8, Math.min(window.innerWidth - 200, panelStartX + dx));
const newBottom = Math.max(8, Math.min(window.innerHeight - 100, panelStartY + dy));
panel.style.right = newRight + 'px';
panel.style.bottom = newBottom + 'px';
});
document.addEventListener('mouseup', () => { isDragging = false; });
// ── Manual toggle
manualToggle.addEventListener('click', () => {
const expanded = manualInput.style.display === 'block';
manualInput.style.display = expanded ? 'none' : 'block';
useManualBtn.style.display = expanded ? 'none' : 'block';
});
useManualBtn.addEventListener('click', () => {
const text = manualInput.value.trim();
if (text.length < 50) {
showPanelToast(root, 'Please paste a longer job description');
return;
}
jobData.description = text;
jobData.jobTitle = jobData.jobTitle || 'Pasted Job';
root.querySelector('#lja-job-title').textContent = jobData.jobTitle;
root.querySelector('#lja-status-text').textContent = 'Manual mode — ready to analyze';
statusDot.style.background = '#6c63ff';
showPanelToast(root, '✅ Manual text applied');
});
// ── Tab switching
root.querySelectorAll('.lja-tab').forEach(btn => {
btn.addEventListener('click', () => {
currentTab = btn.dataset.tab;
root.querySelectorAll('.lja-tab').forEach(b => b.classList.remove('active'));
root.querySelectorAll('.lja-tab-label').forEach(b => b.classList.remove('active'));
root.querySelectorAll('.lja-pane').forEach(p => p.style.display = 'none');
btn.classList.add('active');
root.querySelector(`.lja-tab-label[data-tab="${currentTab}"]`).classList.add('active');
root.querySelector(`#lja-pane-${currentTab}`).style.display = 'block';
// Show copy button if we have content
copyBtn.style.display = results[currentTab] ? 'block' : 'none';
});
});
// ── Analyze button
analyzeBtn.addEventListener('click', async () => {
if (!jobData.description && !jobData.jobTitle) {
showPanelToast(root, '⚠️ No job detected. Use manual mode.');
return;
}
const settings = await getSettings();
if (!settings.apiKey) {
showPanelToast(root, '⚠️ Set your API key in extension settings first!');
return;
}
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,
jobTitle: jobData.jobTitle || 'Job',
action: 'generatePdf',
// 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);
});
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);
// 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');
}
} catch (e) {
showPanelToast(root, '❌ PDF Error: ' + e.message, 'error');
} finally {
pdfBtn.textContent = '📥 Get ATS PDF';
pdfBtn.disabled = false;
}
});
}
// ── Copy button
copyBtn.addEventListener('click', () => {
if (results[currentTab]) {
navigator.clipboard.writeText(results[currentTab])
.then(() => showPanelToast(root, '📋 Copied!'))
.catch(() => showPanelToast(root, 'Copy failed'));
}
});
}
// ─── Run Analysis ────────────────────────────────────────────────────────
async function runAnalysis(settings, jobData, tab, results, root, loading, loadingText, copyBtn) {
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...'
};
loading.style.display = 'flex';
loadingText.textContent = loadingMsgs[tab] || 'Generating...';
const pane = root.querySelector(`#lja-pane-${tab}`);
const analyzeBtn = root.querySelector('#lja-analyze-btn');
analyzeBtn.disabled = true;
try {
if (tab === 'qa') {
const freshData = extractApplicationQuestions();
if (freshData.length > 0) {
jobData.questions = freshData;
}
}
const prompt = buildPromptV2(tab, jobData, settings.userProfile, settings.language);
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: { apiKey: settings.apiKey, prompt, tab }
});
if (!response.success) {
throw new Error(response.error);
}
const text = response.data.text;
results[tab] = text;
// Render markdown-like output
const isArabic = /[\u0600-\u06FF]/.test(text);
const rtlAttr = isArabic ? 'dir="rtl" style="text-align: right; padding-right: 15px;"' : '';
// For Q&A, show how many questions were detected
const qaHeader = (tab === 'qa' && jobData.questions)
? `
🔍 Detected ${jobData.questions.length} question(s) from Easy Apply form
`
: '';
pane.innerHTML = `
${qaHeader}
${renderMarkdown(text)}
`;
// Attach inline copy button listeners
if (tab === 'qa') {
pane.querySelectorAll('.lja-qa-copy-btn').forEach(btn => {
btn.addEventListener('click', function() {
navigator.clipboard.writeText(this.getAttribute('data-answer'));
this.textContent = '✅';
setTimeout(() => this.textContent = '📋', 1200);
});
});
}
copyBtn.style.display = 'block';
// Show auto-fill button for Q&A tab
if (tab === 'qa') {
const fillBtn = document.createElement('button');
fillBtn.className = 'lja-fill-btn';
fillBtn.textContent = '📝 Auto-fill Answers';
fillBtn.addEventListener('click', () => autoFillAnswers(text, root));
pane.appendChild(fillBtn);
}
} catch (err) {
pane.innerHTML = `❌ ${err.message}
`;
} finally {
loading.style.display = 'none';
analyzeBtn.disabled = false;
}
}
// ─── Markdown Renderer ───────────────────────────────────────────────────
function renderMarkdown(text) {
try {
const startIdx = text.indexOf('{');
const endIdx = text.lastIndexOf('}');
if (startIdx !== -1 && endIdx !== -1) {
const parsed = JSON.parse(text.substring(startIdx, endIdx + 1));
let html = '';
const entries = Object.entries(parsed);
entries.forEach(([q, a], idx) => {
const safeAnswer = String(a).replace(/"/g, '"').replace(/'/g, ''');
html += `
`;
});
html += '
';
return html;
}
} catch(e) {
// Not JSON, continue to normal markdown rendering
}
return text
.replace(/&/g, '&').replace(//g, '>')
.replace(/^## (.+)$/gm, '$1
')
.replace(/^### (.+)$/gm, '$1
')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/^- (.+)$/gm, '$1')
.replace(/(.*<\/li>(\n|$))+/g, m => '')
.replace(/\n{2,}/g, '')
.replace(/\n/g, '
')
.replace(/^(?!<[hup])(.+)$/gm, (m) => m.startsWith('<') ? m : '
' + m + '
');
}
// ─── Auto-fill ───────────────────────────────────────────────────────────
function autoFillAnswers(qaText, root) {
showPanelToast(root, '🔄 Attempting to fill answers...');
let answers = {};
try {
const startIndex = qaText.indexOf('{');
const endIndex = qaText.lastIndexOf('}');
if (startIndex !== -1 && endIndex !== -1) {
answers = JSON.parse(qaText.substring(startIndex, endIndex + 1));
}
} catch (e) {
console.error('Failed to parse AI answers JSON:', e);
}
if (Object.keys(answers).length === 0) {
showPanelToast(root, '⚠️ Could not parse answers. Please copy manually.');
return;
}
let filled = 0;
const labels = document.querySelectorAll(
'.jobs-easy-apply-content label, .jobs-easy-apply-content legend, ' +
'.artdeco-modal label, .artdeco-modal legend, ' +
'[role="dialog"] label, [role="dialog"] legend, ' +
'#artdeco-modal-outlet label, #artdeco-modal-outlet legend'
);
labels.forEach(label => {
// Use same clean-text logic as the scraper
const ariaSpan = label.querySelector('span[aria-hidden="true"]');
const qText = (ariaSpan ? ariaSpan.textContent : label.textContent)
.replace(/\*/g, '').replace(/required/gi, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
if (!qText || qText.length < 5) return;
const matchedKey = Object.keys(answers).find(k => {
const kLower = k.toLowerCase().trim();
return kLower.includes(qText) || qText.includes(kLower);
});
if (matchedKey && answers[matchedKey]) {
const parent = label.closest('.fb-dash-form-element') || label.parentElement;
const answerVal = String(answers[matchedKey]);
// 1. Fill Text inputs / Textareas
const input = parent.querySelector('input[type="text"], textarea');
if (input) {
input.value = answerVal;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
filled++;
}
// 2. Select dropdowns
const select = parent.querySelector('select');
if (select) {
const options = Array.from(select.options);
const matchedOpt = options.find(o => o.text.toLowerCase() === answerVal.toLowerCase() || o.value.toLowerCase() === answerVal.toLowerCase() || o.text.toLowerCase().includes(answerVal.toLowerCase()));
if (matchedOpt) {
select.value = matchedOpt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
filled++;
}
}
// 3. Radio Buttons (Yes/No)
const radios = parent.querySelectorAll('input[type="radio"]');
if (radios.length > 0) {
let radioClicked = false;
radios.forEach(radio => {
const radioLabel = radio.parentElement.textContent.trim().toLowerCase();
const aLower = answerVal.toLowerCase();
if (radioLabel === aLower || (aLower === 'yes' && radioLabel.includes('yes')) || (aLower === 'no' && radioLabel.includes('no'))) {
radio.click();
radioClicked = true;
}
});
if (radioClicked) filled++;
}
}
});
showPanelToast(root, filled > 0 ? `✅ Filled ${filled} fields` : '⚠️ No matching fields found to auto-fill.');
}
// ─── 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 (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);
});
});
}
function showPanelToast(root, msg) {
let toast = root.querySelector('#lja-panel-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'lja-panel-toast';
root.querySelector('#lja-panel').appendChild(toast);
}
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ─── List Scanner ────────────────────────────────────────────────────────
function injectListScanner() {
if (document.getElementById('lja-scan-btn')) return;
const btn = document.createElement('button');
btn.id = 'lja-scan-btn';
btn.innerHTML = '🔍 Scan List';
btn.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
z-index: 999999;
background: linear-gradient(135deg, #00d67e, #00a65e);
color: white;
border: none;
border-radius: 50px;
padding: 12px 24px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 214, 126, 0.4);
transition: transform 0.2s;
font-family: Arial, sans-serif;
`;
btn.onmouseover = () => btn.style.transform = 'scale(1.05)';
btn.onmouseout = () => btn.style.transform = 'scale(1)';
btn.onclick = async () => {
btn.innerHTML = '⏳ Scanning...';
try {
await scanJobList();
} catch (e) {
console.error(e);
alert('Scan failed: ' + e.message);
}
btn.innerHTML = '🔍 Scan List';
};
document.body.appendChild(btn);
}
async function scanJobList() {
const listItems = document.querySelectorAll('.jobs-search-results__list-item, .job-card-container');
if (!listItems.length) {
alert('No job items found on this page. Scroll down to load them.');
return;
}
const jobsToScan = [];
listItems.forEach((item, index) => {
const titleEl = item.querySelector('.job-card-list__title, .artdeco-entity-lockup__title');
const companyEl = item.querySelector('.job-card-container__primary-description, .artdeco-entity-lockup__subtitle');
if (titleEl && companyEl) {
jobsToScan.push({
index: index,
title: titleEl.textContent.trim().replace(/\n/g, ''),
company: companyEl.textContent.trim().replace(/\n/g, ''),
element: item
});
}
});
if (!jobsToScan.length) {
alert('Could not parse job titles.');
return;
}
document.querySelectorAll('.lja-badge').forEach(b => b.remove());
if (jobsToScan.length === 0) return;
// Limit to 25 jobs per scan to prevent AI token truncation
const jobsToProcess = jobsToScan.slice(0, 25);
const listDataStr = JSON.stringify(jobsToProcess.map(j => ({ index: j.index, title: j.title, company: j.company })));
const settings = await getSettings();
if (!settings.apiKey) {
alert('Please set your API key in the extension popup first.');
return;
}
if (typeof buildPromptV2 !== 'function') {
alert('buildPromptV2 not found. Extension error.');
return;
}
const promptStr = buildPromptV2('list_analysis', { skills: [], listData: listDataStr }, settings.userProfile, settings.language);
const response = await chrome.runtime.sendMessage({
type: 'GEMINI_REQUEST',
payload: { apiKey: settings.apiKey, prompt: promptStr, tab: 'list_analysis' }
});
if (!response.success) {
console.error('[LJA] Background Error:', response.error);
throw new Error('API Error: ' + response.error);
}
let resultText = response.data.text;
resultText = resultText.replace(/```json/gi, '').replace(/```/g, '').trim();
let results;
try {
const startIdx = resultText.indexOf('[');
const endIdx = resultText.lastIndexOf(']');
if (startIdx !== -1 && endIdx !== -1) {
results = JSON.parse(resultText.substring(startIdx, endIdx + 1));
} else {
results = JSON.parse(resultText);
}
} catch(e) {
console.error("AI JSON Parse Error. Raw Response:", resultText);
throw new Error('JSON Error: ' + resultText.substring(0, 80));
}
results.forEach(res => {
const jobItem = jobsToScan.find(j => j.index === res.index);
if (jobItem) {
const badge = document.createElement('div');
badge.className = 'lja-badge';
badge.style.cssText = `
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: bold;
margin-top: 6px;
color: white;
width: 100%;
box-sizing: border-box;
`;
if (res.verdict === 'YES') {
badge.style.background = 'linear-gradient(135deg, #00d67e, #00a65e)';
badge.innerHTML = `✅ MATCH: ${res.reason}`;
} else if (res.verdict === 'NO') {
badge.style.background = 'linear-gradient(135deg, #ff4d6d, #d90429)';
badge.innerHTML = `❌ SKIP: ${res.reason}`;
} else {
badge.style.background = 'linear-gradient(135deg, #ffb347, #ff9200)';
badge.innerHTML = `⚠️ MAYBE: ${res.reason}`;
}
const contentContainer = jobItem.element.querySelector('.artdeco-entity-lockup__content');
if (contentContainer) contentContainer.appendChild(badge);
}
});
}
// ─── SPA Navigation Observer ─────────────────────────────────────────────
let lastUrl = location.href;
let lastJobId = new URLSearchParams(location.search).get('currentJobId') || '';
let debounceTimer;
function getJobIdFromUrl() {
const params = new URLSearchParams(location.search);
return params.get('currentJobId') || '';
}
function refreshOverlay() {
const old = document.getElementById('lja-root');
if (old) old.remove();
window.__linkedinAnalyzerLoaded = false;
setTimeout(() => { injectOverlay(); injectListScanner(); }, 1200);
}
// Watch for URL changes (SPA navigation + job switches)
const observer = new MutationObserver(() => {
const currentUrl = location.href;
const currentJobId = getJobIdFromUrl();
// Job switched via sidebar click (URL param changed)
if (currentJobId && currentJobId !== lastJobId) {
lastJobId = currentJobId;
lastUrl = currentUrl;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refreshOverlay, 600);
return;
}
// Full URL change (different page)
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
lastJobId = currentJobId;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (currentUrl.includes('/jobs/')) {
refreshOverlay();
}
}, 800);
}
});
observer.observe(document.body, { childList: true, subtree: true });
// ─── Init ─────────────────────────────────────────────────────────────────
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(() => { injectOverlay(); injectListScanner(); }, 1000));
} else {
setTimeout(() => { injectOverlay(); injectListScanner(); }, 1000);
}
}
init();
})();