Skip to content

Commit 477e33d

Browse files
committed
feat: add pdf support + log statements to verify
1 parent b155589 commit 477e33d

1 file changed

Lines changed: 70 additions & 70 deletions

File tree

web-app/js/projects/resume-analyzer.js

Lines changed: 70 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,40 @@ function initAIResumeAnalyzer() {
6868

6969
if (!analyzeBtn || !resumeInput) return;
7070

71-
// ── Skill list (mirrored from Python script) ──────────────────
71+
function loadPdfJs() {
72+
return new Promise((resolve, reject) => {
73+
if (window.pdfjsLib) { resolve(window.pdfjsLib); return; }
74+
const script = document.createElement('script');
75+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
76+
script.onload = () => {
77+
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
78+
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
79+
resolve(window.pdfjsLib);
80+
};
81+
script.onerror = () => reject(new Error('Failed to load PDF.js'));
82+
document.head.appendChild(script);
83+
});
84+
}
85+
86+
async function readPdfAsText(file) {
87+
const pdfjsLib = await loadPdfJs();
88+
const arrayBuffer = await file.arrayBuffer();
89+
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
90+
const pageTexts = [];
91+
for (let i = 1; i <= pdf.numPages; i++) {
92+
const page = await pdf.getPage(i);
93+
const content = await page.getTextContent();
94+
pageTexts.push(content.items.map(item => item.str).join(' '));
95+
}
96+
return pageTexts.join('\n');
97+
}
98+
7299
const SKILLS = [
73100
"python", "java", "c++", "c#", ".net", "django", "sql",
74101
"machine learning", "html", "css", "javascript",
75102
"communication", "teamwork"
76103
];
77104

78-
// Aliases → canonical skill name
79105
const ALIASES = {
80106
"py": "python",
81107
"js": "javascript",
@@ -96,24 +122,20 @@ function initAIResumeAnalyzer() {
96122
"github": "git",
97123
};
98124

99-
// Section regex (mirrored from Python)
100125
const EDU_RE = /education|academic|qualification/i;
101126
const EXP_RE = /experience|internship|work experience/i;
102127
const PROJ_RE = /projects?/i;
103128

104-
// ── File-selection feedback ───────────────────────────────────
105129
resumeInput.addEventListener('change', () => {
106130
const file = resumeInput.files[0];
107131
if (!file) return;
108132

109-
// Visual feedback
110133
fileStatus.textContent = `✅ File selected: ${file.name}`;
111134
fileStatus.style.color = 'var(--accent)';
112135
uploadIcon.style.color = 'var(--accent)';
113136
uploadIcon.className = 'fa-solid fa-circle-check resume-upload-icon';
114137
});
115138

116-
// ── Drag-and-drop support ─────────────────────────────────────
117139
const dropZone = document.getElementById('resumeDropZone');
118140
if (dropZone) {
119141
dropZone.addEventListener('dragover', e => {
@@ -128,7 +150,6 @@ function initAIResumeAnalyzer() {
128150
dropZone.style.borderColor = '';
129151
const file = e.dataTransfer.files[0];
130152
if (file) {
131-
// Assign to the input so the rest of the flow works
132153
const dt = new DataTransfer();
133154
dt.items.add(file);
134155
resumeInput.files = dt.files;
@@ -139,8 +160,7 @@ function initAIResumeAnalyzer() {
139160
});
140161
}
141162

142-
// ── Main analysis ─────────────────────────────────────────────
143-
analyzeBtn.addEventListener('click', () => {
163+
analyzeBtn.addEventListener('click', async () => {
144164
if (!resumeInput.files.length) {
145165
fileStatus.textContent = '⚠️ Please upload a resume file first!';
146166
fileStatus.style.color = 'var(--danger-color, #ef4444)';
@@ -150,47 +170,52 @@ function initAIResumeAnalyzer() {
150170
const file = resumeInput.files[0];
151171
const ext = file.name.split('.').pop().toLowerCase();
152172

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
178173
analyzeBtn.textContent = 'Analyzing… ⏳';
179174
analyzeBtn.disabled = true;
180175

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-
});
176+
try {
177+
let text = '';
178+
179+
if (ext === 'pdf') {
180+
fileStatus.textContent = '📄 Extracting text from PDF…';
181+
fileStatus.style.color = 'var(--accent)';
182+
text = await readPdfAsText(file);
183+
//console.log(`[ResumeAnalyzer] PDF extracted (${text.length} chars):`, text);
184+
if (!text || text.trim().length < 30) {
185+
console.warn('[ResumeAnalyzer] Text too short — likely a scanned/image PDF.');
186+
fileStatus.textContent =
187+
`⚠️ "${file.name}" appears to be a scanned/image-only PDF. ` +
188+
`For best results, export your resume as a text-based PDF or .txt file.`;
189+
fileStatus.style.color = 'var(--warning-color, #f59e0b)';
190+
return;
191+
}
192+
} else if (ext === 'doc' || ext === 'docx') {
193+
text = await readFileAsText(file);
194+
//console.log(`[ResumeAnalyzer] DOCX raw text (${text.length} chars):`, text);
195+
if (!text || text.trim().length < 30) {
196+
console.warn('[ResumeAnalyzer] DOCX text too short — binary file.');
197+
fileStatus.textContent =
198+
`⚠️ "${file.name}" could not be read as plain text in the browser. ` +
199+
`For best results, export your resume as a .pdf or .txt file and re-upload.`;
200+
fileStatus.style.color = 'var(--warning-color, #f59e0b)';
201+
return;
202+
}
203+
} else {
204+
text = await readFileAsText(file);
205+
//console.log(`[ResumeAnalyzer] TXT extracted (${text.length} chars):`, text);
206+
}
207+
208+
//console.log('[ResumeAnalyzer] Running analysis on text:', text);
209+
runAnalysis(text);
210+
} catch (err) {
211+
fileStatus.textContent = `❌ Could not read file: ${err.message}`;
212+
fileStatus.style.color = 'var(--danger-color, #ef4444)';
213+
} finally {
214+
analyzeBtn.textContent = 'Analyze Resume 🚀';
215+
analyzeBtn.disabled = false;
216+
}
191217
});
192218

193-
// ── Read file as text ─────────────────────────────────────────
194219
function readFileAsText(file) {
195220
return new Promise((resolve, reject) => {
196221
const reader = new FileReader();
@@ -200,42 +225,27 @@ function initAIResumeAnalyzer() {
200225
});
201226
}
202227

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) ─────────
212228
function runAnalysis(rawText) {
213229
const text = rawText.toLowerCase();
214230

215-
// Tokenise: keep alphanumeric tokens plus "c++" / "c#" / ".net"
216231
const tokenRe = /c\+\+|c#|\.net|[a-z0-9]+/gi;
217232
const tokens = (text.match(tokenRe) || []).map(t => t.toLowerCase());
218233

219-
// Normalise aliases
220234
const normalised = tokens.map(t => ALIASES[t] ?? t);
221235

222-
// Build bigrams from normalised tokens
223236
const bigrams = [];
224237
for (let i = 0; i < normalised.length - 1; i++) {
225238
bigrams.push(normalised[i] + ' ' + normalised[i + 1]);
226239
}
227240

228-
// Detect skills
229241
const foundSkills = SKILLS.filter(
230242
skill => normalised.includes(skill) || bigrams.includes(skill)
231243
);
232244

233-
// Detect sections
234245
const eduFound = EDU_RE.test(text);
235246
const expFound = EXP_RE.test(text);
236247
const projFound = PROJ_RE.test(text);
237248

238-
// ── Score (identical formula to Python script) ────────────
239249
let score = 0;
240250
score += Math.min(foundSkills.length * 8, 40); // up to 40
241251
if (eduFound) score += 20;
@@ -244,24 +254,20 @@ function initAIResumeAnalyzer() {
244254
if (foundSkills.length >= 5) score += 10; // quality bonus
245255
score = Math.min(score, 100);
246256

247-
// Derived sub-scores for display
248257
const formattingScore = Math.round(
249258
((eduFound ? 1 : 0) + (expFound ? 1 : 0) + (projFound ? 1 : 0)) / 3 * 100
250259
);
251260
const keywordScore = Math.min(
252261
Math.round((foundSkills.length / SKILLS.length) * 100), 100
253262
);
254263

255-
// Strength label
256264
let strength;
257265
if (score >= 80) strength = '✅ Resume Strength: Excellent';
258266
else if (score >= 50) strength = '👍 Resume Strength: Good';
259267
else strength = '⚠️ Resume Strength: Needs Improvement';
260268

261-
// Missing skills → suggestions
262269
const missingSkills = SKILLS.filter(s => !foundSkills.includes(s));
263270

264-
// ── Render results ────────────────────────────────────────
265271
renderResults({
266272
score, formattingScore, keywordScore, strength,
267273
foundSkills, missingSkills,
@@ -272,21 +278,17 @@ function initAIResumeAnalyzer() {
272278
function renderResults({ score, formattingScore, keywordScore, strength,
273279
foundSkills, missingSkills,
274280
eduFound, expFound, projFound }) {
275-
// Show panels
276281
ats.classList.remove('hidden');
277282
bottomSection.classList.remove('hidden');
278283

279-
// ATS score circle
280284
document.getElementById('atsScoreDisplay').textContent = score + '%';
281285
document.getElementById('atsFormattingScore').textContent = `✔ Formatting Score: ${formattingScore}%`;
282286
document.getElementById('atsKeywordScore').textContent = `✔ Keyword Match: ${keywordScore}%`;
283287
document.getElementById('atsStrength').textContent = strength;
284288

285-
// Keyword bars
286289
const kwList = document.getElementById('resumeKeywordsList');
287290
kwList.innerHTML = '';
288291

289-
// Show found skills first, then a few missing ones grayed out
290292
const displaySkills = [
291293
...foundSkills.map(s => ({ name: s, pct: Math.min(70 + Math.random() * 30 | 0, 100), found: true })),
292294
...missingSkills.slice(0, Math.max(0, 5 - foundSkills.length))
@@ -307,7 +309,6 @@ function initAIResumeAnalyzer() {
307309
});
308310
}
309311

310-
// Suggestions
311312
const suggestionsEl = document.getElementById('resumeSuggestions');
312313
suggestionsEl.innerHTML = '';
313314

@@ -320,7 +321,6 @@ function initAIResumeAnalyzer() {
320321
</div>`;
321322
});
322323

323-
// Scroll into view
324324
ats.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
325325
}
326326

0 commit comments

Comments
 (0)