v3: Detailed scoring system

This commit is contained in:
Dauren777 2026-06-19 06:35:49 +00:00
parent f2151ec2b6
commit b28bb7682b

View File

@ -338,7 +338,7 @@ async function extractTextFromDOCX(file) {
}
// Анализ резюме
function analyzeResume(text, fileName) {
function analyzeResume(text, fileName, jobRequirements) {
const textLower = text.toLowerCase();
// Извлечение данных
@ -363,15 +363,16 @@ function analyzeResume(text, fileName) {
const hardSkills = extractHardSkills(text);
const softSkills = extractSoftSkills(text);
// Расчет Score
const score = calculateMatchScore(hardSkills, experience, level);
// Расчет детализированного Score
const score = calculateDetailedScore(hardSkills, softSkills, experience, level, education, jobRequirements, text);
// Определение статуса
let status = 'Weak Match';
if (score >= 90) status = 'Top Candidate';
else if (score >= 80) status = 'Strong Match';
else if (score >= 70) status = 'Good Match';
else if (score >= 60) status = 'Potential Match';
// Определение статуса на основе нового шкалы
let status = 'No Hire';
if (score.total >= 95) status = 'Exceptional Candidate';
else if (score.total >= 90) status = 'Strong Hire';
else if (score.total >= 80) status = 'Hire';
else if (score.total >= 70) status = 'Consider';
else if (score.total >= 60) status = 'Weak Match';
return {
name,
@ -484,30 +485,184 @@ function extractEducation(text) {
return 'Не указано';
}
function calculateMatchScore(hardSkills, experience, level) {
// Базовый балл
let score = 50;
function calculateDetailedScore(hardSkills, softSkills, experience, level, education, jobRequirements, text) {
const textLower = text.toLowerCase();
// Балл за навыки (35%)
const skillScore = Math.min(35, hardSkills.length * 5);
score += skillScore - 25; // Корректировка
// 1. Профессиональный опыт — 30 баллов
let experienceScore = 0;
const experienceYears = parseInt(experience) || 0;
// Балл за уровень (25%)
const levelScores = { 'Junior': 10, 'Middle': 20, 'Senior': 25, 'Lead': 25, 'Head': 25 };
score += (levelScores[level] || 15) - 15;
// Оценка релевантного опыта
if (experienceYears >= 5) experienceScore = 25;
else if (experienceYears >= 3) experienceScore = 20;
else if (experienceYears >= 1) experienceScore = 15;
else experienceScore = 5;
// Балл за опыт (25%)
if (experience.includes('5') || experience.includes('6') || experience.includes('7') || experience.includes('8') || experience.includes('9') || experience.includes('10')) {
score += 10;
} else if (experience.includes('3') || experience.includes('4')) {
score += 5;
// Проверка опыта в аналогичной должности
const positionKeywords = ['разработчик', 'developer', 'менеджер', 'manager', 'аналитик', 'analyst', 'дизайнер', 'designer'];
const hasRelevantPosition = positionKeywords.some(keyword => textLower.includes(keyword));
if (hasRelevantPosition) experienceScore = Math.min(30, experienceScore + 5);
// Проверка опыта в аналогичной отрасли (если указана отрасль)
if (jobRequirements.department) {
const industryKeywords = jobRequirements.department.toLowerCase().split(' ');
const hasIndustryExperience = industryKeywords.some(keyword => textLower.includes(keyword));
if (hasIndustryExperience) experienceScore = Math.min(30, experienceScore + 5);
}
// Случайная вариация для демо
score += Math.floor(Math.random() * 10) - 5;
// Сложность проектов (по keywords)
const complexKeywords = ['lead', 'руководитель', 'senior', 'старший', 'управление', 'management'];
const hasComplexProjects = complexKeywords.some(keyword => textLower.includes(keyword));
if (hasComplexProjects) experienceScore = Math.min(30, experienceScore + 5);
// Ограничение 0-100
return Math.max(0, Math.min(100, score));
experienceScore = Math.min(30, experienceScore);
// 2. Hard Skills — 25 баллов
let hardSkillsScore = 0;
const requiredSkills = jobRequirements.requiredSkills.map(s => s.toLowerCase());
const foundRequiredSkills = requiredSkills.filter(skill => textLower.includes(skill));
const requiredPercentage = requiredSkills.length > 0 ? (foundRequiredSkills.length / requiredSkills.length) * 100 : 100;
if (requiredPercentage >= 100) hardSkillsScore = 25;
else if (requiredPercentage >= 80) hardSkillsScore = 20;
else if (requiredPercentage >= 60) hardSkillsScore = 15;
else if (requiredPercentage >= 40) hardSkillsScore = 10;
else hardSkillsScore = 5;
// Бонус за наличие желательных навыков
const preferredSkills = jobRequirements.preferredSkills.map(s => s.toLowerCase());
const foundPreferredSkills = preferredSkills.filter(skill => textLower.includes(skill));
hardSkillsScore = Math.min(25, hardSkillsScore + foundPreferredSkills.length * 2);
// 3. Соответствие должности — 15 баллов
let positionScore = 0;
const levelOrder = ['Junior', 'Middle', 'Senior', 'Lead', 'Head'];
const candidateLevelIndex = levelOrder.indexOf(level);
const jobLevelIndex = levelOrder.indexOf(jobRequirements.level);
if (candidateLevelIndex === jobLevelIndex) positionScore = 15;
else if (candidateLevelIndex === jobLevelIndex - 1) positionScore = 10;
else if (candidateLevelIndex === jobLevelIndex - 2) positionScore = 5;
else if (candidateLevelIndex > jobLevelIndex) positionScore = 12; // Higher level than required
else positionScore = 3;
// Проверка обязанностей по описанию вакансии
if (jobRequirements.description) {
const jobKeywords = jobRequirements.description.toLowerCase().split(' ').filter(word => word.length > 3);
const matchingJobKeywords = jobKeywords.filter(keyword => textLower.includes(keyword));
const jobMatchPercentage = jobKeywords.length > 0 ? (matchingJobKeywords.length / jobKeywords.length) * 100 : 0;
positionScore = Math.min(15, positionScore + Math.floor(jobMatchPercentage / 10));
}
// 4. Отраслевой опыт — 10 баллов
let industryScore = 0;
const industryKeywords = ['fintech', 'banking', 'retail', 'e-commerce', 'healthcare', 'education', 'telecom', 'gaming'];
const jobIndustry = jobRequirements.description.toLowerCase();
for (const industry of industryKeywords) {
if (jobIndustry.includes(industry)) {
if (textLower.includes(industry)) {
industryScore = 10;
} else if (textLower.includes('банк') || textLower.includes('финанс')) {
industryScore = 7;
} else if (textLower.includes('ритейл') || textLower.includes('магазин')) {
industryScore = 5;
} else {
industryScore = 2;
}
break;
}
}
// 5. Образование и сертификаты — 5 баллов
let educationScore = 0;
if (education === 'Высшее образование') {
educationScore = 5;
} else if (education === 'Среднее профессиональное') {
educationScore = 3;
} else {
educationScore = 1;
}
// Проверка сертификатов
const certKeywords = ['сертификат', 'certificate', 'certification', 'диплом', 'degree'];
const hasCerts = certKeywords.some(keyword => textLower.includes(keyword));
if (hasCerts) educationScore = Math.min(5, educationScore + 1);
// 6. Soft Skills — 5 баллов
let softSkillsScore = 0;
const softSkillKeywords = ['коммуникация', 'лидерство', 'управление', 'команда', 'переговоры', 'communication', 'leadership', 'team', 'management'];
const foundSoftSkills = softSkillKeywords.filter(skill => textLower.includes(skill));
if (foundSoftSkills.length >= 3) softSkillsScore = 5;
else if (foundSoftSkills.length >= 2) softSkillsScore = 4;
else if (foundSoftSkills.length >= 1) softSkillsScore = 3;
else softSkillsScore = 1;
// 7. Стабильность карьеры — 5 баллов
let stabilityScore = 3; // По умолчанию средняя стабильность
// Поиск мест работы
const workKeywords = ['работал', 'работаю', 'занимался', 'experience', 'work'];
const workMatches = textLower.split('\n').filter(line => workKeywords.some(keyword => line.includes(keyword)));
if (workMatches.length > 0) {
// Анализ длительности
const durationKeywords = ['года', 'лет', 'месяцев', 'год', ' year', ' years'];
const hasLongDuration = workMatches.some(match => durationKeywords.some(keyword => match.includes(keyword)));
if (hasLongDuration) {
stabilityScore = 5;
} else if (workMatches.length <= 2) {
stabilityScore = 4;
} else if (workMatches.length <= 3) {
stabilityScore = 3;
} else {
stabilityScore = 2;
}
}
// 8. Дополнительные преимущества — 5 баллов
let bonusScore = 0;
const bonusKeywords = ['английский', 'english', 'international', 'международный', 'награда', 'award', 'публикация', 'publication', 'выступление', 'presentation'];
const foundBonus = bonusKeywords.filter(keyword => textLower.includes(keyword));
bonusScore = Math.min(5, foundBonus.length * 2);
// Итоговый балл
const totalScore = experienceScore + hardSkillsScore + positionScore + industryScore + educationScore + softSkillsScore + stabilityScore + bonusScore;
return {
total: Math.min(100, totalScore),
experience: experienceScore,
hardSkills: hardSkillsScore,
position: positionScore,
industry: industryScore,
education: educationScore,
softSkills: softSkillsScore,
stability: stabilityScore,
bonus: bonusScore
};
}
// Получение требований вакансии из формы
function getJobRequirements() {
const requiredSkillsText = document.getElementById('requiredSkills').value;
const preferredSkillsText = document.getElementById('preferredSkills').value;
const stopFactorsText = document.getElementById('stopFactors').value;
return {
title: document.getElementById('jobTitle').value,
company: document.getElementById('company').value,
department: document.getElementById('department').value,
city: document.getElementById('city').value,
format: document.querySelector('input[name="format"]:checked')?.value || 'hybrid',
level: document.getElementById('level').value,
description: document.getElementById('jobDescription').value,
requiredSkills: requiredSkillsText.split('\n').map(s => s.trim()).filter(s => s),
preferredSkills: preferredSkillsText.split('\n').map(s => s.trim()).filter(s => s),
stopFactors: stopFactorsText.split('\n').map(s => s.trim()).filter(s => s)
};
}
// Анализ всех резюме
@ -519,12 +674,13 @@ async function analyzeAllResumes() {
analyzeBtn.disabled = true;
analyzedCandidates = [];
const jobRequirements = getJobRequirements();
for (const file of uploadedFiles) {
try {
const text = await extractText(file);
if (text) {
const candidate = analyzeResume(text, file.name);
const candidate = analyzeResume(text, file.name, jobRequirements);
analyzedCandidates.push(candidate);
}
} catch (error) {
@ -533,7 +689,7 @@ async function analyzeAllResumes() {
}
// Сортировка по Score
analyzedCandidates.sort((a, b) => b.score - a.score);
analyzedCandidates.sort((a, b) => b.score.total - a.score.total);
// Обновление UI
updateResultsUI();
@ -557,18 +713,18 @@ function updateResultsUI() {
resultsSummary.style.display = 'block';
noResults.style.display = 'none';
// Статистика
// Статистика (обновлено под новые статусы)
document.getElementById('totalCandidates').textContent = analyzedCandidates.length;
document.getElementById('topCandidates').textContent = analyzedCandidates.filter(c => c.status === 'Top Candidate').length;
document.getElementById('strongMatches').textContent = analyzedCandidates.filter(c => c.status === 'Strong Match').length;
document.getElementById('weakMatches').textContent = analyzedCandidates.filter(c => c.status === 'Weak Match').length;
document.getElementById('topCandidates').textContent = analyzedCandidates.filter(c => c.status === 'Exceptional Candidate' || c.status === 'Strong Hire').length;
document.getElementById('strongMatches').textContent = analyzedCandidates.filter(c => c.status === 'Hire').length;
document.getElementById('weakMatches').textContent = analyzedCandidates.filter(c => c.status === 'Weak Match' || c.status === 'No Hire').length;
// Таблица
tbody.innerHTML = analyzedCandidates.map((candidate, index) => `
<tr>
<td>${index + 1}</td>
<td><strong>${candidate.name}</strong><br><small>${candidate.city} • ${candidate.lastPosition}</small></td>
<td><span class="score-badge ${candidate.score >= 80 ? 'high' : candidate.score >= 60 ? 'medium' : 'low'}">${candidate.score}%</span></td>
<td><span class="score-badge ${candidate.score.total >= 80 ? 'high' : candidate.score.total >= 60 ? 'medium' : 'low'}">${candidate.score.total}%</span></td>
<td>${candidate.level}</td>
<td>${candidate.experience}</td>
<td>${candidate.status}</td>
@ -596,10 +752,87 @@ function showCandidateModal(index) {
<div>
<div class="section-title">Match Score</div>
<div style="text-align:center;padding:20px">
<div style="font-size:48px;font-weight:800;color:var(--cyan)">${candidate.score}%</div>
<div style="font-size:48px;font-weight:800;color:var(--cyan)">${candidate.score.total}%</div>
<div class="progress-bar" style="margin-top:16px">
<div class="progress-fill" style="width:${candidate.score}%"></div>
<div class="progress-fill" style="width:${candidate.score.total}%"></div>
</div>
<div style="margin-top:8px;font-weight:600;color:var(--gray-500)">${candidate.status}</div>
</div>
</div>
</div>
<div class="section-title">Детализация баллов</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px">
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Профессиональный опыт</span>
<span style="font-weight:700">${candidate.score.experience}/30</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.experience/30)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Hard Skills</span>
<span style="font-weight:700">${candidate.score.hardSkills}/25</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.hardSkills/25)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Соответствие должности</span>
<span style="font-weight:700">${candidate.score.position}/15</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.position/15)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Отраслевой опыт</span>
<span style="font-weight:700">${candidate.score.industry}/10</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.industry/10)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Образование и сертификаты</span>
<span style="font-weight:700">${candidate.score.education}/5</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.education/5)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Soft Skills</span>
<span style="font-weight:700">${candidate.score.softSkills}/5</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.softSkills/5)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Стабильность карьеры</span>
<span style="font-weight:700">${candidate.score.stability}/5</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.stability/5)*100}%"></div>
</div>
</div>
<div class="card" style="padding:16px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>Дополнительные преимущества</span>
<span style="font-weight:700">${candidate.score.bonus}/5</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${(candidate.score.bonus/5)*100}%"></div>
</div>
</div>
</div>
@ -612,21 +845,20 @@ function showCandidateModal(index) {
<div class="section-title">Слабые стороны</div>
<ul style="margin-bottom:24px">
<li>Ограниченный набор технологий</li>
<li>Недостаточно информации в резюме</li>
<li>Отсутствие портфолио</li>
<li>Нет опыта в смежных областях</li>
<li>Слабое описание достижений</li>
${candidate.score.hardSkills < 15 ? '<li>Недостаточно технических навыков</li>' : ''}
${candidate.score.experience < 20 ? '<li>Ограниченный профессиональный опыт</li>' : ''}
${candidate.score.position < 10 ? '<li>Несоответствие уровня позиции</li>' : ''}
${candidate.score.industry < 5 ? '<li>Нет отраслевого опыта</li>' : ''}
${candidate.score.stability < 3 ? '<li>Частая смена мест работы</li>' : ''}
</ul>
<div class="section-title">GAP-анализ</div>
<p style="margin-bottom:24px">Требуется дополнительная проверка следующих компетенций:</p>
<ul style="margin-bottom:24px">
<li>Глубокое знание конкретных технологий</li>
<li>Опыт работы в команде</li>
<li>Понимание бизнес-процессов</li>
<li>Коммуникативные навыки</li>
<li>Мотивация и карьерные цели</li>
${candidate.score.hardSkills < 20 ? '<li>Глубокое знание конкретных технологий</li>' : ''}
${candidate.score.softSkills < 4 ? '<li>Коммуникативные и лидерские навыки</li>' : ''}
${candidate.score.industry < 8 ? '<li>Понимание бизнес-процессов отрасли</li>' : ''}
${candidate.score.position < 12 ? '<li>Соответствие обязанностям должности</li>' : ''}
</ul>
<div class="section-title">Вопросы для интервью</div>
@ -639,12 +871,20 @@ function showCandidateModal(index) {
<li>Где видите себя через 5 лет?</li>
<li>Как справляетесь со стрессом?</li>
</ol>
<h4 style="margin-top:16px">Профессиональные вопросы:</h4>
<ol>
<li>Опишите ваш опыт работы с ключевыми технологиями</li>
<li>Расскажите о самом сложном проекте</li>
<li>Как вы подходите к решению задач?</li>
<li>Опишите ваш опыт работы в команде</li>
<li>Как вы следите за трендами в отрасли?</li>
</ol>
</div>
<div class="section-title">Рекомендация</div>
<div style="padding:16px;background:var(--cyan-50);border-radius:8px;border-left:4px solid var(--cyan)">
<strong>${candidate.score >= 80 ? 'Hire' : candidate.score >= 60 ? 'Hire with Reservations' : 'No Hire'}</strong>
<p style="margin-top:8px">Кандидат ${candidate.score >= 80 ? 'соответствует' : 'частично соответствует'} требованиям вакансии. ${candidate.score >= 80 ? 'Рекомендуется к интервью.' : 'Требуется дополнительная проверка компетенций.'}</p>
<strong>${candidate.status}</strong>
<p style="margin-top:8px">${candidate.score.total >= 90 ? 'Кандидат полностью соответствует требованиям. Рекомендуется к немедленному интервью.' : candidate.score.total >= 80 ? 'Кандидат соответствует требованиям. Рекомендуется к интервью.' : candidate.score.total >= 70 ? 'Кандидат частично соответствует. Рекомендуется рассмотреть после уточнения деталей.' : 'Кандидат не соответствует основным требованиям. Требуется дополнительная оценка.'}</p>
</div>
`;
@ -677,13 +917,22 @@ function generateHRReport() {
top10.forEach((candidate, index) => {
report += `${index + 1}. ${candidate.name}\n`;
report += ` Match Score: ${candidate.score}%\n`;
report += ` Match Score: ${candidate.score.total}/100\n`;
report += ` Уровень: ${candidate.level}\n`;
report += ` Опыт: ${candidate.experience}\n`;
report += ` Статус: ${candidate.status}\n`;
report += ` Город: ${candidate.city}\n`;
report += ` Последняя должность: ${candidate.lastPosition}\n`;
report += ` Рекомендация: ${candidate.score >= 80 ? 'Strong Hire' : candidate.score >= 60 ? 'Hire' : 'No Hire'}\n\n`;
report += ` Детализация:\n`;
report += ` - Профессиональный опыт: ${candidate.score.experience}/30\n`;
report += ` - Hard Skills: ${candidate.score.hardSkills}/25\n`;
report += ` - Соответствие должности: ${candidate.score.position}/15\n`;
report += ` - Отраслевой опыт: ${candidate.score.industry}/10\n`;
report += ` - Образование: ${candidate.score.education}/5\n`;
report += ` - Soft Skills: ${candidate.score.softSkills}/5\n`;
report += ` - Стабильность карьеры: ${candidate.score.stability}/5\n`;
report += ` - Дополнительные преимущества: ${candidate.score.bonus}/5\n`;
report += ` Рекомендация: ${candidate.status}\n\n`;
});
// Скачивание отчета