v1.0.19: Nubes UI + CORS + favicon/logo + таблица как на ВМ
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1017 B |
@@ -5,83 +5,126 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Сверка договоров — Lucee v1.0.18</title>
|
<title>Сверка договоров</title>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<style>
|
<style>
|
||||||
:root { --brand: #2563eb; --brand-dark: #1d4ed8; --gray: #d1d5db; --gray-light: #f3f4f6; --bg: #fff; --fg: #1a1a1a; --muted: #6b7280; --red: #ef4444; --green: #22c55e; --yellow: #eab308; }
|
: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; }
|
* { 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; }
|
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--foreground); background: var(--brand-grey-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 { 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; }
|
.topbar .title { font-weight: 600; font-size: 16px; }
|
||||||
.content { max-width: 900px; margin: 32px auto; padding: 0 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 { 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(--gray-light); padding: 12px 16px; font-weight: 600; font-size: 15px; }
|
.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; }
|
.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 { 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); border-color: var(--brand); color: #fff; }
|
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
|
||||||
.btn-primary:hover { background: var(--brand-dark); }
|
.btn-primary:hover { background: var(--brand-primary-dark); }
|
||||||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||||||
.btn-sm { height: 26px; font-size: 12px; padding: 0 8px; }
|
.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; }
|
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); }
|
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 8px; border: 1px solid var(--gray); }
|
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-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-deleted td { background: rgba(239,68,68,.08); text-decoration: line-through; color: var(--destructive); }
|
||||||
.diff-changed td { background: rgba(234,179,8,.08); }
|
.diff-changed td { background: rgba(234,179,8,.08); }
|
||||||
input[type=file] { width: 100%; margin-bottom: 8px; }
|
.flex-row { display: flex; gap: 8px; align-items: center; margin-top: 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; }
|
.flex-grow { flex: 1; }
|
||||||
.msg { font-size: 13px; padding: 4px 0; }
|
#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; }
|
||||||
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<span class="title">Сверка договоров</span>
|
<img src="/nubes-logo.svg" alt="Nubes">
|
||||||
<span style="color:var(--muted);font-size:12px;">v1.0.18 — Lucee</span>
|
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.19 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<!-- Загрузка -->
|
<!-- Загрузка -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">📁 Загрузка</div>
|
<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 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">
|
<div class="flex-row">
|
||||||
<button class="btn btn-primary" id="btnUpload">Загрузить</button>
|
<button class="btn btn-primary" id="btnParse">Парсинг</button>
|
||||||
<span id="uploadStatus" class="msg"></span>
|
<button class="btn btn-primary" id="btnExtract">LLM-извлечение</button>
|
||||||
|
<button class="btn btn-primary" id="btnDiffer">Сравнить</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="log"></div>
|
||||||
</div>
|
</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" id="diffCard" style="display:none;">
|
||||||
<div class="card-header">🔍 Сравнение</div>
|
<div class="card-header">
|
||||||
|
<i data-lucide="git-compare" style="width:18px;height:18px;"></i>
|
||||||
|
Сравнение
|
||||||
|
</div>
|
||||||
<div class="card-body" id="diffBody"></div>
|
<div class="card-body" id="diffBody"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Чат -->
|
<!-- Чат -->
|
||||||
<div class="card" id="chatCard" style="display:none;">
|
<div class="card" id="chatCard" style="display:none;">
|
||||||
<div class="card-header">💬 Чат с договором</div>
|
<div class="card-header">
|
||||||
|
<i data-lucide="message-circle" style="width:18px;height:18px;"></i>
|
||||||
|
Чат с договором
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;"></div>
|
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;max-height:300px;overflow-y:auto;"></div>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<input type="text" id="chatInput" placeholder="Какая общая сумма? Что изменилось? ..." class="flex-grow">
|
<input type="text" id="chatInput" placeholder="Какая общая сумма? Что изменилось? ..." class="flex-grow">
|
||||||
<button class="btn btn-primary" id="chatSend">→</button>
|
<button class="btn btn-primary" id="chatSend">→</button>
|
||||||
@@ -89,12 +132,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="log"></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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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 contractId = '';
|
||||||
var supplements = [];
|
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) {
|
function log(msg) {
|
||||||
var el = document.getElementById('log');
|
var el = document.getElementById('log');
|
||||||
@@ -102,180 +176,227 @@ function log(msg) {
|
|||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка
|
function renderTable() {
|
||||||
document.getElementById('btnUpload').addEventListener('click', async function() {
|
if (fileQueue.length === 0) {
|
||||||
var files = document.getElementById('fileInput').files;
|
fileTable.innerHTML = '<tr class="empty-row"><td colspan="6">Нет файлов — выберите .docx / .pdf / .zip</td></tr>';
|
||||||
if (!files.length) return;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('btnUpload').disabled = true;
|
function removeFile(i) {
|
||||||
document.getElementById('uploadStatus').textContent = 'Загрузка...';
|
fileQueue.splice(i, 1);
|
||||||
log('Загрузка ' + files.length + ' файлов...');
|
if (fileQueue.length === 0) contractId = '';
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем первый файл (или все)
|
fileInput.addEventListener('change', function() {
|
||||||
|
var files = this.files;
|
||||||
for (var i = 0; i < files.length; i++) {
|
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();
|
var fd = new FormData();
|
||||||
fd.append('files', files[i]);
|
fd.append('files', f.file);
|
||||||
if (contractId) fd.append('contract_id', contractId);
|
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 {
|
try {
|
||||||
var resp = await fetch('/upload.cfm', { method: 'POST', body: fd });
|
var r = JSON.parse(xhr.responseText);
|
||||||
var data = await resp.json();
|
if (r.OK) {
|
||||||
if (data.OK) {
|
contractId = r.CONTRACT_ID;
|
||||||
contractId = data.CONTRACT_ID;
|
f.doc_id = r.DOC_ID;
|
||||||
log('✅ ' + data.FILENAME + ' (' + data.SIZE + ' bytes)');
|
f.status = 'uploaded';
|
||||||
if (!contractId) contractId = data.CONTRACT_ID;
|
var elapsed = ((Date.now() - uploadStartTime) / 1000).toFixed(1);
|
||||||
|
log('✅ ' + f.name + ' (' + formatSize(f.size) + ') — ' + elapsed + 'с');
|
||||||
} else {
|
} else {
|
||||||
log('❌ ' + files[i].name + ': ' + data.ERROR);
|
f.status = 'error';
|
||||||
|
log('❌ ' + f.name + ': ' + (r.ERROR || ''));
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) { f.status = 'error'; log('❌ ' + f.name + ': ' + e.message); }
|
||||||
log('❌ ' + files[i].name + ': ' + e.message);
|
renderTable();
|
||||||
}
|
uploadNext(i + 1);
|
||||||
}
|
};
|
||||||
|
xhr.onerror = function() {
|
||||||
document.getElementById('btnUpload').disabled = false;
|
f.status = 'error';
|
||||||
document.getElementById('uploadStatus').textContent = '';
|
log('❌ ' + f.name + ': сеть');
|
||||||
if (contractId) {
|
renderTable();
|
||||||
document.getElementById('contractLabel').textContent = contractId.substring(0,8) + '...';
|
uploadNext(i + 1);
|
||||||
loadFiles();
|
};
|
||||||
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
uploadNext(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadFiles() {
|
// ── Инфо ──────────────────────────────────────────────────
|
||||||
if (!contractId) return;
|
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 {
|
try {
|
||||||
var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent(
|
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"
|
"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();
|
var data = await resp.json();
|
||||||
if (data.OK && data.ROWS) {
|
if (!data.OK || !data.ROWS) { log('❌ Не удалось получить допники'); btn.disabled = false; return; }
|
||||||
supplements = data.ROWS;
|
|
||||||
renderFiles();
|
|
||||||
document.getElementById('filesCard').style.display = 'block';
|
|
||||||
document.getElementById('chatCard').style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch(e) { log('Ошибка загрузки файлов: ' + e.message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFiles() {
|
for (var i = 0; i < data.ROWS.length; i++) {
|
||||||
var html = '<table><tr><th>Файл</th><th>Тип</th><th>Статус</th><th>Действия</th></tr>';
|
var row = data.ROWS[i];
|
||||||
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 {
|
try {
|
||||||
var resp = await fetch('/parser.cfm?doc_id=' + s.doc_id);
|
var er = await fetch('/extractor.cfm?supplement_id=' + row.id);
|
||||||
var data = await resp.json();
|
var ed = await er.json();
|
||||||
if (data.OK) {
|
if (ed.OK) {
|
||||||
s.status = data.STATUS;
|
log('✅ ' + row.filename + ': ' + ed.ROWS_SAVED + ' строк');
|
||||||
log('📄 ' + s.filename + ': ' + data.ELEMENT_COUNT + ' элементов');
|
} else { log('❌ ' + row.filename + ': ' + (ed.ERROR||'')); }
|
||||||
|
} catch(e) { log('❌ ' + row.filename + ': ' + e.message); }
|
||||||
}
|
}
|
||||||
} catch(e) { log('❌ Парсинг ' + s.filename + ': ' + e.message); }
|
} catch(e) { log('❌ ' + e.message); }
|
||||||
}
|
btn.disabled = false;
|
||||||
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('btnDiffer').addEventListener('click', async function() {
|
||||||
document.getElementById('actionStatus').textContent = 'Сравнение...';
|
var btn = this; btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
|
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (data.OK) {
|
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 s = data.SUMMARY;
|
||||||
var html = '<p>Изменено: ' + (s.CHANGED||0) + ', Добавлено: ' + (s.ADDED||0) + ', Удалено: ' + (s.DELETED||0) + ', Без изменений: ' + (s.UNCHANGED||0) + '</p>';
|
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>';
|
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++) {
|
for (var i = 0; i < data.CHANGES.length; i++) {
|
||||||
var c = data.CHANGES[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 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;
|
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 += '<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>';
|
html += '</table>';
|
||||||
document.getElementById('diffBody').innerHTML = html;
|
document.getElementById('diffBody').innerHTML = html;
|
||||||
}
|
document.getElementById('diffCard').style.display = 'block';
|
||||||
|
} else { log('❌ ' + (data.ERROR||'')); }
|
||||||
|
} catch(e) { log('❌ ' + e.message); }
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Чат
|
// ── Чат ───────────────────────────────────────────────────
|
||||||
document.getElementById('chatSend').addEventListener('click', async function() {
|
document.getElementById('chatSend').addEventListener('click', async function() {
|
||||||
var input = document.getElementById('chatInput');
|
var input = document.getElementById('chatInput');
|
||||||
var q = input.value.trim();
|
var q = input.value.trim();
|
||||||
if (!q || !contractId) return;
|
if (!q || !contractId) return;
|
||||||
|
|
||||||
var msgs = document.getElementById('chatMessages');
|
var msgs = document.getElementById('chatMessages');
|
||||||
msgs.innerHTML += '<div><b>Вы:</b> ' + q + '</div>';
|
msgs.innerHTML += '<div style="margin-bottom:4px;"><b>Вы:</b> ' + q + '</div>';
|
||||||
input.value = '';
|
input.value = '';
|
||||||
document.getElementById('chatSend').disabled = true;
|
document.getElementById('chatSend').disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
fd.append('question', q);
|
fd.append('question', q);
|
||||||
var resp = await fetch('/chat.cfm?contract_id=' + contractId, { method: 'POST', body: fd });
|
var resp = await fetch('/chat.cfm?contract_id=' + contractId, { method: 'POST', body: fd });
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (data.OK) {
|
if (data.OK) {
|
||||||
msgs.innerHTML += '<div><b>AI:</b> ' + data.ANSWER + '</div>';
|
msgs.innerHTML += '<div style="margin-bottom:8px;"><b>AI:</b> ' + data.ANSWER + '</div>';
|
||||||
} else {
|
} else {
|
||||||
msgs.innerHTML += '<div style="color:var(--red)">Ошибка: ' + (data.ERROR||'') + '</div>';
|
msgs.innerHTML += '<div style="color:var(--destructive);margin-bottom:8px;">Ошибка: ' + (data.ERROR||'') + '</div>';
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
msgs.innerHTML += '<div style="color:var(--red)">Сеть: ' + e.message + '</div>';
|
msgs.innerHTML += '<div style="color:var(--destructive);margin-bottom:8px;">Сеть: ' + e.message + '</div>';
|
||||||
}
|
}
|
||||||
document.getElementById('chatSend').disabled = false;
|
document.getElementById('chatSend').disabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('chatInput').addEventListener('keydown', function(e) {
|
document.getElementById('chatInput').addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') document.getElementById('chatSend').click();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 32" width="120" height="32">
|
||||||
|
<text x="0" y="24" font-family="system-ui,sans-serif" font-weight="700" font-size="24" fill="#2563eb">nubes</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 210 B |
@@ -1,5 +1,14 @@
|
|||||||
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
|
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
|
||||||
<cfheader name="Content-Type" value="application/json; charset=utf-8">
|
<cfheader name="Content-Type" value="application/json; charset=utf-8">
|
||||||
|
<cfheader name="Access-Control-Allow-Origin" value="*">
|
||||||
|
<cfheader name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS">
|
||||||
|
<cfheader name="Access-Control-Allow-Headers" value="Content-Type">
|
||||||
|
|
||||||
|
<cfif cgi.request_method EQ "OPTIONS">
|
||||||
|
<cfheader name="Content-Type" value="text/plain">
|
||||||
|
<cfoutput>OK</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfset result = {ok: false, error: "no file"}>
|
<cfset result = {ok: false, error: "no file"}>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user