diff --git a/site/static/app.js b/site/static/app.js index c480962..9038454 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -14,7 +14,7 @@ var VM_UPLOAD_URL = 'https://contracts-upload.sinefo.ru/contracts-upload/'; // Прогрев приложения при загрузке страницы. // ⚠️ Без прогрева первый POST может упасть с ERR_CONNECTION_RESET (MTU/Geneve) (async function init() { - try { await fetch('/health'); } catch(e) { /* ignore */ } + try { await fetch('/health'); } catch (e) { /* ignore */ } })(); /** @@ -31,15 +31,15 @@ filePicker = window.FilePicker.initFilePicker({ allowedExt: ['.pdf', '.doc', '.docx'], limits: FILE_PICKER_LIMITS, onChange: onPickerFilesChange, - onError: function(error, file) { + onError: function (error, file) { filePicker.setMessage('Ошибка чтения ' + file.name + ': ' + error.message); }, - onDetails: function(id) { - var index = state.files.findIndex(function(file) { return file.pickerId === id; }); + onDetails: function (id) { + var index = state.files.findIndex(function (file) { return file.pickerId === id; }); if (index >= 0) toggleClassifyDetail(index); }, - onText: function(id) { - var index = state.files.findIndex(function(file) { return file.pickerId === id; }); + onText: function (id) { + var index = state.files.findIndex(function (file) { return file.pickerId === id; }); if (index >= 0) showText(index); } }); @@ -59,10 +59,10 @@ function renderStepper(state) { var steps = state.ui.steps; var map = { upload: 'stepUpload', classify: 'stepClassify', groups: 'stepGroups', compare: 'stepCompare' }; var colors = { '○': 'var(--topbar-muted)', '⏳': 'var(--topbar-active)', '✓': 'var(--topbar-done)' }; - Object.keys(map).forEach(function(key) { + Object.keys(map).forEach(function (key) { var el = document.getElementById(map[key]); if (el) { - el.textContent = steps[key] + ' ' + {upload:'Загрузка',classify:'Классификация',groups:'Группировка',compare:'Сравнение'}[key]; + el.textContent = steps[key] + ' ' + { upload: 'Загрузка', classify: 'Классификация', groups: 'Группировка', compare: 'Сравнение' }[key]; el.style.color = colors[steps[key]] || 'var(--muted)'; } }); @@ -92,10 +92,10 @@ function stepActive(id) { * Мутирует state.ui.steps, вызывает renderStepper. */ function resetStepper(fromId) { - var order = ['stepUpload','stepClassify','stepGroups','stepCompare']; - var keys = ['upload','classify','groups','compare']; + var order = ['stepUpload', 'stepClassify', 'stepGroups', 'stepCompare']; + var keys = ['upload', 'classify', 'groups', 'compare']; var reset = false; - order.forEach(function(id, i) { + order.forEach(function (id, i) { if (id === fromId) reset = true; if (reset) { state.ui.steps[keys[i]] = '○'; } }); @@ -133,7 +133,7 @@ async function runClassify() { stepActive('stepClassify'); var start = Date.now(); - var timer = setInterval(function() { + var timer = setInterval(function () { btn.innerHTML = ' Классификация... ' + Math.round((Date.now() - start) / 1000) + 'с'; }, 1000); @@ -141,7 +141,7 @@ async function runClassify() { var classifyDone = false; // Poll progress каждые 2с — работает и для sync, и для async classify - var pollTimer = setInterval(async function() { + var pollTimer = setInterval(async function () { try { var progressUrl = state.v3Mode ? '/api/classify-progress?batch=' + encodeURIComponent(state.batchId) @@ -171,8 +171,8 @@ async function runClassify() { } if (d.ok && d.counts) { var done = (d.counts.classified || 0) - + (d.counts.failed || 0) - + (d.counts.garbage || 0); + + (d.counts.failed || 0) + + (d.counts.garbage || 0); var total = d.total || 0; if (total > 0) totalFiles = total; btn.innerHTML = ' Классификация... ' + done + '/' + total + ' (' + Math.round((Date.now() - start) / 1000) + 'с)'; @@ -186,14 +186,14 @@ async function runClassify() { loadGroupsAction(); } } - } catch(e) {} + } catch (e) { } }, 2000); try { var resp = await fetch(VM_API + '/api/classify-batch', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({batch_id: state.batchId}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ batch_id: state.batchId }) }); var data = await resp.json(); state.v3Mode = Boolean(data.v3); @@ -211,7 +211,7 @@ async function runClassify() { if (data.v3) summary += ' | арифметика: ' + data.math_checks_passed; if (data.v3 && data.failed) summary += ' | ошибок: ' + data.failed; if (data.garbage) summary += ' | 🗑️ ' + data.garbage + ' мусор'; - if (data.types) summary += ' | ' + Object.entries(data.types).map(function(e){return e[1]+'×'+e[0];}).join(', '); + 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'); @@ -231,7 +231,7 @@ async function runClassify() { btn.innerHTML = '✗ ' + (data.error || 'ошибка'); btn.disabled = false; } - } catch(e) { + } catch (e) { if (!classifyDone) { clearInterval(timer); clearInterval(pollTimer); @@ -246,7 +246,7 @@ async function runClassify() { // Текущий активный EventSource (только один одновременно) // state._activeCompare — теперь в state.js (Фаза 0: state + render) -window.runCompareForGroup = async function(gi, btn) { +window.runCompareForGroup = async function (gi, btn) { var group = state.groups[gi]; if (!group || group.contract_number === '__unresolved__') return; @@ -265,17 +265,17 @@ window.runCompareForGroup = async function(gi, btn) { btn.disabled = true; btn.innerHTML = ' Применяю...'; // Заблокировать ВСЕ кнопки сравнения пока идёт процесс - document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = true; }); + document.querySelectorAll('.cmp-btn').forEach(function (b) { b.disabled = true; }); var ar = await fetch(VM_API + '/api/apply-groups', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({batch_id: state.batchId, groups: [group]}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ batch_id: state.batchId, groups: [group] }) }); var ad = await ar.json(); if (!ad.ok || !ad.contract_ids || !ad.contract_ids.length) { btn.innerHTML = '✗ ошибка'; btn.disabled = false; - document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; }); + document.querySelectorAll('.cmp-btn').forEach(function (b) { b.disabled = false; }); return; } @@ -306,7 +306,7 @@ window.runCompareForGroup = async function(gi, btn) { cmpBody, statusEl, { - onDone: function(d, sections) { + onDone: function (d, sections) { state._activeCompare.timer = null; state._activeCompare.es = null; group.compare = { @@ -316,23 +316,23 @@ window.runCompareForGroup = async function(gi, btn) { sections: sections }; renderGroups(state); - document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; }); + document.querySelectorAll('.cmp-btn').forEach(function (b) { b.disabled = false; }); stepDone('stepCompare'); }, - onError: function(d) { + onError: function (d) { state._activeCompare.timer = null; state._activeCompare.es = null; group.compare = null; cmpBody.innerHTML += '
✗ ' + escHtml(d.message) + '
'; btn.innerHTML = '✗'; btn.disabled = false; - document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; }); + document.querySelectorAll('.cmp-btn').forEach(function (b) { b.disabled = false; }); }, - onConnectionError: function() { + onConnectionError: function () { state._activeCompare.timer = null; state._activeCompare.es = null; group.compare = null; btn.innerHTML = '✗ соединение'; btn.disabled = false; - document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; }); + document.querySelectorAll('.cmp-btn').forEach(function (b) { b.disabled = false; }); } } ); @@ -343,7 +343,7 @@ window.runCompareForGroup = async function(gi, btn) { // uploadFile → files.js (Фаза 1) // ── LLM: Общее сравнение (Event Sourcing) ──────────────────────── -document.getElementById('llmBtn').addEventListener('click', function() { +document.getElementById('llmBtn').addEventListener('click', function () { if (!state.contractId) return; var btn = this; btn.disabled = true; @@ -356,13 +356,13 @@ document.getElementById('llmBtn').addEventListener('click', function() { compareBody.innerHTML = ''; compareStatus.textContent = '⏳ 0.0с'; - var order = state.files.map(function(f) { return f.supp_id; }).filter(Boolean).join(','); + var order = state.files.map(function (f) { return f.supp_id; }).filter(Boolean).join(','); var url = '/process-v2?contract_id=' + state.contractId + (order ? '&order=' + encodeURIComponent(order) : ''); // Фаза 3: унифицированный SSE через startCompareSSE (compare.js) startCompareSSE(url, compareBody, compareStatus, { - onDone: function(d) { + onDone: function (d) { btn.innerHTML = ' ✓ Готово'; stepDone('stepCompare'); if (d.total_unresolved > 0) { @@ -371,12 +371,12 @@ document.getElementById('llmBtn').addEventListener('click', function() { document.getElementById('chatCard').style.display = 'block'; lucide.createIcons(); }, - onError: function(d) { + onError: function (d) { compareBody.innerHTML += '
✗ ' + escHtml(d.message) + '
'; btn.innerHTML = ' Общее сравнение'; btn.disabled = false; }, - onConnectionError: function() { + onConnectionError: function () { if (compareBody.innerHTML === '') { compareBody.innerHTML = '
✗ Ошибка соединения
'; } @@ -390,7 +390,7 @@ document.getElementById('llmBtn').addEventListener('click', function() { // refreshSupps → files.js (Фаза 1) // ── Чат ────────────────────────────────────────────────────── -document.getElementById('chatSend').addEventListener('click', function() { +document.getElementById('chatSend').addEventListener('click', function () { var input = document.getElementById('chatInput'); var q = input.value.trim(); if (!q || !state.contractId) return; @@ -404,21 +404,21 @@ document.getElementById('chatSend').addEventListener('click', function() { var fd = new FormData(); fd.append('question', q); fetch('/chat.cfm?contract_id=' + state.contractId, { method: 'POST', body: fd }) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { msgs.lastChild.remove(); msgs.innerHTML += '
Ответ: ' + (d.OK ? d.ANSWER : (d.ERROR || '—')) + '
'; input.disabled = false; document.getElementById('chatSend').disabled = false; input.focus(); - }).catch(function() { + }).catch(function () { msgs.lastChild.remove(); msgs.innerHTML += '
Ошибка сети
'; input.disabled = false; document.getElementById('chatSend').disabled = false; }); }); -document.getElementById('chatInput').addEventListener('keydown', function(e) { +document.getElementById('chatInput').addEventListener('keydown', function (e) { if (e.key === 'Enter') document.getElementById('chatSend').click(); }); @@ -454,24 +454,24 @@ async function showText(i) { // Общий textify для обеих панелей var textLines = []; - elements.forEach(function(el) { + elements.forEach(function (el) { var etype = el.type || el.TYPE || '?'; if (etype === 'paragraph') { textLines.push(el.text || el.TEXT || ''); } else if (etype === 'table') { var rows = el.rows || el.ROWS || []; if (rows.length > 0) { - var ncols = (rows[0]||[]).length; + var ncols = (rows[0] || []).length; textLines.push('--- Таблица ' + rows.length + '×' + ncols + ' ---'); - rows.forEach(function(row) { - var cells = (row||[]).map(function(c){ return String(c||'').replace(/\n/g,' ').replace(/\|/g,'\\|'); }); + rows.forEach(function (row) { + var cells = (row || []).map(function (c) { return String(c || '').replace(/\n/g, ' ').replace(/\|/g, '\\|'); }); textLines.push('| ' + cells.join(' | ') + ' |'); }); textLines.push(''); } } }); - var textify = textLines.join('\n').replace(//g,'>'); + var textify = textLines.join('\n').replace(//g, '>'); // Левая: сырой текст (textify) leftHtml = '
' + textify + '
'; @@ -481,7 +481,7 @@ async function showText(i) { rightHtml += '
Изменён' + formatDate(f.lastModified) + '
'; if (f.parseInfo) { var d = f.parseInfo; - rightHtml += '
Элементов' + (d.element_count||0) + '
'; + rightHtml += '
Элементов' + (d.element_count || 0) + '
'; if (d.error) rightHtml += '
' + d.error + '
'; } rightHtml += '
Все элементы:
'; @@ -499,13 +499,13 @@ async function showText(i) { if (qData.doc_date) rightHtml += '
Дата' + escHtml(qData.doc_date) + '
'; if (qData.counterparty) rightHtml += '
Контрагент' + escHtml(qData.counterparty) + '
'; if (qData.classify_raw) { - var rawId = 'raw_' + docId.replace(/-/g,''); + var rawId = 'raw_' + docId.replace(/-/g, ''); rightHtml += '
Сырой ответ LLM'; rightHtml += '
' + escHtml(qData.classify_raw) + '
'; rightHtml += '
'; } } - } catch(e) { + } catch (e) { leftHtml = 'Ошибка'; } } else { @@ -535,11 +535,11 @@ var promptNotes = document.getElementById('promptNotes'); // extract — извлечение строк из первого документа (базовый договор) // diff — сравнение допсоглашений (ADD/UPDATE/DELETE) // classify — классификация документа (тип, номер, контрагент, дата) -var activePromptId = {extract: '', diff: '', classify: ''}; +var activePromptId = { extract: '', diff: '', classify: '' }; function openPromptModal(role) { promptRole = role; - var labels = {extract: 'Извлечение', diff: 'Сравнение', classify: 'Классификация'}; + var labels = { extract: 'Извлечение', diff: 'Сравнение', classify: 'Классификация' }; promptModalTitle.textContent = 'Промпт: ' + (labels[role] || role); document.getElementById('promptTabExtract').style.background = role === 'extract' ? 'var(--brand-primary)' : ''; document.getElementById('promptTabExtract').style.color = role === 'extract' ? '#fff' : ''; @@ -561,26 +561,26 @@ function closePromptModal(e) { function loadPrompt(role) { fetch(VM_API + '/api/prompts?role=' + role) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { if (d.ok) { promptEditor.value = d.body || ''; activePromptId[role] = d.id || ''; loadHistory(role); } }) - .catch(function(e) { console.error('loadPrompt:', e); }); + .catch(function (e) { console.error('loadPrompt:', e); }); } function loadHistory(role) { fetch(VM_API + '/api/prompts/list?role=' + role) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { if (!d.ok || !d.versions || !d.versions.length) { promptHistory.innerHTML = '
нет версий
'; return; } - promptHistory.innerHTML = d.versions.map(function(v) { + promptHistory.innerHTML = d.versions.map(function (v) { var isActive = v.is_active ? ' ' : ''; var style = v.is_active ? 'font-weight:600;' : ''; return '
' @@ -592,17 +592,17 @@ function loadHistory(role) { + '
'; }).join(''); }) - .catch(function(e) { console.error('loadHistory:', e); }); + .catch(function (e) { console.error('loadHistory:', e); }); } function activatePrompt(id) { fetch(VM_API + '/api/prompts/activate', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({id: id}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id }) }) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { if (d.ok) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; } else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); } }); @@ -612,30 +612,30 @@ function deletePrompt(id) { if (!confirm('Удалить эту версию промпта?')) return; fetch(VM_API + '/api/prompts/delete', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({id: id}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id }) }) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { if (d.ok) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; } else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); } }); } -document.getElementById('promptSave').addEventListener('click', function() { +document.getElementById('promptSave').addEventListener('click', function () { var body = promptEditor.value.trim(); if (!body) { promptStatus.textContent = '✕ тело промпта пустое'; return; } - var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g,'-').substring(0,16)); + var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16)); fetch(VM_API + '/api/prompts/save', { method: 'POST', - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: promptRole, name: name, body: body, notes: promptNotes.value.trim(), is_active: true }) }) - .then(function(r) { return r.json(); }) - .then(function(d) { + .then(function (r) { return r.json(); }) + .then(function (d) { if (d.ok) { promptStatus.textContent = '✓ сохранено (новая версия)'; promptName.value = ''; promptNotes.value = ''; @@ -646,10 +646,10 @@ document.getElementById('promptSave').addEventListener('click', function() { }); }); -document.getElementById('promptTabExtract').addEventListener('click', function() { openPromptModal('extract'); }); -document.getElementById('promptTabDiff').addEventListener('click', function() { openPromptModal('diff'); }); -document.getElementById('promptTabClassify').addEventListener('click', function() { openPromptModal('classify'); }); -document.getElementById('promptBtnExtract').addEventListener('click', function() { openPromptModal('extract'); }); -document.getElementById('promptBtnDiff').addEventListener('click', function() { openPromptModal('diff'); }); -document.getElementById('promptBtnClassify').addEventListener('click', function() { openPromptModal('classify'); }); +document.getElementById('promptTabExtract').addEventListener('click', function () { openPromptModal('extract'); }); +document.getElementById('promptTabDiff').addEventListener('click', function () { openPromptModal('diff'); }); +document.getElementById('promptTabClassify').addEventListener('click', function () { openPromptModal('classify'); }); +document.getElementById('promptBtnExtract').addEventListener('click', function () { openPromptModal('extract'); }); +document.getElementById('promptBtnDiff').addEventListener('click', function () { openPromptModal('diff'); }); +document.getElementById('promptBtnClassify').addEventListener('click', function () { openPromptModal('classify'); }); lucide.createIcons();