ekspertiza-dokumentov-dlya-s/script.js

885 lines
46 KiB
JavaScript
Raw Permalink 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.

"use strict";
/* Экспертиза документов для советов — статичный прототип, состояние в localStorage. */
var LS_KEY = "edds-v1";
function $(id) { return document.getElementById(id); }
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function escJs(s) { return esc(String(s == null ? "" : s).replace(/\\/g, "\\\\").replace(/'/g, "\\'")); }
var ST = {
wait: { label: "Ожидает проверки", tone: "warn" },
ready: { label: "Готово к анализу", tone: "ok" },
process: { label: "Процесс отбора", tone: "info" },
qual: { label: "Квалификация", tone: "info" },
contra: { label: "Противоречие", tone: "risk" },
checked: { label: "На проверке", tone: "info" },
conf: { label: "Подтверждено", tone: "ok" },
falsep: { label: "Ложное срабатывание", tone: "muted" },
draft: { label: "Черновик", tone: "warn" },
sent: { label: "Готово к согласованию", tone: "info" },
agree: { label: "Согласовано", tone: "ok" },
decl: { label: "Отклонено", tone: "risk" },
pending: { label: "Ожидает решения", tone: "warn" }
};
function pill(key, label) {
var st = ST[key] || { label: label || key, tone: "muted" };
return '<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span>";
}
function num(s) { return '<span class="kt-ai-num">' + s + "</span>"; }
function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href="#' + name + '"></use></svg>'; }
/* ---------- Демо-данные ---------- */
function seed() {
return {
view: "upload",
req: "Заявка ЭД-2026-118 · Управление сетевых сервисов · 14.09.2026",
candidate: "А. Т. Нурмолдаев",
sources: [
{ n: "Трудовой кодекс Республики Казахстан", s: "Законодательство", d: 207, u: 1 },
{ n: "Закон Республики Казахстан «Об акционерных обществах»", s: "Законодательство", d: 148, u: 1 },
{ n: "Правила трудового распорядка", s: "Внутренние документы", d: 12, u: 1 },
{ n: "Правила поиска и подбора персонала", s: "Внутренние документы", d: 8, u: 1 },
{ n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 },
{ n: "Должностная инструкция SEO-1 (управление сетевыми сервисами)", s: "Внутренние документы", d: 14, u: 1 },
{ n: "Положение о совещаниях и комиссиях фонда", s: "Внутренние документы", d: 22, u: 1 }
],
files: [],
progress: null,
dropActive: false,
running: false,
contra: false,
falseDone: false,
log: [],
violations: [],
checklist: [],
verdict: "",
status: "draft",
rejected: null,
stamp: null
};
}
var D = null;
function load() {
try { var raw = localStorage.getItem(LS_KEY); D = raw ? JSON.parse(raw) : seed(); }
catch (e) { D = seed(); }
if (!D.files || !D.violations) D = seed();
}
function save() {
try { localStorage.setItem(LS_KEY, JSON.stringify(D)); } catch (e) {}
}
/* ---------- Прогресс процесса ---------- */
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;
return true;
}
function doneCount() {
var n = 0, i;
for (i = 0; i < D.violations.length; i++) if (D.violations[i].v === "conf") n++;
return n;
}
function stepNum() {
if (D.view === "upload") return allFilesReady() ? 2 : 1;
if (D.view === "results") return 2;
if (D.view === "verdict") return 3;
if (D.view === "approve") return D.status === "agree" || D.status === "decl" ? 5 : 4;
return 1;
}
var STEPS = ["Загрузка", "Анализ", "Заключение", "Согласование"];
/* ---------- UI: тосты, модалки ---------- */
var S = {};
function toast(msg, tone) {
var box = $("toasts");
if (!box) return;
var el = document.createElement("div");
el.className = "kt-ai-toast";
if (tone) el.dataset.tone = tone;
el.textContent = msg;
box.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(6px)";
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
}, 2600);
}
function openModal(html) { var m = $("modal"); m.innerHTML = html; m.style.display = "flex"; }
function closeModal() { var m = $("modal"); m.style.display = "none"; m.innerHTML = ""; }
S.closeModal = closeModal;
function openDrawer(html) { var d = $("drawer"); d.innerHTML = html; d.style.display = "block"; }
function closeDrawer() { var d = $("drawer"); d.style.display = "none"; d.innerHTML = ""; }
/* ---------- Навигация и рендер ---------- */
var NAV = [
{ id: "upload", label: "Загрузка и обзор", icon: "upload" },
{ id: "results", label: "Результаты анализа", icon: "sparkles" },
{ id: "verdict", label: "Формирование заключения", icon: "pencil" },
{ id: "approve", label: "Согласование", icon: "check-circle" },
{ id: "about", label: "О решении", icon: "info", group: "Решение" }
];
var TITLES = {
upload: "Загрузка и обзор материалов",
results: "Результаты анализа ИИ",
verdict: "Формирование заключения",
approve: "Согласование руководителем",
about: "О решении"
};
var SUBS = {
upload: "Пакет документов · первичное ознакомление",
results: "Сверка процесса · проверка квалификации",
verdict: "Верификация выводов · итоговый документ",
approve: "Итоговое заключение · решение руководителя",
about: "Паспорт ИИ-решения"
};
function renderNav() {
var nav = $("nav");
if (!nav) return;
var group = null, html = "";
for (var i = 0; i < NAV.length; i++) {
var n = NAV[i];
if (n.group && n.group !== group) {
html += '<div class="kt-ai-nav-group">' + esc(n.group) + "</div>";
group = n.group;
}
if (!n.group) group = null;
var active = D.view === n.id ? ' data-active="true"' : "";
html += '<div class="kt-ai-nav-item" role="link" tabindex="0" style="cursor:pointer"' + active +
'><svg class="kt-icon" aria-hidden="true"><use href="#' + n.icon + '"></use></svg>' +
'<span class="nav-label">' + esc(n.label) + "</span></div>";
(function (id) {
nav.lastChild.onclick = function () { S.go(id); };
})(n.id);
}
var sb = $("sbReq");
if (sb) sb.innerHTML = '<div class="sb-req-in"><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-2)">Текущая заявка</div><span class="kt-ai-meta">' + esc(D.req) + "</span></div>";
}
function render() {
var html;
if (D.view === "upload") html = vUpload();
else if (D.view === "results") html = vResults();
else if (D.view === "verdict") html = vVerdict();
else if (D.view === "approve") html = vApprove();
else html = vAbout();
var view = $("viewInner");
if (!view) view = $("view");
if (!view) return;
view.innerHTML = html;
$("pageTitle").textContent = TITLES[D.view] || "";
$("pageSub").textContent = SUBS[D.view] || "";
$("roleChip").textContent = (D.view === "approve") ? "Руководитель" : "Эксперт";
renderNav();
save();
}
function vStepper() {
var cur = stepNum();
var html = '<div class="kt-ai-stepper">';
for (var i = 0; i < STEPS.length; i++) {
var st = i + 1 < cur ? "done" : (i + 1 === cur ? "current" : "");
var n = i + 1 < cur ? "✓" : String(i + 1);
html += '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + '><span class="n">' + n + "</span>" + esc(STEPS[i]) + "</div>";
if (i < STEPS.length - 1) html += '<span class="kt-ai-step-sep"></span>';
}
return html + "</div>";
}
function kpiStrip(items) {
return '<div class="kt-ai-kpi-strip">' + items.map(function (k) {
return '<div class="kt-ai-kpi"><span class="label">' + esc(k.l) + '</span><span class="value">' + k.v + "</span>" + (k.h ? '<span class="hint">' + esc(k.h) + "</span>" : "") + "</div>";
}).join("") + "</div>";
}
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */
function fmtSize(bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
var mb = bytes / 1024 / 1024;
return (mb < 100 ? mb.toFixed(1) : Math.round(mb)) + " MB";
}
function classifyFile(name) {
var n = name.toLowerCase();
if (n.indexOf("пояснительная") >= 0 || n.indexOf("записк") >= 0 || n.indexOf("memo") >= 0) return "Пояснительная записка";
var ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
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 head = "";
if (!progress && D.files.length) {
var all = D.files.every(function (f) { return f.s === "ready"; });
if (all && !D.running) {
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)">' +
"<span style='flex:1;min-width:0'><strong>Все материалы получены</strong> — пакет готов к запуску анализа</span>" +
'<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
} else if (!all && !D.running) {
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)">' +
"<span style='flex:1;min-width:0'><strong>Пакет не собран</strong> — не все файлы получены от кандидата</span>" +
'<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.uploadFiles()">Загрузка файлов</button></div>';
}
}
var table = D.files.length
? '<div class="kt-ai-table-wrap" style="margin-top:var(--kt-ai-stack-tight)">' +
'<table class="kt-ai-table"><thead><tr><th>Название</th><th>Размер</th><th>Дата</th><th>Статус</th><th></th></tr></thead>' +
"<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">Процесс' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">Заявка ' + num("ЭД-2026-118") + "</span>" +
'<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">Материалы' +
'<span class="spacer"></span>' +
(D.files.length ? '<span class="kt-ai-meta">' + num(D.files.length) + " файл(а)</span>" : "") + "</div>" +
zoneBlock + table + "</section>" +
'<section class="sec"><div class="sec-h">Источники для сверки' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(D.sources.length) + " источника</span></div>" +
'<div class="tiles">' + D.sources.map(function (s) {
var pct = s.u ? 100 : 82;
return '<div class="kt-ai-card src-card"><div class="src-name">' + esc(s.n) + "</div>" +
'<div class="src-meta"><span>Класс<span>' + esc(s.s) + "</span></span>" +
'<span>Разделов<span class="kt-ai-num">' + num(s.d) + "</span></span>" +
'<span>Обновлён<span class="kt-ai-num">01.09.2026</span></span></div>' +
'<div class="src-progress"><i style="width:' + pct + '%"></i></div>' +
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
}).join("") + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
"<p>Загрузите документы: перетащите файлы в зону загрузки или нажмите «выберите на устройстве». Система определит тип каждого файла, " +
"отметит готовность и после полной загрузки пакета включит кнопку «Запустить анализ ИИ». Анализ будет идти по фактическим загруженным файлам — " +
"сверка с внутренними правилами и законодательством по 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. Результаты анализа ИИ ---------- */
function falseCount() {
var n = 0;
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "falsep") n++;
return n;
}
function vResults() {
var confN = doneCount(), falseN = falseCount();
var procN = 0, qualN = 0, contraN = 0;
for (var i = 0; i < D.violations.length; i++) {
var y = D.violations[i].y;
if (y === "process") procN++;
else if (y === "qual") qualN++;
else if (y === "contra") contraN++;
}
var kpis = kpiStrip([
{ l: "Несогласованностей", v: num(confN + falseN) },
{ l: "Процесс отбора", v: num(procN) },
{ l: "Квалификация", v: num(qualN) },
{ l: "Противоречий", v: num(contraN) }
]);
var banner = "";
if (D.running) return runScreen();
if (D.contra) {
var contraRow = null;
for (var ci = 0; ci < D.violations.length; ci++) if (D.violations[ci].y === "contra") contraRow = ci;
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
'<strong>Найдено противоречие между документами</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Протокол — голосование 4:1, Правила назначений — единогласно. ' +
(contraRow != null ? "Проверьте вывод №" + num(contraRow + 1) + " вручную." : "Противоречие разобрано.") + "</span></div>";
}
var typeTone = { process: "blue", qual: "purple", contra: "red" };
var rows = D.violations.map(function (v, i) {
return "<tr>" +
'<td data-label="Ссылка на пункт"><span class="kt-ai-meta">' + esc(v.r) + "</span></td>" +
'<td data-label="Нарушение">' + esc(v.d) + "</td>" +
'<td data-label="Тип"><span class="kt-ai-chip" data-tone="' + (typeTone[v.y] || "gray") + '">' + esc(ST[v.y].label) + "</span></td>" +
'<td data-label="Статус">' + pill(v.v) + "</td>" +
'<td><div class="row-act"><button class="kt-ai-icon-button" title="Открыть пункт нормативного акта" onclick="S.openRef(' + i + ')">' + icon("link") + "</button>" +
'<button class="kt-ai-btn" data-size="sm" onclick="S.refute(' + i + ')">' + (v.v === "falsep" ? "Вернуть" : "Ложное") + "</button></div></td>" +
"</tr>";
}).join("");
var logRows = D.log.map(function (l) {
return '<div class="logline"><span class="lt">' + esc(l.t) + '</span><span class="lb">' + esc(l.b) + "</span></div>";
}).join("");
var nextOk = true;
for (var k = 0; k < D.violations.length; k++) if (D.violations[k].v === "checked") nextOk = false;
var next = "";
if (nextOk) {
next = '<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'upload\')">Назад</button>' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'verdict\')">Перейти к подготовке заключения</button></div>';
}
return kpis + banner +
'<section class="sec"><div class="sec-h">Несогласованности</div>' +
'<div class="kt-ai-table-wrap" data-framed="true">' +
'<table class="kt-ai-table"><thead><tr><th>Ссылка на пункт</th><th>Нарушение</th><th>Тип</th><th>Статус</th><th>Действие</th></tr></thead>' +
"<tbody>" + rows + "</tbody></table></div>" + next + "</section>" +
'<section class="sec"><div class="sec-h">Журнал действия ИИ' +
'<span class="spacer"></span><span class="kt-ai-meta">10:42 — 10:46</span></div>' +
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + logRows + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что показывает анализ</h3>' +
"<p>Система сверяет процесс отбора с внутренними документами и законодательством, затем проверяет квалификацию кандидата по должностной инструкции. " +
"Каждый вывод ссылается на пункт нормативного акта — эксперт проверяет ссылки и при необходимости отмечает ложные срабатывания.</p></section>";
}
function runScreen() {
var steps = [
"Извлечение текста из пакета документов",
"Сверка с законодательством и внутренними документами",
"Проверка квалификации кандидата",
"Формирование вывода"
];
var rows = steps.map(function (s, i) {
return '<div class="logline"><span class="lt">' + esc(stepsTime[i] || "10:4" + i) + '</span><span class="lb">' + esc(s) + "</span></div>";
}).join("");
return '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-block);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
'<span style="flex:1;min-width:0">Идёт анализ пакета — ожидайте</span></div>' +
'<section class="sec"><div class="sec-h">Ход работы' +
'<span class="spacer"></span><span class="kt-ai-status-pill" data-status="info">Идёт анализ</span></div>' +
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + rows + "</div>" +
"<p class='kt-ai-meta' style='margin-top:var(--kt-ai-space-3)'>Анализ занимает обычно 3–5 минут. Можно закрыть вкладку — прогресс сохранится.</p></section>";
}
var stepsTime = ["10:46", "10:46", "10:46", "10:46"];
/* ---------- Экран 3. Формирование заключения ---------- */
function vVerdict() {
var checks = D.checklist.map(function (idx) {
var v = D.violations[idx];
if (!v) return "";
return '<label class="chk-row" for="ck' + idx + '">' +
'<input type="checkbox" id="ck' + idx + '"' + (D.checklist.indexOf(idx) >= 0 ? " checked" : "") + ' onchange="S.ck(' + idx + ')">' +
'<span class="chk-main"><span class="chk-desc">' + esc(v.d) + "</span>" +
'<span class="chk-ref kt-ai-meta">' + icon("link") + esc(v.r) + "</span></span></label>";
}).join("");
var sent = D.status === "sent";
var status = sent ? pill("sent") : pill("draft");
var docText = $("docText") && $("docText").textContent ? $("docText").textContent : D.verdict;
if (docText !== D.verdict) D.verdict = docText;
return vStepper() +
'<section class="sec"><div class="sec-h">Итоговое заключение' +
'<span class="spacer"></span>' + status + "</div>" +
'<div class="doc-paper" id="docPaper">' +
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ' + esc("ЭД-2026-118") + " · SEO-1, управление сетевых сервисов</span></div>" +
'<div class="doc-text" id="docText" contenteditable="true" spellcheck="false">' + esc(docText) + "</div></div>" +
'<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'results\')">Назад к анализу</button>' +
(sent
? '<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'approve\')">К согласованию</button>'
: '<button class="kt-ai-btn" data-variant="primary" onclick="S.sendForApproval()">Отправить на согласование руководителю</button>') +
"</div></section>" +
'<section class="sec"><div class="sec-h">Чек-лист подтвержденных нарушений' +
'<span class="spacer"></span><span class="kt-ai-meta">' + num(D.checklist.length + " из " + D.violations.length) + " включены в заключение</span></div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
'<section class="sec"><div class="sec-h" style="margin-bottom:var(--kt-ai-space-4)"></div>' +
'<button class="kt-ai-btn" data-variant="danger" onclick="S.manualMode()">Вернуть в ручной режим</button>' +
'<span class="kt-ai-meta" style="margin-left:var(--kt-ai-space-4)">Без системы — вручную по действующим правилам</span></section>';
}
/* ---------- Экран 4. Согласование руководителем ---------- */
function vApprove() {
var st = D.status;
var banner = "";
var btns = "";
var kpis = kpiStrip([
{ l: "Заявка", v: num("ЭД-2026-118") },
{ l: "Подтверждено", v: num(D.checklist.length) },
{ l: "Исключено", v: num(D.violations.length - D.checklist.length) },
{ l: "Статус", v: ST[st] ? esc(ST[st].label) : "—" }
]);
if (st === "agree") {
banner = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-stack-block)"><strong>Материал согласован</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Согласовано ' + num(D.stamp || "15.09.2026, 09:12") + " · руководитель</span></div>";
} else if (st === "decl") {
banner = '<div class="kt-ai-banner" data-tone="risk" style="margin-bottom:var(--kt-ai-stack-block)"><strong>Материал отклонён</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">' + num(D.stamp || "15.09.2026") + ' · ' + esc(D.rejected || "Без комментария") + "</span></div>";
} else {
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
'<strong>Ожидает решения руководителя</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Проверены материалы на назначение руководителя SEO-1 · ' + num("14.09.2026") + "</span></div>";
btns = '<div class="row-btns" style="margin-top:var(--kt-ai-stack-block)">' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.approve()">Согласовать документы</button>' +
'<button class="kt-ai-btn" data-variant="danger" onclick="S.declare()">Отклонить с комментарием</button></div>';
}
var checks = D.checklist.map(function (idx) {
var v = D.violations[idx];
if (!v) return "";
return '<div class="logline"><span class="lt">✓</span><span class="lb">' + esc(v.d) + ' <span class="kt-ai-meta">' + esc(v.r) + "</span></span></div>";
}).join("");
return kpis + banner +
'<section class="sec"><div class="sec-h">Итоговое заключение</div>' +
'<div class="doc-paper" style="pointer-events:none">' +
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ЭД-2026-118 · SEO-1, управление сетевых сервисов</span></div>' +
'<div class="doc-text" style="border-color:transparent;background:transparent;padding:0">' + esc(D.verdict) + "</div></div></section>" +
'<section class="sec"><div class="sec-h">Подтвержденные нарушения</div>' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
btns;
}
/* ---------- Экран 5. О решении ---------- */
function vAbout() {
return '<section class="sec"><div class="sec-h" style="font-size:var(--kt-ai-text-xl)">О решении</div></section>' +
'<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-stack-block)"><div class="mini" style="padding:0"><div class="kv">' +
'<span class="k">Наименование</span><span class="v">Экспертиза документов для советов</span></div><div class="kv">' +
'<span class="k">Исполнитель</span><span class="v">Владелец процесса — сборка на платформе Vibe AI</span></div><div class="kv">' +
'<span class="k">Пользователь</span><span class="v">Эксперт, руководитель</span></div><div class="kv">' +
'<span class="k">Дата внедрения</span><span class="v">—</span></div></div></div>' +
'<div class="pass-grid" style="margin-bottom:var(--kt-ai-stack-block)">' +
passCard("Предпосылки", "<ul><li>Необходимость читать большое количество внутренних документов и законодательных актов</li><li>Необходимость сравнивать документы на соответствие нормативным требованиям</li></ul>") +
passCard("Входные данные и источники", "<ul><li>Трудовой кодекс Республики Казахстан</li><li>Закон «Об акционерных обществах»</li><li>Правила трудового распорядка</li><li>Правила поиска и подбора персонала</li><li>Корпоративный стандарт по управлению человеческими ресурсами фонда</li></ul>") +
passCard("Процессы для внедрения ИИ", "<ul><li>Сверка пояснительной записки с фактическим процессом отбора</li><li>Проверка соответствия кандидата квалификационным требованиям</li><li>Поиск несоответствий и противоречий в документах</li></ul>") +
passCard("Результат", "<ul><li>Согласование документов со списком нарушений и ссылками на пункты законов и внутренних документов</li><li>Заключение со списком нарушений и ссылками на пункты законов и внутренних документов</li></ul>") +
passCard("Ожидаемый эффект", "<ul><li>Сокращение времени рассмотрения материалов</li><li>Повышение качества проверки за счёт снижения риска человеческих ошибок</li></ul>") +
passCard("Экономический эффект", "<div class='kv'><span class='k'>Объём</span><span class='v'>20 экспертиз в месяц во всех экспертах</span></div><div class='kv'><span class='k'>Высвобождение</span><span class='v'>50 часов в месяц (оценка Төре)</span></div>") +
passCard("Контроль и риски", "<ul><li>Возможность ошибки ИИ при противоречиях в разных внутренних документах</li><li>Необходимость проверки выводов экспертом</li></ul>") +
passCard("Меры по снижению", "<ul><li>Проверка результата человеком перед использованием</li><li>Журнал действий ИИ и возможность вернуться в ручной режим</li></ul>") +
passCard("Формат реализации", "<ul><li>ИИ-агент на платформе Alem; при необходимости — простой веб-экран на Vibe AI</li><li>Экраны макета: Загрузка и обзор; Результаты анализа; Формирование заключения; Согласование руководителем</li></ul>") +
"</div>" +
'<section class="kt-ai-conclusion"><h3>Путь сотрудника</h3>' +
"<p>Эксперт загружает документы на экране «Загрузка и обзор материалов», переходит на «Результаты анализа ИИ» и проверяет выводы, " +
"формировает итоговое заключение на «Формировании заключения», руководитель просматривает и согласует документы на «Согласовании у руководителя».</p></section>";
}
function passCard(title, body) {
return '<div class="kt-ai-card mini"><h4>' + esc(title) + "</h4>" + body + "</div>";
}
function nowTime() {
var d = new Date();
function p(x) { return (x < 10 ? "0" : "") + x; }
return p(d.getHours()) + ":" + p(d.getMinutes());
}
/* ---------- Действия ---------- */
var STEPS_UPLOAD = [
"Проверка файла",
"Сканирование на вирусы",
"Извлечение текста"
];
/* Скрытый input для выбора файлов на устройстве */
if (typeof window !== "undefined") {
var _fileInput = null;
function getFileInput() {
if (!_fileInput) {
_fileInput = document.createElement("input");
_fileInput.type = "file";
_fileInput.multiple = true;
_fileInput.accept = ".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt";
_fileInput.style.display = "none";
_fileInput.addEventListener("change", function () {
if (_fileInput.files && _fileInput.files.length) {
handleFiles(Array.prototype.slice.call(_fileInput.files));
}
_fileInput.value = "";
});
document.body.appendChild(_fileInput);
}
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();
render();
if (pct >= 100) {
clearInterval(iv);
setTimeout(function () {
D.files.push({ n: name, t: item.t, size: size, d: item.d, s: "ready" });
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) {
D.files[i].s = "ready";
render();
toast("Файл отмечен как готовый к анализу", "ok");
};
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 () {
if (!D.files.length) { toast("Сначала загрузите файлы", "warn"); return; }
if (D.files.some(function (f) { return f.s !== "ready"; })) { toast("Дождитесь завершения загрузки всех файлов", "warn"); return; }
D.running = true;
D.contra = false;
D.falseDone = false;
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.rejected = null;
D.stamp = null;
D.violations = vio;
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
D.log = [];
save();
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.viewFile = function (i) {
var f = D.files[i];
openDrawer(
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-5)">' +
"<strong>" + esc(f.n) + "</strong>" + pill(f.s) +
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeDrawer()" style="margin-left:auto">' + icon("x") + "</button></div>" +
'<div class="doc-paper">' +
'<div class="doc-head"><strong>' + esc(f.t) + '</strong><span class="doc-id">' + esc(f.d) + "</span></div>" +
'<div class="doc-text" style="border:none;background:transparent;padding:0;font-size:var(--kt-ai-text-sm)">' +
"<p>Документ загружен во внутреннюю систему. Эксперт просматривает комплектность и при необходимости открывает исходник в корпоративном хранилище.</p>" +
"<p style='margin-top:var(--kt-ai-space-3)'>Статус: " + (ST[f.s] ? esc(ST[f.s].label) : esc(f.s)) + ".</p>" +
"</div></div>"
);
};
S.refute = function (i) {
var v = D.violations[i];
v.v = v.v === "falsep" ? "checked" : "falsep";
if (v.y === "contra") {
D.contra = v.v === "falsep";
D.falseDone = v.v === "falsep";
}
D.checklist.forEach(function (idx, pos) {
if (D.violations[idx].v === "falsep") { D.checklist.splice(pos, 1); pos--; }
});
render();
toast(v.v === "falsep" ? "Вывод отмечен как ложное срабатывание" : "Вывод возвращён в работу", v.v === "falsep" ? "warn" : "ok");
};
S.ck = function (idx) {
var pos = D.checklist.indexOf(idx);
var el = $("ck" + idx);
if (el && el.checked && pos < 0) D.checklist.push(idx);
if (el && !el.checked && pos >= 0) D.checklist.splice(pos, 1);
D.checklist.sort(function (a, b) { return a - b; });
save();
// обновить счётчик без перерисовки чек-бокса
render();
};
function rebuildVerdict() {
var parts = D.checklist.map(function (idx) {
var v = D.violations[idx];
return "\n" + (parts.indexOf(idx) + 1) + ". " + v.d + " (" + v.r + ").";
});
var base = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + D.candidate + ") прошла проверку соответствия нормативной базе.";
if (D.checklist.length > 0) {
base += " По завершённой сверке зафиксированы следующие несоответствия:" + parts.join("") +
"\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.";
} else {
base += " Несоответствий не выявлено.";
}
return base;
}
S.sendForApproval = function () {
var n = 0;
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "checked") n++;
if (n > 0) { toast("Проверьте все выводы: " + n + " на проверке", "warn"); return; }
D.status = "sent";
D.rejected = null;
D.stamp = null;
D.verdict = rebuildVerdict();
render();
toast("Заключение отправлено на согласование руководителю", "ok");
};
S.approve = function () {
D.status = "agree";
D.stamp = nowTime();
render();
toast("Материал согласован", "ok");
};
S.declare = function () {
var m = prompt("Комментарий к отклонению:");
if (m === null) return;
D.status = "decl";
D.stamp = nowTime();
D.rejected = m || "Без комментария";
render();
toast("Материал отклонён", "warn");
};
S.manualMode = function () {
if (confirm("Вернуться в ручной режим? Текущий черновик заключения сохранится.")) {
toast("Ручной режим: работа ведётся без системы", "info");
closeModal();
}
};
S.openRef = function (i) {
var v = D.violations[i];
openModal(
'<div class="kt-ai-card" style="max-width:560px;padding:var(--kt-ai-space-6)">' +
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-4)">' +
icon("link") + "<strong>Ссылка на нормативный акт</strong>" +
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeModal()" style="margin-left:auto">' + icon("x") + "</button></div>" +
'<div class="mini" style="padding:0"><div class="kv"><span class="k">Пункт</span><span class="v">' + esc(v.r) + "</span></div>" +
"<div class='kv' style='display:block'><span class='k'>Вывод системы</span><span class='v' style='text-align:left;margin-top:var(--kt-ai-space-2)'>" + esc(v.d) + "</span></div></div>" +
'</div>'
);
};
S.burger = function () {
var sb = $("sidebar");
if (!sb) return;
if (sb.dataset.open === "true") sb.removeAttribute("data-open");
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();
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);
};