'use strict'; /* admin → «Помощник Квантик»: системный вкл/выкл + несколько провайдеров ИИ * (ключи/модели) с выбором активного и авто-перехватом при лимите + RAG, кнопки * экзамена, статистика и качество. */ (function () { 'use strict'; let inited = false; var editingId = null; var IN = 'padding:8px 10px;border:1px solid var(--border,#e2e8f0);border-radius:8px;font:inherit;font-size:.85rem;width:100%;box-sizing:border-box;background:var(--surface,#fff);color:var(--text,#0f172a)'; var BTN = 'padding:8px 14px;border-radius:9px;border:1px solid var(--border,#e2e8f0);background:var(--surface,#fff);font:inherit;font-size:.82rem;font-weight:700;cursor:pointer;color:var(--text-2,#475569)'; var SBTN = 'border:1px solid var(--border,#e2e8f0);background:var(--surface,#fff);border-radius:7px;padding:3px 9px;font:inherit;font-size:.74rem;font-weight:700;cursor:pointer;color:var(--text-2,#475569)'; var esc = (window.LS && LS.escapeHtml) ? LS.escapeHtml : function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }; async function render() { var host = document.getElementById('assistant-admin'); if (!host) return; host.innerHTML = ''; // ── Системный выключатель ── var feats = {}; try { feats = await LS.api('/api/admin/features'); } catch (e) {} var on = feats.assistant !== false; var master = document.createElement('div'); master.className = 'perm-card' + (on ? ' enabled' : ''); master.innerHTML = '
Помощник включён для всей системы
' + '
Выключатель «Квантика» для всех пользователей. Выключено — помощник не загружается нигде.
' + ''; host.appendChild(master); master.querySelector('#asst-master').addEventListener('change', function () { var v = this.checked; LS.api('/api/admin/features', { method: 'PATCH', body: JSON.stringify({ assistant: v }) }) .then(function () { master.classList.toggle('enabled', v); LS.toast(v ? 'Помощник включён' : 'Помощник выключен для всех', 'success'); }) .catch(function () { master.querySelector('#asst-master').checked = !v; LS.toast('Ошибка', 'error'); }); }); var cfg = {}; try { cfg = await LS.adminGetAssistant(); } catch (e) {} var providers = cfg.providers || []; var activeId = cfg.activeId; var presets = cfg.presets || []; // ── Провайдеры ИИ ── var pc = document.createElement('div'); pc.className = 'perm-card'; pc.style.cssText = 'flex-direction:column;align-items:stretch;gap:10px;margin-top:14px'; var rows = providers.length ? providers.map(function (p) { return ''; }).join('') : '
Пока нет провайдеров — добавьте ниже.
'; pc.innerHTML = '
Провайдеры ИИ для «Спроси Квантика»
' + '
Активный (отмечен точкой) используется первым. При лимите или ошибке Квантик автоматически пробует следующего с ключом. Без ключей — режим FAQ.
' + '
' + rows + '
' + '
' + '
' + '
Добавить провайдера
' + '' + '' + '' + '' + '' + '
' + '' + '' + '
'; host.appendChild(pc); // ── Настройки и статистика ── var sc = document.createElement('div'); sc.className = 'perm-card'; sc.style.cssText = 'flex-direction:column;align-items:stretch;gap:9px;margin-top:14px'; sc.innerHTML = '
Настройки и статистика
' + '' + '' + '
' + (cfg.chunks || 0) + ' фрагментов
' + '
Сегодня: ' + ((cfg.usage || {}).model_calls || 0) + ' к ИИ, ' + ((cfg.usage || {}).cache_hits || 0) + ' из кэша, ' + ((cfg.usage || {}).faq || 0) + ' FAQ. За 30 дней: ' + ((cfg.usage30 || {}).model_calls || 0) + ' / ' + ((cfg.usage30 || {}).cache_hits || 0) + ' / ' + ((cfg.usage30 || {}).faq || 0) + '.
' + '
Оценки (30 дн): ' + ((cfg.feedback || {}).up || 0) + ' лайков, ' + ((cfg.feedback || {}).down || 0) + ' дизлайков' + (((cfg.feedback || {}).recent || []).length ? '. Не помогло: ' + cfg.feedback.recent.map(function (x) { return '«' + esc(String(x.q || '').slice(0, 40)) + '»'; }).join(', ') : '') + '
'; host.appendChild(sc); if (window.lucide) lucide.createIcons(); var q = function (s) { return host.querySelector(s); }; function clearForm() { editingId = null; q('#asst-form-title').textContent = 'Добавить провайдера'; q('#asst-name').value = ''; q('#asst-url').value = ''; q('#asst-model').value = ''; q('#asst-key').value = ''; q('#asst-key').placeholder = 'API-ключ'; q('#asst-cancel').style.display = 'none'; } // активный провайдер pc.querySelectorAll('input[name="asst-active"]').forEach(function (r) { r.addEventListener('change', function () { LS.adminSetActiveProvider(this.value).then(function () { LS.toast('Активный провайдер обновлён', 'success'); }).catch(function () {}); }); }); // действия по провайдеру pc.querySelectorAll('[data-act]').forEach(function (b) { b.addEventListener('click', async function () { var id = b.getAttribute('data-id'), act = b.getAttribute('data-act'); if (act === 'del') { if (!await LS.confirm('Удалить провайдера?', { title: 'Удалить?', confirmText: 'Удалить' })) return; try { await LS.adminDeleteProvider(id); LS.toast('Удалён', 'success'); render(); } catch (e) { LS.toast('Ошибка', 'error'); } } else if (act === 'edit') { var p = providers.find(function (x) { return x.id === id; }); if (!p) return; editingId = id; q('#asst-form-title').textContent = 'Изменить: ' + (p.name || ''); q('#asst-name').value = p.name || ''; q('#asst-url').value = p.url || ''; q('#asst-model').value = p.model || ''; q('#asst-key').value = ''; q('#asst-key').placeholder = p.hasKey ? 'Ключ сохранён — введите новый, чтобы заменить' : 'API-ключ'; q('#asst-cancel').style.display = ''; q('#asst-name').focus(); } else if (act === 'test') { var res = q('#asst-prov-test'); res.innerHTML = 'Проверяю…'; try { var r = await LS.adminTestAssistant({ id: id }); res.innerHTML = r && r.ok ? '✓ Работает (' + (r.model || '') + '): ' + esc(String(r.sample || 'ответ получен')) + '' : '✗ ' + esc(String((r && (r.error || ('HTTP ' + r.status))) || 'ошибка').slice(0, 200)) + ''; } catch (e) { res.innerHTML = '✗ ' + esc(e.message || 'ошибка') + ''; } } }); }); // пресет → заполнить url/model/name q('#asst-preset').addEventListener('change', function () { var p = presets[Number(this.value)]; if (p) { q('#asst-url').value = p.url; q('#asst-model').value = p.model; if (!q('#asst-name').value) q('#asst-name').value = p.name; } }); q('#asst-cancel').addEventListener('click', clearForm); q('#asst-save').addEventListener('click', async function () { var body = { name: q('#asst-name').value, url: q('#asst-url').value, model: q('#asst-model').value }; if (editingId) body.id = editingId; var k = q('#asst-key').value.trim(); if (k) body.key = k; if (!body.url || !body.model) { LS.toast('Заполни URL и модель', 'warn'); return; } try { await LS.adminSaveProvider(body); LS.toast('Сохранено', 'success'); clearForm(); render(); } catch (e) { LS.toast('Ошибка: ' + (e.message || ''), 'error'); } }); // настройки q('#asst-rag').addEventListener('change', function () { LS.adminSaveAssistant({ rag: q('#asst-rag').checked }).then(function () { LS.toast('Сохранено', 'success'); }).catch(function () {}); }); q('#asst-exambtn').addEventListener('change', function () { LS.adminSaveAssistant({ examButtons: q('#asst-exambtn').checked }).then(function () { LS.toast('Сохранено (обновите страницу экзамена)', 'success'); }).catch(function () {}); }); q('#asst-reindex').addEventListener('click', async function () { var btn = q('#asst-reindex'); btn.disabled = true; btn.textContent = 'Индексирую…'; try { var r = await LS.adminReindexTextbooks(); q('#asst-chunks').textContent = ((r && r.chunks) || 0) + ' фрагментов'; LS.toast('Готово', 'success'); } catch (e) { LS.toast('Ошибка индексации', 'error'); } finally { btn.disabled = false; btn.textContent = 'Переиндексировать учебники'; } }); } window.AdminSections = window.AdminSections || {}; window.AdminSections.assistant = { init: async () => { if (inited) return; inited = true; await render(); }, reload: render, }; })();