@@ -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 = '';
- 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 += '| ' + action + ' | ' + name + ' | ' + price + ' | ' + qty + ' | ' + sum + ' | ' + ds + ' |
';
- });
- html += '
';
- 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 += '| ' + (c.ROW_NUM||'') + ' | ' + (vals.NAME||'') + ' | ' + _dc(c,'PRICE') + ' | ' + _dc(c,'QTY') + ' | ' + _dc(c,'SUM') + ' | ' + (vals.DATE_START||'') + ' |
';
+ });
+ tbl += '
';
+ 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');