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