// 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 ''; } // LinkedIn's job panel is full of text that is neither navigation nor the // job title: screen-reader-only headings ("0 notifications"), and product // furniture inside the detail column itself ("Are these results helpful?", // "Your profile and resume match...", "Get personalized tips..."). A // heuristic DOM scan cannot reliably tell those from a job title, so the // scan is now the LAST resort rather than the second. 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 = new RegExp([ // chrome / navigation 'notification', 'message', 'invitation', 'my network', 'feed update', 'premium', 'search', 'similar', 'featured', 'recommended', // in-panel product furniture — the class of string that broke this before 'helpful', 'results', 'feedback', 'your profile', 'match', 'tips', 'people you', 'about the job', 'sign in', 'join now', 'people also', 'jobs based on', 'reach out', // counters '^\\d+\\s' ].join('|'), 'i'); function plausibleTitle(text) { return text.length > 4 && text.length < 100 && !NAV_NOISE.test(text); } // 1. Most Reliable Method: Extract from active job link using currentJobId let currentJobId = new URLSearchParams(window.location.search).get('currentJobId'); if (!currentJobId) { const match = window.location.pathname.match(/\/jobs\/view\/(\d+)/); if (match) currentJobId = match[1]; } if (currentJobId) { // Find all links containing this job ID const links = document.querySelectorAll(`a[href*="/jobs/view/${currentJobId}"]`); for (const link of links) { const text = (link.textContent || '').trim(); // Filter out empty or obviously wrong texts (like "Save", "Easy Apply") if (text && plausibleTitle(text) && !text.includes('Easy Apply') && !text.includes('Save')) { data.jobTitle = text; break; } } } // 2. Fallback: Try known selectors if (!data.jobTitle) { data.jobTitle = trySelectors([ '.job-details-jobs-unified-top-card__job-title h1 a', '.job-details-jobs-unified-top-card__job-title h1', '.jobs-unified-top-card__job-title h1', '.jobs-unified-top-card__job-title', '.job-details-jobs-unified-top-card__job-title', 'h2.job-details-jobs-unified-top-card__job-title', '.jobs-unified-top-card__job-title a', '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', ]); // If we grabbed garbage, reset it if (data.jobTitle && !plausibleTitle(data.jobTitle)) { data.jobTitle = ''; } } // 3. Last Resort Fallback: Browser tab title if (!data.jobTitle) { const m = document.title.match(/^(?:\(\d+\+?\)\s*)?(.+?)\s*[|·]/); if (m) { const candidate = m[1].trim(); if (plausibleTitle(candidate) && !/^linkedin$/i.test(candidate)) { data.jobTitle = candidate; } } } // Last resort: 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) { // h1 first: the job title is a level-1 heading, while the cards that // polluted this before ("Are these results helpful?") are h2/h3. for (const h of [...jobPanel.querySelectorAll('h1'), ...jobPanel.querySelectorAll('h2')]) { const text = h.textContent.trim(); if (isVisible(h) && plausibleTitle(text)) { 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 `
')
.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, [componentkey^="job-card-component-ref"], [data-job-id]'); if (!listItems.length) { alert('No job items found on this page. Scroll down to load them.'); return; } const jobsToScan = []; listItems.forEach((item, index) => { let title = ''; let company = ''; 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) { title = titleEl.textContent; company = companyEl.textContent; } else { // Fallback for obfuscated classes: use innerText const textContent = item.innerText || ''; const lines = textContent.split('\n') .map(t => t.trim()) .filter(t => t.length > 0 && !t.includes('promoted jobs are ranked') && !t.includes('Actively reviewing')); let startIndex = 0; if (lines[0] === 'Promoted') startIndex = 1; if (lines.length >= startIndex + 2) { title = lines[startIndex].replace(/\(Verified job\)/i, '').trim(); company = lines[startIndex + 1]; } } if (title && company) { jobsToScan.push({ index: index, title: title.replace(/\n/g, ''), company: company.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(); })();