diff --git a/index.cfm b/index.cfm index 45675c5..4855ef4 100644 --- a/index.cfm +++ b/index.cfm @@ -34,15 +34,14 @@ .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: #f87171; background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; } - .remove-btn:hover { color: #ef4444; } + .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); } .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; } @@ -57,28 +56,19 @@ .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; }
Nubes - Сверка договоров — LLM v1.0.91 — Lucee + Сверка договоров — LLM AI-driven Event Sourcing v1.0.92 — Lucee
- Загрузка договоров приложений спецификаций + Загрузка договоров/приложений/спецификаций
@@ -86,14 +76,18 @@
- +
ИмяИзменёнРазмерПарсингБазовыйТекст
ИмяИзменёнРазмерСтатусБазовый
- + +
@@ -150,6 +144,7 @@ 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; @@ -170,6 +165,7 @@ 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 = ''; @@ -182,7 +178,7 @@ function renderTable() { '' + formatSize(f.size) + '' + '' + (f.status || '') + '' + '' + radio + '' + - '' + + '' + '' + ''; }).join(''); @@ -203,23 +199,14 @@ 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%' }; - // Заменить старый файл с тем же именем, если есть - 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; - } + fileQueue.push(entry); + var rowIdx = fileQueue.length - 1; renderTable(); try { @@ -231,26 +218,6 @@ 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 + ''; } @@ -261,10 +228,8 @@ fileInput.addEventListener('change', async function() { fileInput.value = ''; fileInput.disabled = false; - // Показать кнопки LLM после авто-парсинга - var llmBtn = document.getElementById('llmBtn'); - llmBtn.style.display = 'flex'; - llmBtn.disabled = false; + parseBtn.disabled = (contractId === null); + parseBtn.innerHTML = ' Парсинг'; lucide.createIcons(); }); @@ -318,105 +283,42 @@ function uploadFile(file, onProgress) { }); } -// ── LLM: Сравнить (Event Sourcing) ──────────────────────── -document.getElementById('llmBtn').addEventListener('click', function() { +// ── Парсинг ────────────────────────────────────────────────── +parseBtn.addEventListener('click', async function() { if (!contractId) return; - var btn = this; - btn.disabled = true; - btn.innerHTML = ' Сравнение...'; + parseBtn.disabled = true; + parseBtn.innerHTML = ' Парсинг...'; - 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; - } - 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)'; + 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 = '✗ сеть'; } - 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; - } - }; + renderTable(); + } - es.onerror = function() { - clearInterval(timerInterval); - es.close(); - if (diffBody.innerHTML === '') { - diffBody.innerHTML = '
✗ Ошибка соединения
'; - } - btn.innerHTML = ' Сравнить'; - btn.disabled = false; - lucide.createIcons(); - }; + // Обновить информацию о supplement_id/type для radio + await refreshSupps(); + + parseBtn.innerHTML = ' ✓ Готово'; + var llmBtn = document.getElementById('llmBtn'); + llmBtn.style.display = 'flex'; + llmBtn.disabled = false; + lucide.createIcons(); }); // ── Обновить supplement_id/type для radio ────────────────── @@ -450,6 +352,106 @@ 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'); @@ -483,101 +485,44 @@ 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'); } - -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.showInfo = showInfo; window.closeModal = closeModal; -window.showText = showText; // ── Промпт ─────────────────────────────────────────────────── var promptEditor = document.getElementById('promptEditor');