Logankunfall's picture
Upload 16 files
621a86c verified
/* 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();
})();