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

Seedream settings

Source file: tgmenu/settings/seedream.html

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

Code


<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8" />
  <title>Seedream settings</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <!-- Telegram WebApp SDK ОБЯЗАТЕЛЕН для CloudStorage + sendData -->
  <script src="https://telegram.org/js/telegram-web-app.js"></script>

  <style>
    :root {
      color-scheme: dark;
      --bg-gradient-start: #050712;
      --bg-gradient-end: #111827;
      --card-bg: rgba(15, 23, 42, 0.96);
      --card-border: rgba(148, 163, 184, 0.35);
      --accent: #8b5cf6;
      --accent-soft: rgba(139, 92, 246, 0.16);
      --accent-strong: rgba(139, 92, 246, 0.55);
      --accent-text: #e5e7eb;
      --text-main: #e5e7eb;
      --text-muted: #9ca3af;
      --chip-bg: rgba(15, 23, 42, 0.9);
      --chip-border: rgba(148, 163, 184, 0.4);
      --chip-bg-active: rgba(139, 92, 246, 0.22);
      --chip-border-active: rgba(167, 139, 250, 0.95);
      --danger: #f97373;
    }

    * {
      box-sizing: border-box;
    }

    html, body {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
        "Roboto", "Segoe UI", sans-serif;
      background: radial-gradient(circle at top, var(--bg-gradient-start), #020617 45%, var(--bg-gradient-end) 100%);
      color: var(--text-main);
    }

    body {
      display: flex;
      align-items: stretch;
      justify-content: center;
    }

    .app {
      width: 100%;
      max-width: 480px;
      margin: 0 auto;
      padding: 16px 12px 24px;
      display: flex;
      align-items: stretch;
      justify-content: center;
    }

    .card {
      width: 100%;
      border-radius: 20px;
      padding: 16px 16px 14px;
      background: linear-gradient(
        145deg,
        rgba(15, 23, 42, 0.98),
        rgba(15, 23, 42, 0.96)
      );
      border: 1px solid var(--card-border);
      box-shadow:
        0 10px 40px rgba(15, 23, 42, 0.9),
        0 0 0 1px rgba(15, 23, 42, 0.9);
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    .card-header {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-bottom: 2px;
    }

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

    .icon-pill {
      width: 32px;
      height: 32px;
      border-radius: 999px;
      display: flex;
      align-items: center;
      justify-content: center;
      background: radial-gradient(circle at 30% 0%, #a855f7, #4f46e5 65%, #1e293b 100%);
      box-shadow:
        0 0 0 1px rgba(129, 140, 248, 0.55),
        0 8px 24px rgba(88, 28, 135, 0.65);
      font-size: 16px;
    }

    .card-title {
      font-size: 18px;
      font-weight: 600;
      letter-spacing: 0.01em;
    }

    .card-subtitle {
      font-size: 13px;
      color: var(--text-muted);
      margin-top: 1px;
    }

    .card-body {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-top: 4px;
    }

    .block {
      padding: 10px 10px 9px;
      border-radius: 14px;
      background: radial-gradient(circle at top left, rgba(15,23,42,1), rgba(15,23,42,0.95));
      border: 1px solid rgba(148, 163, 184, 0.35);
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .block-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 6px;
      margin-bottom: 2px;
    }

    .block-title {
      font-size: 13px;
      font-weight: 500;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: #9ca3af;
    }

    .block-caption {
      font-size: 12px;
      color: var(--text-muted);
      opacity: 0.95;
    }

    .model-row {
      display: grid;
      grid-template-columns: repeat(2, minmax(0,1fr));
      gap: 8px;
    }

    .chip {
      font-size: 13px;
      padding: 8px 10px;
      border-radius: 999px;
      border: 1px solid var(--chip-border);
      background: var(--chip-bg);
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      cursor: pointer;
      white-space: nowrap;
      transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
        transform 0.08s ease;
      user-select: none;
    }

    .chip span.sub {
      font-size: 11px;
      color: var(--text-muted);
    }

    .chip.active {
      border-color: var(--chip-border-active);
      background: radial-gradient(circle at top left, rgba(79,70,229,0.42), rgba(15,23,42,0.95));
      color: var(--accent-text);
      box-shadow:
        0 0 0 1px rgba(129, 140, 248, 0.7),
        0 0 25px rgba(79, 70, 229, 0.6);
    }

    .chip.active span.sub {
      color: rgba(209, 213, 219, 0.85);
    }

    .chip:hover {
      transform: translateY(-0.5px);
    }

    .segment-row {
      display: inline-flex;
      border-radius: 999px;
      padding: 3px;
      background: rgba(15,23,42,0.9);
      border: 1px solid rgba(148, 163, 184, 0.5);
    }

    .segment-btn {
      border-radius: 999px;
      padding: 5px 11px 6px;
      font-size: 13px;
      border: none;
      outline: none;
      background: transparent;
      color: var(--text-muted);
      cursor: pointer;
      min-width: 44px;
      text-align: center;
      transition: background 0.15s ease, color 0.15s ease, transform 0.08s ease;
    }

    .segment-btn.active {
      background: linear-gradient(135deg, rgba(129, 140, 248, 0.95), rgba(236, 72, 153, 0.9));
      color: #f9fafb;
      box-shadow:
        0 0 0 1px rgba(129, 140, 248, 0.8),
        0 0 18px rgba(129, 140, 248, 0.9);
      transform: translateY(-0.5px);
    }

    .segment-btn:not(.active):hover {
      background: rgba(148, 163, 184, 0.16);
      color: #e5e7eb;
    }

    .ratio-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0,1fr));
      gap: 6px;
    }

    .ratio-btn {
      font-size: 13px;
      padding: 7px 4px;
      border-radius: 999px;
      border: 1px solid var(--chip-border);
      background: var(--chip-bg);
      color: var(--text-main);
      cursor: pointer;
      text-align: center;
      transition: background 0.15s ease, border-color 0.15s ease,
        transform 0.08s ease, color 0.15s ease;
    }

    .ratio-btn span.sub {
      display: block;
      margin-top: 1px;
      font-size: 11px;
      color: var(--text-muted);
    }

    .ratio-btn.active {
      border-color: var(--chip-border-active);
      background: radial-gradient(circle at top left, rgba(79,70,229,0.45), rgba(15,23,42,0.96));
      color: var(--accent-text);
      box-shadow:
        0 0 0 1px rgba(129, 140, 248, 0.65),
        0 0 20px rgba(79, 70, 229, 0.7);
    }

    .ratio-btn.active span.sub {
      color: rgba(209, 213, 219, 0.9);
    }

    .ratio-btn:hover {
      transform: translateY(-0.5px);
    }

    .dims-row {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
      gap: 6px;
      margin-top: 2px;
    }

    .dims-main {
      font-size: 13px;
      color: var(--text-muted);
    }

    .dims-main strong {
      color: #e5e7eb;
      font-weight: 600;
    }

    .dims-extra {
      font-size: 11px;
      color: var(--text-muted);
      opacity: 0.9;
    }

    .card-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 8px;
      margin-top: 4px;
    }

    .btn {
      border-radius: 999px;
      padding: 9px 14px 10px;
      border: none;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      flex: 1 1 0;
      transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease,
        transform 0.08s ease, border-color 0.15s ease;
      user-select: none;
      white-space: nowrap;
    }

    .btn-outline {
      background: transparent;
      color: #e5e7eb;
      border: 1px solid rgba(148, 163, 184, 0.65);
    }

    .btn-primary {
      background: linear-gradient(135deg, #8b5cf6, #ec4899);
      color: #f9fafb;
      border: 1px solid rgba(167, 139, 250, 0.9);
      box-shadow:
        0 10px 25px rgba(88, 28, 135, 0.7),
        0 0 0 1px rgba(129, 140, 248, 0.7);
    }

    .btn-outline:hover {
      background: rgba(30, 64, 175, 0.3);
      border-color: rgba(129, 140, 248, 0.9);
    }

    .btn-primary:hover {
      box-shadow:
        0 14px 30px rgba(88, 28, 135, 0.9),
        0 0 0 1px rgba(196, 181, 253, 0.95);
      transform: translateY(-1px);
    }

    .btn:active {
      transform: translateY(0px) scale(0.99);
      box-shadow: none;
    }

    .status-row {
      margin-top: 4px;
      font-size: 11px;
      color: var(--text-muted);
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 4px;
    }

    .status-pill {
      padding: 3px 8px;
      border-radius: 999px;
      border: 1px solid rgba(148, 163, 184, 0.5);
      font-size: 10px;
      text-transform: uppercase;
      letter-spacing: 0.09em;
      color: #9ca3af;
    }

    .status-pill.ok {
      border-color: rgba(74, 222, 128, 0.8);
      color: #bbf7d0;
      background: rgba(22, 163, 74, 0.16);
    }

    .status-pill.err {
      border-color: rgba(248, 113, 113, 0.85);
      color: #fecaca;
      background: rgba(248, 113, 113, 0.18);
    }
  </style>
</head>
<body>
  <div class="app">
    <div class="card">
      <div class="card-header">
        <div class="title-row">
          <div class="icon-pill">✨</div>
          <div>
            <div class="card-title">Настройки Seedream</div>
            <!-- Подзаголовок убран по запросу -->
            <div class="card-subtitle"></div>
          </div>
        </div>
      </div>

      <div class="card-body">
        <!-- MODEL -->
        <div class="block">
          <div class="block-header">
            <div class="block-title">Модель</div>
          </div>
          <div class="model-row">
            <button class="chip" data-model="4.5">
              <span>Seedream 4.5</span>
            </button>
            <button class="chip" data-model="4.0">
              <span>Seedream 4.0</span>
            </button>
          </div>
        </div>

        <!-- QUALITY -->
        <div class="block">
          <div class="block-header">
            <div class="block-title">Качество / разрешение</div>
          </div>
          <div class="segment-row" id="qualityRow">
            <button class="segment-btn" data-quality="1k">1K</button>
            <button class="segment-btn" data-quality="2k">2K</button>
            <button class="segment-btn" data-quality="4k">4K</button>
          </div>
        </div>

        <!-- RATIO -->
        <div class="block">
          <div class="block-header">
            <div class="block-title">Соотношение сторон</div>
          </div>

          <div class="ratio-grid" id="ratioGrid">
            <button class="ratio-btn" data-ratio="4:3">
              <span>4:3</span>
            </button>
            <button class="ratio-btn" data-ratio="3:4">
              <span>3:4</span>
            </button>
            <button class="ratio-btn" data-ratio="9:16">
              <span>9:16</span>
            </button>
            <button class="ratio-btn" data-ratio="3:2">
              <span>3:2</span>
            </button>
            <button class="ratio-btn" data-ratio="1:1">
              <span>1:1</span>
            </button>
            <button class="ratio-btn" data-ratio="16:9">
              <span>16:9</span>
            </button>
            <button class="ratio-btn" data-ratio="2:3">
              <span>2:3</span>
            </button>
            <button class="ratio-btn" data-ratio="21:9">
              <span>21:9</span>
            </button>
          </div>

          <div class="dims-row">
            <div class="dims-main" id="dimsMain">
              Размер: <strong>—</strong>
            </div>
            <div class="dims-extra" id="dimsExtra">
              Long edge (i2i): —
            </div>
          </div>
        </div>
      </div>

      <div class="card-footer">
        <button class="btn btn-outline" id="resetBtn">Сбросить</button>
        <button class="btn btn-primary" id="saveBtn">Сохранить</button>
      </div>

      <div class="status-row">
        <div id="statusText">Локальные настройки</div>
        <div id="statusPill" class="status-pill">offline</div>
      </div>
    </div>
  </div>


  <script>
    (function () {
  // Инициализация как во FLUX-настройках
  const tg = window.Telegram?.WebApp || null;

  if (tg) {
    // Сообщаем Телеграму, что WebApp полностью готов
    tg.ready?.();
    tg.expand();
    // Ничего не включаем/не трогаем про closingConfirmation
  }

      const CS_KEY = "seedream_settings_v2";
      const LS_KEY = "seedream_settings_v2";

      const RATIO_BASE_2K = {
        "1:1":  { w: 2048, h: 2048 },
        "4:3":  { w: 2304, h: 1728 },
        "3:4":  { w: 1728, h: 2304 },
        "16:9": { w: 2560, h: 1440 },
        "9:16": { w: 1440, h: 2560 },
        "3:2":  { w: 2496, h: 1664 },
        "2:3":  { w: 1664, h: 2496 },
        "21:9": { w: 3024, h: 1296 }
      };

      const LONG_EDGE_BY_QUALITY = {
        "1k": 1024,
        "2k": 2048,
        "4k": 4096
      };

      const DEFAULT_STATE = {
        model: "4.5",
        quality: "2k",
        ratio: "1:1"
      };

      let state = { ...DEFAULT_STATE };

      const modelChips = document.querySelectorAll(".chip[data-model]");
      const qualityBtns = document.querySelectorAll(".segment-btn[data-quality]");
      const ratioBtns = document.querySelectorAll(".ratio-btn[data-ratio]");
      const dimsMainEl = document.getElementById("dimsMain");
      const dimsExtraEl = document.getElementById("dimsExtra");
      const resetBtn = document.getElementById("resetBtn");
      const saveBtn = document.getElementById("saveBtn");
      const statusText = document.getElementById("statusText");
      const statusPill = document.getElementById("statusPill");

      function sanitizeState(raw) {
        const s = { ...DEFAULT_STATE, ...(raw || {}) };

        if (!["4.5", "4.0"].includes(String(s.model))) {
          s.model = DEFAULT_STATE.model;
        }
        if (!["1k", "2k", "4k"].includes(String(s.quality))) {
          s.quality = DEFAULT_STATE.quality;
        }
        if (!Object.prototype.hasOwnProperty.call(RATIO_BASE_2K, s.ratio)) {
          s.ratio = DEFAULT_STATE.ratio;
        }
        return s;
      }

      function computeDims(ratio, quality) {
        const base = RATIO_BASE_2K[ratio] || RATIO_BASE_2K["1:1"];
        const scale = quality === "1k" ? 0.5 : quality === "4k" ? 2 : 1;
        return {
          w: Math.round(base.w * scale),
          h: Math.round(base.h * scale)
        };
      }

      function updateDimsUI() {
        const dims = computeDims(state.ratio, state.quality);
        dimsMainEl.innerHTML =
          'Размер: <strong>' + dims.w + ' × ' + dims.h + "</strong>";
        const le = LONG_EDGE_BY_QUALITY[state.quality] || LONG_EDGE_BY_QUALITY["2k"];
        dimsExtraEl.textContent = "Long edge (i2i): " + le;
      }

      function applyStateToUI() {
        modelChips.forEach(ch => {
          ch.classList.toggle("active", ch.dataset.model === String(state.model));
        });
        qualityBtns.forEach(btn => {
          btn.classList.toggle("active", btn.dataset.quality === String(state.quality));
        });
        ratioBtns.forEach(btn => {
          btn.classList.toggle("active", btn.dataset.ratio === String(state.ratio));
        });
        updateDimsUI();
      }

      function setStatus(text, type) {
        statusText.textContent = text;
        statusPill.classList.remove("ok", "err");
        if (type === "ok") {
          statusPill.textContent = "cloud";
          statusPill.classList.add("ok");
        } else if (type === "err") {
          statusPill.textContent = "local";
          statusPill.classList.add("err");
        } else {
          statusPill.textContent = "offline";
        }
      }

      function buildPayload() {
        const dims = computeDims(state.ratio, state.quality);
        const longEdge = LONG_EDGE_BY_QUALITY[state.quality] || LONG_EDGE_BY_QUALITY["2k"];

        return {
          type: "seedream_settings",
          model: state.model,
          quality: state.quality,
          ratio: state.ratio,
          dims_t2i: dims,
          long_edge_i2i: longEdge
        };
      }

      function saveToLocal(payload) {
        try {
          localStorage.setItem(LS_KEY, JSON.stringify(payload));
        } catch (e) {
          console.warn("localStorage error", e);
        }
      }

      function loadFromLocal() {
        try {
          const raw = localStorage.getItem(LS_KEY);
          if (!raw) return null;
          return JSON.parse(raw);
        } catch (e) {
          console.warn("localStorage read error", e);
          return null;
        }
      }

      function saveSettings() {
        const payload = buildPayload();
        setStatus("Сохраняю…", null);

        const finalize = (storedToCloud) => {
          saveToLocal(payload);
          if (storedToCloud) {
            setStatus("Сохранено (Cloud + local)", "ok");
          } else {
            setStatus("Сохранено (local)", "err");
          }

          if (tg && typeof tg.sendData === "function") {
            try {
              tg.sendData(JSON.stringify(payload));
            } catch (e) {
              console.error("sendData error", e);
            }
          }

          setTimeout(() => {
            if (tg) tg.close();
          }, 160);
        };

        if (tg && tg.CloudStorage && typeof tg.CloudStorage.setItem === "function") {
          try {
            tg.CloudStorage.setItem(
              CS_KEY,
              JSON.stringify(payload),
              function (err, stored) {
                if (err) {
                  console.warn("CloudStorage error", err);
                  finalize(false);
                } else {
                  finalize(!!stored);
                }
              }
            );
          } catch (e) {
            console.warn("CloudStorage exception", e);
            finalize(false);
          }
        } else {
          finalize(false);
        }
      }

      function restoreState() {
        // 1) CloudStorage, если доступен
        if (tg && tg.CloudStorage && typeof tg.CloudStorage.getItem === "function") {
          try {
            tg.CloudStorage.getItem(CS_KEY, function (err, value) {
              if (err || !value) {
                const local = loadFromLocal();
                state = sanitizeState(local);
                applyStateToUI();
                setStatus(local ? "Загружено из localStorage" : "Настройки по умолчанию", null);
              } else {
                try {
                  const parsed = JSON.parse(value);
                  state = sanitizeState(parsed);
                } catch {
                  const local = loadFromLocal();
                  state = sanitizeState(local);
                }
                applyStateToUI();
                setStatus("Загружено из CloudStorage", "ok");
              }
            });
          } catch (e) {
            console.warn("CloudStorage get exception", e);
            const local = loadFromLocal();
            state = sanitizeState(local);
            applyStateToUI();
            setStatus(local ? "Загружено из localStorage" : "Настройки по умолчанию", null);
          }
        } else {
          const local = loadFromLocal();
          state = sanitizeState(local);
          applyStateToUI();
          setStatus(local ? "Загружено из localStorage" : "Настройки по умолчанию", null);
        }
      }

      modelChips.forEach(ch => {
        ch.addEventListener("click", () => {
          state.model = ch.dataset.model;
          applyStateToUI();
        });
      });

      qualityBtns.forEach(btn => {
        btn.addEventListener("click", () => {
          state.quality = btn.dataset.quality;
          applyStateToUI();
        });
      });

      ratioBtns.forEach(btn => {
        btn.addEventListener("click", () => {
          state.ratio = btn.dataset.ratio;
          applyStateToUI();
        });
      });

      resetBtn.addEventListener("click", () => {
        state = { ...DEFAULT_STATE };
        applyStateToUI();
        setStatus("Сброшено к умолчанию", null);
      });

      saveBtn.addEventListener("click", () => {
        saveSettings();
      });

      restoreState();
    })();
  </script>
</body>
</html>