282 lines
12 KiB
Plaintext
282 lines
12 KiB
Plaintext
<cfsetting showdebugoutput="no" enablecfoutputonly="no">
|
||
<cfheader name="Content-Type" value="text/html; charset=utf-8">
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Сверка договоров — Lucee v1.0.18</title>
|
||
<style>
|
||
:root { --brand: #2563eb; --brand-dark: #1d4ed8; --gray: #d1d5db; --gray-light: #f3f4f6; --bg: #fff; --fg: #1a1a1a; --muted: #6b7280; --red: #ef4444; --green: #22c55e; --yellow: #eab308; }
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--fg); background: var(--gray-light); min-height: 100vh; }
|
||
.topbar { background: var(--bg); border-bottom: 1px solid var(--gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 12px; }
|
||
.topbar .title { font-weight: 600; font-size: 16px; }
|
||
.content { max-width: 900px; margin: 32px auto; padding: 0 16px; }
|
||
.card { background: var(--bg); border-radius: 12px; border: 1px solid var(--gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); margin-bottom: 16px; }
|
||
.card-header { background: var(--gray-light); padding: 12px 16px; font-weight: 600; font-size: 15px; }
|
||
.card-body { padding: 16px; }
|
||
.btn { height: 32px; border-radius: 6px; padding: 0 12px; font-size: 13px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--gray); background: var(--bg); }
|
||
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
|
||
.btn-primary:hover { background: var(--brand-dark); }
|
||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
.btn-sm { height: 26px; font-size: 12px; padding: 0 8px; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th { background: var(--gray-light); padding: 6px 8px; border: 1px solid var(--gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); }
|
||
td { padding: 6px 8px; border: 1px solid var(--gray); }
|
||
.diff-added td { background: rgba(34,197,94,.08); }
|
||
.diff-deleted td { background: rgba(239,68,68,.08); text-decoration: line-through; color: var(--red); }
|
||
.diff-changed td { background: rgba(234,179,8,.08); }
|
||
input[type=file] { width: 100%; margin-bottom: 8px; }
|
||
input[type=text] { width: 100%; height: 32px; border: 1px solid var(--gray); border-radius: 6px; padding: 0 8px; font-size: 13px; }
|
||
.flex-row { display: flex; gap: 8px; align-items: center; }
|
||
.flex-grow { flex: 1; }
|
||
.msg { font-size: 13px; padding: 4px 0; }
|
||
.msg-green { color: var(--green); }
|
||
.msg-red { color: var(--red); }
|
||
#log { font-size: 12px; color: var(--muted); max-height: 200px; overflow-y: auto; background: var(--gray-light); padding: 8px; border-radius: 6px; margin-top: 8px; white-space: pre-wrap; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<span class="title">Сверка договоров</span>
|
||
<span style="color:var(--muted);font-size:12px;">v1.0.18 — Lucee</span>
|
||
</div>
|
||
<div class="content">
|
||
|
||
<!-- Загрузка -->
|
||
<div class="card">
|
||
<div class="card-header">📁 Загрузка</div>
|
||
<div class="card-body">
|
||
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple>
|
||
<div id="fileList" style="margin-bottom:8px;"></div>
|
||
<div class="flex-row">
|
||
<button class="btn btn-primary" id="btnUpload">Загрузить</button>
|
||
<span id="uploadStatus" class="msg"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Файлы -->
|
||
<div class="card" id="filesCard" style="display:none;">
|
||
<div class="card-header">📋 Файлы договора <span id="contractLabel" style="color:var(--muted);font-weight:400;"></span></div>
|
||
<div class="card-body">
|
||
<div id="filesTable"></div>
|
||
<div class="flex-row" style="margin-top:8px;">
|
||
<button class="btn btn-primary btn-sm" id="btnParseAll">Парсинг</button>
|
||
<button class="btn btn-primary btn-sm" id="btnExtractAll">LLM-извлечение</button>
|
||
<button class="btn btn-primary btn-sm" id="btnDiffer">Сравнить</button>
|
||
<span id="actionStatus" class="msg"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Результаты сравнения -->
|
||
<div class="card" id="diffCard" style="display:none;">
|
||
<div class="card-header">🔍 Сравнение</div>
|
||
<div class="card-body" id="diffBody"></div>
|
||
</div>
|
||
|
||
<!-- Чат -->
|
||
<div class="card" id="chatCard" style="display:none;">
|
||
<div class="card-header">💬 Чат с договором</div>
|
||
<div class="card-body">
|
||
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;"></div>
|
||
<div class="flex-row">
|
||
<input type="text" id="chatInput" placeholder="Какая общая сумма? Что изменилось? ..." class="flex-grow">
|
||
<button class="btn btn-primary" id="chatSend">→</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="log"></div>
|
||
</div>
|
||
|
||
<script>
|
||
var contractId = '';
|
||
var supplements = [];
|
||
|
||
function log(msg) {
|
||
var el = document.getElementById('log');
|
||
el.textContent += msg + '\n';
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
|
||
// Загрузка
|
||
document.getElementById('btnUpload').addEventListener('click', async function() {
|
||
var files = document.getElementById('fileInput').files;
|
||
if (!files.length) return;
|
||
|
||
document.getElementById('btnUpload').disabled = true;
|
||
document.getElementById('uploadStatus').textContent = 'Загрузка...';
|
||
log('Загрузка ' + files.length + ' файлов...');
|
||
|
||
// Загружаем первый файл (или все)
|
||
for (var i = 0; i < files.length; i++) {
|
||
var fd = new FormData();
|
||
fd.append('files', files[i]);
|
||
if (contractId) fd.append('contract_id', contractId);
|
||
|
||
try {
|
||
var resp = await fetch('/upload.cfm', { method: 'POST', body: fd });
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
contractId = data.CONTRACT_ID;
|
||
log('✅ ' + data.FILENAME + ' (' + data.SIZE + ' bytes)');
|
||
if (!contractId) contractId = data.CONTRACT_ID;
|
||
} else {
|
||
log('❌ ' + files[i].name + ': ' + data.ERROR);
|
||
}
|
||
} catch(e) {
|
||
log('❌ ' + files[i].name + ': ' + e.message);
|
||
}
|
||
}
|
||
|
||
document.getElementById('btnUpload').disabled = false;
|
||
document.getElementById('uploadStatus').textContent = '';
|
||
if (contractId) {
|
||
document.getElementById('contractLabel').textContent = contractId.substring(0,8) + '...';
|
||
loadFiles();
|
||
}
|
||
});
|
||
|
||
async function loadFiles() {
|
||
if (!contractId) return;
|
||
try {
|
||
var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent(
|
||
"SELECT s.id as supp_id,d.id as doc_id,d.filename,d.mime_type,d.status,s.type FROM supplements s JOIN documents d ON s.document_id=d.id WHERE s.contract_id='" + contractId + "' ORDER BY s.created_at"
|
||
));
|
||
var data = await resp.json();
|
||
if (data.OK && data.ROWS) {
|
||
supplements = data.ROWS;
|
||
renderFiles();
|
||
document.getElementById('filesCard').style.display = 'block';
|
||
document.getElementById('chatCard').style.display = 'block';
|
||
}
|
||
} catch(e) { log('Ошибка загрузки файлов: ' + e.message); }
|
||
}
|
||
|
||
function renderFiles() {
|
||
var html = '<table><tr><th>Файл</th><th>Тип</th><th>Статус</th><th>Действия</th></tr>';
|
||
for (var i = 0; i < supplements.length; i++) {
|
||
var s = supplements[i];
|
||
var statusClass = s.status === 'parsed' ? 'msg-green' : (s.status === 'error' ? 'msg-red' : '');
|
||
html += '<tr><td>' + s.filename + '</td><td>' + s.type + '</td><td class="' + statusClass + '">' + (s.status||'') + '</td>';
|
||
html += '<td><button class="btn btn-sm" onclick="parseOne(\'' + s.doc_id + '\',this)">Парс</button> ';
|
||
html += '<button class="btn btn-sm" onclick="extractOne(\'' + s.supp_id + '\',this)">LLM</button></td></tr>';
|
||
}
|
||
html += '</table>';
|
||
document.getElementById('filesTable').innerHTML = html;
|
||
}
|
||
|
||
// Парсинг
|
||
document.getElementById('btnParseAll').addEventListener('click', async function() {
|
||
document.getElementById('actionStatus').textContent = 'Парсинг...';
|
||
for (var i = 0; i < supplements.length; i++) {
|
||
var s = supplements[i];
|
||
if (s.status === 'parsed') continue;
|
||
try {
|
||
var resp = await fetch('/parser.cfm?doc_id=' + s.doc_id);
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
s.status = data.STATUS;
|
||
log('📄 ' + s.filename + ': ' + data.ELEMENT_COUNT + ' элементов');
|
||
}
|
||
} catch(e) { log('❌ Парсинг ' + s.filename + ': ' + e.message); }
|
||
}
|
||
renderFiles();
|
||
document.getElementById('actionStatus').textContent = 'Готово';
|
||
});
|
||
|
||
// Извлечение
|
||
document.getElementById('btnExtractAll').addEventListener('click', async function() {
|
||
document.getElementById('actionStatus').textContent = 'LLM-извлечение...';
|
||
for (var i = 0; i < supplements.length; i++) {
|
||
var s = supplements[i];
|
||
try {
|
||
var resp = await fetch('/extractor.cfm?supplement_id=' + s.supp_id);
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
log('🤖 ' + s.filename + ': ' + data.ROWS_SAVED + ' строк');
|
||
} else {
|
||
log('❌ LLM ' + s.filename + ': ' + (data.ERROR||''));
|
||
}
|
||
} catch(e) { log('❌ LLM ' + s.filename + ': ' + e.message); }
|
||
}
|
||
document.getElementById('actionStatus').textContent = 'Готово';
|
||
});
|
||
|
||
// Сравнение
|
||
document.getElementById('btnDiffer').addEventListener('click', async function() {
|
||
document.getElementById('actionStatus').textContent = 'Сравнение...';
|
||
try {
|
||
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
renderDiff(data);
|
||
document.getElementById('diffCard').style.display = 'block';
|
||
} else {
|
||
log('❌ Сравнение: ' + (data.ERROR||''));
|
||
}
|
||
} catch(e) { log('❌ Сравнение: ' + e.message); }
|
||
document.getElementById('actionStatus').textContent = 'Готово';
|
||
});
|
||
|
||
function renderDiff(data) {
|
||
var s = data.SUMMARY;
|
||
var html = '<p>Изменено: ' + (s.CHANGED||0) + ', Добавлено: ' + (s.ADDED||0) + ', Удалено: ' + (s.DELETED||0) + ', Без изменений: ' + (s.UNCHANGED||0) + '</p>';
|
||
html += '<table><tr><th>#</th><th>Тип</th><th>Услуга</th><th>Цена</th><th>Кол-во</th><th>Сумма</th></tr>';
|
||
for (var i = 0; i < data.CHANGES.length; i++) {
|
||
var c = data.CHANGES[i];
|
||
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;
|
||
html += '<tr class="' + cls + '"><td>' + c.ROW_NUM + '</td><td>' + c.CHANGE_TYPE + '</td><td>' + (vals.NAME||'') + '</td><td>' + (vals.PRICE||'') + '</td><td>' + (vals.QTY||'') + '</td><td>' + (vals.SUM||'') + '</td></tr>';
|
||
}
|
||
html += '</table>';
|
||
document.getElementById('diffBody').innerHTML = html;
|
||
}
|
||
|
||
// Чат
|
||
document.getElementById('chatSend').addEventListener('click', async function() {
|
||
var input = document.getElementById('chatInput');
|
||
var q = input.value.trim();
|
||
if (!q || !contractId) return;
|
||
|
||
var msgs = document.getElementById('chatMessages');
|
||
msgs.innerHTML += '<div><b>Вы:</b> ' + q + '</div>';
|
||
input.value = '';
|
||
document.getElementById('chatSend').disabled = true;
|
||
|
||
try {
|
||
var fd = new FormData();
|
||
fd.append('question', q);
|
||
var resp = await fetch('/chat.cfm?contract_id=' + contractId, { method: 'POST', body: fd });
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
msgs.innerHTML += '<div><b>AI:</b> ' + data.ANSWER + '</div>';
|
||
} else {
|
||
msgs.innerHTML += '<div style="color:var(--red)">Ошибка: ' + (data.ERROR||'') + '</div>';
|
||
}
|
||
} catch(e) {
|
||
msgs.innerHTML += '<div style="color:var(--red)">Сеть: ' + e.message + '</div>';
|
||
}
|
||
document.getElementById('chatSend').disabled = false;
|
||
});
|
||
|
||
document.getElementById('chatInput').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') document.getElementById('chatSend').click();
|
||
});
|
||
|
||
// Превью файлов
|
||
document.getElementById('fileInput').addEventListener('change', function() {
|
||
var files = this.files;
|
||
var html = '';
|
||
for (var i = 0; i < files.length; i++) {
|
||
html += '<div style="font-size:13px;">📄 ' + files[i].name + ' (' + (files[i].size/1024).toFixed(1) + ' KB)</div>';
|
||
}
|
||
document.getElementById('fileList').innerHTML = html;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|