feat(imggen): генерация картинок ИИ (FLUX.1) — ассистент, флэшкарты, редактор уроков
Бэкенд /api/imggen (status/generate, CF Workers AI, cooldown+дневной лимит). Переиспользуемый модал LS.imagePromptModal (js/imggen.js). Квантик: режим «Нарисовать» в чате (inline). Флэшкарты: кнопка «ИИ» в блоке картинки карточки. Редактор уроков: кнопка «Сгенерировать» в блоке изображения. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -490,7 +490,8 @@
|
||||
chatEl.innerHTML = '';
|
||||
_chat.forEach(function (m) {
|
||||
var d = msgEl(m.role);
|
||||
if (m.role === 'assistant') { d.innerHTML = '<div class="asst-rich"></div>'; renderRich(d.querySelector('.asst-rich'), m.content); }
|
||||
if (m.img) d.innerHTML = '<img src="' + m.img + '" alt="" style="width:100%;border-radius:10px;display:block">';
|
||||
else if (m.role === 'assistant') { d.innerHTML = '<div class="asst-rich"></div>'; renderRich(d.querySelector('.asst-rich'), m.content); }
|
||||
else d.textContent = m.content;
|
||||
chatEl.appendChild(d);
|
||||
});
|
||||
@@ -498,7 +499,7 @@
|
||||
}
|
||||
var FB_UP = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v11"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>';
|
||||
var FB_DOWN = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:rotate(180deg)"><path d="M7 10v11"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>';
|
||||
var MODE_PH = { answer: 'Спроси что угодно: «объясни…», «как…»', hint: 'Задача, по которой нужна подсказка…', check: 'Вставь своё решение — проверю…' };
|
||||
var MODE_PH = { answer: 'Спроси что угодно: «объясни…», «как…»', hint: 'Задача, по которой нужна подсказка…', check: 'Вставь своё решение — проверю…', draw: 'Опиши картинку: «кот-учёный, плоская иллюстрация»' };
|
||||
function openAsk(prefill) {
|
||||
var sel = _lastSel, pc = getPageContext();
|
||||
var ctxBtns = '';
|
||||
@@ -512,7 +513,8 @@
|
||||
var modes = '<div class="asst-modes">' +
|
||||
'<button class="asst-mode on" data-m="answer">Ответ</button>' +
|
||||
'<button class="asst-mode" data-m="hint">Подсказка</button>' +
|
||||
'<button class="asst-mode" data-m="check">Проверить решение</button></div>';
|
||||
'<button class="asst-mode" data-m="check">Проверить решение</button>' +
|
||||
'<button class="asst-mode" data-m="draw">Нарисовать</button></div>';
|
||||
openBubble(
|
||||
'<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>' +
|
||||
@@ -580,9 +582,29 @@
|
||||
});
|
||||
}
|
||||
function srcUrl(s) { return '/textbook/' + s.slug + (s.ref ? '#sec-' + s.ref : ''); }
|
||||
function drawInChat(prompt, chatEl) {
|
||||
prompt = (prompt || '').trim();
|
||||
if (prompt.length < 3) return;
|
||||
_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;
|
||||
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 = 'Не получилось нарисовать.';
|
||||
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
||||
}).catch(function (err) {
|
||||
ph.remove(); var d = msgEl('assistant');
|
||||
d.textContent = (err && err.data && err.data.error) || 'Не получилось нарисовать.';
|
||||
chatEl.appendChild(d); chatEl.scrollTop = chatEl.scrollHeight;
|
||||
});
|
||||
}
|
||||
function send(q, context, chatEl, mode) {
|
||||
q = (q || '').trim();
|
||||
if (q.length < 2) return;
|
||||
if (mode === 'draw') return drawInChat(q, chatEl);
|
||||
var history = _chat.slice(-6);
|
||||
_chat.push({ role: 'user', content: q });
|
||||
var u = msgEl('user'); u.textContent = q; chatEl.appendChild(u);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
/* Переиспользуемый модал генерации картинок. LS.imagePromptModal({title, placeholder, onUse}).
|
||||
* Зависит от LS.imageGen (api.js) и LS.toast. Подключать на страницах с кнопкой генерации. */
|
||||
(function () {
|
||||
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return ({'&':'&','<':'<','>':'>','"':'"'})[c];}); }
|
||||
function ensureStyle(){
|
||||
if (document.getElementById('imggen-style')) return;
|
||||
var s=document.createElement('style'); s.id='imggen-style';
|
||||
s.textContent=[
|
||||
'.ig-ov{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.5);backdrop-filter:blur(6px);padding:20px}',
|
||||
'.ig-box{background:var(--surface,#fff);border:1.5px solid var(--border,#e2e8f0);border-radius:20px;width:440px;max-width:96vw;max-height:92vh;overflow:auto;padding:20px 22px;box-shadow:0 24px 70px rgba(0,0,0,.3)}',
|
||||
'.ig-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}',
|
||||
".ig-title{font-family:'Unbounded',sans-serif;font-weight:800;font-size:.98rem}",
|
||||
'.ig-x{border:none;background:none;font-size:1.4rem;line-height:1;cursor:pointer;color:var(--text-2,#64748b)}',
|
||||
'.ig-ta{width:100%;box-sizing:border-box;min-height:64px;padding:10px 12px;border:1.5px solid var(--border,#e2e8f0);border-radius:11px;font:inherit;font-size:.86rem;resize:vertical;background:var(--surface,#fff);color:var(--text,#0f172a)}',
|
||||
'.ig-hint{font-size:.7rem;color:var(--text-3,#94a3b8);margin:6px 0 10px;line-height:1.45}',
|
||||
".ig-btn{padding:9px 16px;border-radius:10px;border:none;cursor:pointer;font:700 .82rem 'Manrope',sans-serif}",
|
||||
'.ig-btn.primary{background:var(--violet,#9B5DE5);color:#fff}',
|
||||
'.ig-btn.ghost{background:transparent;border:1.5px solid var(--border-h,#cbd5e1);color:var(--text-2,#475569)}',
|
||||
'.ig-btn:disabled{opacity:.55;cursor:not-allowed}',
|
||||
'.ig-preview{margin-top:14px;border-radius:14px;overflow:hidden;border:1.5px solid var(--border,#e2e8f0);background:#0d0d1f;min-height:120px;display:flex;align-items:center;justify-content:center}',
|
||||
'.ig-preview img{width:100%;display:block}',
|
||||
'.ig-busy{color:#9aa5b4;font-size:.84rem;padding:28px;text-align:center}',
|
||||
'.ig-actions{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}',
|
||||
].join('');
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
window.LS = window.LS || {};
|
||||
LS.imagePromptModal = function (opts) {
|
||||
opts = opts || {}; ensureStyle();
|
||||
var ov = document.createElement('div'); ov.className = 'ig-ov';
|
||||
ov.innerHTML = '<div class="ig-box">'
|
||||
+ '<div class="ig-head"><span class="ig-title">' + esc(opts.title || 'Сгенерировать картинку') + '</span><button class="ig-x" data-x>×</button></div>'
|
||||
+ '<textarea class="ig-ta" placeholder="' + esc(opts.placeholder || 'Опиши картинку: «кот-учёный в очках, плоская иллюстрация»') + '"></textarea>'
|
||||
+ '<div class="ig-hint">ИИ-картинка для иллюстраций и декора (не для точных схем — графиков, формул). FLUX.1 · бесплатно.</div>'
|
||||
+ '<div class="ig-actions"><button class="ig-btn primary" data-gen>Сгенерировать</button></div>'
|
||||
+ '<div class="ig-preview" data-prev style="display:none"></div>'
|
||||
+ '<div class="ig-actions" data-userow style="display:none"><button class="ig-btn primary" data-use>' + esc(opts.useLabel || 'Использовать') + '</button><button class="ig-btn ghost" data-again>Ещё вариант</button></div>'
|
||||
+ '</div>';
|
||||
document.body.appendChild(ov);
|
||||
var ta = ov.querySelector('.ig-ta'), prev = ov.querySelector('[data-prev]'), useRow = ov.querySelector('[data-userow]'), genBtn = ov.querySelector('[data-gen]');
|
||||
var lastUrl = null;
|
||||
if (opts.prompt) ta.value = opts.prompt;
|
||||
function close(){ ov.remove(); }
|
||||
ov.addEventListener('click', function (e) { if (e.target === ov || e.target.hasAttribute('data-x')) close(); });
|
||||
async function gen(){
|
||||
var prompt = ta.value.trim();
|
||||
if (prompt.length < 3) { LS.toast && LS.toast('Опиши, что нарисовать', 'warn'); return; }
|
||||
genBtn.disabled = true; genBtn.textContent = 'Рисую…'; useRow.style.display = 'none';
|
||||
prev.style.display = 'flex'; prev.innerHTML = '<div class="ig-busy">Генерирую картинку… (5–15 сек)</div>';
|
||||
try {
|
||||
var r = await LS.imageGen(prompt);
|
||||
if (r && r.url) { lastUrl = r.url; prev.innerHTML = '<img src="' + r.url + '" alt="">'; useRow.style.display = 'flex'; genBtn.textContent = 'Перегенерировать'; }
|
||||
else prev.innerHTML = '<div class="ig-busy">Не получилось</div>';
|
||||
} catch (e) { prev.innerHTML = '<div class="ig-busy">' + esc((e && e.data && e.data.error) || e.message || 'Ошибка') + '</div>'; }
|
||||
finally { genBtn.disabled = false; if (genBtn.textContent === 'Рисую…') genBtn.textContent = 'Сгенерировать'; }
|
||||
}
|
||||
genBtn.onclick = gen;
|
||||
ov.querySelector('[data-again]').onclick = gen;
|
||||
ov.querySelector('[data-use]').onclick = function () { if (lastUrl && opts.onUse) opts.onUse(lastUrl); close(); };
|
||||
setTimeout(function () { ta.focus(); }, 50);
|
||||
return ov;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user