Compare commits

..

2 Commits

3 changed files with 407 additions and 140 deletions

View File

@ -37,7 +37,9 @@
<span class="spacer"></span> <span class="spacer"></span>
<span class="kt-ai-chip" data-tone="gray" id="roleChip">Эксперт</span> <span class="kt-ai-chip" data-tone="gray" id="roleChip">Эксперт</span>
</header> </header>
<div id="view" class="kt-ai-work-column"></div> <div id="view" class="kt-ai-work-column">
<div id="viewInner"></div>
</div>
</main> </main>
</div> </div>

469
script.js
View File

@ -38,53 +38,28 @@ function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href=
function seed() { function seed() {
return { return {
view: "upload", view: "upload",
req: "Кандидат: А. Т. Нурмолдаев · SEO-1, управление сетевых сервисов · заявка от 14.09.2026", req: "Заявка ЭД-2026-118 · Управление сетевых сервисов · 14.09.2026",
candidate: "А. Т. Нурмолдаев", candidate: "А. Т. Нурмолдаев",
sources: [ sources: [
{ n: "Трудовой кодекс Республики Казахстан", s: "Законодательство", d: 207, u: 1 }, { n: "Трудовой кодекс Республики Казахстан", s: "Законодательство", d: 207, u: 1 },
{ n: "Закон Республики Казахстан «Об акционерных обществах»", s: "Законодательство", d: 148, u: 1 }, { n: "Закон Республики Казахстан «Об акционерных обществах»", s: "Законодательство", d: 148, u: 1 },
{ n: "Правила трудового распорядка", s: "Внутренние документы", d: 12, u: 1 }, { n: "Правила трудового распорядка", s: "Внутренние документы", d: 12, u: 1 },
{ n: "Правила поиска и подбора персонала", s: "Внутренние документы", d: 8, u: 1 }, { n: "Правила поиска и подбора персонала", s: "Внутренние документы", d: 8, u: 1 },
{ n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 } { n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 },
], { n: "Должностная инструкция SEO-1 (управление сетевыми сервисами)", s: "Внутренние документы", d: 14, u: 1 },
files: [ { n: "Положение о совещаниях и комиссиях фонда", s: "Внутренние документы", d: 22, u: 1 }
{ n: "Пояснительная записка на назначение — Нурмолдаев А.Т.docx", t: "Пояснительная записка", d: "14.09.2026, 10:24", s: "ready" },
{ n: "План отбора 2026-09 — управление сетевых сервисов.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:24", s: "ready" },
{ n: "Протокол собеседования №18-09.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:25", s: "ready" },
{ n: "Резюме кандидата — Нурмолдаев А.Т.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:28", s: "wait" },
{ n: "Анкета о соответствии требованиям.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:31", s: "ready" },
{ n: "Проверка требований (внутренняя форма) — скан.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:32", s: "ready" },
{ n: "Справка о стаже и опыте работы.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:33", s: "ready" },
{ n: "Диплом о высшем образовании (копия).pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:35", s: "ready" }
], ],
files: [],
progress: null,
dropActive: false,
running: false, running: false,
log: [ contra: false,
{ t: "10:42", b: "Пакет документов принят, 8 файлов" }, falseDone: false,
{ t: "10:42", b: "Извлечение текста: пояснительная записка, 12 разд." }, log: [],
{ t: "10:43", b: "Сверка с Правилами поиска и подбора персонала: раздел «Организация отбора», п. 4.2" }, violations: [],
{ t: "10:43", b: "Найдено отклонение: дата объявления о вакансии" }, checklist: [],
{ t: "10:44", b: "Сверка с Правилами трудового распорядка, гл. 5 «Назначение руководителей»" }, verdict: "",
{ t: "10:44", b: "Сверка с Законом «Об АО»: компетенции совета директоров, ст. 36" }, status: "draft",
{ t: "10:45", b: "Скрытое голосование: противоречие в протоколе и правилах" },
{ t: "10:45", b: "Проверка квалификации: стаж, образование, аттестация" },
{ t: "10:46", b: "Обнаружено 4 несоответствия, 1 противоречие" },
{ t: "10:46", b: "Журнал завершён — выводы переданы на верификацию эксперту" }
],
found: 4,
contra: true,
violations: [
{ d: "Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10", y: "process", r: "Правила поиска и подбора персонала, раздел 4.2", v: "conf" },
{ d: "В пояснительной записке 3 кандидата, в протоколе собеседования фиксированы 2", y: "process", r: "План отбора 2026-09, п. 3", v: "conf" },
{ d: "Срок согласования кандидатуры с материнской компанией — 2 дня при нормативе 5", y: "process", r: "Корпоративный стандарт по УХР, ред. 12", v: "conf" },
{ d: "У кандидата 6 лет опыта в телекоммуникациях при требуемых 7 для уровня SEO-1", y: "qual", r: "Должностная инструкция SEO-1, п. 2 «Требования к квалификации»", v: "conf" },
{ d: "Стаж в должности SEO-3: 3 года 4 месяца при нормативе не менее 4 лет", y: "qual", r: "Должностная инструкция SEO-2 и SEO-3, п. 2", v: "conf" },
{ d: "Аттестация по управленческой программе в материалах не подтверждена", y: "qual", r: "Корпоративный стандарт по УХР, гл. 8", v: "conf" },
{ d: "В протоколе — решение голосованием 4:1, в правилах назначений — единогласно", y: "contra", r: "Трудовой Кодекс РК, ст. 165, часть 3", v: "falsep" },
{ d: "Внутренняя форма проверки заполнена не полностью — графа «Аттестация»", y: "process", r: "Правила трудового распорядка, приложение 2", v: "falsep" }
],
verdict: "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат А. Т. Нурмолдаев) прошла проверку соответствия нормативной базе. По завершённой сверке зафиксированы следующие несоответствия:\n\n1. Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10 (Правила поиска и подбора персонала, раздел 4.2).\n2. В пояснительной записке указан круг из 3 кандидатов, в протоколе собеседования зафиксировано 2 (План отбора 2026-09, п. 3).\n3. Срок согласования кандидатуры с материнской компанией составил 2 дня при нормативе 5 (Корпоративный стандарт по УХР, ред. 12).\n4. Стаж в отрасли 6 лет при требуемых 7 для уровня SEO-1 (Должностная инструкция SEO-1, п. 2).\n5. Стаж в должности SEO-3 — 3 года 4 месяца при нормативе не менее 4 лет (Должностная инструкция SEO-2 и SEO-3, п. 2).\n6. Аттестация по управленческой программе в материалах не подтверждена (Корпоративный стандарт по УХР, гл. 8).\n\nПротиворечие в способе принятия решения (протокол — голосование 4:1, правила — единогласно) рассмотрено экспертом: позиция кандидата, поданная в установленном порядке, исключает применение требования единогласия; вывод системы отклонён как не относящийся к рассматриваемому пакету.\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.",
checklist: [1, 2, 3, 4, 5, 6],
status: "sent",
rejected: null, rejected: null,
stamp: null stamp: null
}; };
@ -102,6 +77,7 @@ function save() {
/* ---------- Прогресс процесса ---------- */ /* ---------- Прогресс процесса ---------- */
function allFilesReady() { function allFilesReady() {
if (!D.files.length) return false;
for (var i = 0; i < D.files.length; i++) if (D.files[i].s !== "ready") return false; for (var i = 0; i < D.files.length; i++) if (D.files[i].s !== "ready") return false;
return true; return true;
} }
@ -111,8 +87,8 @@ function doneCount() {
return n; return n;
} }
function stepNum() { function stepNum() {
if (D.view === "upload") return D.running || allFilesReady() ? 2 : 1; if (D.view === "upload") return allFilesReady() ? 2 : 1;
if (D.view === "results") return D.contra && !D.falseDone ? 2 : 3; if (D.view === "results") return 2;
if (D.view === "verdict") return 3; if (D.view === "verdict") return 3;
if (D.view === "approve") return D.status === "agree" || D.status === "decl" ? 5 : 4; if (D.view === "approve") return D.status === "agree" || D.status === "decl" ? 5 : 4;
return 1; return 1;
@ -196,7 +172,8 @@ function render() {
else if (D.view === "verdict") html = vVerdict(); else if (D.view === "verdict") html = vVerdict();
else if (D.view === "approve") html = vApprove(); else if (D.view === "approve") html = vApprove();
else html = vAbout(); else html = vAbout();
var view = $("view"); var view = $("viewInner");
if (!view) view = $("view");
if (!view) return; if (!view) return;
view.innerHTML = html; view.innerHTML = html;
$("pageTitle").textContent = TITLES[D.view] || ""; $("pageTitle").textContent = TITLES[D.view] || "";
@ -223,46 +200,97 @@ function kpiStrip(items) {
} }
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */ /* ---------- Экран 1. Загрузка и обзор материалов ---------- */
function vUpload() { function fmtSize(bytes) {
var readyCount = 0; if (bytes < 1024) return bytes + " B";
for (var i = 0; i < D.files.length; i++) if (D.files[i].s === "ready") readyCount++; if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
var all = readyCount === D.files.length; var mb = bytes / 1024 / 1024;
return (mb < 100 ? mb.toFixed(1) : Math.round(mb)) + " MB";
}
var head = headReady; function classifyFile(name) {
if (all && !D.running) { var n = name.toLowerCase();
head = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-ai-stack-block);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' + if (n.indexOf("пояснительная") >= 0 || n.indexOf("записк") >= 0 || n.indexOf("memo") >= 0) return "Пояснительная записка";
'<span style="flex:1;min-width:0">Все материалы получены — пакет готов к запуску анализа</span>' + var ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
'<button class="kt-ai-btn" data-variant="primary" onclick="S.analyze()">Запустить анализ ИИ</button></div>'; if (n.indexOf("договор") >= 0 || n.indexOf("contract") >= 0) return "Договор";
if (n.indexOf("акт") >= 0 || n.indexOf("certificate") >= 0) return "Сертификат / Акт";
if (n.indexOf("счёт") >= 0 || n.indexOf("счет") >= 0 || n.indexOf("invoice") >= 0) return "Счёт";
if (["doc", "docx"].indexOf(ext) >= 0) return "Документ Word";
if (["xls", "xlsx", "csv"].indexOf(ext) >= 0) return "Таблица";
if (ext === "pdf") return "PDF";
if (["jpg", "jpeg", "png"].indexOf(ext) >= 0) return "Изображение";
return "Документ";
}
function vUpload() {
var progress = D.progress;
var zoneBlock;
if (progress) {
zoneBlock = dropZoneHtml() +
'<div class="progress">' +
'<div class="pl">' +
'<span class="pl-ico">' + (progress.step < 3 ? "…" : icon("check")) + "</span>" +
'<div><strong>' + esc(STEPS_UPLOAD[progress.step] || "…") + "</strong>" +
'<span class="kt-ai-meta">' + esc(progress.file) + "</span></div>" +
'"</div>' +
'<div class="kp">' +
'<div class="bar"><i style="width:' + progress.pct + '%"></i></div>' +
'<div class="lp"><span>' + esc(progress.file) + "</span>" +
'<span class="kp-num">' + num(progress.pct) + " %" + "</span></div>" +
"</div></div>";
} else if (progress && progress.file) {
zoneBlock = dropZoneHtml() +
'<div class="progress done">' +
'<div class="pl">' +
'<span class="pl-ico">' + icon("check") + "</span>" +
'<div><strong>Загрузка завершена</strong>' +
'<span class="kt-ai-meta">' + esc(progress.file) + "</span></div>" +
"</div>" +
'<div class="kp"><div class="bar done"><i style="width:100%"></i></div>' +
'<div class="lp"><span>' + esc(progress.file) + "</span>" +
'<span class="kp-num">' + fmtSize(progress.size || 0) + "</span></div></div>" +
"</div>";
} else {
zoneBlock = dropZoneHtml();
} }
var rows = D.files.map(function (f, i) { var head = "";
var statusCell = pill(f.s); if (!progress && D.files.length) {
if (f.s === "wait") statusCell += ' <span class="kt-ai-chip" data-tone="orange" style="margin-left:var(--kt-ai-space-2)">ждёт загрузки</span>'; var all = D.files.every(function (f) { return f.s === "ready"; });
return "<tr" + (f.s === "wait" ? ' data-wait="true"' : "") + ">" + if (all && !D.running) {
'<td data-label="Файл">' + esc(f.n) + "</td>" + head = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-ai-stack-tight);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
'<td data-label="Тип"><span class="kt-ai-meta">' + esc(f.t) + "</span></td>" + "<span style='flex:1;min-width:0'><strong>Все материалы получены</strong> — пакет готов к запуску анализа</span>" +
'<td data-label="Загружен"><span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs)">' + esc(f.d) + "</span></td>" + '<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
'<td data-label="Статус">' + statusCell + "</td>" + } else if (!all && !D.running) {
'<td class="row-act"><button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="S.viewFile(' + i + ')">Смотреть</button></td>' + head = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-tight);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
"</tr>"; "<span style='flex:1;min-width:0'><strong>Пакет не собран</strong> — не все файлы получены от кандидата</span>" +
}).join(""); '<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.uploadFiles()">Загрузка файлов</button></div>';
}
}
var headReady = hasWaiting var table = D.files.length
? '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-block)">' + ? '<div class="kt-ai-table-wrap" style="margin-top:var(--kt-ai-stack-tight)">' +
"<strong>Пакет не собран</strong>" + '<table class="kt-ai-table"><thead><tr><th>Название</th><th>Размер</th><th>Дата</th><th>Статус</th><th></th></tr></thead>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Не все файлы получены от кандидата — анализ станет доступен после загрузки остальных материалов.</span></div>' "<tbody>" + D.files.map(function (f, i) {
return "<tr>" +
'<td data-label="Название">' + esc(f.n) + "</td>" +
'<td data-label="Размер"><span class="kt-ai-num">' + fmtSize(f.size || 0) + "</span></td>" +
'<td data-label="Дата"><span class="kt-ai-num">' + esc(f.d) + "</span></td>" +
'<td data-label="Статус">' + pill(f.s) + "</td>" +
'<td><button class="kt-ai-icon-button" title="Удалить" onclick="S.delFile(' + i + ')">' + icon("trash") + "</button></td>" +
"</tr>";
}).join("") + "</tbody></table></div>"
: ""; : "";
return '<section class="sec"><div class="sec-h">Процесс' + return '<section class="sec"><div class="sec-h">Процесс' +
'<span class="spacer"></span>' + '<span class="spacer"></span>' +
'<span class="kt-ai-meta">Заявка ' + num("ЭД-2026-118") + "</span></div>" + vStepper() + "</section>" + '<span class="kt-ai-meta">Заявка ' + num("ЭД-2026-118") + "</span>" +
head + '<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="S.resetDemo()" style="margin-left:var(--kt-ai-space-2)">Сбросить демо</button>' +
"</div>" + vStepper() + "</section>" +
(head ? '<div style="margin-bottom:var(--kt-ai-stack-tight)">' + head + "</div>" : "") +
'<section class="sec"><div class="sec-h">Материалы' + '<section class="sec"><div class="sec-h">Материалы' +
'<span class="spacer"></span>' + '<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(readyCount + " из " + D.files.length) + " готово</span></div>" + (D.files.length ? '<span class="kt-ai-meta">' + num(D.files.length) + " файл(а)</span>" : "") + "</div>" +
'<div class="kt-ai-table-wrap" data-framed="true">' + zoneBlock + table + "</section>" +
'<table class="kt-ai-table"><thead><tr><th>Файл</th><th>Тип</th><th>Загружен</th><th>Статус</th><th></th></tr></thead>' +
"<tbody>" + rows + "</tbody></table></div></section>" +
'<section class="sec"><div class="sec-h">Источники для сверки' + '<section class="sec"><div class="sec-h">Источники для сверки' +
'<span class="spacer"></span>' + '<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(D.sources.length) + " источника</span></div>" + '<span class="kt-ai-meta">' + num(D.sources.length) + " источника</span></div>" +
@ -276,8 +304,21 @@ function vUpload() {
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>"; '<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
}).join("") + "</div></section>" + }).join("") + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' + '<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
"<p>Строками идёт по заявке и пакету документов. Статусы файлов: «Ожидает проверки» — файл ещё получен не был, «Готово к анализу» — файл проверен и получен. " + "<p>Загрузите документы: перетащите файлы в зону загрузки или нажмите «выберите на устройстве». Система определит тип каждого файла, " +
"Когда собран весь пакет, вверху экрана появляется кнопка «Запустить анализ ИИ».</p></section>"; "отметит готовность и после полной загрузки пакета включит кнопку «Запустить анализ ИИ». Анализ будет идти по фактическим загруженным файлам — " +
"сверка с внутренними правилами и законодательством по 20 источникам.</p></section>";
}
function dropZoneHtml() {
var active = D.dropActive ? ' data-active="true"' : "";
return '<div class="drop" id="drop"' + active + '>' +
'<div class="drop-main">' +
'<span class="drop-ico">' + icon("upload") + "</span>" +
"<strong>Перетащите файлы сюда</strong>" +
"<span class='kt-ai-meta'>или <span class='drop-link' onclick='S.uploadFiles()'>выберите на устройстве</span></span>" +
"</div>" +
"<span class='drop-limit'>PDF, DOCX, XLSX, TXT — до 50 МБ</span>" +
"</div>";
} }
/* ---------- Экран 2. Результаты анализа ИИ ---------- */ /* ---------- Экран 2. Результаты анализа ИИ ---------- */
@ -485,42 +526,106 @@ function nowTime() {
} }
/* ---------- Действия ---------- */ /* ---------- Действия ---------- */
S.uploadFiles = function () { var STEPS_UPLOAD = [
var input = document.createElement("input"); "Проверка файла",
input.type = "file"; "Сканирование на вирусы",
input.multiple = true; "Извлечение текста"
input.accept = ".pdf,.docx,.xlsx,.csv,.txt,.doc"; ];
input.onchange = function () {
var files = input.files || []; /* Скрытый input для выбора файлов на устройстве */
if (files.length === 0) return; if (typeof window !== "undefined") {
var added = 0; var _fileInput = null;
for (var i = 0; i < files.length; i++) { function getFileInput() {
var f = files[i]; if (!_fileInput) {
var exists = false; _fileInput = document.createElement("input");
for (var j = 0; j < D.files.length; j++) { _fileInput.type = "file";
if (D.files[j].s === "wait" && D.files[j].n.toLowerCase().indexOf(f.name.toLowerCase().slice(0, 20)) >= 0) { _fileInput.multiple = true;
D.files[j].s = "ready"; _fileInput.accept = ".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt";
D.files[j].d = nowTime(); _fileInput.style.display = "none";
exists = true; _fileInput.addEventListener("change", function () {
added++; if (_fileInput.files && _fileInput.files.length) {
break; handleFiles(Array.prototype.slice.call(_fileInput.files));
} }
} _fileInput.value = "";
if (!exists) { });
var ext = (f.name.lastIndexOf(".") >= 0) ? f.name.slice(f.name.lastIndexOf(".")) : ""; document.body.appendChild(_fileInput);
var typeLabel = "Подтверждающий документ";
if (f.name.toLowerCase().indexOf("записк") >= 0) typeLabel = "Пояснительная записка";
D.files.push({ n: f.name, t: typeLabel, d: nowTime() + " (загрузка)", s: "ready" });
added++;
}
} }
return _fileInput;
}
window.__getFileInput = getFileInput;
}
function handleFiles(arr) {
if (!arr || !arr.length) return;
var existing = {};
for (var k = 0; k < D.files.length; k++) existing[D.files[k].n] = true;
var toAdd = [];
for (var i = 0; i < arr.length; i++) {
var f = arr[i];
if (existing[f.name]) continue;
toAdd.push({ f: f, t: classifyFile(f.name), size: f.size, d: nowTime() + " · " + nowTime() });
}
if (!toAdd.length) { toast("Файлы уже загружены", "warn"); return; }
var total = 0;
for (var j = 0; j < toAdd.length; j++) total += toAdd[j].size;
D.progress = { active: true, step: 0, pct: 0, file: toAdd[0].f.name, size: total, done: {} };
D.files = [];
save();
render();
simulateUpload(toAdd, 0, total);
}
function simulateUpload(list, idx, total) {
if (idx >= list.length) {
if (typeof _uploadTimer !== "undefined" && _uploadTimer) clearInterval(_uploadTimer);
D.progress = null;
save();
toast(D.files.length + " файл(ов) загружено в пакет", "ok");
return;
}
var item = list[idx];
var name = item.f.name;
var size = item.size;
var step = 0;
var pct = 0;
D.progress = { active: true, step: 0, pct: 0, file: name, size: total };
save();
render();
var iv = setInterval(function () {
pct += 8 + Math.floor(Math.random() * 10);
if (pct >= 100) pct = 100;
step = pct < 34 ? 0 : (pct < 67 ? 1 : 2);
D.progress = { active: true, step: step, pct: pct, file: name, size: total };
save(); save();
render(); render();
toast(added + " файл(ов) добавлено в пакет", "ok"); if (pct >= 100) {
var allNow = D.files.every(function (x) { return x.s === "ready"; }); clearInterval(iv);
if (allNow) toast("Пакет собран — анализ стал доступен", "ok"); setTimeout(function () {
}; D.files.push({ n: name, t: item.t, size: size, d: item.d, s: "ready" });
input.click(); save();
render();
setTimeout(function () {
simulateUpload(list, idx + 1, total);
}, 700);
}, 400);
}
}, 140);
}
S.resetDemo = function () {
if (confirm("Сбросить текущее состояние (загруженные файлы, результаты, черновик заключения)?")) {
D = seed();
save();
render();
toast("Демо сброшено — можно начать заново", "info");
}
};
S.delFile = function (i) {
D.files.splice(i, 1);
save();
render();
toast("Файл удалён из пакета", "info");
}; };
S.fileReady = function (i) { S.fileReady = function (i) {
@ -531,49 +636,90 @@ S.fileReady = function (i) {
S.go = function (id) { D.view = id; render(); }; S.go = function (id) { D.view = id; render(); };
function genViolations() {
var base = [];
for (var i = 0; i < D.files.length; i++) {
var f = D.files[i];
base.push(
{ d: f.n + ": текст извлечён и сверен с внутренними документами", y: "process", r: f.t + ", раздел 1", v: "checked" },
{ d: f.n + ": найдено 2 несоответствия с нормативной базой", y: "qual", r: f.t + ", ст. 36", v: "checked" }
);
}
base.push(
{ d: "Протокол: решение голосованием 4:1, правила — единогласно", y: "contra", r: "ТК РК, ст. 165", v: "falsep" }
);
if (base.length === 1) {
base.unshift({ d: "Пакет пуст — нет данных для полного анализа", y: "process", r: "—", v: "falsep" });
}
return base;
}
S.analyze = function () { S.analyze = function () {
if (!allFilesReady()) { toast("Дополните материалы — все файлы должны быть готовы", "warn"); return; } if (!D.files.length) { toast("Сначала загрузите файлы", "warn"); return; }
if (D.files.some(function (f) { return f.s !== "ready"; })) { toast("Дождитесь завершения загрузки всех файлов", "warn"); return; }
D.running = true; D.running = true;
D.contra = false; D.contra = false;
D.falseDone = false; D.falseDone = false;
D.checklist = [0, 1, 2, 3, 4, 5, 6, 7]; var vio = genViolations();
D.checklist = [];
for (var k = 0; k < vio.length; k++) {
vio[k].v = "checked";
D.checklist.push(k);
}
D.status = "draft"; D.status = "draft";
D.rejected = null; D.rejected = null;
D.stamp = null; D.stamp = null;
for (var i = 0; i < D.violations.length; i++) D.violations[i].v = D.violations[i].v === "conf" ? "checked" : "checked"; D.violations = vio;
D.violations[6].v = "falsep";
D.violations[7].v = "falsep";
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов."; D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
D.log = [ D.log = [];
{ t: nowTime(), b: "Пакет принят, 8 файлов" },
{ t: nowTime(), b: "Запущен анализ: " + D.sources.length + " источника для сверки" }
];
render();
setTimeout(function () { pushLogs(); }, 900);
setTimeout(function () { pushLogs(); }, 2100);
setTimeout(function () {
D.running = false;
D.violations[6].v = "falsep";
D.contra = true;
D.log.push({ t: nowTime(), b: "Обнаружено несоответствий: " + (D.violations.length - 2) + ", противоречий: 1" });
D.log.push({ t: nowTime(), b: "Журнал завершён — выводы переданы эксперту" });
S.go("results");
}, 3600);
};
function pushLogs() {
var extra = [
{ t: nowTime(), b: "Извлечение текста: пояснительная записка" },
{ t: nowTime(), b: "Сверка с Правилами поиска и подбора персонала, п. 4.2" },
{ t: nowTime(), b: "Найдено отклонение: дата объявления о вакансии" },
{ t: nowTime(), b: "Сверка с Законом «Об АО», ст. 36" },
{ t: nowTime(), b: "Проверка квалификации кандидата" }
];
if (D.log.length <= 2) { D.log.push(extra[0]); D.log.push(extra[1]); }
else if (D.log.length <= 4) { D.log.push(extra[2]); D.log.push(extra[3]); }
else { D.log.push(extra[4]); }
save(); save();
render(); render();
startLogStream();
setTimeout(function () {
if (!D.running) return;
for (var m = 0; m < D.violations.length; m++) if (D.violations[m].y === "contra") D.violations[m].v = "falsep";
D.running = false;
D.contra = true;
var proc = 0, qual = 0, contra = 0;
for (var n = 0; n < D.violations.length; n++) {
var y = D.violations[n].y;
if (y === "process") proc++;
else if (y === "qual") qual++;
else if (y === "contra") contra++;
}
D.log.push({ t: nowTime(), b: "Обнаружено несоответствий: " + (proc + qual) + ", противоречий: " + contra });
D.log.push({ t: nowTime(), b: "Журнал завершён — выводы переданы эксперту" });
D.view = "results";
save();
render();
toast("Анализ завершён — переход в результаты", "ok");
}, 6000);
};
function genLogPlan() {
var plan = [];
var files = D.files.concat([]);
for (var i = 0; i < files.length; i++) {
plan.push({ b: "Извлечение текста: " + files[i].n });
}
plan.push({ b: "Сверка с Правилами поиска и подбора персонала, п. 4.2" });
plan.push({ b: "Сверка с Законом «Об АО», ст. 36" });
plan.push({ b: "Проверка квалификации кандидата: стаж, образование, аттестация" });
return plan.map(function (line) { return { t: nowTime(), b: line.b }; });
}
function startLogStream() {
var plan = genLogPlan();
var delay = 300;
for (var i = 0; i < plan.length; i++) (function (line) {
setTimeout(function () {
if (D.running) {
D.log.push(line);
save();
render();
}
}, delay);
})(plan[i]);
} }
S.closeDrawer = closeDrawer; S.closeDrawer = closeDrawer;
@ -688,6 +834,51 @@ S.burger = function () {
else sb.setAttribute("data-open", "true"); else sb.setAttribute("data-open", "true");
}; };
/* ---------- Drag & drop ---------- */
function initDnD() {
document.addEventListener("dragover", function (e) {
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf("Files") >= 0) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
D.dropActive = true;
var d = $("drop");
if (d) d.setAttribute("data-active", "true");
}
});
document.addEventListener("dragleave", function (e) {
if (e.relatedTarget === null) {
D.dropActive = false;
var d = $("drop");
if (d) d.removeAttribute("data-active");
}
});
document.addEventListener("drop", function (e) {
e.preventDefault();
D.dropActive = false;
var d = $("drop");
if (d) d.removeAttribute("data-active");
var files = e.dataTransfer && e.dataTransfer.files;
if (files && files.length) handleFiles(Array.prototype.slice.call(files));
});
}
/* ---------- Инициализация ---------- */ /* ---------- Инициализация ---------- */
load(); load();
render(); render();
initDnD();
/* Обновление drop-зоны после dragover/drop без рендера всего экрана */
function refreshDrop() {
var d = $("drop");
if (d) {
if (D.dropActive) d.setAttribute("data-active", "true");
else d.removeAttribute("data-active");
}
}
var _dndTimer = null;
var _origRender = render;
render = function () {
_origRender();
if (_dndTimer) clearTimeout(_dndTimer);
_dndTimer = setTimeout(refreshDrop, 0);
};

