Skip to content

Commit b155589

Browse files
committed
feat: added analyzer js file with similar structure as that of other modules, complete js
1 parent a1833dd commit b155589

1 file changed

Lines changed: 355 additions & 0 deletions

File tree

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
// ============================================
2+
// AI RESUME ANALYZER
3+
// Logic ported from utilities/AI-Resume-Analyzer/AI-Resume-Analyzer.py
4+
// No API calls — pure client-side text analysis.
5+
// ============================================
6+
7+
function getAIResumeAnalyzerHTML() {
8+
return `
9+
<div class="resume-analyzer-shell">
10+
<div class="resume-analyzer-hero">
11+
<div class="resume-analyzer-copy">
12+
<span class="resume-analyzer-badge">GSSoC Utility</span>
13+
<h2>AI Resume Analyzer</h2>
14+
<p>Upload a resume and get a quick ATS-style snapshot with keyword, structure, and formatting feedback.</p>
15+
</div>
16+
<div class="resume-analyzer-pill">No backend required</div>
17+
</div>
18+
19+
<div class="resume-analyzer-grid">
20+
<section class="resume-panel resume-upload-panel">
21+
<div class="resume-upload-box" id="resumeDropZone">
22+
<i class="fa-solid fa-file-lines resume-upload-icon" id="resumeUploadIcon"></i>
23+
<label class="resume-file-btn">
24+
Choose File
25+
<input type="file" id="resumeInput" accept=".pdf,.doc,.docx,.txt" hidden>
26+
</label>
27+
<p id="resumeFileStatus">Drag &amp; drop or click to choose a resume (.pdf, .doc, .docx, .txt)</p>
28+
</div>
29+
30+
<button class="resume-analyze-btn" id="analyzeBtn" type="button">Analyze Resume 🚀</button>
31+
</section>
32+
33+
<section class="resume-panel resume-score-panel hidden" id="ats">
34+
<h3>ATS Score</h3>
35+
<div class="resume-progress-circle">
36+
<span id="atsScoreDisplay">0%</span>
37+
</div>
38+
<div class="resume-extra-info">
39+
<p id="atsFormattingScore">✔ Formatting Score: –</p>
40+
<p id="atsKeywordScore">✔ Keyword Match: –</p>
41+
<p id="atsStrength" style="font-weight:700; margin-top:0.5rem;"></p>
42+
</div>
43+
</section>
44+
45+
<section class="resume-panel resume-bottom-panel hidden" id="bottomSection">
46+
<div class="resume-mini-card">
47+
<h3>Keywords Match</h3>
48+
<div id="resumeKeywordsList"></div>
49+
</div>
50+
51+
<div class="resume-mini-card">
52+
<h3><i class="fa-solid fa-lightbulb"></i> Suggestions</h3>
53+
<div id="resumeSuggestions"></div>
54+
</div>
55+
</section>
56+
</div>
57+
</div>
58+
`;
59+
}
60+
61+
function initAIResumeAnalyzer() {
62+
const analyzeBtn = document.getElementById('analyzeBtn');
63+
const resumeInput = document.getElementById('resumeInput');
64+
const fileStatus = document.getElementById('resumeFileStatus');
65+
const uploadIcon = document.getElementById('resumeUploadIcon');
66+
const ats = document.getElementById('ats');
67+
const bottomSection = document.getElementById('bottomSection');
68+
69+
if (!analyzeBtn || !resumeInput) return;
70+
71+
// ── Skill list (mirrored from Python script) ──────────────────
72+
const SKILLS = [
73+
"python", "java", "c++", "c#", ".net", "django", "sql",
74+
"machine learning", "html", "css", "javascript",
75+
"communication", "teamwork"
76+
];
77+
78+
// Aliases → canonical skill name
79+
const ALIASES = {
80+
"py": "python",
81+
"js": "javascript",
82+
"ts": "typescript",
83+
"cpp": "c++",
84+
"postgres": "postgresql",
85+
"mongo": "mongodb",
86+
"ml": "machine learning",
87+
"dl": "deep learning",
88+
"cv": "computer vision",
89+
"reactjs": "react",
90+
"react.js": "react",
91+
"nodejs": "node.js",
92+
"expressjs": "express",
93+
"aws": "amazon web services",
94+
"gcp": "google cloud platform",
95+
"gitlab": "git",
96+
"github": "git",
97+
};
98+
99+
// Section regex (mirrored from Python)
100+
const EDU_RE = /education|academic|qualification/i;
101+
const EXP_RE = /experience|internship|work experience/i;
102+
const PROJ_RE = /projects?/i;
103+
104+
// ── File-selection feedback ───────────────────────────────────
105+
resumeInput.addEventListener('change', () => {
106+
const file = resumeInput.files[0];
107+
if (!file) return;
108+
109+
// Visual feedback
110+
fileStatus.textContent = `✅ File selected: ${file.name}`;
111+
fileStatus.style.color = 'var(--accent)';
112+
uploadIcon.style.color = 'var(--accent)';
113+
uploadIcon.className = 'fa-solid fa-circle-check resume-upload-icon';
114+
});
115+
116+
// ── Drag-and-drop support ─────────────────────────────────────
117+
const dropZone = document.getElementById('resumeDropZone');
118+
if (dropZone) {
119+
dropZone.addEventListener('dragover', e => {
120+
e.preventDefault();
121+
dropZone.style.borderColor = 'var(--accent)';
122+
});
123+
dropZone.addEventListener('dragleave', () => {
124+
dropZone.style.borderColor = '';
125+
});
126+
dropZone.addEventListener('drop', e => {
127+
e.preventDefault();
128+
dropZone.style.borderColor = '';
129+
const file = e.dataTransfer.files[0];
130+
if (file) {
131+
// Assign to the input so the rest of the flow works
132+
const dt = new DataTransfer();
133+
dt.items.add(file);
134+
resumeInput.files = dt.files;
135+
fileStatus.textContent = `✅ File selected: ${file.name}`;
136+
fileStatus.style.color = 'var(--accent)';
137+
uploadIcon.className = 'fa-solid fa-circle-check resume-upload-icon';
138+
}
139+
});
140+
}
141+
142+
// ── Main analysis ─────────────────────────────────────────────
143+
analyzeBtn.addEventListener('click', () => {
144+
if (!resumeInput.files.length) {
145+
fileStatus.textContent = '⚠️ Please upload a resume file first!';
146+
fileStatus.style.color = 'var(--danger-color, #ef4444)';
147+
return;
148+
}
149+
150+
const file = resumeInput.files[0];
151+
const ext = file.name.split('.').pop().toLowerCase();
152+
153+
// PDFs and binary docs can't be read as plain text in the browser.
154+
// Show a clear message rather than silently fail.
155+
if (ext === 'pdf' || ext === 'doc' || ext === 'docx') {
156+
analyzeBtn.textContent = 'Analyzing… ⏳';
157+
analyzeBtn.disabled = true;
158+
159+
readFileAsText(file)
160+
.then(text => {
161+
// PDF/DOCX raw bytes usually look garbled but may still
162+
// contain readable ASCII keywords – try it.
163+
if (!text || text.trim().length < 30) {
164+
showBinaryFallback(file.name, ext);
165+
} else {
166+
runAnalysis(text);
167+
}
168+
})
169+
.catch(() => showBinaryFallback(file.name, ext))
170+
.finally(() => {
171+
analyzeBtn.textContent = 'Analyze Resume 🚀';
172+
analyzeBtn.disabled = false;
173+
});
174+
return;
175+
}
176+
177+
// .txt → read directly
178+
analyzeBtn.textContent = 'Analyzing… ⏳';
179+
analyzeBtn.disabled = true;
180+
181+
readFileAsText(file)
182+
.then(text => runAnalysis(text))
183+
.catch(() => {
184+
fileStatus.textContent = '❌ Could not read file. Please try a .txt file.';
185+
fileStatus.style.color = 'var(--danger-color, #ef4444)';
186+
})
187+
.finally(() => {
188+
analyzeBtn.textContent = 'Analyze Resume 🚀';
189+
analyzeBtn.disabled = false;
190+
});
191+
});
192+
193+
// ── Read file as text ─────────────────────────────────────────
194+
function readFileAsText(file) {
195+
return new Promise((resolve, reject) => {
196+
const reader = new FileReader();
197+
reader.onload = e => resolve(e.target.result);
198+
reader.onerror = () => reject(new Error('Read error'));
199+
reader.readAsText(file);
200+
});
201+
}
202+
203+
// ── Fallback UI for binary files ──────────────────────────────
204+
function showBinaryFallback(filename, ext) {
205+
fileStatus.textContent =
206+
`⚠️ "${filename}" could not be read as plain text in the browser. ` +
207+
`For best results, export your resume as a .txt file and re-upload.`;
208+
fileStatus.style.color = 'var(--warning-color, #f59e0b)';
209+
}
210+
211+
// ── Core analysis (JS port of AI-Resume-Analyzer.py) ─────────
212+
function runAnalysis(rawText) {
213+
const text = rawText.toLowerCase();
214+
215+
// Tokenise: keep alphanumeric tokens plus "c++" / "c#" / ".net"
216+
const tokenRe = /c\+\+|c#|\.net|[a-z0-9]+/gi;
217+
const tokens = (text.match(tokenRe) || []).map(t => t.toLowerCase());
218+
219+
// Normalise aliases
220+
const normalised = tokens.map(t => ALIASES[t] ?? t);
221+
222+
// Build bigrams from normalised tokens
223+
const bigrams = [];
224+
for (let i = 0; i < normalised.length - 1; i++) {
225+
bigrams.push(normalised[i] + ' ' + normalised[i + 1]);
226+
}
227+
228+
// Detect skills
229+
const foundSkills = SKILLS.filter(
230+
skill => normalised.includes(skill) || bigrams.includes(skill)
231+
);
232+
233+
// Detect sections
234+
const eduFound = EDU_RE.test(text);
235+
const expFound = EXP_RE.test(text);
236+
const projFound = PROJ_RE.test(text);
237+
238+
// ── Score (identical formula to Python script) ────────────
239+
let score = 0;
240+
score += Math.min(foundSkills.length * 8, 40); // up to 40
241+
if (eduFound) score += 20;
242+
if (expFound) score += 25;
243+
if (projFound) score += 15;
244+
if (foundSkills.length >= 5) score += 10; // quality bonus
245+
score = Math.min(score, 100);
246+
247+
// Derived sub-scores for display
248+
const formattingScore = Math.round(
249+
((eduFound ? 1 : 0) + (expFound ? 1 : 0) + (projFound ? 1 : 0)) / 3 * 100
250+
);
251+
const keywordScore = Math.min(
252+
Math.round((foundSkills.length / SKILLS.length) * 100), 100
253+
);
254+
255+
// Strength label
256+
let strength;
257+
if (score >= 80) strength = '✅ Resume Strength: Excellent';
258+
else if (score >= 50) strength = '👍 Resume Strength: Good';
259+
else strength = '⚠️ Resume Strength: Needs Improvement';
260+
261+
// Missing skills → suggestions
262+
const missingSkills = SKILLS.filter(s => !foundSkills.includes(s));
263+
264+
// ── Render results ────────────────────────────────────────
265+
renderResults({
266+
score, formattingScore, keywordScore, strength,
267+
foundSkills, missingSkills,
268+
eduFound, expFound, projFound
269+
});
270+
}
271+
272+
function renderResults({ score, formattingScore, keywordScore, strength,
273+
foundSkills, missingSkills,
274+
eduFound, expFound, projFound }) {
275+
// Show panels
276+
ats.classList.remove('hidden');
277+
bottomSection.classList.remove('hidden');
278+
279+
// ATS score circle
280+
document.getElementById('atsScoreDisplay').textContent = score + '%';
281+
document.getElementById('atsFormattingScore').textContent = `✔ Formatting Score: ${formattingScore}%`;
282+
document.getElementById('atsKeywordScore').textContent = `✔ Keyword Match: ${keywordScore}%`;
283+
document.getElementById('atsStrength').textContent = strength;
284+
285+
// Keyword bars
286+
const kwList = document.getElementById('resumeKeywordsList');
287+
kwList.innerHTML = '';
288+
289+
// Show found skills first, then a few missing ones grayed out
290+
const displaySkills = [
291+
...foundSkills.map(s => ({ name: s, pct: Math.min(70 + Math.random() * 30 | 0, 100), found: true })),
292+
...missingSkills.slice(0, Math.max(0, 5 - foundSkills.length))
293+
.map(s => ({ name: s, pct: 0, found: false }))
294+
].slice(0, 6);
295+
296+
if (displaySkills.length === 0) {
297+
kwList.innerHTML = '<p style="color:var(--text-secondary);font-size:0.9rem;">No matching keywords detected. Try a .txt version of your resume.</p>';
298+
} else {
299+
displaySkills.forEach(({ name, pct, found }) => {
300+
kwList.innerHTML += `
301+
<div class="resume-keyword-item">
302+
<span style="${found ? '' : 'color:var(--text-secondary);opacity:0.6;'}">${capitalise(name)}${found ? '' : ' ❌'}</span>
303+
<div class="resume-bar">
304+
<div style="width:${pct}%; background:${found ? 'linear-gradient(90deg,var(--accent),#06b6d4)' : '#6b7280'};"></div>
305+
</div>
306+
</div>`;
307+
});
308+
}
309+
310+
// Suggestions
311+
const suggestionsEl = document.getElementById('resumeSuggestions');
312+
suggestionsEl.innerHTML = '';
313+
314+
const suggestions = buildSuggestions({ foundSkills, missingSkills, eduFound, expFound, projFound });
315+
suggestions.forEach(s => {
316+
suggestionsEl.innerHTML += `
317+
<div class="resume-suggestion">
318+
<i class="fa-solid ${s.ok ? 'fa-check' : 'fa-triangle-exclamation'}" style="color:${s.ok ? 'var(--accent)' : 'var(--warning-color,#f59e0b)'}; margin-top:0.2rem;"></i>
319+
<p style="margin:0;">${s.text}</p>
320+
</div>`;
321+
});
322+
323+
// Scroll into view
324+
ats.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
325+
}
326+
327+
function buildSuggestions({ foundSkills, missingSkills, eduFound, expFound, projFound }) {
328+
const list = [];
329+
330+
if (eduFound) list.push({ ok: true, text: 'Education section detected' });
331+
else list.push({ ok: false, text: 'Add an Education section' });
332+
333+
if (expFound) list.push({ ok: true, text: 'Experience section detected' });
334+
else list.push({ ok: false, text: 'Add a Work Experience / Internship section' });
335+
336+
if (projFound) list.push({ ok: true, text: 'Projects section detected' });
337+
else list.push({ ok: false, text: 'Add a Projects section' });
338+
339+
if (foundSkills.length >= 5)
340+
list.push({ ok: true, text: `Strong skill set detected (${foundSkills.length} skills)` });
341+
else
342+
list.push({ ok: false, text: 'Add more technical skills' });
343+
344+
if (missingSkills.length > 0)
345+
list.push({ ok: false, text: `Consider adding: ${missingSkills.slice(0, 3).map(capitalise).join(', ')}` });
346+
347+
list.push({ ok: false, text: 'Use strong action verbs (e.g. "Developed", "Led", "Built")' });
348+
349+
return list.slice(0, 6);
350+
}
351+
352+
function capitalise(str) {
353+
return str.charAt(0).toUpperCase() + str.slice(1);
354+
}
355+
}

0 commit comments

Comments
 (0)