(async function() { const API = location.origin; const PAGE_SIZE = 20; let page = 1, total = 0, searchTimer, groupsLoaded, feedbacks = [], shortsListCache = null, shortsFetchTimer = null, lastFbAutoText = '', userSettings = { settings: {}, defaults: {} }, aiActive = 'native', myModels = [], modelEditIdx = null; const topicReportCache = new Map(); function hdr() { const t = sessionStorage.getItem('authToken'); return t ? { 'X-Auth-Token': t } : {}; } function fmtDate(d) { if (!d) return ''; if (window.datetime && datetime.fmtShortIso) return datetime.fmtShortIso(d); return d; } function fmtDay(d) { if (!d) return ''; try { const dt = new Date(d + 'T12:00:00'); return dt.toLocaleDateString('ru-RU', { weekday: 'short' }); } catch (e) { return ''; } } function escapeHtml(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } function aiBadge(s) { switch (s) { case 'pending': return 'Ожидает'; case 'processing': return 'В обработке'; case 'done': return 'Готово'; case 'error': return 'Ошибка'; case 'skipped': return 'Пропущен'; default: return 'Без ИИ'; } } function fbItemHtml(f) { const text = (f.text||'').trim(); const del = f.deleted_at ? ' style="opacity:.5"' : ''; const day = fmtDay(f.feedback_date); const hasAiInfo = f.ai_status !== 'none' || !!f.ai_prompt; return `
${escapeHtml(f.group_name||'')} ${fmtDate(f.feedback_date)}${day ? ` · ${day}` : ''} ${f.deleted_at ? 'Удалён' : ''} ${aiBadge(f.ai_status)} ${f.sent_at ? 'Отправлено' : ''}
${hasAiInfo ? `` : ''} ${f.ai_status === 'error' ? `` : ''} ${f.deleted_at ? '' : ``} ${f.deleted_at ? `` : ''}
${f.resident ? `
${escapeHtml(f.resident)}
` : ''} ${f.topic ? `
${escapeHtml(f.topic)}
` : ''} ${text ? `
${escapeHtml(text)}
` : ''}
`; } function fmtDurMs(ms) { if (!Number.isFinite(ms) || ms < 0) return ''; return ms >= 1000 ? `${(ms / 1000).toFixed(1)} с` : `${Math.round(ms)} мс`; } function fmtTs(v) { if (!v) return ''; try { if (typeof fmtFull === 'function') return fmtFull(v) || String(v); } catch (e) {} return String(v); } function fbAiInfoHtml(f) { let html = `
Статус${aiBadge(f.ai_status)}
`; html += `
Модель${escapeHtml(f.ai_model || '—')}
`; const dur = fmtDurMs(f.ai_duration_ms); html += `
Время ответа${escapeHtml(dur || '—')}
`; if (f.ai_checked_at) html += `
Проверено${escapeHtml(fmtTs(f.ai_checked_at))}
`; if (f.ai_error) html += `
Ошибка: ${escapeHtml(f.ai_error)}
`; if (f.ai_prompt) { html += `
Промпт, отправленный в модель
${escapeHtml(f.ai_prompt)}
`; } else { html += '
Нет данных о запуске ИИ
'; } return html; } function closeAiPops() { document.querySelectorAll('.fb-ai-pop').forEach(p => { p.hidden = true; }); } document.addEventListener('click', e => { if (e.target.closest('[data-aiinfo]') || e.target.closest('.fb-ai-pop')) return; closeAiPops(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAiPops(); }); function ensureModal() { let m = document.getElementById('feedbackModal'); if (m) return m; m = document.createElement('div'); m.className = 'modal-overlay'; m.id = 'feedbackModal'; document.body.appendChild(m); return m; } function openModal({id}) { const m = document.getElementById('feedbackModal'); if (!m) return; m.classList.add('open'); resetForm(); if (id) loadFeedback(id); } function closeModal() { const m = document.getElementById('feedbackModal'); if (m) m.classList.remove('open'); closePromptModal(); } function closePromptModal() { const m = document.getElementById('fbPromptModal'); if (m) m.classList.remove('open'); } function updatePromptCount() { const el = document.getElementById('fbPromptCount'); const ta = document.getElementById('fbPromptText'); if (el && ta) el.textContent = `${ta.value.length} / 8000`; } async function loadUserSettings() { try { const r = await fetch(`${API}/api/user-settings`, { headers: hdr() }); if (!r.ok) return; const j = await r.json(); userSettings.settings = j.settings || {}; userSettings.defaults = j.defaults || {}; parseMyModels(); } catch (e) {} } function parseMyModels() { try { myModels = JSON.parse(userSettings.settings.feedback_models || '[]'); } catch (e) { myModels = []; } if (!Array.isArray(myModels)) myModels = []; } function renderModelsList() { const el = document.getElementById('fbModelsList'); if (!el) return; const cnt = document.getElementById('fbModelsCount'); if (cnt) cnt.textContent = myModels.length ? ` (${myModels.length})` : ''; if (!myModels.length) { el.innerHTML = '
Своих моделей нет — используются системные
'; return; } el.innerHTML = myModels.map((m, i) => `
${escapeHtml(m.name)} — ${escapeHtml(m.model)}
`).join(''); } function showModelForm(idx) { modelEditIdx = idx; const m = idx == null ? null : myModels[idx]; document.getElementById('fbMName').value = m ? m.name : ''; document.getElementById('fbMModel').value = m ? m.model : ''; document.getElementById('fbMUrl').value = m ? m.base_url : ''; document.getElementById('fbMKey').value = m ? (m.api_key || '') : ''; document.getElementById('fbMMaxTok').value = m && m.max_tokens ? m.max_tokens : ''; document.getElementById('fbModelEdit').hidden = false; document.getElementById('fbMName').focus(); } function hideModelForm() { const el = document.getElementById('fbModelEdit'); if (el) el.hidden = true; modelEditIdx = null; } function saveModelForm() { const name = document.getElementById('fbMName').value.trim(); const model = document.getElementById('fbMModel').value.trim(); const base = document.getElementById('fbMUrl').value.trim().replace(/\/+$/, ''); const key = document.getElementById('fbMKey').value.trim(); const tok = document.getElementById('fbMMaxTok').value.trim(); if (!name) return alert('Укажите название модели'); if (!model) return alert('Укажите имя модели'); try { const u = new URL(base); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad'); } catch (e) { return alert('Base URL должен начинаться с http:// или https://'); } let maxTokens = null; if (tok) { maxTokens = parseInt(tok, 10); if (!Number.isFinite(maxTokens) || maxTokens < 16 || maxTokens > 131072) return alert('max_tokens: 16–131072'); } if (modelEditIdx == null && myModels.length >= 10) return alert('Не больше 10 моделей'); const entry = { id: modelEditIdx == null ? `me-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` : myModels[modelEditIdx].id, name, model, base_url: base, api_key: key, max_tokens: maxTokens }; if (modelEditIdx == null) myModels.push(entry); else myModels[modelEditIdx] = entry; hideModelForm(); renderModelsList(); refreshModelSelects(); } function deleteModel(i) { const m = myModels[i]; if (!m) return; if (!confirm(`Удалить модель «${m.name}»?`)) return; myModels.splice(i, 1); const dst = document.getElementById('fbDefaultModel'); if (dst && dst.value === m.id) dst.value = ''; renderModelsList(); refreshModelSelects(); } function fillDefaultModelOptions(preserve) { const src = document.getElementById('fbModel'); const dst = document.getElementById('fbDefaultModel'); if (!src || !dst) return; dst.innerHTML = '' + src.innerHTML; dst.value = preserve || ''; if (preserve && dst.value !== preserve) { const o = document.createElement('option'); o.value = preserve; o.textContent = `${preserve} (недоступна)`; dst.appendChild(o); dst.value = preserve; } } async function refreshModelSelects() { const keep = document.getElementById('fbDefaultModel').value; await loadAiProfiles(); fillDefaultModelOptions(keep); } function openPromptModal() { fillDefaultModelOptions(userSettings.settings.feedback_profile); const ta = document.getElementById('fbPromptText'); ta.value = userSettings.settings.feedback_prompt || ''; ta.placeholder = userSettings.defaults.feedback_prompt || 'Системный шаблон'; document.getElementById('fbPromptStatus').hidden = true; hideModelForm(); renderModelsList(); updatePromptCount(); document.getElementById('fbPromptModal').classList.add('open'); if (window.lucide) lucide.createIcons(); } async function savePromptSettings() { const st = document.getElementById('fbPromptStatus'); if (!document.getElementById('fbModel').options.length) { st.hidden = false; st.innerHTML = 'Список моделей не загружен — сохранение отменено'; return; } const body = { settings: { feedback_prompt: document.getElementById('fbPromptText').value.trim(), feedback_profile: document.getElementById('fbDefaultModel').value || '', feedback_models: myModels.length ? JSON.stringify(myModels) : '', } }; try { const r = await fetch(`${API}/api/user-settings`, { method: 'PUT', headers: {...hdr(), 'Content-Type': 'application/json'}, body: JSON.stringify(body) }); const j = await r.json().catch(() => ({})); if (!r.ok) { st.hidden = false; st.innerHTML = `${escapeHtml(j.error || 'Не удалось сохранить')}`; return; } userSettings.settings = j.settings || {}; userSettings.defaults = j.defaults || userSettings.defaults; parseMyModels(); renderModelsList(); await loadAiProfiles(); st.hidden = false; st.innerHTML = 'Сохранено'; setTimeout(closePromptModal, 700); } catch (e) { st.hidden = false; st.innerHTML = 'Не удалось сохранить'; } } function todayIso() { try { if (window.datetime && datetime.todayIso) return datetime.todayIso(); } catch (e) {} const d = new Date(); const y=d.getFullYear(), mo=String(d.getMonth()+1).padStart(2,'0'), da=String(d.getDate()).padStart(2,'0'); return `${y}-${mo}-${da}`; } let residentNamesLoaded = false; const residentGroups = new Map(); const normName = s => String(s || '').trim().replace(/\s+/g, ' ').toLowerCase(); async function loadResidentNames() { if (residentNamesLoaded) return; try { const r = await fetch(`${API}/api/students/names-with-groups`, { headers: hdr() }); if (!r.ok) return; const rows = await r.json(); residentGroups.clear(); (rows||[]).forEach(x => { const k = normName(x.name); if (k && !residentGroups.has(k)) residentGroups.set(k, x.group_id); }); const dl = document.getElementById('fbResidentList'); if (dl) dl.innerHTML = (rows||[]).map(x => `'; if (fsel) fsel.innerHTML = ''; (list||[]).filter(x=>!x.deleted_at).forEach(g => { const opt = document.createElement('option'); opt.value = g.id; opt.textContent = g.name; sel && sel.appendChild(opt.cloneNode(true)); fsel && fsel.appendChild(opt); }); groupsLoaded = true; } } catch (e) {} } async function loadAiProfiles() { try { const r = await fetch(`${API}/api/ai/profiles/options`, { headers: hdr() }); if (r.ok) { const d = await r.json(); const sel = document.getElementById('fbModel'); if (!sel) return; const keep = sel.value; sel.innerHTML = ''; if (myModels.length) { const mine = document.createElement('optgroup'); mine.label = 'Мои модели'; myModels.forEach(m => { const o = document.createElement('option'); o.value = m.id; o.textContent = m.name; mine.appendChild(o); }); sel.appendChild(mine); } const sys = document.createElement('optgroup'); sys.label = 'Системные'; const nativeOpt = document.createElement('option'); nativeOpt.value = 'native'; nativeOpt.textContent = 'Нативная (llama.cpp)'; sys.appendChild(nativeOpt); (d.profiles||[]).forEach(p => { const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; sys.appendChild(o); }); sel.appendChild(sys); aiActive = d.active || 'native'; if (keep && [...sel.options].some(o => o.value === keep)) sel.value = keep; else sel.value = aiActive; } } catch (e) {} } async function fetchShorts(q) { if (!q || q.length < 2) { shortsListCache = []; renderShorts([]); return; } try { const r = await fetch(`${API}/api/shorts?search=${encodeURIComponent(q)}&limit=50&deleted=0&used=0`, { headers: hdr() }); if (r.ok) { const j = await r.json(); shortsListCache = j.items || []; renderShorts(shortsListCache); } } catch (e) {} } async function loadLastPastReview() { const resident = document.getElementById('fbResident').value.trim(); if (!resident) return; try { const r = await fetch(`${API}/api/feedbacks/last?resident=${encodeURIComponent(resident)}`, { headers: hdr() }); if (r.ok) { const row = await r.json(); if (row && row.text) document.getElementById('fbPastReview').value = row.text; } } catch (e) {} } function renderShorts(items) { const wrap = document.getElementById('fbShortsWrap'); const list = document.getElementById('fbShortsList'); if (!list || !wrap) return; if (!items.length) { wrap.style.display = 'none'; list.innerHTML = ''; return; } wrap.style.display = ''; list.innerHTML = items.map(s => { const text = (s.message||'').slice(0,120); const topic = s.topic || '-'; return `
`; }).join(''); } function resetForm() { document.getElementById('fbId').value = ''; document.getElementById('fbDate').value = todayIso(); document.getElementById('fbGroup').value = ''; document.getElementById('fbResident').value = ''; document.getElementById('fbTopic').value = ''; document.getElementById('fbPastCheck').checked = false; document.getElementById('fbPastReviewWrap').style.display = 'none'; document.getElementById('fbPastReview').value = ''; document.getElementById('fbShortsWrap').style.display = 'none'; document.getElementById('fbShortsList').innerHTML = ''; document.getElementById('fbText').value = ''; lastFbAutoText = ''; document.getElementById('fbTextHint').textContent = '0 / 5000'; document.getElementById('fbAiCheck').checked = true; document.getElementById('fbAiStatus').hidden = true; const ms = document.getElementById('fbModel'); const pref = userSettings.settings.feedback_profile; if (pref && [...ms.options].some(o => o.value === pref)) ms.value = pref; else if (aiActive && [...ms.options].some(o => o.value === aiActive)) ms.value = aiActive; document.getElementById('fbModalTitle').textContent = 'Добавить отзыв'; document.getElementById('fbSubmit').textContent = 'Отправить в очередь'; } async function loadFeedback(id) { try { const r = await fetch(`${API}/api/feedbacks/${id}`, { headers: hdr() }); if (r.ok) { const f = await r.json(); document.getElementById('fbId').value = f.id; document.getElementById('fbDate').value = f.feedback_date || todayIso(); document.getElementById('fbGroup').value = f.group_id || ''; document.getElementById('fbResident').value = f.resident || ''; document.getElementById('fbTopic').value = f.topic || ''; if (f.past_review) { document.getElementById('fbPastCheck').checked = true; document.getElementById('fbPastReviewWrap').style.display = ''; document.getElementById('fbPastReview').value = f.past_review; } document.getElementById('fbText').value = f.text || ''; lastFbAutoText = ''; document.getElementById('fbTextHint').textContent = `${(f.text||'').length} / 5000`; document.getElementById('fbAiCheck').checked = (f.ai_status === 'pending' || f.ai_status === 'processing' || f.ai_status === 'none') ? true : false; const fms = document.getElementById('fbModel'); if (f.ai_profile && [...fms.options].some(o => o.value === f.ai_profile)) fms.value = f.ai_profile; document.getElementById('fbModalTitle').textContent = 'Редактировать отзыв'; document.getElementById('fbSubmit').textContent = 'Сохранить'; if (f.short_message_ids && f.short_message_ids.length) { try { const list = []; for (const sid of f.short_message_ids.slice(0,20)) { const sr = await fetch(`${API}/api/shorts/${sid}`, { headers: hdr() }); if (sr.ok) list.push(await sr.json()); } renderShorts(list); } catch (e) {} } } } catch (e) {} } async function loadFeedbacks() { const el = document.getElementById('feedbackList'); const st = document.getElementById('feedbackState'); if (!el) return; el.innerHTML = '
Загрузка…
'; const q = new URLSearchParams(); q.set('limit', PAGE_SIZE); q.set('offset', (page-1)*PAGE_SIZE); const s = document.getElementById('fbSearch').value.trim(); if (s) q.set('search', s); const g = document.getElementById('fbGroupFilter').value; if (g) q.set('group_id', g); const stv = document.getElementById('fbStatusFilter').value; if (stv) q.set('status', stv); const df = document.getElementById('fbDateFrom').value; if (df) q.set('date_from', df); const dt = document.getElementById('fbDateTo').value; if (dt) q.set('date_to', dt); if (document.getElementById('fbDeletedToggle').checked) q.set('deleted', '1'); try { const r = await fetch(`${API}/api/feedbacks?${q.toString()}`, { headers: hdr() }); if (r.ok) { const j = await r.json(); feedbacks = j.items || []; total = j.total || 0; el.innerHTML = feedbacks.length ? feedbacks.map(fbItemHtml).join('') : '
Список пуст
'; if (st) st.textContent = `Всего: ${total}`; renderPager(total, page, PAGE_SIZE, p => { page = p; loadFeedbacks(); }); } else { el.innerHTML = '
Ошибка загрузки
'; } } catch (e) { el.innerHTML = '
Ошибка
'; } if (window.lucide) lucide.createIcons(); } function renderPager(total, cur, size, onChange) { const el = document.getElementById('pager'); if (!el) return; const pages = Math.max(1, Math.ceil(total/size)); if (pages <= 1) { el.innerHTML = ''; return; } let html = ''; for (let i=1;i<=pages;i++) { html += ``; } el.innerHTML = html; el.querySelectorAll('[data-page]').forEach(b => b.addEventListener('click', () => onChange(parseInt(b.dataset.page,10)))); } document.addEventListener('DOMContentLoaded', async () => { await checkAuth().catch(()=>{}); buildSidebar('feedback'); await loadGroups(); await loadUserSettings(); await loadAiProfiles(); await loadResidentNames(); loadTopics(); loadFeedbacks(); connectFeedbackLive(); document.getElementById('addFeedbackBtn').addEventListener('click', () => openModal({})); document.getElementById('fbCancel').addEventListener('click', closeModal); document.getElementById('fbPromptBtn').addEventListener('click', openPromptModal); document.getElementById('fbPromptClose').addEventListener('click', closePromptModal); document.getElementById('fbPromptModal').addEventListener('click', e => { if (e.target.id === 'fbPromptModal') closePromptModal(); }); document.getElementById('fbPromptText').addEventListener('input', updatePromptCount); document.getElementById('fbPromptReset').addEventListener('click', () => { document.getElementById('fbPromptText').value = ''; document.getElementById('fbDefaultModel').value = ''; document.getElementById('fbPromptStatus').hidden = true; updatePromptCount(); }); document.getElementById('fbPromptSave').addEventListener('click', savePromptSettings); document.getElementById('fbModelAdd').addEventListener('click', () => showModelForm(null)); document.getElementById('fbMCancel').addEventListener('click', hideModelForm); document.getElementById('fbMSave').addEventListener('click', saveModelForm); document.getElementById('fbModelsList').addEventListener('click', e => { const ed = e.target.closest('[data-medit]'); if (ed) { showModelForm(parseInt(ed.dataset.medit, 10)); return; } const del = e.target.closest('[data-mdel]'); if (del) deleteModel(parseInt(del.dataset.mdel, 10)); }); document.getElementById('feedbackModal').addEventListener('click', e => { if (e.target.id === 'feedbackModal') closeModal(); }); document.getElementById('fbForm').addEventListener('submit', async e => { e.preventDefault(); const id = document.getElementById('fbId').value; const body = { feedback_date: document.getElementById('fbDate').value, group_id: parseInt(document.getElementById('fbGroup').value,10), resident: document.getElementById('fbResident').value.trim(), topic: document.getElementById('fbTopic').value.trim(), past_review: document.getElementById('fbPastCheck').checked ? document.getElementById('fbPastReview').value.trim() : null, text: document.getElementById('fbText').value.trim(), ai_check: document.getElementById('fbAiCheck').checked, ai_profile: document.getElementById('fbModel').value || null, }; const shortIds = []; document.querySelectorAll('input[name="fbShort"]:checked').forEach(c => shortIds.push(parseInt(c.value,10))); body.short_message_ids = shortIds; const url = id ? `${API}/api/feedbacks/${id}` : `${API}/api/feedbacks`; const method = id ? 'PUT' : 'POST'; try { const r = await fetch(url, { method, headers: {...hdr(), 'Content-Type':'application/json'}, body: JSON.stringify(body) }); if (r.ok) { closeModal(); loadFeedbacks(); } } catch (e) {} }); document.getElementById('fbResident').addEventListener('input', () => { clearTimeout(shortsFetchTimer); const v = document.getElementById('fbResident').value.trim(); applyResidentGroup(); shortsFetchTimer = setTimeout(() => fetchShorts(v), 400); }); document.getElementById('fbResident').addEventListener('change', applyResidentGroup); document.getElementById('fbPastCheck').addEventListener('change', e => { document.getElementById('fbPastReviewWrap').style.display = e.target.checked ? '' : 'none'; if (e.target.checked) loadLastPastReview(); }); document.getElementById('fbText').addEventListener('input', () => { document.getElementById('fbTextHint').textContent = `${document.getElementById('fbText').value.length} / 5000`; }); document.getElementById('fbTopic').addEventListener('change', applyTopicText); document.getElementById('feedbackList').addEventListener('click', async e => { const aiinfo = e.target.closest('[data-aiinfo]'); if (aiinfo) { const pop = document.getElementById(`fbAiPop-${aiinfo.dataset.aiinfo}`); if (pop) { const wasOpen = !pop.hidden; closeAiPops(); pop.hidden = wasOpen; } return; } const sentBtn = e.target.closest('[data-sent]'); if (sentBtn) { const id = parseInt(sentBtn.dataset.sent, 10); if (!id) return; const f = feedbacks.find(x => x.id === id); const next = !(f && f.sent_at); try { const r = await fetch(`${API}/api/feedbacks/${id}/sent`, { method:'PUT', headers: {...hdr(), 'Content-Type':'application/json'}, body: JSON.stringify({ sent: next }) }); if (r.ok) loadFeedbacks(); } catch (e) {} return; } const edit = e.target.closest('[data-edit]'); if (edit) { openModal({id: parseInt(edit.dataset.edit,10)}); return; } const del = e.target.closest('[data-del]'); if (del) { const id = parseInt(del.dataset.id||del.dataset.del,10); if (!id) return; const row = feedbacks.find(x => x.id === id); const perm = !!(row && row.deleted_at); if (!confirm(perm ? 'Удалить отзыв навсегда? Это действие нельзя отменить.' : 'Удалить отзыв?')) return; const url = perm ? `${API}/api/feedbacks/${id}/permanent` : `${API}/api/feedbacks/${id}`; try { const r = await fetch(url, { method:'DELETE', headers: hdr() }); if (r.ok) { loadFeedbacks(); return; } const err = await r.json().catch(() => null); alert((err && err.error) || `Ошибка ${r.status}`); } catch (err) { alert('Ошибка сети'); } return; } const ret = e.target.closest('[data-restore]'); if (ret) { const id = parseInt(ret.dataset.id||ret.dataset.restore,10); if (!id) return; try { const r = await fetch(`${API}/api/feedbacks/${id}/restore`, { method:'PUT', headers: hdr() }); if (r.ok) { loadFeedbacks(); return; } const err = await r.json().catch(() => null); alert((err && err.error) || `Ошибка ${r.status}`); } catch (err) { alert('Ошибка сети'); } return; } const retry = e.target.closest('[data-retry]'); if (retry) { const id = parseInt(retry.dataset.id||retry.dataset.retry,10); if (!id) return; try { const r = await fetch(`${API}/api/feedbacks/${id}/ai/retry`, { method:'POST', headers: hdr() }); if (r.ok) loadFeedbacks(); } catch (e) {} return; } }); ['fbSearch','fbGroupFilter','fbStatusFilter','fbDateFrom','fbDateTo','fbDeletedToggle'].forEach(id => { document.getElementById(id).addEventListener('change', () => { page = 1; loadFeedbacks(); }); if (id === 'fbSearch') document.getElementById(id).addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { page = 1; loadFeedbacks(); }, 350); }); }); if (window.lucide) lucide.createIcons(); }); let liveSource = null, liveReconnectTimer = null; function connectFeedbackLive() { if (liveSource) { liveSource.close(); liveSource = null; } const connect = () => { const t = sessionStorage.getItem('authToken'); if (!t) return; liveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`); liveSource.addEventListener('feedbacks_changed', () => { page = 1; loadFeedbacks(); }); liveSource.addEventListener('feedback_status', () => { loadFeedbacks(); }); liveSource.onerror = () => { if (liveSource) { liveSource.close(); liveSource = null; } if (liveReconnectTimer) clearTimeout(liveReconnectTimer); liveReconnectTimer = setTimeout(connect, 5000); }; }; connect(); } })();