/* HR Candidate Matcher — AI Adapter (Demo Mode) */ /* global window */ (function () { 'use strict'; var APP = window.HRMatcher = window.HRMatcher || {}; var SKILLS = APP.SKILL_DICTIONARY; var TEMPLATES = APP.INTERVIEW_TEMPLATES; var EXPERIENCE_PATTERN = /(?:опыт(?:\s+работы)?[\s:]*(?:от\s+)?(\d+)\s*(?:год|лет|года))/i; var EXPERIENCE_PATTERN2 = /(?:от\s+(\d+)\s*(?:год|лет|года)\s*опыта)/i; var EXPERIENCE_PATTERN3 = /(?:стаж\s+(?:от\s+)?(\d+)\s*(?:год|лет|года))/i; var EXPERIENCE_PATTERN4 = /(\d+)\s*(?:год|лет|года)\s*(?:опыта|стажа)/i; var REQUIREMENT_PATTERNS = [ /(?:знания?\s+|владение\s+|умени[ея]\s+(?:работать\s+)?(?:с\s+)?|опыт\s+(?:работы\s+)?(?:с\s+)?(?:в\s+)?)((?:[A-Za-zА-Яа-яЁё0-9.]+\s*){1,4})/gi, /(?:требуется|необходимо|обязательно|нужно)[\s:]+((?:[A-Za-zА-Яа-яЁё0-9.]+\s*){1,4})/gi ]; var NEGATION_PATTERN = /(?:без\s+|нет\s+|не\s+(?:имеет?\s+)?|отсутств(?:ие|ует|уют)\s+|не\s+требует(?:ся)?)\s*/i; function normalizeText(text) { return (text || '').toLowerCase().replace(/\s+/g, ' ').trim(); } function detectSkillType(label) { var lower = normalizeText(label); if (/\b(опыт|стаж|лет\s+работ)\b/.test(lower)) return 'experience'; if (/\b(analytics|analyt|метрик|аналитик|отчёт|отчет|тестирован|a\/b)\b/.test(lower)) return 'tool'; if (/\b(ads|реклам|campaign|кампани|target|таргет)\b/.test(lower)) return 'tool'; return 'skill'; } APP.aiAdapter = { isDemoMode: true, extractRequirements: function (vacancyText) { var text = vacancyText || ''; var criteria = []; var seen = {}; Object.keys(SKILLS).forEach(function (key) { var skill = SKILLS[key]; var allTerms = [key].concat(skill.synonyms); var found = false; for (var i = 0; i < allTerms.length; i++) { var term = allTerms[i]; var regex = new RegExp('\\b' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i'); if (regex.test(text)) { found = true; break; } } if (found && !seen[key]) { seen[key] = true; var isDesired = /(?:плюс|желательно|будет\s+плюсом|приветствуется|желател)/i.test( text.substring(Math.max(0, text.toLowerCase().indexOf(key) - 50), text.toLowerCase().indexOf(key) + key.length + 50) ); criteria.push(APP.createCriterion( skill.label, !isDesired, isDesired ? 2 : 3, skill.synonyms.slice(0, 3) )); } }); var expMatch = text.match(EXPERIENCE_PATTERN) || text.match(EXPERIENCE_PATTERN2) || text.match(EXPERIENCE_PATTERN3) || text.match(EXPERIENCE_PATTERN4); if (expMatch) { var years = parseInt(expMatch[1], 10); if (years > 0 && years <= 20 && !seen['experience_years']) { seen['experience_years'] = true; criteria.push(APP.createCriterion( 'Опыт работы от ' + years + ' лет', true, 4, ['стаж от ' + years, years + ' лет опыта', 'опыт ' + years + ' года'] )); } } var lines = text.split('\n'); for (var li = 0; li < lines.length; li++) { var line = lines[li].trim(); if (/^[-–—•]\s+/.test(line)) { var content = line.replace(/^[-–—•]\s+/, '').trim(); if (content.length > 5 && content.length < 100) { var lowerContent = normalizeText(content); var alreadyMatched = false; Object.keys(SKILLS).forEach(function (key) { var allTerms = [key].concat(SKILLS[key].synonyms); for (var ti = 0; ti < allTerms.length; ti++) { if (lowerContent.indexOf(normalizeText(allTerms[ti])) !== -1) { alreadyMatched = true; break; } } }); if (!alreadyMatched && /(?:знани|владе|уме|опыт|способн|знаком)/i.test(content)) { var cleanLabel = content.replace(/^(?:знание|владение|умение|опыт|знакомство)\s+(?:с\s+)?/i, '').trim(); if (cleanLabel.length >= 3 && !seen[normalizeText(cleanLabel)]) { seen[normalizeText(cleanLabel)] = true; criteria.push(APP.createCriterion(cleanLabel, true, 3, [])); } } } } } return criteria; }, matchCriterion: function (criterion, resumeText) { var label = criterion.label || ''; var synonyms = criterion.synonyms || []; var allTerms = [label].concat(synonyms).map(function (t) { return t.trim(); }).filter(Boolean); var resumeLower = normalizeText(resumeText); var evidence = []; for (var i = 0; i < allTerms.length; i++) { var term = allTerms[i]; var termLower = normalizeText(term); var idx = resumeLower.indexOf(termLower); if (idx !== -1) { var contextStart = Math.max(0, idx - 40); var contextEnd = Math.min(resumeText.length, idx + term.length + 40); var fragment = resumeText.substring(contextStart, contextEnd).trim(); if (contextStart > 0) fragment = '…' + fragment; if (contextEnd < resumeText.length) fragment = fragment + '…'; var beforeText = resumeText.substring(Math.max(0, idx - 30), idx); if (NEGATION_PATTERN.test(beforeText)) { continue; } evidence.push(APP.createEvidence(fragment, idx)); } } if (evidence.length > 0) { return APP.createMatchResult(criterion.id, APP.STATUS.CONFIRMED, evidence, ''); } var resumeSentences = resumeText.split(/[.!?\n]+/).map(function (s) { return s.trim(); }).filter(Boolean); var unclear = false; for (var si = 0; si < resumeSentences.length; si++) { var sentence = normalizeText(resumeSentences[si]); for (var ti = 0; ti < allTerms.length; ti++) { var tLower = normalizeText(allTerms[ti]); var words = tLower.split(/\s+/); var matchedWords = 0; for (var wi = 0; wi < words.length; wi++) { if (words[wi].length >= 3 && sentence.indexOf(words[wi]) !== -1) { matchedWords++; } } if (words.length > 1 && matchedWords >= words.length * 0.5) { var sentIdx = resumeText.toLowerCase().indexOf(resumeSentences[si].toLowerCase()); if (sentIdx !== -1) { evidence.push(APP.createEvidence(resumeSentences[si], sentIdx)); } unclear = true; break; } } if (unclear) break; } if (unclear) { return APP.createMatchResult(criterion.id, APP.STATUS.UNCLEAR, evidence, 'Требуется уточнение'); } return APP.createMatchResult(criterion.id, APP.STATUS.NOT_FOUND, [], ''); }, generateQuestions: function (criterion, matchResult) { var type = detectSkillType(criterion.label); var templates = TEMPLATES[type] || TEMPLATES.skill; var questions = []; var label = criterion.label; for (var i = 0; i < templates.length; i++) { questions.push(templates[i].replace(/\{skill\}/g, label)); } if (matchResult && matchResult.status === APP.STATUS.UNCLEAR && matchResult.evidence.length > 0) { questions.unshift('В резюме упоминается «' + label + '», но контекст неясен. Можете уточнить ваш уровень и примеры использования?'); } return questions; }, detectSkillType: detectSkillType, computeCoverage: function (criteria, matchResults) { if (!criteria || criteria.length === 0) return 0; var totalWeight = 0; var confirmedWeight = 0; var resultMap = {}; (matchResults || []).forEach(function (r) { resultMap[r.criterionId] = r; }); for (var i = 0; i < criteria.length; i++) { var c = criteria[i]; totalWeight += c.weight; var result = resultMap[c.id]; if (result && result.status === APP.STATUS.CONFIRMED) { confirmedWeight += c.weight; } } if (totalWeight === 0) return 0; return Math.round((confirmedWeight / totalWeight) * 100); } }; })();