v1.0.18: chat.cfm + UI (index.cfm — полный интерфейс)

This commit is contained in:
2026-06-18 18:52:11 +04:00
parent fbe16c4826
commit 27068d92b8
2 changed files with 360 additions and 1 deletions
+281 -1
View File
@@ -1 +1,281 @@
<cfsetting showdebugoutput=no enablecfoutputonly=yes><cfoutput>OK v1.0.17</cfoutput>
<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>