Виртуальный HR-специалист для автоматического анализа резюме с помощью ИИ
Size
36.9 KB
Version
1.1.6
Created
Jan 30, 2026
Updated
5 days ago
1// ==UserScript==
2// @name HR-ассистент для hh.ru
3// @description Виртуальный HR-специалист для автоматического анализа резюме с помощью ИИ
4// @version 1.1.6
5// @match *://hh.ru/employer/vacancyresponses*
6// @grant GM.getValue
7// @grant GM.setValue
8// @grant GM.deleteValue
9// @grant GM.listValues
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 // Глобальные переменные
15 let isAnalyzing = false;
16 let isPaused = false;
17 let currentResumeIndex = 0;
18 let resumesList = [];
19 let analyzedResumes = [];
20
21 // Утилита для debounce
22 function debounce(func, wait) {
23 let timeout;
24 return function executedFunction(...args) {
25 const later = () => {
26 clearTimeout(timeout);
27 func(...args);
28 };
29 clearTimeout(timeout);
30 timeout = setTimeout(later, wait);
31 };
32 }
33
34 // Получение ID вакансии из URL
35 function getVacancyId() {
36 const urlParams = new URLSearchParams(window.location.search);
37 return urlParams.get('vacancyId');
38 }
39
40 // Получение названия вакансии
41 function getVacancyName() {
42 // Пробуем найти название вакансии в разных местах
43 const selectors = [
44 '.magritte-text___pbpft_4-4-4.magritte-text_style-primary___AQ7MW_4-4-4.magritte-text_typography-label-3-regular___Nhtlp_4-4-4',
45 'h1[data-qa="vacancy-title"]',
46 '[class*="magritte-interactive-text"]',
47 'a[href*="/vacancy/"]'
48 ];
49
50 for (const selector of selectors) {
51 const element = document.querySelector(selector);
52 if (element && element.textContent.trim().length > 5) {
53 return element.textContent.trim();
54 }
55 }
56
57 // Если не нашли, пробуем извлечь из параметра поиска
58 const urlParams = new URLSearchParams(window.location.search);
59 const searchText = urlParams.get('searchText');
60 if (searchText) {
61 return decodeURIComponent(searchText);
62 }
63
64 return 'Вакансия';
65 }
66
67 // Добавление стилей
68 function addStyles() {
69 const styles = `
70 .hr-assistant-button {
71 position: fixed;
72 bottom: 20px;
73 right: 20px;
74 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
75 color: white;
76 border: none;
77 padding: 12px 20px;
78 border-radius: 50px;
79 font-size: 14px;
80 font-weight: 600;
81 cursor: move;
82 box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
83 z-index: 10000;
84 transition: all 0.3s ease;
85 user-select: none;
86 }
87
88 .hr-assistant-button:hover {
89 transform: translateY(-2px);
90 box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
91 }
92
93 .hr-modal {
94 position: fixed;
95 background: white;
96 border-radius: 12px;
97 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
98 z-index: 10001;
99 min-width: 400px;
100 max-width: 600px;
101 max-height: 80vh;
102 overflow: hidden;
103 display: flex;
104 flex-direction: column;
105 }
106
107 .hr-modal-header {
108 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
109 color: white;
110 padding: 15px 20px;
111 cursor: move;
112 display: flex;
113 justify-content: space-between;
114 align-items: center;
115 user-select: none;
116 }
117
118 .hr-modal-title {
119 font-size: 18px;
120 font-weight: 600;
121 margin: 0;
122 }
123
124 .hr-modal-close {
125 background: rgba(255, 255, 255, 0.2);
126 border: none;
127 color: white;
128 width: 30px;
129 height: 30px;
130 border-radius: 50%;
131 cursor: pointer;
132 font-size: 18px;
133 display: flex;
134 align-items: center;
135 justify-content: center;
136 transition: background 0.2s;
137 }
138
139 .hr-modal-close:hover {
140 background: rgba(255, 255, 255, 0.3);
141 }
142
143 .hr-modal-body {
144 padding: 20px;
145 overflow-y: auto;
146 flex: 1;
147 }
148
149 .hr-form-group {
150 margin-bottom: 20px;
151 }
152
153 .hr-form-label {
154 display: block;
155 margin-bottom: 8px;
156 font-weight: 600;
157 color: #333;
158 font-size: 14px;
159 }
160
161 .hr-form-input {
162 width: 100%;
163 padding: 10px 12px;
164 border: 2px solid #e0e0e0;
165 border-radius: 8px;
166 font-size: 14px;
167 transition: border-color 0.2s;
168 box-sizing: border-box;
169 }
170
171 .hr-form-input:focus {
172 outline: none;
173 border-color: #667eea;
174 }
175
176 .hr-form-textarea {
177 min-height: 100px;
178 resize: vertical;
179 font-family: inherit;
180 }
181
182 .hr-button-group {
183 display: flex;
184 gap: 10px;
185 margin-top: 20px;
186 }
187
188 .hr-button {
189 flex: 1;
190 padding: 12px 20px;
191 border: none;
192 border-radius: 8px;
193 font-size: 14px;
194 font-weight: 600;
195 cursor: pointer;
196 transition: all 0.2s;
197 }
198
199 .hr-button-primary {
200 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
201 color: white;
202 }
203
204 .hr-button-primary:hover {
205 transform: translateY(-1px);
206 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
207 }
208
209 .hr-button-secondary {
210 background: #f5f5f5;
211 color: #333;
212 }
213
214 .hr-button-secondary:hover {
215 background: #e0e0e0;
216 }
217
218 .hr-button-danger {
219 background: #ff4757;
220 color: white;
221 }
222
223 .hr-button-danger:hover {
224 background: #ff3838;
225 }
226
227 .hr-button-warning {
228 background: #ffa502;
229 color: white;
230 }
231
232 .hr-button-warning:hover {
233 background: #ff8c00;
234 }
235
236 .hr-button:disabled {
237 opacity: 0.5;
238 cursor: not-allowed;
239 }
240
241 .hr-status {
242 padding: 12px;
243 background: #f0f4ff;
244 border-radius: 8px;
245 margin-bottom: 15px;
246 font-size: 14px;
247 color: #667eea;
248 text-align: center;
249 font-weight: 500;
250 }
251
252 .hr-resume-list {
253 margin-top: 20px;
254 }
255
256 .hr-resume-item {
257 padding: 15px;
258 background: #f9f9f9;
259 border-radius: 8px;
260 margin-bottom: 10px;
261 border-left: 4px solid #667eea;
262 }
263
264 .hr-resume-name {
265 font-weight: 600;
266 color: #333;
267 margin-bottom: 5px;
268 font-size: 15px;
269 }
270
271 .hr-resume-salary {
272 color: #27ae60;
273 font-weight: 600;
274 margin-bottom: 5px;
275 font-size: 14px;
276 }
277
278 .hr-resume-pros {
279 color: #27ae60;
280 font-size: 13px;
281 margin-bottom: 3px;
282 line-height: 1.4;
283 }
284
285 .hr-resume-cons {
286 color: #e74c3c;
287 font-size: 13px;
288 margin-bottom: 8px;
289 line-height: 1.4;
290 }
291
292 .hr-resume-score {
293 display: inline-block;
294 padding: 4px 12px;
295 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
296 color: white;
297 border-radius: 20px;
298 font-size: 13px;
299 font-weight: 600;
300 }
301
302 .hr-resume-score.high {
303 background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
304 }
305
306 .hr-resume-score.medium {
307 background: linear-gradient(135deg, #ffa502 0%, #ff8c00 100%);
308 }
309
310 .hr-resume-score.low {
311 background: linear-gradient(135deg, #ff4757 0%, #ff3838 100%);
312 }
313
314 .hr-resize-handle {
315 position: absolute;
316 bottom: 0;
317 right: 0;
318 width: 20px;
319 height: 20px;
320 cursor: nwse-resize;
321 background: linear-gradient(135deg, transparent 0%, transparent 50%, #667eea 50%, #667eea 100%);
322 }
323
324 .hr-loading {
325 display: inline-block;
326 width: 16px;
327 height: 16px;
328 border: 3px solid rgba(255, 255, 255, 0.3);
329 border-radius: 50%;
330 border-top-color: white;
331 animation: hr-spin 0.8s linear infinite;
332 }
333
334 @keyframes hr-spin {
335 to { transform: rotate(360deg); }
336 }
337 `;
338
339 const styleElement = document.createElement('style');
340 styleElement.textContent = styles;
341 document.head.appendChild(styleElement);
342 }
343
344 // Создание кнопки HR-ассистента
345 function createAssistantButton() {
346 const button = document.createElement('button');
347 button.className = 'hr-assistant-button';
348 button.textContent = '🤖 HR-ассистент';
349 button.addEventListener('click', openModal);
350 document.body.appendChild(button);
351
352 // Делаем кнопку перемещаемой
353 makeButtonDraggable(button);
354
355 console.log('HR-ассистент: Кнопка создана');
356 }
357
358 // Функция для перемещения кнопки
359 function makeButtonDraggable(button) {
360 let isDragging = false;
361 let currentX;
362 let currentY;
363 let initialX;
364 let initialY;
365
366 button.addEventListener('mousedown', (e) => {
367 isDragging = true;
368 initialX = e.clientX - button.offsetLeft;
369 initialY = e.clientY - button.offsetTop;
370 e.preventDefault();
371 });
372
373 document.addEventListener('mousemove', (e) => {
374 if (!isDragging) return;
375
376 e.preventDefault();
377 currentX = e.clientX - initialX;
378 currentY = e.clientY - initialY;
379
380 button.style.left = currentX + 'px';
381 button.style.top = currentY + 'px';
382 button.style.right = 'auto';
383 button.style.bottom = 'auto';
384 });
385
386 document.addEventListener('mouseup', (e) => {
387 if (isDragging) {
388 isDragging = false;
389 // Если это был просто клик (не перемещение), открываем модальное окно
390 const distance = Math.sqrt(Math.pow(e.clientX - (initialX + button.offsetLeft), 2) + Math.pow(e.clientY - (initialY + button.offsetTop), 2));
391 if (distance < 5) {
392 openModal();
393 }
394 }
395 });
396 }
397
398 // Создание модального окна
399 function createModal() {
400 const modal = document.createElement('div');
401 modal.className = 'hr-modal';
402 modal.style.display = 'none';
403 modal.style.left = '50%';
404 modal.style.top = '50%';
405 modal.style.transform = 'translate(-50%, -50%)';
406
407 const vacancyId = getVacancyId();
408 const vacancyName = getVacancyName();
409
410 modal.innerHTML = `
411 <div class="hr-modal-header">
412 <h3 class="hr-modal-title">🤖 HR-ассистент</h3>
413 <button class="hr-modal-close">×</button>
414 </div>
415 <div class="hr-modal-body">
416 <div class="hr-form-group">
417 <label class="hr-form-label">Вакансия</label>
418 <input type="text" class="hr-form-input" id="hr-vacancy-name" value="${vacancyName}" placeholder="Введите название вакансии">
419 </div>
420 <div class="hr-form-group">
421 <label class="hr-form-label">Требования к кандидату</label>
422 <textarea class="hr-form-input hr-form-textarea" id="hr-requirements" placeholder="Опишите требования к кандидату..."></textarea>
423 </div>
424 <div id="hr-status-container"></div>
425 <div class="hr-button-group" id="hr-initial-buttons">
426 <button class="hr-button hr-button-secondary" id="hr-close-btn">Закрыть</button>
427 <button class="hr-button hr-button-primary" id="hr-evaluate-btn">Оценить новые</button>
428 <button class="hr-button hr-button-primary" id="hr-evaluate-all-btn">Анализировать всех</button>
429 </div>
430 <div class="hr-button-group" id="hr-control-buttons" style="display: none;">
431 <button class="hr-button hr-button-warning" id="hr-pause-btn">Пауза</button>
432 <button class="hr-button hr-button-danger" id="hr-stop-btn">Остановить</button>
433 </div>
434 <div class="hr-resume-list" id="hr-resume-list"></div>
435 </div>
436 <div class="hr-resize-handle"></div>
437 `;
438
439 document.body.appendChild(modal);
440
441 // Обработчики событий
442 modal.querySelector('.hr-modal-close').addEventListener('click', closeModal);
443 modal.querySelector('#hr-close-btn').addEventListener('click', closeModal);
444 modal.querySelector('#hr-evaluate-btn').addEventListener('click', startEvaluation);
445 modal.querySelector('#hr-evaluate-all-btn').addEventListener('click', startEvaluationAll);
446 modal.querySelector('#hr-pause-btn').addEventListener('click', togglePause);
447 modal.querySelector('#hr-stop-btn').addEventListener('click', stopEvaluation);
448
449 // Перемещение модального окна
450 makeDraggable(modal);
451
452 // Изменение размера модального окна
453 makeResizable(modal);
454
455 console.log('HR-ассистент: Модальное окно создано');
456 return modal;
457 }
458
459 // Функция для перемещения модального окна
460 function makeDraggable(modal) {
461 const header = modal.querySelector('.hr-modal-header');
462 let isDragging = false;
463 let currentX;
464 let currentY;
465 let initialX;
466 let initialY;
467
468 header.addEventListener('mousedown', (e) => {
469 if (e.target.classList.contains('hr-modal-close')) return;
470
471 isDragging = true;
472 const rect = modal.getBoundingClientRect();
473 initialX = e.clientX - rect.left;
474 initialY = e.clientY - rect.top;
475
476 modal.style.transform = 'none';
477 });
478
479 document.addEventListener('mousemove', (e) => {
480 if (!isDragging) return;
481
482 e.preventDefault();
483 currentX = e.clientX - initialX;
484 currentY = e.clientY - initialY;
485
486 modal.style.left = currentX + 'px';
487 modal.style.top = currentY + 'px';
488 });
489
490 document.addEventListener('mouseup', () => {
491 isDragging = false;
492 });
493 }
494
495 // Функция для изменения размера модального окна
496 function makeResizable(modal) {
497 const resizeHandle = modal.querySelector('.hr-resize-handle');
498 let isResizing = false;
499 let startX, startY, startWidth, startHeight;
500
501 resizeHandle.addEventListener('mousedown', (e) => {
502 isResizing = true;
503 startX = e.clientX;
504 startY = e.clientY;
505 startWidth = modal.offsetWidth;
506 startHeight = modal.offsetHeight;
507 e.preventDefault();
508 });
509
510 document.addEventListener('mousemove', (e) => {
511 if (!isResizing) return;
512
513 const width = startWidth + (e.clientX - startX);
514 const height = startHeight + (e.clientY - startY);
515
516 if (width > 400) {
517 modal.style.width = width + 'px';
518 }
519 if (height > 300) {
520 modal.style.height = height + 'px';
521 }
522 });
523
524 document.addEventListener('mouseup', () => {
525 isResizing = false;
526 });
527 }
528
529 // Открытие модального окна
530 async function openModal() {
531 let modal = document.querySelector('.hr-modal');
532 if (!modal) {
533 modal = createModal();
534 }
535
536 modal.style.display = 'flex';
537
538 // Загрузка сохраненных требований
539 const vacancyId = getVacancyId();
540 const savedRequirements = await GM.getValue(`requirements_${vacancyId}`, '');
541 const savedVacancyName = await GM.getValue(`vacancy_name_${vacancyId}`, getVacancyName());
542
543 document.getElementById('hr-requirements').value = savedRequirements;
544 document.getElementById('hr-vacancy-name').value = savedVacancyName;
545
546 // Загрузка и отображение проанализированных резюме
547 await loadAnalyzedResumes();
548 displayResumes();
549
550 console.log('HR-ассистент: Модальное окно открыто');
551 }
552
553 // Закрытие модального окна
554 function closeModal() {
555 const modal = document.querySelector('.hr-modal');
556 if (modal) {
557 modal.style.display = 'none';
558 }
559 console.log('HR-ассистент: Модальное окно закрыто');
560 }
561
562 // Загрузка проанализированных резюме
563 async function loadAnalyzedResumes() {
564 const vacancyId = getVacancyId();
565 const saved = await GM.getValue(`analyzed_${vacancyId}`, '[]');
566 analyzedResumes = JSON.parse(saved);
567 console.log('HR-ассистент: Загружено резюме:', analyzedResumes.length);
568 }
569
570 // Сохранение проанализированных резюме
571 async function saveAnalyzedResumes() {
572 const vacancyId = getVacancyId();
573 await GM.setValue(`analyzed_${vacancyId}`, JSON.stringify(analyzedResumes));
574 console.log('HR-ассистент: Резюме сохранены');
575 }
576
577 // Отображение списка резюме
578 function displayResumes() {
579 const container = document.getElementById('hr-resume-list');
580 if (!container) return;
581
582 // Сортировка по оценке (от высокой к низкой)
583 const sorted = [...analyzedResumes].sort((a, b) => b.score - a.score);
584
585 if (sorted.length === 0) {
586 container.innerHTML = '<p style="text-align: center; color: #999; padding: 20px;">Пока нет проанализированных резюме</p>';
587 return;
588 }
589
590 container.innerHTML = sorted.map(resume => {
591 let scoreClass = 'low';
592 if (resume.score >= 8) scoreClass = 'high';
593 else if (resume.score >= 6) scoreClass = 'medium';
594
595 const prosHtml = resume.pros ? `<div class="hr-resume-pros">✓ ${resume.pros}</div>` : '';
596 const consHtml = resume.cons ? `<div class="hr-resume-cons">✗ ${resume.cons}</div>` : '';
597
598 return `
599 <div class="hr-resume-item">
600 <a href="${resume.url}" target="_blank" class="hr-resume-name" style="text-decoration: none; color: #333; cursor: pointer;">${resume.name}</a>
601 <div class="hr-resume-salary">${resume.salary || 'Зарплата не указана'}</div>
602 <div class="hr-resume-score ${scoreClass}">Оценка: ${resume.score}/10</div>
603 ${prosHtml}
604 ${consHtml}
605 </div>
606 `;
607 }).join('');
608 }
609
610 // Обновление статуса
611 function updateStatus(message) {
612 const container = document.getElementById('hr-status-container');
613 if (!container) return;
614
615 container.innerHTML = `<div class="hr-status">${message}</div>`;
616 }
617
618 // Получение списка резюме на странице
619 function getResumesList() {
620 const resumes = [];
621 const resumeElements = document.querySelectorAll('[data-qa="resume-serp__resume"]');
622
623 console.log('HR-ассистент: Найдено резюме на странице:', resumeElements.length);
624
625 resumeElements.forEach((element) => {
626 const resumeId = element.getAttribute('data-resume-id');
627 const titleElement = element.querySelector('.title--Z9FeLyEY3sZrwn2k a');
628 const nameElement = element.querySelector('[data-qa="resume-serp__resume-fullname"]');
629 const salaryElement = element.querySelector('[data-qa="resume-serp__resume-compensation"]');
630
631 if (resumeId && titleElement) {
632 resumes.push({
633 id: resumeId,
634 title: titleElement.textContent.trim(),
635 name: nameElement ? nameElement.textContent.trim() : 'Имя не указано',
636 salary: salaryElement ? salaryElement.textContent.trim() : 'Не указана',
637 url: titleElement.href,
638 element: element
639 });
640 }
641 });
642
643 return resumes;
644 }
645
646 // Начало оценки резюме
647 async function startEvaluation() {
648 const requirements = document.getElementById('hr-requirements').value.trim();
649 const vacancyName = document.getElementById('hr-vacancy-name').value.trim();
650
651 if (!requirements) {
652 alert('Пожалуйста, укажите требования к кандидату');
653 return;
654 }
655
656 if (!vacancyName) {
657 alert('Пожалуйста, укажите название вакансии');
658 return;
659 }
660
661 // Сохранение требований и названия вакансии
662 const vacancyId = getVacancyId();
663 await GM.setValue(`requirements_${vacancyId}`, requirements);
664 await GM.setValue(`vacancy_name_${vacancyId}`, vacancyName);
665
666 // Получение списка резюме
667 resumesList = getResumesList();
668
669 if (resumesList.length === 0) {
670 alert('На странице не найдено резюме для анализа');
671 return;
672 }
673
674 // Фильтрация уже проанализированных резюме
675 const analyzedIds = new Set(analyzedResumes.map(r => r.id));
676 resumesList = resumesList.filter(r => !analyzedIds.has(r.id));
677
678 if (resumesList.length === 0) {
679 updateStatus('Все резюме на этой странице уже проанализированы');
680 return;
681 }
682
683 console.log('HR-ассистент: Начинаем анализ', resumesList.length, 'резюме');
684
685 // Переключение кнопок
686 document.getElementById('hr-initial-buttons').style.display = 'none';
687 document.getElementById('hr-control-buttons').style.display = 'flex';
688
689 isAnalyzing = true;
690 isPaused = false;
691 currentResumeIndex = 0;
692
693 updateStatus(`Анализ резюме: 0 из ${resumesList.length}`);
694
695 // Запуск анализа
696 await processNextResume(requirements);
697 }
698
699 // Начало оценки всех резюме (включая уже проанализированные)
700 async function startEvaluationAll() {
701 const requirements = document.getElementById('hr-requirements').value.trim();
702 const vacancyName = document.getElementById('hr-vacancy-name').value.trim();
703
704 if (!requirements) {
705 alert('Пожалуйста, укажите требования к кандидату');
706 return;
707 }
708
709 if (!vacancyName) {
710 alert('Пожалуйста, укажите название вакансии');
711 return;
712 }
713
714 // Подтверждение повторного анализа
715 if (analyzedResumes.length > 0) {
716 const confirmed = confirm(`Вы уверены? Это удалит ${analyzedResumes.length} проанализированных резюме и начнет анализ заново.`);
717 if (!confirmed) {
718 return;
719 }
720 }
721
722 // Очищаем список проанализированных резюме
723 analyzedResumes = [];
724 await saveAnalyzedResumes();
725 displayResumes();
726
727 // Сохранение требований и названия вакансии
728 const vacancyId = getVacancyId();
729 await GM.setValue(`requirements_${vacancyId}`, requirements);
730 await GM.setValue(`vacancy_name_${vacancyId}`, vacancyName);
731
732 // Получение списка резюме
733 resumesList = getResumesList();
734
735 if (resumesList.length === 0) {
736 alert('На странице не найдено резюме для анализа');
737 return;
738 }
739
740 console.log('HR-ассистент: Начинаем анализ всех', resumesList.length, 'резюме');
741
742 // Переключение кнопок
743 document.getElementById('hr-initial-buttons').style.display = 'none';
744 document.getElementById('hr-control-buttons').style.display = 'flex';
745
746 isAnalyzing = true;
747 isPaused = false;
748 currentResumeIndex = 0;
749
750 updateStatus(`Анализ резюме: 0 из ${resumesList.length}`);
751
752 // Запуск анализа
753 await processNextResume(requirements);
754 }
755
756 // Обработка следующего резюме
757 async function processNextResume(requirements) {
758 if (!isAnalyzing || currentResumeIndex >= resumesList.length) {
759 finishEvaluation();
760 return;
761 }
762
763 if (isPaused) {
764 updateStatus(`Пауза. Обработано: ${currentResumeIndex} из ${resumesList.length}`);
765 return;
766 }
767
768 const resume = resumesList[currentResumeIndex];
769 updateStatus(`Анализ резюме: ${currentResumeIndex + 1} из ${resumesList.length} <div class="hr-loading"></div>`);
770
771 console.log('HR-ассистент: Анализируем резюме', resume.name);
772
773 try {
774 // Получаем содержимое резюме напрямую через HTTP запрос (без открытия вкладки)
775 let resumeContent = '';
776 try {
777 console.log('HR-ассистент: Загружаем содержимое резюме:', resume.url);
778
779 // Получаем текст резюме через GM.xmlhttpRequest
780 const response = await GM.xmlhttpRequest({
781 method: 'GET',
782 url: resume.url,
783 headers: {
784 'User-Agent': navigator.userAgent
785 }
786 });
787
788 // Парсим HTML и извлекаем текст
789 const parser = new DOMParser();
790 const doc = parser.parseFromString(response.responseText, 'text/html');
791
792 // Извлекаем основные блоки резюме
793 const experienceBlocks = doc.querySelectorAll('[data-qa="resume-block-experience"]');
794 const skillsBlock = doc.querySelector('[data-qa="skills-table"]');
795 const educationBlock = doc.querySelector('[data-qa="resume-block-education"]');
796 const aboutBlock = doc.querySelector('[data-qa="resume-block-skills-content"]');
797
798 let experienceText = '';
799 experienceBlocks.forEach(block => {
800 experienceText += block.textContent.trim() + '\n';
801 });
802
803 resumeContent = `
804Название: ${resume.title}
805Имя: ${resume.name}
806Зарплата: ${resume.salary}
807
808Опыт работы:
809${experienceText || 'Не указан'}
810
811Навыки:
812${skillsBlock ? skillsBlock.textContent.trim() : 'Не указаны'}
813
814Образование:
815${educationBlock ? educationBlock.textContent.trim() : 'Не указано'}
816
817О себе:
818${aboutBlock ? aboutBlock.textContent.trim() : 'Не указано'}
819 `.trim();
820
821 console.log('HR-ассистент: Получено содержимое резюме, длина:', resumeContent.length);
822
823 } catch (error) {
824 console.error('HR-ассистент: Ошибка при получении содержимого резюме', error);
825 // Используем базовую информацию
826 resumeContent = `
827 Название: ${resume.title}
828 Имя: ${resume.name}
829 Зарплата: ${resume.salary}
830 `;
831 }
832
833 // Анализ с помощью ИИ
834 const prompt = `Ты HR-специалист. Проанализируй резюме кандидата и оцени его соответствие требованиям вакансии.
835
836Требования к вакансии:
837${requirements}
838
839Резюме кандидата:
840${resumeContent}
841
842Оцени кандидата и предоставь результат в следующем формате.`;
843
844 const aiResponse = await RM.aiCall(prompt, {
845 type: 'json_schema',
846 json_schema: {
847 name: 'resume_analysis',
848 schema: {
849 type: 'object',
850 properties: {
851 score: {
852 type: 'number',
853 minimum: 1,
854 maximum: 10,
855 description: 'Оценка от 1 до 10'
856 },
857 pros: {
858 type: 'string',
859 description: 'Краткие плюсы кандидата (1-2 предложения)'
860 },
861 cons: {
862 type: 'string',
863 description: 'Краткие минусы кандидата (1-2 предложения)'
864 }
865 },
866 required: ['score', 'pros', 'cons']
867 }
868 }
869 });
870
871 console.log('HR-ассистент: Оценка резюме', resume.name, '=', aiResponse.score);
872
873 // Сохраняем результат
874 analyzedResumes.push({
875 id: resume.id,
876 name: resume.name,
877 title: resume.title,
878 salary: resume.salary,
879 url: resume.url,
880 score: aiResponse.score,
881 pros: aiResponse.pros,
882 cons: aiResponse.cons,
883 analyzedAt: new Date().toISOString()
884 });
885
886 await saveAnalyzedResumes();
887 displayResumes();
888
889 // Переходим к следующему резюме
890 currentResumeIndex++;
891
892 // Небольшая задержка перед следующим резюме
893 await new Promise(resolve => setTimeout(resolve, 1000));
894
895 // Рекурсивно обрабатываем следующее резюме
896 await processNextResume(requirements);
897
898 } catch (error) {
899 console.error('HR-ассистент: Ошибка при анализе резюме', error);
900
901 // Пропускаем проблемное резюме и переходим к следующему
902 currentResumeIndex++;
903 await processNextResume(requirements);
904 }
905 }
906
907 // Переключение паузы
908 function togglePause() {
909 isPaused = !isPaused;
910 const pauseBtn = document.getElementById('hr-pause-btn');
911
912 if (isPaused) {
913 pauseBtn.textContent = 'Продолжить';
914 pauseBtn.classList.remove('hr-button-warning');
915 pauseBtn.classList.add('hr-button-primary');
916 updateStatus(`Пауза. Обработано: ${currentResumeIndex} из ${resumesList.length}`);
917 console.log('HR-ассистент: Анализ приостановлен');
918 } else {
919 pauseBtn.textContent = 'Пауза';
920 pauseBtn.classList.remove('hr-button-primary');
921 pauseBtn.classList.add('hr-button-warning');
922 console.log('HR-ассистент: Анализ возобновлен');
923
924 // Продолжаем анализ
925 const requirements = document.getElementById('hr-requirements').value.trim();
926 processNextResume(requirements);
927 }
928 }
929
930 // Остановка оценки
931 function stopEvaluation() {
932 isAnalyzing = false;
933 isPaused = false;
934 finishEvaluation();
935 console.log('HR-ассистент: Анализ остановлен пользователем');
936 }
937
938 // Завершение оценки
939 function finishEvaluation() {
940 isAnalyzing = false;
941 isPaused = false;
942
943 document.getElementById('hr-initial-buttons').style.display = 'flex';
944 document.getElementById('hr-control-buttons').style.display = 'none';
945
946 updateStatus(`Анализ завершен! Обработано резюме: ${currentResumeIndex}`);
947
948 console.log('HR-ассистент: Анализ завершен');
949 }
950
951 // Инициализация
952 function init() {
953 console.log('HR-ассистент: Инициализация расширения');
954
955 // Проверяем, что мы на странице откликов
956 if (!window.location.href.includes('vacancyresponses')) {
957 console.log('HR-ассистент: Не на странице откликов');
958 return;
959 }
960
961 // Добавляем стили
962 addStyles();
963
964 // Создаем кнопку
965 createAssistantButton();
966
967 console.log('HR-ассистент: Расширение готово к работе');
968 }
969
970 // Запуск после загрузки страницы
971 if (document.readyState === 'loading') {
972 document.addEventListener('DOMContentLoaded', init);
973 } else {
974 init();
975 }
976})();