950 lines
44 KiB
HTML
950 lines
44 KiB
HTML
<!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 SQL Опыт от 3 лет Знание английского языка"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Желательные требования (каждое с новой строки)</label>
|
||
<textarea id="preferredSkills" placeholder="Power BI Machine Learning Опыт в FinTech"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Стоп-факторы (каждое с новой строки)</label>
|
||
<textarea id="stopFactors" placeholder="Нет опыта в нужной области Нет знания английского языка Отсутствует высшее образование"></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()">×</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>
|