feat(assistant): контент по всем разделам, FAQ x5, поиск по платформе, умный проактив

Контент: контекстные подсказки на ВСЕ разделы (PAGE_HINTS), «Совет дня» (ротация),
FAQ расширен ~10 -> ~50 записей по всем фичам. «Спроси Квантика» теперь ищет и по
платформе (LS.globalSearch) рядом с FAQ. Умный проактив: weakSubject (слабый предмет
по тестам, /api/assistant/context) -> «потренируемся» и daily-plan (план на сегодня из
квестов и карточек к повторению).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 17:07:57 +03:00
parent 2f3fd7475b
commit c33295e975
2 changed files with 191 additions and 64 deletions
+67 -30
View File
@@ -11,36 +11,57 @@ const db = require('../db/db');
/* Корпус справки для «Спроси Квантика» (поиск по ключевым словам в ask()). /* Корпус справки для «Спроси Квантика» (поиск по ключевым словам в ask()).
* Это же место — контекст для будущей локальной модели (см. ask). Правьте свободно. */ * Это же место — контекст для будущей локальной модели (см. ask). Правьте свободно. */
const FAQ = [ const FAQ = [
{ id: 'clip-textbook', q: 'Как сохранить кусок учебника?', // ── Учебники / чтение ──
a: 'На странице учебника нажми «Вырезать область» (внизу), выдели фрагмент — он сохранится картинкой в «Мои материалы».', { id: 'clip-textbook', q: 'Как сохранить кусок учебника?', a: 'На странице учебника нажми «Вырезать область» (внизу), выдели фрагмент — он сохранится картинкой в «Мои материалы».', url: '/my-materials', keywords: ['учебник','вырезать','область','скриншот','фрагмент','сохранить','картинк'] },
url: '/my-materials', keywords: ['учебник', 'вырезать', 'область', 'скриншот', 'фрагмент', 'сохранить', 'картинк', 'материал'] }, { id: 'textbook-link', q: 'Как поделиться ссылкой на параграф?', a: 'Открой нужный параграф учебника и скопируй адрес из строки браузера — ссылка ведёт прямо на этот раздел.', url: '/textbooks', keywords: ['учебник','ссылк','параграф','поделит','раздел','deep'] },
{ id: 'materials-folders', q: 'Как разложить материалы по папкам?', { id: 'textbooks-where', q: 'Где учебники?', a: 'Раздел «Учебники» в меню. Внутри — главы и параграфы с теорией, формулами и задачами.', url: '/textbooks', keywords: ['учебник','теори','глава','параграф','книг'] },
a: 'В «Мои материалы» нажми «+ папка», затем у карточки выбери папку. Можно фильтровать по папкам и типам.', { id: 'read-progress', q: 'Считается ли прогресс чтения?', a: 'Да — прочитанные параграфы отмечаются, прогресс по учебнику виден в самом учебнике и влияет на питомца.', url: '/textbooks', keywords: ['прогресс','чтени','прочит','отмет'] },
url: '/my-materials', keywords: ['папк', 'материал', 'коллекци', 'разложить', 'сортиров', 'фильтр'] }, // ── Тесты / экзамен ──
{ id: 'materials-annotate', q: 'Как рисовать поверх фото?', { id: 'exam-modes', q: 'Чем отличаются режимы экзамена?', a: 'Экзамен — как на ЦТ/ЦЭ, на время. Тренировка — с разбором после каждого ответа. Случайный — быстрый набор вопросов.', url: '/exam-prep/math9', keywords: ['экзамен','режим','тренировк','случайн','цт','цэ','тест'] },
a: 'Открой материал-картинку и нажми кнопку с карандашом-линейкой — откроется редактор. Сохранение обновит ту же карточку.', { id: 'exam-start', q: 'Как начать тест?', a: 'Зайди в «Подготовка к экзамену», выбери тему и режим. Можно быстро стартовать тест и с дашборда.', url: '/exam-prep/math9', keywords: ['тест','начать','старт','экзамен','реши'] },
url: '/my-materials', keywords: ['рисовать', 'аннотир', 'поверх', 'фото', 'карандаш', 'разметк', 'редактир'] }, { id: 'exam-review', q: 'Как разобрать ошибки после теста?', a: 'После завершения теста доступен разбор: правильные ответы и решения по каждому заданию. В режиме тренировки разбор идёт сразу.', url: '/exam-prep/math9', keywords: ['ошибк','разбор','решени','ответ','проверк'] },
{ id: 'flashcards', q: 'Как работают флешкарты?', { id: 'results-where', q: 'Где мои результаты?', a: 'Последние результаты — на дашборде, история и проценты по предметам — там же в виджетах.', url: '/dashboard', keywords: ['результат','оценк','процент','истори','статистик'] },
a: 'Создай колоду, добавь карточки (вопрос/ответ, можно картинку и формулы KaTeX). Система сама напомнит, что пора повторить.', // ── Флешкарты ──
url: '/flashcards', keywords: ['флешкарт', 'карточк', 'колод', 'повтор', 'память', 'katex', 'формул'] }, { id: 'flashcards', q: 'Как работают флешкарты?', a: 'Создай колоду, добавь карточки (вопрос/ответ, можно картинку и формулы KaTeX). Система сама напомнит, что пора повторить.', url: '/flashcards', keywords: ['флешкарт','карточк','колод','повтор','память'] },
{ id: 'exam-modes', q: 'Чем отличаются режимы экзамена?', { id: 'flashcards-katex', q: 'Как вставить формулу в карточку?', a: 'В редакторе карточки есть палитра KaTeX — выбирай символы или пиши LaTeX, превью покажет результат.', url: '/flashcards', keywords: ['формул','katex','latex','карточк','математ'] },
a: 'Экзамен — как на ЦТ/ЦЭ, на время. Тренировка — с разбором после каждого ответа. Случайный — быстрый набор вопросов.', { id: 'flashcards-img', q: 'Можно ли картинку на карточке?', a: 'Да — при создании карточки загрузи изображение, оно отрендерится на лицевой или оборотной стороне.', url: '/flashcards', keywords: ['картинк','изображен','карточк','фото'] },
url: '/exam-prep', keywords: ['экзамен', 'режим', 'тренировк', 'случайн', 'цт', 'цэ', 'тест'] }, { id: 'flashcards-fab', q: 'Что за кнопка «Запомнить» в углу?', a: 'Это быстрый способ создать карточку из любого места платформы — плавающая кнопка справа внизу.', url: '/flashcards', keywords: ['запомнить','кнопк','быстр','карточк','fab'] },
{ id: 'board-tools', q: 'Что умеет доска?', // ── Мои материалы ──
a: 'Карандаш, маркер, лазер, фигуры, соединители, стикеры, текст, формулы KaTeX, таблицы, линейка и транспортир. «Выделение» двигает и поворачивает объекты.', { id: 'materials-where', q: 'Что такое «Мои материалы»?', a: 'Личное хранилище: вырезки из учебника, страницы доски, заметки и рисунки. Хранятся у тебя, даже если урок удалят.', url: '/my-materials', keywords: ['материал','хранилищ','заметк','доск','вырезк'] },
url: '/board', keywords: ['доск', 'инструмент', 'рисов', 'фигур', 'линейк', 'маркер', 'whiteboard'] }, { id: 'materials-folders', q: 'Как разложить материалы по папкам?', a: 'В «Мои материалы» нажми «+ папка», затем у карточки выбери папку. Можно фильтровать по папкам и типам.', url: '/my-materials', keywords: ['папк','материал','коллекци','разложить','сортиров','фильтр'] },
{ id: 'pet', q: 'Зачем нужен питомец и XP?', { id: 'materials-annotate', q: 'Как рисовать поверх фото?', a: 'Открой материал-картинку и нажми кнопку с карандашом-линейкой — откроется редактор. Сохранение обновит ту же карточку.', url: '/my-materials', keywords: ['рисовать','аннотир','поверх','фото','карандаш','разметк'] },
a: 'Квантик растёт от твоей активности: за тесты, уроки и карточки идут XP и монеты. Серия за ежедневные занятия поднимает настроение и даёт бонусы.', { id: 'materials-note', q: 'Как создать заметку?', a: 'В «Мои материалы» кнопка «Заметка» вверху. Заметку потом можно превратить во флешкарту.', url: '/my-materials', keywords: ['заметк','создать','текст','note'] },
url: '/pet', keywords: ['питомец', 'квантик', 'xp', 'опыт', 'монет', 'серия', 'streak', 'настроение', 'уровень'] }, { id: 'materials-view', q: 'Почему картинка открывается в окне?', a: 'Клик по материалу-картинке открывает её в просмотрщике на странице. Там же «Скачать» и «В новой вкладке».', url: '/my-materials', keywords: ['картинк','открыт','просмотр','окно','лайтбокс'] },
{ id: 'homework', q: 'Где мои домашние задания?', // ── Доска / онлайн-урок ──
a: 'Все задания и дедлайны — в разделе «Домашние задания». Там же можно загрузить выполненную работу.', { id: 'board-tools', q: 'Что умеет доска?', a: 'Карандаш, маркер, лазер, фигуры, соединители, стикеры, текст, формулы KaTeX, таблицы, линейка и транспортир. «Выделение» двигает и поворачивает объекты.', url: '/board', keywords: ['доск','инструмент','рисов','фигур','линейк','маркер','whiteboard'] },
url: '/homework', keywords: ['домашк', 'домашн', 'задани', 'дедлайн', 'сдать', 'загрузить', 'работ'] }, { id: 'board-save', q: 'Как сохранить доску себе?', a: 'На уроке или в архиве нажми «К себе» — страница доски сохранится в «Мои материалы». Можно сохранить и выделенную область.', url: '/my-materials', keywords: ['доск','сохранить','к себе','область','материал'] },
{ id: 'quick-lesson', q: 'Как создать один урок без курса?', { id: 'classroom-join', q: 'Как попасть на онлайн-урок?', a: 'Когда учитель начинает урок, в меню «Онлайн-урок» появится приглашение — нажми, чтобы войти.', url: '/classroom', keywords: ['онлайн','урок','classroom','подключ','войти','вызов'] },
a: 'В «Теории» нажми «Быстрый урок» — урок создастся в скрытом личном контейнере, его не видно в общем каталоге.', { id: 'classroom-materials', q: 'Сохранятся ли заметки с онлайн-урока?', a: 'Да — доску и заметки можно сохранить в «Мои материалы», они переживут окончание сессии.', url: '/my-materials', keywords: ['онлайн','урок','заметк','сохран','материал'] },
url: '/theory', keywords: ['урок', 'быстрый', 'без курса', 'создать', 'теори'] }, // ── Теория / уроки ──
{ id: 'lab', q: 'Как открыть симуляции?', { id: 'theory', q: 'Что в разделе «Теория»?', a: 'Курсы и уроки с теорией и заданиями. Прогресс по урокам отслеживается.', url: '/theory', keywords: ['теори','курс','урок','обучен'] },
a: 'В «Лаборатории» симуляции запускаются прямо в браузере — установка не нужна. Выбери предмет и опыт.', { id: 'quick-lesson', q: 'Как создать один урок без курса?', a: 'В «Теории» нажми «Быстрый урок» — урок создастся в скрытом личном контейнере, его не видно в общем каталоге.', url: '/theory', keywords: ['урок','быстрый','без курса','создать','теори'] },
url: '/lab', keywords: ['лаборатори', 'симуляци', 'опыт', 'эксперимент', 'физик', 'хими'] }, { id: 'continue-lesson', q: 'Как продолжить с того же места?', a: 'На дашборде есть «Продолжить» с последним незаконченным уроком. Я тоже подскажу, когда есть что продолжить.', url: '/dashboard', keywords: ['продолж','урок','место','незаконч'] },
// ── Лаборатория / игры ──
{ id: 'lab', q: 'Как открыть симуляции?', a: 'В «Лаборатории» симуляции запускаются прямо в браузере — установка не нужна. Выбери предмет и опыт.', url: '/lab', keywords: ['лаборатори','симуляци','опыт','эксперимент','физик','хими'] },
{ id: 'optics', q: 'Есть ли конструктор оптики?', a: 'Да — в оптической скамье есть режим «Конструктор»: собирай системы из линз, зеркал и призм.', url: '/lab', keywords: ['оптик','линз','призм','конструктор','скамья'] },
{ id: 'games', q: 'Какие есть игры?', a: 'Кроссворд и «Виселица» по терминам предметов — закрепляют слова и понятия, дают XP.', url: '/crossword', keywords: ['игр','кроссворд','виселиц','термин','слов'] },
// ── Питомец / геймификация ──
{ id: 'pet', q: 'Зачем нужен питомец и XP?', a: 'Квантик растёт от твоей активности: за тесты, уроки и карточки идут XP и монеты. Серия за ежедневные занятия поднимает настроение и даёт бонусы.', url: '/pet', keywords: ['питомец','квантик','xp','опыт','монет','серия','streak','уровень'] },
{ id: 'streak', q: 'Что такое серия (streak)?', a: 'Серия — сколько дней подряд ты занимаешься. Чем длиннее, тем счастливее питомец и больше бонусов. Пропуск дня обнуляет серию.', url: '/pet', keywords: ['серия','streak','подряд','дни','бонус'] },
{ id: 'coins', q: 'Где потратить монеты?', a: 'В магазине на странице питомца — например, на фоны для Квантика.', url: '/pet', keywords: ['монет','магазин','купить','фон','награда'] },
{ id: 'achievements', q: 'Где достижения?', a: 'В профиле есть вкладка «Достижения» — ачивки за активность, серии и результаты.', url: '/profile', keywords: ['достижени','ачивк','награда','бейдж'] },
// ── Домашка / классы ──
{ id: 'homework', q: 'Где мои домашние задания?', a: 'Все задания и дедлайны — в разделе «Домашние задания». Там же можно загрузить выполненную работу.', url: '/homework', keywords: ['домашк','домашн','задани','дедлайн','сдать','загрузить'] },
{ id: 'join-class', q: 'Как вступить в класс?', a: 'Кнопка «Вступить в класс» в меню — введи код от учителя, и задания класса появятся у тебя.', url: '/dashboard', keywords: ['класс','вступить','код','присоедин'] },
// ── Профиль / настройки / поиск ──
{ id: 'search', q: 'Как искать по платформе?', a: 'Нажми «Поиск» в меню или Ctrl+K — найдёшь уроки, курсы, файлы и вопросы. Спроси меня — я тоже поищу.', url: null, keywords: ['поиск','найти','search','ctrl'] },
{ id: 'theme', q: 'Как включить тёмную тему?', a: 'В профиле → «Настройки» → «Внешний вид». Там же звуки и анимации.', url: '/profile', keywords: ['тема','тёмн','dark','внешний','настройк'] },
{ id: 'sounds', q: 'Как отключить звуки?', a: 'Профиль → «Настройки» → «Звуки системы»: общий выключатель, громкость и категории.', url: '/profile', keywords: ['звук','выключ','громкост','sound'] },
{ id: 'parent', q: 'Как дать доступ родителю?', a: 'В профиле есть «Доступ для родителей» — создай ссылку, по ней родитель видит твой прогресс.', url: '/profile', keywords: ['родител','доступ','ссылк','контрол'] },
{ id: 'bookmarks', q: 'Где мои закладки?', a: 'В профиле вкладка «Закладки» — сохранённые параграфы и страницы.', url: '/profile', keywords: ['закладк','сохран','избранн'] },
// ── Сам помощник ──
{ id: 'assistant-off', q: 'Как выключить помощника?', a: 'Профиль → «Настройки» → «Помощник Квантик». Можно выключить совсем или скрыть конкретные подсказки кнопкой «Не показывать».', url: '/profile', keywords: ['помощник','выключ','ассистент','подсказк','квантик','скрыть'] },
{ id: 'assistant-tour', q: 'Как пройти тур заново?', a: 'Нажми на меня и выбери «Тур по системе» — проведу по разделам ещё раз.', url: null, keywords: ['тур','онбординг','обзор','заново'] },
]; ];
/* ── Источники проактивных подсказок (всё уже есть в БД) ──────────────── */ /* ── Источники проактивных подсказок (всё уже есть в БД) ──────────────── */
@@ -101,6 +122,21 @@ function activeLesson(uid, role) {
} catch (e) { return null; } } catch (e) { return null; }
} }
function weakSubject(uid) {
// Предмет с наименьшим средним по завершённым тестам (минимум 2 теста).
try {
const row = db.prepare(`
SELECT s.slug AS slug, s.name AS name,
ROUND(AVG(ts.score * 100.0 / ts.total)) AS avg, COUNT(*) AS n
FROM test_sessions ts JOIN subjects s ON s.id = ts.subject_id
WHERE ts.user_id = ? AND ts.status = 'completed' AND ts.total > 0
GROUP BY ts.subject_id HAVING n >= 2
ORDER BY avg ASC LIMIT 1
`).get(uid);
return (row && row.avg != null && row.avg < 70) ? { slug: row.slug, name: row.name, avg: row.avg } : null;
} catch (e) { return null; }
}
/* ── GET /api/assistant/context ───────────────────────────────────────── */ /* ── GET /api/assistant/context ───────────────────────────────────────── */
function getContext(req, res) { function getContext(req, res) {
const uid = req.user.id; const uid = req.user.id;
@@ -118,6 +154,7 @@ function getContext(req, res) {
dueCards: dueCardsCount(uid), dueCards: dueCardsCount(uid),
homework: pendingHomework(uid), homework: pendingHomework(uid),
activeLesson: activeLesson(uid, req.user.role), activeLesson: activeLesson(uid, req.user.role),
weakSubject: weakSubject(uid),
}); });
} }
+124 -34
View File
@@ -34,6 +34,21 @@
if (p === '/my-materials') return 'materials'; if (p === '/my-materials') return 'materials';
if (p === '/lab') return 'lab'; if (p === '/lab') return 'lab';
if (p === '/theory' || p.indexOf('/course') === 0 || p.indexOf('/lesson') === 0) return 'theory'; 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'; return 'other';
} }
var PAGE = pageId(); var PAGE = pageId();
@@ -56,32 +71,7 @@
/* ── каталог правил ──────────────────────────────────────────────────── */ /* ── каталог правил ──────────────────────────────────────────────────── */
// scope: page | proactive | celebration. when(C) → bool. action(C) → {label,url}|null. // scope: page | proactive | celebration. when(C) → bool. action(C) → {label,url}|null.
var RULES = [ var RULES = [
// — контекстные — // — контекстные подсказки генерируются из PAGE_HINTS ниже
{ id: 'p-textbook', scope: 'page', cooldownDays: 14, maxShows: 2,
when: function () { return PAGE === 'textbook'; },
text: function () { return 'Любой кусок страницы можно вырезать картинкой в «Мои материалы» — кнопка «Вырезать область» внизу.'; },
action: function () { return null; } },
{ id: 'p-exam', scope: 'page', cooldownDays: 14, maxShows: 2,
when: function () { return PAGE === 'exam'; },
text: function () { return 'Три режима: экзамен (как на ЦТ/ЦЭ), тренировка (с разбором) и случайный. Выбирай под свою цель.'; },
action: function () { return null; } },
{ id: 'p-flashcards', scope: 'page', cooldownDays: 14, maxShows: 2,
when: function () { return PAGE === 'flashcards'; },
text: function () { return 'Формулы в карточках вводятся через KaTeX-палитру, а ещё можно добавить картинку.'; },
action: function () { return null; } },
{ id: 'p-materials', scope: 'page', cooldownDays: 14, maxShows: 2,
when: function () { return PAGE === 'materials'; },
text: function () { return 'Раскладывай материалы по папкам, а поверх фото можно рисовать — кнопка с карандашом.'; },
action: function () { return null; } },
{ id: 'p-lab', scope: 'page', cooldownDays: 14, maxShows: 2,
when: function () { return PAGE === 'lab'; },
text: function () { return 'Симуляции запускаются прямо в браузере — ничего ставить не нужно.'; },
action: function () { return null; } },
{ id: 'p-dashboard', scope: 'page', cooldownDays: 30, maxShows: 1,
when: function () { return PAGE === 'dashboard'; },
text: function () { return 'Виджеты на дашборде можно включать и переставлять под себя.'; },
action: function () { return null; } },
// — проактивные (из реальных данных) — // — проактивные (из реальных данных) —
{ id: 'hw-overdue', scope: 'proactive', cooldownDays: 1, maxShows: 30, { id: 'hw-overdue', scope: 'proactive', cooldownDays: 1, maxShows: 30,
when: function () { return !!(SRV && SRV.homework && SRV.homework.overdue); }, when: function () { return !!(SRV && SRV.homework && SRV.homework.overdue); },
@@ -102,13 +92,34 @@
{ id: 'streak-risk', scope: 'proactive', cooldownDays: 1, maxShows: 60, { id: 'streak-risk', scope: 'proactive', cooldownDays: 1, maxShows: 60,
when: function () { return !!(PET && PET.streakCurrent >= 1 && !activeToday() && new Date().getHours() >= 18); }, when: function () { return !!(PET && PET.streakCurrent >= 1 && !activeToday() && new Date().getHours() >= 18); },
text: function () { return 'Серия ' + PET.streakCurrent + ' ' + plural(PET.streakCurrent, 'день', 'дня', 'дней') + ' под угрозой — позанимайся сегодня, чтобы не потерять.'; }, text: function () { return 'Серия ' + PET.streakCurrent + ' ' + plural(PET.streakCurrent, 'день', 'дня', 'дней') + ' под угрозой — позанимайся сегодня, чтобы не потерять.'; },
action: function () { return { label: 'Заниматься', url: '/exam-prep' }; } }, action: function () { return { label: 'Заниматься', url: '/exam-prep/math9' }; } },
{ id: 'quest', scope: 'proactive', cooldownDays: 1, maxShows: 90, { id: 'quest', scope: 'proactive', cooldownDays: 1, maxShows: 90,
when: function () { return !!(quest(true) && new Date().getHours() >= 16); }, when: function () { return !!(quest(true) && new Date().getHours() >= 16); },
text: function () { var q = quest(true); return 'Остался квест дня: «' + (q.label || 'задание') + '».'; }, text: function () { var q = quest(true); return 'Остался квест дня: «' + (q.label || 'задание') + '».'; },
action: function () { return null; } }, 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) { function plural(n, one, few, many) {
var m10 = n % 10, m100 = n % 100; var m10 = n % 10, m100 = n % 100;
if (m10 === 1 && m100 !== 11) return one; if (m10 === 1 && m100 !== 11) return one;
@@ -116,6 +127,69 @@
return many; 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) { function eligible(rule) {
if (SUPPRESS_PAGE && rule.scope !== 'celebration') return false; if (SUPPRESS_PAGE && rule.scope !== 'celebration') return false;
@@ -217,6 +291,8 @@
'.asst-ans:first-of-type{border-top:none;}', '.asst-ans:first-of-type{border-top:none;}',
'.asst-ans-q{font-weight:700;color:#0F172A;margin-bottom:2px;}', '.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-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-empty{font-size:.82rem;color:#8a94a6;padding:6px 0;}', '.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;}}', '@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;}}',
@@ -291,13 +367,27 @@
q = (q || '').trim(); q = (q || '').trim();
if (q.length < 3) { box.innerHTML = ''; return; } if (q.length < 3) { box.innerHTML = ''; return; }
box.innerHTML = '<div class="asst-empty">Ищу…</div>'; box.innerHTML = '<div class="asst-empty">Ищу…</div>';
LS.assistantAsk(q).then(function (r) { Promise.all([
var ans = (r && r.answers) || []; LS.assistantAsk(q).catch(function () { return { answers: [] }; }),
if (!ans.length) { box.innerHTML = '<div class="asst-empty">Не нашёл точного ответа. Попробуй переформулировать.</div>'; return; } (LS.globalSearch ? LS.globalSearch(q, 'all', 4) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; }),
box.innerHTML = ans.map(function (a) { ]).then(function (res) {
return '<div class="asst-ans"><div class="asst-ans-q">' + esc(a.q) + '</div>' + esc(a.a) + var ans = (res[0] && res[0].answers) || [];
(a.url ? '<br><a class="asst-ans-link" href="' + esc(a.url) + '">Открыть</a>' : '') + '</div>'; var found = (res[1] && res[1].results) || [];
}).join(''); var html = '';
if (ans.length) {
html += ans.map(function (a) {
return '<div class="asst-ans"><div class="asst-ans-q">' + esc(a.q) + '</div>' + esc(a.a) +
(a.url ? '<br><a class="asst-ans-link" href="' + esc(a.url) + '">Открыть</a>' : '') + '</div>';
}).join('');
}
if (found.length) {
html += '<div class="asst-ans-sec">На платформе</div>';
html += found.slice(0, 4).map(function (f) {
return '<div class="asst-ans"><a class="asst-ans-link" style="margin-top:0" href="' + esc(f.url || '#') + '">' + esc(f.title || 'Без названия') + '</a>' +
(f.subtitle ? ' <span style="color:#8a94a6">— ' + esc(f.subtitle) + '</span>' : '') + '</div>';
}).join('');
}
box.innerHTML = html || '<div class="asst-empty">Ничего не нашёл. Попробуй переформулировать.</div>';
}).catch(function () { box.innerHTML = '<div class="asst-empty">Не удалось получить ответ.</div>'; }); }).catch(function () { box.innerHTML = '<div class="asst-empty">Не удалось получить ответ.</div>'; });
} }