Spaces:
Paused
Paused
| /* New NAI Frontend App JS */ | |
| (() => { | |
| const $ = (sel, root = document) => root.querySelector(sel); | |
| const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| const byId = (id) => document.getElementById(id); | |
| // 提示音状态(运行时),默认关闭,URL 指向 /ring/ring.mp3 | |
| let soundEnabled = false; | |
| let soundUrl = "/ring/ring.mp3"; | |
| // 主题:深色/浅色 切换 | |
| const rootEl = document.documentElement; | |
| function applyTheme(theme) { | |
| rootEl.setAttribute('data-theme', theme); | |
| localStorage.setItem('theme', theme); | |
| const btn = byId('theme-toggle'); | |
| if (btn) btn.textContent = '主题:' + (theme === 'dark' ? '深色' : '浅色'); | |
| } | |
| const initialTheme = localStorage.getItem('theme') || 'dark'; | |
| applyTheme(initialTheme); | |
| const themeBtn = byId('theme-toggle'); | |
| if (themeBtn) { | |
| themeBtn.addEventListener('click', () => { | |
| const next = (rootEl.getAttribute('data-theme') === 'dark') ? 'light' : 'dark'; | |
| applyTheme(next); | |
| }); | |
| } | |
| // 提示音开关(在主题按钮旁) | |
| const soundBtn = byId('sound-toggle'); | |
| const soundPlayer = byId('sound-player'); | |
| function updateSoundToggle() { | |
| if (!soundBtn) return; | |
| soundBtn.setAttribute('aria-pressed', soundEnabled ? 'true' : 'false'); | |
| soundBtn.textContent = '提示音:' + (soundEnabled ? '开' : '关'); | |
| } | |
| if (soundBtn) { | |
| soundBtn.addEventListener('click', () => { | |
| soundEnabled = !soundEnabled; | |
| updateSoundToggle(); | |
| }); | |
| } | |
| // ===== 背景方案处理(非字体/主按钮):根据配置切换页面背景 ===== | |
| function setBackground(color) { | |
| // 统一覆盖背景变量,立即生效(无刷新) | |
| rootEl.style.setProperty("--bg", color); | |
| } | |
| function applyBackgroundScheme(scheme, custom) { | |
| const theme = rootEl.getAttribute("data-theme") || "dark"; | |
| if (scheme === "custom" && custom) { | |
| setBackground(custom); | |
| return; | |
| } | |
| if (scheme === "bamboo") { | |
| setBackground("#7ba23f"); // 竹子色 | |
| return; | |
| } | |
| // auto:浅=白 深=深灰 | |
| if (theme === "light") setBackground("#ffffff"); | |
| else setBackground("#121315"); | |
| } | |
| function applyAccentFromConfig(cfg) { | |
| const scheme = cfg?.color_scheme || "auto"; | |
| const custom = cfg?.custom_primary || ""; | |
| applyBackgroundScheme(scheme, custom); | |
| } | |
| // Tabs | |
| $$(".tab-btn").forEach((btn) => { | |
| btn.addEventListener("click", () => { | |
| $$(".tab-btn").forEach((b) => b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| const tabId = btn.getAttribute("data-tab"); | |
| $$(".tab").forEach((tab) => tab.classList.remove("active")); | |
| byId(tabId).classList.add("active"); | |
| }); | |
| }); | |
| // UI helpers | |
| // 固定状态栏:不弹出、不自动隐藏 | |
| const toast = (msg, type = "info") => { | |
| const el = byId("toast"); | |
| el.textContent = msg || ""; | |
| el.className = `toast ${type}`; | |
| // 成功提示时若开启提示音则播放 | |
| try { | |
| if (type === "success" && soundEnabled) { | |
| const p = byId("sound-player"); | |
| if (p) { | |
| p.currentTime = 0; | |
| // 忽略浏览器自动播放限制的异常 | |
| p.play().catch(() => {}); | |
| } | |
| } | |
| } catch {} | |
| }; | |
| const loading = { | |
| show() { | |
| byId("loading").classList.remove("hidden"); | |
| }, | |
| hide() { | |
| byId("loading").classList.add("hidden"); | |
| }, | |
| }; | |
| // Config | |
| async function loadConfig() { | |
| try { | |
| const res = await fetch("/api/config"); | |
| if (!res.ok) throw new Error(await res.text()); | |
| const cfg = await res.json(); | |
| byId("cfg-key").value = cfg.key ?? ""; | |
| byId("cfg-model").value = cfg.model ?? "nai-diffusion-3"; | |
| byId("cfg-sampler").value = cfg.sampler ?? "k_euler"; | |
| byId("cfg-steps").value = cfg.steps ?? ""; | |
| byId("cfg-scale").value = cfg.scale ?? ""; | |
| byId("cfg-cfg-rescale").value = cfg.cfg_rescale ?? ""; | |
| byId("cfg-noise-schedule").value = cfg.noise_schedule ?? ""; | |
| byId("cfg-uc-preset").value = cfg.uc_preset ?? ""; | |
| byId("cfg-quality-toggle").checked = !!cfg.quality_toggle; | |
| byId("cfg-legacy-uc").checked = !!cfg.legacy_uc; | |
| byId("cfg-port").value = cfg.port ?? 9180; | |
| byId("cfg-save-output").checked = !!cfg.save_output; | |
| if (byId("cfg-output-dir")) byId("cfg-output-dir").value = cfg.output_dir ?? ""; | |
| // 颜色配置(带预览) | |
| if (byId("cfg-color-scheme")) byId("cfg-color-scheme").value = cfg.color_scheme ?? "auto"; | |
| if (byId("cfg-custom-primary")) byId("cfg-custom-primary").value = cfg.custom_primary ?? "#7ba23f"; | |
| applyAccentFromConfig(cfg); | |
| // 提示音配置 | |
| soundEnabled = !!cfg.sound_enabled; | |
| soundUrl = cfg.sound_url || "/ring/ring.mp3"; | |
| if (byId("sound-player")) byId("sound-player").src = soundUrl; | |
| (typeof updateSoundToggle === "function") && updateSoundToggle(); | |
| // 隐藏下方“配置已读取”提示 | |
| byId("cfg-message").textContent = ""; | |
| } catch (err) { | |
| byId("cfg-message").textContent = "读取失败:" + err.message; | |
| toast("读取配置失败", "error"); | |
| } | |
| } | |
| function nullIfEmpty(v) { | |
| if (v === undefined || v === null) return null; | |
| if (typeof v === "string" && v.trim() === "") return null; | |
| return v; | |
| } | |
| async function saveConfig() { | |
| const payload = { | |
| key: nullIfEmpty(byId("cfg-key").value), | |
| model: nullIfEmpty(byId("cfg-model").value), | |
| sampler: nullIfEmpty(byId("cfg-sampler").value), | |
| steps: byId("cfg-steps").value ? Number(byId("cfg-steps").value) : null, | |
| scale: byId("cfg-scale").value ? Number(byId("cfg-scale").value) : null, | |
| cfg_rescale: byId("cfg-cfg-rescale").value ? Number(byId("cfg-cfg-rescale").value) : null, | |
| noise_schedule: nullIfEmpty(byId("cfg-noise-schedule").value), | |
| uc_preset: byId("cfg-uc-preset").value ? Number(byId("cfg-uc-preset").value) : null, | |
| quality_toggle: byId("cfg-quality-toggle").checked, | |
| legacy_uc: byId("cfg-legacy-uc").checked, | |
| port: byId("cfg-port").value ? Number(byId("cfg-port").value) : null, | |
| save_output: byId("cfg-save-output").checked, | |
| output_dir: nullIfEmpty(byId("cfg-output-dir")?.value), | |
| color_scheme: nullIfEmpty(byId("cfg-color-scheme")?.value), | |
| custom_primary: nullIfEmpty(byId("cfg-custom-primary")?.value), | |
| sound_enabled: !!soundEnabled, | |
| sound_url: nullIfEmpty(soundUrl), | |
| }; | |
| try { | |
| loading.show(); | |
| const res = await fetch("/api/config", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!res.ok) throw new Error(await res.text()); | |
| await res.json(); | |
| byId("cfg-message").textContent = "保存成功"; | |
| toast("配置已保存", "success"); | |
| } catch (err) { | |
| byId("cfg-message").textContent = "保存失败:" + err.message; | |
| toast("保存配置失败", "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| } | |
| // File -> Base64 (strip data URL prefix) | |
| function fileToBase64(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const result = reader.result || ""; | |
| const idx = String(result).indexOf(","); | |
| resolve(idx >= 0 ? String(result).slice(idx + 1) : String(result)); | |
| }; | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| // Ensure x64 | |
| function ensureX64(n) { | |
| const val = Number(n || 0); | |
| if (!val || val <= 64) return 64; | |
| if (val % 64 === 0) return val; | |
| const fract = (val / 64) % 1; | |
| return fract >= 0.5 ? (Math.floor(val / 64) + 1) * 64 : Math.floor(val / 64) * 64; | |
| } | |
| // T2I | |
| async function handleT2I() { | |
| const payload = { | |
| prompt: byId("t2i-prompt").value || "", | |
| negative: byId("t2i-negative").value || "", | |
| width: ensureX64(byId("t2i-width").value || 768), | |
| height: ensureX64(byId("t2i-height").value || 768), | |
| steps: byId("t2i-steps").value ? Number(byId("t2i-steps").value) : null, | |
| scale: byId("t2i-scale").value ? Number(byId("t2i-scale").value) : null, | |
| sampler: nullIfEmpty(byId("t2i-sampler").value), | |
| noise_schedule: nullIfEmpty(byId("t2i-noise-schedule").value), | |
| seed: byId("t2i-seed").value ? Number(byId("t2i-seed").value) : -1, | |
| variety: byId("t2i-variety").checked, | |
| decrisp: byId("t2i-decrisp").checked, | |
| cfg_rescale: byId("t2i-cfg-rescale").value ? Number(byId("t2i-cfg-rescale").value) : null, | |
| }; | |
| if (!payload.prompt.trim()) { | |
| toast("请填写正面提示词", "error"); | |
| return; | |
| } | |
| try { | |
| loading.show(); | |
| const res = await fetch("/api/generate/t2i", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const body = await (async () => { | |
| const txt = await res.text(); | |
| try { | |
| return JSON.parse(txt); | |
| } catch { | |
| throw new Error(txt); | |
| } | |
| })(); | |
| if (!res.ok) throw new Error(body?.detail || "生成失败"); | |
| const imgEl = byId("t2i-img"); | |
| imgEl.src = body.image_base64; | |
| byId("t2i-saved").value = body.saved_path || ""; | |
| toast("生成成功", "success"); | |
| } catch (err) { | |
| toast(String(err.message || err), "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| } | |
| // I2I | |
| async function handleI2I() { | |
| const file = byId("i2i-image").files?.[0]; | |
| if (!file) { | |
| toast("请上传输入图片", "error"); | |
| return; | |
| } | |
| const image_base64 = await fileToBase64(file); | |
| const payload = { | |
| positive: byId("i2i-positive").value || "", | |
| negative: byId("i2i-negative").value || "", | |
| image_base64, | |
| width: byId("i2i-width").value ? ensureX64(byId("i2i-width").value) : null, | |
| height: byId("i2i-height").value ? ensureX64(byId("i2i-height").value) : null, | |
| steps: byId("i2i-steps").value ? Number(byId("i2i-steps").value) : null, | |
| scale: byId("i2i-scale").value ? Number(byId("i2i-scale").value) : null, | |
| sampler: nullIfEmpty(byId("i2i-sampler").value), | |
| noise_schedule: nullIfEmpty(byId("i2i-noise-schedule").value), | |
| seed: byId("i2i-seed").value ? Number(byId("i2i-seed").value) : -1, | |
| strength: byId("i2i-strength").value ? Number(byId("i2i-strength").value) : 0.5, | |
| noise: byId("i2i-noise").value ? Number(byId("i2i-noise").value) : 0.0, | |
| variety: byId("i2i-variety").checked, | |
| decrisp: byId("i2i-decrisp").checked, | |
| cfg_rescale: byId("i2i-cfg-rescale").value ? Number(byId("i2i-cfg-rescale").value) : null, | |
| }; | |
| if (!payload.positive.trim()) { | |
| toast("请填写正面提示词", "error"); | |
| return; | |
| } | |
| try { | |
| loading.show(); | |
| const res = await fetch("/api/generate/i2i", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const body = await (async () => { | |
| const txt = await res.text(); | |
| try { | |
| return JSON.parse(txt); | |
| } catch { | |
| throw new Error(txt); | |
| } | |
| })(); | |
| if (!res.ok) throw new Error(body?.detail || "生成失败"); | |
| const imgEl = byId("i2i-img"); | |
| imgEl.src = body.image_base64; | |
| byId("i2i-saved").value = body.saved_path || ""; | |
| toast("生成成功", "success"); | |
| } catch (err) { | |
| toast(String(err.message || err), "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| } | |
| // Inpaint | |
| async function handleInpaint() { | |
| const imgFile = byId("inpaint-image").files?.[0]; | |
| const maskFile = byId("inpaint-mask").files?.[0]; | |
| if (!imgFile || !maskFile) { | |
| toast("请上传底图与遮罩", "error"); | |
| return; | |
| } | |
| const image_base64 = await fileToBase64(imgFile); | |
| const mask_base64 = await fileToBase64(maskFile); | |
| const payload = { | |
| positive: byId("inpaint-positive").value || "", | |
| negative: byId("inpaint-negative").value || "", | |
| image_base64, | |
| mask_base64, | |
| add_original_image: byId("inpaint-add-original").checked, | |
| width: byId("inpaint-width").value ? ensureX64(byId("inpaint-width").value) : null, | |
| height: byId("inpaint-height").value ? ensureX64(byId("inpaint-height").value) : null, | |
| steps: byId("inpaint-steps").value ? Number(byId("inpaint-steps").value) : null, | |
| scale: byId("inpaint-scale").value ? Number(byId("inpaint-scale").value) : null, | |
| sampler: nullIfEmpty(byId("inpaint-sampler").value), | |
| noise_schedule: nullIfEmpty(byId("inpaint-noise-schedule").value), | |
| seed: byId("inpaint-seed").value ? Number(byId("inpaint-seed").value) : -1, | |
| strength: byId("inpaint-strength").value ? Number(byId("inpaint-strength").value) : 0.5, | |
| noise: byId("inpaint-noise-val").value ? Number(byId("inpaint-noise-val").value) : 0.0, | |
| variety: byId("inpaint-variety").checked, | |
| decrisp: byId("inpaint-decrisp").checked, | |
| cfg_rescale: byId("inpaint-cfg-rescale").value ? Number(byId("inpaint-cfg-rescale").value) : null, | |
| }; | |
| if (!payload.positive.trim()) { | |
| toast("请填写 Positive", "error"); | |
| return; | |
| } | |
| try { | |
| loading.show(); | |
| const res = await fetch("/api/generate/inpaint", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const body = await (async () => { | |
| const txt = await res.text(); | |
| try { | |
| return JSON.parse(txt); | |
| } catch { | |
| throw new Error(txt); | |
| } | |
| })(); | |
| if (!res.ok) throw new Error(body?.detail || "生成失败"); | |
| const imgEl = byId("inpaint-img"); | |
| imgEl.src = body.image_base64; | |
| byId("inpaint-saved").value = body.saved_path || ""; | |
| toast("生成成功", "success"); | |
| } catch (err) { | |
| toast(String(err.message || err), "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| } | |
| // Bindings | |
| byId("btn-load-config").addEventListener("click", loadConfig); | |
| byId("btn-save-config").addEventListener("click", saveConfig); | |
| const selOutBtn = byId("btn-select-output-dir"); | |
| if (selOutBtn) { | |
| selOutBtn.addEventListener("click", async () => { | |
| try { | |
| loading.show(); | |
| const res = await fetch("/api/select-output-dir"); | |
| let body = {}; | |
| try { body = await res.json(); } catch {} | |
| if (!res.ok) { | |
| // 失败兜底:提示手动输入,并尝试打开当前保存目录帮助定位 | |
| toast(body?.detail || "选择失败,请手动在输入框填入保存目录", "error"); | |
| try { | |
| await fetch("/api/open-dir", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: byId("cfg-output-dir")?.value || "" }) }); | |
| } catch {} | |
| return; | |
| } | |
| const p = String(body?.path || ""); | |
| if (p) { | |
| const input = byId("cfg-output-dir"); | |
| if (input) input.value = p; | |
| toast("已选择保存目录", "success"); | |
| } else { | |
| toast("未选择任何目录", "info"); | |
| } | |
| } catch (e) { | |
| toast(String(e?.message || e), "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| }); | |
| } | |
| const openOutBtn = byId("btn-open-output-dir"); | |
| if (openOutBtn) { | |
| openOutBtn.addEventListener("click", async () => { | |
| try { | |
| loading.show(); | |
| const p = byId("cfg-output-dir")?.value || ""; | |
| const res = await fetch("/api/open-dir", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ path: p }), | |
| }); | |
| const body = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(body?.detail || "打开目录失败"); | |
| toast("已打开保存目录", "success"); | |
| } catch (e) { | |
| toast(String(e?.message || e), "error"); | |
| } finally { | |
| loading.hide(); | |
| } | |
| }); | |
| } | |
| // 颜色方案即时预览(无需保存即可体验) | |
| const schemeSel = byId("cfg-color-scheme"); | |
| const customColor = byId("cfg-custom-primary"); | |
| if (schemeSel) { | |
| schemeSel.addEventListener("change", () => { | |
| applyBackgroundScheme(schemeSel.value || "auto", customColor?.value || ""); | |
| }); | |
| } | |
| if (customColor) { | |
| customColor.addEventListener("input", () => { | |
| applyBackgroundScheme("custom", customColor.value || "#7ba23f"); | |
| }); | |
| } | |
| byId("btn-t2i").addEventListener("click", handleT2I); | |
| byId("btn-i2i").addEventListener("click", handleI2I); | |
| byId("btn-inpaint").addEventListener("click", handleInpaint); | |
| // Init | |
| loadConfig(); | |
| })(); |