diff --git a/index.html b/index.html index 5ca6c16..bedd7f5 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,9 @@ Эксперт -
+
+
+
diff --git a/script.js b/script.js index 9ce7ba7..47b6b9e 100644 --- a/script.js +++ b/script.js @@ -38,53 +38,28 @@ function icon(name) { return '
' + + '
' + + '' + (progress.step < 3 ? "…" : icon("check")) + "" + + '
' + esc(STEPS_UPLOAD[progress.step] || "…") + "" + + '' + esc(progress.file) + "
" + + '"
' + + '
' + + '
' + + '
' + esc(progress.file) + "" + + '' + num(progress.pct) + " %" + "
" + + "
"; + } else if (progress && progress.file) { + zoneBlock = dropZoneHtml() + + '
' + + '
' + + '' + icon("check") + "" + + '
Загрузка завершена' + + '' + esc(progress.file) + "
" + + "
" + + '
' + + '
' + esc(progress.file) + "" + + '' + fmtSize(progress.size || 0) + "
" + + "
"; + } else { + zoneBlock = dropZoneHtml(); } - var rows = D.files.map(function (f, i) { - var statusCell = pill(f.s); - if (f.s === "wait") statusCell += ' ждёт загрузки'; - return "" + - '' + esc(f.n) + "" + - '' + esc(f.t) + "" + - '' + esc(f.d) + "" + - '' + statusCell + "" + - '' + - ""; - }).join(""); + var head = ""; + if (!progress && D.files.length) { + var all = D.files.every(function (f) { return f.s === "ready"; }); + if (all && !D.running) { + head = '
' + + "Все материалы получены — пакет готов к запуску анализа" + + '
'; + } else if (!all && !D.running) { + head = '
' + + "Пакет не собран — не все файлы получены от кандидата" + + '
'; + } + } - var headReady = hasWaiting - ? '
' + - "Пакет не собран" + - 'Не все файлы получены от кандидата — анализ станет доступен после загрузки остальных материалов.
' + var table = D.files.length + ? '
' + + '' + + "" + D.files.map(function (f, i) { + return "" + + '" + + '" + + '" + + '" + + '" + + ""; + }).join("") + "
НазваниеРазмерДатаСтатус
' + esc(f.n) + "' + fmtSize(f.size || 0) + "' + esc(f.d) + "' + pill(f.s) + "
" : ""; return '
Процесс' + '' + 'Заявка ' + num("ЭД-2026-118") + "
" + vStepper() + "
" + - head + + (head ? '
' + head + "
" : "") + '
Материалы' + '' + - '' + num(readyCount + " из " + D.files.length) + " готово
" + - '
' + - '' + - "" + rows + "
ФайлТипЗагруженСтатус
" + + (D.files.length ? '' + num(D.files.length) + " файл(а)" : "") + "" + + zoneBlock + table + "" + '
Источники для сверки' + '' + '' + num(D.sources.length) + " источника
" + @@ -276,8 +301,21 @@ function vUpload() { '
' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "
"; }).join("") + "
" + '

Что на этом экране

' + - "

Строками идёт по заявке и пакету документов. Статусы файлов: «Ожидает проверки» — файл ещё получен не был, «Готово к анализу» — файл проверен и получен. " + - "Когда собран весь пакет, вверху экрана появляется кнопка «Запустить анализ ИИ».

"; + "

Загрузите документы: перетащите файлы в зону загрузки или нажмите «выберите на устройстве». Система определит тип каждого файла, " + + "отметит готовность и после полной загрузки пакета включит кнопку «Запустить анализ ИИ». Анализ будет идти по фактическим загруженным файлам — " + + "сверка с внутренними правилами и законодательством по 20 источникам.

"; +} + +function dropZoneHtml() { + var active = D.dropActive ? ' data-active="true"' : ""; + return '
' + + '
' + + '' + icon("upload") + "" + + "Перетащите файлы сюда" + + "или выберите на устройстве" + + "
" + + "PDF, DOCX, XLSX, TXT — до 50 МБ" + + "
"; } /* ---------- Экран 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); +}; diff --git a/style.css b/style.css index 433609b..59cdddd 100644 --- a/style.css +++ b/style.css @@ -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); }