From c49077abbc1644dbf3d7a40f156b9c78ae6864a0 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 15:12:49 +0300 Subject: [PATCH] =?UTF-8?q?feat(assistant):=20=D0=B6=D0=B8=D0=B2=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=BF=D0=B8=D1=82=D0=BE=D0=BC=D1=86=D0=B0?= =?UTF-8?q?=20=E2=80=94=20=D0=BB=D0=B8=D1=86=D0=BE=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=B3=D0=B8=D1=80=D1=83=D0=B5=D1=82=20=D0=BD=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=20(=D1=84=D0=B8=D1=87=D0=B0=206/?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Лицо Квантика в шапке чата (PetSprite) меняет настроение по состоянию: - думает (нейтральное + лёгкая анимация-покачивание asstThink) пока ждём/стримим - радуется (happy) на готовый ответ; грустит (sad) на ошибку/лимит/«не нашёл» - ликует (ecstatic) на сгенерированный тест и нарисованную картинку Вплетено в send/sendNonStream/makeQuiz/drawInChat через setNameFace(). Анимация уважает prefers-reduced-motion. Только frontend. Серия из 6 фич доработки Квантика завершена (стриминг, контекст урока, сократический режим, авто-здоровье провайдеров, генерация тестов, живость). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/assistant.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/js/assistant.js b/frontend/js/assistant.js index c282d99..1df7e8c 100644 --- a/frontend/js/assistant.js +++ b/frontend/js/assistant.js @@ -282,6 +282,9 @@ '.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-name-face{display:inline-block;transition:transform .2s;}', + reduceMotion ? '' : '.asst-name-face.asst-think{animation:asstThink 1.3s ease-in-out infinite;transform-origin:60% 70%;}', + '@keyframes asstThink{0%,100%{transform:scale(1) rotate(0);}50%{transform:scale(1.08) rotate(-4deg);}}', '.asst-bubble{position:absolute;left:0;bottom:66px;width:380px;max-width:92vw;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;}', @@ -363,6 +366,8 @@ /* ── рендер ──────────────────────────────────────────────────────────── */ function setFace(mood) { var f = root.querySelector('.asst-face'); if (f) f.innerHTML = faceSVG(mood); } + // живость: лицо Квантика в шапке чата реагирует на состояние (думает/радуется/грустит) + function setNameFace(mood) { var f = bubble && bubble.querySelector && bubble.querySelector('.asst-name-face'); if (f) { f.innerHTML = faceSVG(mood === 'thinking' ? 'neutral' : mood); f.classList.toggle('asst-think', mood === 'thinking'); } } function openBubble(html, opts) { opts = opts || {}; @@ -622,15 +627,15 @@ _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; + chatEl.scrollTop = chatEl.scrollHeight; setNameFace('thinking'); 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 = 'Не получилось нарисовать.'; + if (r && r.url) { d.innerHTML = ''; _chat.push({ role: 'assistant', content: '[картинка]', img: r.url }); setNameFace('ecstatic'); } + else { d.textContent = 'Не получилось нарисовать.'; setNameFace('sad'); } chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight; }).catch(function (err) { - ph.remove(); var d = msgEl('assistant'); + ph.remove(); var d = msgEl('assistant'); setNameFace('sad'); d.textContent = (err && err.data && err.data.error) || 'Не получилось нарисовать.'; chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight; }); @@ -648,6 +653,7 @@ 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; + setNameFace('thinking'); 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; @@ -666,9 +672,10 @@ _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; + chatEl.appendChild(em); chatEl.scrollTop = chatEl.scrollHeight; setNameFace('sad'); return; } var isModel = src === 'model' && (full || done.answer); + setNameFace(isModel ? 'happy' : 'neutral'); searchP.then(function (sres) { var found = (sres && sres.results) || []; var ansArr = (done.answers && done.answers.length ? done.answers : meta.answers) || []; @@ -720,6 +727,7 @@ 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; + setNameFace('thinking'); 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: [] }; }), @@ -730,9 +738,10 @@ 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; + chatEl.appendChild(em); chatEl.scrollTop = chatEl.scrollHeight; setNameFace('sad'); return; } var model = r0.source === 'model' ? r0.answer : null; + setNameFace(model ? 'happy' : 'neutral'); var ans = r0.answers || []; var sources = r0.sources || []; var found = (res[1] && res[1].results) || []; @@ -793,14 +802,14 @@ topic = (topic || '').trim(); var note = msgEl('assistant'); note.innerHTML = '
Составляю тестовые вопросы…
'; - chatEl.appendChild(note); chatEl.scrollTop = chatEl.scrollHeight; + chatEl.appendChild(note); chatEl.scrollTop = chatEl.scrollHeight; setNameFace('thinking'); Promise.all([ LS.assistantQuestions(topic, 5), (LS.getSubjects ? LS.getSubjects() : Promise.resolve([])).catch(function () { return []; }), ]).then(function (res) { var qs = (res[0] && res[0].questions) || []; var subjects = Array.isArray(res[1]) ? res[1] : ((res[1] && res[1].subjects) || []); - if (!qs.length) { note.innerHTML = '
Не получилось сгенерировать вопросы. Уточни тему и попробуй ещё.
'; return; } + if (!qs.length) { note.innerHTML = '
Не получилось сгенерировать вопросы. Уточни тему и попробуй ещё.
'; setNameFace('sad'); return; } note.remove(); var wrap = msgEl('assistant'); wrap.style.maxWidth = '100%'; var box = document.createElement('div'); box.className = 'asst-rich'; wrap.appendChild(box); @@ -839,9 +848,9 @@ saveB.style.display = 'none'; sel.disabled = true; topicIn.disabled = true; }); }); - chatEl.appendChild(wrap); chatEl.scrollTop = chatEl.scrollHeight; + chatEl.appendChild(wrap); chatEl.scrollTop = chatEl.scrollHeight; setNameFace('ecstatic'); }).catch(function (e) { - note.innerHTML = '
' + ((e && e.data && e.data.error) ? esc(e.data.error) : 'Не удалось сгенерировать вопросы.') + '
'; + note.innerHTML = '
' + ((e && e.data && e.data.error) ? esc(e.data.error) : 'Не удалось сгенерировать вопросы.') + '
'; setNameFace('sad'); }); }