dc073e2114
Админка (Управление → игры/фичи): карточка «Помощник Квантик — модель» — пресеты провайдеров, URL/модель, поле ключа, кнопки Сохранить/Проверить/ Очистить ключ, индикатор статуса. Конфиг в app_settings (без рестарта), откат на ENV/дефолты; нет ключа → автоматически FAQ-режим. Эндпоинты GET/PUT/POST /api/admin/assistant(/test), admin-only. «Спроси Квантика» теперь многоходовой чат: история диалога (последние 6 реплик) уходит модели, ответы рендерятся как чат-лента, кнопка «Очистить». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
719 lines
49 KiB
JavaScript
719 lines
49 KiB
JavaScript
'use strict';
|
||
/* assistant.js — «Квантик-ассистент»: плавающий компаньон на всех страницах.
|
||
* Подсказки по контексту + проактивные напоминания + поздравления + «Спроси».
|
||
* Правиловый движок (без модели). Состояние «видел» — на сервере (assistant_seen),
|
||
* дневной лимит/детект событий — в localStorage. Лицо = pet-sprite.js, данные —
|
||
* /api/assistant/context и /api/pet. Гейт фичи 'pet' проверяется на сервере.
|
||
* Грузится через sidebar.js (app-страницы) и серверный inject (учебник). */
|
||
(function () {
|
||
if (window.__assistantBooted) return;
|
||
window.__assistantBooted = true;
|
||
if (window.parent !== window) return; // не в iframe/embed
|
||
if (!window.LS || !LS.getToken || !LS.getToken()) return; // только залогиненным
|
||
|
||
var DAILY_CAP = 2; // консервативно: не больше 2 авто-подсказок в день
|
||
var AUTO_DELAY = 7000; // показать подсказку через 7с на странице
|
||
var reduceMotion = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
|
||
var SRV = null, PET = null, picked = null, root = null, bubble = null, openState = false;
|
||
|
||
/* ── helpers ─────────────────────────────────────────────────────────── */
|
||
function esc(s) { return (window.LS && LS.escapeHtml) ? LS.escapeHtml(String(s == null ? '' : s)) : String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
||
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||
function todayKey() { return new Date().toISOString().slice(0, 10); }
|
||
|
||
function pageId() {
|
||
var p = location.pathname.replace(/\/+$/, '') || '/';
|
||
if (p === '/' || p === '/dashboard') return 'dashboard';
|
||
if (p.indexOf('/textbook') === 0) return 'textbook';
|
||
if (p === '/classroom') return 'classroom';
|
||
if (p === '/board') return 'board';
|
||
if (p.indexOf('/exam') === 0) return 'exam';
|
||
if (p === '/flashcards') return 'flashcards';
|
||
if (p === '/my-materials') return 'materials';
|
||
if (p === '/lab') return 'lab';
|
||
if (p === '/theory' || p.indexOf('/course') === 0 || p.indexOf('/lesson') === 0) return 'theory';
|
||
if (p === '/textbooks') return 'textbooks';
|
||
if (p === '/library') return 'library';
|
||
if (p === '/knowledge-map') return 'knowledge';
|
||
if (p === '/biochem') return 'biochem';
|
||
if (p === '/red-book') return 'redbook';
|
||
if (p === '/crossword') return 'crossword';
|
||
if (p === '/hangman') return 'hangman';
|
||
if (p === '/collection') return 'collection';
|
||
if (p === '/analytics') return 'analytics';
|
||
if (p === '/gradebook') return 'gradebook';
|
||
if (p === '/lesson-history') return 'lessonhistory';
|
||
if (p === '/question-bank') return 'qbank';
|
||
if (p === '/classes') return 'classes';
|
||
if (p === '/homework') return 'homework';
|
||
if (p === '/pet') return 'pet';
|
||
return 'other';
|
||
}
|
||
var PAGE = pageId();
|
||
var SUPPRESS_PAGE = (PAGE === 'classroom'); // не мешаем на живом уроке
|
||
|
||
function quest(undoneOnly) {
|
||
var qs = (PET && PET.quests) || [];
|
||
for (var i = 0; i < qs.length; i++) if (!undoneOnly || !qs[i].done) return qs[i];
|
||
return null;
|
||
}
|
||
function activeToday() {
|
||
if (!PET) return true;
|
||
if (PET.daysSinceLogin === 0) {
|
||
var q = (PET.quests || []).find ? (PET.quests || []).find(function (x) { return x.id === 'xp30'; }) : null;
|
||
return q ? (q.progress || 0) > 0 : true; // есть прогресс сегодня
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* ── каталог правил ──────────────────────────────────────────────────── */
|
||
// scope: page | proactive | celebration. when(C) → bool. action(C) → {label,url}|null.
|
||
var RULES = [
|
||
// — контекстные подсказки генерируются из PAGE_HINTS ниже —
|
||
// — проактивные (из реальных данных) —
|
||
{ id: 'hw-overdue', scope: 'proactive', cooldownDays: 1, maxShows: 30,
|
||
when: function () { return !!(SRV && SRV.homework && SRV.homework.overdue); },
|
||
text: function () { return 'Просрочена домашка: «' + (SRV.homework.overdue.title || 'задание') + '». Загляни в раздел.'; },
|
||
action: function () { return { label: 'К домашке', url: '/homework' }; } },
|
||
{ id: 'hw-soon', scope: 'proactive', cooldownDays: 1, maxShows: 30,
|
||
when: function () { return !!(SRV && SRV.homework && SRV.homework.dueSoon); },
|
||
text: function () { return 'Скоро дедлайн: «' + (SRV.homework.dueSoon.title || 'задание') + '».'; },
|
||
action: function () { return { label: 'К домашке', url: '/homework' }; } },
|
||
{ id: 'lesson-continue', scope: 'proactive', cooldownDays: 1, maxShows: 60,
|
||
when: function () { return !!(SRV && SRV.activeLesson && SRV.activeLesson.courseId); },
|
||
text: function () { return 'Продолжи: «' + (SRV.activeLesson.courseTitle || SRV.activeLesson.lessonTitle || 'урок') + '».'; },
|
||
action: function () { return { label: 'Продолжить', url: '/course?id=' + SRV.activeLesson.courseId }; } },
|
||
{ id: 'cards-due', scope: 'proactive', cooldownDays: 1, maxShows: 60,
|
||
when: function () { return !!(SRV && SRV.dueCards > 0); },
|
||
text: function () { return 'К повторению ' + SRV.dueCards + ' ' + plural(SRV.dueCards, 'карточка', 'карточки', 'карточек') + ' — освежим память?'; },
|
||
action: function () { return { label: 'Повторить', url: '/flashcards' }; } },
|
||
{ id: 'streak-risk', scope: 'proactive', cooldownDays: 1, maxShows: 60,
|
||
when: function () { return !!(PET && PET.streakCurrent >= 1 && !activeToday() && new Date().getHours() >= 18); },
|
||
text: function () { return 'Серия ' + PET.streakCurrent + ' ' + plural(PET.streakCurrent, 'день', 'дня', 'дней') + ' под угрозой — позанимайся сегодня, чтобы не потерять.'; },
|
||
action: function () { return { label: 'Заниматься', url: '/exam-prep/math9' }; } },
|
||
{ id: 'quest', scope: 'proactive', cooldownDays: 1, maxShows: 90,
|
||
when: function () { return !!(quest(true) && new Date().getHours() >= 16); },
|
||
text: function () { var q = quest(true); return 'Остался квест дня: «' + (q.label || 'задание') + '».'; },
|
||
action: function () { return null; } },
|
||
{ id: 'weak-subject', scope: 'proactive', cooldownDays: 2, maxShows: 40,
|
||
when: function () { return !!(SRV && SRV.weakSubject); },
|
||
text: function () { var w = SRV.weakSubject; return 'Слабее всего идёт ' + (w.name || 'предмет') + ' (' + w.avg + '%). Потренируемся?'; },
|
||
action: function () { return { label: 'Потренироваться', url: '/exam-prep/math9' }; } },
|
||
{ id: 'daily-plan', scope: 'proactive', cooldownDays: 1, maxShows: 120,
|
||
when: function () { return PAGE === 'dashboard' && dailyPlan().length > 0; },
|
||
text: function () { return 'План на сегодня: ' + dailyPlan().join(', ') + '. Начнём?'; },
|
||
action: function () { var p = dailyPlanAction(); return p; } },
|
||
];
|
||
|
||
/* План на сегодня — из дневных квестов и карточек к повторению */
|
||
function dailyPlan() {
|
||
var out = [];
|
||
var qs = (PET && PET.quests) || [];
|
||
qs.forEach(function (q) { if (!q.done && q.label) out.push(q.label.toLowerCase()); });
|
||
if (SRV && SRV.dueCards > 0) out.push('повторить ' + SRV.dueCards + ' ' + plural(SRV.dueCards, 'карточку', 'карточки', 'карточек'));
|
||
return out.slice(0, 3);
|
||
}
|
||
function dailyPlanAction() {
|
||
if (SRV && SRV.dueCards > 0) return { label: 'Повторить карточки', url: '/flashcards' };
|
||
return { label: 'К занятиям', url: '/exam-prep/math9' };
|
||
}
|
||
|
||
function plural(n, one, few, many) {
|
||
var m10 = n % 10, m100 = n % 100;
|
||
if (m10 === 1 && m100 !== 11) return one;
|
||
if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return few;
|
||
return many;
|
||
}
|
||
|
||
/* ── контентные подсказки по страницам (генерируются в RULES) ────────── */
|
||
var PAGE_HINTS = {
|
||
textbook: ['Любой кусок страницы можно вырезать картинкой в «Мои материалы» — кнопка «Вырезать область» внизу.',
|
||
'Ссылка из адресной строки ведёт прямо на этот параграф — удобно делиться.'],
|
||
exam: ['Три режима: экзамен (как на ЦТ/ЦЭ), тренировка (с разбором) и случайный.',
|
||
'После теста доступен разбор: правильные ответы и решения.'],
|
||
flashcards: ['Формулы вводятся через KaTeX-палитру, можно добавить и картинку.',
|
||
'Система сама напоминает, какие карточки пора повторить.'],
|
||
materials: ['Раскладывай материалы по папкам, а поверх фото можно рисовать — кнопка с карандашом.',
|
||
'Клик по картинке открывает её в просмотрщике прямо на странице.'],
|
||
lab: ['Симуляции запускаются прямо в браузере — ничего ставить не нужно.',
|
||
'У оптической скамьи есть режим «Конструктор» — собирай системы из линз и зеркал.'],
|
||
dashboard: ['Виджеты на дашборде можно включать и переставлять под себя.',
|
||
'Блок «Активность» показывает серию и занятия за недели.'],
|
||
textbooks: ['Внутри учебника — главы и параграфы; прочитанное отмечается и идёт в прогресс.'],
|
||
library: ['В библиотеке — файлы и материалы от учителя; ищи по предмету.'],
|
||
knowledge: ['Карта знаний показывает связи тем — видно, что уже освоено.'],
|
||
biochem: ['Интерактивные модели молекул и реакций — крути и разбирай.'],
|
||
redbook: ['Красная книга — карточки видов; листай и запоминай.'],
|
||
crossword: ['Кроссворд по терминам предмета — отгадывай и получай XP.'],
|
||
hangman: ['«Виселица» по терминам — угадывай слово по буквам.'],
|
||
collection: ['Коллекция — твои собранные награды и предметы.'],
|
||
board: ['Инструмент «выделение» двигает и поворачивает объекты; есть фигуры, формулы и линейка.',
|
||
'Страницу доски можно сохранить в «Мои материалы».'],
|
||
lessonhistory: ['Архив онлайн-уроков — доска и заметки сохраняются после занятия.'],
|
||
qbank: ['Банк вопросов — основа для тестов и заданий.'],
|
||
theory: ['Можно создать одиночный «Быстрый урок» без курса.',
|
||
'Прогресс по урокам отслеживается — продолжай с того же места.'],
|
||
classes: ['Создавай классы и выдавай задания; ученики входят по коду.'],
|
||
analytics: ['Аналитика: динамика результатов и слабые темы.'],
|
||
gradebook: ['Журнал — оценки и сдачи по заданиям класса.'],
|
||
homework: ['Здесь все задания и дедлайны; работу можно загрузить прямо тут.'],
|
||
pet: ['Гладь и корми Квантика, закрывай квесты — за это XP и монеты.'],
|
||
};
|
||
Object.keys(PAGE_HINTS).forEach(function (pg) {
|
||
PAGE_HINTS[pg].forEach(function (text, idx) {
|
||
RULES.push({
|
||
id: 'p-' + pg + '-' + idx, scope: 'page', cooldownDays: 12, maxShows: 2,
|
||
when: (function (p) { return function () { return PAGE === p; }; })(pg),
|
||
text: (function (t) { return function () { return t; }; })(text),
|
||
action: function () { return null; },
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ── «Совет дня» — ротация фич-открытий (дашборд) ────────────────────── */
|
||
var TIPS = [
|
||
'фрагмент учебника можно сохранить картинкой в «Мои материалы».',
|
||
'в «Теории» есть «Быстрый урок» — один урок без создания курса.',
|
||
'поверх сохранённого фото можно рисовать пометки.',
|
||
'формулы в карточках и на доске вводятся через KaTeX.',
|
||
'серия за ежедневные занятия делает Квантика счастливее.',
|
||
'поиск по платформе — Ctrl+K: уроки, курсы, файлы и вопросы.',
|
||
'заметку из «Мои материалы» можно превратить во флешкарту.',
|
||
'тёмную тему и звуки включишь в Профиль → Настройки.',
|
||
];
|
||
RULES.push({
|
||
id: 'tip-daily', scope: 'page', cooldownDays: 1, maxShows: 200,
|
||
when: function () { return PAGE === 'dashboard'; },
|
||
text: function () { return 'А ты знал: ' + TIPS[(new Date().getDate()) % TIPS.length]; },
|
||
action: function () { return null; },
|
||
});
|
||
|
||
/* ── выбор подсказки ─────────────────────────────────────────────────── */
|
||
function eligible(rule) {
|
||
if (SUPPRESS_PAGE && rule.scope !== 'celebration') return false;
|
||
var s = (SRV && SRV.seen && SRV.seen[rule.id]) || null;
|
||
if (s && s.dismissed) return false;
|
||
if (s && rule.maxShows && s.count >= rule.maxShows) return false;
|
||
if (s && s.lastAt && rule.cooldownDays) {
|
||
var days = (Date.now() - Date.parse(s.lastAt + 'Z')) / 86400000;
|
||
if (days < rule.cooldownDays) return false;
|
||
}
|
||
try { return !!rule.when(); } catch (e) { return false; }
|
||
}
|
||
function pickRule() {
|
||
var order = { celebration: 3, proactive: 2, page: 1 };
|
||
var cands = RULES.filter(eligible).sort(function (a, b) { return (order[b.scope] || 0) - (order[a.scope] || 0); });
|
||
return cands[0] || null;
|
||
}
|
||
|
||
/* ── поздравления (детект по дельте, localStorage) ───────────────────── */
|
||
function celebration() {
|
||
if (!PET) return null;
|
||
var lvl = PET.petLevel || 1;
|
||
var prevLvl = parseInt(lsGet('asst_lvl') || '', 10);
|
||
if (!isNaN(prevLvl) && lvl > prevLvl) {
|
||
lsSet('asst_lvl', String(lvl));
|
||
return { id: 'cel-level', mood: 'ecstatic', text: 'Ура! ' + (PET.petName || 'Квантик') + ' дорос до уровня ' + lvl + '! Так держать.' };
|
||
}
|
||
if (isNaN(prevLvl)) lsSet('asst_lvl', String(lvl));
|
||
|
||
var ms = [3, 7, 14, 30, 60, 100];
|
||
var cur = PET.streakCurrent || 0;
|
||
var prevMs = parseInt(lsGet('asst_streak_ms') || '0', 10) || 0;
|
||
var hit = 0;
|
||
for (var i = 0; i < ms.length; i++) if (cur >= ms[i]) hit = ms[i];
|
||
if (hit > prevMs) {
|
||
lsSet('asst_streak_ms', String(hit));
|
||
return { id: 'cel-streak', mood: 'ecstatic', text: 'Серия ' + hit + ' ' + plural(hit, 'день', 'дня', 'дней') + ' подряд! Огонь.' };
|
||
}
|
||
if (hit > 0 && prevMs === 0) lsSet('asst_streak_ms', String(hit));
|
||
return null;
|
||
}
|
||
|
||
/* ── дневной лимит авто-показов ──────────────────────────────────────── */
|
||
function dayCount() {
|
||
if (lsGet('asst_day') !== todayKey()) { lsSet('asst_day', todayKey()); lsSet('asst_day_n', '0'); }
|
||
return parseInt(lsGet('asst_day_n') || '0', 10) || 0;
|
||
}
|
||
function bumpDay() { lsSet('asst_day', todayKey()); lsSet('asst_day_n', String(dayCount() + 1)); }
|
||
|
||
/* ── PetSprite ───────────────────────────────────────────────────────── */
|
||
function ensurePet(cb) {
|
||
if (window.PetSprite && PetSprite.render) return cb();
|
||
var s = document.createElement('script'); s.src = '/js/pet-sprite.js';
|
||
s.onload = cb; s.onerror = cb; document.head.appendChild(s);
|
||
}
|
||
function faceSVG(mood) {
|
||
try {
|
||
if (window.PetSprite && PetSprite.render) {
|
||
return PetSprite.render(PET ? (PET.petLevel || 1) : 1, mood || (PET && PET.mood) || 'happy',
|
||
(PET && PET.accessories) || [], (PET && PET.petColor) || 'purple', (PET && PET.streakCurrent) || 0);
|
||
}
|
||
} catch (e) {}
|
||
return '<svg viewBox="0 0 100 100"><circle cx="55" cy="58" r="34" fill="#9B5DE5"/></svg>';
|
||
}
|
||
|
||
/* ── styles ──────────────────────────────────────────────────────────── */
|
||
function ensureStyles() {
|
||
if (document.getElementById('asst-style')) return;
|
||
var s = document.createElement('style'); s.id = 'asst-style';
|
||
s.textContent = [
|
||
'.asst-root{position:fixed;left:18px;bottom:18px;z-index:8500;font-family:Manrope,system-ui,sans-serif;transition:left .28s cubic-bezier(.4,0,.2,1);}',
|
||
// в приложении сдвигаем правее сайдбара (230/62px), чтобы не перекрывать профиль в сайдбаре
|
||
'.app-layout ~ .asst-root{left:248px;}',
|
||
'.app-layout.sb-collapsed ~ .asst-root{left:80px;}',
|
||
'.asst-fab{width:54px;height:54px;border-radius:50%;border:none;background:#fff;cursor:pointer;padding:4px;',
|
||
' box-shadow:0 8px 24px rgba(139,92,246,.32);transition:transform .15s;position:relative;display:block;}',
|
||
'.asst-fab:hover{transform:translateY(-2px) scale(1.04);}',
|
||
'.asst-fab svg{width:100%;height:100%;display:block;}',
|
||
'.asst-dot{position:absolute;top:0;right:0;width:13px;height:13px;border-radius:50%;background:#F15BB5;border:2px solid #fff;}',
|
||
reduceMotion ? '' : '.asst-fab.pulse{animation:asstPulse 2.2s ease-in-out infinite;}',
|
||
'@keyframes asstPulse{0%,100%{box-shadow:0 8px 24px rgba(139,92,246,.32);}50%{box-shadow:0 8px 30px rgba(241,91,181,.5);}}',
|
||
'.asst-bubble{position:absolute;left:0;bottom:64px;width:300px;max-width:78vw;background:#fff;border-radius:16px;',
|
||
' box-shadow:0 18px 50px rgba(15,23,42,.22);padding:14px 16px;border:1px solid rgba(15,23,42,.07);',
|
||
' opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .18s,transform .18s;}',
|
||
'.asst-bubble.open{opacity:1;transform:translateY(0);pointer-events:auto;}',
|
||
'.asst-x{position:absolute;top:8px;right:8px;width:26px;height:26px;border:none;background:transparent;color:#8a94a6;',
|
||
' cursor:pointer;border-radius:7px;font-size:18px;line-height:1;}',
|
||
'.asst-x:hover{background:rgba(15,23,42,.06);color:#0F172A;}',
|
||
'.asst-name{font-size:.7rem;font-weight:800;color:#9B5DE5;text-transform:uppercase;letter-spacing:.03em;margin-bottom:6px;}',
|
||
'.asst-text{font-size:.86rem;line-height:1.5;color:#28324a;margin-bottom:12px;white-space:pre-line;}',
|
||
'.asst-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}',
|
||
'.asst-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:99px;border:none;cursor:pointer;',
|
||
' font:700 .78rem Manrope,sans-serif;background:#9B5DE5;color:#fff;text-decoration:none;}',
|
||
'.asst-btn:hover{background:#7e3eca;}',
|
||
'.asst-link{background:none;border:none;color:#8a94a6;cursor:pointer;font:600 .76rem Manrope,sans-serif;padding:4px 2px;text-decoration:none;}',
|
||
'.asst-link:hover{color:#9B5DE5;}',
|
||
'.asst-ask-in{width:100%;box-sizing:border-box;padding:9px 12px;border:1px solid #e2e8f0;border-radius:10px;font:inherit;font-size:.84rem;margin-bottom:10px;}',
|
||
'.asst-ans{font-size:.82rem;line-height:1.5;color:#28324a;border-top:1px solid rgba(15,23,42,.06);padding:9px 0;}',
|
||
'.asst-ans:first-of-type{border-top:none;}',
|
||
'.asst-ans-q{font-weight:700;color:#0F172A;margin-bottom:2px;}',
|
||
'.asst-ans-link{display:inline-block;margin-top:4px;color:#9B5DE5;font-weight:700;font-size:.78rem;text-decoration:none;}',
|
||
'.asst-ans-sec{font-size:.66rem;font-weight:800;color:#8a94a6;text-transform:uppercase;letter-spacing:.03em;margin:12px 0 2px;}',
|
||
'.asst-ans-box{max-height:46vh;overflow:auto;}',
|
||
'.asst-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;}',
|
||
'.asst-chip{border:1px solid #e2e8f0;background:#f8fafc;border-radius:99px;padding:5px 10px;font:600 .72rem Manrope,sans-serif;color:#475569;cursor:pointer;text-align:left;}',
|
||
'.asst-chip:hover{border-color:#9B5DE5;color:#9B5DE5;}',
|
||
'.asst-chip-ctx{background:rgba(155,93,229,.1);border-color:rgba(155,93,229,.35);color:#7e3eca;}',
|
||
'.asst-rich{font-size:.84rem;line-height:1.55;color:#28324a;}',
|
||
'.asst-rich>div{margin:3px 0;}',
|
||
'.asst-rich ul,.asst-rich ol{margin:4px 0 4px 18px;padding:0;}',
|
||
'.asst-rich li{margin:2px 0;}',
|
||
'.asst-rich code{background:rgba(15,23,42,.06);border-radius:4px;padding:1px 4px;}',
|
||
'.asst-md-h{font-weight:800;color:#0F172A;margin:6px 0 2px;}',
|
||
'.asst-chat{max-height:46vh;overflow:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:8px;}',
|
||
'.asst-chat:empty{display:none;}',
|
||
'.asst-msg{font-size:.84rem;line-height:1.5;border-radius:12px;padding:8px 11px;max-width:92%;word-break:break-word;}',
|
||
'.asst-msg-user{align-self:flex-end;background:#9B5DE5;color:#fff;}',
|
||
'.asst-msg-assistant{align-self:flex-start;background:rgba(15,23,42,.05);}',
|
||
'.asst-msg-assistant .asst-rich{color:#28324a;}',
|
||
'.asst-msg-ph{opacity:.6;}',
|
||
'.asst-msg-links{align-self:flex-start;font-size:.74rem;}',
|
||
'.asst-empty{font-size:.82rem;color:#8a94a6;padding:6px 0;}',
|
||
// на мобиле сайдбар — выезжающая шторка, контент во всю ширину → к левому краю
|
||
'@media(max-width:768px){.asst-root,.app-layout ~ .asst-root,.app-layout.sb-collapsed ~ .asst-root{left:12px;bottom:18px;}.asst-fab{width:48px;height:48px;}}',
|
||
].join('');
|
||
document.head.appendChild(s);
|
||
}
|
||
|
||
/* ── рендер ──────────────────────────────────────────────────────────── */
|
||
function setFace(mood) { var f = root.querySelector('.asst-face'); if (f) f.innerHTML = faceSVG(mood); }
|
||
|
||
function openBubble(html, opts) {
|
||
opts = opts || {};
|
||
bubble.innerHTML = '<button class="asst-x" aria-label="Закрыть">×</button>' + html;
|
||
bubble.querySelector('.asst-x').onclick = closeBubble;
|
||
bubble.classList.add('open');
|
||
openState = true;
|
||
root.querySelector('.asst-fab').classList.remove('pulse');
|
||
root.querySelector('.asst-dot') && root.querySelector('.asst-dot').remove();
|
||
if (opts.mood) setFace(opts.mood);
|
||
}
|
||
function closeBubble() { bubble.classList.remove('open'); openState = false; setFace(); }
|
||
|
||
function hintHtml(rule) {
|
||
var act = null; try { act = rule.action(); } catch (e) {}
|
||
var actHtml = act && act.url ? '<a class="asst-btn" href="' + esc(act.url) + '">' + esc(act.label || 'Открыть') + '</a>' : '';
|
||
var dismiss = (rule.scope !== 'celebration') ? '<button class="asst-link" data-a="dismiss">Не показывать</button>' : '';
|
||
return '<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
||
'<div class="asst-text">' + esc(rule.text ? rule.text() : rule.text) + '</div>' +
|
||
'<div class="asst-actions">' + actHtml +
|
||
'<button class="asst-link" data-a="ok">Понятно</button>' + dismiss +
|
||
'<button class="asst-link" data-a="ask" style="margin-left:auto">Спросить</button></div>';
|
||
}
|
||
|
||
function showRule(rule, isCelebration) {
|
||
openBubble(hintHtml(rule), { mood: rule.mood });
|
||
// отметить показ (для page/proactive — на сервере; celebration — локально через дельту)
|
||
if (!isCelebration) { try { LS.assistantSeen(rule.id); } catch (e) {} bumpDay(); }
|
||
bubble.querySelector('[data-a="ok"]').onclick = closeBubble;
|
||
var dz = bubble.querySelector('[data-a="dismiss"]');
|
||
if (dz) dz.onclick = function () { try { LS.assistantDismiss(rule.id); } catch (e) {} closeBubble(); };
|
||
bubble.querySelector('[data-a="ask"]').onclick = openAsk;
|
||
}
|
||
|
||
function greetHtml() {
|
||
return '<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
||
'<div class="asst-text">Привет! Я помогу разобраться в системе. Спроси, как что-то сделать, или пройди короткий тур.</div>' +
|
||
'<div class="asst-actions"><button class="asst-btn" data-a="ask">Спросить</button>' +
|
||
'<button class="asst-link" data-a="tour">Тур по системе</button>' +
|
||
'<button class="asst-link" data-a="ok" style="margin-left:auto">Закрыть</button></div>';
|
||
}
|
||
function showGreet() {
|
||
openBubble(greetHtml(), {});
|
||
bubble.querySelector('[data-a="ok"]').onclick = closeBubble;
|
||
bubble.querySelector('[data-a="ask"]').onclick = openAsk;
|
||
bubble.querySelector('[data-a="tour"]').onclick = function () { startTour(); };
|
||
}
|
||
|
||
/* ── рендер markdown + KaTeX в ответах модели ────────────────────────── */
|
||
var _katexP = null;
|
||
function ensureKatex() {
|
||
if (window.renderMathInElement) return Promise.resolve();
|
||
if (_katexP) return _katexP;
|
||
_katexP = new Promise(function (resolve) {
|
||
var base = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/';
|
||
var css = document.createElement('link'); css.rel = 'stylesheet'; css.href = base + 'katex.min.css'; document.head.appendChild(css);
|
||
var s1 = document.createElement('script'); s1.src = base + 'katex.min.js';
|
||
s1.onload = function () {
|
||
var s2 = document.createElement('script'); s2.src = base + 'contrib/auto-render.min.js';
|
||
s2.onload = function () { resolve(); }; s2.onerror = function () { resolve(); };
|
||
document.head.appendChild(s2);
|
||
};
|
||
s1.onerror = function () { resolve(); };
|
||
document.head.appendChild(s1);
|
||
});
|
||
return _katexP;
|
||
}
|
||
function mdInline(s) {
|
||
return s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>')
|
||
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||
}
|
||
function mdToHtml(src) {
|
||
var lines = esc(src).split(/\r?\n/), html = '', list = null;
|
||
function closeList() { if (list) { html += '</' + list + '>'; list = null; } }
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var ln = lines[i];
|
||
var mUl = ln.match(/^\s*[-*]\s+(.*)$/), mOl = ln.match(/^\s*\d+\.\s+(.*)$/), mH = ln.match(/^\s*#{1,6}\s+(.*)$/);
|
||
if (mUl) { if (list !== 'ul') { closeList(); html += '<ul>'; list = 'ul'; } html += '<li>' + mdInline(mUl[1]) + '</li>'; continue; }
|
||
if (mOl) { if (list !== 'ol') { closeList(); html += '<ol>'; list = 'ol'; } html += '<li>' + mdInline(mOl[1]) + '</li>'; continue; }
|
||
closeList();
|
||
if (mH) { html += '<div class="asst-md-h">' + mdInline(mH[1]) + '</div>'; continue; }
|
||
if (ln.trim() !== '') html += '<div>' + mdInline(ln) + '</div>';
|
||
}
|
||
closeList();
|
||
return html;
|
||
}
|
||
function renderRich(container, text) {
|
||
var math = [];
|
||
var protectedText = String(text || '').replace(/(\$\$[\s\S]+?\$\$|\$[^\n$]+?\$)/g, function (m) { math.push(m); return '@@M' + (math.length - 1) + '@@'; });
|
||
var html = mdToHtml(protectedText).replace(/@@M(\d+)@@/g, function (_, i) { return esc(math[+i] || ''); });
|
||
container.innerHTML = html;
|
||
ensureKatex().then(function () {
|
||
try {
|
||
if (window.renderMathInElement) renderMathInElement(container, {
|
||
delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }],
|
||
throwOnError: false,
|
||
});
|
||
} catch (e) {}
|
||
});
|
||
}
|
||
|
||
/* ── контекст: выделенный текст / текущий параграф ───────────────────── */
|
||
var _lastSel = '';
|
||
function getPageContext() {
|
||
try {
|
||
if (PAGE === 'textbook') {
|
||
var sec = document.querySelector('.sec.active') || document.querySelector('.sec');
|
||
if (sec) {
|
||
var h = sec.querySelector('.sec-h');
|
||
var title = (h && h.textContent.trim()) || (document.title || 'Параграф').split('·')[0].trim();
|
||
var text = (sec.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 3500);
|
||
if (text.length > 40) return { title: title, text: text };
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
return null;
|
||
}
|
||
|
||
/* ── «Спроси Квантика» ───────────────────────────────────────────────── */
|
||
var SUGGESTIONS = ['Как вырезать кусок учебника?', 'Как создать карточки?', 'Как начать тест?', 'Объясни теорему Пифагора', 'Где мои домашние задания?', 'Как включить тёмную тему?'];
|
||
var _chat = []; // многоходовой диалог: [{role:'user'|'assistant', content}]
|
||
function msgEl(role) { var d = document.createElement('div'); d.className = 'asst-msg asst-msg-' + role; return d; }
|
||
function renderChat(chatEl) {
|
||
chatEl.innerHTML = '';
|
||
_chat.forEach(function (m) {
|
||
var d = msgEl(m.role);
|
||
if (m.role === 'assistant') { d.innerHTML = '<div class="asst-rich"></div>'; renderRich(d.querySelector('.asst-rich'), m.content); }
|
||
else d.textContent = m.content;
|
||
chatEl.appendChild(d);
|
||
});
|
||
chatEl.scrollTop = chatEl.scrollHeight;
|
||
}
|
||
function openAsk(prefill) {
|
||
var sel = _lastSel, pc = getPageContext();
|
||
var ctxBtns = '';
|
||
if (sel) ctxBtns += '<button class="asst-chip asst-chip-ctx" data-ctx="sel" type="button">Объяснить выделенное</button>';
|
||
if (pc) ctxBtns += '<button class="asst-chip asst-chip-ctx" data-ctx="sec" type="button">Объяснить этот параграф</button>' +
|
||
'<button class="asst-chip asst-chip-ctx" data-ctx="sum" type="button">Конспект параграфа</button>' +
|
||
'<button class="asst-chip asst-chip-ctx" data-ctx="cards" type="button">Флешкарты из параграфа</button>';
|
||
var chips = '<div class="asst-chips">' + ctxBtns +
|
||
SUGGESTIONS.map(function (q) { return '<button class="asst-chip" type="button">' + esc(q) + '</button>'; }).join('') + '</div>';
|
||
openBubble(
|
||
'<div class="asst-name">Спроси Квантика' + (_chat.length ? '<button class="asst-link" data-a="clear" style="float:right;font-weight:600;margin-right:24px">Очистить</button>' : '') + '</div>' +
|
||
'<div class="asst-chat"></div>' + chips +
|
||
'<input class="asst-ask-in" type="text" placeholder="Спроси что угодно: «объясни…», «как…»" maxlength="300" />', {});
|
||
var inp = bubble.querySelector('.asst-ask-in');
|
||
var chatEl = bubble.querySelector('.asst-chat');
|
||
var chipsEl = bubble.querySelector('.asst-chips');
|
||
renderChat(chatEl);
|
||
if (_chat.length) chipsEl.style.display = 'none';
|
||
function go(q, context) { if (chipsEl) chipsEl.style.display = 'none'; inp.value = ''; send(q, context, chatEl); }
|
||
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(inp.value); });
|
||
bubble.querySelectorAll('.asst-chip').forEach(function (c) {
|
||
c.addEventListener('click', function () {
|
||
var ctx = c.getAttribute('data-ctx');
|
||
if (ctx === 'sel') return go('Объясни простыми словами и приведи пример.', sel);
|
||
if (ctx === 'sec') return go('Объясни простыми словами, о чём этот параграф, и выдели главное.', pc && pc.text);
|
||
if (ctx === 'sum') return go('Сделай краткий конспект этого материала: 4–6 главных пунктов.', pc && pc.text);
|
||
if (ctx === 'cards') { if (chipsEl) chipsEl.style.display = 'none'; return makeFlashcards(pc, chatEl); }
|
||
go(c.textContent);
|
||
});
|
||
});
|
||
var clr = bubble.querySelector('[data-a="clear"]');
|
||
if (clr) clr.onclick = function () { _chat = []; openAsk(); };
|
||
if (prefill && prefill.q) go(prefill.q, prefill.context);
|
||
else inp.focus();
|
||
}
|
||
function send(q, context, chatEl) {
|
||
q = (q || '').trim();
|
||
if (q.length < 2) return;
|
||
var history = _chat.slice(-6);
|
||
_chat.push({ role: 'user', content: q });
|
||
var u = msgEl('user'); u.textContent = q; chatEl.appendChild(u);
|
||
var ph = msgEl('assistant'); ph.className += ' asst-msg-ph'; ph.textContent = 'Думаю…'; chatEl.appendChild(ph);
|
||
chatEl.scrollTop = chatEl.scrollHeight;
|
||
Promise.all([
|
||
LS.assistantAsk(q, context, history).catch(function () { return { answers: [] }; }),
|
||
(LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; }),
|
||
]).then(function (res) {
|
||
ph.remove();
|
||
var model = res[0] && res[0].answer;
|
||
var ans = (res[0] && res[0].answers) || [];
|
||
var found = (res[1] && res[1].results) || [];
|
||
var content = model || (ans[0] && (ans[0].q + '\n' + ans[0].a)) || 'Не нашёл точного ответа. Попробуй переформулировать или поищи (Ctrl+K).';
|
||
_chat.push({ role: 'assistant', content: content });
|
||
var d = msgEl('assistant'); d.innerHTML = '<div class="asst-rich"></div>'; chatEl.appendChild(d);
|
||
renderRich(d.querySelector('.asst-rich'), content);
|
||
var links = '';
|
||
if (!model && ans.length) links += ans.slice(0, 2).filter(function (a) { return a.url; }).map(function (a) { return '<a class="asst-ans-link" href="' + esc(a.url) + '">' + esc(a.q) + '</a>'; }).join(' · ');
|
||
if (found.length) links += (links ? '<br>' : '') + '<span style="color:#8a94a6">На платформе: </span>' + found.slice(0, 3).map(function (f) { return '<a class="asst-ans-link" href="' + esc(f.url || '#') + '">' + esc(f.title || '…') + '</a>'; }).join(' · ');
|
||
if (links) { var l = document.createElement('div'); l.className = 'asst-msg-links'; l.innerHTML = links; chatEl.appendChild(l); }
|
||
chatEl.scrollTop = chatEl.scrollHeight;
|
||
}).catch(function () { ph.textContent = 'Не удалось получить ответ.'; });
|
||
}
|
||
|
||
/* ── «Флешкарты из параграфа» — модель → колода через API флешкарт ─────── */
|
||
function makeFlashcards(pc, chatEl) {
|
||
var note = msgEl('assistant');
|
||
if (!pc || !pc.text) { note.innerHTML = '<div class="asst-rich">Открой параграф учебника, чтобы сделать карточки.</div>'; chatEl.appendChild(note); return; }
|
||
note.innerHTML = '<div class="asst-rich">Готовлю карточки…</div>'; chatEl.appendChild(note); chatEl.scrollTop = chatEl.scrollHeight;
|
||
LS.assistantFlashcards(pc.text, pc.title || 'Карточки').then(function (r) {
|
||
var cards = (r && r.cards) || [];
|
||
if (!cards.length) throw new Error('empty');
|
||
return LS.fcCreateDeck({ title: (r.title || pc.title || 'Карточки').slice(0, 80) }).then(function (d) {
|
||
var deckId = d && d.id;
|
||
return cards.reduce(function (p, c) {
|
||
return p.then(function () { return LS.fcAddCard(deckId, { front: c.front, back: c.back }).catch(function () {}); });
|
||
}, Promise.resolve()).then(function () { return cards.length; });
|
||
});
|
||
}).then(function (n) {
|
||
note.innerHTML = '<div class="asst-rich">Готово: создано ' + n + ' ' + plural(n, 'карточка', 'карточки', 'карточек') +
|
||
'. <a class="asst-ans-link" href="/flashcards">Открыть флешкарты</a></div>';
|
||
}).catch(function () { note.innerHTML = '<div class="asst-rich">Не удалось сделать карточки. Попробуй позже.</div>'; });
|
||
}
|
||
|
||
/* ── Ф2: онбординг-тур по разделам ───────────────────────────────────── */
|
||
var TOUR = [
|
||
{ sel: '#app-sidebar a[href="/dashboard"]', title: 'Дашборд', text: 'Главная: твой прогресс, активность и питомец.' },
|
||
{ sel: '#app-sidebar a[href="/exam-prep/math9"]', title: 'Подготовка к экзамену', text: 'Тесты по темам, режимы экзамена и тренировки.' },
|
||
{ sel: '#app-sidebar a[href="/theory"]', title: 'Теория', text: 'Курсы и уроки. Можно создать и быстрый одиночный урок.' },
|
||
{ sel: '#app-sidebar a[href="/flashcards"]', title: 'Флэшкарты', text: 'Карточки для повторения — система сама напомнит, что освежить.' },
|
||
{ sel: '#app-sidebar a[href="/my-materials"]', title: 'Мои материалы', text: 'Сюда сохраняются вырезки из учебника, заметки и рисунки.' },
|
||
{ sel: '#app-sidebar a[href="/pet"]', title: 'Питомец', text: 'Квантик растёт от занятий и серий — заглядывай к нему.' },
|
||
{ sel: '.asst-fab', title: 'Я рядом', text: 'Нажми на меня в любой момент, чтобы спросить «как сделать…».' },
|
||
];
|
||
function vis(el) { return !!(el && el.getBoundingClientRect && el.getBoundingClientRect().width > 0 && el.offsetParent !== null); }
|
||
function ensureTourStyles() {
|
||
if (document.getElementById('asst-tour-style')) return;
|
||
var s = document.createElement('style'); s.id = 'asst-tour-style';
|
||
s.textContent = [
|
||
'.asst-tour-ov{position:fixed;inset:0;z-index:9600;}',
|
||
'.asst-tour-ring{position:absolute;border-radius:10px;border:2px solid #9B5DE5;box-shadow:0 0 0 9999px rgba(15,12,30,.55);transition:all .2s ease;pointer-events:none;}',
|
||
'.asst-tour-tip{position:fixed;width:280px;max-width:84vw;background:#fff;border-radius:14px;padding:14px 16px;box-shadow:0 18px 50px rgba(15,23,42,.3);font-family:Manrope,system-ui,sans-serif;}',
|
||
'.asst-tour-h{font-size:.78rem;font-weight:800;color:#9B5DE5;text-transform:uppercase;letter-spacing:.03em;margin-bottom:5px;}',
|
||
'.asst-tour-t{font-size:.86rem;line-height:1.5;color:#28324a;margin-bottom:12px;}',
|
||
'.asst-tour-nav{display:flex;align-items:center;gap:8px;}',
|
||
'.asst-tour-pg{font-size:.72rem;color:#8a94a6;font-weight:700;}',
|
||
].join('');
|
||
document.head.appendChild(s);
|
||
}
|
||
function startTour() {
|
||
closeBubble();
|
||
ensureTourStyles();
|
||
var steps = TOUR.filter(function (st) { return !st.sel || vis(document.querySelector(st.sel)); });
|
||
if (!steps.length) return;
|
||
var i = 0;
|
||
var ov = document.createElement('div'); ov.className = 'asst-tour-ov'; ov.setAttribute('data-h2c-ignore', '');
|
||
ov.innerHTML = '<div class="asst-tour-ring"></div><div class="asst-tour-tip"></div>';
|
||
document.body.appendChild(ov);
|
||
var ring = ov.querySelector('.asst-tour-ring');
|
||
var tip = ov.querySelector('.asst-tour-tip');
|
||
ov.addEventListener('click', function (e) { if (e.target === ov) finish(); }); // клик по фону — выход
|
||
function finish() {
|
||
ov.remove();
|
||
window.removeEventListener('resize', render);
|
||
document.removeEventListener('keydown', onKey);
|
||
try { LS.assistantDismiss('onboarding'); } catch (e) {}
|
||
}
|
||
function onKey(e) { if (e.key === 'Escape') finish(); }
|
||
function render() {
|
||
var st = steps[i];
|
||
var el = st.sel ? document.querySelector(st.sel) : null;
|
||
// сперва проматываем элемент в зону видимости, ПОТОМ меряем (иначе позиция стухшая)
|
||
if (el && vis(el) && el.scrollIntoView) { try { el.scrollIntoView({ block: 'center', inline: 'nearest' }); } catch (e) {} }
|
||
var r = el && vis(el) ? el.getBoundingClientRect() : null;
|
||
|
||
// контент сначала — чтобы измерить реальный размер подсказки и привязать кнопки
|
||
tip.innerHTML =
|
||
'<div class="asst-tour-h">' + esc(st.title) + '</div>' +
|
||
'<div class="asst-tour-t">' + esc(st.text) + '</div>' +
|
||
'<div class="asst-tour-nav"><span class="asst-tour-pg">' + (i + 1) + ' / ' + steps.length + '</span>' +
|
||
'<span style="margin-left:auto"></span>' +
|
||
(i > 0 ? '<button class="asst-link" data-a="back">Назад</button>' : '') +
|
||
'<button class="asst-link" data-a="skip">Пропустить</button>' +
|
||
'<button class="asst-btn" data-a="next">' + (i === steps.length - 1 ? 'Готово' : 'Далее') + '</button></div>';
|
||
tip.querySelector('[data-a="next"]').onclick = function () { if (i === steps.length - 1) finish(); else { i++; render(); } };
|
||
var b = tip.querySelector('[data-a="back"]'); if (b) b.onclick = function () { i--; render(); };
|
||
tip.querySelector('[data-a="skip"]').onclick = finish;
|
||
|
||
// позиционирование: подсказка ВСЕГДА целиком в пределах экрана
|
||
var W = window.innerWidth, H = window.innerHeight;
|
||
var tw = tip.offsetWidth || 280, th = tip.offsetHeight || 150;
|
||
tip.style.transform = '';
|
||
if (r) {
|
||
var pad = 6;
|
||
ring.style.display = 'block';
|
||
ring.style.left = (r.left - pad) + 'px'; ring.style.top = (r.top - pad) + 'px';
|
||
ring.style.width = (r.width + pad * 2) + 'px'; ring.style.height = (r.height + pad * 2) + 'px';
|
||
ov.style.background = '';
|
||
var tx = r.right + 14;
|
||
if (tx + tw > W - 12) tx = r.left - tw - 14; // не влезает справа — показать слева
|
||
tx = Math.max(12, Math.min(tx, W - tw - 12));
|
||
var ty = Math.max(12, Math.min(r.top, H - th - 12));
|
||
tip.style.left = tx + 'px'; tip.style.top = ty + 'px';
|
||
} else {
|
||
ring.style.display = 'none';
|
||
ov.style.background = 'rgba(15,12,30,.55)';
|
||
tip.style.left = Math.max(12, (W - tw) / 2) + 'px';
|
||
tip.style.top = Math.max(12, (H - th) / 2) + 'px';
|
||
}
|
||
}
|
||
window.addEventListener('resize', render);
|
||
document.addEventListener('keydown', onKey);
|
||
render();
|
||
}
|
||
|
||
function onboardingOffer() {
|
||
openBubble(
|
||
'<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
||
'<div class="asst-text">Привет! Я ' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '. Показать за минуту, что где в системе?</div>' +
|
||
'<div class="asst-actions"><button class="asst-btn" data-a="tour">Показать тур</button>' +
|
||
'<button class="asst-link" data-a="later">Позже</button></div>', {});
|
||
try { LS.assistantSeen('onboarding'); } catch (e) {}
|
||
bubble.querySelector('[data-a="tour"]').onclick = function () { startTour(); };
|
||
bubble.querySelector('[data-a="later"]').onclick = closeBubble;
|
||
}
|
||
|
||
/* ── монтирование ────────────────────────────────────────────────────── */
|
||
function mount() {
|
||
ensureStyles();
|
||
root = document.createElement('div');
|
||
root.className = 'asst-root';
|
||
root.setAttribute('data-h2c-ignore', ''); // не попадать в скриншоты учебника
|
||
root.innerHTML =
|
||
'<div class="asst-bubble" role="dialog" aria-live="polite"></div>' +
|
||
'<button class="asst-fab" aria-label="Помощник Квантик"><span class="asst-face">' + faceSVG() + '</span></button>';
|
||
document.body.appendChild(root);
|
||
bubble = root.querySelector('.asst-bubble');
|
||
var fab = root.querySelector('.asst-fab');
|
||
fab.onclick = function () {
|
||
if (openState) return closeBubble();
|
||
if (picked) showRule(picked, picked.scope === 'celebration');
|
||
else showGreet();
|
||
};
|
||
|
||
// Запоминаем выделенный пользователем текст (для «Объяснить выделенное»)
|
||
document.addEventListener('mouseup', function () {
|
||
try {
|
||
var s = (window.getSelection && window.getSelection().toString() || '').trim();
|
||
if (s.length >= 8 && !(root && window.getSelection().anchorNode && root.contains(window.getSelection().anchorNode))) {
|
||
_lastSel = s.slice(0, 3000);
|
||
}
|
||
} catch (e) {}
|
||
});
|
||
|
||
// Онбординг новичка — приоритетно на дашборде, пока не пройден/не закрыт
|
||
var ob = (SRV.seen && SRV.seen['onboarding']) || {};
|
||
if (PAGE === 'dashboard' && !ob.dismissed && (ob.count || 0) < 3) {
|
||
var d0 = document.createElement('span'); d0.className = 'asst-dot'; fab.appendChild(d0);
|
||
fab.classList.add('pulse');
|
||
setTimeout(function () { if (!openState) onboardingOffer(); }, 1500);
|
||
return;
|
||
}
|
||
|
||
// поздравление — сразу; иначе подсказка — через паузу (с учётом дневного лимита)
|
||
var cel = celebration();
|
||
if (cel) {
|
||
picked = { id: cel.id, scope: 'celebration', text: cel.text, mood: cel.mood, action: function () { return null; } };
|
||
var dot = document.createElement('span'); dot.className = 'asst-dot'; fab.appendChild(dot);
|
||
fab.classList.add('pulse');
|
||
setTimeout(function () { if (!openState) showRule(picked, true); }, 1200);
|
||
return;
|
||
}
|
||
picked = pickRule();
|
||
if (picked && dayCount() < DAILY_CAP) {
|
||
var d = document.createElement('span'); d.className = 'asst-dot'; fab.appendChild(d);
|
||
fab.classList.add('pulse');
|
||
setTimeout(function () { if (!openState && picked) showRule(picked, false); }, AUTO_DELAY);
|
||
}
|
||
}
|
||
|
||
/* ── boot ────────────────────────────────────────────────────────────── */
|
||
function boot() {
|
||
if (!document.body) { return setTimeout(boot, 200); }
|
||
LS.assistantContext().then(function (ctx) {
|
||
SRV = ctx || {};
|
||
if (SRV.enabled === false) return; // выключено пользователем
|
||
return (LS.api ? LS.api('/api/pet') : Promise.resolve(null)).then(function (pet) {
|
||
PET = pet || null;
|
||
ensurePet(mount);
|
||
});
|
||
}).catch(function () { /* фича выключена / нет доступа — тихо выходим */ });
|
||
}
|
||
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 400); });
|
||
else setTimeout(boot, 400);
|
||
|
||
window.Assistant = {
|
||
open: function () { if (root) root.querySelector('.asst-fab').click(); },
|
||
tour: function () { startTour(); },
|
||
// открыть «Спроси» с готовым вопросом и контекстом (для интеграций, напр. экзамен)
|
||
ask: function (q, context) { if (root && bubble) openAsk({ q: q, context: context }); },
|
||
explainSelection: function () { if (_lastSel) window.Assistant.ask('Объясни простыми словами и приведи пример.', _lastSel); },
|
||
};
|
||
})();
|