/** * 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 = `
${escapeHtml(message)}
`; 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 }; }