View File

@ -138,3 +138,77 @@ tr[data-wait="true"] td:first-child { color: var(--kt-ai-status-warn-fg); font-w
.kt-ai-table-wrap .kt-ai-table tbody tr { display: table-row; } .kt-ai-table-wrap .kt-ai-table tbody tr { display: table-row; }
.kt-ai-table-wrap .kt-ai-table td { display: table-cell; } .kt-ai-table-wrap .kt-ai-table td { display: table-cell; }
} }
/* ====== Загрузка: зона + прогресс-бар ====== */
.drop {
border: 2px dashed var(--kt-ai-border);
border-radius: var(--kt-ai-radius-2xl);
background: var(--kt-ai-bg-elevated);
padding: var(--kt-ai-space-6);
display: flex;
align-items: center;
gap: var(--kt-ai-space-5);
cursor: pointer;
transition: border-color .15s, background .15s;
}
.drop:hover, .drop[data-active="true"] {
border-color: var(--kt-ai-primary);
background: var(--kt-ai-primary-subtle);
}
.drop-main { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; }
.drop-main strong { font-size: var(--kt-ai-text-md); font-weight: var(--kt-ai-weight-semibold); }
.drop-ico {
width: 40px; height: 40px; flex: 0 0 40px;
border-radius: var(--kt-ai-radius-lg);
background: var(--kt-ai-bg-active);
color: var(--kt-ai-fg-muted);
display: flex; align-items: center; justify-content: center;
}
.drop[data-active="true"] .drop-ico { background: var(--kt-ai-primary); color: var(--kt-ai-fg-on-primary); }
.drop-limit { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); }
.drop-link { color: var(--kt-ai-link); text-decoration: underline; cursor: pointer; }
/* Прогресс-бар загрузки */
.progress {
margin-top: var(--kt-ai-space-4);
padding: var(--kt-ai-space-6);
border: 1px solid var(--kt-ai-divider);
border-radius: var(--kt-ai-radius-xl);
background: var(--kt-ai-bg);
display: flex;
align-items: center;
gap: var(--kt-ai-space-6);
flex-wrap: wrap;
}
.progress .pl { display: flex; align-items: center; gap: var(--kt-ai-space-4); min-width: 280px; }
.progress .pl-ico {
width: 34px; height: 34px; flex: 0 0 34px;
border-radius: var(--kt-ai-radius-md);
background: var(--kt-ai-status-info-bg);
color: var(--kt-ai-status-info-fg);
display: flex; align-items: center; justify-content: center;
font-size: var(--kt-ai-text-sm);
}
.progress.done .pl-ico { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
.progress .pl strong { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); display: block; }
.progress .pl .kt-ai-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); display: block; margin-top: 1px; }
.progress .kp { flex: 1; min-width: 240px; }
.progress .bar {
height: 6px;
border-radius: var(--kt-ai-radius-full);
background: var(--kt-ai-bg-active);
overflow: hidden;
}
.progress .bar i {
display: block; height: 100%;
background: var(--kt-ai-primary);
border-radius: var(--kt-ai-radius-full);
transition: width .3s ease;
}
.progress.done .bar i { background: var(--kt-ai-status-ok-fg); }
.progress .lp {
display: flex; justify-content: space-between; gap: var(--kt-ai-space-3);
margin-top: var(--kt-ai-space-2);
font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted);
}
.progress .lp .kp-num { font-variant-numeric: tabular-nums; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }