feat(imggen): фон питомца, обложки курсов, аватары и доска через ИИ

Питомец: кастомный фон (миграция 068 pet_bg_custom, POST /api/pet/bg/custom,
  карточка «Свой фон (ИИ)» в гардеробной, применение картинкой).
Курсы: обложка-картинка (миграция 069 cover_image, генерация в модалке
  редактирования, рендер вместо эмодзи).
Аватар: кнопка «Сгенерировать (ИИ)» в загрузке → кадрирование → модерация.
Доска (classroom): кнопка-инструмент «Сгенерировать картинку (ИИ)».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-12 10:59:26 +03:00
parent d6faf6b22c
commit 6fcdafed50
9 changed files with 200 additions and 45 deletions
+31 -1
View File
@@ -449,6 +449,7 @@
<!-- Add section modal -->
<script src="/js/api.js"></script>
<script src="/js/imggen.js"></script>
<script src="/js/sidebar.js"></script>
<script>
if (!LS.requireAuth()) throw new Error();
@@ -548,7 +549,9 @@
document.getElementById('header-body').innerHTML = `
${!course.isPublished ? '<span class="ch-draft-tag">Черновик</span><br><br>' : ''}
<div class="ch-emoji">${course.coverEmoji || LS.icon('book-open',24)}</div>
${course.coverImage
? `<div class="ch-cover" style="width:100%;max-width:440px;height:160px;margin:0 auto 14px;border-radius:16px;background:center/cover url('${esc(course.coverImage)}');box-shadow:0 8px 24px rgba(0,0,0,.18)"></div>`
: `<div class="ch-emoji">${course.coverEmoji || LS.icon('book-open',24)}</div>`}
<div class="ch-subj ${SUBJ_CLASS[course.subjectSlug] || ''}">${esc(SUBJ_LABEL[course.subjectSlug] || course.subjectSlug)}</div>
<div class="ch-title">${esc(course.title)}</div>
${course.description ? `<div class="ch-desc">${esc(course.description)}</div>` : ''}
@@ -862,6 +865,15 @@
<option value="phys">Физика</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:12px">
<label class="form-label">Обложка (картинка)</label>
<div id="ec-cover-prev" style="${course.coverImage ? '' : 'display:none;'}height:120px;border-radius:12px;margin-bottom:8px;background:center/cover url('${LS.esc(course.coverImage || '')}')"></div>
<input type="hidden" id="ec-cover" value="${LS.esc(course.coverImage || '')}" />
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="button" class="btn-primary" id="ec-cover-gen" style="padding:8px 16px;min-height:auto;font-size:.82rem">Сгенерировать ИИ</button>
<button type="button" class="btn-ghost" id="ec-cover-clear" style="padding:8px 16px;min-height:auto;${course.coverImage ? '' : 'display:none'}">Убрать</button>
</div>
</div>`;
_editCourseModal = LS.modal({
title: 'Редактировать курс', content: body, size: 'sm',
@@ -871,6 +883,23 @@
],
});
_editCourseModal.body.querySelector('#ec-subject').value = course.subjectSlug || '';
const coverInput = _editCourseModal.body.querySelector('#ec-cover');
const coverPrev = _editCourseModal.body.querySelector('#ec-cover-prev');
const coverClear = _editCourseModal.body.querySelector('#ec-cover-clear');
_editCourseModal.body.querySelector('#ec-cover-gen').onclick = () => {
if (!LS.imagePromptModal) { LS.toast('Модуль генерации не загружен'); return; }
LS.imagePromptModal({
title: 'Обложка курса',
placeholder: 'Обложка для курса «' + (course.title || '') + '»: яркая иллюстрация',
useLabel: 'Поставить обложкой',
onUse: (url) => {
coverInput.value = url;
coverPrev.style.cssText = "height:120px;border-radius:12px;margin-bottom:8px;background:center/cover url('" + url + "')";
coverClear.style.display = '';
},
});
};
coverClear.onclick = () => { coverInput.value = ''; coverPrev.style.display = 'none'; coverClear.style.display = 'none'; };
}
async function doEditCourse() {
const btn = document.getElementById('btn-do-edit-course');
@@ -883,6 +912,7 @@
title: document.getElementById('ec-title').value.trim(),
description: document.getElementById('ec-desc').value.trim(),
coverEmoji: document.getElementById('ec-emoji').value.trim(),
coverImage: document.getElementById('ec-cover').value.trim(),
subjectSlug: document.getElementById('ec-subject').value || null,
}),
});