Files
Learn_System/frontend/js/admin/sections/imggen.js
T
Maxim Dolgolyov ddc260e114 feat(admin): тумблер вкл/выкл генерации картинок
Главный выключатель в разделе «Генерация картинок» (флаг on в конфиге,
независим от наличия токена). Выключено → /api/imggen отдаёт 503
«временно выключена». Админ-тест работает и при выключенном тумблере
(generateImage проверяет только наличие конфига). Бейдж различает
«Включена / Выключена / Не настроена».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 12:00:05 +03:00

137 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* admin → «Генерация картинок»: провайдер Cloudflare (Account ID, токен, модель),
* лимиты (пауза, дневной лимит), статистика и тест-генерация. */
(function () {
'use strict';
let inited = false;
var esc = (window.LS && LS.escapeHtml) ? LS.escapeHtml : function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); };
var IN = 'padding:8px 11px;border:1px solid var(--border,#e2e8f0);border-radius:9px;font:inherit;font-size:.85rem;width:100%;box-sizing:border-box;background:var(--surface,#fff);color:var(--text,#0f172a)';
function fmtBytes(n) { if (!n) return '0'; if (n >= 1048576) return (n / 1048576).toFixed(1) + ' МБ'; if (n >= 1024) return Math.round(n / 1024) + ' КБ'; return n + ' Б'; }
function ensureStyle() {
if (document.getElementById('img-adm-style')) return;
var s = document.createElement('style'); s.id = 'img-adm-style';
s.textContent = [
'.img-card{border:1.5px solid var(--border,#e2e8f0);border-radius:14px;background:var(--surface,#fff);padding:16px 18px;margin-top:14px;}',
'.img-flabel{font-size:.76rem;font-weight:700;color:var(--text-2,#475569);margin:11px 0 4px;}',
'.img-row{display:flex;gap:12px;flex-wrap:wrap;}',
'.img-row > div{flex:1;min-width:140px;}',
'.img-bdg{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:3px 10px;border-radius:99px;}',
'.img-bdg.on{background:rgba(5,150,82,.13);color:#059652;}',
'.img-bdg.off{background:rgba(140,148,166,.16);color:#8a94a6;}',
'.img-btn{padding:9px 17px;border-radius:10px;border:none;cursor:pointer;font:700 .82rem Manrope,sans-serif;}',
'.img-btn.primary{background:#9B5DE5;color:#fff;}',
'.img-btn.primary:hover{background:#7e3eca;}',
'.img-btn.ghost{background:transparent;border:1.5px solid var(--border-h,#cbd5e1);color:var(--text-2,#475569);}',
'.img-btn:disabled{opacity:.55;cursor:not-allowed;}',
'.img-prev{margin-top:12px;border-radius:12px;overflow:hidden;border:1px solid var(--border,#e2e8f0);background:#0d0d1f;min-height:90px;display:flex;align-items:center;justify-content:center;}',
'.img-prev img{max-width:100%;display:block;}',
'.img-busy{color:#9aa5b4;font-size:.82rem;padding:24px;text-align:center;}',
'.img-hint{font-size:.72rem;color:#8a94a6;margin-top:5px;line-height:1.45;}',
].join('');
document.head.appendChild(s);
}
async function render() {
var host = document.getElementById('imggen-admin');
if (!host) return;
ensureStyle();
host.innerHTML = '<div style="color:var(--muted);font-size:.84rem">Загрузка…</div>';
var cfg = {};
try { cfg = await LS.api('/api/admin/imggen'); } catch (e) { host.innerHTML = '<div style="color:#e0335e">Не удалось загрузить настройки</div>'; return; }
var models = cfg.models || [];
var cdSec = Math.round((cfg.cooldownMs != null ? cfg.cooldownMs : 4000) / 1000);
var on = cfg.on !== false;
var badgeTxt = cfg.enabled ? 'Включена' : (cfg.configured ? 'Выключена' : 'Не настроена');
var badgeCls = cfg.enabled ? 'on' : 'off';
host.innerHTML =
'<div class="perm-card' + (on ? ' enabled' : '') + '">' +
'<div class="perm-info"><div class="perm-label">Генерация картинок включена</div>' +
'<div class="perm-desc">Главный выключатель. Выключено — генерация недоступна во всей системе (ассистент, флэшкарты, уроки, питомец, обложки, аватар, доска), даже если токен задан.</div></div>' +
'<label class="perm-toggle"><input type="checkbox" id="img-master" ' + (on ? 'checked' : '') + '><span class="perm-track"></span><span class="perm-thumb"></span></label>' +
'</div>' +
'<div class="img-card">' +
'<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">' +
'<div style="font-weight:800;font-size:.95rem">Cloudflare Workers AI</div>' +
'<span class="img-bdg ' + badgeCls + '">' + badgeTxt + '</span>' +
'</div>' +
'<div class="img-hint">Бесплатная генерация (FLUX.1 / SDXL). Токен и Account ID — из дашборда Cloudflare (Workers AI). Хранятся в БД, не в git.</div>' +
'<div class="img-flabel">Account ID</div>' +
'<input id="img-acc" style="' + IN + '" value="' + esc(cfg.accountId || '') + '" placeholder="напр. df9aedbb7626…">' +
'<div class="img-flabel">API-токен</div>' +
'<input id="img-token" type="password" style="' + IN + '" placeholder="' + (cfg.hasToken ? '•••••••• (сохранён — оставьте пустым, чтобы не менять)' : 'вставьте токен') + '">' +
(cfg.hasToken ? '<label style="font-size:.72rem;color:#8a94a6;display:inline-flex;align-items:center;gap:6px;margin-top:6px"><input type="checkbox" id="img-clear-token"> очистить токен (выключить)</label>' : '') +
'<div class="img-flabel">Модель</div>' +
'<select id="img-model" style="' + IN + '">' +
models.map(function (m) { return '<option value="' + esc(m.id) + '"' + (m.id === cfg.model ? ' selected' : '') + '>' + esc(m.label) + '</option>'; }).join('') +
'</select>' +
'<div class="img-row" style="margin-top:4px">' +
'<div><div class="img-flabel">Пауза между запросами (сек)</div><input id="img-cd" type="number" min="0" step="1" style="' + IN + '" value="' + cdSec + '"></div>' +
'<div><div class="img-flabel">Дневной лимит на пользователя</div><input id="img-cap" type="number" min="0" step="1" style="' + IN + '" value="' + (cfg.dailyCap != null ? cfg.dailyCap : 40) + '"></div>' +
'</div>' +
'<div class="img-hint">0 в дневном лимите — без ограничения.</div>' +
'<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap">' +
'<div style="font-size:.78rem;color:#8a94a6">Сгенерировано: <b style="color:var(--text-2,#475569)">' + (cfg.stats ? cfg.stats.count : 0) + '</b> картинок · ' + (cfg.stats ? fmtBytes(cfg.stats.bytes) : '0') + '</div>' +
'<button class="img-btn primary" id="img-save">Сохранить</button>' +
'</div>' +
'</div>' +
'<div class="img-card">' +
'<div style="font-weight:800;font-size:.92rem;margin-bottom:8px">Тест генерации</div>' +
'<input id="img-test-prompt" style="' + IN + '" placeholder="Опиши картинку (можно по-русски — переведётся автоматически)">' +
'<div style="margin-top:10px"><button class="img-btn ghost" id="img-test-btn">Сгенерировать тест</button></div>' +
'<div class="img-prev" id="img-test-prev" style="display:none"></div>' +
'</div>';
var Q = function (s) { return host.querySelector(s); };
Q('#img-master').addEventListener('change', function () {
var v = this.checked;
LS.api('/api/admin/imggen', { method: 'PUT', body: JSON.stringify({ on: v }) })
.then(function () { LS.toast(v ? 'Генерация включена' : 'Генерация выключена для всех', 'success'); render(); })
.catch(function () { LS.toast('Ошибка', 'error'); render(); });
});
Q('#img-save').addEventListener('click', async function () {
var btn = this; btn.disabled = true;
var body = {
provider: 'cloudflare',
accountId: Q('#img-acc').value.trim(),
model: Q('#img-model').value,
cooldownMs: Math.max(0, Number(Q('#img-cd').value) || 0) * 1000,
dailyCap: Math.max(0, Number(Q('#img-cap').value) || 0),
};
var clr = Q('#img-clear-token');
if (clr && clr.checked) body.clearToken = true;
else { var t = Q('#img-token').value.trim(); if (t) body.token = t; }
try { await LS.api('/api/admin/imggen', { method: 'PUT', body: JSON.stringify(body) }); LS.toast('Сохранено', 'success'); render(); }
catch (e) { LS.toast('Ошибка сохранения', 'error'); btn.disabled = false; }
});
Q('#img-test-btn').addEventListener('click', async function () {
var btn = this, prev = Q('#img-test-prev');
var prompt = Q('#img-test-prompt').value.trim();
btn.disabled = true; btn.textContent = 'Рисую…';
prev.style.display = 'flex'; prev.innerHTML = '<div class="img-busy">Генерирую… (515 сек)</div>';
try {
var r = await LS.api('/api/admin/imggen/test', { method: 'POST', body: JSON.stringify({ prompt: prompt }) });
if (r && r.url) prev.innerHTML = '<img src="' + r.url + '" alt="">';
else prev.innerHTML = '<div class="img-busy">Пустой ответ</div>';
} catch (e) {
var msg = (e && e.data && (e.data.error || e.data.detail)) || e.message || 'Ошибка';
prev.innerHTML = '<div class="img-busy">' + esc(msg) + '</div>';
} finally { btn.disabled = false; btn.textContent = 'Сгенерировать тест'; }
});
}
window.AdminSections = window.AdminSections || {};
window.AdminSections.imggen = { init: async () => { if (inited) return; inited = true; await render(); }, reload: render };
})();