diff --git a/backend/src/controllers/assistantController.js b/backend/src/controllers/assistantController.js
index c7e1a48..ad849cc 100644
--- a/backend/src/controllers/assistantController.js
+++ b/backend/src/controllers/assistantController.js
@@ -85,6 +85,22 @@ function pendingHomework(uid) {
} catch (e) { return { overdue: null, dueSoon: null }; }
}
+function activeLesson(uid, role) {
+ // Начатый, но не завершённый урок (как «продолжить чтение» на дашборде).
+ try {
+ const pub = role === 'student' ? 'AND l.is_published = 1 AND c.is_published = 1' : '';
+ const row = db.prepare(`
+ SELECT l.id AS lessonId, l.title AS lessonTitle, l.course_id AS courseId, c.title AS courseTitle
+ FROM lesson_progress lp
+ JOIN lessons l ON lp.lesson_id = l.id
+ JOIN courses c ON l.course_id = c.id
+ WHERE lp.user_id = ? AND lp.completed = 0 ${pub}
+ ORDER BY lp.updated_at DESC LIMIT 1
+ `).get(uid);
+ return row || null;
+ } catch (e) { return null; }
+}
+
/* ── GET /api/assistant/context ───────────────────────────────────────── */
function getContext(req, res) {
const uid = req.user.id;
@@ -97,10 +113,11 @@ function getContext(req, res) {
} catch (e) { /* table may be missing on a legacy instance */ }
res.json({
- enabled: u ? u.assistant_enabled !== 0 : true,
+ enabled: u ? u.assistant_enabled !== 0 : true,
seen,
- dueCards: dueCardsCount(uid),
- homework: pendingHomework(uid),
+ dueCards: dueCardsCount(uid),
+ homework: pendingHomework(uid),
+ activeLesson: activeLesson(uid, req.user.role),
});
}
diff --git a/frontend/js/assistant.js b/frontend/js/assistant.js
index bb094fb..49be345 100644
--- a/frontend/js/assistant.js
+++ b/frontend/js/assistant.js
@@ -91,6 +91,10 @@
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, 'карточка', 'карточки', 'карточек') + ' — освежим память?'; },
@@ -254,14 +258,16 @@
function greetHtml() {
return '
' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '
' +
- 'Привет! Я помогу разобраться в системе. Спроси, как что-то сделать.
' +
+ 'Привет! Я помогу разобраться в системе. Спроси, как что-то сделать, или пройди короткий тур.
' +
'' +
- '
';
+ '' +
+ '';
}
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(); };
}
/* ── «Спроси Квантика» ───────────────────────────────────────────────── */
@@ -291,6 +297,98 @@
}).catch(function () { box.innerHTML = 'Не удалось получить ответ.
'; });
}
+ /* ── Ф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 = '';
+ document.body.appendChild(ov);
+ var ring = ov.querySelector('.asst-tour-ring');
+ var tip = ov.querySelector('.asst-tour-tip');
+ 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;
+ var r = el && vis(el) ? el.getBoundingClientRect() : null;
+ 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, ty = Math.max(12, r.top);
+ if (tx + 290 > window.innerWidth) tx = Math.max(12, r.left - 294); // показать слева, если справа не влезает
+ tip.style.left = Math.min(tx, window.innerWidth - 290) + 'px';
+ tip.style.top = Math.min(ty, window.innerHeight - 160) + 'px';
+ tip.style.transform = '';
+ } else {
+ ring.style.display = 'none';
+ ov.style.background = 'rgba(15,12,30,.55)';
+ tip.style.left = '50%'; tip.style.top = '50%'; tip.style.transform = 'translate(-50%,-50%)';
+ }
+ tip.innerHTML =
+ '' + esc(st.title) + '
' +
+ '' + esc(st.text) + '
' +
+ '' + (i + 1) + ' / ' + steps.length + '' +
+ '' +
+ (i > 0 ? '' : '') +
+ '' +
+ '
';
+ 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;
+ if (el && el.scrollIntoView) { try { el.scrollIntoView({ block: 'nearest' }); } catch (e) {} }
+ }
+ window.addEventListener('resize', render);
+ document.addEventListener('keydown', onKey);
+ render();
+ }
+
+ function onboardingOffer() {
+ openBubble(
+ '' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '
' +
+ 'Привет! Я ' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '. Показать за минуту, что где в системе?
' +
+ '' +
+ '
', {});
+ try { LS.assistantSeen('onboarding'); } catch (e) {}
+ bubble.querySelector('[data-a="tour"]').onclick = function () { startTour(); };
+ bubble.querySelector('[data-a="later"]').onclick = closeBubble;
+ }
+
/* ── монтирование ────────────────────────────────────────────────────── */
function mount() {
ensureStyles();
@@ -309,6 +407,15 @@
else showGreet();
};
+ // Онбординг новичка — приоритетно на дашборде, пока не пройден/не закрыт
+ 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) {
@@ -342,5 +449,8 @@
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(); } };
+ window.Assistant = {
+ open: function () { if (root) root.querySelector('.asst-fab').click(); },
+ tour: function () { startTour(); },
+ };
})();