careermatch-ai / engine.js
Hugdrigo's picture
Upload 5 files
53361fa verified
Raw History Blame Contribute Delete
12.3 kB
/* CareerMatch AI Static: explainable browser-only lexical CV/job alignment.
Original Gradio/Python project retained separately. No embeddings here. */
(function(root) {
'use strict';
const SKILLS = {
'Project Management':['project management','gestión de proyectos','gerenciamento de projetos'],
'Program Management':['program management','gestión de programas','gerenciamento de programas'],
'Operations':['operations','operaciones','operações','operacoes','operational management','gestión operativa'],
'Process Improvement':['process improvement','mejora de procesos','melhoria de processos','continuous improvement'],
'Stakeholder Management':['stakeholder management','gestión de stakeholders','gestão de stakeholders','stakeholders'],
'Change Management':['change management','gestión del cambio','gestão da mudança'],
'Strategic Planning':['strategic planning','planificación estratégica','planejamento estratégico'],
'Team Leadership':['team leadership','liderazgo de equipos','liderança de equipes','people management','gestión de equipos'],
'Customer Success':['customer success','éxito del cliente','sucesso do cliente'],
'Customer Service':['customer service','servicio al cliente','atención al cliente','atendimento ao cliente'],
'Logistics':['logistics','logística','logistica'],
'Supply Chain':['supply chain','cadena de suministro','cadeia de suprimentos'],
'Procurement':['procurement','compras','adquisiciones','suprimentos'],
'Business Analysis':['business analysis','análisis de negocio','análise de negócios'],
'Market Research':['market research','investigación de mercados','pesquisa de mercado'],
'Digital Marketing':['digital marketing','marketing digital'],
'SEO':['seo','search engine optimization','optimización para motores de búsqueda'],
'CRM':['crm','customer relationship management','gestión de relaciones con clientes'],
'Sales':['sales','ventas','vendas','business development','desarrollo de negocios'],
'Financial Analysis':['financial analysis','análisis financiero','análise financeira'],
'Budgeting':['budgeting','presupuesto','orçamento','orcamento'],
'Forecasting':['forecasting','pronóstico','proyecciones','previsão'],
'Risk Management':['risk management','gestión de riesgos','gestão de riscos'],
'Data Analysis':['data analysis','análisis de datos','análise de dados','analítica de datos'],
'Data Visualization':['data visualization','visualización de datos','visualização de dados'],
'Machine Learning':['machine learning','aprendizaje automático','aprendizado de máquina'],
'Generative AI':['generative ai','genai','ia generativa','inteligencia artificial generativa'],
'NLP':['natural language processing','nlp','procesamiento de lenguaje natural','processamento de linguagem natural'],
'RAG':['retrieval augmented generation','rag','generación aumentada por recuperación'],
'Prompt Engineering':['prompt engineering','ingeniería de prompts','engenharia de prompts'],
'Python':['python'], 'SQL':['sql','postgresql','mysql','sql server'],
'Excel':['excel','microsoft excel','spreadsheets','hojas de cálculo','planilhas'],
'Power BI':['power bi','powerbi'], 'Tableau':['tableau'], 'Pandas':['pandas'],
'scikit-learn':['scikit-learn','sklearn'], 'Transformers':['transformers','hugging face transformers'],
'Hugging Face':['hugging face','huggingface'], 'Git':['git','version control','control de versiones'],
'GitHub':['github'], 'GitLab':['gitlab'], 'Docker':['docker','containers','contenedores'],
'APIs':['api','apis','rest api','restful'], 'Cloud':['cloud','aws','azure','gcp','google cloud'],
'Research':['research','investigación','investigação','pesquisa acadêmica'],
'Academic Writing':['academic writing','escritura académica','redacción académica','escrita acadêmica'],
'Literature Review':['literature review','revisión de literatura','revisão de literatura','revisión bibliográfica'],
'Quantitative Research':['quantitative research','investigación cuantitativa','pesquisa quantitativa'],
'Qualitative Research':['qualitative research','investigación cualitativa','pesquisa qualitativa'],
'Teaching':['teaching','docencia','enseñanza','ensino','professor','lecturer'],
'Training':['training','capacitación','treinamento'],
'International Relations':['international relations','relaciones internacionales','relações internacionais'],
'International Trade':['international trade','comercio internacional','comércio internacional'],
'Public Policy':['public policy','políticas públicas','politicas publicas'],
'Economic Development':['economic development','desarrollo económico','desenvolvimento econômico'],
'Agile':['agile','scrum','kanban'],
'KPI Management':['kpi','kpis','key performance indicators','indicadores clave','indicadores de desempenho'],
'Process Documentation':['process documentation','documentación de procesos','documentação de processos','sop'],
'Automation':['automation','automatización','automação','workflow automation']
};
const LANGUAGES = {
'English':['english','inglés','ingles','inglês'],
'Spanish':['spanish','español','espanol','castellano'],
'Portuguese':['portuguese','portugués','portugues','português'],
'Italian':['italian','italiano','italiana'],
'French':['french','francés','frances','francês'],
'German':['german','alemán','aleman','alemão']
};
const STOPWORDS = new Set(('the and for with from that this your you our are will have has job role years year experience must be or of in as an to we a is on not by who can and plus strong ability required preferred position seeking skills skill excellent support demonstrated using and of ' +
'de la el los las y en con para por que se del un una su sus como al es lo las o este esta ser tener experiencia años año trabajo puesto buscamos deberá debe mínima mínimo ' +
'da do das dos e em com para por que um uma sua seu como os as na no profissional vaga').split(/\s+/));
const NECESSARY=['required','requirements','must','minimum','essential','mandatory','requerido','requisitos','debe','deberá','mínimo','obligatorio','necessário','obrigatório','precisa'];
const PREFERRED=['preferred','nice to have','plus','desirable','advantage','deseable','preferido','valorable','desejável','preferencial','diferencial'];
function norm(s) { return String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/\s+/g,' ').trim(); }
function terms(s) { return (norm(s).match(/[\p{L}\p{N}+#.-]{2,}/gu)||[]).map(t=>t.replace(/^[.-]+|[.-]+$/g,'')).filter(t=>t && !STOPWORDS.has(t) && !/^\d+$/.test(t)); }
function hasAlias(text,alias) {
const h=' '+norm(text).replace(/[^\p{L}\p{N}]+/gu,' ')+' ';
const n=' '+norm(alias).replace(/[^\p{L}\p{N}]+/gu,' ').trim()+' ';
return h.includes(n);
}
function extract(text,map){return Object.keys(map).filter(k=>map[k].some(a=>hasAlias(text,a)));}
function splitSentences(text){ return String(text||'').replace(/\r/g,'\n').split(/(?:\n+|(?<=[.!?;])\s+|\s*[•▪◦]\s*)/u).map(s=>s.trim()).filter(s=>s.length>=15).slice(0,100); }
function keywordStats(text){const c=new Map();for(const t of terms(text)){if(t.length<3)continue;c.set(t,(c.get(t)||0)+1);}return Array.from(c.entries()).sort((a,b)=>b[1]-a[1]);}
function tfidfCosine(a,b) {
const docs=[terms(a),terms(b)];
if (!docs[0].length||!docs[1].length) return 0;
const maps=docs.map(arr=>{const features=arr.concat(arr.slice(1).map((x,i)=>arr[i]+' '+x));const count=new Map();for(const t of features)count.set(t,(count.get(t)||0)+1);return count;});
const features=new Set([...maps[0].keys(),...maps[1].keys()]);let dot=0,na=0,nb=0;
for(const t of features){const df=Number(maps[0].has(t))+Number(maps[1].has(t));const idf=1+Math.log(3/(1+df));const aa=(maps[0].get(t)||0)*idf,bb=(maps[1].get(t)||0)*idf;dot+=aa*bb;na+=aa*aa;nb+=bb*bb;}
return na&&nb?dot/Math.sqrt(na*nb):0;
}
const clamp=v=>Math.max(0,Math.min(1,v));
function requirements(job){
const ss=splitSentences(job);
return ss.map(s=>{
const lower=norm(s);const required=NECESSARY.some(t=>hasAlias(lower,t));const preferred=PREFERRED.some(t=>hasAlias(lower,t));
return {text:s.slice(0,300),importance:required?'Required':preferred?'Preferred':'Context',weight:required?1.5:preferred?0.75:0.3};
}).sort((a,b)=>b.weight-a.weight).slice(0,12);
}
function compare(cv,job){
cv=String(cv||'').trim();job=String(job||'').trim();
if(cv.length<40||job.length<40)throw new Error('Enter at least 40 characters in BOTH the CV and job description.');
if(cv.length>60000||job.length>60000)throw new Error('Please limit each document to 60,000 characters.');
const cvSkills=extract(cv,SKILLS), jobSkills=extract(job,SKILLS),jobLang=extract(job,LANGUAGES),cvLang=extract(cv,LANGUAGES);
const matchedSkills=jobSkills.filter(x=>cvSkills.includes(x)),missingSkills=jobSkills.filter(x=>!cvSkills.includes(x));
const matchedLang=jobLang.filter(x=>cvLang.includes(x)),missingLang=jobLang.filter(x=>!cvLang.includes(x));
const semantic=clamp(tfidfCosine(cv,job));
const skill=jobSkills.length?matchedSkills.length/jobSkills.length:0.5;
const cvWords=new Set(terms(cv));
const jwords=[...new Set(keywordStats(job).slice(0,35).map(x=>x[0]))];
const kwMatched=jwords.filter(x=>cvWords.has(x)),kwMissing=jwords.filter(x=>!cvWords.has(x));
const keyword=jwords.length?kwMatched.length/jwords.length:0;
const reqs=requirements(job).map(r=>{
const a=clamp(tfidfCosine(cv,r.text));const ts=terms(r.text);const m=ts.filter(w=>cvWords.has(w));
return {...r, evidence:Math.max(a,ts.length?m.length/ts.length:0),signals:m.slice(0,6)};
});
const weightSum=reqs.reduce((a,r)=>a+r.weight,0);
const reqScore=weightSum?reqs.reduce((a,r)=>a+r.weight*r.evidence,0)/weightSum:0.5;
const langRatio=jobLang.length?matchedLang.length/jobLang.length:1;
const weighted=semantic*.35+skill*.30+reqScore*.20+keyword*.15;
const score=Math.round(100*clamp(weighted*(jobLang.length?0.9+0.1*langRatio:1)));
const recommendations=[];
if(missingSkills.length)recommendations.push('Check whether you have evidence for these unmentioned job skills: '+missingSkills.slice(0,7).join(', ')+'. Add only verifiable experience.');
if(missingLang.length)recommendations.push('The role mentions '+missingLang.join(', ')+'; verify your proficiency before adding language claims.');
if(reqs.filter(x=>x.importance==='Required'&&x.evidence<0.25).length)recommendations.push('Review the required statements with weak textual evidence. Consider adding a specific achievement or example if true.');
if(semantic<0.25)recommendations.push('Your CV and the job description use different vocabulary. Clarify relevant experience with truthful, role-specific terms.');
if(!recommendations.length) recommendations.push('Most detected textual signals are present. Review them manually and focus on clear, specific achievements.');
recommendations.push('This analysis does not assess actual competence, eligibility, years worked or hiring probability.');
return {
score,band:score>=75?'Strong textual alignment':score>=50?'Partial textual alignment':'Limited textual alignment',
components:[{name:'Lexical TF-IDF alignment',value:Math.round(semantic*100),weight:35},{name:'Skill taxonomy coverage',value:Math.round(skill*100),weight:30},{name:'Requirement evidence',value:Math.round(reqScore*100),weight:20},{name:'Keyword overlap',value:Math.round(keyword*100),weight:15}],
matchedSkills,missingSkills,cvSkills,jobSkills,matchedLang,missingLang,jobLang,
reqs,keywordsMatched:kwMatched.slice(0,15),keywordsMissing:kwMissing.slice(0,15),recommendations,
method:'Browser-only TF-IDF (unigrams and bigrams), multilingual alias taxonomy, and heuristic requirement evidence. NO semantic embeddings or generative model.'
};
}
root.CareerMatchEngine=Object.freeze({compare,tfidfCosine,extractSkills:t=>extract(t,SKILLS),extractLanguages:t=>extract(t,LANGUAGES),requirements});
})(typeof window!=='undefined'?window:globalThis);