Files
contractor/index.cfm
T

490 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.21 — 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;">&#xd7;</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');
// Загружаем промпт с сервера при появлении contract_id
var origUploadNext = null;
(function() {
var checkPrompt = setInterval(function() {
if (contractId) {
clearInterval(checkPrompt);
fetch('/prompt.cfm?contract_id=' + contractId)
.then(function(r) { return r.json(); })
.then(function(d) { if (d.OK) promptEditor.value = d.PROMPT || ''; });
}
}, 500);
})();
// Автосохранение на сервер при изменении
var promptTimer = null;
promptEditor.addEventListener('input', function() {
clearTimeout(promptTimer);
promptTimer = setTimeout(function() {
if (!contractId) return;
fetch('/prompt.cfm?contract_id=' + contractId, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: promptEditor.value})
});
}, 800);
});
// ── Чат ───────────────────────────────────────────────────
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>