= 75) return { label:'ATS Friendly', color:'#16a34a', bg:'rgba(22,163,74,0.12)', border:'rgba(22,163,74,0.30)' }; if (s >= 51) return { label:'Needs Improvement', color:'#b45309', bg:'rgba(180,83,9,0.10)', border:'rgba(180,83,9,0.28)' }; return { label:'High ATS Risk', color:'#b91c1c', bg:'rgba(185,28,28,0.09)', border:'rgba(185,28,28,0.26)' }; } function setStep(n) { for (var i = 1; i <= 4; i++) { document.getElementById('step-' + i).className = 'atsStep' + (i < n ? ' done' : i === n ? ' active' : ''); } } function animateCount(target, duration) { var el = document.getElementById('r-score'); var cur = 0; var step = Math.max(1, Math.ceil(duration / Math.max(target, 1))); var timer = setInterval(function () { cur = Math.min(cur + 1, target); el.textContent = cur; if (cur >= target) clearInterval(timer); }, step); } function animateBar(id, pct, color) { setTimeout(function () { var el = document.getElementById(id); if (!el) return; el.style.width = pct + '%'; if (color) el.style.background = color; }, 120); } /* ─── Keyword extraction ────────────────── */ function extractKeywords(text, limit) { var freq = {}; text.toLowerCase() .replace(/[^a-z0-9\s\+\#\.]/g, ' ') .split(/\s+/) .forEach(function (w) { w = w.replace(/[^a-z0-9\+\#]/g, ''); if (w.length < 3 || STOP.has(w)) return; freq[w] = (freq[w] || 0) + 1; }); var arr = Object.keys(freq).map(function (k) { return { word: k, count: freq[k] }; }); arr.sort(function (a, b) { return b.count - a.count; }); return limit ? arr.slice(0, limit) : arr; } /* ─── Stemmer ───────────────────────────── */ function stem(w) { w = w.toLowerCase(); var rules = [ [/ations?$/,'ate'],[/ments?$/,''],[/nesses?$/,''],[/ings?$/,''], [/tion$/,'te'],[/ities?$/,'ity'],[/ers?$/,''],[/ied$/,'y'], [/ed$/,''],[/ly$/,''],[/ful$/,''],[/ive$/,''],[/al$/,''], [/ous$/,''],[/ence$/,''],[/ance$/,''],[/ship$/,''] ]; for (var i = 0; i < rules.length; i++) { var r = rules[i]; if (r[0].test(w)) { var res = w.replace(r[0], r[1]); if (res.length >= 3) return res; } } return w; } /* ─── Match type ────────────────────────── */ function matchType(jdWord, cvText) { var lcv = cvText.toLowerCase(); var lcw = jdWord.toLowerCase(); if (new RegExp('\\b' + lcw.replace(/[+#]/g,'\\$&') + '\\b').test(lcv)) return 'exact'; var stemJd = stem(lcw); var found = lcv.split(/\s+/).some(function (w) { return stem(w.replace(/[^a-z]/g,'')) === stemJd; }); if (found) return 'partial'; if (lcw.length >= 5 && lcv.indexOf(lcw.slice(0, lcw.length - 2)) !== -1) return 'partial'; return 'miss'; } /* ─── Count occurrences in CV ───────────── */ function countInCv(word, cvText) { var m = cvText.toLowerCase().match(new RegExp('\\b' + word.replace(/[+#]/g,'\\$&') + '\\b', 'gi')); return m ? m.length : 0; } /* ─── Section detection ─────────────────── */ function detectSections(cvText) { var r = {}; Object.keys(SECTIONS).forEach(function (k) { r[k] = SECTIONS[k].some(function (re) { return re.test(cvText); }); }); return r; } /* ─── Experience recency (15 pts) ───────── */ function scoreExperienceRecency(cvText) { var now = new Date().getFullYear(); var years = (cvText.match(/\b(19|20)\d{2}\b/g) || []).map(Number).filter(function (y) { return y >= 1970 && y <= now + 1; }); var unique = Array.from(new Set(years)).sort(function (a,b) { return b - a; }); if (unique.length === 0) return { score: 4, hasRecent: false }; var mostRecent = unique[0]; var oldest = unique[unique.length - 1]; var recency = now - mostRecent; var span = mostRecent - oldest; var pts = 0; if (recency <= 1) pts += 7; else if (recency <= 2) pts += 5; else if (recency <= 4) pts += 3; else pts += 1; if (span >= 5 && unique.length >= 4) pts += 5; else if (span >= 2) pts += 3; else pts += 1; if (/\b(present|current|now|ongoing)\b/i.test(cvText)) pts += 3; return { score: Math.min(15, pts), hasRecent: recency <= 2 }; } /* ─── Formatting quality (10 pts) ────────── */ function scoreFormatting(cvText) { var pts = 10; var warn = []; var words = cvText.trim().split(/\s+/).length; var lines = cvText.trim().split(/\n/).length; if (words < 150) { pts -= 4; warn.push('CV appears too short (' + words + ' words). Aim for 400\u2013800 words.'); } else if (words < 300) { pts -= 2; warn.push('CV is on the short side (' + words + ' words). Add more detail to experience.'); } if (words > 1200) { pts -= 2; warn.push('CV may be too long (' + words + ' words). Keep it focused and concise.'); } var specials = (cvText.match(/[|■●▪►◄▶★☆✦✧✓✗✘•‣⁃]/g) || []).length; if (specials > 20) { pts -= 2; warn.push('Too many special characters (' + specials + '). These can break ATS parsing.'); } var seps = (cvText.match(/[-\u2500\u2501\u2550\u2015]{5,}/g) || []).length; if (seps > 5) { pts -= 1; warn.push('Many separator lines detected. Use standard section headers instead.'); } var avgLen = lines > 0 ? cvText.length / lines : 999; if (avgLen < 15 && lines > 30) { pts -= 2; warn.push('Short fragmented lines detected \u2014 possible table or column layout. ATS may misparse this.'); } return { score: Math.max(0, pts), warnings: warn }; } /* ─── Section recognition score (15 pts) ── */ function scoreSections(sections) { var w = { experience:5, skills:4, education:3, summary:2, certifications:1 }; return Math.min(15, Object.keys(w).reduce(function (t,k) { return t + (sections[k] ? w[k] : 0); }, 0)); } /* ─── Keyword placement score (20 pts) ───── */ function scoreKeywordPlacement(jdKws, cvText) { if (!jdKws || jdKws.length === 0) return 10; var lines = cvText.split('\n'); var sMap = { summary:'', skills:'', experience:'', education:'', other:'' }; var cur = 'other'; lines.forEach(function (line) { var l = line.toLowerCase(); if (/\b(summary|profile|objective|overview)\b/.test(l)) cur = 'summary'; else if (/\b(skills|competencies|technologies|expertise)\b/.test(l)) cur = 'skills'; else if (/\b(experience|employment|career|work\s+history)\b/.test(l)) cur = 'experience'; else if (/\b(education|academic|university|college)\b/.test(l)) cur = 'education'; if (sMap[cur] !== undefined) sMap[cur] += ' ' + line; }); var weights = { summary:4, skills:3, experience:2, education:1 }; var found = 0, possible = 0; jdKws.slice(0, 15).forEach(function (kw) { Object.keys(weights).forEach(function (sec) { possible += weights[sec]; if (matchType(kw.word, sMap[sec]) !== 'miss') found += weights[sec]; }); }); return Math.round(possible > 0 ? (found / possible) * 20 : 0); } /* ─── Keyword match score (40 pts) ─────── */ function scoreKeywordMatch(jdKws, cvText) { if (!jdKws || jdKws.length === 0) { var cvRich = Math.min(1, extractKeywords(cvText).length / 40); return { score: Math.round(cvRich * 28), matched:[], missing:[], partial:[], all:[] }; } var matched = [], missing = [], partial = []; var wScore = 0, wMax = 0; jdKws.forEach(function (kw, idx) { var w = Math.max(1, jdKws.length - idx); wMax += w; var mt = matchType(kw.word, cvText); if (mt === 'exact') { wScore += w; matched.push(kw.word); } else if (mt === 'partial') { wScore += w * 0.5; partial.push(kw.word); } else { missing.push(kw.word); } }); return { score: Math.min(40, Math.round(wMax > 0 ? (wScore / wMax) * 40 : 0)), matched: matched, missing: missing, partial: partial, all: jdKws }; } /* ─── Contact & quality signals ────────── */ function detectContact(t) { return { email: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-z]{2,}/.test(t), phone: /(\+?\d[\d\s\-().]{7,}\d)/.test(t), linkedin: /linkedin\.com\/in\//i.test(t) || /linkedin/i.test(t) }; } function detectQuality(t) { return { has_dates: /\b(19|20)\d{2}\b/.test(t) || /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s*\d{4}\b/i.test(t), has_metrics: /\b(\d+\s*(%|percent|k|m|million|billion|users|clients|employees|team|projects?|years?))\b/i.test(t), has_action_verbs: ACTION_VERBS.some(function (v) { return new RegExp('\\b' + v + '\\b', 'i').test(t); }) }; } /* ─── Word count & page estimate ─────────── */ function wc(t) { return t.trim().split(/\s+/).filter(Boolean).length; } function pages(w) { return Math.max(1, Math.round(w / 400)); } /* ─── Career level ───────────────────────── */ function level(t) { var now = new Date().getFullYear(); var years = (t.match(/\b(19|20)\d{2}\b/g) || []).map(Number).filter(function (y) { return y >= 1970 && y <= now; }); if (!years.length) return 'Unknown'; var exp = now - Math.min.apply(null, years); if (exp >= 15) return 'Senior+'; if (exp >= 8) return 'Senior'; if (exp >= 3) return 'Mid-Level'; return 'Junior'; } /* ─── Generate tips ──────────────────────── */ function generateTips(sections, contact, quality, kwResult, fmtWarns, hasJd) { var warn = fmtWarns.slice(), sugg = []; if (!contact.email) warn.push('No email address detected. Always include your email at the top.'); if (!contact.phone) warn.push('No phone number detected. Recruiters need a way to contact you.'); if (!contact.linkedin) sugg.push('Add your LinkedIn profile URL to increase credibility.'); if (!sections.experience) warn.push('No Work Experience section detected. This is critical for ATS ranking.'); if (!sections.skills) warn.push('No Skills section detected. ATS systems heavily weight dedicated skills sections.'); if (!sections.education) sugg.push('Add an Education section \u2014 even if brief, ATS may require it.'); if (!sections.summary) sugg.push('Add a Professional Summary at the top with 2\u20133 keywords from the job description.'); if (!sections.certifications) sugg.push('Add a Certifications section if you hold any relevant credentials.'); if (!quality.has_dates) warn.push('No dates detected. Always include start/end dates for each role.'); if (!quality.has_metrics) sugg.push('Add measurable achievements: percentages, revenue figures, team sizes, etc.'); if (!quality.has_action_verbs) sugg.push('Start bullet points with strong action verbs: Led, Built, Delivered, Increased\u2026'); if (hasJd && kwResult.missing.length > 0) { warn.push('Missing JD keywords: ' + kwResult.missing.slice(0, 5).join(', ') + '. Add them naturally to your CV.'); } if (hasJd && kwResult.matched.length === 0) { warn.push('No exact keyword matches with the job description. Tailor your CV language to match the JD.'); } if (hasJd && kwResult.partial.length > 3) { sugg.push('Use exact keyword forms: e.g. "project management" not "managing projects".'); } if (!sugg.length) sugg.push('Your CV looks good. Keep keywords consistent with each job you apply for.'); if (!warn.length) warn.push('No major ATS issues detected. Review suggestions above for further improvement.'); return { warnings: warn, suggestions: sugg }; } /* ─── MAIN ANALYZE ───────────────────────── */ function analyzeCv(cvText, jdText, ext) { var hasJd = jdText && jdText.trim().length > 30; var jdKws = hasJd ? extractKeywords(jdText, 30) : []; var kwRes = scoreKeywordMatch(jdKws, cvText); var kpScore = scoreKeywordPlacement(jdKws, cvText); var sects = detectSections(cvText); var srScore = scoreSections(sects); var erRes = scoreExperienceRecency(cvText); var fmtRes = scoreFormatting(cvText); var contact = detectContact(cvText); var quality = detectQuality(cvText); var tips = generateTips(sects, contact, quality, kwRes, fmtRes.warnings, hasJd); var total = Math.min(100, Math.max(0, kwRes.score + kpScore + srScore + erRes.score + fmtRes.score)); var words = wc(cvText); return { score: total, breakdown: { kw: kwRes.score, kp: kpScore, sr: srScore, er: erRes.score, fq: fmtRes.score }, sections: sects, contact: contact, quality: quality, words: words, pages: pages(words), level: level(cvText), fileExt: ext.toUpperCase(), kwResult: kwRes, jdKws: jdKws, hasJd: hasJd, warnings: tips.warnings, suggestions: tips.suggestions, hasRecent: erRes.hasRecent }; } /* ─── RENDER RESULTS ─────────────────────── */ function renderResults(r) { var si = scoreInfo(r.score); setStep(4); document.getElementById('r-score').textContent = '0'; document.getElementById('r-score').style.color = si.color; animateCount(r.score, 800); var badge = document.getElementById('r-badge'); badge.textContent = si.label; badge.style.cssText = 'background:' + si.bg + ';border:1px solid ' + si.border + ';color:' + si.color; animateBar('r-bar', r.score, si.color); document.getElementById('r-type').textContent = r.fileExt; document.getElementById('r-words').textContent = r.words; document.getElementById('r-pages').textContent = r.pages; document.getElementById('r-level').textContent = r.level; var bd = r.breakdown; document.getElementById('bd-kw').textContent = bd.kw + '/40'; document.getElementById('bd-kp').textContent = bd.kp + '/20'; document.getElementById('bd-sr').textContent = bd.sr + '/15'; document.getElementById('bd-er').textContent = bd.er + '/15'; document.getElementById('bd-fq').textContent = bd.fq + '/10'; animateBar('bdf-kw', (bd.kw/40)*100, '#2563eb'); animateBar('bdf-kp', (bd.kp/20)*100, '#7c3aed'); animateBar('bdf-sr', (bd.sr/15)*100, '#0891b2'); animateBar('bdf-er', (bd.er/15)*100, '#059669'); animateBar('bdf-fq', (bd.fq/10)*100, '#d97706'); setCheck('c-email', r.contact.email); setCheck('c-phone', r.contact.phone); setCheck('c-linkedin', r.contact.linkedin); setCheck('c-exp', r.sections.experience); setCheck('c-skills', r.sections.skills); setCheck('c-edu', r.sections.education); setCheck('c-summary', r.sections.summary); setCheck('c-certs', r.sections.certifications); setCheck('c-dates', r.quality.has_dates); setCheck('c-metrics', r.quality.has_metrics); setCheck('c-verbs', r.quality.has_action_verbs); setCheck('c-recent', r.hasRecent); /* Keyword tags */ var kwSec = document.getElementById('ats-kwSection'); if (r.hasJd) { kwSec.style.display = 'block'; var tags = document.getElementById('r-kwTags'); tags.innerHTML = ''; r.kwResult.matched.slice(0,20).forEach(function (w) { var t = document.createElement('span'); t.className = 'atsKwTag atsKwTag-match'; t.textContent = w; tags.appendChild(t); }); r.kwResult.partial.slice(0,15).forEach(function (w) { var t = document.createElement('span'); t.className = 'atsKwTag atsKwTag-partial'; t.textContent = w + ' ~'; tags.appendChild(t); }); r.kwResult.missing.slice(0,15).forEach(function (w) { var t = document.createElement('span'); t.className = 'atsKwTag atsKwTag-miss'; t.textContent = w; tags.appendChild(t); }); } else { kwSec.style.display = 'none'; } /* Density table */ var densitySec = document.getElementById('ats-kwDensity'); if (r.hasJd && r.jdKws.length > 0) { densitySec.style.display = 'block'; var tbody = document.getElementById('r-densityBody'); tbody.innerHTML = ''; r.jdKws.slice(0,20).forEach(function (kw) { var mt = matchType(kw.word, extractedCvText); var count = mt !== 'miss' ? countInCv(kw.word, extractedCvText) : 0; var colorMap = { exact:'#15803d', partial:'#92400e', miss:'#b91c1c' }; var bgMap = { exact:'rgba(22,163,74,0.10)', partial:'rgba(180,83,9,0.08)', miss:'rgba(220,38,38,0.07)' }; var labelMap = { exact:'Exact', partial:'Partial', miss:'Missing' }; var tr = document.createElement('tr'); tr.innerHTML = '' + kw.word + '' + '' + count + '' + '' + labelMap[mt] + ''; tbody.appendChild(tr); }); } else { densitySec.style.display = 'none'; } fillList('r-warnings', r.warnings); fillList('r-suggestions', r.suggestions); ctaEl.style.display = r.score < 75 ? 'block' : 'none'; resultsEl.style.display = 'block'; } /* ─── FILE HANDLING ──────────────────────── */ function handleFile(f) { if (!f) return; var ext = f.name.split('.').pop().toLowerCase(); if (['pdf','docx'].indexOf(ext) === -1) { setStatus('Only PDF or DOCX files are supported.', 'error'); return; } if (f.size > 5*1024*1024) { setStatus('File is too large. Maximum 5MB.', 'error'); return; } currentFile = f; runBtn.disabled = false; document.getElementById('ats-dropTitle').textContent = f.name; setStatus('CV loaded \u2014 paste a job description (optional) then click Analyze CV.', 'info'); resultsEl.style.display = 'none'; ctaEl.style.display = 'none'; setStep(2); } /* ─── TEXT EXTRACTION ────────────────────── */ function extractDocx(file) { return file.arrayBuffer().then(function (buf) { return mammoth.extractRawText({ arrayBuffer: buf }); }).then(function (r) { var text = r.value || ''; if (text.trim().length < 50) throw new Error('Could not extract enough text from DOCX.'); return text; }); } function extractPdf(file) { if (typeof pdfjsLib === 'undefined') throw new Error('PDF.js library failed to load. Please reload the page.'); return file.arrayBuffer().then(function (buf) { return pdfjsLib.getDocument({ data: buf }).promise; }).then(function (pdf) { var pagePromises = []; for (var i = 1; i <= pdf.numPages; i++) { pagePromises.push( pdf.getPage(i).then(function (page) { return page.getTextContent(); }).then(function (content) { return content.items.map(function (item) { return item.str; }).join(' '); }) ); } return Promise.all(pagePromises); }).then(function (texts) { var text = texts.join('\n'); if (text.trim().length < 50) throw new Error('Could not extract text from PDF. Ensure it is not a scanned image.'); return text; }); } /* ─── EVENT LISTENERS ────────────────────── */ dropzone.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { handleFile(fileInput.files[0]); }); dropzone.addEventListener('dragover', function (e) { e.preventDefault(); dropzone.classList.add('drag-over'); }); dropzone.addEventListener('dragleave', function () { dropzone.classList.remove('drag-over'); }); dropzone.addEventListener('drop', function (e) { e.preventDefault(); dropzone.classList.remove('drag-over'); handleFile(e.dataTransfer.files[0]); }); clearBtn.addEventListener('click', function () { currentFile = null; extractedCvText = ''; fileInput.value = ''; runBtn.disabled = true; jdArea.value = ''; document.getElementById('ats-dropTitle').textContent = 'Drop your CV here to analyze'; resultsEl.style.display = 'none'; ctaEl.style.display = 'none'; setStatus(''); setStep(1); }); jdArea.addEventListener('input', function () { if (currentFile) setStep(jdArea.value.trim().length > 30 ? 3 : 2); }); /* ─── RUN ────────────────────────────────── */ runBtn.addEventListener('click', function () { if (!currentFile) return; runBtn.disabled = true; resultsEl.style.display = 'none'; ctaEl.style.display = 'none'; setStatus('Reading file\u2026', 'info'); setStep(3); var ext = currentFile.name.split('.').pop().toLowerCase(); var jdText = jdArea.value || ''; var promise = ext === 'docx' ? extractDocx(currentFile) : extractPdf(currentFile); promise.then(function (cvText) { extractedCvText = cvText; setStatus('Analyzing\u2026', 'info'); var result = analyzeCv(cvText, jdText, ext); renderResults(result); setStatus('Analysis complete \u2713', 'success'); }).catch(function (err) { setStatus('Error: ' + (err.message || 'Could not parse file.'), 'error'); setStep(currentFile ? 1 : 1); }).finally(function () { runBtn.disabled = false; }); }); })();