v1.0.163: classify UI — batch_id, Классифицировать button, group cards

This commit is contained in:
2026-06-24 08:27:29 +04:00
parent eb98790802
commit 8a5d93a5c7
2 changed files with 85 additions and 2 deletions
+84 -1
View File
@@ -11,6 +11,7 @@ var fileInput = document.getElementById('fileInput');
var fileTable = document.getElementById('fileTable'); var fileTable = document.getElementById('fileTable');
var fileQueue = []; var fileQueue = [];
var contractId = null; var contractId = null;
var batchId = crypto.randomUUID(); // классификация: привязка всех файлов сессии
function formatSize(bytes) { function formatSize(bytes) {
if (!bytes || bytes === 0) return '—'; if (!bytes || bytes === 0) return '—';
@@ -207,13 +208,94 @@ fileInput.addEventListener('change', async function() {
fileInput.value = ''; fileInput.value = '';
fileInput.disabled = false; fileInput.disabled = false;
// Показать кнопки LLM после авто-парсинга // Показать кнопки после авто-парсинга
var llmBtn = document.getElementById('llmBtn'); var llmBtn = document.getElementById('llmBtn');
llmBtn.style.display = 'flex'; llmBtn.style.display = 'flex';
llmBtn.disabled = false; llmBtn.disabled = false;
showClassifyBtn();
lucide.createIcons(); lucide.createIcons();
}); });
// ── Классификация ──────────────────────────────────────────
function showClassifyBtn() {
var btn = document.getElementById('classifyBtn');
if (!btn) {
btn = document.createElement('button');
btn.id = 'classifyBtn';
btn.className = 'btn btn-primary';
btn.style.cssText = 'width:100%;justify-content:center;margin-top:8px;background:var(--brand-primary);border-color:var(--brand-primary);';
btn.innerHTML = '<i data-lucide="tags" style="width:16px;height:16px;"></i> Классифицировать';
btn.addEventListener('click', runClassify);
document.getElementById('llmBtn').parentNode.insertBefore(btn, document.getElementById('llmBtn'));
}
btn.style.display = 'flex';
btn.disabled = false;
}
async function runClassify() {
var btn = document.getElementById('classifyBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Классификация...';
try {
var resp = await fetch(VM_API + '/api/classify-batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({batch_id: batchId})
});
var data = await resp.json();
if (data.ok) {
btn.innerHTML = '✓ ' + data.done + '/' + data.total + ' классифицировано';
loadGroups();
} else {
btn.innerHTML = '✗ ' + (data.error || 'ошибка');
btn.disabled = false;
}
} catch(e) {
btn.innerHTML = '✗ ' + e.message;
btn.disabled = false;
}
}
async function loadGroups() {
var resp = await fetch(VM_API + '/api/groups?batch=' + batchId);
var data = await resp.json();
if (!data.ok || !data.groups) return;
var diffBody = document.getElementById('diffBody');
var diffCard = document.getElementById('diffCard');
diffCard.style.display = 'block';
diffBody.innerHTML = '<div style="font-weight:600;margin-bottom:8px;">📋 Найдено групп: ' + data.groups.length + '</div>';
data.groups.forEach(function(g, gi) {
var isUnresolved = g.contract_number === '__unresolved__';
var card = document.createElement('div');
card.style.cssText = 'border:1px solid ' + (isUnresolved ? 'var(--destructive)' : 'var(--brand-gray)') + ';border-radius:6px;padding:10px;margin-bottom:8px;';
card.innerHTML = '<div style="font-weight:600;margin-bottom:6px;">' +
(isUnresolved ? '❓ Не распознано' : '📄 ' + (g.contract_number || 'Без номера') + ' — ' + (g.counterparty || 'контрагент не определён')) +
'</div>' +
'<div style="font-size:12px;">' +
g.documents.map(function(d) {
return '<div style="padding:2px 0;">' +
'<span style="color:var(--muted);">[' + (d.doc_type || '?') + ']</span> ' +
d.filename +
(d.doc_date ? ' <span style="color:var(--muted);">' + d.doc_date + '</span>' : '') +
'</div>';
}).join('') +
'</div>';
if (!isUnresolved) {
card.innerHTML += '<button class="btn" style="margin-top:6px;font-size:12px;height:28px;" onclick="runCompareForGroup(\'' + gi + '\')">▶ Сравнить</button>';
}
diffBody.appendChild(card);
});
lucide.createIcons();
}
window.runCompareForGroup = function(gi) {
// TODO: run /process-v2 for the specific contract
alert('Сравнение для группы ' + gi + ' — в разработке');
};
function uploadFile(file, onProgress) { function uploadFile(file, onProgress) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
// .doc → конвертация в docx // .doc → конвертация в docx
@@ -226,6 +308,7 @@ function uploadFile(file, onProgress) {
var fd = new FormData(); var fd = new FormData();
fd.append('files', uploadFile, uploadName); fd.append('files', uploadFile, uploadName);
if (contractId) fd.append('contract_id', contractId); if (contractId) fd.append('contract_id', contractId);
fd.append('batch_id', batchId);
xhr.open('POST', UPLOAD_URL); xhr.open('POST', UPLOAD_URL);
xhr.upload.onprogress = function(e) { xhr.upload.onprogress = function(e) {
if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 100)); if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 100));
+1 -1
View File
@@ -75,7 +75,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="/nubes-logo.svg" alt="Nubes"> <img src="/nubes-logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.162 — Lucee</span></span> <span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.163 — Lucee</span></span>
</div> </div>
<div class="content"> <div class="content">