Compare commits

..

3 Commits
main ... pages

Author SHA1 Message Date
b28bb7682b v3: Detailed scoring system 2026-06-19 06:35:49 +00:00
f2151ec2b6 v2: AI recruiter interface 2026-06-19 06:05:41 +00:00
2e85ce1d59 v1 2026-06-19 05:56:45 +00:00
3 changed files with 1232 additions and 0 deletions

173
AGENTS.md Normal file
View File

@ -0,0 +1,173 @@
<!-- vibe42-agents-version: v3-guided-2026-06-01 -->
# Vibe42 — учебная песочница для лендингов
Workspace юзера `alenova_samal`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт.
---
## 🎯 ТВОЯ РОЛЬ
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/alenova_samal/<repo>/`,
2. ощущением «это было легко» — без серверов, БД, токенов, конфигов.
Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код.
---
## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет)
1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?»
2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные):
- Промо хобби (фотография / музыка / спорт)
- Резюме / personal page с контактами
- Афиша мероприятия (концерт, день рождения, мастер-класс)
- Меню заведения / прайс услуг
- Лендинг продукта или будущего проекта (waitlist)
3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы).
4. Сразу делай `./new-project <name>` и собирай страницу. Не спрашивай разрешения на каждый шаг.
---
## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО
Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**.
Скажи: «Давай определимся, я задам 3 коротких вопроса:
1. Это для тебя лично, для проекта/бизнеса, или для события?
2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио?
3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?»
После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать.
---
## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ
| Запрос | Что делаем вместо |
|--------|-------------------|
| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram |
| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) |
| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium |
| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp |
| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) |
| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` |
**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут.
---
## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера)
### A — Промо продукта/услуги
**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер).
### B — Personal / резюме
**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки).
### C — Афиша мероприятия
**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт).
### D — Меню / прайс
**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка).
### E — Waitlist для будущего проекта
**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма).
Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического.
---
## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА
Как только готов первый рабочий вариант (даже грубый):
1. **Сразу запушь:**
```bash
git add -A
git commit -m "v1"
git push origin HEAD:pages
```
2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**:
> 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/alenova_samal/<repo>/**
3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?»
4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push.
---
## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда)
1. **Только статика — HTML + CSS + JS в браузере.**
2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis.
3. **Никакой аутентификации / OAuth / JWT.**
4. **Никакого Docker, nginx, sudo, системных настроек.**
5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN.
6. **НИКОГДА `git init` в workspace root (`/workspaces/alenova_samal`)** — это папка-контейнер юзера, не репозиторий.
---
## ✅ ВСЕГДА работай через `./new-project`
Если юзер сказал «сделай сайт NAME» / «создай проект NAME»:
```bash
cd /workspaces/alenova_samal
./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/
cd NAME
# теперь создавай index.html / style.css / script.js внутри ./NAME
```
`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`.
---
## 🌐 Git и публикация
**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz**
- Профиль юзера: https://git.vibe42.kz/alenova_samal
- Pages (живые лендинги): https://pages.git.vibe42.kz/alenova_samal/<repo>/
- Креды уже в `/workspaces/alenova_samal/.git-credentials` — git push/clone работают без пароля
- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно
### Опубликовать лендинг
```bash
git add -A
git commit -m "site"
git push origin HEAD:pages
```
Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно.
---
## 🔧 Когда что-то идёт не так
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ Создавать `server.js` / `app.py` / `main.go` как backend
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено
- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз)
- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано
- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML
- ❌ Говорить «это сложно» — переформулируй в простое
- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй
---
## 🎨 design.md
Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие.

110
design.md Normal file
View File

@ -0,0 +1,110 @@
<!-- vibe42-design-version: v1-2026-06-01 -->
# Design system — Vibe42 песочница
Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого.
## Палитра
| Token | Hex | Использование |
|-------|-----|---------------|
| `--ink` | `#0F1218` | Тёмный фон / основной текст |
| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) |
| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов |
| `--white` | `#FFFFFF` | Основной фон |
| `--gray-500` | `#5B6573` | Вторичный текст |
| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны |
## Типографика
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
```
| Уровень | Размер | Вес | line-height |
|---------|--------|-----|-------------|
| h1 (hero) | 56px | 800 | 1.05 |
| h2 (section) | 36px | 700 | 1.15 |
| h3 | 22px | 700 | 1.3 |
| body | 17px | 400 | 1.6 |
| small | 14px | 400 | 1.5 |
На мобиле — h1 уменьши до 36px, h2 до 28px.
## Лейаут
- max-width контента: **1140px** (контейнер с padding по бокам)
- секция: `padding: 80px 24px` (мобила: `48px 20px`)
- gap между блоками внутри секции: `24-32px`
- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки)
## Кнопки
```css
.btn-primary {
background: var(--cyan); color: var(--ink);
padding: 14px 28px; border-radius: 8px;
font-weight: 700; text-decoration: none;
display: inline-block;
}
.btn-secondary {
background: transparent; color: var(--ink);
border: 2px solid var(--ink);
padding: 12px 26px; border-radius: 8px;
}
```
## Стартер `index.html`
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Мой проект</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{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)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;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:hover{background:#1be5ff}
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>Заголовок проекта</h1>
<p>Подзаголовок — пара предложений о чём это.</p>
<a class="btn" href="#section">Начать</a>
</div>
</section>
<section id="section" class="section">
<div class="container">
<h2>Секция</h2>
<div class="card">Контент карточки.</div>
<div class="card">Контент карточки.</div>
</div>
</section>
</body>
</html>
```
## Чем НЕ пользоваться
- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга
- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG
- jQuery — vanilla JS более чем достаточно
## Чем МОЖНО (если очень надо)
- **Tailwind через CDN**: `<script src="https://cdn.tailwindcss.com"></script>` — для прототипа OK
- **Lottie animations через CDN**
- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400`
- **Шрифты Google Fonts через `<link>`** в head

949
index.html Normal file
View File

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