Files
cv/content.js
T

1069 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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',
]);
// Fallback: find any h1 in the job details area
if (!data.jobTitle) {
const jobPanel = document.querySelector('.scaffold-layout__detail-column, .jobs-search__job-details, .job-view-layout');
if (jobPanel) {
const h1 = jobPanel.querySelector('h1');
if (h1 && h1.textContent.trim()) data.jobTitle = h1.textContent.trim();
}
}
// Last resort: any bold h2 in main area
if (!data.jobTitle) {
const allH2 = document.querySelectorAll('h2');
for (const h of allH2) {
const text = h.textContent.trim();
if (text.length > 4 && text.length < 100 && !text.match(/jobs|people|similar|featured/i)) {
data.jobTitle = text;
break;
}
}
}
// ── 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 `
<div id="lja-toggle" title="LinkedIn Job Analyzer">🔍</div>
<div id="lja-panel">
<div id="lja-header">
<div id="lja-title">
<span>🔍</span>
<div>
<div id="lja-panel-name">Job Analyzer</div>
<div id="lja-status-bar">
<span id="lja-status-dot"></span>
<span id="lja-status-text">${hasJob ? 'Job detected' : 'No job detected — use manual mode'}</span>
</div>
</div>
</div>
<div id="lja-header-actions">
<button id="lja-minimize" title="Minimize">−</button>
</div>
</div>
<!-- Job preview -->
<div id="lja-job-preview">
<div id="lja-job-title">${job.jobTitle || 'Unknown Position'}</div>
<div id="lja-job-company">${job.company || ''}${job.location ? ' · ' + job.location : ''}</div>
${job.skills.length ? `<div id="lja-skills">${job.skills.slice(0, 6).map(s => `<span class="lja-skill-tag">${s}</span>`).join('')}</div>` : ''}
</div>
<!-- Manual mode -->
<div id="lja-manual-section">
<div id="lja-manual-toggle">📋 Paste job description manually</div>
<textarea id="lja-manual-input" placeholder="Paste the full job description here..."></textarea>
<button id="lja-use-manual" class="lja-btn-secondary">Use this text</button>
</div>
<!-- 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>
<button class="lja-tab" data-tab="benefits">⭐</button>
</div>
<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>
<span class="lja-tab-label" data-tab="benefits">Benefits</span>
</div>
<!-- Tab panes -->
<div id="lja-content">
${['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>
<div class="lja-empty-text">Click <strong>Analyze</strong> to generate</div>
</div>
</div>
`).join('')}
</div>
<!-- Actions -->
<div id="lja-actions" style="display: flex; gap: 8px;">
<button id="lja-pdf-btn" style="background: #1a237e; border: 1px solid #3949ab; color: white; padding: 10px; border-radius: 6px; font-weight: 600; cursor: pointer; flex: 1; transition: 0.2s;">📥 Get ATS PDF</button>
<button id="lja-analyze-btn" style="flex: 1.5;">⚡ Analyze Job</button>
<button id="lja-copy-btn" style="display:none; flex: 0.5;">📋 Copy</button>
</div>
<!-- Loading -->
<div id="lja-loading" style="display:none">
<div class="lja-spinner"></div>
<div id="lja-loading-text">Analyzing...</div>
</div>
</div>
`;
}
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)
? `<div style="background:rgba(108,99,255,0.2);padding:6px 10px;border-radius:6px;margin-bottom:10px;font-size:12px;color:#b0b0ff;">
🔍 Detected <strong>${jobData.questions.length}</strong> question(s) from Easy Apply form
</div>`
: '';
pane.innerHTML = `
${qaHeader}
<div class="lja-result" ${rtlAttr}>
${renderMarkdown(text)}
</div>
`;
// 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 = `<div class="lja-error">❌ ${err.message}</div>`;
} 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 = '<div style="display:flex;flex-direction:column;gap:10px;">';
const entries = Object.entries(parsed);
entries.forEach(([q, a], idx) => {
const safeAnswer = String(a).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
html += `<div style="background: rgba(255,255,255,0.06); padding: 10px 12px; border-radius: 8px; border-left: 3px solid #6c63ff;">
<div style="font-weight: 600; font-size: 12px; color: #aaa; margin-bottom: 4px;">❓ ${q}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<div style="color: #4caf50; font-size: 14px; font-weight: 500; flex: 1;" id="lja-qa-answer-${idx}">💡 ${a}</div>
<button class="lja-qa-copy-btn" data-answer="${safeAnswer}" style="background: rgba(108,99,255,0.2); border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; color: #b0b0ff; font-size: 14px; flex-shrink: 0;" title="Copy answer">📋</button>
</div>
</div>`;
});
html += '</div>';
return html;
}
} catch(e) {
// Not JSON, continue to normal markdown rendering
}
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>(\n|$))+/g, m => '<ul>' + m + '</ul>')
.replace(/\n{2,}/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/^(?!<[hup])(.+)$/gm, (m) => m.startsWith('<') ? m : '<p>' + m + '</p>');
}
// ─── 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();
})();