From 89e787a3ad6358bc9a30a5ebba4390a9411ec210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 22 Jun 2026 07:35:25 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.95:=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20index.cfm=20?= =?UTF-8?q?+=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.cfm | 421 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 238 insertions(+), 183 deletions(-) diff --git a/index.cfm b/index.cfm index a6e1abb..afa6ca6 100644 --- a/index.cfm +++ b/index.cfm @@ -34,14 +34,15 @@ .status-err { color: var(--destructive); } .summary-row td { background: rgba(34,197,94,.05); font-weight: 600; } .empty-row td { color: var(--muted); text-align: center; padding: 24px; } - .remove-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; } - .remove-btn:hover { color: var(--destructive); } + .remove-btn { cursor: pointer; color: #f87171; background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; } + .remove-btn:hover { color: #ef4444; } .info-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 14px; display: none; } .info-btn:hover { color: var(--brand-primary); } .info-btn.visible { display: inline; } .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.4); z-index: 100; justify-content: center; align-items: center; } .modal-overlay.open { display: flex; } .modal { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 4px 24px rgba(0,0,0,.15); max-width: 520px; width: 90%; max-height: 80vh; display: flex; flex-direction: column; } + .modal.wide { max-width: 900px; } .modal-header { padding: 14px 16px; border-bottom: 1px solid var(--brand-gray); display: flex; align-items: center; gap: 8px; font-weight: 600; flex-shrink: 0; } .modal-body { padding: 16px; overflow-y: auto; } .modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; } @@ -56,6 +57,15 @@ .diff-field-new { color: var(--green); font-weight: 600; } @keyframes spin { to { transform: rotate(360deg); } } .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; } + .text-panes { display: flex; gap: 0; } + .text-pane { flex: 1; min-width: 0; overflow-y: auto; max-height: 65vh; } + .text-pane:first-child { border-right: 1px solid var(--brand-gray); padding-right: 12px; } + .text-pane:last-child { padding-left: 12px; } + .text-pane pre { background: var(--brand-grey-light); padding: 10px; border-radius: 6px; font-size: 11px; white-space: pre-wrap; word-break: break-word; overflow-x: auto; margin: 0; max-height: 60vh; overflow-y: auto; } + .text-pane table { font-size: 11px; width: 100%; } + .text-pane th { font-size: 10px; padding: 4px 6px; } + .text-pane td { padding: 3px 6px; font-size: 11px; } + .text-pane .pane-title { font-weight: 600; font-size: 12px; margin-bottom: 8px; color: var(--muted); text-transform: uppercase; } @@ -76,18 +86,14 @@
- +
ИмяИзменёнРазмерСтатусБазовый
ИмяИзменёнРазмерПарсингБазовыйТекст
- - -
@@ -144,7 +150,6 @@ var CONVERT_URL = 'https://contracts.kube5s.ru/convert-doc'; var SITE_URL = ''; // same origin for api calls var fileInput = document.getElementById('fileInput'); -var parseBtn = document.getElementById('parseBtn'); var fileTable = document.getElementById('fileTable'); var fileQueue = []; var contractId = null; @@ -165,7 +170,6 @@ function formatDate(ts) { function renderTable() { if (fileQueue.length === 0) { fileTable.innerHTML = 'Нет файлов — выберите .docx / .pdf / .zip'; - parseBtn.disabled = true; } else { fileTable.innerHTML = fileQueue.map(function(f, i) { var radio = ''; @@ -178,7 +182,7 @@ function renderTable() { '' + formatSize(f.size) + '' + '' + (f.status || '') + '' + '' + radio + '' + - '' + + '' + '' + ''; }).join(''); @@ -199,14 +203,23 @@ fileInput.addEventListener('change', async function() { if (newFiles.length === 0) return; fileInput.disabled = true; - parseBtn.disabled = true; - parseBtn.innerHTML = ' Загрузка...'; for (var i = 0; i < newFiles.length; i++) { var f = newFiles[i]; var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' }; - fileQueue.push(entry); - var rowIdx = fileQueue.length - 1; + // Заменить старый файл с тем же именем, если есть + var existingIdx = -1; + for (var j = 0; j < fileQueue.length; j++) { + if (fileQueue[j].name === f.name) { existingIdx = j; break; } + } + var rowIdx; + if (existingIdx >= 0) { + fileQueue[existingIdx] = entry; + rowIdx = existingIdx; + } else { + fileQueue.push(entry); + rowIdx = fileQueue.length - 1; + } renderTable(); try { @@ -218,6 +231,26 @@ fileInput.addEventListener('change', async function() { fileQueue[rowIdx].doc_id = resp.DOC_ID; fileQueue[rowIdx].status = ''; fileQueue[rowIdx].uploaded = true; + + // Авто-парсинг сразу после загрузки + var t0 = Date.now(); + fileQueue[rowIdx].status = '⏳ парсинг...'; + renderTable(); + try { + var pr = await fetch('/parser.cfm?doc_id=' + resp.DOC_ID); + var pd = await pr.json(); + var elapsed = ((Date.now() - t0) / 1000).toFixed(1); + if (pd.OK && pd.STATUS === 'parsed') { + fileQueue[rowIdx].parsed = true; + fileQueue[rowIdx].parseInfo = pd; + fileQueue[rowIdx].status = '✓ ' + pd.ELEMENT_COUNT + ' эл. (' + elapsed + 'с)'; + } else { + fileQueue[rowIdx].status = '✗ ' + (pd.ERROR_COUNT > 0 ? (pd.ERRORS && pd.ERRORS[0] || 'ошибка') : 'ошибка') + ''; + fileQueue[rowIdx].parseInfo = pd; + } + } catch(pe) { + fileQueue[rowIdx].status = '✗ парсинг: сеть'; + } } catch(err) { fileQueue[rowIdx].status = '✗ ' + err.message + ''; } @@ -228,8 +261,10 @@ fileInput.addEventListener('change', async function() { fileInput.value = ''; fileInput.disabled = false; - parseBtn.disabled = (contractId === null); - parseBtn.innerHTML = ' Парсинг'; + // Показать кнопки LLM после авто-парсинга + var llmBtn = document.getElementById('llmBtn'); + llmBtn.style.display = 'flex'; + llmBtn.disabled = false; lucide.createIcons(); }); @@ -283,42 +318,105 @@ function uploadFile(file, onProgress) { }); } -// ── Парсинг ────────────────────────────────────────────────── -parseBtn.addEventListener('click', async function() { +// ── LLM: Сравнить (Event Sourcing) ──────────────────────── +document.getElementById('llmBtn').addEventListener('click', function() { if (!contractId) return; - parseBtn.disabled = true; - parseBtn.innerHTML = ' Парсинг...'; + var btn = this; + btn.disabled = true; + btn.innerHTML = ' Сравнение...'; - for (var i = 0; i < fileQueue.length; i++) { - var f = fileQueue[i]; - if (!f.doc_id || f.parsed) continue; - f.status = '⏳'; - renderTable(); - try { - var resp = await fetch('/parser.cfm?doc_id=' + f.doc_id); - var d = await resp.json(); - if (d.OK && d.STATUS === 'parsed') { - f.parsed = true; - f.parseInfo = d; - f.status = '✓ ' + d.ELEMENT_COUNT + ' эл.'; - } else { - f.status = '✗ ' + (d.ERROR_COUNT > 0 ? (d.ERRORS && d.ERRORS[0] || 'ошибка') : 'ошибка') + ''; - f.parseInfo = d; - } - } catch(e) { - f.status = '✗ сеть'; + var diffCard = document.getElementById('diffCard'); + var diffBody = document.getElementById('diffBody'); + var diffStatus = document.getElementById('diffStatus'); + diffCard.style.display = 'block'; + diffBody.innerHTML = ''; + diffStatus.textContent = '⏳ 0.0с'; + + var llmStart = Date.now(); + var timerInterval = setInterval(function() { + diffStatus.textContent = '⏳ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с'; + }, 200); + + var es = new EventSource('https://contracts.kube5s.ru/process-v2?contract_id=' + contractId); + var sections = {}; // supplement_id → DOM element + + es.onmessage = function(e) { + var d = JSON.parse(e.data); + + if (d.type === 'extract_start') { + var sec = document.createElement('div'); + sec.style.cssText = 'margin-top:8px;border:1px solid var(--brand-gray);border-radius:6px;overflow:hidden;'; + sec.innerHTML = '
⏳ ' + d.filename + '
'; + sec.querySelector('.diff-section-header').addEventListener('click', function() { + var body = sec.querySelector('.diff-section-body'); + body.style.display = body.style.display === 'none' ? 'block' : 'none'; + }); + diffBody.appendChild(sec); + sections[d.supplement_id] = sec; } - renderTable(); - } + else if (d.type === 'llm_done') { + var sec = sections[d.supplement_id]; + if (sec) { + sec.querySelector('.diff-section-header').innerHTML = '✓ ' + d.filename + ' — ' + d.ops_count + ' оп., ' + d.mode + ' (' + d.time_s + 'с)'; + sec.querySelector('.diff-section-header').style.color = 'var(--green)'; + } + } + else if (d.type === 'applied') { + var sec = sections[d.supplement_id]; + if (sec && d.ops && d.ops.length > 0) { + var s = d.summary || {}; + var body = sec.querySelector('.diff-section-body'); + var html = '
+' + (s.added||0) + ' ~' + (s.updated||0) + ' -' + (s.deleted||0) + ' ?' + (s.unresolved||0) + '
'; + html += '
'; + d.ops.forEach(function(op) { + var action = op.action; + var cls = action === 'ADD' ? 'diff-added' : action === 'DELETE' ? 'diff-deleted' : action === 'UPDATE' ? 'diff-changed' : ''; + var nr = op.new_row || {}; + var name = nr.name || ''; + var price = nr.price != null ? nr.price : ''; + var qty = nr.qty != null ? nr.qty : ''; + var sum = nr.sum != null ? nr.sum : ''; + var ds = nr.date_start || ''; + html += ''; + }); + html += '
ДействиеУслугаЦенаКол-воСуммаДата
' + action + '' + name + '' + price + '' + qty + '' + sum + '' + ds + '
'; + body.innerHTML = html; + body.style.display = 'block'; + } + } + else if (d.type === 'extract_error' || d.type === 'apply_error') { + diffBody.innerHTML += '
✗ ' + d.filename + ': ' + d.error + '
'; + } + else if (d.type === 'done') { + clearInterval(timerInterval); + es.close(); + diffStatus.textContent = '✓ ' + d.total_time_s + 'с'; + btn.innerHTML = ' ✓ Готово'; + if (d.total_unresolved > 0) { + diffBody.innerHTML += '
⚠ ' + d.total_unresolved + ' неразрешённых строк — разобрать
'; + } + document.getElementById('chatCard').style.display = 'block'; + lucide.createIcons(); + } + else if (d.type === 'error') { + clearInterval(timerInterval); + es.close(); + diffBody.innerHTML += '
✗ ' + d.message + '
'; + btn.innerHTML = ' Сравнить'; + btn.disabled = false; + } + }; - // Обновить информацию о supplement_id/type для radio - await refreshSupps(); - - parseBtn.innerHTML = ' ✓ Готово'; - var llmBtn = document.getElementById('llmBtn'); - llmBtn.style.display = 'flex'; - llmBtn.disabled = false; - lucide.createIcons(); + es.onerror = function() { + clearInterval(timerInterval); + es.close(); + if (diffBody.innerHTML === '') { + diffBody.innerHTML = '
✗ Ошибка соединения
'; + } + btn.innerHTML = ' Сравнить'; + btn.disabled = false; + lucide.createIcons(); + }; }); // ── Обновить supplement_id/type для radio ────────────────── @@ -352,106 +450,6 @@ function setInitial(suppId) { } window.setInitial = setInitial; -// ── LLM: Сравнить ──────────────────────────────────────────── -document.getElementById('llmBtn').addEventListener('click', async function() { - if (!contractId) return; - var btn = this; - btn.disabled = true; - btn.innerHTML = ' LLM-анализ...'; - - // Сохранить промпт на сервер (ДОЖДАТЬСЯ) - var p = promptEditor.value.trim() || DEFAULT_PROMPT; - if (contractId) { - await fetch('/prompt.cfm?contract_id=' + contractId, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({prompt: p}) - }); - } - - var diffCard = document.getElementById('diffCard'); - var diffBody = document.getElementById('diffBody'); - var diffStatus = document.getElementById('diffStatus'); - diffCard.style.display = 'block'; - diffBody.innerHTML = '⏳ LLM-извлечение...'; - diffStatus.textContent = '⏳ 0.0с'; - - var llmStart = Date.now(); - var timerInterval = setInterval(function() { - diffStatus.textContent = '⏳ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с'; - }, 200); - - // 1. Extract for each supplement - try { - var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent("SELECT id, type FROM supplements WHERE contract_id='" + contractId + "' ORDER BY created_at")); - var data = await resp.json(); - if (data.OK && data.ROWS) { - for (var i = 0; i < data.ROWS.length; i++) { - try { - var er = await fetch('/extractor.cfm?supplement_id=' + data.ROWS[i].id); - var ed = await er.json(); - if (ed.OK) { - var info = (ed.SKIPPED ? '⏭ ' : '✓ ') + (data.ROWS[i].type || '') + ': ' + ed.ROWS_SAVED + ' строк'; - diffBody.innerHTML += '
' + info + '
'; - } else { - diffBody.innerHTML += '
✗ ' + (ed.ERROR||'') + '
'; - } - } catch(e) {} - } - } - } catch(e) {} - - // 2. Differ - diffBody.innerHTML += '
📊 Сравнение:
'; - try { - var resp = await fetch('/differ.cfm?contract_id=' + contractId); - var data = await resp.json(); - if (data.OK && data.ALL_CHANGES) { - var all = data.ALL_CHANGES; - diffBody.innerHTML = ''; - all.forEach(function(ch) { - var s = ch.SUMMARY || {}; - diffBody.innerHTML += '
📄 Допник (' + ch.TYPE + ')
'; - diffBody.innerHTML += '
+' + (s.ADDED||0) + ' ~' + (s.CHANGED||0) + ' -' + (s.DELETED||0) + '
'; - diffStatus.textContent = 'изм:' + (s.CHANGED||0) + ' +' + (s.ADDED||0) + ' -' + (s.DELETED||0); - - var changes = ch.CHANGES || []; - if (changes.length === 0) { - diffBody.innerHTML += '
Нет изменений
'; - } else { - var tbl = '
'; - changes.forEach(function(c) { - var cls = c.CHANGE_TYPE === 'added' ? 'diff-added' : c.CHANGE_TYPE === 'deleted' ? 'diff-deleted' : c.CHANGE_TYPE === 'changed' ? 'diff-changed' : ''; - var vals = c.NEW_VALUES && Object.keys(c.NEW_VALUES).length > 0 ? c.NEW_VALUES : (c.OLD_VALUES || {}); - tbl += ''; - }); - tbl += '
УслугаЦенаОбъёмСуммаНачало
' + (c.ROW_NUM||'') + '' + (vals.NAME||'') + '' + _dc(c,'PRICE') + '' + _dc(c,'QTY') + '' + _dc(c,'SUM') + '' + (vals.DATE_START||'') + '
'; - diffBody.innerHTML += tbl; - } - }); - } else { - diffBody.innerHTML += '
✗ ' + (data.ERROR||'') + '
'; - } - } catch(e) { diffBody.innerHTML += '
✗ ' + e.message + '
'; } - - clearInterval(timerInterval); - diffStatus.textContent = '✓ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с'; - - btn.innerHTML = ' ✓ Готово'; - document.getElementById('chatCard').style.display = 'block'; - lucide.createIcons(); -}); - -function _dc(c, field) { - if (c.CHANGE_TYPE === 'changed' && c.OLD_VALUES && c.NEW_VALUES) { - var ov = (c.OLD_VALUES||{})[field], nv = (c.NEW_VALUES||{})[field]; - if (ov !== nv && ov !== undefined) - return '' + (ov||'—') + '' + (nv||'—') + ''; - } - var v = ((c.NEW_VALUES || c.OLD_VALUES || {})[field]); - return v != null ? v : '—'; -} - // ── Чат ────────────────────────────────────────────────────── document.getElementById('chatSend').addEventListener('click', function() { var input = document.getElementById('chatInput'); @@ -485,44 +483,101 @@ document.getElementById('chatInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') document.getElementById('chatSend').click(); }); -// ── Инфо ───────────────────────────────────────────────────── -function showInfo(i) { - var f = fileQueue[i]; - document.getElementById('modalTitle').textContent = f.name; - var html = '
Размер' + formatSize(f.size) + '
'; - html += '
Изменён' + formatDate(f.lastModified) + '
'; - if (f.parseInfo) { - var d = f.parseInfo; - html += '
Элементов' + (d.ELEMENT_COUNT||0) + '
'; - html += '
Параграфов' + (d.PARAGRAPHS||0) + '
'; - html += '
Таблиц' + (d.TABLES||0) + '
'; - html += '
Строк таблиц' + (d.TABLE_ROWS||0) + '
'; - if (d.TEXT_PREVIEW) { - html += '
Превью текста:
'; - html += '
' + d.TEXT_PREVIEW + '
'; - } - if (d.TABLE_HEADERS && d.TABLE_HEADERS.length > 0) { - html += '
Первые строки таблиц:
'; - d.TABLE_HEADERS.forEach(function(hdr, ti) { - html += '
Таблица ' + (ti+1) + ':
'; - html += '
' + (hdr||[]).map(function(c){return c||'(пусто)';}).join(' | ') + '
'; - }); - } - if (d.ERRORS && d.ERRORS.length > 0) { - html += '
Ошибки:
'; - html += '
' + d.ERRORS.join('\n') + '
'; - } - } - html += '
doc_id' + (f.doc_id || '—') + '
'; - document.getElementById('modalBody').innerHTML = html; - document.getElementById('modalOverlay').classList.add('open'); -} function closeModal(e) { if (e && e.target !== document.getElementById('modalOverlay')) return; + document.querySelector('.modal').classList.remove('wide'); document.getElementById('modalOverlay').classList.remove('open'); } -window.showInfo = showInfo; + +async function showText(i) { + var f = fileQueue[i]; + var docId = f.doc_id; + document.getElementById('modalTitle').textContent = f.name; + document.getElementById('modalBody').innerHTML = 'Загрузка...'; + var modal = document.querySelector('.modal'); + modal.classList.add('wide'); + document.getElementById('modalOverlay').classList.add('open'); + + var leftHtml = 'Загрузка...'; + var rightHtml = ''; + + if (docId) { + try { + var qResp = await fetch('/api.cfm?action=query&sql=SELECT%20elements_json%20FROM%20documents%20WHERE%20id%3D\'' + docId + '\''); + var qData = await qResp.json(); + var elements = []; + if (qData.OK && qData.ROWS && qData.ROWS.length > 0) { + var ej = qData.ROWS[0].ELEMENTS_JSON; + if (typeof ej === 'object' && ej.Value) ej = ej.Value; + elements = JSON.parse(ej); + } + + // Общий textify для обеих панелей + var textLines = []; + 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; + textLines.push('--- Таблица ' + rows.length + '×' + ncols + ' ---'); + 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,'>'); + + // Левая: сырой текст (textify) + leftHtml = '
' + textify + '
'; + + // Правая: статистика + textify + rightHtml += '
Размер' + formatSize(f.size) + '
'; + rightHtml += '
Изменён' + formatDate(f.lastModified) + '
'; + if (f.parseInfo) { + var d = f.parseInfo; + rightHtml += '
Элементов' + (d.ELEMENT_COUNT||0) + '
'; + rightHtml += '
Параграфов' + (d.PARAGRAPHS||0) + '
'; + rightHtml += '
Таблиц' + (d.TABLES||0) + '
'; + rightHtml += '
Строк таблиц' + (d.TABLE_ROWS||0) + '
'; + if (d.PAGES) rightHtml += '
Страниц' + d.PAGES + '
'; + if (d.PARSE_TIME_MS) { + rightHtml += '
Время парсинга' + (d.PARSE_TIME_MS > 1000 ? (d.PARSE_TIME_MS/1000).toFixed(1) + ' с' : d.PARSE_TIME_MS + ' мс') + '
'; + } + if (d.TEXT_LENGTH) rightHtml += '
Символов' + d.TEXT_LENGTH.toLocaleString() + '
'; + if (d.ERRORS && d.ERRORS.length > 0) { + rightHtml += '
Ошибки:
'; + rightHtml += '
' + d.ERRORS.join('\n') + '
'; + } + } + rightHtml += '
Все элементы:
'; + rightHtml += '
' + textify + '
'; + rightHtml += '
doc_id' + (f.doc_id || '—') + '
'; + if (f.supp_id) { + rightHtml += '
supp_id' + f.supp_id + '
'; + rightHtml += '
Тип ДС' + (f.supp_type || '—') + '
'; + } + } catch(e) { + leftHtml = 'Ошибка'; + } + } else { + leftHtml = '(ещё не загружен)'; + } + + var html = '
' + + '
Сырой текст
' + leftHtml + '
' + + '
Распарсено
' + rightHtml + '
' + + '
'; + document.getElementById('modalBody').innerHTML = html; +} + window.closeModal = closeModal; +window.showText = showText; // ── Промпт ─────────────────────────────────────────────────── var promptEditor = document.getElementById('promptEditor');