v1.0.178: Фаза 1.4 — разбит fileInput handler. Чистые функции: reconcileSelection, applyParseResult. Async-actions: addZipFile, addRegularFile. Оркестратор: onFilesSelected (15 строк). finalizeUpload. app.js fileInput listener: 4 строки.

This commit is contained in:
2026-06-25 08:59:17 +04:00
parent 67658d0ef1
commit ba9a833ce8
2 changed files with 232 additions and 147 deletions
+4 -147
View File
@@ -71,155 +71,12 @@ function resetStepper(fromId) {
// moveUp/moveDown/removeFile/openAbout/closeAbout — вынесены в app_utils.js
// ── Автозагрузка при выборе файлов ──────────────────────────
// Фаза 1 (decoupling-final-plan.md): fileInput handler разбит на чистые функции
// onFilesSelected → reconcileSelection → addZipFile/addRegularFile → finalizeUpload
// Вся логика — в files.js. Здесь только тонкая обёртка.
fileInput.addEventListener('change', async function() {
var newFiles = Array.from(fileInput.files);
if (newFiles.length === 0) return;
fileInput.disabled = true;
// Удалить из очереди файлы, которых нет в новом выборе
var newNames = new Set(newFiles.map(function(f) { return f.name; }));
state.files = state.files.filter(function(f) {
return newNames.has(f.name);
});
render(state);
// Разблокировать кнопку классификации — состав файлов изменился
showClassifyBtn();
// Сбросить прогресс пайплайна
resetStepper('stepUpload');
for (var i = 0; i < newFiles.length; i++) {
var f = newFiles[i];
var isZip = f.name.toLowerCase().endsWith('.zip');
if (isZip) {
// ZIP: показать временную строку, распаковать, убрать
var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: { kind: 'unzipping' } };
state.files.push(zipEntry);
var zipIdx = state.files.length - 1;
render(state);
try {
var zipResp = await new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', UNZIP_URL);
xhr.responseType = 'json';
xhr.onload = function() { resolve(xhr.response); };
xhr.onerror = function() { reject(new Error('Сеть')); };
xhr.ontimeout = function() { reject(new Error('Таймаут')); };
xhr.timeout = 60000;
xhr.send(f);
});
var zipData = zipResp;
if (!zipData.ok || !zipData.files) throw new Error('unzip failed');
// Убрать строку ZIP из таблицы
state.files.splice(zipIdx, 1);
render(state);
// Добавить файлы из архива, пропуская дубликаты
for (var zi = 0; zi < zipData.files.length; zi++) {
var zf = zipData.files[zi];
if (zf.error) continue; // пропускаем ошибки
// Проверить дубликат по имени
var dup = -1;
for (var dj = 0; dj < state.files.length; dj++) {
if (state.files[dj].name === zf.filename) { dup = dj; break; }
}
if (dup >= 0) continue; // уже есть — не добавляем
var zEntry = {
name: zf.filename, size: zf.size,
doc_id: zf.doc_id, uploaded: true,
status: { kind: 'parsing' }
};
state.files.push(zEntry);
var zIdx = state.files.length - 1;
if (!state.contractId && zf.contract_id) state.contractId = zf.contract_id;
render(state);
// Авто-парсинг: VM Python парсит ВСЁ
var zpr = zf.parsed;
if (zpr && zpr.status === 'parsed') {
state.files[zIdx].parsed = true;
state.files[zIdx].status = { kind: 'parsed', count: zpr.element_count };
} else if (zpr && zpr.status === 'error') {
state.files[zIdx].status = { kind: 'error', text: zpr.error || 'ошибка парсинга' };
} else {
state.files[zIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
render(state);
}
} catch(ze) {
state.files[zipIdx].status = { kind: 'error', text: 'ZIP: ' + ze.message };
render(state);
}
continue; // ZIP обработан, переходим к следующему файлу
}
// Обычный файл (не ZIP)
var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'uploading', pct: 0 } };
var existingIdx = -1;
for (var j = 0; j < state.files.length; j++) {
if (state.files[j].name === f.name) { existingIdx = j; break; }
}
var rowIdx;
if (existingIdx >= 0) {
state.files[existingIdx] = entry;
rowIdx = existingIdx;
} else {
state.files.push(entry);
rowIdx = state.files.length - 1;
}
render(state);
try {
var resp = await uploadFile(f, function(pct) {
state.files[rowIdx].status = { kind: 'uploading', pct: pct };
render(state);
});
// Python API returns lowercase keys (doc_id, contract_id, parsed)
if (resp && resp.contract_id) state.contractId = resp.contract_id;
state.files[rowIdx].doc_id = resp.doc_id;
state.files[rowIdx].status = { kind: 'uploaded' };
state.files[rowIdx].uploaded = true;
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
var t0 = Date.now();
var pr = resp.parsed;
if (pr && pr.status === 'parsed') {
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
state.files[rowIdx].parsed = true;
state.files[rowIdx].parseInfo = pr;
state.files[rowIdx].status = { kind: 'parsed', count: pr.element_count, elapsed: elapsed };
} else if (pr && pr.status === 'error') {
state.files[rowIdx].status = { kind: 'error', text: pr.error || 'ошибка парсинга' };
state.files[rowIdx].parseInfo = pr;
} else {
state.files[rowIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
} catch(err) {
state.files[rowIdx].status = { kind: 'error', text: err.message };
}
render(state);
}
await refreshSupps();
stepDone('stepUpload');
stepActive('stepClassify');
// Синхронизировать БД с таблицей — удалить всё, чего нет в state.files
syncDB();
fileInput.value = '';
fileInput.disabled = false;
// Показать кнопки после авто-парсинга
var llmBtn = document.getElementById('llmBtn');
llmBtn.style.display = 'flex';
llmBtn.disabled = false;
showClassifyBtn();
lucide.createIcons();
onFilesSelected(newFiles);
});
// ── Классификация ──────────────────────────────────────────