v1.0.95: восстановление index.cfm + заголовки
This commit is contained in:
@@ -34,14 +34,15 @@
|
|||||||
.status-err { color: var(--destructive); }
|
.status-err { color: var(--destructive); }
|
||||||
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
|
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
|
||||||
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
|
.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 { cursor: pointer; color: #f87171; background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; }
|
||||||
.remove-btn:hover { color: var(--destructive); }
|
.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 { 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:hover { color: var(--brand-primary); }
|
||||||
.info-btn.visible { display: inline; }
|
.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 { 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-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 { 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-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 { padding: 16px; overflow-y: auto; }
|
||||||
.modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
|
.modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
|
||||||
@@ -56,6 +57,15 @@
|
|||||||
.diff-field-new { color: var(--green); font-weight: 600; }
|
.diff-field-new { color: var(--green); font-weight: 600; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@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; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -76,18 +86,14 @@
|
|||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Имя</th><th style="width:130px;">Изменён</th><th style="width:90px;">Размер</th><th style="width:115px;">Статус</th><th style="width:30px;">Базовый</th><th style="width:30px;"></th><th style="width:30px;"></th></tr>
|
<tr><th>Имя</th><th style="width:130px;">Изменён</th><th style="width:90px;">Размер</th><th style="width:115px;">Парсинг</th><th style="width:30px;">Базовый</th><th style="width:30px;">Текст</th><th style="width:30px;">✕</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="fileTable"></tbody>
|
<tbody id="fileTable"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" id="parseBtn" style="width:100%;justify-content:center;margin-top:12px;" disabled>
|
<button class="btn btn-primary" id="llmBtn" style="width:100%;justify-content:center;margin-top:8px;display:none;background:var(--green);border-color:var(--green);">
|
||||||
<i data-lucide="play" style="width:16px;height:16px;"></i> Парсинг
|
<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-primary" id="llmBtn" style="width:100%;justify-content:center;margin-top:8px;display:none;">
|
|
||||||
<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить (LLM)
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<details style="margin-top:8px;font-size:12px;">
|
<details style="margin-top:8px;font-size:12px;">
|
||||||
@@ -144,7 +150,6 @@ var CONVERT_URL = 'https://contracts.kube5s.ru/convert-doc';
|
|||||||
var SITE_URL = ''; // same origin for api calls
|
var SITE_URL = ''; // same origin for api calls
|
||||||
|
|
||||||
var fileInput = document.getElementById('fileInput');
|
var fileInput = document.getElementById('fileInput');
|
||||||
var parseBtn = document.getElementById('parseBtn');
|
|
||||||
var fileTable = document.getElementById('fileTable');
|
var fileTable = document.getElementById('fileTable');
|
||||||
var fileQueue = [];
|
var fileQueue = [];
|
||||||
var contractId = null;
|
var contractId = null;
|
||||||
@@ -165,7 +170,6 @@ function formatDate(ts) {
|
|||||||
function renderTable() {
|
function renderTable() {
|
||||||
if (fileQueue.length === 0) {
|
if (fileQueue.length === 0) {
|
||||||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf / .zip</td></tr>';
|
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf / .zip</td></tr>';
|
||||||
parseBtn.disabled = true;
|
|
||||||
} else {
|
} else {
|
||||||
fileTable.innerHTML = fileQueue.map(function(f, i) {
|
fileTable.innerHTML = fileQueue.map(function(f, i) {
|
||||||
var radio = '';
|
var radio = '';
|
||||||
@@ -178,7 +182,7 @@ function renderTable() {
|
|||||||
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
|
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
|
||||||
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
||||||
'<td style="text-align:center;">' + radio + '</td>' +
|
'<td style="text-align:center;">' + radio + '</td>' +
|
||||||
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" id="info_' + i + '" onclick="showInfo(' + i + ')" title="Инфо"><i data-lucide="info" style="width:16px;height:16px;"></i></button></td>' +
|
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" onclick="showText(' + i + ')" title="Текст / Распарсено"><i data-lucide="file-text" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
|
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -199,14 +203,23 @@ fileInput.addEventListener('change', async function() {
|
|||||||
if (newFiles.length === 0) return;
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
fileInput.disabled = true;
|
fileInput.disabled = true;
|
||||||
parseBtn.disabled = true;
|
|
||||||
parseBtn.innerHTML = '<span class="spinner"></span> Загрузка...';
|
|
||||||
|
|
||||||
for (var i = 0; i < newFiles.length; i++) {
|
for (var i = 0; i < newFiles.length; i++) {
|
||||||
var f = newFiles[i];
|
var f = newFiles[i];
|
||||||
var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' };
|
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);
|
fileQueue.push(entry);
|
||||||
var rowIdx = fileQueue.length - 1;
|
rowIdx = fileQueue.length - 1;
|
||||||
|
}
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -218,6 +231,26 @@ fileInput.addEventListener('change', async function() {
|
|||||||
fileQueue[rowIdx].doc_id = resp.DOC_ID;
|
fileQueue[rowIdx].doc_id = resp.DOC_ID;
|
||||||
fileQueue[rowIdx].status = '<span class="status-ok">✓</span>';
|
fileQueue[rowIdx].status = '<span class="status-ok">✓</span>';
|
||||||
fileQueue[rowIdx].uploaded = true;
|
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 = '<span class="status-ok">✓ ' + pd.ELEMENT_COUNT + ' эл. (' + elapsed + 'с)</span>';
|
||||||
|
} else {
|
||||||
|
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + (pd.ERROR_COUNT > 0 ? (pd.ERRORS && pd.ERRORS[0] || 'ошибка') : 'ошибка') + '</span>';
|
||||||
|
fileQueue[rowIdx].parseInfo = pd;
|
||||||
|
}
|
||||||
|
} catch(pe) {
|
||||||
|
fileQueue[rowIdx].status = '<span class="status-err">✗ парсинг: сеть</span>';
|
||||||
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
||||||
}
|
}
|
||||||
@@ -228,8 +261,10 @@ fileInput.addEventListener('change', async function() {
|
|||||||
|
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
fileInput.disabled = false;
|
fileInput.disabled = false;
|
||||||
parseBtn.disabled = (contractId === null);
|
// Показать кнопки LLM после авто-парсинга
|
||||||
parseBtn.innerHTML = '<i data-lucide="play" style="width:16px;height:16px;"></i> Парсинг';
|
var llmBtn = document.getElementById('llmBtn');
|
||||||
|
llmBtn.style.display = 'flex';
|
||||||
|
llmBtn.disabled = false;
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,42 +318,105 @@ function uploadFile(file, onProgress) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Парсинг ──────────────────────────────────────────────────
|
// ── LLM: Сравнить (Event Sourcing) ────────────────────────
|
||||||
parseBtn.addEventListener('click', async function() {
|
document.getElementById('llmBtn').addEventListener('click', function() {
|
||||||
if (!contractId) return;
|
if (!contractId) return;
|
||||||
parseBtn.disabled = true;
|
var btn = this;
|
||||||
parseBtn.innerHTML = '<span class="spinner"></span> Парсинг...';
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner"></span> Сравнение...';
|
||||||
|
|
||||||
for (var i = 0; i < fileQueue.length; i++) {
|
var diffCard = document.getElementById('diffCard');
|
||||||
var f = fileQueue[i];
|
var diffBody = document.getElementById('diffBody');
|
||||||
if (!f.doc_id || f.parsed) continue;
|
var diffStatus = document.getElementById('diffStatus');
|
||||||
f.status = '⏳';
|
diffCard.style.display = 'block';
|
||||||
renderTable();
|
diffBody.innerHTML = '';
|
||||||
try {
|
diffStatus.textContent = '⏳ 0.0с';
|
||||||
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 = '<span class="status-ok">✓ ' + d.ELEMENT_COUNT + ' эл.</span>';
|
|
||||||
} else {
|
|
||||||
f.status = '<span class="status-err">✗ ' + (d.ERROR_COUNT > 0 ? (d.ERRORS && d.ERRORS[0] || 'ошибка') : 'ошибка') + '</span>';
|
|
||||||
f.parseInfo = d;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
f.status = '<span class="status-err">✗ сеть</span>';
|
|
||||||
}
|
|
||||||
renderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить информацию о supplement_id/type для radio
|
var llmStart = Date.now();
|
||||||
await refreshSupps();
|
var timerInterval = setInterval(function() {
|
||||||
|
diffStatus.textContent = '⏳ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с';
|
||||||
|
}, 200);
|
||||||
|
|
||||||
parseBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
|
var es = new EventSource('https://contracts.kube5s.ru/process-v2?contract_id=' + contractId);
|
||||||
var llmBtn = document.getElementById('llmBtn');
|
var sections = {}; // supplement_id → DOM element
|
||||||
llmBtn.style.display = 'flex';
|
|
||||||
llmBtn.disabled = false;
|
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 = '<div class="diff-section-header" style="padding:8px 12px;background:var(--brand-grey-light);cursor:pointer;font-size:13px;font-weight:600;">⏳ ' + d.filename + '</div><div class="diff-section-body" style="display:none;padding:8px 12px;"></div>';
|
||||||
|
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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = '<div style="font-size:12px;margin-bottom:6px;">+' + (s.added||0) + ' ~' + (s.updated||0) + ' -' + (s.deleted||0) + ' ?' + (s.unresolved||0) + '</div>';
|
||||||
|
html += '<div class="table-wrap"><table style="font-size:11px;"><thead><tr><th>Действие</th><th>Услуга</th><th>Цена</th><th>Кол-во</th><th>Сумма</th><th>Дата</th></tr></thead><tbody>';
|
||||||
|
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 += '<tr class="' + cls + '"><td>' + action + '</td><td>' + name + '</td><td class="num-cell">' + price + '</td><td class="num-cell">' + qty + '</td><td class="num-cell">' + sum + '</td><td>' + ds + '</td></tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
body.innerHTML = html;
|
||||||
|
body.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (d.type === 'extract_error' || d.type === 'apply_error') {
|
||||||
|
diffBody.innerHTML += '<div style="font-size:12px;color:var(--destructive);">✗ ' + d.filename + ': ' + d.error + '</div>';
|
||||||
|
}
|
||||||
|
else if (d.type === 'done') {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
es.close();
|
||||||
|
diffStatus.textContent = '✓ ' + d.total_time_s + 'с';
|
||||||
|
btn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
|
||||||
|
if (d.total_unresolved > 0) {
|
||||||
|
diffBody.innerHTML += '<div style="margin-top:8px;color:#eab308;">⚠ ' + d.total_unresolved + ' неразрешённых строк — <a href="/resolve.cfm?contract_id=' + contractId + '">разобрать</a></div>';
|
||||||
|
}
|
||||||
|
document.getElementById('chatCard').style.display = 'block';
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
else if (d.type === 'error') {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
es.close();
|
||||||
|
diffBody.innerHTML += '<div style="color:var(--destructive);margin-top:8px;">✗ ' + d.message + '</div>';
|
||||||
|
btn.innerHTML = '<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
es.onerror = function() {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
es.close();
|
||||||
|
if (diffBody.innerHTML === '') {
|
||||||
|
diffBody.innerHTML = '<div style="color:var(--destructive);">✗ Ошибка соединения</div>';
|
||||||
|
}
|
||||||
|
btn.innerHTML = '<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить';
|
||||||
|
btn.disabled = false;
|
||||||
|
lucide.createIcons();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Обновить supplement_id/type для radio ──────────────────
|
// ── Обновить supplement_id/type для radio ──────────────────
|
||||||
@@ -352,106 +450,6 @@ function setInitial(suppId) {
|
|||||||
}
|
}
|
||||||
window.setInitial = setInitial;
|
window.setInitial = setInitial;
|
||||||
|
|
||||||
// ── LLM: Сравнить ────────────────────────────────────────────
|
|
||||||
document.getElementById('llmBtn').addEventListener('click', async function() {
|
|
||||||
if (!contractId) return;
|
|
||||||
var btn = this;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<span class="spinner"></span> 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 += '<div style="font-size:12px;color:' + (ed.SKIPPED ? 'var(--muted)' : 'var(--green)') + ';">' + info + '</div>';
|
|
||||||
} else {
|
|
||||||
diffBody.innerHTML += '<div style="font-size:12px;color:var(--destructive);">✗ ' + (ed.ERROR||'') + '</div>';
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
// 2. Differ
|
|
||||||
diffBody.innerHTML += '<div style="margin-top:8px;font-weight:600;">📊 Сравнение:</div>';
|
|
||||||
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 += '<div style="font-weight:600;margin-top:12px;">📄 Допник (' + ch.TYPE + ')</div>';
|
|
||||||
diffBody.innerHTML += '<div style="font-size:12px;margin-left:8px;"><span style="color:var(--green);">+' + (s.ADDED||0) + '</span> <span style="color:#eab308;">~' + (s.CHANGED||0) + '</span> <span style="color:var(--destructive);">-' + (s.DELETED||0) + '</span></div>';
|
|
||||||
diffStatus.textContent = 'изм:' + (s.CHANGED||0) + ' +' + (s.ADDED||0) + ' -' + (s.DELETED||0);
|
|
||||||
|
|
||||||
var changes = ch.CHANGES || [];
|
|
||||||
if (changes.length === 0) {
|
|
||||||
diffBody.innerHTML += '<div style="color:var(--muted);font-size:12px;">Нет изменений</div>';
|
|
||||||
} else {
|
|
||||||
var tbl = '<div class="table-wrap" style="margin-top:4px;"><table><thead><tr><th>№</th><th>Услуга</th><th>Цена</th><th>Объём</th><th>Сумма</th><th>Начало</th></tr></thead><tbody>';
|
|
||||||
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 += '<tr class="' + cls + '"><td>' + (c.ROW_NUM||'') + '</td><td>' + (vals.NAME||'') + '</td><td class="num-cell">' + _dc(c,'PRICE') + '</td><td class="num-cell">' + _dc(c,'QTY') + '</td><td class="num-cell">' + _dc(c,'SUM') + '</td><td>' + (vals.DATE_START||'') + '</td></tr>';
|
|
||||||
});
|
|
||||||
tbl += '</tbody></table></div>';
|
|
||||||
diffBody.innerHTML += tbl;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
diffBody.innerHTML += '<div style="color:var(--destructive);">✗ ' + (data.ERROR||'') + '</div>';
|
|
||||||
}
|
|
||||||
} catch(e) { diffBody.innerHTML += '<div style="color:var(--destructive);">✗ ' + e.message + '</div>'; }
|
|
||||||
|
|
||||||
clearInterval(timerInterval);
|
|
||||||
diffStatus.textContent = '✓ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с';
|
|
||||||
|
|
||||||
btn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
|
|
||||||
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 '<span class="diff-field-old">' + (ov||'—') + '</span> → <span class="diff-field-new">' + (nv||'—') + '</span>';
|
|
||||||
}
|
|
||||||
var v = ((c.NEW_VALUES || c.OLD_VALUES || {})[field]);
|
|
||||||
return v != null ? v : '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Чат ──────────────────────────────────────────────────────
|
// ── Чат ──────────────────────────────────────────────────────
|
||||||
document.getElementById('chatSend').addEventListener('click', function() {
|
document.getElementById('chatSend').addEventListener('click', function() {
|
||||||
var input = document.getElementById('chatInput');
|
var input = document.getElementById('chatInput');
|
||||||
@@ -485,44 +483,101 @@ document.getElementById('chatInput').addEventListener('keydown', function(e) {
|
|||||||
if (e.key === 'Enter') document.getElementById('chatSend').click();
|
if (e.key === 'Enter') document.getElementById('chatSend').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Инфо ─────────────────────────────────────────────────────
|
|
||||||
function showInfo(i) {
|
|
||||||
var f = fileQueue[i];
|
|
||||||
document.getElementById('modalTitle').textContent = f.name;
|
|
||||||
var html = '<div class="kv"><span class="k">Размер</span><span class="v">' + formatSize(f.size) + '</span></div>';
|
|
||||||
html += '<div class="kv"><span class="k">Изменён</span><span class="v">' + formatDate(f.lastModified) + '</span></div>';
|
|
||||||
if (f.parseInfo) {
|
|
||||||
var d = f.parseInfo;
|
|
||||||
html += '<div class="kv"><span class="k">Элементов</span><span class="v">' + (d.ELEMENT_COUNT||0) + '</span></div>';
|
|
||||||
html += '<div class="kv"><span class="k">Параграфов</span><span class="v">' + (d.PARAGRAPHS||0) + '</span></div>';
|
|
||||||
html += '<div class="kv"><span class="k">Таблиц</span><span class="v">' + (d.TABLES||0) + '</span></div>';
|
|
||||||
html += '<div class="kv"><span class="k">Строк таблиц</span><span class="v">' + (d.TABLE_ROWS||0) + '</span></div>';
|
|
||||||
if (d.TEXT_PREVIEW) {
|
|
||||||
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Превью текста:</div>';
|
|
||||||
html += '<pre>' + d.TEXT_PREVIEW + '</pre>';
|
|
||||||
}
|
|
||||||
if (d.TABLE_HEADERS && d.TABLE_HEADERS.length > 0) {
|
|
||||||
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Первые строки таблиц:</div>';
|
|
||||||
d.TABLE_HEADERS.forEach(function(hdr, ti) {
|
|
||||||
html += '<div style="margin-top:4px;font-size:11px;color:var(--muted);">Таблица ' + (ti+1) + ':</div>';
|
|
||||||
html += '<pre>' + (hdr||[]).map(function(c){return c||'(пусто)';}).join(' | ') + '</pre>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (d.ERRORS && d.ERRORS.length > 0) {
|
|
||||||
html += '<div style="margin-top:8px;color:var(--destructive);font-weight:600;">Ошибки:</div>';
|
|
||||||
html += '<pre>' + d.ERRORS.join('\n') + '</pre>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '<div class="kv"><span class="k">doc_id</span><span class="v">' + (f.doc_id || '—') + '</span></div>';
|
|
||||||
document.getElementById('modalBody').innerHTML = html;
|
|
||||||
document.getElementById('modalOverlay').classList.add('open');
|
|
||||||
}
|
|
||||||
function closeModal(e) {
|
function closeModal(e) {
|
||||||
if (e && e.target !== document.getElementById('modalOverlay')) return;
|
if (e && e.target !== document.getElementById('modalOverlay')) return;
|
||||||
|
document.querySelector('.modal').classList.remove('wide');
|
||||||
document.getElementById('modalOverlay').classList.remove('open');
|
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 = '<span style="color:var(--muted);">Загрузка...</span>';
|
||||||
|
var modal = document.querySelector('.modal');
|
||||||
|
modal.classList.add('wide');
|
||||||
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
|
|
||||||
|
var leftHtml = '<span style="color:var(--muted);">Загрузка...</span>';
|
||||||
|
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,'<').replace(/>/g,'>');
|
||||||
|
|
||||||
|
// Левая: сырой текст (textify)
|
||||||
|
leftHtml = '<pre>' + textify + '</pre>';
|
||||||
|
|
||||||
|
// Правая: статистика + textify
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Размер</span><span class="v">' + formatSize(f.size) + '</span></div>';
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Изменён</span><span class="v">' + formatDate(f.lastModified) + '</span></div>';
|
||||||
|
if (f.parseInfo) {
|
||||||
|
var d = f.parseInfo;
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Элементов</span><span class="v">' + (d.ELEMENT_COUNT||0) + '</span></div>';
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Параграфов</span><span class="v">' + (d.PARAGRAPHS||0) + '</span></div>';
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Таблиц</span><span class="v">' + (d.TABLES||0) + '</span></div>';
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Строк таблиц</span><span class="v">' + (d.TABLE_ROWS||0) + '</span></div>';
|
||||||
|
if (d.PAGES) rightHtml += '<div class="kv"><span class="k">Страниц</span><span class="v">' + d.PAGES + '</span></div>';
|
||||||
|
if (d.PARSE_TIME_MS) {
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Время парсинга</span><span class="v">' + (d.PARSE_TIME_MS > 1000 ? (d.PARSE_TIME_MS/1000).toFixed(1) + ' с' : d.PARSE_TIME_MS + ' мс') + '</span></div>';
|
||||||
|
}
|
||||||
|
if (d.TEXT_LENGTH) rightHtml += '<div class="kv"><span class="k">Символов</span><span class="v">' + d.TEXT_LENGTH.toLocaleString() + '</span></div>';
|
||||||
|
if (d.ERRORS && d.ERRORS.length > 0) {
|
||||||
|
rightHtml += '<div style="margin-top:8px;color:var(--destructive);font-weight:600;">Ошибки:</div>';
|
||||||
|
rightHtml += '<pre>' + d.ERRORS.join('\n') + '</pre>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rightHtml += '<div style="margin-top:10px;font-weight:600;font-size:12px;">Все элементы:</div>';
|
||||||
|
rightHtml += '<pre>' + textify + '</pre>';
|
||||||
|
rightHtml += '<div class="kv" style="margin-top:10px;"><span class="k">doc_id</span><span class="v">' + (f.doc_id || '—') + '</span></div>';
|
||||||
|
if (f.supp_id) {
|
||||||
|
rightHtml += '<div class="kv"><span class="k">supp_id</span><span class="v">' + f.supp_id + '</span></div>';
|
||||||
|
rightHtml += '<div class="kv"><span class="k">Тип ДС</span><span class="v">' + (f.supp_type || '—') + '</span></div>';
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
leftHtml = '<span style="color:var(--destructive);">Ошибка</span>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
leftHtml = '<span style="color:var(--muted);">(ещё не загружен)</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<div class="text-panes">' +
|
||||||
|
'<div class="text-pane"><div class="pane-title">Сырой текст</div>' + leftHtml + '</div>' +
|
||||||
|
'<div class="text-pane"><div class="pane-title">Распарсено</div>' + rightHtml + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.getElementById('modalBody').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
window.closeModal = closeModal;
|
window.closeModal = closeModal;
|
||||||
|
window.showText = showText;
|
||||||
|
|
||||||
// ── Промпт ───────────────────────────────────────────────────
|
// ── Промпт ───────────────────────────────────────────────────
|
||||||
var promptEditor = document.getElementById('promptEditor');
|
var promptEditor = document.getElementById('promptEditor');
|
||||||
|
|||||||
Reference in New Issue
Block a user