'use strict'; /* admin → games (game features + free-student features) section */ (function () { 'use strict'; let inited = false; const GAME_FEATURES = [ { key: 'hangman', label: 'Виселица', desc: 'Игра «Угадай слово» — отгадывание терминов по буквам', icon: 'gamepad-2' }, { key: 'crossword', label: 'Кроссворд', desc: 'Кроссворд из терминов — генерируется автоматически по темам', icon: 'grid-3x3' }, { key: 'pet', label: 'Питомец', desc: 'Виртуальный питомец, отражающий активность ученика', icon: 'heart' }, { key: 'red_book', label: 'Красная книга', desc: 'Интерактивная Красная книга РБ: виды, биомы, пищевые сети, квесты', icon: 'leaf' }, { key: 'collection', label: 'Коллекция', desc: 'Коллекция карточек и достижений — игровой прогресс ученика', icon: 'layers' }, { key: 'flashcards', label: 'Флеш-карточки', desc: 'Карточки для запоминания терминов и понятий методом интервальных повторений', icon: 'square-stack' }, { key: 'knowledge_map', label: 'Карта знаний', desc: 'Визуальная карта тем и связей между биологическими понятиями', icon: 'share-2' }, { key: 'board', label: 'Доска', desc: 'Классная доска с объявлениями, постами и обсуждениями', icon: 'layout-dashboard'}, { key: 'biochem', label: 'Биохимия', desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' }, { key: 'live_quiz', label: 'Живая викторина', desc: 'Синхронная викторина в реальном времени для всего класса', icon: 'radio' }, { key: 'classroom', label: 'Онлайн-уроки (classroom)', desc: 'Синхронные онлайн-уроки с доской и видео', icon: 'video' }, { key: 'assistant', label: 'Помощник «Квантик»', desc: 'Плавающий помощник: подсказки по разделам, напоминания и «Спроси Квантика»', icon: 'sparkles' }, ]; const FS_FEATURES = [ { key: 'gamification', label: 'Геймификация', desc: 'XP, уровни, достижения, монеты, стрики, магазин', icon: 'trophy' }, { key: 'hangman', label: 'Виселица', desc: 'Игра «Угадай слово» — отгадывание терминов по буквам', icon: 'gamepad-2' }, { key: 'crossword', label: 'Кроссворд', desc: 'Кроссворд из терминов — генерируется автоматически', icon: 'grid-3x3' }, { key: 'pet', label: 'Питомец', desc: 'Виртуальный питомец, отражающий активность ученика', icon: 'heart' }, { key: 'red_book', label: 'Красная книга', desc: 'Интерактивная Красная книга РБ: виды, биомы, квесты', icon: 'leaf' }, { key: 'collection', label: 'Коллекция', desc: 'Коллекция карточек и игровой прогресс ученика', icon: 'layers' }, { key: 'lab', label: 'Лаборатория', desc: 'Виртуальные симуляции и интерактивные опыты', icon: 'flask-conical' }, { key: 'knowledge_map',label: 'Карта знаний', desc: 'Визуальная карта тем и связей между понятиями', icon: 'map' }, { key: 'flashcards', label: 'Флеш-карточки', desc: 'Карточки для повторения терминов и понятий', icon: 'square-stack' }, { key: 'board', label: 'Доска', desc: 'Классная доска с объявлениями и постами', icon: 'layout-dashboard' }, { key: 'biochem', label: 'Биохимия', desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' }, { key: 'live_quiz', label: 'Живая викторина', desc: 'Синхронная викторина в реальном времени для всего класса', icon: 'radio' }, ]; /* ── Конфиг LLM для помощника «Квантик» ── */ var IN_STYLE = '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_STYLE = '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)'; async function renderAssistantLlmCard(grid) { if (!grid || document.getElementById('asst-llm-card')) return; var wrap = document.createElement('div'); wrap.id = 'asst-llm-card'; wrap.className = 'perm-card'; wrap.style.cssText = 'flex-direction:column;align-items:stretch;gap:9px;margin-bottom:14px'; wrap.innerHTML = '
Помощник «Квантик» — модель (ИИ)
' + '
Подключение LLM к «Спроси Квантика». OpenAI-совместимый эндпоинт. Без ключа — режим обычного FAQ.
' + '
' + '' + '' + '' + '' + '
' + '' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '' + '' + '
' + '
'; grid.parentNode.insertBefore(wrap, grid); if (window.lucide) lucide.createIcons(); var q = function (s) { return wrap.querySelector(s); }; var cfg = {}; try { cfg = await LS.adminGetAssistant(); } catch (e) {} var presetSel = q('#asst-preset'); (cfg.presets || []).forEach(function (p, i) { var o = document.createElement('option'); o.value = String(i); o.textContent = p.name; presetSel.appendChild(o); }); q('#asst-url').value = cfg.url || ''; q('#asst-model').value = cfg.model || ''; q('#asst-key').placeholder = cfg.hasKey ? 'Ключ сохранён — введите новый, чтобы заменить' : 'API-ключ'; function setStatus() { q('#asst-llm-status').innerHTML = cfg.active ? '● Подключено — «Спроси» отвечает через ИИ' : '○ Ключ не задан — работает обычный FAQ-режим'; q('#asst-rag').checked = cfg.rag !== false; q('#asst-exambtn').checked = !!cfg.examButtons; q('#asst-chunks').textContent = (cfg.chunks || 0) + ' фрагментов учебников в индексе'; var u = cfg.usage || {}, u30 = cfg.usage30 || {}; q('#asst-usage').innerHTML = 'Сегодня: ' + (u.model_calls || 0) + ' к ИИ, ' + (u.cache_hits || 0) + ' из кэша, ' + (u.faq || 0) + ' FAQ. ' + 'За 30 дней: ' + (u30.model_calls || 0) + ' / ' + (u30.cache_hits || 0) + ' / ' + (u30.faq || 0) + '.'; } setStatus(); 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(); cfg.chunks = (r && r.chunks) || 0; setStatus(); LS.toast('Готово: ' + cfg.chunks + ' фрагментов', 'success'); } catch (e) { LS.toast('Ошибка индексации', 'error'); } finally { btn.disabled = false; btn.textContent = 'Переиндексировать учебники'; } }); presetSel.addEventListener('change', function () { var p = (cfg.presets || [])[Number(presetSel.value)]; if (p) { q('#asst-url').value = p.url; q('#asst-model').value = p.model; } }); q('#asst-save').addEventListener('click', async function () { var body = { url: q('#asst-url').value, model: q('#asst-model').value }; var k = q('#asst-key').value.trim(); if (k) body.key = k; try { await LS.adminSaveAssistant(body); q('#asst-key').value = ''; LS.toast('Сохранено', 'success'); cfg = await LS.adminGetAssistant(); cfg.hasKey && (q('#asst-key').placeholder = 'Ключ сохранён — введите новый, чтобы заменить'); setStatus(); } catch (e) { LS.toast('Ошибка: ' + (e.message || ''), 'error'); } }); q('#asst-test').addEventListener('click', async function () { var res = q('#asst-test-res'); res.innerHTML = 'Проверяю…'; var body = { url: q('#asst-url').value, model: q('#asst-model').value }; var k = q('#asst-key').value.trim(); if (k) body.key = k; try { var r = await LS.adminTestAssistant(body); res.innerHTML = r && r.ok ? '✓ Работает (' + (r.model || '') + '): ' + (r.sample || 'ответ получен').replace(/' : '✗ ' + ((r && (r.error || ('HTTP ' + r.status))) || 'ошибка').toString().slice(0, 200).replace(/'; } catch (e) { res.innerHTML = '✗ ' + (e.message || 'ошибка') + ''; } }); q('#asst-clearkey').addEventListener('click', async function () { if (!await LS.confirm('Очистить сохранённый ключ? «Спроси» вернётся в FAQ-режим.', { title: 'Очистить ключ?', confirmText: 'Очистить' })) return; try { await LS.adminSaveAssistant({ clearKey: true }); LS.toast('Ключ очищен', 'success'); cfg = await LS.adminGetAssistant(); q('#asst-key').placeholder = 'API-ключ'; setStatus(); } catch (e) { LS.toast('Ошибка', 'error'); } }); } async function loadGamesAdmin() { const grid = document.getElementById('games-features-grid'); renderAssistantLlmCard(grid); try { const features = await LS.api('/api/admin/features'); grid.innerHTML = ''; for (const f of GAME_FEATURES) { const enabled = features[f.key] !== false; const card = document.createElement('div'); card.className = 'perm-card' + (enabled ? ' enabled' : ''); card.innerHTML = `
${f.label}
${f.desc}
`; grid.appendChild(card); } if (window.lucide) lucide.createIcons(); } catch(e) { grid.innerHTML = '
Ошибка загрузки
'; } } async function toggleGameFeature(key, enabled, checkbox) { try { await LS.api('/api/admin/features', { method: 'PATCH', body: JSON.stringify({ [key]: enabled }), }); const card = checkbox.closest('.perm-card'); if (card) card.classList.toggle('enabled', enabled); LS.toast(enabled ? 'Функция включена' : 'Функция отключена', 'success'); } catch(e) { checkbox.checked = !enabled; LS.toast('Ошибка: ' + e.message, 'error'); } } async function loadFsFeatures() { const grid = document.getElementById('fs-features-grid'); try { const features = await LS.api('/api/admin/free-student-features'); grid.innerHTML = ''; for (const f of FS_FEATURES) { const enabled = features[f.key] !== false; const card = document.createElement('div'); card.className = 'perm-card' + (enabled ? ' enabled' : ''); card.innerHTML = `
${f.label}
${f.desc}
`; grid.appendChild(card); } if (window.lucide) lucide.createIcons(); } catch(e) { grid.innerHTML = '
Ошибка загрузки
'; } } async function toggleFsFeature(key, enabled, checkbox) { try { await LS.api('/api/admin/free-student-features', { method: 'PATCH', body: JSON.stringify({ [key]: enabled }), }); const card = checkbox.closest('.perm-card'); if (card) card.classList.toggle('enabled', enabled); LS.toast(enabled ? 'Модуль включён' : 'Модуль отключён', 'success'); } catch(e) { checkbox.checked = !enabled; LS.toast('Ошибка: ' + e.message, 'error'); } } async function load() { await loadGamesAdmin(); await loadFsFeatures(); } window.toggleGameFeature = toggleGameFeature; window.toggleFsFeature = toggleFsFeature; window.AdminSections = window.AdminSections || {}; window.AdminSections.games = { init: async () => { if (inited) return; inited = true; await load(); }, reload: load, }; })();