ty-produktovyy-dizayner-i-se/js/aiAdapter.js

218 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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);
}
};
})();