v2: полный процесс — drag&drop загрузка, живой прогресс, анализ по фактическим файлам, живой журнал

This commit is contained in:
tore 2026-09-22 10:54:08 +00:00
parent d2eeb631f1
commit b19daf0005
3 changed files with 394 additions and 139 deletions

View File

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

455
script.js
View File

@ -38,53 +38,28 @@ function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href=
function seed() {
return {
view: "upload",
req: "Кандидат: А. Т. Нурмолдаев · SEO-1, управление сетевых сервисов · заявка от 14.09.2026",
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 }
],
files: [
{ 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" }
{ 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,
log: [
{ t: "10:42", b: "Пакет документов принят, 8 файлов" },
{ t: "10:42", b: "Извлечение текста: пояснительная записка, 12 разд." },
{ t: "10:43", b: "Сверка с Правилами поиска и подбора персонала: раздел «Организация отбора», п. 4.2" },
{ t: "10:43", b: "Найдено отклонение: дата объявления о вакансии" },
{ t: "10:44", b: "Сверка с Правилами трудового распорядка, гл. 5 «Назначение руководителей»" },
{ t: "10:44", b: "Сверка с Законом «Об АО»: компетенции совета директоров, ст. 36" },
{ 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",
contra: false,
falseDone: false,
log: [],
violations: [],
checklist: [],
verdict: "",
status: "draft",
rejected: null,
stamp: null
};
@ -111,8 +86,8 @@ function doneCount() {
return n;
}
function stepNum() {
if (D.view === "upload") return D.running || allFilesReady() ? 2 : 1;
if (D.view === "results") return D.contra && !D.falseDone ? 2 : 3;
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;
@ -196,7 +171,8 @@ function render() {
else if (D.view === "verdict") html = vVerdict();
else if (D.view === "approve") html = vApprove();
else html = vAbout();
var view = $("view");
var view = $("viewInner");
if (!view) view = $("view");
if (!view) return;
view.innerHTML = html;
$("pageTitle").textContent = TITLES[D.view] || "";
@ -223,46 +199,95 @@ function kpiStrip(items) {
}
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */
function vUpload() {
var readyCount = 0;
for (var i = 0; i < D.files.length; i++) if (D.files[i].s === "ready") readyCount++;
var all = readyCount === D.files.length;
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";
}
var head = headReady;
if (all && !D.running) {
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)">' +
'<span style="flex:1;min-width:0">Все материалы получены — пакет готов к запуску анализа</span>' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
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 rows = D.files.map(function (f, i) {
var statusCell = pill(f.s);
if (f.s === "wait") statusCell += ' <span class="kt-ai-chip" data-tone="orange" style="margin-left:var(--kt-ai-space-2)">ждёт загрузки</span>';
return "<tr" + (f.s === "wait" ? ' data-wait="true"' : "") + ">" +
'<td data-label="Файл">' + esc(f.n) + "</td>" +
'<td data-label="Тип"><span class="kt-ai-meta">' + esc(f.t) + "</span></td>" +
'<td data-label="Загружен"><span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs)">' + esc(f.d) + "</span></td>" +
'<td data-label="Статус">' + statusCell + "</td>" +
'<td class="row-act"><button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="S.viewFile(' + i + ')">Смотреть</button></td>' +
"</tr>";
}).join("");
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 headReady = hasWaiting
? '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-block)">' +
"<strong>Пакет не собран</strong>" +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Не все файлы получены от кандидата — анализ станет доступен после загрузки остальных материалов.</span></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></div>" + vStepper() + "</section>" +
head +
(head ? '<div style="margin-bottom:var(--kt-ai-stack-tight)">' + head + "</div>" : "") +
'<section class="sec"><div class="sec-h">Материалы' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(readyCount + " из " + D.files.length) + " готово</span></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></section>" +
(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>" +
@ -276,8 +301,21 @@ function vUpload() {
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
}).join("") + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
"<p>Строками идёт по заявке и пакету документов. Статусы файлов: «Ожидает проверки» — файл ещё получен не был, «Готово к анализу» — файл проверен и получен. " +
"Когда собран весь пакет, вверху экрана появляется кнопка «Запустить анализ ИИ».</p></section>";
"<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. Результаты анализа ИИ ---------- */
@ -485,42 +523,97 @@ function nowTime() {
}
/* ---------- Действия ---------- */
S.uploadFiles = function () {
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 || [];
if (files.length === 0) return;
var added = 0;
for (var i = 0; i < files.length; i++) {
var f = files[i];
var exists = false;
for (var j = 0; j < D.files.length; j++) {
if (D.files[j].s === "wait" && D.files[j].n.toLowerCase().indexOf(f.name.toLowerCase().slice(0, 20)) >= 0) {
D.files[j].s = "ready";
D.files[j].d = nowTime();
exists = true;
added++;
break;
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));
}
}
if (!exists) {
var ext = (f.name.lastIndexOf(".") >= 0) ? f.name.slice(f.name.lastIndexOf(".")) : "";
var typeLabel = "Подтверждающий документ";
if (f.name.toLowerCase().indexOf("записк") >= 0) typeLabel = "Пояснительная записка";
D.files.push({ n: f.name, t: typeLabel, d: nowTime() + " (загрузка)", s: "ready" });
added++;
}
_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();
toast(added + " файл(ов) добавлено в пакет", "ok");
var allNow = D.files.every(function (x) { return x.s === "ready"; });
if (allNow) toast("Пакет собран — анализ стал доступен", "ok");
};
input.click();
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.delFile = function (i) {
D.files.splice(i, 1);
save();
render();
toast("Файл удалён из пакета", "info");
};
S.fileReady = function (i) {
@ -531,49 +624,90 @@ S.fileReady = function (i) {
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 (!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.contra = 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.rejected = 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[6].v = "falsep";
D.violations[7].v = "falsep";
D.violations = vio;
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
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]); }
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;
@ -688,6 +822,51 @@ S.burger = function () {
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);
};

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 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); }