'use strict';
/* admin → «Помощник Квантик»: системный вкл/выкл + провайдеры ИИ (карточки,
* активный, авто-перехват, переключение модели Kilo) + RAG/экзамен/статистика. */
(function () {
'use strict';
let inited = false, editingId = null;
var esc = (window.LS && LS.escapeHtml) ? LS.escapeHtml : function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); };
var IN = 'padding:8px 11px;border:1px solid var(--border,#e2e8f0);border-radius:9px;font:inherit;font-size:.85rem;width:100%;box-sizing:border-box;background:var(--surface,#fff);color:var(--text,#0f172a)';
var SPARK = ' ';
function ensureStyle() {
if (document.getElementById('asst-adm-style')) return;
var s = document.createElement('style'); s.id = 'asst-adm-style';
s.textContent = [
'.asst-prov{display:flex;flex-direction:column;gap:9px;}',
'.asst-pcard{display:flex;align-items:center;gap:12px;padding:11px 13px;border:1.5px solid var(--border,#e2e8f0);border-radius:13px;background:var(--surface,#fff);transition:border-color .15s,box-shadow .15s,transform .12s;}',
'.asst-pcard:hover{transform:translateY(-1px);box-shadow:0 6px 18px rgba(15,23,42,.07);}',
'.asst-pcard.active{border-color:#9B5DE5;background:rgba(155,93,229,.06);box-shadow:0 2px 12px rgba(155,93,229,.14);}',
'.asst-pcic{width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;background:rgba(155,93,229,.12);color:#9B5DE5;flex-shrink:0;}',
'.asst-pcb{flex:1;min-width:0;}',
'.asst-pcn{font-weight:800;font-size:.92rem;color:var(--text,#0f172a);display:flex;align-items:center;gap:7px;flex-wrap:wrap;}',
'.asst-pcs{font-size:.76rem;color:#8a94a6;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'.asst-bdg{font-size:.6rem;font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:2px 8px;border-radius:99px;}',
'.asst-bdg.act{background:#9B5DE5;color:#fff;}',
'.asst-bdg.key{background:rgba(5,150,82,.12);color:#059652;}',
'.asst-bdg.nokey{background:rgba(224,51,94,.12);color:#e0335e;}',
'.asst-pca{display:flex;gap:6px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;}',
'.asst-ib{border:1px solid var(--border,#e2e8f0);background:var(--surface,#fff);border-radius:8px;padding:5px 9px;font:inherit;font-size:.74rem;font-weight:700;cursor:pointer;color:var(--text-2,#475569);}',
'.asst-ib:hover{border-color:#9B5DE5;color:#9B5DE5;}',
'.asst-ib.primary{background:#9B5DE5;border-color:#9B5DE5;color:#fff;}',
'.asst-ib.primary:hover{background:#7e3eca;color:#fff;}',
'.asst-ksel{margin-top:7px;font:inherit;font-size:.78rem;padding:5px 9px;border:1px solid rgba(155,93,229,.3);border-radius:8px;background:var(--surface,#fff);color:var(--text,#0f172a);max-width:100%;}',
'.asst-flabel{font-size:.74rem;font-weight:700;color:var(--text-2,#475569);margin:2px 0 -3px;}',
'.asst-ptest{font-size:.8rem;line-height:1.5;padding:2px 0;}',
].join('');
document.head.appendChild(s);
}
async function render() {
var host = document.getElementById('assistant-admin');
if (!host) return;
ensureStyle();
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 || [], activeId = cfg.activeId, presets = cfg.presets || [], kiloModels = cfg.kiloModels || [];
// ── Баннер failover ──
if (cfg.failover) {
var fo = cfg.failover, rmap = { rate_limit: 'исчерпан лимит', http: 'ошибка API', timeout: 'таймаут', network: 'нет связи', error: 'ошибка' };
var when = ''; try { when = new Date(fo.at).toLocaleString('ru'); } catch (e) {}
var ban = document.createElement('div');
ban.style.cssText = 'margin-top:14px;padding:11px 14px;border-radius:11px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.4);color:#92400e;font-size:.84rem;line-height:1.5';
ban.innerHTML = fo.servedName
? 'Переключение провайдера. «' + esc(fo.failedName || '?') + '» недоступен (' + (rmap[fo.reason] || 'ошибка') + ') — работаю на «' + esc(fo.servedName) + '». ' + (when ? '' + esc(when) + ' ' : '') + 'Снимется автоматически, когда активный снова заработает. '
: 'Все провайдеры ИИ недоступны (' + (rmap[fo.reason] || 'ошибка') + '). Сейчас «Спроси» в FAQ-режиме.';
host.appendChild(ban);
}
// ── Провайдеры (карточки) ──
var pc = document.createElement('div');
pc.className = 'perm-card';
pc.style.cssText = 'flex-direction:column;align-items:stretch;gap:11px;margin-top:14px';
pc.innerHTML =
' Провайдеры ИИ для «Спроси Квантика»
' +
'Активный (фиолетовый) используется первым. При лимите/ошибке Квантик автоматически пробует следующего с ключом. Без ключей — режим FAQ.
' +
'
' +
'
' +
' ' +
' Добавить провайдера ' +
'';
host.appendChild(pc);
// ── Настройки/статистика ──
var u = cfg.usage || {}, u30 = cfg.usage30 || {}, f = cfg.feedback || {};
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 =
' Настройки и статистика
' +
' Искать ответы по учебникам (RAG) ' +
' Кнопки помощника на карточках экзамена ' +
'Переиндексировать учебники ' + (cfg.chunks || 0) + ' фрагментов
' +
'Сегодня: ' + (u.model_calls || 0) + ' к ИИ, ' + (u.cache_hits || 0) + ' из кэша, ' + (u.faq || 0) + ' FAQ. За 30 дней: ' + (u30.model_calls || 0) + ' / ' + (u30.cache_hits || 0) + ' / ' + (u30.faq || 0) + '.
' +
'Оценки (30 дн): ' + (f.up || 0) + ' лайков, ' + (f.down || 0) + ' дизлайков' + ((f.recent || []).length ? '. Не помогло: ' + f.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); };
// ── рендер карточек провайдеров ──
var listEl = Q('#asst-prov');
if (!providers.length) { listEl.innerHTML = 'Пока нет провайдеров — добавьте ниже.
'; }
else listEl.innerHTML = providers.map(function (p) {
var isKilo = /kilocode\.ai/.test(p.url || '');
var act = p.id === activeId;
var ksel = '';
if (isKilo) {
var opts = kiloModels.slice();
if (!opts.some(function (m) { return m.id === p.model; })) opts = [{ id: p.model, label: p.model }].concat(opts);
ksel = '' + opts.map(function (m) { return '' + esc(m.label) + ' '; }).join('') + ' ';
}
return '' +
'
' + SPARK + '
' +
'
' + esc(p.name || 'Провайдер') +
(act ? 'активен ' : '') +
'' + (p.hasKey ? 'ключ есть' : 'нет ключа') + '
' +
'
' + esc(p.model || '') + '
' + ksel + '
' +
'
' +
(act ? '' : 'Сделать активным ') +
'Тест ' +
'Изм. ' +
'Удалить ' +
'
';
}).join('');
function openForm(show) { Q('#asst-form').style.display = show ? 'flex' : 'none'; }
function clearForm() { editingId = null; Q('#asst-fhead').textContent = 'Новый провайдер'; Q('#asst-preset').value = ''; Q('#asst-name').value = ''; Q('#asst-url').value = ''; Q('#asst-model').value = ''; Q('#asst-key').value = ''; Q('#asst-key').placeholder = 'ключ'; toggleKilo(); openForm(false); }
function toggleKilo() {
var isKilo = /kilocode\.ai/.test(Q('#asst-url').value || '');
Q('#asst-kbox').style.display = isKilo ? '' : 'none';
if (isKilo) {
var sel = Q('#asst-kmodels');
if (!sel.options.length) sel.innerHTML = kiloModels.map(function (m) { return '' + esc(m.label) + ' '; }).join('');
}
}
// карточные действия
listEl.querySelectorAll('[data-ksel]').forEach(function (sel) {
sel.addEventListener('change', function () {
LS.adminSaveProvider({ id: sel.getAttribute('data-ksel'), model: sel.value }).then(function () { LS.toast('Модель обновлена', 'success'); render(); }).catch(function () { LS.toast('Ошибка', 'error'); });
});
});
listEl.querySelectorAll('[data-act]').forEach(function (b) {
b.addEventListener('click', async function () {
var id = b.getAttribute('data-id'), act = b.getAttribute('data-act');
if (act === 'activate') { try { await LS.adminSetActiveProvider(id); LS.toast('Активный провайдер обновлён', 'success'); render(); } catch (e) { LS.toast('Ошибка', 'error'); } }
else 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-fhead').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 ? 'ключ сохранён — введите новый, чтобы заменить' : 'ключ';
toggleKilo(); openForm(true); Q('#asst-form').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); Q('#asst-name').focus();
} else if (act === 'test') {
var res = Q('#asst-ptest'); res.innerHTML = 'Проверяю…';
try { var r = await LS.adminTestAssistant({ id: id }); res.innerHTML = r && r.ok ? '✓ Работает (' + esc(r.model || '') + '): ' + esc(String(r.sample || 'ответ получен')) + ' ' : '✗ ' + esc(String((r && (r.error || ('HTTP ' + r.status))) || 'ошибка').slice(0, 200)) + ' '; } catch (e) { res.innerHTML = '✗ ' + esc(e.message || 'ошибка') + ' '; }
}
});
});
// форма
Q('#asst-ftoggle').addEventListener('click', function () { var open = Q('#asst-form').style.display !== 'none'; if (open) { clearForm(); } else { clearForm(); openForm(true); Q('#asst-name').focus(); } });
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; toggleKilo(); } });
Q('#asst-url').addEventListener('input', toggleKilo);
Q('#asst-kmodels').addEventListener('change', function () { Q('#asst-model').value = this.value; });
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 };
})();