diff --git a/index.html b/index.html index 8afea7e..6a3699e 100644 --- a/index.html +++ b/index.html @@ -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) => `
Требуется дополнительная проверка следующих компетенций:
Кандидат ${candidate.score >= 80 ? 'соответствует' : 'частично соответствует'} требованиям вакансии. ${candidate.score >= 80 ? 'Рекомендуется к интервью.' : 'Требуется дополнительная проверка компетенций.'}
+ ${candidate.status} +${candidate.score.total >= 90 ? 'Кандидат полностью соответствует требованиям. Рекомендуется к немедленному интервью.' : candidate.score.total >= 80 ? 'Кандидат соответствует требованиям. Рекомендуется к интервью.' : candidate.score.total >= 70 ? 'Кандидат частично соответствует. Рекомендуется рассмотреть после уточнения деталей.' : 'Кандидат не соответствует основным требованиям. Требуется дополнительная оценка.'}