// 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 `
')
.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 ─────────────────────────────────────────────────────────── function getSettings() { return new Promise(resolve => { chrome.storage.sync.get(['apiKey', 'userProfile', 'language'], (data) => { if (data.userProfile && (data.userProfile.includes('hamzaayed@intaleqapp.com') || data.userProfile.includes('hamzaayedflutter@gmail.com'))) { data.userProfile = data.userProfile .replace(/hamzaayed@intaleqapp\.com/g, 'hamzaayed@tripz-egypt.com') .replace(/hamzaayedflutter@gmail\.com/g, 'hamzaayed@tripz-egypt.com'); 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()); const listDataStr = JSON.stringify(jobsToScan.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 fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${settings.apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: promptStr }] }], generationConfig: { maxOutputTokens: 2048, temperature: 0.1 } }) } ); if (!response.ok) throw new Error('API Request failed'); const data = await response.json(); let resultText = data.candidates[0].content.parts[0].text; resultText = resultText.replace(/```json/g, '').replace(/```/g, '').trim(); let results; try { const match = resultText.match(/\[[\s\S]*\]/); if (match) { results = JSON.parse(match[0]); } else { results = JSON.parse(resultText); } } catch(e) { console.error(resultText); throw new Error('Failed to parse AI response'); } 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(); })();