// 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(); // Strategy 1: Look inside known modal containers const containers = document.querySelectorAll( '.artdeco-modal, .jobs-easy-apply-modal, #artdeco-modal-outlet, ' + '.jobs-easy-apply-content, .jobs-easy-apply-form-section__grouping, ' + '[data-test-modal], [role="dialog"]' ); // Strategy 2: If no modal found, scan the full page const searchRoots = containers.length > 0 ? containers : [document.body]; searchRoots.forEach(container => { // Scan labels, legends, spans that look like question text const candidates = container.querySelectorAll( 'label, legend, .fb-dash-form-element__label, ' + '.jobs-easy-apply-form-element__label, ' + 'span.t-14, span.t-bold' ); candidates.forEach(el => { let text = el.textContent.replace(/\*/g, '').replace(/required/gi, '').trim(); // Remove "Select an option" and similar noise text = text.replace(/Select an option/gi, '').replace(/Show less|Show more/gi, '').trim(); if (!text || text.length < 8 || text.length > 300 || seen.has(text.toLowerCase())) return; // Only accept text that looks like a question or form label const isQuestion = ( text.endsWith('?') || /^(how many|what|do you|are you|have you|will you|can you|would you|is your|describe|tell us)/i.test(text) || /salary|experience|education|degree|visa|relocat|notice period|start date|certif/i.test(text) ); if (!isQuestion) return; seen.add(text.toLowerCase()); // Detect input type from nearest form element let inputType = 'text'; const parent = el.closest('.fb-dash-form-element, .jobs-easy-apply-form-section__grouping, div') || el.parentElement; if (parent) { if (parent.querySelector('select')) inputType = 'select'; else if (parent.querySelector('input[type="radio"]')) inputType = 'radio'; else if (parent.querySelector('textarea')) inputType = 'textarea'; else if (parent.querySelector('input[type="checkbox"]')) inputType = 'checkbox'; else if (parent.querySelector('input[type="number"]')) inputType = 'number'; } questions.push({ question: text, type: inputType }); }); }); // Strategy 3: Last resort — scan ALL visible text for question patterns if (questions.length === 0) { const allSpans = document.querySelectorAll('span, label, legend, p'); allSpans.forEach(el => { const text = el.textContent.replace(/\*/g, '').trim(); if (text && text.endsWith('?') && text.length > 10 && text.length < 200 && !seen.has(text.toLowerCase())) { // Make sure this is a visible form question, not random page text const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.top < window.innerHeight) { 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 => { const qText = label.textContent.trim().toLowerCase(); if (!qText) return; const matchedKey = Object.keys(answers).find(k => { const kLower = k.toLowerCase(); 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 ─────────────────────────────────────────────────────────── function getSettings() { return new Promise(resolve => { chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], resolve); }); } 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); } // ─── 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, 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, 1000)); } else { setTimeout(injectOverlay, 1000); } } init(); })();