v1.0.178: Фаза 0 — state + render(state). Введён state.js (центральное состояние), render(state) вызывает renderTable(). fileQueue/contractId/batchId/_activeCompare → state.*. app_utils.js адаптирован. Подробные комментарии из decoupling-final-plan.md.
This commit is contained in:
+101
-84
@@ -9,9 +9,10 @@ var SITE_URL = ''; // same origin for api calls
|
||||
|
||||
var fileInput = document.getElementById('fileInput');
|
||||
var fileTable = document.getElementById('fileTable');
|
||||
var fileQueue = [];
|
||||
var contractId = null;
|
||||
var batchId = crypto.randomUUID(); // классификация: привязка всех файлов сессии
|
||||
// state.files — теперь в state.js (Фаза 0: state + render)
|
||||
// state.contractId — теперь в state.js (Фаза 0: state + render)
|
||||
// state.batchId — теперь в state.js (Фаза 0: state + render)
|
||||
// (batchId = crypto.randomUUID() — уникальный ID сессии для классификации)
|
||||
|
||||
// Автоочистка старых записей при загрузке страницы
|
||||
(async function cleanup() {
|
||||
@@ -20,9 +21,26 @@ var batchId = crypto.randomUUID(); // классификация: привяз
|
||||
} catch(e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
// Синхронизация БД с таблицей — удалить всё, чего нет в fileQueue
|
||||
/**
|
||||
* render(state) — Главная функция рендеринга (Фаза 0, decoupling-final-plan.md).
|
||||
*
|
||||
* ПАТТЕРН: action → мутация state → render(state)
|
||||
*
|
||||
* Фаза 0: вызывает существующий renderTable().
|
||||
* renderTable читает state.files напрямую (state — глобальный объект).
|
||||
* Фаза 1+: будет вызывать renderFiles(state), renderGroups(state), renderStepper(state) и т.д.
|
||||
*
|
||||
* ПРАВИЛО: любое изменение state ДОЛЖНО завершаться вызовом render(state).
|
||||
* Никакой прямой манипуляции DOM в обход render().
|
||||
* console.log(state) показывает ВСЁ состояние в любой момент.
|
||||
*/
|
||||
function render(state) {
|
||||
renderTable();
|
||||
}
|
||||
|
||||
// Синхронизация БД с таблицей — удалить всё, чего нет в state.files
|
||||
async function syncDB() {
|
||||
var keepIds = fileQueue.map(function(f) { return f.doc_id; }).filter(Boolean);
|
||||
var keepIds = state.files.map(function(f) { return f.doc_id; }).filter(Boolean);
|
||||
try {
|
||||
await fetch(VM_API + '/api/sync', {
|
||||
method: 'POST',
|
||||
@@ -55,10 +73,10 @@ function resetStepper(fromId) {
|
||||
// вынесены в app_utils.js для облегчения анализа и отладки.
|
||||
|
||||
function renderTable() {
|
||||
if (fileQueue.length === 0) {
|
||||
if (state.files.length === 0) {
|
||||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf</td></tr>';
|
||||
} else {
|
||||
fileTable.innerHTML = fileQueue.map(function(f, i) {
|
||||
fileTable.innerHTML = state.files.map(function(f, i) {
|
||||
var rows = '<tr id="row_' + i + '">' +
|
||||
'<td class="name-cell" style="cursor:pointer;" onclick="toggleClassifyDetail(' + i + ')">' +
|
||||
(f.doc_id ? '<span id="expand_' + i + '" style="font-size:10px;margin-right:4px;">▸</span>' : '') +
|
||||
@@ -88,7 +106,7 @@ window.toggleClassifyDetail = async function(i) {
|
||||
return;
|
||||
}
|
||||
|
||||
var f = fileQueue[i];
|
||||
var f = state.files[i];
|
||||
if (!f || !f.doc_id) return;
|
||||
|
||||
detailRow.style.display = '';
|
||||
@@ -149,10 +167,10 @@ fileInput.addEventListener('change', async function() {
|
||||
|
||||
// Удалить из очереди файлы, которых нет в новом выборе
|
||||
var newNames = new Set(newFiles.map(function(f) { return f.name; }));
|
||||
fileQueue = fileQueue.filter(function(f) {
|
||||
state.files = state.files.filter(function(f) {
|
||||
return newNames.has(f.name);
|
||||
});
|
||||
renderTable();
|
||||
render(state);
|
||||
// Разблокировать кнопку классификации — состав файлов изменился
|
||||
showClassifyBtn();
|
||||
// Сбросить прогресс пайплайна
|
||||
@@ -165,9 +183,9 @@ fileInput.addEventListener('change', async function() {
|
||||
if (isZip) {
|
||||
// ZIP: показать временную строку, распаковать, убрать
|
||||
var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: '⏳ распаковка...' };
|
||||
fileQueue.push(zipEntry);
|
||||
var zipIdx = fileQueue.length - 1;
|
||||
renderTable();
|
||||
state.files.push(zipEntry);
|
||||
var zipIdx = state.files.length - 1;
|
||||
render(state);
|
||||
|
||||
try {
|
||||
var zipResp = await new Promise(function(resolve, reject) {
|
||||
@@ -184,8 +202,8 @@ fileInput.addEventListener('change', async function() {
|
||||
if (!zipData.ok || !zipData.files) throw new Error('unzip failed');
|
||||
|
||||
// Убрать строку ZIP из таблицы
|
||||
fileQueue.splice(zipIdx, 1);
|
||||
renderTable();
|
||||
state.files.splice(zipIdx, 1);
|
||||
render(state);
|
||||
|
||||
// Добавить файлы из архива, пропуская дубликаты
|
||||
for (var zi = 0; zi < zipData.files.length; zi++) {
|
||||
@@ -194,8 +212,8 @@ fileInput.addEventListener('change', async function() {
|
||||
|
||||
// Проверить дубликат по имени
|
||||
var dup = -1;
|
||||
for (var dj = 0; dj < fileQueue.length; dj++) {
|
||||
if (fileQueue[dj].name === zf.filename) { dup = dj; break; }
|
||||
for (var dj = 0; dj < state.files.length; dj++) {
|
||||
if (state.files[dj].name === zf.filename) { dup = dj; break; }
|
||||
}
|
||||
if (dup >= 0) continue; // уже есть — не добавляем
|
||||
|
||||
@@ -204,26 +222,26 @@ fileInput.addEventListener('change', async function() {
|
||||
doc_id: zf.doc_id, uploaded: true,
|
||||
status: '⏳ парсинг...'
|
||||
};
|
||||
fileQueue.push(zEntry);
|
||||
var zIdx = fileQueue.length - 1;
|
||||
if (!contractId && zf.contract_id) contractId = zf.contract_id;
|
||||
renderTable();
|
||||
state.files.push(zEntry);
|
||||
var zIdx = state.files.length - 1;
|
||||
if (!state.contractId && zf.contract_id) state.contractId = zf.contract_id;
|
||||
render(state);
|
||||
|
||||
// Авто-парсинг: VM Python парсит ВСЁ
|
||||
var zpr = zf.parsed;
|
||||
if (zpr && zpr.status === 'parsed') {
|
||||
fileQueue[zIdx].parsed = true;
|
||||
fileQueue[zIdx].status = '<span class="status-ok">✓ ' + zpr.element_count + ' эл.</span>';
|
||||
state.files[zIdx].parsed = true;
|
||||
state.files[zIdx].status = '<span class="status-ok">✓ ' + zpr.element_count + ' эл.</span>';
|
||||
} else if (zpr && zpr.status === 'error') {
|
||||
fileQueue[zIdx].status = '<span class="status-err">✗ ' + (zpr.error || 'ошибка парсинга') + '</span>';
|
||||
state.files[zIdx].status = '<span class="status-err">✗ ' + (zpr.error || 'ошибка парсинга') + '</span>';
|
||||
} else {
|
||||
fileQueue[zIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
||||
state.files[zIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
||||
}
|
||||
renderTable();
|
||||
render(state);
|
||||
}
|
||||
} catch(ze) {
|
||||
fileQueue[zipIdx].status = '<span class="status-err">✗ ZIP: ' + ze.message + '</span>';
|
||||
renderTable();
|
||||
state.files[zipIdx].status = '<span class="status-err">✗ ZIP: ' + ze.message + '</span>';
|
||||
render(state);
|
||||
}
|
||||
continue; // ZIP обработан, переходим к следующему файлу
|
||||
}
|
||||
@@ -231,55 +249,55 @@ fileInput.addEventListener('change', async function() {
|
||||
// Обычный файл (не ZIP)
|
||||
var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' };
|
||||
var existingIdx = -1;
|
||||
for (var j = 0; j < fileQueue.length; j++) {
|
||||
if (fileQueue[j].name === f.name) { existingIdx = j; break; }
|
||||
for (var j = 0; j < state.files.length; j++) {
|
||||
if (state.files[j].name === f.name) { existingIdx = j; break; }
|
||||
}
|
||||
var rowIdx;
|
||||
if (existingIdx >= 0) {
|
||||
fileQueue[existingIdx] = entry;
|
||||
state.files[existingIdx] = entry;
|
||||
rowIdx = existingIdx;
|
||||
} else {
|
||||
fileQueue.push(entry);
|
||||
rowIdx = fileQueue.length - 1;
|
||||
state.files.push(entry);
|
||||
rowIdx = state.files.length - 1;
|
||||
}
|
||||
renderTable();
|
||||
render(state);
|
||||
|
||||
try {
|
||||
var resp = await uploadFile(f, function(pct) {
|
||||
fileQueue[rowIdx].status = '↑ ' + pct + '%';
|
||||
renderTable();
|
||||
state.files[rowIdx].status = '↑ ' + pct + '%';
|
||||
render(state);
|
||||
});
|
||||
// Python API returns lowercase keys (doc_id, contract_id, parsed)
|
||||
if (resp && resp.contract_id) contractId = resp.contract_id;
|
||||
fileQueue[rowIdx].doc_id = resp.doc_id;
|
||||
fileQueue[rowIdx].status = '<span class="status-ok">✓</span>';
|
||||
fileQueue[rowIdx].uploaded = true;
|
||||
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
||||
state.files[rowIdx].doc_id = resp.doc_id;
|
||||
state.files[rowIdx].status = '<span class="status-ok">✓</span>';
|
||||
state.files[rowIdx].uploaded = true;
|
||||
|
||||
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
|
||||
var t0 = Date.now();
|
||||
var pr = resp.parsed;
|
||||
if (pr && pr.status === 'parsed') {
|
||||
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||||
fileQueue[rowIdx].parsed = true;
|
||||
fileQueue[rowIdx].parseInfo = pr;
|
||||
fileQueue[rowIdx].status = '<span class="status-ok">✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)</span>';
|
||||
state.files[rowIdx].parsed = true;
|
||||
state.files[rowIdx].parseInfo = pr;
|
||||
state.files[rowIdx].status = '<span class="status-ok">✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)</span>';
|
||||
} else if (pr && pr.status === 'error') {
|
||||
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + (pr.error || 'ошибка парсинга') + '</span>';
|
||||
fileQueue[rowIdx].parseInfo = pr;
|
||||
state.files[rowIdx].status = '<span class="status-err">✗ ' + (pr.error || 'ошибка парсинга') + '</span>';
|
||||
state.files[rowIdx].parseInfo = pr;
|
||||
} else {
|
||||
fileQueue[rowIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
||||
state.files[rowIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
||||
}
|
||||
} catch(err) {
|
||||
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
||||
state.files[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
||||
}
|
||||
renderTable();
|
||||
render(state);
|
||||
}
|
||||
|
||||
await refreshSupps();
|
||||
stepDone('stepUpload');
|
||||
stepActive('stepClassify');
|
||||
|
||||
// Синхронизировать БД с таблицей — удалить всё, чего нет в fileQueue
|
||||
// Синхронизировать БД с таблицей — удалить всё, чего нет в state.files
|
||||
syncDB();
|
||||
|
||||
fileInput.value = '';
|
||||
@@ -322,7 +340,7 @@ async function runClassify() {
|
||||
// Poll progress каждые 2с
|
||||
var pollTimer = setInterval(async function() {
|
||||
try {
|
||||
var r = await fetch(VM_API + '/api/batch-progress?batch=' + batchId);
|
||||
var r = await fetch(VM_API + '/api/batch-progress?batch=' + state.batchId);
|
||||
var d = await r.json();
|
||||
if (d.ok && d.counts) {
|
||||
var done = (d.counts.classified || 0) + (d.counts.failed || 0);
|
||||
@@ -339,7 +357,7 @@ async function runClassify() {
|
||||
var resp = await fetch(VM_API + '/api/classify-batch', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({batch_id: batchId})
|
||||
body: JSON.stringify({batch_id: state.batchId})
|
||||
});
|
||||
var data = await resp.json();
|
||||
clearInterval(timer);
|
||||
@@ -362,7 +380,7 @@ async function runClassify() {
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
var resp = await fetch(VM_API + '/api/groups?batch=' + batchId);
|
||||
var resp = await fetch(VM_API + '/api/groups?batch=' + state.batchId);
|
||||
var data = await resp.json();
|
||||
if (!data.ok || !data.groups) return;
|
||||
|
||||
@@ -375,7 +393,7 @@ async function loadGroups() {
|
||||
stepDone('stepGroups');
|
||||
|
||||
// Сохраняем группы для compare
|
||||
window._groupsData = data.groups;
|
||||
state.groups = data.groups;
|
||||
|
||||
data.groups.forEach(function(g, gi) {
|
||||
var isUnresolved = g.contract_number === '__unresolved__';
|
||||
@@ -442,7 +460,7 @@ function markGroupDone(gi, time) {
|
||||
var bodyEl = document.getElementById('cmpBody_' + gi);
|
||||
var bodyHTML = bodyEl ? bodyEl.innerHTML : '';
|
||||
var card = bodyEl.parentNode;
|
||||
var g = window._groupsData[gi];
|
||||
var g = state.groups[gi];
|
||||
card.innerHTML = '<div class="cmp-header" style="font-weight:600;margin-bottom:6px;cursor:pointer;">' +
|
||||
'<span class="cmp-arrow" id="cmpArrow_' + gi + '" style="font-size:10px;margin-right:4px;">▾</span>' +
|
||||
'📄 Договор №' + escHtml(g.contract_number || '?') + ' — ' + escHtml(g.counterparty || 'контрагент не определён') +
|
||||
@@ -475,16 +493,15 @@ function markGroupDone(gi, time) {
|
||||
}
|
||||
|
||||
// Текущий активный EventSource (только один одновременно)
|
||||
var _activeCompareES = null;
|
||||
var _activeCompareTimer = null;
|
||||
// state._activeCompare — теперь в state.js (Фаза 0: state + render)
|
||||
|
||||
window.runCompareForGroup = async function(gi, btn) {
|
||||
var group = window._groupsData[gi];
|
||||
var group = state.groups[gi];
|
||||
if (!group || group.contract_number === '__unresolved__') return;
|
||||
|
||||
// Остановить предыдущее сравнение
|
||||
if (_activeCompareES) { _activeCompareES.close(); _activeCompareES = null; }
|
||||
if (_activeCompareTimer) { clearInterval(_activeCompareTimer); _activeCompareTimer = null; }
|
||||
if (state._activeCompare.es) { state._activeCompare.es.close(); state._activeCompare.es = null; }
|
||||
if (state._activeCompare.timer) { clearInterval(state._activeCompare.timer); state._activeCompare.timer = null; }
|
||||
|
||||
// Свернуть ТОЛЬКО тело текущей группы
|
||||
var curBody = document.getElementById('cmpBody_' + gi);
|
||||
@@ -498,7 +515,7 @@ window.runCompareForGroup = async function(gi, btn) {
|
||||
var ar = await fetch(VM_API + '/api/apply-groups', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({batch_id: batchId, groups: [group]})
|
||||
body: JSON.stringify({batch_id: state.batchId, groups: [group]})
|
||||
});
|
||||
var ad = await ar.json();
|
||||
if (!ad.ok || !ad.contract_ids || !ad.contract_ids.length) {
|
||||
@@ -530,12 +547,12 @@ window.runCompareForGroup = async function(gi, btn) {
|
||||
cmpBody.appendChild(statusEl);
|
||||
|
||||
var start = Date.now();
|
||||
_activeCompareTimer = setInterval(function() {
|
||||
state._activeCompare.timer = setInterval(function() {
|
||||
statusEl.textContent = '⏳ ' + ((Date.now() - start) / 1000).toFixed(1) + 'с';
|
||||
}, 200);
|
||||
|
||||
var es = new EventSource(VM_API + '/process-v2?contract_id=' + cid);
|
||||
_activeCompareES = es;
|
||||
state._activeCompare.es = es;
|
||||
var sections = {};
|
||||
|
||||
es.onmessage = function(e) {
|
||||
@@ -573,8 +590,8 @@ window.runCompareForGroup = async function(gi, btn) {
|
||||
}
|
||||
}
|
||||
else if (d.type === 'done') {
|
||||
clearInterval(_activeCompareTimer); _activeCompareTimer = null;
|
||||
es.close(); _activeCompareES = null;
|
||||
clearInterval(state._activeCompare.timer); state._activeCompare.timer = null;
|
||||
es.close(); state._activeCompare.es = null;
|
||||
statusEl.textContent = '✓ ' + d.total_time_s + 'с';
|
||||
statusEl.style.color = 'var(--green)';
|
||||
// Заменить карточку на обработанную (другая вёрстка, нет кнопок)
|
||||
@@ -584,16 +601,16 @@ window.runCompareForGroup = async function(gi, btn) {
|
||||
stepDone('stepCompare');
|
||||
}
|
||||
else if (d.type === 'error') {
|
||||
clearInterval(_activeCompareTimer); _activeCompareTimer = null;
|
||||
es.close(); _activeCompareES = null;
|
||||
clearInterval(state._activeCompare.timer); state._activeCompare.timer = null;
|
||||
es.close(); state._activeCompare.es = null;
|
||||
cmpBody.innerHTML += '<div style="color:var(--destructive);">✗ ' + d.message + '</div>';
|
||||
btn.innerHTML = '✗'; btn.disabled = false;
|
||||
document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; });
|
||||
}
|
||||
};
|
||||
es.onerror = function() {
|
||||
clearInterval(_activeCompareTimer); _activeCompareTimer = null;
|
||||
es.close(); _activeCompareES = null;
|
||||
clearInterval(state._activeCompare.timer); state._activeCompare.timer = null;
|
||||
es.close(); state._activeCompare.es = null;
|
||||
btn.innerHTML = '✗ соединение'; btn.disabled = false;
|
||||
document.querySelectorAll('.cmp-btn').forEach(function(b) { b.disabled = false; });
|
||||
};
|
||||
@@ -610,8 +627,8 @@ function uploadFile(file, onProgress) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var fd = new FormData();
|
||||
fd.append('files', uploadFile, uploadName);
|
||||
if (contractId) fd.append('contract_id', contractId);
|
||||
fd.append('batch_id', batchId);
|
||||
if (state.contractId) fd.append('contract_id', state.contractId);
|
||||
fd.append('batch_id', state.batchId);
|
||||
xhr.open('POST', UPLOAD_URL);
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 100));
|
||||
@@ -652,7 +669,7 @@ function uploadFile(file, onProgress) {
|
||||
|
||||
// ── LLM: Общее сравнение (Event Sourcing) ────────────────────────
|
||||
document.getElementById('llmBtn').addEventListener('click', function() {
|
||||
if (!contractId) return;
|
||||
if (!state.contractId) return;
|
||||
var btn = this;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Сравнение...';
|
||||
@@ -669,8 +686,8 @@ document.getElementById('llmBtn').addEventListener('click', function() {
|
||||
compareStatus.textContent = '⏳ ' + ((Date.now() - llmStart) / 1000).toFixed(1) + 'с';
|
||||
}, 200);
|
||||
|
||||
var order = fileQueue.map(function(f) { return f.supp_id; }).filter(Boolean).join(',');
|
||||
var es = new EventSource('https://contracts.kube5s.ru/process-v2?contract_id=' + contractId + (order ? '&order=' + encodeURIComponent(order) : ''));
|
||||
var order = state.files.map(function(f) { return f.supp_id; }).filter(Boolean).join(',');
|
||||
var es = new EventSource('https://contracts.kube5s.ru/process-v2?contract_id=' + state.contractId + (order ? '&order=' + encodeURIComponent(order) : ''));
|
||||
var sections = {}; // supplement_id → DOM element
|
||||
|
||||
es.onmessage = function(e) {
|
||||
@@ -723,7 +740,7 @@ document.getElementById('llmBtn').addEventListener('click', function() {
|
||||
btn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
|
||||
stepDone('stepCompare');
|
||||
if (d.total_unresolved > 0) {
|
||||
compareBody.innerHTML += '<div style="margin-top:8px;color:#eab308;">⚠ ' + d.total_unresolved + ' неразрешённых строк — <a href=\"/resolve.cfm?contract_id=' + contractId + '\">разобрать</a></div>';
|
||||
compareBody.innerHTML += '<div style="margin-top:8px;color:#eab308;">⚠ ' + d.total_unresolved + ' неразрешённых строк — <a href=\"/resolve.cfm?contract_id=' + state.contractId + '\">разобрать</a></div>';
|
||||
}
|
||||
document.getElementById('chatCard').style.display = 'block';
|
||||
lucide.createIcons();
|
||||
@@ -751,22 +768,22 @@ document.getElementById('llmBtn').addEventListener('click', function() {
|
||||
|
||||
// ── Обновить supplement_id/type для radio ──────────────────
|
||||
async function refreshSupps() {
|
||||
if (!contractId) { console.log('refreshSupps: no contractId'); return; }
|
||||
if (!state.contractId) { console.log('refreshSupps: no state.contractId'); return; }
|
||||
try {
|
||||
var resp = await fetch(VM_API + '/api/supplements?contract_id=' + contractId);
|
||||
var resp = await fetch(VM_API + '/api/supplements?contract_id=' + state.contractId);
|
||||
var data = await resp.json();
|
||||
console.log('refreshSupps: data=', data);
|
||||
if (data.ok && data.supplements) {
|
||||
data.supplements.forEach(function(supp) {
|
||||
console.log('refreshSupps: supp=', supp);
|
||||
for (var i = 0; i < fileQueue.length; i++) {
|
||||
if (fileQueue[i].doc_id === supp.document_id) {
|
||||
fileQueue[i].supp_id = supp.id;
|
||||
fileQueue[i].supp_type = supp.type;
|
||||
for (var i = 0; i < state.files.length; i++) {
|
||||
if (state.files[i].doc_id === supp.document_id) {
|
||||
state.files[i].supp_id = supp.id;
|
||||
state.files[i].supp_type = supp.type;
|
||||
}
|
||||
}
|
||||
});
|
||||
renderTable();
|
||||
render(state);
|
||||
}
|
||||
} catch(e) { console.log('refreshSupps error:', e); }
|
||||
}
|
||||
@@ -775,7 +792,7 @@ async function refreshSupps() {
|
||||
document.getElementById('chatSend').addEventListener('click', function() {
|
||||
var input = document.getElementById('chatInput');
|
||||
var q = input.value.trim();
|
||||
if (!q || !contractId) return;
|
||||
if (!q || !state.contractId) return;
|
||||
var msgs = document.getElementById('chatMessages');
|
||||
msgs.innerHTML += '<div style="color:var(--brand-primary);margin-top:4px;"><strong>Вы:</strong> ' + q + '</div>';
|
||||
input.value = '';
|
||||
@@ -785,7 +802,7 @@ document.getElementById('chatSend').addEventListener('click', function() {
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('question', q);
|
||||
fetch('/chat.cfm?contract_id=' + contractId, { method: 'POST', body: fd })
|
||||
fetch('/chat.cfm?contract_id=' + state.contractId, { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
msgs.lastChild.remove();
|
||||
@@ -811,7 +828,7 @@ function closeModal(e) {
|
||||
}
|
||||
|
||||
async function showText(i) {
|
||||
var f = fileQueue[i];
|
||||
var f = state.files[i];
|
||||
var docId = f.doc_id;
|
||||
document.getElementById('modalTitle').textContent = f.name;
|
||||
document.getElementById('modalBody').innerHTML = '<span style="color:var(--muted);">Загрузка...</span>';
|
||||
|
||||
Reference in New Issue
Block a user