feat(assistant): живость питомца — лицо реагирует на диалог (фича 6/6)
Лицо Квантика в шапке чата (PetSprite) меняет настроение по состоянию: - думает (нейтральное + лёгкая анимация-покачивание asstThink) пока ждём/стримим - радуется (happy) на готовый ответ; грустит (sad) на ошибку/лимит/«не нашёл» - ликует (ecstatic) на сгенерированный тест и нарисованную картинку Вплетено в send/sendNonStream/makeQuiz/drawInChat через setNameFace(). Анимация уважает prefers-reduced-motion. Только frontend. Серия из 6 фич доработки Квантика завершена (стриминг, контекст урока, сократический режим, авто-здоровье провайдеров, генерация тестов, живость). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-10
@@ -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 = '<img src="' + r.url + '" alt="" style="width:100%;border-radius:10px;display:block">'; _chat.push({ role: 'assistant', content: '[картинка]', img: r.url }); }
|
||||
else d.textContent = 'Не получилось нарисовать.';
|
||||
if (r && r.url) { d.innerHTML = '<img src="' + r.url + '" alt="" style="width:100%;border-radius:10px;display:block">'; _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 = '<div class="asst-rich">Составляю тестовые вопросы…</div>';
|
||||
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 = '<div class="asst-rich">Не получилось сгенерировать вопросы. Уточни тему и попробуй ещё.</div>'; return; }
|
||||
if (!qs.length) { note.innerHTML = '<div class="asst-rich">Не получилось сгенерировать вопросы. Уточни тему и попробуй ещё.</div>'; 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 = '<div class="asst-rich">' + ((e && e.data && e.data.error) ? esc(e.data.error) : 'Не удалось сгенерировать вопросы.') + '</div>';
|
||||
note.innerHTML = '<div class="asst-rich">' + ((e && e.data && e.data.error) ? esc(e.data.error) : 'Не удалось сгенерировать вопросы.') + '</div>'; setNameFace('sad');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user