HR-ассистент для hh.ru

Виртуальный 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})();
HR-ассистент для hh.ru | Robomonkey