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
+27
View File
@@ -1373,6 +1373,7 @@
</div>
<script src="/js/api.js"></script>
<script src="/js/imggen.js"></script>
<script src="/js/sound.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
@@ -2337,6 +2338,30 @@
_avDrag = false;
}
/* Загрузить изображение в шаг кадрирования (из URL — для ИИ-генерации) */
function avLoadFromUrl(src) {
const img = new Image();
img.onload = () => {
_avImg = img; _avZoom = 1; _avOffX = 0; _avOffY = 0;
document.getElementById('av-zoom').value = 100;
document.getElementById('av-s1').style.display = 'none';
document.getElementById('av-s2').style.display = 'flex';
avDraw(); avBindCanvas();
};
img.onerror = () => LS.toast('Не удалось загрузить картинку', 'error');
img.src = src;
}
/* Сгенерировать аватар через ИИ → кадрирование → отправка на проверку */
function avGenerate() {
if (!LS.imagePromptModal) { LS.toast('Модуль генерации не загружен'); return; }
LS.imagePromptModal({
title: 'Сгенерировать аватар',
placeholder: 'Аватар: «дружелюбный лис в наушниках, плоский стиль, по центру»',
useLabel: 'Кадрировать',
onUse: (url) => avLoadFromUrl(url),
});
}
/* ── Step 2: Canvas crop ── */
function avDraw() {
const c = document.getElementById('av-canvas');
@@ -2623,6 +2648,8 @@
<input type="file" id="av-file-inp" accept="image/png,image/jpeg,image/webp"
style="display:none" onchange="avFileChosen(this)">
</div>
<div class="av-or">или нарисуйте ИИ</div>
<button class="av-btn-send" type="button" onclick="avGenerate()" style="width:100%">Сгенерировать аватар (ИИ)</button>
</div>
<!-- Delete current avatar -->