diff --git a/site/static/app.js b/site/static/app.js index 08c2b63..36a8b0a 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -180,7 +180,10 @@ async function runClassify() { clearInterval(timer); clearInterval(pollTimer); classifyDone = true; - btn.innerHTML = '✓ ' + data.done + '/' + data.total + ' классифицировано (' + Math.round((Date.now() - start) / 1000) + 'с)'; + var summary = data.summary || (data.done + '/' + data.total + ' классифицировано'); + if (data.garbage) summary += ' | 🗑️ ' + data.garbage + ' мусор'; + if (data.types) summary += ' | ' + Object.entries(data.types).map(function(e){return e[1]+'×'+e[0];}).join(', '); + btn.innerHTML = '✓ ' + summary + ' (' + Math.round((Date.now() - start) / 1000) + 'с)'; stepDone('stepClassify'); stepActive('stepGroups'); loadGroupsAction(); diff --git a/site/static/files.js b/site/static/files.js index a025050..2f050f2 100644 --- a/site/static/files.js +++ b/site/static/files.js @@ -35,7 +35,10 @@ function statusToHTML(st) { case 'unzipping': return '⏳ распаковка...'; case 'parsing': return '⏳ парсинг...'; case 'parsed': return '✓ ' + (st.count || 0) + ' эл.' + (st.elapsed ? ' (' + st.elapsed + 'с)' : '') + ''; - case 'error': return '✗ ' + (st.text || 'Неизвестная ошибка') + ''; + case 'error': return '✗ ' + escHtml(st.text || 'Неизвестная ошибка') + ''; + case 'duplicate': return '⤿ дубликат'; + case 'warning': return '⚠ ' + escHtml(st.text || 'пустой') + ''; + case 'no_format': return '✗ формат не поддерживается'; default: return ''; } } @@ -367,6 +370,16 @@ async function addZipFile(file) { }); if (!zipResp.ok || !zipResp.files) throw new Error('unzip failed'); + // Подтверждение: показать список файлов перед загрузкой + var fileList = zipResp.files.map(function(f) { return ' • ' + f.filename + ' (' + (f.size/1024).toFixed(1) + ' KB)'; }).join('\n'); + if (!confirm('Архив «' + file.name + '» — ' + total + ' файлов:\n\n' + fileList + '\n\nЗагрузить эти файлы?')) { + // Отмена — убрать строку ZIP + var zipPos2 = state.files.indexOf(zipEntry); + if (zipPos2 >= 0) state.files.splice(zipPos2, 1); + render(state); + return; + } + // Шаг 2: обработать каждый файл из архива // Временная строка ZIP остаётся в таблице — обновляем её статус var total = zipResp.files.length; @@ -443,7 +456,20 @@ async function addRegularFile(file, zipSource) { // Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys) if (resp && resp.contract_id) state.contractId = resp.contract_id; state.files[rowIdx].doc_id = resp.doc_id; - state.files[rowIdx].status = { kind: 'uploaded' }; + + // Дубликат? + if (resp && resp.duplicate_of) { + state.files[rowIdx].status = { kind: 'duplicate' }; + render(state); + return; + } + // Warning от парсинга? + if (resp && resp.warning) { + state.files[rowIdx].status = { kind: 'warning', text: resp.warning }; + render(state); + } else { + state.files[rowIdx].status = { kind: 'uploaded' }; + } state.files[rowIdx].uploaded = true; // Авто-парсинг: бэкенд парсит всё (PDF + DOCX + DOC)