@@ -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 = '';
+ 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 += '| ' + 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;
+ }
+ };
- // Обновить информацию о 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 += '| ' + (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');
@@ -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');