870 lines
31 KiB
JavaScript
870 lines
31 KiB
JavaScript
// 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 formSelectors = [
|
||
'.jobs-easy-apply-content',
|
||
'.jobs-easy-apply-form-section__grouping',
|
||
'.fb-dash-form-element'
|
||
];
|
||
|
||
for (const sel of formSelectors) {
|
||
document.querySelectorAll(sel).forEach(section => {
|
||
// Labels
|
||
const labels = section.querySelectorAll('label, legend, .fb-dash-form-element__label');
|
||
labels.forEach(label => {
|
||
const text = label.textContent.trim();
|
||
if (text && text.length > 3 && text.length < 300) {
|
||
// Determine input type
|
||
const parent = label.closest('.fb-dash-form-element') || section;
|
||
let inputType = 'text';
|
||
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';
|
||
|
||
questions.push({ question: text, type: inputType });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
return questions.slice(0, 15); // Max 15 questions
|
||
}
|
||
|
||
// ─── 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="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="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', '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: '📊', 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,
|
||
action: 'generatePdf'
|
||
}
|
||
}, 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);
|
||
|
||
showPanelToast(root, '✅ PDF Downloaded Successfully!', '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...',
|
||
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 {
|
||
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;"' : '';
|
||
|
||
pane.innerHTML = `
|
||
<div class="lja-result ${response.data.fromCache ? 'lja-cached' : ''}" ${rtlAttr}>
|
||
${response.data.fromCache ? '<div class="lja-cache-badge">⚡ Cached result</div>' : ''}
|
||
${renderMarkdown(text)}
|
||
</div>
|
||
`;
|
||
|
||
copyBtn.style.display = 'block';
|
||
|
||
// Show auto-fill button for Q&A tab
|
||
if (tab === 'qa' && jobData.questions.length > 0) {
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ─── Prompt Builder ──────────────────────────────────────────────────────
|
||
|
||
function buildPrompt(tab, job, userProfile, language) {
|
||
const langInstruction = language === 'arabic'
|
||
? 'Respond entirely in Arabic.'
|
||
: language === 'english'
|
||
? 'Respond entirely in English.'
|
||
: 'Respond in the same language as the job posting (Arabic or English).';
|
||
|
||
const jobContext = `
|
||
Job Title: ${job.jobTitle || 'Not specified'}
|
||
Company: ${job.company || 'Not specified'}
|
||
Location: ${job.location || 'Not specified'}
|
||
Description:
|
||
${job.description || 'No description available'}
|
||
${job.skills.length ? `\nRequired Skills: ${job.skills.join(', ')}` : ''}`.trim();
|
||
|
||
const profileBlock = `MY PROFILE:\n${userProfile}`;
|
||
|
||
const prompts = {
|
||
analysis: `You are a senior career advisor specializing in tech roles in the Middle East.
|
||
${langInstruction}
|
||
Analyze this job posting against my profile. Be direct and honest.
|
||
|
||
${profileBlock}
|
||
|
||
JOB POSTING:
|
||
${jobContext}
|
||
|
||
Provide in this exact format:
|
||
## MATCH SCORE: X/100
|
||
Brief explanation.
|
||
|
||
## ✅ STRONG MATCHES
|
||
- bullet points of matching skills/experience
|
||
|
||
## ⚠️ GAPS
|
||
- bullet points of missing requirements or weak areas
|
||
|
||
## 🎯 RECOMMENDATION
|
||
Apply / Apply with preparation / Skip — with clear reason
|
||
|
||
## 💡 KEY SELLING POINTS
|
||
- what to emphasize when applying
|
||
|
||
## 🏷️ BEST CV VARIANT
|
||
Which of my CV variants (Solutions Architect / Flutter Developer / GIS Developer / FinTech Engineer) best fits and why`,
|
||
|
||
coverletter: `Write a compelling, personalized cover letter for this position.
|
||
${langInstruction}
|
||
|
||
${profileBlock}
|
||
|
||
JOB:
|
||
${jobContext}
|
||
|
||
Requirements:
|
||
- 3 paragraphs maximum
|
||
- Reference specific requirements from the job description
|
||
- Highlight my most relevant achievements with numbers
|
||
- Professional but confident tone — not generic
|
||
- End with a strong call to action
|
||
- Do NOT use placeholder brackets — write a complete ready-to-send letter
|
||
- Address to "Hiring Team" if no specific name is available
|
||
- Sign as "Hamza Ayed"`,
|
||
|
||
cvtips: `Based on this job posting, provide specific CV optimization advice.
|
||
${langInstruction}
|
||
|
||
${profileBlock}
|
||
|
||
JOB:
|
||
${jobContext}
|
||
|
||
Provide:
|
||
## 📌 SUGGESTED HEADLINE
|
||
A LinkedIn-style headline optimized for this specific role (max 120 characters)
|
||
|
||
## 📋 SUGGESTED SUMMARY
|
||
3-4 sentence professional summary tailored for this role
|
||
|
||
## 🔑 KEYWORDS TO ADD
|
||
- specific keywords from the job to add to CV
|
||
|
||
## ⬆️ EXPERIENCE TO PRIORITIZE
|
||
- which of my experiences to move to top or emphasize
|
||
|
||
## ✂️ WHAT TO DE-EMPHASIZE
|
||
- what to remove or minimize for this application
|
||
|
||
## 🎯 SKILLS SECTION
|
||
- specific skills to list for this role`,
|
||
|
||
qa: `I am applying for this job and need to answer application questions.
|
||
${langInstruction}
|
||
Answer each question based on my profile. Be specific, professional, and concise.
|
||
|
||
${profileBlock}
|
||
|
||
JOB:
|
||
${jobContext}
|
||
|
||
${job.questions.length > 0
|
||
? 'QUESTIONS TO ANSWER:\n' + job.questions.map((q, i) => `${i + 1}. [${q.type}] ${q.question}`).join('\n')
|
||
: 'Generate answers for these common application questions:\n1. Why are you interested in this role?\n2. What is your relevant experience?\n3. What are your salary expectations?\n4. When can you start?\n5. Do you require visa sponsorship?\n6. What is your current notice period?'}
|
||
|
||
For each question provide a clear, professional answer ready to paste. Keep answers concise (2-4 sentences for text fields).`,
|
||
|
||
benefits: `Analyze this job posting and extract all benefits and advantages.
|
||
${langInstruction}
|
||
|
||
JOB:
|
||
${jobContext}
|
||
|
||
Provide:
|
||
## 💰 COMPENSATION & BENEFITS
|
||
- salary, bonuses, equity if mentioned; otherwise state "Not specified"
|
||
|
||
## 🏢 WORK ARRANGEMENT
|
||
- remote/hybrid/onsite, flexibility, hours
|
||
|
||
## 📈 GROWTH OPPORTUNITIES
|
||
- career development, training, advancement potential
|
||
|
||
## 🌟 COMPANY PERKS
|
||
- insurance, vacation, equipment, culture perks etc.
|
||
|
||
## ⚠️ POTENTIAL CONCERNS
|
||
- red flags, very demanding requirements, unclear aspects
|
||
|
||
## 📊 OVERALL ATTRACTIVENESS: X/10
|
||
Brief honest assessment of this opportunity for my profile`
|
||
};
|
||
|
||
return prompts[tab] || prompts.analysis;
|
||
}
|
||
|
||
// ─── Markdown Renderer ───────────────────────────────────────────────────
|
||
|
||
function renderMarkdown(text) {
|
||
return text
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.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...');
|
||
// Parse numbered answers from AI response
|
||
const answerLines = qaText.split('\n');
|
||
const inputs = document.querySelectorAll('.jobs-easy-apply-content input[type="text"], .jobs-easy-apply-content textarea');
|
||
|
||
let filled = 0;
|
||
inputs.forEach((input, idx) => {
|
||
const answerLine = answerLines.find(l => l.match(new RegExp(`^${idx + 1}\\.`)));
|
||
if (answerLine) {
|
||
const answer = answerLine.replace(/^\d+\.\s*/, '').trim();
|
||
if (answer) {
|
||
input.value = answer;
|
||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
filled++;
|
||
}
|
||
}
|
||
});
|
||
|
||
showPanelToast(root, filled > 0 ? `✅ Filled ${filled} fields` : '⚠️ Could not auto-fill. Copy answers manually.');
|
||
}
|
||
|
||
// ─── 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();
|
||
|
||
})();
|