/** * Matcher - логика сопоставления резюме и вакансии */ function extractRequirements(vacancyText) { if (!vacancyText || typeof vacancyText !== 'string') { return { criteria: [], error: 'Введите текст вакансии' }; } const result = window.aiAdapter.generate('extractCriteria', { vacancyText }); return result; } function matchResumeToCriteria(criteria, resumeText) { if (!criteria || criteria.length === 0) { return { results: [], coverage: 0, error: 'Список критериев пуст' }; } if (!resumeText || typeof resumeText !== 'string') { return { results: [], coverage: 0, error: 'Введите текст резюме' }; } const result = window.aiAdapter.generate('matchResume', { criteria, resumeText }); return result; } function updateCriterionStatus(results, criterionId, newStatus, note) { if (!results || !Array.isArray(results)) { return results; } return results.map(result => { if (result.criterionId === criterionId) { return { ...result, status: newStatus, note: note || result.note }; } return result; }); } function addCustomCriterion(criteria, label, required, weight, synonyms) { const newCriterion = { id: `custom-${Date.now()}`, label: label.trim(), required: required !== false, weight: Math.max(1, Math.min(5, parseInt(weight, 10) || 3)), synonyms: (synonyms || []).filter(s => s.trim().length > 0), category: 'custom' }; return [...criteria, newCriterion]; } function removeCriterion(criteria, criterionId) { return criteria.filter(c => c.id !== criterionId); } function updateCriterion(criteria, criterionId, updates) { return criteria.map(c => { if (c.id === criterionId) { return { ...c, ...updates }; } return c; }); } function generateInterviewQuestions(matchResults) { if (!matchResults || !matchResults.results || matchResults.results.length === 0) { return { questions: [] }; } const unmatched = matchResults.results.filter( r => r.status === 'clarify' || r.status === 'notfound' ); if (unmatched.length === 0) { return { questions: [] }; } const result = window.aiAdapter.generate('generateQuestions', { unmatchedCriteria: unmatched }); return result; } function calculateCoverage(results) { if (!results || results.length === 0) { return 0; } const totalWeight = results.reduce((sum, r) => sum + (r.weight || 0), 0); if (totalWeight === 0) { return 0; } const confirmedWeight = results .filter(r => r.status === 'confirmed') .reduce((sum, r) => sum + (r.weight || 0), 0); return Math.round((confirmedWeight / totalWeight) * 100); } function getStatusStats(results) { if (!results || !Array.isArray(results)) { return { confirmed: 0, clarify: 0, notfound: 0, total: 0 }; } return { confirmed: results.filter(r => r.status === 'confirmed').length, clarify: results.filter(r => r.status === 'clarify').length, notfound: results.filter(r => r.status === 'notfound').length, total: results.length }; } function exportSummary(vacancyText, resumeText, criteria, matchResults, questions) { const coverage = calculateCoverage(matchResults?.results || []); const stats = getStatusStats(matchResults?.results || []); const summary = { exportedAt: new Date().toISOString(), vacancy: { title: 'Вакансия', criteriaCount: criteria?.length || 0 }, coverage: { percentage: coverage, confirmed: stats.confirmed, toClarify: stats.clarify, notFound: stats.notfound, total: stats.total }, results: matchResults?.results?.map(r => ({ requirement: r.label, type: r.required ? 'Обязательное' : 'Желательное', weight: r.weight, status: getStatusLabel(r.status), evidence: r.evidence?.[0] || 'Нет данных' })) || [], interviewQuestions: questions?.questions?.map(q => ({ topic: q.label, question: q.question, followUp: q.followUp })) || [] }; return summary; } function getStatusLabel(status) { const labels = { confirmed: 'Подтверждено', clarify: 'Уточнить', notfound: 'Не найдено' }; return labels[status] || status; } function downloadJSON(data, filename) { const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function downloadText(text, filename) { const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function formatExportText(summary) { let text = 'HR CANDIDATE MATCHER — СВОДКА\n'; text += '=' .repeat(50) + '\n\n'; text += `Дата экспорта: ${new Date(summary.exportedAt).toLocaleString('ru-RU')}\n\n`; text += 'ПОКРЫТИЕ ТРЕБОВАНИЙ\n'; text += '-'.repeat(30) + '\n'; text += `Общее покрытие: ${summary.coverage.percentage}%\n`; text += `Подтверждено: ${summary.coverage.confirmed}\n`; text += `Требует уточнения: ${summary.coverage.toClarify}\n`; text += `Не найдено: ${summary.coverage.notFound}\n`; text += `Всего критериев: ${summary.coverage.total}\n\n`; text += 'РЕЗУЛЬТАТЫ СОПОСТАВЛЕНИЯ\n'; text += '-'.repeat(30) + '\n'; summary.results.forEach((r, i) => { text += `${i + 1}. ${r.requirement}\n`; text += ` Тип: ${r.type}, Вес: ${r.weight}\n`; text += ` Статус: ${r.status}\n`; text += ` Доказательство: ${r.evidence}\n\n`; }); if (summary.interviewQuestions.length > 0) { text += 'ВОПРОСЫ ДЛЯ ИНТЕРВЬЮ\n'; text += '-'.repeat(30) + '\n'; summary.interviewQuestions.forEach((q, i) => { text += `${i + 1}. Тема: ${q.topic}\n`; text += ` Вопрос: ${q.question}\n`; if (q.followUp && q.followUp.length > 0) { text += ` Дополнительные: ${q.followUp.join('; ')}\n`; } text += '\n'; }); } text += '=' .repeat(50) + '\n'; text += 'Создано с помощью HR Candidate Matcher\n'; return text; } if (typeof module !== 'undefined' && module.exports) { module.exports = { extractRequirements, matchResumeToCriteria, updateCriterionStatus, addCustomCriterion, removeCriterion, updateCriterion, generateInterviewQuestions, calculateCoverage, getStatusStats, exportSummary, downloadJSON, downloadText, formatExportText }; }