v3.2.1: /llm — отдельная страница, / — без LLM

This commit is contained in:
2026-06-18 09:01:30 +04:00
parent e534d014ef
commit f1495d07e3
3 changed files with 536 additions and 160 deletions
+4 -159
View File
@@ -54,19 +54,12 @@
.modal-body .kv .k { color: var(--muted); width: 110px; flex-shrink: 0; }
.modal-body .kv .v { word-break: break-all; }
.modal-body pre { background: var(--brand-grey-light); padding: 10px; border-radius: 6px; font-size: 12px; max-height: 200px; overflow-y: auto; white-space: pre-wrap; margin-top: 4px; }
.diff-added { background: rgba(34,197,94,.1); }
.diff-deleted { background: rgba(239,68,68,.1); text-decoration: line-through; }
.diff-changed { background: rgba(234,179,8,.1); }
.diff-added td { color: var(--green); }
.diff-deleted td { color: var(--destructive); }
.diff-field-old { color: var(--destructive); text-decoration: line-through; font-size: 11px; }
.diff-field-new { color: var(--green); font-weight: 600; }
</style>
</head>
<body>
<div class="topbar">
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v3.2.0</span></span>
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v3.0.1</span></span>
</div>
<div class="content">
@@ -90,21 +83,8 @@
<button class="btn btn-primary" id="parseBtn" style="width:100%;justify-content:center;margin-top:12px;" disabled>
<i data-lucide="play" 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> Сравнить
</button>
</div>
</div>
<!-- Результаты сравнения -->
<div class="card" id="diffCard" style="display:none;">
<div class="card-header">
<i data-lucide="file-diff" style="width:18px;height:18px;"></i>
Результаты сравнения <span id="diffStatus" style="font-weight:400;font-size:12px;margin-left:8px;"></span>
</div>
<div class="card-body" id="diffBody"></div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
@@ -179,7 +159,6 @@
function uploadFile(file, onProgress) {
return new Promise(function(resolve, reject) {
var startTime = Date.now();
var fd = new FormData();
fd.append('files', file);
if (contractId) fd.append('cid', contractId);
@@ -188,7 +167,7 @@
xhr.open('POST', '/');
xhr.upload.onprogress = function(e) {
if (e.lengthComputable && onProgress) {
onProgress({pct: Math.round(e.loaded / e.total * 100), elapsed: (Date.now() - startTime) / 1000});
onProgress(Math.round(e.loaded / e.total * 100));
}
};
xhr.onload = function() {
@@ -226,8 +205,8 @@
var startTime = Date.now();
try {
var resp = await uploadFile(f, function(info) {
fileQueue[rowIdx].status = '<span style="color:var(--muted);">↑ ' + info.elapsed.toFixed(1) + 'с ' + info.pct + '%</span>';
var resp = await uploadFile(f, function(pct) {
fileQueue[rowIdx].status = '<span style="color:var(--muted);">↑ ' + pct + '%</span>';
renderTable();
});
if (resp && resp.contract_id) contractId = resp.contract_id;
@@ -338,12 +317,6 @@
es.close();
parseBtn.disabled = true;
parseBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
// Показать кнопку «Сравнить»
var llmBtn = document.getElementById('llmBtn');
llmBtn.style.display = 'block';
llmBtn.disabled = false;
llmBtn.innerHTML = '<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить';
lucide.createIcons();
var summaryRow = document.createElement('tr');
@@ -436,134 +409,6 @@
window.showInfo = showInfo;
window.closeModal = closeModal;
// ── LLM: Сравнить ────────────────────────────────────────────
document.getElementById('llmBtn').addEventListener('click', function() {
if (!contractId) return;
var llmBtn = document.getElementById('llmBtn');
llmBtn.disabled = true;
llmBtn.innerHTML = '<span style="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;"></span> Анализ...';
var diffCard = document.getElementById('diffCard');
var diffBody = document.getElementById('diffBody');
var diffStatus = document.getElementById('diffStatus');
diffCard.style.display = 'block';
diffBody.innerHTML = '<span style="color:var(--muted);">⏳ LLM-извлечение...</span>';
var es = new EventSource('/llm/process/' + contractId);
var filesDone = 0;
es.onmessage = function(e) {
var d = JSON.parse(e.data);
if (d.type === 'extract_start') {
diffBody.innerHTML += '<div style="font-size:12px;color:var(--muted);">🔍 ' + d.name + ' — извлечение...</div>';
}
else if (d.type === 'extract_done') {
filesDone++;
diffStatus.textContent = '✓ ' + filesDone + ' файлов, ' + d.count + ' строк';
diffBody.innerHTML += '<div style="font-size:12px;color:var(--green);">✓ ' + d.name + ' — ' + d.count + ' строк (' + d.time_s + 'с)</div>';
}
else if (d.type === 'extract_skip') {
filesDone++;
diffBody.innerHTML += '<div style="font-size:12px;color:var(--muted);">⏭ ' + d.name + ' — уже извлечено (' + d.count + ' строк)</div>';
}
else if (d.type === 'extract_error') {
diffBody.innerHTML += '<div style="font-size:12px;color:var(--destructive);">✗ ' + d.name + ': ' + d.error + '</div>';
}
else if (d.type === 'diff_start') {
diffBody.innerHTML += '<div style="margin-top:8px;font-weight:600;">📊 Сравнение допников:</div>';
}
else if (d.type === 'diff_file') {
var added = d.summary ? (d.summary.added || 0) : 0;
var changed = d.summary ? (d.summary.changed || 0) : 0;
var deleted = d.summary ? (d.summary.deleted || 0) : 0;
diffBody.innerHTML += '<div style="font-size:12px;"> ' + d.name + ': <span style="color:var(--green);">+' + added + '</span> <span style="color:#eab308;">~' + changed + '</span> <span style="color:var(--destructive);">-' + deleted + '</span></div>';
}
else if (d.type === 'done') {
es.close();
llmBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
lucide.createIcons();
diffStatus.textContent = '✓ ' + d.total_time_s + 'с';
// Построить таблицу изменений
var all = d.all_changes || [];
if (all.length === 0) {
diffBody.innerHTML += '<div style="color:var(--muted);margin-top:8px;">Нет допников для сравнения.</div>';
return;
}
all.forEach(function(ch) {
diffBody.innerHTML += '<div style="font-weight:600;margin-top:12px;">📄 ' + ch.filename + ' (' + ch.type + ')</div>';
if (!ch.changes || ch.changes.length === 0) {
diffBody.innerHTML += '<div style="color:var(--muted);font-size:12px;">Нет изменений</div>';
return;
}
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>';
ch.changes.forEach(function(c) {
var cls = '';
if (c.change_type === 'added') cls = 'diff-added';
else if (c.change_type === 'deleted') cls = 'diff-deleted';
else if (c.change_type === 'changed') cls = 'diff-changed';
var vals = c.new_values || c.old_values || {};
var price = _diffCell(c, 'price');
var qty = _diffCell(c, 'qty');
var sum = _diffCell(c, 'sum');
tbl += '<tr class="' + cls + '">' +
'<td>' + (c.row_num || '') + '</td>' +
'<td>' + (vals.name || '') + '</td>' +
'<td class="num-cell">' + price + '</td>' +
'<td class="num-cell">' + qty + '</td>' +
'<td class="num-cell">' + sum + '</td>' +
'<td>' + (vals.date_start || '') + '</td>' +
'</tr>';
});
tbl += '</tbody></table></div>';
diffBody.innerHTML += tbl;
});
}
else if (d.type === 'error') {
diffBody.innerHTML += '<div style="color:var(--destructive);margin-top:8px;">✗ ' + d.message + '</div>';
}
};
es.addEventListener('error', function() {
es.close();
llmBtn.innerHTML = '<i data-lucide="scale" style="width:16px;height:16px;"></i> Сравнить';
llmBtn.disabled = false;
lucide.createIcons();
});
});
function _diffCell(c, field) {
if (c.change_type === 'changed' && c.old_values && c.new_values) {
var ov = c.old_values[field];
var nv = c.new_values[field];
if (ov !== nv && ov !== undefined) {
return '<span class="diff-field-old">' + (ov !== null ? ov : '—') + '</span> → <span class="diff-field-new">' + (nv !== null ? nv : '—') + '</span>';
}
}
var vals = c.new_values || c.old_values || {};
var v = vals[field];
return v !== null && v !== undefined ? v : '—';
}
var style = document.createElement('style');
style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
document.head.appendChild(style);