469 lines
24 KiB
Plaintext
469 lines
24 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>Сверка договоров</title>
|
||
<link rel="icon" type="image/png" href="/favicon.png">
|
||
<script src="https://unpkg.com/lucide@latest"></script>
|
||
<style>
|
||
:root {
|
||
--brand-primary: #2563eb; --brand-primary-dark: #1d4ed8;
|
||
--brand-gray: #d1d5db; --brand-grey-light: #f3f4f6;
|
||
--background: #ffffff; --foreground: #1a1a1a;
|
||
--muted: #6b7280; --destructive: #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(--foreground); background: var(--brand-grey-light); min-height: 100vh; }
|
||
.topbar { background: var(--background); border-bottom: 1px solid var(--brand-gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 16px; }
|
||
.topbar img { height: 28px; }
|
||
.topbar .title { font-weight: 600; font-size: 16px; }
|
||
.content { max-width: 900px; margin: 32px auto; padding: 0 16px; }
|
||
.card { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); overflow: hidden; margin-bottom: 16px; }
|
||
.card-header { background: var(--brand-grey-light); padding: 12px 16px; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
|
||
.card-body { padding: 16px; }
|
||
.btn { height: 36px; border-radius: 6px; gap: 6px; padding: 0 14px; font-size: 14px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; border: 1px solid var(--brand-gray); background: var(--background); }
|
||
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
|
||
.btn-primary:hover { background: var(--brand-primary-dark); }
|
||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
.btn-sm { height: 28px; font-size: 12px; padding: 0 10px; }
|
||
.table-wrap { border-radius: 6px; border: 1px solid var(--brand-gray); overflow: hidden; margin-top: 12px; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th { background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 10px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); }
|
||
td { padding: 6px 10px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); }
|
||
tr:hover td { background: rgba(243,244,246,.5); }
|
||
.name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.num-cell { text-align: right; white-space: nowrap; }
|
||
.status-ok { color: var(--green); }
|
||
.status-err { color: var(--destructive); }
|
||
.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:hover { color: var(--destructive); }
|
||
.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.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.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-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 .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
|
||
.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 td { background: rgba(34,197,94,.08); }
|
||
.diff-deleted td { background: rgba(239,68,68,.08); text-decoration: line-through; color: var(--destructive); }
|
||
.diff-changed td { background: rgba(234,179,8,.08); }
|
||
.flex-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
||
.flex-grow { flex: 1; }
|
||
#log { font-size: 12px; color: var(--muted); max-height: 200px; overflow-y: auto; background: var(--brand-grey-light); padding: 8px; border-radius: 6px; margin-top: 8px; white-space: pre-wrap; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<img src="/nubes-logo.svg" alt="Nubes">
|
||
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.20 — Lucee</span></span>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- Загрузка -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
|
||
Загрузка договоров / допников
|
||
</div>
|
||
<div class="card-body">
|
||
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<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></tr>
|
||
</thead>
|
||
<tbody id="fileTable"></tbody>
|
||
</table>
|
||
</div>
|
||
<button class="btn btn-primary" id="uploadBtn" style="width:100%;justify-content:center;margin-top:12px;" disabled>
|
||
<i data-lucide="upload" style="width:16px;height:16px;"></i> Загрузить
|
||
</button>
|
||
<div id="uploadTimer" style="text-align:center;font-size:12px;color:var(--muted);margin-top:4px;display:none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Действия -->
|
||
<div class="card" id="actionsCard" style="display:none;">
|
||
<div class="card-header">
|
||
<i data-lucide="play" style="width:18px;height:18px;"></i>
|
||
Обработка
|
||
</div>
|
||
<div class="card-body">
|
||
<div style="margin-bottom:8px;">
|
||
<textarea id="promptEditor" style="width:100%;height:120px;font-family:monospace;font-size:11px;border:1px solid var(--brand-gray);border-radius:6px;padding:8px;" placeholder="Промпт для LLM (оставьте пустым для стандартного)..."></textarea>
|
||
</div>
|
||
<div class="flex-row">
|
||
<button class="btn btn-primary" id="btnParse">Парсинг</button>
|
||
<button class="btn btn-primary" id="btnExtract">LLM-извлечение</button>
|
||
<button class="btn btn-primary" id="btnDiffer">Сравнить</button>
|
||
<button class="btn btn-primary" id="btnFull" style="background:var(--green);border-color:var(--green);">⚡ Полный цикл</button>
|
||
</div>
|
||
<div id="log"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Сравнение -->
|
||
<div class="card" id="diffCard" style="display:none;">
|
||
<div class="card-header">
|
||
<i data-lucide="git-compare" style="width:18px;height:18px;"></i>
|
||
Сравнение
|
||
</div>
|
||
<div class="card-body" id="diffBody"></div>
|
||
</div>
|
||
|
||
<!-- Чат -->
|
||
<div class="card" id="chatCard" style="display:none;">
|
||
<div class="card-header">
|
||
<i data-lucide="message-circle" style="width:18px;height:18px;"></i>
|
||
Чат с договором
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;max-height:300px;overflow-y:auto;"></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 class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
|
||
<div class="modal" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
|
||
<span id="modalTitle">Информация о файле</span>
|
||
<button class="remove-btn" onclick="closeModal()" style="margin-left:auto;font-size:18px;">×</button>
|
||
</div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
|
||
var fileInput = document.getElementById('fileInput');
|
||
var uploadBtn = document.getElementById('uploadBtn');
|
||
var fileTable = document.getElementById('fileTable');
|
||
var uploadTimer = document.getElementById('uploadTimer');
|
||
var fileQueue = [];
|
||
var contractId = '';
|
||
var uploadStartTime = 0;
|
||
|
||
function formatSize(bytes) {
|
||
if (!bytes || bytes === 0) return '—';
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
function formatDate(ts) {
|
||
if (!ts) return '—';
|
||
var d = new Date(ts);
|
||
return d.toLocaleDateString('ru-RU') + ' ' + d.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit'});
|
||
}
|
||
|
||
function log(msg) {
|
||
var el = document.getElementById('log');
|
||
el.textContent += msg + '\n';
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
|
||
function renderTable() {
|
||
if (fileQueue.length === 0) {
|
||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="6">Нет файлов — выберите .docx / .pdf / .zip</td></tr>';
|
||
uploadBtn.disabled = true;
|
||
} else {
|
||
fileTable.innerHTML = fileQueue.map(function(f, i) {
|
||
return '<tr id="row_' + i + '">' +
|
||
'<td class="name-cell">' + f.name + '</td>' +
|
||
'<td style="font-size:12px;color:var(--muted);">' + formatDate(f.lastModified) + '</td>' +
|
||
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
|
||
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
||
'<td><button class="info-btn' + (f.status ? ' visible' : '') + '" id="info_' + i + '" onclick="showInfo(' + i + ')" title="Инфо"><i data-lucide="info" 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>';
|
||
}).join('');
|
||
uploadBtn.disabled = false;
|
||
}
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function removeFile(i) {
|
||
fileQueue.splice(i, 1);
|
||
if (fileQueue.length === 0) contractId = '';
|
||
renderTable();
|
||
}
|
||
|
||
fileInput.addEventListener('change', function() {
|
||
var files = this.files;
|
||
for (var i = 0; i < files.length; i++) {
|
||
fileQueue.push({
|
||
name: files[i].name,
|
||
size: files[i].size,
|
||
lastModified: files[i].lastModified,
|
||
file: files[i],
|
||
status: ''
|
||
});
|
||
}
|
||
this.value = '';
|
||
renderTable();
|
||
});
|
||
|
||
// ── Загрузка (XHR с прогрессом) ──────────────────────────
|
||
uploadBtn.addEventListener('click', function() {
|
||
if (fileQueue.length === 0) return;
|
||
uploadBtn.disabled = true;
|
||
uploadStartTime = Date.now();
|
||
uploadTimer.style.display = 'block';
|
||
|
||
function uploadNext(i) {
|
||
if (i >= fileQueue.length) {
|
||
uploadTimer.style.display = 'none';
|
||
uploadBtn.disabled = false;
|
||
document.getElementById('actionsCard').style.display = 'block';
|
||
document.getElementById('chatCard').style.display = 'block';
|
||
return;
|
||
}
|
||
var f = fileQueue[i];
|
||
if (f.status === 'uploaded') { uploadNext(i + 1); return; }
|
||
|
||
f.status = '0%';
|
||
renderTable();
|
||
uploadTimer.textContent = 'Загрузка ' + (i+1) + '/' + fileQueue.length + '...';
|
||
|
||
var xhr = new XMLHttpRequest();
|
||
var fd = new FormData();
|
||
fd.append('files', f.file);
|
||
if (contractId) fd.append('contract_id', contractId);
|
||
|
||
xhr.open('POST', '/upload.cfm');
|
||
xhr.upload.onprogress = function(e) {
|
||
if (e.lengthComputable) {
|
||
f.status = Math.round(e.loaded / e.total * 100) + '%';
|
||
renderTable();
|
||
}
|
||
};
|
||
xhr.onload = function() {
|
||
try {
|
||
var r = JSON.parse(xhr.responseText);
|
||
if (r.OK) {
|
||
contractId = r.CONTRACT_ID;
|
||
f.doc_id = r.DOC_ID;
|
||
f.status = 'uploaded';
|
||
var elapsed = ((Date.now() - uploadStartTime) / 1000).toFixed(1);
|
||
log('✅ ' + f.name + ' (' + formatSize(f.size) + ') — ' + elapsed + 'с');
|
||
} else {
|
||
f.status = 'error';
|
||
log('❌ ' + f.name + ': ' + (r.ERROR || ''));
|
||
}
|
||
} catch(e) { f.status = 'error'; log('❌ ' + f.name + ': ' + e.message); }
|
||
renderTable();
|
||
uploadNext(i + 1);
|
||
};
|
||
xhr.onerror = function() {
|
||
f.status = 'error';
|
||
log('❌ ' + f.name + ': сеть');
|
||
renderTable();
|
||
uploadNext(i + 1);
|
||
};
|
||
xhr.send(fd);
|
||
}
|
||
uploadNext(0);
|
||
});
|
||
|
||
// ── Инфо ──────────────────────────────────────────────────
|
||
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>';
|
||
html += '<div class="kv"><span class="k">Статус</span><span class="v">' + (f.status || '—') + '</span></div>';
|
||
if (f.doc_id) html += '<div class="kv"><span class="k">doc_id</span><span class="v">' + f.doc_id + '</span></div>';
|
||
if (f.element_count) html += '<div class="kv"><span class="k">Элементов</span><span class="v">' + f.element_count + '</span></div>';
|
||
document.getElementById('modalBody').innerHTML = html;
|
||
document.getElementById('modalOverlay').classList.add('open');
|
||
}
|
||
function closeModal(e) {
|
||
if (e && e.target !== document.getElementById('modalOverlay')) return;
|
||
document.getElementById('modalOverlay').classList.remove('open');
|
||
}
|
||
window.showInfo = showInfo;
|
||
window.closeModal = closeModal;
|
||
window.removeFile = removeFile;
|
||
|
||
// ── Парсинг ───────────────────────────────────────────────
|
||
document.getElementById('btnParse').addEventListener('click', async function() {
|
||
var btn = this; btn.disabled = true; log('⏳ Парсинг...');
|
||
for (var i = 0; i < fileQueue.length; i++) {
|
||
var f = fileQueue[i];
|
||
if (!f.doc_id || f.parsed) continue;
|
||
try {
|
||
var resp = await fetch('/parser.cfm?doc_id=' + f.doc_id);
|
||
var d = await resp.json();
|
||
if (d.OK) {
|
||
f.element_count = d.ELEMENT_COUNT;
|
||
f.parsed = true;
|
||
f.status = 'parsed (' + d.ELEMENT_COUNT + ')';
|
||
log('📄 ' + f.name + ': ' + d.ELEMENT_COUNT + ' эл.');
|
||
} else { log('❌ ' + f.name + ': ' + (d.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + f.name + ': ' + e.message); }
|
||
renderTable();
|
||
}
|
||
btn.disabled = false;
|
||
});
|
||
|
||
// ── LLM ───────────────────────────────────────────────────
|
||
document.getElementById('btnExtract').addEventListener('click', async function() {
|
||
var btn = this; btn.disabled = true; log('🤖 LLM-извлечение...');
|
||
// Получаем supplement_id
|
||
try {
|
||
var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent(
|
||
"SELECT s.id,d.filename 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) { log('❌ Не удалось получить допники'); btn.disabled = false; return; }
|
||
|
||
for (var i = 0; i < data.ROWS.length; i++) {
|
||
var row = data.ROWS[i];
|
||
try {
|
||
var er = await fetch('/extractor.cfm?supplement_id=' + row.id);
|
||
var ed = await er.json();
|
||
if (ed.OK) {
|
||
log('✅ ' + row.filename + ': ' + ed.ROWS_SAVED + ' строк');
|
||
} else { log('❌ ' + row.filename + ': ' + (ed.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + row.filename + ': ' + e.message); }
|
||
}
|
||
} catch(e) { log('❌ ' + e.message); }
|
||
btn.disabled = false;
|
||
});
|
||
|
||
// ── Сравнение ─────────────────────────────────────────────
|
||
document.getElementById('btnDiffer').addEventListener('click', async function() {
|
||
var btn = this; btn.disabled = true;
|
||
try {
|
||
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
var s = data.SUMMARY;
|
||
log('🔍 Изменено: ' + (s.CHANGED||0) + ', Добавлено: ' + (s.ADDED||0) + ', Удалено: ' + (s.DELETED||0));
|
||
var html = '<p style="margin-bottom:8px;"><b>Изменено:</b> ' + (s.CHANGED||0) + ' <b>Добавлено:</b> ' + (s.ADDED||0) + ' <b>Удалено:</b> ' + (s.DELETED||0) + ' <b>Без изменений:</b> ' + (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 style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (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('diffCard').style.display = 'block';
|
||
} else { log('❌ ' + (data.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + e.message); }
|
||
btn.disabled = false;
|
||
});
|
||
|
||
// ── Полный цикл: парсинг → LLM → сравнение ────────────────
|
||
document.getElementById('btnFull').addEventListener('click', async function() {
|
||
var btn = this; btn.disabled = true; log('⚡ Полный цикл...');
|
||
// 1. Парсинг
|
||
for (var i = 0; i < fileQueue.length; i++) {
|
||
var f = fileQueue[i];
|
||
if (!f.doc_id || f.parsed) continue;
|
||
try {
|
||
var resp = await fetch('/parser.cfm?doc_id=' + f.doc_id);
|
||
var d = await resp.json();
|
||
if (d.OK) { f.element_count = d.ELEMENT_COUNT; f.parsed = true; f.status = 'parsed (' + d.ELEMENT_COUNT + ')'; log('📄 ' + f.name + ': ' + d.ELEMENT_COUNT + ' эл.'); }
|
||
else { log('❌ ' + f.name + ': ' + (d.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + f.name + ': ' + e.message); }
|
||
renderTable();
|
||
}
|
||
// 2. LLM-извлечение
|
||
try {
|
||
var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent("SELECT s.id,d.filename 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) {
|
||
for (var i = 0; i < data.ROWS.length; i++) {
|
||
var row = data.ROWS[i];
|
||
try {
|
||
var er = await fetch('/extractor.cfm?supplement_id=' + row.id);
|
||
var ed = await er.json();
|
||
if (ed.OK) { log('🤖 ' + row.filename + ': ' + ed.ROWS_SAVED + ' строк'); }
|
||
else { log('❌ ' + row.filename + ': ' + (ed.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + row.filename + ': ' + e.message); }
|
||
}
|
||
}
|
||
} catch(e) { log('❌ ' + e.message); }
|
||
// 3. Сравнение
|
||
try {
|
||
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
|
||
var data = await resp.json();
|
||
if (data.OK) {
|
||
var s = data.SUMMARY;
|
||
log('🔍 Изменено: ' + (s.CHANGED||0) + ', Добавлено: ' + (s.ADDED||0) + ', Удалено: ' + (s.DELETED||0));
|
||
var html = '<p style="margin-bottom:8px;"><b>Изменено:</b> ' + (s.CHANGED||0) + ' <b>Добавлено:</b> ' + (s.ADDED||0) + ' <b>Удалено:</b> ' + (s.DELETED||0) + ' <b>Без изменений:</b> ' + (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 style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (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('diffCard').style.display = 'block';
|
||
} else { log('❌ ' + (data.ERROR||'')); }
|
||
} catch(e) { log('❌ ' + e.message); }
|
||
log('✅ Полный цикл завершён');
|
||
btn.disabled = false;
|
||
});
|
||
|
||
// ── Промпт ─────────────────────────────────────────────────
|
||
var promptEditor = document.getElementById('promptEditor');
|
||
promptEditor.value = localStorage.getItem('llm_prompt') || '';
|
||
promptEditor.addEventListener('input', function() {
|
||
localStorage.setItem('llm_prompt', this.value);
|
||
});
|
||
|
||
// ── Чат ───────────────────────────────────────────────────
|
||
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 style="margin-bottom:4px;"><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 style="margin-bottom:8px;"><b>AI:</b> ' + data.ANSWER + '</div>';
|
||
} else {
|
||
msgs.innerHTML += '<div style="color:var(--destructive);margin-bottom:8px;">Ошибка: ' + (data.ERROR||'') + '</div>';
|
||
}
|
||
} catch(e) {
|
||
msgs.innerHTML += '<div style="color:var(--destructive);margin-bottom:8px;">Сеть: ' + e.message + '</div>';
|
||
}
|
||
document.getElementById('chatSend').disabled = false;
|
||
});
|
||
document.getElementById('chatInput').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') document.getElementById('chatSend').click();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|