@@ -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 = / e d u c a t i o n | a c a d e m i c | q u a l i f i c a t i o n / i;
101126 const EXP_RE = / e x p e r i e n c e | i n t e r n s h i p | w o r k e x p e r i e n c e / i;
102127 const PROJ_RE = / p r o j e c t s ? / 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 # | \. n e t | [ a - z 0 - 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