Привет! Я помогу разобраться в системе. Спроси, как что-то сделать, или пройди короткий тур.
' +
'
' +
'' +
'
';
}
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, '$1')
.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2')
.replace(/`([^`]+)`/g, '$1');
}
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 += '
'; list = 'ul'; } html += '
' + mdInline(mUl[1]) + '
'; continue; }
if (mOl) { if (list !== 'ol') { closeList(); html += ''; list = 'ol'; } html += '
' + mdInline(mOl[1]) + '
'; continue; }
closeList();
if (mH) { html += '
' + mdInline(mH[1]) + '
'; continue; }
if (ln.trim() !== '') html += '
' + mdInline(ln) + '
';
}
closeList();
return html;
}
function renderRich(container, text) {
var src = String(text || '');
// ответ мог оборваться по лимиту токенов посреди $$…$$ — не показываем сырой LaTeX, обрезаем хвост
var dd = src.match(/\$\$/g);
if (dd && dd.length % 2 === 1) { var li = src.lastIndexOf('$$'); src = src.slice(0, li).replace(/[\s\\]+$/, '') + ' …'; }
var math = [];
var protectedText = src.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 = '';
var _role = 'student';
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, kind: 'textbook' };
}
}
if (PAGE === 'theory') {
var c = document.querySelector('.lesson-content, .lsn-content, .lesson-body, #lesson-content, article.lesson, .course-lesson, .lesson-view');
if (c) {
var lt = document.querySelector('h1, .lsn-title, .lesson-title');
var ltitle = (lt && lt.textContent.trim()) || (document.title || 'Урок').split('·')[0].trim();
var ltext = (c.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 3500);
if (ltext.length > 60) return { title: ltitle, text: ltext, kind: 'lesson' };
}
}
} catch (e) {}
return null;
}
// Лёгкий ситуативный контекст для ЛЮБОГО вопроса — где сейчас ученик (заголовок+раздел).
var PAGE_LABEL = { textbook: 'учебник', theory: 'урок/курс', exam: 'экзамен или тест', flashcards: 'флешкарты',
lab: 'лаборатория', homework: 'домашние задания', dashboard: 'главная', knowledge: 'карта знаний',
library: 'библиотека', analytics: 'аналитика', gradebook: 'журнал', qbank: 'банк вопросов' };
function pageHint() {
try {
var label = PAGE_LABEL[PAGE] || '';
var hEl = document.querySelector('.sec.active .sec-h, h1, .lsn-title, .lesson-title, .page-title');
var title = (hEl && hEl.textContent.trim()) || (document.title || '').split('·')[0].split('|')[0].trim();
title = title.replace(/\s+/g, ' ').slice(0, 120);
if (!title && !label) return '';
return 'Ученик сейчас на странице платформы' + (label ? ' («' + label + '»)' : '') + (title ? ': «' + title + '»' : '') + '. Учитывай это, если вопрос относится к материалу страницы.';
} catch (e) { return ''; }
}
/* ── «Спроси Квантика» ───────────────────────────────────────────────── */
var SUGGESTIONS = ['Как вырезать кусок учебника?', 'Как создать карточки?', 'Как начать тест?', 'Объясни теорему Пифагора', 'Где мои домашние задания?', 'Как включить тёмную тему?'];
var TEACHER_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.img) d.innerHTML = '';
else if (m.role === 'assistant') { d.innerHTML = ''; renderRich(d.querySelector('.asst-rich'), m.content); }
else d.textContent = m.content;
chatEl.appendChild(d);
});
chatEl.scrollTop = chatEl.scrollHeight;
}
var FB_UP = '';
var FB_DOWN = '';
var MODE_PH = { answer: 'Спроси что угодно: «объясни…», «как…»', hint: 'Задача, по которой нужна подсказка…', check: 'Вставь своё решение — проверю…', draw: 'Опиши картинку: «кот-учёный, плоская иллюстрация»', quiz: 'Тема или текст — сгенерирую вопросы для банка' };
function openAsk(prefill) {
var sel = _lastSel, pc = getPageContext();
var noun = pc && pc.kind === 'lesson' ? 'этот урок' : 'этот параграф';
var noun2 = pc && pc.kind === 'lesson' ? 'урока' : 'параграфа';
var ctxBtns = '';
if (sel) ctxBtns += '';
if (pc) ctxBtns += '' +
'' +
'';
var sug = (_role === 'teacher' || _role === 'admin') ? TEACHER_SUGGESTIONS : SUGGESTIONS;
var chips = '
Я помню последние ~6 сообщений этого разговора — как рабочая память: что было раньше, понимаю; старое постепенно забывается. «Очистить» — начать с чистого листа.
', {});
var inp = bubble.querySelector('.asst-ask-in');
var chatEl = bubble.querySelector('.asst-chat');
var chipsEl = bubble.querySelector('.asst-chips');
var mode = 'answer';
renderChat(chatEl);
if (_chat.length) chipsEl.style.display = 'none';
// свободный вопрос (context не задан явно) → подмешиваем лёгкий ситуативный контекст страницы
function go(q, context, m) { if (chipsEl) chipsEl.style.display = 'none'; inp.value = ''; if (context == null) context = pageHint() || undefined; send(q, context, chatEl, m || mode); }
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(inp.value); });
bubble.querySelectorAll('.asst-mode').forEach(function (b) {
b.addEventListener('click', function () {
mode = b.getAttribute('data-m');
bubble.querySelectorAll('.asst-mode').forEach(function (x) { x.classList.toggle('on', x === b); });
inp.placeholder = MODE_PH[mode] || MODE_PH.answer; inp.focus();
});
});
bubble.querySelectorAll('.asst-chip').forEach(function (c) {
c.addEventListener('click', function () {
var ctx = c.getAttribute('data-ctx');
if (ctx === 'sel') return go('Объясни простыми словами и приведи пример.', sel, 'answer');
if (ctx === 'sec') return go('Объясни простыми словами, о чём этот параграф, и выдели главное.', pc && pc.text, 'answer');
if (ctx === 'sum') return go('Сделай краткий конспект этого материала: 4–6 главных пунктов.', pc && pc.text, 'answer');
if (ctx === 'cards') { if (chipsEl) chipsEl.style.display = 'none'; return makeFlashcards(pc, chatEl); }
go(c.textContent, null, 'answer');
});
});
var clr = bubble.querySelector('[data-a="clear"]');
if (clr) clr.onclick = function () { _chat = []; openAsk(); };
var memBtn = bubble.querySelector('[data-a="mem"]');
if (memBtn) memBtn.onclick = openMemory;
if (prefill && prefill.q) go(prefill.q, prefill.context, prefill.mode);
else inp.focus();
}
/* ── «Что я о тебе помню» ── */
function openMemory() {
LS.assistantMemory().then(function (m) {
if (!m) return;
var p = m.profile || {}, prof = [];
if (p.exam) prof.push('Готовишься к экзамену' + (p.exam.date ? ' (до ' + esc(p.exam.date) + ')' : ''));
if (p.weakSubjects && p.weakSubjects.length) prof.push('Слабые предметы: ' + p.weakSubjects.map(function (s) { return esc(s.name) + ' ' + s.avg + '%'; }).join(', '));
if (p.weakTopics && p.weakTopics.length) prof.push('Трудные темы: ' + p.weakTopics.map(function (t) { return esc(t.topic) + ' ' + t.rate + '%'; }).join(', '));
if (p.streak >= 3) prof.push('Серия занятий: ' + p.streak + ' дн.');
var notes = (m.notes || []).map(function (n) { return '
' + esc(n.text) + '
'; }).join('');
var body = m.enabled === false
? '
Персональная память выключена администратором.
'
: '
' +
(prof.length ? '
' + prof.map(function (x) { return '
• ' + x + '
'; }).join('') + '
' : '') +
(notes ? '
Заметки
' + notes : (prof.length ? '' : '
Пока я ничего не запомнил — позанимайся, и здесь появятся слабые темы и заметки.
')) +
((notes || prof.length) ? '' : '') +
'
';
openBubble(
'
' + faceSVG('happy') + 'Что я о тебе помню' +
'
' +
body +
'
Память помогает объяснять под тебя. Видна только тебе; учитель видит лишь общие слабые темы.
', {});
var bk = bubble.querySelector('[data-a="back"]'); if (bk) bk.onclick = function () { openAsk(); };
var fg = bubble.querySelector('[data-a="forget"]'); if (fg) fg.onclick = function () { LS.assistantMemoryClear().then(openMemory); };
bubble.querySelectorAll('.asst-mem-x').forEach(function (b) { b.onclick = function () { LS.assistantMemoryClear(b.getAttribute('data-id')).then(openMemory); }; });
});
}
function srcUrl(s) { return '/textbook/' + s.slug + (s.ref ? '#sec-' + s.ref : ''); }
function drawInChat(prompt, chatEl) {
prompt = (prompt || '').trim();
if (prompt.length < 3) return;
_chat.push({ role: 'user', content: 'Нарисуй: ' + prompt });
var u = msgEl('user'); u.textContent = 'Нарисуй: ' + prompt; chatEl.appendChild(u);
var ph = msgEl('assistant'); ph.className += ' asst-msg-ph'; ph.textContent = 'Рисую картинку…'; chatEl.appendChild(ph);
chatEl.scrollTop = chatEl.scrollHeight;
LS.imageGen(prompt).then(function (r) {
ph.remove();
var d = msgEl('assistant');
if (r && r.url) { d.innerHTML = ''; _chat.push({ role: 'assistant', content: '[картинка]', img: r.url }); }
else d.textContent = 'Не получилось нарисовать.';
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
}).catch(function (err) {
ph.remove(); var d = msgEl('assistant');
d.textContent = (err && err.data && err.data.error) || 'Не получилось нарисовать.';
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
});
}
function send(q, context, chatEl, mode) {
q = (q || '').trim();
if (q.length < 2) return;
if (mode === 'draw') return drawInChat(q, chatEl);
if (mode === 'quiz') return makeQuiz(q, chatEl);
// стриминг недоступен (старый кэш api.js / нет ReadableStream) — обычный путь
if (!LS.assistantAskStream || typeof ReadableStream === 'undefined') return sendNonStream(q, context, chatEl, mode);
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 = mode === 'check' ? 'Проверяю…' : 'Думаю…'; chatEl.appendChild(ph);
chatEl.scrollTop = chatEl.scrollHeight;
var searchP = (LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; });
var meta = { answers: [], sources: [] }, full = '', msgD = null, richEl = null, streamed = false, finalized = false;
function ensureMsg() {
if (msgD) return;
if (ph.parentNode) ph.remove();
msgD = msgEl('assistant'); msgD.innerHTML = '';
richEl = msgD.querySelector('.asst-rich'); chatEl.appendChild(msgD);
}
function finalize(done) {
if (finalized) return; finalized = true;
done = done || {};
var src = done.source;
if ((src === 'limit' || src === 'error') && !full) {
_chat.pop();
if (msgD) msgD.remove(); if (ph.parentNode) ph.remove();
var em = msgEl('assistant'); em.className += ' asst-msg-ph'; em.textContent = done.answer || 'Сейчас не получилось. Попробуй ещё раз.';
chatEl.appendChild(em); chatEl.scrollTop = chatEl.scrollHeight; return;
}
var isModel = src === 'model' && (full || done.answer);
searchP.then(function (sres) {
var found = (sres && sres.results) || [];
var ansArr = (done.answers && done.answers.length ? done.answers : meta.answers) || [];
var sources = done.sources || meta.sources || [];
var content = isModel ? (full || done.answer) : ((ansArr[0] && (ansArr[0].q + '\n' + ansArr[0].a)) || 'Не нашёл точного ответа. Попробуй переформулировать или поищи (Ctrl+K).');
ensureMsg(); richEl.classList.remove('asst-streaming');
_chat.push({ role: 'assistant', content: content });
renderRich(richEl, content);
if (isModel && sources.length) {
var sc = document.createElement('div'); sc.className = 'asst-src';
sc.innerHTML = 'Источник: ' + sources.map(function (s) { return '' + esc(s.title) + (s.section ? ', ' + esc(s.section) : '') + ''; }).join('; ');
chatEl.appendChild(sc);
}
var links = '';
if (!isModel && ansArr.length) links += ansArr.slice(0, 2).filter(function (a) { return a.url; }).map(function (a) { return '' + esc(a.q) + ''; }).join(' · ');
if (found.length) links += (links ? ' ' : '') + 'На платформе: ' + found.slice(0, 3).map(function (f) { return '' + esc(f.title || '…') + ''; }).join(' · ');
if (links) { var l = document.createElement('div'); l.className = 'asst-msg-links'; l.innerHTML = links; chatEl.appendChild(l); }
if (isModel) {
var fb = document.createElement('div'); fb.className = 'asst-fb';
fb.innerHTML = '';
fb.querySelectorAll('button').forEach(function (b) {
b.addEventListener('click', function () { if (fb.dataset.done) return; fb.dataset.done = '1'; b.classList.add('on'); try { LS.assistantFeedback(Number(b.getAttribute('data-r')), q); } catch (e) {} });
});
chatEl.appendChild(fb);
}
chatEl.scrollTop = chatEl.scrollHeight;
});
}
LS.assistantAskStream(q, context, history, mode, {
onMeta: function (m) { if (m.answers) meta.answers = m.answers; if (m.sources) meta.sources = m.sources; },
onDelta: function (t) { streamed = true; ensureMsg(); full += t; richEl.textContent = full; chatEl.scrollTop = chatEl.scrollHeight; },
onDone: function (o) { finalize(o); },
}).then(function () { if (!finalized) finalize({ source: full ? 'model' : 'faq' }); })
.catch(function () {
if (finalized) return;
if (!streamed) { if (ph.parentNode) ph.remove(); _chat.pop(); sendNonStream(q, context, chatEl, mode); }
else finalize({ source: 'model' });
});
}
function sendNonStream(q, context, chatEl, mode) {
q = (q || '').trim();
if (q.length < 2) return;
if (mode === 'draw') return drawInChat(q, chatEl);
if (mode === 'quiz') return makeQuiz(q, chatEl);
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 = mode === 'check' ? 'Проверяю…' : 'Думаю…'; chatEl.appendChild(ph);
chatEl.scrollTop = chatEl.scrollHeight;
Promise.all([
LS.assistantAsk(q, context, history, mode).catch(function () { return { answers: [] }; }),
(LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; }),
]).then(function (res) {
ph.remove();
var r0 = res[0] || {};
// лимит/ошибка ИИ — не ломаем память диалога: убираем последний вопрос, показываем сообщение
if (r0.source === 'limit' || r0.source === 'error') {
_chat.pop();
var em = msgEl('assistant'); em.className += ' asst-msg-ph'; em.textContent = r0.answer || 'Сейчас не получилось. Попробуй ещё раз.';
chatEl.appendChild(em); chatEl.scrollTop = chatEl.scrollHeight; return;
}
var model = r0.source === 'model' ? r0.answer : null;
var ans = r0.answers || [];
var sources = r0.sources || [];
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 = ''; chatEl.appendChild(d);
renderRich(d.querySelector('.asst-rich'), content);
// источники (RAG)
if (model && sources.length) {
var sc = document.createElement('div'); sc.className = 'asst-src';
sc.innerHTML = 'Источник: ' + sources.map(function (s) { return '' + esc(s.title) + (s.section ? ', ' + esc(s.section) : '') + ''; }).join('; ');
chatEl.appendChild(sc);
}
// ссылки FAQ/платформа
var links = '';
if (!model && ans.length) links += ans.slice(0, 2).filter(function (a) { return a.url; }).map(function (a) { return '' + esc(a.q) + ''; }).join(' · ');
if (found.length) links += (links ? ' ' : '') + 'На платформе: ' + found.slice(0, 3).map(function (f) { return '' + esc(f.title || '…') + ''; }).join(' · ');
if (links) { var l = document.createElement('div'); l.className = 'asst-msg-links'; l.innerHTML = links; chatEl.appendChild(l); }
// оценка ответа
if (model) {
var fb = document.createElement('div'); fb.className = 'asst-fb';
fb.innerHTML = '';
fb.querySelectorAll('button').forEach(function (b) {
b.addEventListener('click', function () {
if (fb.dataset.done) return; fb.dataset.done = '1';
b.classList.add('on');
try { LS.assistantFeedback(Number(b.getAttribute('data-r')), q); } catch (e) {}
});
});
chatEl.appendChild(fb);
}
chatEl.scrollTop = chatEl.scrollHeight;
}).catch(function () { ph.textContent = 'Не удалось получить ответ.'; });
}
/* ── «Флешкарты из параграфа» — модель → колода через API флешкарт ─────── */
function makeFlashcards(pc, chatEl) {
var note = msgEl('assistant');
if (!pc || !pc.text) { note.innerHTML = '