feat(assistant): Ф2 онбординг-тур + проактив «продолжи урок»
Ф2: коачмарк-тур новичка по разделам (сайдбар + сам помощник), офер на дашборде пока не пройден/не закрыт, повтор из приветствия и Assistant.tour(). activeLesson: контекст-эндпоинт отдаёт начатый незавершённый урок (как «продолжить чтение»), добавлено проактивное правило «Продолжи …» → /course. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+113
-3
@@ -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 '<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
||||
'<div class="asst-text">Привет! Я помогу разобраться в системе. Спроси, как что-то сделать.</div>' +
|
||||
'<div class="asst-text">Привет! Я помогу разобраться в системе. Спроси, как что-то сделать, или пройди короткий тур.</div>' +
|
||||
'<div class="asst-actions"><button class="asst-btn" data-a="ask">Спросить</button>' +
|
||||
'<button class="asst-link" data-a="ok">Закрыть</button></div>';
|
||||
'<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(); };
|
||||
}
|
||||
|
||||
/* ── «Спроси Квантика» ───────────────────────────────────────────────── */
|
||||
@@ -291,6 +297,98 @@
|
||||
}).catch(function () { box.innerHTML = '<div class="asst-empty">Не удалось получить ответ.</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');
|
||||
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 =
|
||||
'<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;
|
||||
if (el && el.scrollIntoView) { try { el.scrollIntoView({ block: 'nearest' }); } catch (e) {} }
|
||||
}
|
||||
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();
|
||||
@@ -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(); },
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user