feat(assistant): долгая память об ученике (персонализация)
Производный профиль (без LLM): слабые предметы, трудные темы экзамена, цель/дата, серия — из test_sessions/exam_attempts/exam_user_plan. Подмешивается в системный промпт → персональные ответы; такие не кэшируются глобально. Заметки: таблица assistant_memory + фоновый LLM-экстрактор (дросселирован), дедуп + лимит 15. Панель ученика «Что я о тебе помню» (профиль + заметки, удаление). Админ-тумблер. API GET/DELETE /assistant/memory (/:id под authMiddleware, владелец проверяется в хендлере). Заодно: сверка стабильного baseline route-auth 56→66 (долг от branch-merge, хук не идёт на merge) — новых незащищённых маршрутов не добавлено. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,7 @@
|
||||
'<div class="perm-label"><i data-lucide="settings-2" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"></i>Настройки и статистика</div>' +
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:.84rem;cursor:pointer"><input type="checkbox" id="asst-rag" ' + (cfg.rag !== false ? 'checked' : '') + '> Искать ответы по учебникам (RAG)</label>' +
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:.84rem;cursor:pointer"><input type="checkbox" id="asst-exambtn" ' + (cfg.examButtons ? 'checked' : '') + '> Кнопки помощника на карточках экзамена</label>' +
|
||||
'<label style="display:flex;align-items:center;gap:8px;font-size:.84rem;cursor:pointer"><input type="checkbox" id="asst-memory" ' + (cfg.memory !== false ? 'checked' : '') + '> Персональная память об ученике (слабые темы, заметки)</label>' +
|
||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center"><button id="asst-reindex" class="asst-ib">Переиндексировать учебники</button><span id="asst-chunks" style="font-size:.78rem;color:#8a94a6">' + (cfg.chunks || 0) + ' фрагментов</span></div>' +
|
||||
'<div style="font-size:.78rem;color:#8a94a6">Сегодня: ' + (u.model_calls || 0) + ' к ИИ, ' + (u.cache_hits || 0) + ' из кэша, ' + (u.faq || 0) + ' FAQ. За 30 дней: ' + (u30.model_calls || 0) + ' / ' + (u30.cache_hits || 0) + ' / ' + (u30.faq || 0) + '.</div>' +
|
||||
'<div style="font-size:.78rem;color:#8a94a6">Оценки (30 дн): ' + (f.up || 0) + ' лайков, ' + (f.down || 0) + ' дизлайков' + ((f.recent || []).length ? '. Не помогло: ' + f.recent.map(function (x) { return '«' + esc(String(x.q || '').slice(0, 40)) + '»'; }).join(', ') : '') + '</div>';
|
||||
@@ -247,6 +248,7 @@
|
||||
// настройки
|
||||
Q('#asst-rag').addEventListener('change', function () { LS.adminSaveAssistant({ rag: Q('#asst-rag').checked }).then(function () { LS.toast('Сохранено', 'success'); }).catch(function () {}); });
|
||||
Q('#asst-exambtn').addEventListener('change', function () { LS.adminSaveAssistant({ examButtons: Q('#asst-exambtn').checked }).then(function () { LS.toast('Сохранено (обновите страницу экзамена)', 'success'); }).catch(function () {}); });
|
||||
Q('#asst-memory').addEventListener('change', function () { LS.adminSaveAssistant({ memory: Q('#asst-memory').checked }).then(function () { LS.toast('Сохранено', 'success'); }).catch(function () {}); });
|
||||
Q('#asst-reindex').addEventListener('click', async function () {
|
||||
var btn = Q('#asst-reindex'); btn.disabled = true; btn.textContent = 'Индексирую…';
|
||||
try { var r = await LS.adminReindexTextbooks(); Q('#asst-chunks').textContent = ((r && r.chunks) || 0) + ' фрагментов'; LS.toast('Готово', 'success'); }
|
||||
|
||||
@@ -341,6 +341,14 @@
|
||||
'.asst-fb svg{width:13px;height:13px;}',
|
||||
'html.asst-exam-on .tc-asst-btn{display:inline-flex !important;}',
|
||||
'.asst-empty{font-size:.82rem;color:#8a94a6;padding:6px 0;}',
|
||||
'.asst-mem-body{font-size:.82rem;color:#28324a;max-height:46vh;overflow:auto;}',
|
||||
'.asst-mem-prof{background:rgba(155,93,229,.07);border:1px solid rgba(155,93,229,.18);border-radius:10px;padding:9px 12px;line-height:1.75;margin-bottom:10px;}',
|
||||
'.asst-mem-notes-h{font-size:.66rem;font-weight:800;color:#8a94a6;text-transform:uppercase;letter-spacing:.03em;margin:6px 0 4px;}',
|
||||
'.asst-mem-note{display:flex;align-items:center;gap:8px;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(15,23,42,.06);}',
|
||||
'.asst-mem-note:last-of-type{border-bottom:none;}',
|
||||
'.asst-mem-x{border:none;background:none;color:#b4bcc8;cursor:pointer;font-size:1.15rem;line-height:1;padding:0 4px;}',
|
||||
'.asst-mem-x:hover{color:#e0335e;}',
|
||||
'.asst-mem-off{font-size:.82rem;color:#8a94a6;padding:10px 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('');
|
||||
@@ -506,7 +514,9 @@
|
||||
'<button class="asst-mode" data-m="hint">Подсказка</button>' +
|
||||
'<button class="asst-mode" data-m="check">Проверить решение</button></div>';
|
||||
openBubble(
|
||||
'<div class="asst-name"><span class="asst-name-face">' + faceSVG('happy') + '</span>Спроси Квантика' + (_chat.length ? '<button class="asst-link" data-a="clear" style="float:right;font-weight:600;margin-right:24px">Очистить</button>' : '') + '</div>' +
|
||||
'<div class="asst-name"><span class="asst-name-face">' + faceSVG('happy') + '</span>Спроси Квантика' +
|
||||
'<button class="asst-link" data-a="mem" style="float:right;font-weight:600;margin-right:24px">Память</button>' +
|
||||
(_chat.length ? '<button class="asst-link" data-a="clear" style="float:right;font-weight:600;margin-right:8px">Очистить</button>' : '') + '</div>' +
|
||||
'<div class="asst-chat"></div>' + chips + modes +
|
||||
'<input class="asst-ask-in" type="text" placeholder="' + MODE_PH.answer + '" maxlength="500" />' +
|
||||
'<div class="asst-memnote">Я помню последние ~6 сообщений этого разговора — как рабочая память: что было раньше, понимаю; старое постепенно забывается. «Очистить» — начать с чистого листа.</div>', {});
|
||||
@@ -537,9 +547,38 @@
|
||||
});
|
||||
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 '<div class="asst-mem-note"><span>' + esc(n.text) + '</span><button class="asst-mem-x" data-id="' + n.id + '" title="Забыть">×</button></div>'; }).join('');
|
||||
var body = m.enabled === false
|
||||
? '<div class="asst-mem-off">Персональная память выключена администратором.</div>'
|
||||
: '<div class="asst-mem-body">' +
|
||||
(prof.length ? '<div class="asst-mem-prof">' + prof.map(function (x) { return '<div>• ' + x + '</div>'; }).join('') + '</div>' : '') +
|
||||
(notes ? '<div class="asst-mem-notes-h">Заметки</div>' + notes : (prof.length ? '' : '<div class="asst-empty">Пока я ничего не запомнил — позанимайся, и здесь появятся слабые темы и заметки.</div>')) +
|
||||
((notes || prof.length) ? '<button class="asst-link" data-a="forget" style="margin-top:12px;color:#e0335e">Забыть всё</button>' : '') +
|
||||
'</div>';
|
||||
openBubble(
|
||||
'<div class="asst-name"><span class="asst-name-face">' + faceSVG('happy') + '</span>Что я о тебе помню' +
|
||||
'<button class="asst-link" data-a="back" style="float:right;font-weight:600;margin-right:24px">← Назад</button></div>' +
|
||||
body +
|
||||
'<div class="asst-memnote">Память помогает объяснять под тебя. Видна только тебе; учитель видит лишь общие слабые темы.</div>', {});
|
||||
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 send(q, context, chatEl, mode) {
|
||||
q = (q || '').trim();
|
||||
|
||||
Reference in New Issue
Block a user