resume-analyzer-ai/index.html

950 lines
44 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ИИ-Рекрутер — Анализ вакансий и резюме</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--success:#10B981;--warning:#F59E0B;--danger:#EF4444}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100)}
.container{max-width:1200px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white);padding:60px 0}
.hero h1{font-size:48px;font-weight:800;line-height:1.1;margin-bottom:16px}
.hero p{font-size:20px;color:#9aa3b2;max-width:700px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none;border:none;cursor:pointer}
.btn:hover{background:#1be5ff}
.btn-secondary{background:transparent;color:var(--ink);border:2px solid var(--ink);padding:12px 26px;border-radius:8px}
.tabs{display:flex;gap:8px;margin-bottom:32px;flex-wrap:wrap}
.tab{padding:12px 24px;background:var(--white);border-radius:8px;cursor:pointer;font-weight:600;border:2px solid transparent}
.tab.active{border-color:var(--cyan);background:var(--cyan-50)}
.tab-content{display:none}
.tab-content.active{display:block}
.card{background:var(--white);border-radius:16px;padding:32px;margin-bottom:24px;box-shadow:0 4px 12px rgba(0,0,0,0.05)}
.form-group{margin-bottom:20px}
.form-group label{display:block;font-weight:600;margin-bottom:8px}
.form-group input,.form-group textarea,.form-group select{width:100%;padding:12px;border:2px solid var(--gray-100);border-radius:8px;font-family:inherit;font-size:16px}
.form-group textarea{height:150px;resize:vertical}
.form-group input:focus,.form-group textarea:focus,.form-group select:focus{outline:none;border-color:var(--cyan)}
.radio-group{display:flex;gap:16px;flex-wrap:wrap}
.radio-group label{display:flex;align-items:center;gap:8px;font-weight:normal;cursor:pointer}
.upload-area{border:3px dashed var(--gray-100);border-radius:16px;padding:48px;text-align:center;cursor:pointer;transition:all 0.3s}
.upload-area:hover{border-color:var(--cyan);background:var(--cyan-50)}
.upload-area.dragover{border-color:var(--cyan);background:var(--cyan-50)}
.file-list{margin-top:24px}
.file-item{display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--gray-100);border-radius:8px;margin-bottom:8px}
.file-item .name{font-weight:600}
.file-item .size{color:var(--gray-500);font-size:14px}
.file-item .remove{color:var(--danger);cursor:pointer;font-weight:600}
.results-table{width:100%;border-collapse:collapse;margin-top:24px}
.results-table th,.results-table td{padding:16px;text-align:left;border-bottom:1px solid var(--gray-100)}
.results-table th{background:var(--gray-100);font-weight:600}
.results-table tr:hover{background:var(--cyan-50)}
.score-badge{display:inline-block;padding:4px 12px;border-radius:20px;font-weight:700;font-size:14px}
.score-badge.high{background:#D1FAE5;color:#065F46}
.score-badge.medium{background:#FEF3C7;color:#92400E}
.score-badge.low{background:#FEE2E2;color:#991B1B}
.candidate-modal{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:none;align-items:center;justify-content:center;z-index:1000;padding:20px}
.candidate-modal.active{display:flex}
.modal-content{background:var(--white);border-radius:16px;padding:32px;max-width:900px;width:100%;max-height:90vh;overflow-y:auto}
.modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
.modal-close{background:none;border:none;font-size:24px;cursor:pointer}
.section-title{font-size:22px;font-weight:700;margin:24px 0 16px;border-bottom:2px solid var(--gray-100);padding-bottom:8px}
.progress-bar{height:8px;background:var(--gray-100);border-radius:4px;overflow:hidden;margin-top:8px}
.progress-fill{height:100%;background:var(--cyan);border-radius:4px}
@media (max-width:768px){.hero h1{font-size:36px}.container{padding:48px 20px}.tabs{gap:4px}.tab{padding:10px 16px;font-size:14px}}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>ИИ-Рекрутер</h1>
<p>Автоматический анализ вакансий и резюме. Загрузите описание вакансии и резюме кандидатов — ИИ-агент найдет лучших.</p>
<button class="btn" onclick="showTab('job')">Начать анализ</button>
</div>
</section>
<div class="container">
<div class="tabs">
<div class="tab active" onclick="showTab('job')">1. Вакансия</div>
<div class="tab" onclick="showTab('requirements')">2. Требования</div>
<div class="tab" onclick="showTab('upload')">3. Загрузка резюме</div>
<div class="tab" onclick="showTab('results')">4. Результаты</div>
</div>
<!-- Блок 1: Информация о вакансии -->
<div id="job" class="tab-content active">
<div class="card">
<h2 style="margin-bottom:24px">Информация о вакансии</h2>
<div class="form-group">
<label>Название вакансии</label>
<input type="text" id="jobTitle" placeholder="например, Senior Python Developer">
</div>
<div class="form-group">
<label>Компания</label>
<input type="text" id="company" placeholder="название компании">
</div>
<div class="form-group">
<label>Отдел</label>
<input type="text" id="department" placeholder="например, IT-отдел">
</div>
<div class="form-group">
<label>Город</label>
<input type="text" id="city" placeholder="например, Алматы">
</div>
<div class="form-group">
<label>Формат работы</label>
<div class="radio-group">
<label><input type="radio" name="format" value="office"> Офис</label>
<label><input type="radio" name="format" value="hybrid" checked> Гибрид</label>
<label><input type="radio" name="format" value="remote"> Удаленно</label>
</div>
</div>
<div class="form-group">
<label>Уровень позиции</label>
<select id="level">
<option value="Junior">Junior</option>
<option value="Middle" selected>Middle</option>
<option value="Senior">Senior</option>
<option value="Lead">Lead</option>
<option value="Head">Head</option>
</select>
</div>
<div class="form-group">
<label>Описание вакансии (Job Description)</label>
<textarea id="jobDescription" placeholder="Вставьте полное описание вакансии..."></textarea>
</div>
<button class="btn" onclick="showTab('requirements')">Далее: Требования</button>
</div>
</div>
<!-- Блок 2: Требования -->
<div id="requirements" class="tab-content">
<div class="card">
<h2 style="margin-bottom:24px">Требования к кандидату</h2>
<div class="form-group">
<label>Обязательные требования (каждое с новой строки)</label>
<textarea id="requiredSkills" placeholder="Python&#10;SQL&#10;Опыт от 3 лет&#10;Знание английского языка"></textarea>
</div>
<div class="form-group">
<label>Желательные требования (каждое с новой строки)</label>
<textarea id="preferredSkills" placeholder="Power BI&#10;Machine Learning&#10;Опыт в FinTech"></textarea>
</div>
<div class="form-group">
<label>Стоп-факторы (каждое с новой строки)</label>
<textarea id="stopFactors" placeholder="Нет опыта в нужной области&#10;Нет знания английского языка&#10;Отсутствует высшее образование"></textarea>
</div>
<button class="btn" onclick="showTab('upload')">Далее: Загрузка резюме</button>
</div>
</div>
<!-- Блок 3: Загрузка резюме -->
<div id="upload" class="tab-content">
<div class="card">
<h2 style="margin-bottom:24px">Загрузка резюме</h2>
<p style="margin-bottom:16px;color:var(--gray-500)">Загрузите до 100 файлов (PDF, DOC, DOCX, TXT). Извлечение текста и анализ выполняются в браузере.</p>
<div class="upload-area" id="uploadArea">
<div style="font-size:48px;margin-bottom:16px">📄</div>
<div style="font-size:20px;font-weight:600;margin-bottom:8px">Перетащите файлы сюда</div>
<div style="color:var(--gray-500)">или нажмите для выбора</div>
<input type="file" id="fileInput" multiple accept=".pdf,.doc,.docx,.txt" style="display:none">
</div>
<div class="file-list" id="fileList"></div>
<div style="margin-top:24px;display:flex;gap:16px">
<button class="btn" onclick="analyzeAllResumes()" id="analyzeBtn" disabled>Анализировать все резюме</button>
<button class="btn-secondary" onclick="clearFiles()">Очистить список</button>
</div>
</div>
</div>
<!-- Блок 4: Результаты -->
<div id="results" class="tab-content">
<div class="card">
<h2 style="margin-bottom:24px">Результаты анализа</h2>
<div id="resultsSummary" style="display:none">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:16px;margin-bottom:32px">
<div class="card" style="text-align:center">
<div style="font-size:32px;font-weight:800;color:var(--cyan)" id="totalCandidates">0</div>
<div style="color:var(--gray-500)">Всего кандидатов</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:32px;font-weight:800;color:var(--success)" id="topCandidates">0</div>
<div style="color:var(--gray-500)">Top Candidate</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:32px;font-weight:800;color:var(--warning)" id="strongMatches">0</div>
<div style="color:var(--gray-500)">Strong Match</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:32px;font-weight:800;color:var(--danger)" id="weakMatches">0</div>
<div style="color:var(--gray-500)">Weak Match</div>
</div>
</div>
<h3>Таблица кандидатов</h3>
<div style="overflow-x:auto">
<table class="results-table">
<thead>
<tr>
<th>Место</th>
<th>Кандидат</th>
<th>Score</th>
<th>Уровень</th>
<th>Опыт</th>
<th>Статус</th>
<th>Действие</th>
</tr>
</thead>
<tbody id="candidatesTableBody">
</tbody>
</table>
</div>
<div style="margin-top:32px">
<button class="btn" onclick="generateHRReport()">Сформировать отчет для HR</button>
</div>
</div>
<div id="noResults" style="text-align:center;padding:48px;color:var(--gray-500)">
<div style="font-size:48px;margin-bottom:16px">📊</div>
<div style="font-size:20px;font-weight:600">Загрузите резюме и запустите анализ</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно кандидата -->
<div class="candidate-modal" id="candidateModal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalCandidateName">Имя кандидата</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div id="modalContent">
<!-- Содержимое карточки кандидата -->
</div>
</div>
</div>
<!-- Скрипты для анализа PDF и DOCX -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
<script>
// Глобальные переменные
let uploadedFiles = [];
let analyzedCandidates = [];
// Переключение вкладок
function showTab(tabId) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
document.getElementById(tabId).classList.add('active');
}
// Загрузка файлов
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
function handleFiles(files) {
for (let file of files) {
if (uploadedFiles.length >= 100) {
alert('Максимум 100 файлов');
break;
}
if (!uploadedFiles.some(f => f.name === file.name)) {
uploadedFiles.push(file);
}
}
updateFileList();
}
function updateFileList() {
const fileList = document.getElementById('fileList');
fileList.innerHTML = uploadedFiles.map((file, index) => `
<div class="file-item">
<div>
<div class="name">${file.name}</div>
<div class="size">${(file.size / 1024).toFixed(1)} КБ</div>
</div>
<div class="remove" onclick="removeFile(${index})">Удалить</div>
</div>
`).join('');
document.getElementById('analyzeBtn').disabled = uploadedFiles.length === 0;
}
function removeFile(index) {
uploadedFiles.splice(index, 1);
updateFileList();
}
function clearFiles() {
uploadedFiles = [];
updateFileList();
}
// Извлечение текста из файлов
async function extractText(file) {
const extension = file.name.split('.').pop().toLowerCase();
if (extension === 'txt') {
return await file.text();
} else if (extension === 'pdf') {
return await extractTextFromPDF(file);
} else if (extension === 'doc' || extension === 'docx') {
return await extractTextFromDOCX(file);
}
return '';
}
async function extractTextFromPDF(file) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
let text = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
text += content.items.map(item => item.str).join(' ') + '\n';
}
return text;
}
async function extractTextFromDOCX(file) {
const arrayBuffer = await file.arrayBuffer();
const result = await mammoth.extractRawText({arrayBuffer});
return result.value;
}
// Анализ резюме
function analyzeResume(text, fileName, jobRequirements) {
const textLower = text.toLowerCase();
// Извлечение данных
const name = extractName(text, fileName);
const city = extractCity(text);
const lastPosition = extractLastPosition(text);
const experience = extractExperience(text);
const skills = extractSkills(text);
const education = extractEducation(text);
// Определение уровня
let level = 'Middle';
if (textLower.includes('senior') || textLower.includes('старший') || textLower.includes('lead') || textLower.includes('руководитель')) {
level = 'Senior';
} else if (textLower.includes('junior') || textLower.includes('младший') || textLower.includes('стажер')) {
level = 'Junior';
} else if (textLower.includes('head') || textLower.includes('директор') || textLower.includes('начальник')) {
level = 'Head';
}
// Анализ навыков
const hardSkills = extractHardSkills(text);
const softSkills = extractSoftSkills(text);
// Расчет детализированного Score
const score = calculateDetailedScore(hardSkills, softSkills, experience, level, education, jobRequirements, text);
// Определение статуса на основе нового шкалы
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,
city,
lastPosition,
experience,
level,
hardSkills,
softSkills,
education,
score,
status,
text,
fileName
};
}
// Вспомогательные функции извлечения
function extractName(text, fileName) {
// Простое извлечение имени из первых строк
const lines = text.split('\n').filter(line => line.trim());
if (lines.length > 0) {
const firstLine = lines[0].trim();
if (firstLine.length < 50 && !firstLine.includes('@')) {
return firstLine;
}
}
return fileName.replace(/\.[^/.]+$/, '');
}
function extractCity(text) {
const cities = ['алматы', 'астана', 'нур-султан', 'караганда', 'шимкент', 'актау', 'атырау', 'павлодар', 'семей', 'уральск'];
const textLower = text.toLowerCase();
for (const city of cities) {
if (textLower.includes(city)) {
return city.charAt(0).toUpperCase() + city.slice(1);
}
}
return 'Не указан';
}
function extractLastPosition(text) {
const lines = text.split('\n');
for (const line of lines) {
const lineLower = line.toLowerCase();
if (lineLower.includes('должность') || lineLower.includes('позиция') || lineLower.includes('менеджер') || lineLower.includes('разработчик') || lineLower.includes('аналитик')) {
return line.trim();
}
}
return 'Не указана';
}
function extractExperience(text) {
const textLower = text.toLowerCase();
if (textLower.includes('стаж') || textLower.includes('опыт')) {
const match = text.match(/(\d+)\s*(год|лет|мес)/i);
if (match) {
return `${match[1]} ${match[2]}`;
}
}
return 'Не указан';
}
function extractSkills(text) {
const skills = [];
const skillKeywords = ['python', 'java', 'javascript', 'sql', 'html', 'css', 'react', 'angular', 'vue', 'node.js', 'django', 'flask', 'spring', 'docker', 'kubernetes', 'aws', 'azure', 'gcp', 'git', 'linux', 'windows', 'sql', 'nosql', 'mongodb', 'postgresql', 'mysql', 'redis', 'kafka', 'rabbitmq', 'jenkins', 'ci/cd', 'agile', 'scrum'];
const textLower = text.toLowerCase();
for (const skill of skillKeywords) {
if (textLower.includes(skill)) {
skills.push(skill);
}
}
return skills;
}
function extractHardSkills(text) {
const hardSkills = [];
const keywords = ['python', 'java', 'javascript', 'sql', 'html', 'css', 'react', 'angular', 'vue', 'node.js', 'django', 'flask', 'spring', 'docker', 'kubernetes', 'aws', 'azure', 'gcp', 'git', 'linux', 'windows', 'mongodb', 'postgresql', 'mysql', 'redis', 'kafka', 'rabbitmq', 'jenkins', 'ci/cd', 'agile', 'scrum', 'machine learning', 'data analysis', 'tableau', 'power bi', 'excel', 'word', 'powerpoint'];
const textLower = text.toLowerCase();
for (const skill of keywords) {
if (textLower.includes(skill)) {
hardSkills.push(skill);
}
}
return hardSkills;
}
function extractSoftSkills(text) {
const softSkills = [];
const keywords = ['коммуникация', 'лидерство', 'командная работа', 'аналитическое мышление', 'креативность', 'стрессоустойчивость', 'организаторские', 'ответственность', 'инициативность', 'обучаемость', 'communication', 'leadership', 'teamwork', 'problem solving', 'creativity', 'stress resistance', 'organizational', 'responsibility', 'initiative', 'learning ability'];
const textLower = text.toLowerCase();
for (const skill of keywords) {
if (textLower.includes(skill)) {
softSkills.push(skill);
}
}
return softSkills;
}
function extractEducation(text) {
const textLower = text.toLowerCase();
if (textLower.includes('университет') || textLower.includes('институт') || textLower.includes('высшее образование')) {
return 'Высшее образование';
} else if (textLower.includes('колледж') || textLower.includes('техникум')) {
return 'Среднее профессиональное';
}
return 'Не указано';
}
function calculateDetailedScore(hardSkills, softSkills, experience, level, education, jobRequirements, text) {
const textLower = text.toLowerCase();
// 1. Профессиональный опыт — 30 баллов
let experienceScore = 0;
const experienceYears = parseInt(experience) || 0;
// Оценка релевантного опыта
if (experienceYears >= 5) experienceScore = 25;
else if (experienceYears >= 3) experienceScore = 20;
else if (experienceYears >= 1) experienceScore = 15;
else experienceScore = 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);
}
// Сложность проектов (по keywords)
const complexKeywords = ['lead', 'руководитель', 'senior', 'старший', 'управление', 'management'];
const hasComplexProjects = complexKeywords.some(keyword => textLower.includes(keyword));
if (hasComplexProjects) experienceScore = Math.min(30, experienceScore + 5);
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)
};
}
// Анализ всех резюме
async function analyzeAllResumes() {
if (uploadedFiles.length === 0) return;
const analyzeBtn = document.getElementById('analyzeBtn');
analyzeBtn.textContent = 'Анализируем...';
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, jobRequirements);
analyzedCandidates.push(candidate);
}
} catch (error) {
console.error(`Ошибка анализа ${file.name}:`, error);
}
}
// Сортировка по Score
analyzedCandidates.sort((a, b) => b.score.total - a.score.total);
// Обновление UI
updateResultsUI();
showTab('results');
analyzeBtn.textContent = 'Анализировать все резюме';
analyzeBtn.disabled = false;
}
function updateResultsUI() {
const tbody = document.getElementById('candidatesTableBody');
const resultsSummary = document.getElementById('resultsSummary');
const noResults = document.getElementById('noResults');
if (analyzedCandidates.length === 0) {
resultsSummary.style.display = 'none';
noResults.style.display = 'block';
return;
}
resultsSummary.style.display = 'block';
noResults.style.display = 'none';
// Статистика (обновлено под новые статусы)
document.getElementById('totalCandidates').textContent = analyzedCandidates.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.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>
<td><button class="btn-secondary" onclick="showCandidateModal(${index})">Подробнее</button></td>
</tr>
`).join('');
}
// Модальное окно кандидата
function showCandidateModal(index) {
const candidate = analyzedCandidates[index];
document.getElementById('modalCandidateName').textContent = candidate.name;
const modalContent = document.getElementById('modalContent');
modalContent.innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px">
<div>
<div class="section-title">Общая информация</div>
<p><strong>Город:</strong> ${candidate.city}</p>
<p><strong>Последняя должность:</strong> ${candidate.lastPosition}</p>
<p><strong>Общий стаж:</strong> ${candidate.experience}</p>
<p><strong>Уровень:</strong> ${candidate.level}</p>
<p><strong>Образование:</strong> ${candidate.education}</p>
</div>
<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.total}%</div>
<div class="progress-bar" style="margin-top:16px">
<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>
<div class="section-title">Сильные стороны</div>
<ul style="margin-bottom:24px">
${candidate.hardSkills.slice(0, 5).map(skill => `<li>${skill}</li>`).join('')}
${candidate.softSkills.slice(0, 2).map(skill => `<li>${skill}</li>`).join('')}
</ul>
<div class="section-title">Слабые стороны</div>
<ul style="margin-bottom:24px">
${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">
${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>
<div style="margin-bottom:24px">
<h4>Общие вопросы:</h4>
<ol>
<li>Расскажите о себе</li>
<li>Почему хотите работать у нас?</li>
<li>Какие ваши сильные стороны?</li>
<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.status}</strong>
<p style="margin-top:8px">${candidate.score.total >= 90 ? 'Кандидат полностью соответствует требованиям. Рекомендуется к немедленному интервью.' : candidate.score.total >= 80 ? 'Кандидат соответствует требованиям. Рекомендуется к интервью.' : candidate.score.total >= 70 ? 'Кандидат частично соответствует. Рекомендуется рассмотреть после уточнения деталей.' : 'Кандидат не соответствует основным требованиям. Требуется дополнительная оценка.'}</p>
</div>
`;
document.getElementById('candidateModal').classList.add('active');
}
function closeModal() {
document.getElementById('candidateModal').classList.remove('active');
}
// Закрытие модального окна при клике за пределами
document.getElementById('candidateModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('candidateModal')) {
closeModal();
}
});
// Формирование отчета для HR
function generateHRReport() {
if (analyzedCandidates.length === 0) {
alert('Сначала загрузите и проанализируйте резюме');
return;
}
const top10 = analyzedCandidates.slice(0, 10);
let report = `ОТЧЕТ ДЛЯ HR — ТОП-10 КАНДИДАТОВ\n\n`;
report += `Дата: ${new Date().toLocaleDateString()}\n`;
report += `Всего кандидатов: ${analyzedCandidates.length}\n\n`;
top10.forEach((candidate, index) => {
report += `${index + 1}. ${candidate.name}\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 += ` Детализация:\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`;
});
// Скачивание отчета
const blob = new Blob([report], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `HR_Report_${new Date().toISOString().slice(0,10)}.txt`;
a.click();
URL.revokeObjectURL(url);
}
</script>
</body>
</html>