Files
Learn_System/frontend/js/admin/sections/imggen.js
T
Maxim Dolgolyov 88651d85ab feat(admin): раздел «Генерация картинок» — управление провайдером и тест
Новый админ-раздел: Account ID / токен (маскируется) / модель Cloudflare,
лимиты (пауза, дневной лимит) из БД, статистика, кнопка теста генерации.
imggenController: лимиты и модель теперь из конфига, поддержка JSON и
бинарного ответа CF, переиспользуемые generateImage() и stats().
Бэкенд GET/PUT /api/admin/imggen + POST /api/admin/imggen/test (admin-only).

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

122 lines
8.3 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);
host.innerHTML =
'<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 ' + (cfg.enabled ? 'on' : 'off') + '">' + (cfg.enabled ? 'Включена' : 'Не настроена') + '</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-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 };
})();