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;}',
|
'.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;}',
|
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);}}',
|
'@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;',
|
'.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);',
|
' 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;}',
|
' 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 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) {
|
function openBubble(html, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@@ -622,15 +627,15 @@
|
|||||||
_chat.push({ role: 'user', content: 'Нарисуй: ' + prompt });
|
_chat.push({ role: 'user', content: 'Нарисуй: ' + prompt });
|
||||||
var u = msgEl('user'); u.textContent = 'Нарисуй: ' + prompt; chatEl.appendChild(u);
|
var u = msgEl('user'); u.textContent = 'Нарисуй: ' + prompt; chatEl.appendChild(u);
|
||||||
var ph = msgEl('assistant'); ph.className += ' asst-msg-ph'; ph.textContent = 'Рисую картинку…'; chatEl.appendChild(ph);
|
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) {
|
LS.imageGen(prompt).then(function (r) {
|
||||||
ph.remove();
|
ph.remove();
|
||||||
var d = msgEl('assistant');
|
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 }); }
|
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 = 'Не получилось нарисовать.';
|
else { d.textContent = 'Не получилось нарисовать.'; setNameFace('sad'); }
|
||||||
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
}).catch(function (err) {
|
}).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) || 'Не получилось нарисовать.';
|
d.textContent = (err && err.data && err.data.error) || 'Не получилось нарисовать.';
|
||||||
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
});
|
});
|
||||||
@@ -648,6 +653,7 @@
|
|||||||
var u = msgEl('user'); u.textContent = q; chatEl.appendChild(u);
|
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);
|
var ph = msgEl('assistant'); ph.className += ' asst-msg-ph'; ph.textContent = mode === 'check' ? 'Проверяю…' : 'Думаю…'; chatEl.appendChild(ph);
|
||||||
chatEl.scrollTop = chatEl.scrollHeight;
|
chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
|
setNameFace('thinking');
|
||||||
|
|
||||||
var searchP = (LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; });
|
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;
|
var meta = { answers: [], sources: [] }, full = '', msgD = null, richEl = null, streamed = false, finalized = false;
|
||||||
@@ -666,9 +672,10 @@
|
|||||||
_chat.pop();
|
_chat.pop();
|
||||||
if (msgD) msgD.remove(); if (ph.parentNode) ph.remove();
|
if (msgD) msgD.remove(); if (ph.parentNode) ph.remove();
|
||||||
var em = msgEl('assistant'); em.className += ' asst-msg-ph'; em.textContent = done.answer || 'Сейчас не получилось. Попробуй ещё раз.';
|
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);
|
var isModel = src === 'model' && (full || done.answer);
|
||||||
|
setNameFace(isModel ? 'happy' : 'neutral');
|
||||||
searchP.then(function (sres) {
|
searchP.then(function (sres) {
|
||||||
var found = (sres && sres.results) || [];
|
var found = (sres && sres.results) || [];
|
||||||
var ansArr = (done.answers && done.answers.length ? done.answers : meta.answers) || [];
|
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 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);
|
var ph = msgEl('assistant'); ph.className += ' asst-msg-ph'; ph.textContent = mode === 'check' ? 'Проверяю…' : 'Думаю…'; chatEl.appendChild(ph);
|
||||||
chatEl.scrollTop = chatEl.scrollHeight;
|
chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
|
setNameFace('thinking');
|
||||||
Promise.all([
|
Promise.all([
|
||||||
LS.assistantAsk(q, context, history, mode).catch(function () { return { answers: [] }; }),
|
LS.assistantAsk(q, context, history, mode).catch(function () { return { answers: [] }; }),
|
||||||
(LS.globalSearch ? LS.globalSearch(q, 'all', 3) : Promise.resolve({ results: [] })).catch(function () { return { results: [] }; }),
|
(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') {
|
if (r0.source === 'limit' || r0.source === 'error') {
|
||||||
_chat.pop();
|
_chat.pop();
|
||||||
var em = msgEl('assistant'); em.className += ' asst-msg-ph'; em.textContent = r0.answer || 'Сейчас не получилось. Попробуй ещё раз.';
|
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;
|
var model = r0.source === 'model' ? r0.answer : null;
|
||||||
|
setNameFace(model ? 'happy' : 'neutral');
|
||||||
var ans = r0.answers || [];
|
var ans = r0.answers || [];
|
||||||
var sources = r0.sources || [];
|
var sources = r0.sources || [];
|
||||||
var found = (res[1] && res[1].results) || [];
|
var found = (res[1] && res[1].results) || [];
|
||||||
@@ -793,14 +802,14 @@
|
|||||||
topic = (topic || '').trim();
|
topic = (topic || '').trim();
|
||||||
var note = msgEl('assistant');
|
var note = msgEl('assistant');
|
||||||
note.innerHTML = '<div class="asst-rich">Составляю тестовые вопросы…</div>';
|
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([
|
Promise.all([
|
||||||
LS.assistantQuestions(topic, 5),
|
LS.assistantQuestions(topic, 5),
|
||||||
(LS.getSubjects ? LS.getSubjects() : Promise.resolve([])).catch(function () { return []; }),
|
(LS.getSubjects ? LS.getSubjects() : Promise.resolve([])).catch(function () { return []; }),
|
||||||
]).then(function (res) {
|
]).then(function (res) {
|
||||||
var qs = (res[0] && res[0].questions) || [];
|
var qs = (res[0] && res[0].questions) || [];
|
||||||
var subjects = Array.isArray(res[1]) ? res[1] : ((res[1] && res[1].subjects) || []);
|
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();
|
note.remove();
|
||||||
var wrap = msgEl('assistant'); wrap.style.maxWidth = '100%';
|
var wrap = msgEl('assistant'); wrap.style.maxWidth = '100%';
|
||||||
var box = document.createElement('div'); box.className = 'asst-rich'; wrap.appendChild(box);
|
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;
|
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) {
|
}).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