v2: AI recruiter interface
This commit is contained in:
parent
2e85ce1d59
commit
f2151ec2b6
779
index.html
779
index.html
@ -3,146 +3,697 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>ИИ-Анализатор Резюме</title>
|
<title>ИИ-Рекрутер — Анализ вакансий и резюме</title>
|
||||||
<style>
|
<style>
|
||||||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
|
: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}
|
*{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(--white)}
|
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:1140px;margin:0 auto;padding:80px 24px}
|
.container{max-width:1200px;margin:0 auto;padding:80px 24px}
|
||||||
.hero{background:var(--ink);color:var(--white)}
|
.hero{background:var(--ink);color:var(--white);padding:60px 0}
|
||||||
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
|
.hero h1{font-size:48px;font-weight:800;line-height:1.1;margin-bottom:16px}
|
||||||
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
|
.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}
|
.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:hover{background:#1be5ff}
|
||||||
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
|
.btn-secondary{background:transparent;color:var(--ink);border:2px solid var(--ink);padding:12px 26px;border-radius:8px}
|
||||||
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
|
.tabs{display:flex;gap:8px;margin-bottom:32px;flex-wrap:wrap}
|
||||||
.upload-form{max-width:600px;margin:0 auto}
|
.tab{padding:12px 24px;background:var(--white);border-radius:8px;cursor:pointer;font-weight:600;border:2px solid transparent}
|
||||||
textarea{width:100%;height:200px;padding:16px;border:2px solid var(--gray-100);border-radius:8px;font-family:inherit;font-size:16px;resize:vertical}
|
.tab.active{border-color:var(--cyan);background:var(--cyan-50)}
|
||||||
textarea:focus{outline:none;border-color:var(--cyan)}
|
.tab-content{display:none}
|
||||||
.file-input{margin:16px 0}
|
.tab-content.active{display:block}
|
||||||
.results{display:none;margin-top:40px}
|
.card{background:var(--white);border-radius:16px;padding:32px;margin-bottom:24px;box-shadow:0 4px 12px rgba(0,0,0,0.05)}
|
||||||
.results h3{font-size:22px;font-weight:700;margin-bottom:16px}
|
.form-group{margin-bottom:20px}
|
||||||
.results .score{font-size:24px;font-weight:800;color:var(--cyan);margin-bottom:8px}
|
.form-group label{display:block;font-weight:600;margin-bottom:8px}
|
||||||
.results .section{margin-bottom:24px}
|
.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}
|
||||||
.results .section h4{font-size:18px;font-weight:700;margin-bottom:8px}
|
.form-group textarea{height:150px;resize:vertical}
|
||||||
.results .section p{margin-bottom:8px}
|
.form-group input:focus,.form-group textarea:focus,.form-group select:focus{outline:none;border-color:var(--cyan)}
|
||||||
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>ИИ-Анализатор Резюме</h1>
|
<h1>ИИ-Рекрутер</h1>
|
||||||
<p>Загрузите ваше резюме, и наш ИИ-агент проанализирует его, выделит ключевые компетенции и предложит подходящие должности.</p>
|
<p>Автоматический анализ вакансий и резюме. Загрузите описание вакансии и резюме кандидатов — ИИ-агент найдет лучших.</p>
|
||||||
<a class="btn" href="#upload">Загрузить резюме</a>
|
<button class="btn" onclick="showTab('job')">Начать анализ</button>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="upload" class="section">
|
|
||||||
<div class="container">
|
|
||||||
<h2>Загрузите ваше резюме</h2>
|
|
||||||
<div class="card upload-form">
|
|
||||||
<form id="resumeForm">
|
|
||||||
<textarea id="resumeText" placeholder="Вставьте текст вашего резюме сюда..."></textarea>
|
|
||||||
<div class="file-input">
|
|
||||||
<label for="resumeFile">Или загрузите файл (TXT, PDF, DOC):</label><br>
|
|
||||||
<input type="file" id="resumeFile" accept=".txt,.pdf,.doc,.docx">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn">Анализировать</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="results" class="section results">
|
|
||||||
<div class="container">
|
|
||||||
<h2>Результаты анализа</h2>
|
|
||||||
<div class="card" id="resultsContent">
|
|
||||||
<!-- Результаты будут вставлены здесь -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<script>
|
||||||
document.getElementById('resumeForm').addEventListener('submit', function(e) {
|
// Глобальные переменные
|
||||||
|
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();
|
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('');
|
||||||
|
|
||||||
const resumeText = document.getElementById('resumeText').value;
|
document.getElementById('analyzeBtn').disabled = uploadedFiles.length === 0;
|
||||||
const fileInput = document.getElementById('resumeFile');
|
}
|
||||||
|
|
||||||
|
function removeFile(index) {
|
||||||
|
uploadedFiles.splice(index, 1);
|
||||||
|
updateFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFiles() {
|
||||||
|
uploadedFiles = [];
|
||||||
|
updateFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлечение текста из файлов
|
||||||
|
async function extractText(file) {
|
||||||
|
const extension = file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
if (!resumeText && !fileInput.files.length) {
|
if (extension === 'txt') {
|
||||||
alert('Пожалуйста, вставьте текст резюме или загрузите файл');
|
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) {
|
||||||
|
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 = calculateMatchScore(hardSkills, experience, level);
|
||||||
|
|
||||||
|
// Определение статуса
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 calculateMatchScore(hardSkills, experience, level) {
|
||||||
|
// Базовый балл
|
||||||
|
let score = 50;
|
||||||
|
|
||||||
|
// Балл за навыки (35%)
|
||||||
|
const skillScore = Math.min(35, hardSkills.length * 5);
|
||||||
|
score += skillScore - 25; // Корректировка
|
||||||
|
|
||||||
|
// Балл за уровень (25%)
|
||||||
|
const levelScores = { 'Junior': 10, 'Middle': 20, 'Senior': 25, 'Lead': 25, 'Head': 25 };
|
||||||
|
score += (levelScores[level] || 15) - 15;
|
||||||
|
|
||||||
|
// Балл за опыт (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Случайная вариация для демо
|
||||||
|
score += Math.floor(Math.random() * 10) - 5;
|
||||||
|
|
||||||
|
// Ограничение 0-100
|
||||||
|
return Math.max(0, Math.min(100, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Анализ всех резюме
|
||||||
|
async function analyzeAllResumes() {
|
||||||
|
if (uploadedFiles.length === 0) return;
|
||||||
|
|
||||||
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||||
|
analyzeBtn.textContent = 'Анализируем...';
|
||||||
|
analyzeBtn.disabled = true;
|
||||||
|
|
||||||
|
analyzedCandidates = [];
|
||||||
|
|
||||||
|
for (const file of uploadedFiles) {
|
||||||
|
try {
|
||||||
|
const text = await extractText(file);
|
||||||
|
if (text) {
|
||||||
|
const candidate = analyzeResume(text, file.name);
|
||||||
|
analyzedCandidates.push(candidate);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ошибка анализа ${file.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортировка по Score
|
||||||
|
analyzedCandidates.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
// Обновление 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем индикатор загрузки
|
resultsSummary.style.display = 'block';
|
||||||
const resultsSection = document.getElementById('results');
|
noResults.style.display = 'none';
|
||||||
const resultsContent = document.getElementById('resultsContent');
|
|
||||||
resultsSection.style.display = 'block';
|
|
||||||
resultsContent.innerHTML = '<div class="score">Анализируем ваше резюме...</div>';
|
|
||||||
|
|
||||||
// Имитация задержки анализа
|
// Статистика
|
||||||
setTimeout(function() {
|
document.getElementById('totalCandidates').textContent = analyzedCandidates.length;
|
||||||
analyzeResume(resumeText || 'Текст из файла');
|
document.getElementById('topCandidates').textContent = analyzedCandidates.filter(c => c.status === 'Top Candidate').length;
|
||||||
}, 2000);
|
document.getElementById('strongMatches').textContent = analyzedCandidates.filter(c => c.status === 'Strong Match').length;
|
||||||
});
|
document.getElementById('weakMatches').textContent = analyzedCandidates.filter(c => c.status === 'Weak Match').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>${candidate.level}</td>
|
||||||
|
<td>${candidate.experience}</td>
|
||||||
|
<td>${candidate.status}</td>
|
||||||
|
<td><button class="btn-secondary" onclick="showCandidateModal(${index})">Подробнее</button></td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function analyzeResume(text) {
|
// Модальное окно кандидата
|
||||||
// Демо-анализ: определяем уровень на основе ключевых слов
|
function showCandidateModal(index) {
|
||||||
let level = 'Middle';
|
const candidate = analyzedCandidates[index];
|
||||||
let specialization = 'Специалист';
|
document.getElementById('modalCandidateName').textContent = candidate.name;
|
||||||
let score = 75;
|
|
||||||
|
|
||||||
const textLower = text.toLowerCase();
|
const modalContent = document.getElementById('modalContent');
|
||||||
|
modalContent.innerHTML = `
|
||||||
if (textLower.includes('senior') || textLower.includes('старший') || textLower.includes('lead') || textLower.includes('руководитель')) {
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px">
|
||||||
level = 'Senior';
|
<div>
|
||||||
score = 85;
|
<div class="section-title">Общая информация</div>
|
||||||
} else if (textLower.includes('junior') || textLower.includes('младший') || textLower.includes('стажер')) {
|
<p><strong>Город:</strong> ${candidate.city}</p>
|
||||||
level = 'Junior';
|
<p><strong>Последняя должность:</strong> ${candidate.lastPosition}</p>
|
||||||
score = 60;
|
<p><strong>Общий стаж:</strong> ${candidate.experience}</p>
|
||||||
}
|
<p><strong>Уровень:</strong> ${candidate.level}</p>
|
||||||
|
<p><strong>Образование:</strong> ${candidate.education}</p>
|
||||||
if (textLower.includes('разработчик') || textLower.includes('developer') || textLower.includes('программист')) {
|
</div>
|
||||||
specialization = 'Разработчик';
|
<div>
|
||||||
} else if (textLower.includes('менеджер') || textLower.includes('manager') || textLower.includes('руководитель')) {
|
<div class="section-title">Match Score</div>
|
||||||
specialization = 'Менеджер';
|
<div style="text-align:center;padding:20px">
|
||||||
} else if (textLower.includes('аналитик') || textLower.includes('analyst')) {
|
<div style="font-size:48px;font-weight:800;color:var(--cyan)">${candidate.score}%</div>
|
||||||
specialization = 'Аналитик';
|
<div class="progress-bar" style="margin-top:16px">
|
||||||
} else if (textLower.includes('дизайнер') || textLower.includes('designer')) {
|
<div class="progress-fill" style="width:${candidate.score}%"></div>
|
||||||
specialization = 'Дизайнер';
|
</div>
|
||||||
} else if (textLower.includes('маркетолог') || textLower.includes('marketing')) {
|
</div>
|
||||||
specialization = 'Маркетолог';
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
// Формируем результаты
|
|
||||||
const resultsHTML = `
|
|
||||||
<div class="score">Общий балл: ${score}/100</div>
|
|
||||||
<div class="section">
|
|
||||||
<h4>Общая оценка</h4>
|
|
||||||
<p><strong>Уровень:</strong> ${level}</p>
|
|
||||||
<p><strong>Специализация:</strong> ${specialization}</p>
|
|
||||||
<p><strong>Предполагаемый стаж:</strong> ${level === 'Senior' ? '5+ лет' : level === 'Middle' ? '3-5 лет' : '1-3 года'}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="section">
|
|
||||||
<h4>Рекомендуемые должности</h4>
|
<div class="section-title">Сильные стороны</div>
|
||||||
<p>• ${specialization} (${level})</p>
|
<ul style="margin-bottom:24px">
|
||||||
<p>• ${specialization}-${level === 'Senior' ? 'лидер' : 'специалист'}</p>
|
${candidate.hardSkills.slice(0, 5).map(skill => `<li>${skill}</li>`).join('')}
|
||||||
<p>• Тимлид ${specialization.toLowerCase()}</p>
|
${candidate.softSkills.slice(0, 2).map(skill => `<li>${skill}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="section-title">Слабые стороны</div>
|
||||||
|
<ul style="margin-bottom:24px">
|
||||||
|
<li>Ограниченный набор технологий</li>
|
||||||
|
<li>Недостаточно информации в резюме</li>
|
||||||
|
<li>Отсутствие портфолио</li>
|
||||||
|
<li>Нет опыта в смежных областях</li>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<div class="section">
|
|
||||||
<h4>Ключевые компетенции</h4>
|
<div class="section-title">Рекомендация</div>
|
||||||
<p>• Профессиональная экспертиза: ${Math.min(10, Math.floor(score/10) + 1)}/10</p>
|
<div style="padding:16px;background:var(--cyan-50);border-radius:8px;border-left:4px solid var(--cyan)">
|
||||||
<p>• Практический опыт: ${Math.min(10, Math.floor(score/12) + 1)}/10</p>
|
<strong>${candidate.score >= 80 ? 'Hire' : candidate.score >= 60 ? 'Hire with Reservations' : 'No Hire'}</strong>
|
||||||
<p>• Технические навыки: ${Math.min(10, Math.floor(score/11) + 1)}/10</p>
|
<p style="margin-top:8px">Кандидат ${candidate.score >= 80 ? 'соответствует' : 'частично соответствует'} требованиям вакансии. ${candidate.score >= 80 ? 'Рекомендуется к интервью.' : 'Требуется дополнительная проверка компетенций.'}</p>
|
||||||
<p>• Коммуникационные навыки: ${Math.min(10, Math.floor(score/13) + 1)}/10</p>
|
|
||||||
<p>• Потенциал развития: ${Math.min(10, Math.floor(score/14) + 1)}/10</p>
|
|
||||||
</div>
|
|
||||||
<div class="section">
|
|
||||||
<h4>Рекомендация</h4>
|
|
||||||
<p>${score >= 80 ? 'Рекомендовать к интервью' : score >= 65 ? 'Рекомендовать с оговорками' : 'Рассмотреть альтернативно'}</p>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.getElementById('resultsContent').innerHTML = resultsHTML;
|
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}%\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`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Скачивание отчета
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user