feat(assistant): чёткий ответ при лимите ИИ (память не теряется), напоминание о памяти, отдельный раздел в админке

- Баг «не помнит»: на самом деле free-лимит Gemini (429). callLLM теперь
  возвращает ошибку; при 429 показываем «много запросов, подожди минутку —
  память не потеряется» и НЕ ломаем историю (убираем неудачный вопрос); при
  сбое — «не получилось, попробуй позже». Раньше показывалось «не нашёл ответ».
- В окне «Спроси» — пояснение, сколько помнит Квантик (≈6 реплик, рабочая память).
- Окна красивее: шире, аватар Квантика в шапке, мягкая анимация.
- Управление помощником вынесено в отдельный раздел админки «Помощник Квантик»
  (системный вкл/выкл + модель/ключ/тест/RAG/кнопки экзамена/статистика/качество);
  из раздела «Игры» конфиг убран.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 20:03:02 +03:00
parent 961504b256
commit 78300845ed
6 changed files with 176 additions and 116 deletions
+19 -8
View File
@@ -280,9 +280,12 @@
'.asst-dot{position:absolute;top:0;right:0;width:13px;height:13px;border-radius:50%;background:#F15BB5;border:2px solid #fff;}',
reduceMotion ? '' : '.asst-fab.pulse{animation:asstPulse 2.2s ease-in-out infinite;}',
'@keyframes asstPulse{0%,100%{box-shadow:0 8px 24px rgba(139,92,246,.32);}50%{box-shadow:0 8px 30px rgba(241,91,181,.5);}}',
'.asst-bubble{position:absolute;left:0;bottom:64px;width:300px;max-width:78vw;background:#fff;border-radius:16px;',
' box-shadow:0 18px 50px rgba(15,23,42,.22);padding:14px 16px;border:1px solid rgba(15,23,42,.07);',
' opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .18s,transform .18s;}',
'.asst-bubble{position:absolute;left:0;bottom:66px;width:330px;max-width:88vw;background:#fff;border-radius:18px;',
' box-shadow:0 20px 56px rgba(15,23,42,.24);padding:15px 17px;border:1px solid rgba(15,23,42,.07);',
' opacity:0;transform:translateY(8px) scale(.98);pointer-events:none;transition:opacity .18s,transform .18s;transform-origin:bottom left;}',
'.asst-name-face{display:inline-block;width:20px;height:20px;vertical-align:-4px;margin-right:7px;}',
'.asst-name-face svg{width:100%;height:100%;display:block;}',
'.asst-memnote{font-size:.66rem;color:#9aa5b4;margin-top:9px;line-height:1.45;border-top:1px solid rgba(15,23,42,.05);padding-top:8px;}',
'.asst-bubble.open{opacity:1;transform:translateY(0);pointer-events:auto;}',
'.asst-x{position:absolute;top:8px;right:8px;width:26px;height:26px;border:none;background:transparent;color:#8a94a6;',
' cursor:pointer;border-radius:7px;font-size:18px;line-height:1;}',
@@ -493,9 +496,10 @@
'<button class="asst-mode" data-m="hint">Подсказка</button>' +
'<button class="asst-mode" data-m="check">Проверить решение</button></div>';
openBubble(
'<div class="asst-name">Спроси Квантика' + (_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>Спроси Квантика' + (_chat.length ? '<button class="asst-link" data-a="clear" style="float:right;font-weight:600;margin-right:24px">Очистить</button>' : '') + '</div>' +
'<div class="asst-chat"></div>' + chips + modes +
'<input class="asst-ask-in" type="text" placeholder="' + MODE_PH.answer + '" maxlength="500" />', {});
'<input class="asst-ask-in" type="text" placeholder="' + MODE_PH.answer + '" maxlength="500" />' +
'<div class="asst-memnote">Я помню последние ~6 сообщений этого разговора — как рабочая память: что было раньше, понимаю; старое постепенно забывается. «Очистить» — начать с чистого листа.</div>', {});
var inp = bubble.querySelector('.asst-ask-in');
var chatEl = bubble.querySelector('.asst-chat');
var chipsEl = bubble.querySelector('.asst-chips');
@@ -540,9 +544,16 @@
(LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; }),
]).then(function (res) {
ph.remove();
var model = res[0] && res[0].answer;
var ans = (res[0] && res[0].answers) || [];
var sources = (res[0] && res[0].sources) || [];
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 });