/**
* UI - управление интерфейсом
*/
let state = {
currentScreen: 'input',
vacancyText: '',
resumeText: '',
criteria: [],
matchResults: null,
questions: []
};
function initUI() {
setupNavigation();
setupInputScreen();
setupCriteriaScreen();
setupMatchScreen();
setupInterviewScreen();
}
function setupNavigation() {
const navButtons = document.querySelectorAll('.hm-nav-btn');
navButtons.forEach(btn => {
btn.addEventListener('click', () => {
const screen = btn.dataset.screen;
navigateTo(screen);
});
});
}
function navigateTo(screen) {
if (!['input', 'criteria', 'match', 'interview'].includes(screen)) {
return;
}
state.currentScreen = screen;
document.querySelectorAll('.hm-screen').forEach(el => {
el.classList.remove('active');
});
document.getElementById(`screen-${screen}`).classList.add('active');
document.querySelectorAll('.hm-nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.screen === screen);
});
if (screen === 'criteria') {
renderCriteriaList();
} else if (screen === 'match') {
renderMatchResults();
} else if (screen === 'interview') {
renderQuestions();
}
}
function setupInputScreen() {
const vacancyField = document.getElementById('vacancyText');
const resumeField = document.getElementById('resumeText');
vacancyField.value = state.vacancyText;
resumeField.value = state.resumeText;
vacancyField.addEventListener('input', () => {
state.vacancyText = vacancyField.value;
});
resumeField.addEventListener('input', () => {
state.resumeText = resumeField.value;
});
document.getElementById('btnExtract').addEventListener('click', handleExtract);
document.getElementById('btnLoadExample').addEventListener('click', loadExample);
document.getElementById('btnClearAll').addEventListener('click', handleClearAll);
}
function handleExtract() {
if (!state.vacancyText.trim()) {
showNotification('Введите текст вакансии', 'error');
document.getElementById('vacancyText').focus();
return;
}
const result = window.matcher.extractRequirements(state.vacancyText);
if (result.error) {
showNotification(result.error, 'error');
return;
}
state.criteria = result.criteria;
navigateTo('criteria');
showNotification(`Найдено требований: ${result.criteria.length}`, 'success');
}
function loadExample() {
const exampleVacancy = `Требуется Frontend-разработчик
Обязанности:
- Разработка веб-интерфейсов на React и TypeScript
- Верстка адаптивных страниц (HTML, CSS, Sass)
- Работа с REST API
- Код-ревью и менторство младших разработчиков
Требования:
- Обязательно: опыт работы от 3 лет
- Уверенное знание JavaScript (ES6+) и TypeScript
- Опыт работы с React и экосистемой (Redux, React Router)
- Знание HTML, CSS, препроцессоров
- Опыт работы с Git
- Понимание принципов Agile/Scrum
- Английский язык не ниже Intermediate
Будет преимуществом:
- Опыт работы с GraphQL
- Знание Node.js
- Опыт настройки CI/CD`;
const exampleResume = `Иванов Иван
Frontend-разработчик
Опыт работы: 4 года
Навыки:
- JavaScript (ES6+), TypeScript — уверенное владение
- React, Redux, React Router — коммерческий опыт 3 года
- HTML5, CSS3, Sass, Less
- Git, GitHub, GitLab
- REST API, GraphQL (базово)
- Webpack, Vite
- Agile/Scrum, Jira, Confluence
Образование:
Высшее техническое, бакалавр информатики
Проекты:
2022-2024: Senior Frontend Developer в IT-компании
- Разработка CRM-системы на React + TypeScript
- Менторство двух junior-разработчиков
- Внедрение код-ревью практик
2020-2022: Middle Frontend Developer
- Создание лендингов и интернет-магазинов
- Работа с REST API
- Оптимизация производительности`;
state.vacancyText = exampleVacancy;
state.resumeText = exampleResume;
document.getElementById('vacancyText').value = exampleVacancy;
document.getElementById('resumeText').value = exampleResume;
showNotification('Пример загружен. Нажмите «Извлечь требования»', 'info');
}
function handleClearAll() {
if (!confirm('Вы уверены? Все данные будут удалены без возможности восстановления.')) {
return;
}
state = {
currentScreen: 'input',
vacancyText: '',
resumeText: '',
criteria: [],
matchResults: null,
questions: []
};
document.getElementById('vacancyText').value = '';
document.getElementById('resumeText').value = '';
window.storage.clearState();
navigateTo('input');
showNotification('Все данные очищены', 'info');
}
function setupCriteriaScreen() {
document.getElementById('btnAddCriterion').addEventListener('click', handleAddCriterion);
document.getElementById('btnApplyCriteria').addEventListener('click', handleApplyCriteria);
}
function renderCriteriaList() {
const container = document.getElementById('criteriaList');
if (state.criteria.length === 0) {
container.innerHTML = `
Нет критериев
Извлеките требования из текста вакансии или добавьте вручную
`;
return;
}
container.innerHTML = state.criteria.map((c, index) => `
`).join('');
container.querySelectorAll('.hm-criterion').forEach(row => {
const id = row.dataset.id;
const criterion = state.criteria.find(c => c.id === id);
row.querySelectorAll('input, select').forEach(input => {
input.addEventListener('input', (e) => {
const field = e.target.dataset.field;
let value = e.target.value;
if (field === 'required') {
value = value === 'true';
} else if (field === 'weight') {
value = parseInt(value, 10) || 3;
} else if (field === 'synonyms') {
value = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
}
Object.assign(criterion, { [field]: value });
});
});
const removeBtn = row.querySelector('[data-action="remove"]');
removeBtn.addEventListener('click', () => {
state.criteria = state.criteria.filter(c => c.id !== id);
renderCriteriaList();
});
});
}
function handleAddCriterion() {
state.criteria = window.matcher.addCustomCriterion(state.criteria, 'Новое требование', true, 3, []);
renderCriteriaList();
}
function handleApplyCriteria() {
if (state.criteria.length === 0) {
showNotification('Добавьте хотя бы один критерий', 'error');
return;
}
if (!state.resumeText.trim()) {
showNotification('Введите текст резюме', 'error');
navigateTo('input');
return;
}
const result = window.matcher.matchResumeToCriteria(state.criteria, state.resumeText);
state.matchResults = result;
navigateTo('match');
}
function setupMatchScreen() {
document.getElementById('btnBackToCriteria').addEventListener('click', () => {
navigateTo('criteria');
});
document.getElementById('btnGenerateQuestions').addEventListener('click', handleGenerateQuestions);
}
function renderMatchResults() {
const tbody = document.getElementById('matchTableBody');
const resultsDiv = document.getElementById('matchResults');
const emptyDiv = document.getElementById('matchEmpty');
if (!state.matchResults || !state.matchResults.results || state.matchResults.results.length === 0) {
resultsDiv.style.display = 'none';
emptyDiv.style.display = 'block';
return;
}
resultsDiv.style.display = 'block';
emptyDiv.style.display = 'none';
tbody.innerHTML = state.matchResults.results.map(r => {
const statusClass = `hm-status-${r.status}`;
const statusLabel = {
confirmed: 'Подтверждено',
clarify: 'Уточнить',
notfound: 'Не найдено'
}[r.status];
const evidenceHtml = r.evidence && r.evidence.length > 0
? `${escapeHtml(r.evidence[0])}
`
: 'Нет цитат';
const statusIcons = {
confirmed: '',
clarify: '',
notfound: ''
};
return `
| ${escapeHtml(r.label)} |
${r.required ? 'Обяз.' : 'Желат.'} |
${r.weight} |
${evidenceHtml} |
${statusLabel}
|
`;
}).join('');
const coverage = state.matchResults.coverage || 0;
document.getElementById('coverageFill').style.width = `${coverage}%`;
document.getElementById('coverageText').textContent = `Покрытие требований: ${coverage}%`;
}
function handleGenerateQuestions() {
if (!state.matchResults || !state.matchResults.results) {
showNotification('Нет результатов для генерации вопросов', 'error');
return;
}
const result = window.matcher.generateInterviewQuestions(state.matchResults);
state.questions = result.questions;
navigateTo('interview');
if (result.questions.length === 0) {
showNotification('Все требования подтверждены. Вопросы не требуются.', 'info');
} else {
showNotification(`Сгенерировано вопросов: ${result.questions.length}`, 'success');
}
}
function setupInterviewScreen() {
document.getElementById('btnBackToMatch').addEventListener('click', () => {
navigateTo('match');
});
document.getElementById('btnCopyQuestions').addEventListener('click', handleCopyQuestions);
document.getElementById('btnExport').addEventListener('click', handleExport);
}
function renderQuestions() {
const container = document.getElementById('questionsList');
if (!state.questions || state.questions.length === 0) {
container.innerHTML = `
Вопросов нет
Все требования подтверждены или ещё не сгенерированы
`;
return;
}
container.innerHTML = state.questions.map(q => `
${escapeHtml(q.label)}
${escapeHtml(q.question)}
${q.followUp && q.followUp.length > 0 ? `
Дополнительно: ${q.followUp.map(f => escapeHtml(f)).join('; ')}
` : ''}
`).join('');
}
async function handleCopyQuestions() {
if (!state.questions || state.questions.length === 0) {
showNotification('Нет вопросов для копирования', 'error');
return;
}
const text = state.questions.map(q => {
let result = `${q.label}:\n${q.question}`;
if (q.followUp && q.followUp.length > 0) {
result += `\nДополнительно: ${q.followUp.join('; ')}`;
}
return result;
}).join('\n\n');
try {
await navigator.clipboard.writeText(text);
showNotification('Вопросы скопированы в буфер обмена', 'success');
} catch (error) {
showNotification('Не удалось скопировать. Выделите текст и скопируйте вручную.', 'error');
}
}
function handleExport() {
const summary = window.matcher.exportSummary(
state.vacancyText,
state.resumeText,
state.criteria,
state.matchResults,
{ questions: state.questions }
);
const textContent = window.matcher.formatExportText(summary);
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
window.matcher.downloadText(textContent, `hr-matcher-${timestamp}.txt`);
showNotification('Сводка экспортирована', 'success');
}
function showNotification(message, type = 'info') {
const existing = document.querySelector('.hm-notification');
if (existing) {
existing.remove();
}
const notification = document.createElement('div');
notification.className = `hm-notification kt-ai-card`;
notification.style.cssText = `
position: fixed;
bottom: var(--kt-ai-space-6);
right: var(--kt-ai-space-6);
padding: var(--kt-ai-space-3) var(--kt-ai-space-4);
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-border);
border-radius: var(--kt-ai-radius-md);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
animation: hm-slide-in 0.3s ease;
max-width: 400px;
`;
const colors = {
success: 'var(--kt-ai-primary)',
error: 'var(--kt-ai-danger)',
info: 'var(--kt-ai-fg-muted)'
};
notification.innerHTML = `
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'hm-slide-out 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function escapeHtml(text) {
if (typeof text !== 'string') {
return '';
}
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
const style = document.createElement('style');
style.textContent = `
@keyframes hm-slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes hm-slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
function getState() {
return state;
}
function setState(newState) {
state = { ...state, ...newState };
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { initUI, navigateTo, getState, setState, showNotification };
}