Перейти к содержанию

Настройки FLUX

Source file: tgmenu/settings/flux3.html

Live URL: https://tgmenu.pages.dev/settings/flux3.html

Code


<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Настройки FLUX</title>
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
  <style>
    :root {
      --bg: var(--tg-theme-bg-color, #0b0d10);
      --text: var(--tg-theme-text-color, #e6e6e6);
      --hint: var(--tg-theme-hint-color, #9aa3ab);
      --button: var(--tg-theme-button-color, #7c6cff);
      --button-text: var(--tg-theme-button-text-color, #fff);
      --sec-bg: var(--tg-theme-secondary-bg-color, #151821);
      --field-bg: #0f1216;
      --border-subtle: rgba(255,255,255,.12);
      --border-strong: rgba(255,255,255,.24);
    }

    html, body { height: 100%; }

    body {
      margin: 0;
      padding: 16px 16px 84px;
      font: 15px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial;
      background: var(--bg);
      color: var(--text);
    }

    h1 {
      margin: 0 0 16px;
      font-size: 20px;
    }

    form {
      display: block;
    }

    fieldset {
      border: 1px solid var(--border-subtle);
      border-radius: 16px;
      padding: 14px 14px 10px;
      margin: 0 0 12px;
      background: var(--sec-bg);
    }

    legend {
      padding: 0 6px;
      color: var(--hint);
      font-size: 12px;
    }

    label {
      display: block;
      margin: 10px 0 6px;
      font-size: 13px;
      font-weight: 600;
    }

    select,
    input[type="number"] {
      width: 100%;
      box-sizing: border-box;
      background: var(--field-bg);
      color: var(--text);
      border: 1px solid var(--border-subtle);
      border-radius: 999px;
      padding: 10px 14px;
      outline: none;
      font: inherit;
      appearance: none;
    }

    select:focus,
    input[type="number"]:focus {
      border-color: var(--border-strong);
    }

    .row {
      display: flex;
      gap: 10px;
      align-items: center;
    }

    .row-inline {
      margin-top: 4px;
    }

    .row-inline input[type="number"] {
      flex: 1;
    }

    .switch {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 13px;
      color: var(--hint);
      cursor: pointer;
      user-select: none;
      padding: 4px 8px;
      border-radius: 999px;
      border: 1px solid var(--border-subtle);
      background: rgba(0,0,0,.12);
      white-space: nowrap;
    }

    .switch input {
      accent-color: var(--button);
    }

    .ref-row {
      margin-top: 4px;
      display: flex;
      gap: 10px;
      align-items: center;
    }

    .ref-col {
      flex: 1;
    }

    input[type="range"] {
      width: 100%;
    }

    .ref-value {
      margin-top: 4px;
      font-size: 12px;
      color: var(--hint);
    }

    .ref-disabled {
      opacity: 0.4;
    }

    .bar {
      position: fixed;
      inset: auto 0 0 0;
      padding: 12px 16px 18px;
      background: linear-gradient(
        180deg,
        rgba(0,0,0,0) 0,
        rgba(0,0,0,.25) 22%,
        var(--bg) 50%
      );
      display: grid;
      gap: 10px;
    }

    .btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 12px 14px;
      border-radius: 12px;
      border: 1px solid var(--border-subtle);
      background: var(--sec-bg);
      color: var(--text);
      font-weight: 600;
      font-size: 15px;
      cursor: pointer;
    }

    .btn.primary {
      background: var(--button);
      color: var(--button-text);
      border-color: transparent;
    }

    .btn.ghost {
      background: transparent;
    }

    #status {
      font-size: 12px;
      color: var(--hint);
      min-height: 16px;
    }
  </style>
</head>
<body>
  <h1>Настройки FLUX</h1>

  <form id="form" autocomplete="off">
    <fieldset>
      <legend>Основные параметры</legend>

      <!-- Модель -->
      <label for="model">Модель</label>
      <select id="model">
        <option value="flux-krea" selected>Flux-Krea</option>
      </select>

      <!-- Качество -->
      <label for="qualityMode">Качество</label>
      <select id="qualityMode">
        <!-- Light временно скрыт -->
        <option value="standard" selected>Standard</option>
        <option value="pro">Pro</option>
      </select>

      <!-- AR -->
      <label for="ar">Соотношение сторон</label>
      <select id="ar">
        <!-- сверху вертикальные -->
        <option value="3:4">3:4 · вертикальное</option>
        <option value="2:3">2:3 · вертикальное</option>
        <option value="9:16">9:16 · вертикальное</option>
        <!-- центр -->
        <option value="1:1" selected>1:1 · квадрат</option>
        <!-- снизу горизонтальные -->
        <option value="4:3">4:3 · горизонтальное</option>
        <option value="3:2">3:2 · горизонтальное</option>
        <option value="16:9">16:9 · горизонтальное</option>
        <option value="21:9">21:9 · горизонтальное</option>
      </select>

      <!-- Batch / Генераций за раз -->
      <label for="batch">Генераций за раз</label>
      <select id="batch">
        <option value="1" selected>1</option>
        <!-- заготовка на будущее:
        <option value="2">2</option>
        <option value="4">4</option>
        -->
      </select>

      <!-- Seed -->
      <label for="seed">Seed</label>
      <div class="row row-inline">
        <input id="seed" type="number" min="0" max="999999" inputmode="numeric" />
        <label class="switch" for="random">
          <input id="random" type="checkbox" />
          <span>random</span>
        </label>
      </div>

      <!-- REF -->
      <label>Сила референса</label>
      <div class="ref-row">
        <div class="ref-col" id="refBlock">
          <input id="ref" type="range" min="0" max="120" step="1" />
          <div class="ref-value">
            Сила: <span id="refValue">75</span>%
          </div>
        </div>
        <label class="switch" for="useRef">
          <input id="useRef" type="checkbox" />
          <span>Использовать реф</span>
        </label>
      </div>
    </fieldset>
  </form>

  <div class="bar">
    <button type="button" id="reset" class="btn ghost">Сбросить</button>
    <button type="button" id="save" class="btn primary">Сохранить</button>
    <div id="status" aria-live="polite"></div>
  </div>

  <script>
    // --- Telegram / CloudStorage setup ---
    const tg = window.Telegram?.WebApp;
    if (tg) {
      tg.ready();
      tg.expand();
      if (tg.MainButton) {
        tg.MainButton.hide();
        tg.MainButton.offClick?.(() => {});
      }
    }
    const CS = tg?.CloudStorage || null;
    const STORAGE_KEY = 'settings:flux:v1';

    const DEFAULTS = {
      model: 'flux-krea',
      // quality: 0=light, 1=standard, 2=pro
      quality: 1,
      qualityMode: 'standard',
      ar: '1:1',
      batch: 1,
      ref: 0.75,     // 75%
      seed: 0,       // дефолт 0
      random: false,
      useRef: false,
    };

    const REF_MAX_PCT = 120;
    const REF_DEFAULT_PCT = Math.round(DEFAULTS.ref * 100);

    const $ = (s) => document.querySelector(s);

    const $model       = $('#model');
    const $qualityMode = $('#qualityMode');
    const $ar          = $('#ar');
    const $batch       = $('#batch');
    const $seed        = $('#seed');
    const $random      = $('#random');
    const $useRef      = $('#useRef');
    const $ref         = $('#ref');
    const $refValue    = $('#refValue');
    const $refBlock    = $('#refBlock');
    const $save        = $('#save');
    const $reset       = $('#reset');
    const $status      = $('#status');

    const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
    const parseNum = (v, def) => {
      const n = Number(v);
      return Number.isFinite(n) ? n : def;
    };

    function qualityToMode(q) {
      if (q === 0) return 'light';
      if (q === 2) return 'pro';
      return 'standard';
    }

    function modeToQuality(mode) {
      if (mode === 'light') return 0;
      if (mode === 'pro') return 2;
      return 1;
    }

    function parseCfg(raw) {
      try {
        const cfg = JSON.parse(raw);

        // нормализуем quality
        let qNum = Number(cfg.quality);
        if (!Number.isFinite(qNum)) {
          if (cfg.qualityMode && ['light','standard','pro'].includes(cfg.qualityMode)) {
            qNum = modeToQuality(cfg.qualityMode);
          } else {
            qNum = DEFAULTS.quality;
          }
        }
        const qualityMode = cfg.qualityMode || qualityToMode(qNum);

        const ar = /^\d+:\d+$/.test(String(cfg.ar || ''))
          ? String(cfg.ar)
          : DEFAULTS.ar;

        let refNum = parseNum(cfg.ref, DEFAULTS.ref);
        refNum = clamp(refNum, 0, 1.2);

        let seed = parseInt(cfg.seed, 10);
        if (!Number.isFinite(seed) || seed < 0) seed = DEFAULTS.seed;
        if (seed > 999999) seed = seed % 1000000;

        const random = !!cfg.random;
        const useRef = !!cfg.useRef;

        let batch = Number(cfg.batch);
        if (!Number.isFinite(batch) || ![1,2,4].includes(batch)) {
          batch = DEFAULTS.batch;
        }

        return {
          ...DEFAULTS,
          ...cfg,
          quality: qNum,
          qualityMode,
          ar,
          ref: refNum,
          seed,
          random,
          useRef,
          batch,
        };
      } catch {
        return { ...DEFAULTS };
      }
    }

    function loadConfig() {
      return new Promise((resolve) => {
        // нет CloudStorage → только localStorage
        if (!CS) {
          const raw = localStorage.getItem(STORAGE_KEY);
          return resolve(raw ? parseCfg(raw) : { ...DEFAULTS });
        }

        CS.getItem(STORAGE_KEY, (err, value) => {
          if (!err && value) {
            try { localStorage.setItem(STORAGE_KEY, value); } catch {}
            return resolve(parseCfg(value));
          }

          // нет в облаке → пробуем localStorage
          const raw = localStorage.getItem(STORAGE_KEY);
          if (raw) {
            const cfg = parseCfg(raw);
            const clean = JSON.stringify(cfg);
            CS.setItem(STORAGE_KEY, clean, () => resolve(cfg));
          } else {
            resolve({ ...DEFAULTS });
          }
        });
      });
    }

    function saveConfig(cfg) {
      const merged = {
        ...DEFAULTS,
        ...cfg,
      };
      const clean = JSON.stringify(merged);

      try { localStorage.setItem(STORAGE_KEY, clean); } catch {}

      if (CS) {
        CS.setItem(STORAGE_KEY, clean, (err) => {
          if (err) console.warn('CloudStorage setItem error', err);
        });
      }
    }

    function applyRandom() {
      if ($random.checked) {
        $seed.disabled = true;
        $seed.value = '';
      } else {
        $seed.disabled = false;
        if ($seed.value === '') {
          $seed.value = String(DEFAULTS.seed);
        }
      }
    }

    function applyUseRef() {
      const on = !!$useRef.checked;
      $ref.disabled = !on;
      $refBlock.classList.toggle('ref-disabled', !on);
      if (!on) {
        $refValue.textContent = '0';
      } else {
        const v = clamp(parseNum($ref.value, REF_DEFAULT_PCT), 0, REF_MAX_PCT);
        $refValue.textContent = String(v);
      }
    }

    function gather() {
      const model = $model.value || DEFAULTS.model;

      const qmRaw = $qualityMode.value || 'standard';
      const qualityMode = ['light','standard','pro'].includes(qmRaw)
        ? qmRaw
        : 'standard';
      const quality = modeToQuality(qualityMode);

      const arIn = String($ar.value || '').replace(/\s+/g, '');
      const ar = /^\d+:\d+$/.test(arIn) ? arIn : DEFAULTS.ar;

      const batchRaw = parseInt($batch.value, 10);
      const batch = [1,2,4].includes(batchRaw) ? batchRaw : 1;

      const useRef = !!$useRef.checked;
      let refPct = clamp(parseNum($ref.value, REF_DEFAULT_PCT), 0, REF_MAX_PCT);
      const ref = useRef ? refPct / 100 : 0;

      const random = !!$random.checked;
      let seed = 0;
      if (!random) {
        const cleaned = String($seed.value || '').replace(/[^\d]/g, '');
        seed = parseInt(cleaned || '0', 10);
        if (!Number.isFinite(seed) || seed < 0) seed = DEFAULTS.seed;
        if (seed > 999999) seed = seed % 1000000;
      }

      return {
        model,
        qualityMode, // удобно для фронта
        quality,     // главное числовое поле 0/1/2
        ar,
        batch,
        ref,
        seed,
        random,
        useRef,
      };
    }

    async function saveAll(cfg) {
      saveConfig(cfg);
      tg?.sendData?.(JSON.stringify({
        type: 'flux_settings',
        ...cfg,
      }));
      if (tg?.HapticFeedback) {
        tg.HapticFeedback.impactOccurred('medium');
      }
    }

    // --- listeners ---
    $random.addEventListener('change', applyRandom);
    $useRef.addEventListener('change', applyUseRef);
    $ref.addEventListener('input', () => {
      const v = clamp(parseNum($ref.value, REF_DEFAULT_PCT), 0, REF_MAX_PCT);
      $refValue.textContent = String(v);
    });

    $save.addEventListener('click', async () => {
      const cfg = gather();
      $status.textContent = 'Сохранение…';
      try {
        await saveAll(cfg);
        $status.textContent = 'Сохранено';
      } catch (e) {
        console.error(e);
        $status.textContent = 'Ошибка сохранения';
      }
    });

    $reset.addEventListener('click', async () => {
      const defQM = qualityToMode(DEFAULTS.quality);

      $model.value = DEFAULTS.model;
      $qualityMode.value = defQM;
      $ar.value = DEFAULTS.ar;
      $batch.value = String(DEFAULTS.batch);

      const refPct = REF_DEFAULT_PCT;
      $ref.value = refPct;
      $useRef.checked = DEFAULTS.useRef;
      $refValue.textContent = DEFAULTS.useRef ? String(refPct) : '0';

      $random.checked = DEFAULTS.random;
      $seed.value = String(DEFAULTS.seed);
      $seed.disabled = false;

      applyRandom();
      applyUseRef();

      const cfg = {
        ...DEFAULTS,
        qualityMode: defQM,
      };

      $status.textContent = 'Сброс…';
      try {
        await saveAll(cfg);
        $status.textContent = 'Сброшено к дефолтам';
      } catch (e) {
        console.error(e);
        $status.textContent = 'Ошибка сброса';
      }
    });

    document.getElementById('form')
      .addEventListener('submit', (e) => e.preventDefault());

    // --- init ---
    (async () => {
      const cfg = await loadConfig();

      $model.value = cfg.model || DEFAULTS.model;
      const qm = cfg.qualityMode || qualityToMode(cfg.quality);
      $qualityMode.value = ['light','standard','pro'].includes(qm)
        ? qm
        : 'standard';

      $ar.value = cfg.ar || DEFAULTS.ar;
      $batch.value = String(cfg.batch ?? DEFAULTS.batch);

      const refPct = clamp(Math.round(cfg.ref * 100), 0, REF_MAX_PCT);
      $ref.value = refPct;
      $useRef.checked = !!cfg.useRef;
      $refValue.textContent = cfg.useRef ? String(refPct) : '0';

      const random = !!cfg.random || cfg.seed === 0;
      $random.checked = random;
      if (!random) {
        $seed.value = String(cfg.seed ?? DEFAULTS.seed);
      } else {
        $seed.value = '';
      }

      applyRandom();
      applyUseRef();
    })();
  </script>
</body>
</html>