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 & 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 = / 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;
101+ 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;
102+ const PROJ_RE = / p r o j e c t s ? / 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 # | \. n e t | [ a - z 0 - 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