ddc260e114
Главный выключатель в разделе «Генерация картинок» (флаг on в конфиге, независим от наличия токена). Выключено → /api/imggen отдаёт 503 «временно выключена». Админ-тест работает и при выключенном тумблере (generateImage проверяет только наличие конфига). Бейдж различает «Включена / Выключена / Не настроена». Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
9.6 KiB
JavaScript
137 lines
9.6 KiB
JavaScript
'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 ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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">Генерирую… (5–15 сек)</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 };
|
||
})();
|