Files
image-generator/ui.js

306 lines
11 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';
// ── State ──────────────────────────────────────────────────────
let currentModel = 'image-01';
let currentImages = []; // [{url, base64}]
let loading = false;
// ── DOM refs ───────────────────────────────────────────────────
const $ = id => document.getElementById(id);
const promptInput = $('promptInput');
const charCount = $('charCount');
const generateForm = $('generateForm');
const generateBtn = $('generateBtn');
const btnIcon = generateBtn.querySelector('.btn-icon');
const btnText = generateBtn.querySelector('.btn-text');
const spinner = generateBtn.querySelector('.spinner');
const errorMsg = $('errorMsg');
const resultArea = $('resultArea');
const resultMeta = $('resultMeta');
const imageGrid = $('imageGrid');
const apiKeyAlert = $('apiKeyAlert');
const alertSettings = $('alertSettingsBtn');
const aspectRatio = $('aspectRatio');
const responseFormat = $('responseFormat');
const nInput = $('nInput');
const nMinus = $('nMinus');
const nPlus = $('nPlus');
const seedInput = $('seedInput');
const widthInput = $('widthInput');
const heightInput = $('heightInput');
const promptOptimizer= $('promptOptimizer');
const watermark = $('watermark');
const settingsModal = $('settingsModal');
const openSettings = $('openSettings');
const closeSettings = $('closeSettings');
const cancelSettings = $('cancelSettings');
const saveSettingsBtn= $('saveSettingsBtn');
const apiKeyInput = $('apiKeyInput');
const toggleKey = $('toggleKey');
const baseUrlInput = $('baseUrlInput');
// ── Stepper (n) ────────────────────────────────────────────────
nMinus.addEventListener('click', () => {
const v = parseInt(nInput.value) - 1;
if (v >= 1) nInput.value = v;
});
nPlus.addEventListener('click', () => {
const v = parseInt(nInput.value) + 1;
if (v <= 9) nInput.value = v;
});
// ── Helpers ────────────────────────────────────────────────────
function setLoading(on) {
loading = on;
generateBtn.disabled = on;
btnIcon.style.display = on ? 'none' : '';
btnText.style.display = on ? 'none' : '';
spinner.style.display = on ? 'inline' : 'none';
generateBtn.classList.toggle('loading', on);
}
function showError(msg) {
errorMsg.textContent = msg;
errorMsg.style.display = 'flex';
errorMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function clearError() {
errorMsg.style.display = 'none';
}
async function api(path, opts = {}) {
const res = await fetch(path, opts);
const json = await res.json();
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
return json;
}
function buildPayload() {
const n = parseInt(nInput.value);
const seed = seedInput.value ? parseInt(seedInput.value) : undefined;
const width = widthInput.value ? parseInt(widthInput.value) : undefined;
const height = heightInput.value ? parseInt(heightInput.value) : undefined;
const payload = {
model: currentModel,
prompt: promptInput.value.trim(),
aspect_ratio: aspectRatio.value,
response_format: responseFormat.value,
n: n,
};
if (seed) payload.seed = seed;
if (width && height) { payload.width = width; payload.height = height; }
if (promptOptimizer.checked) payload.prompt_optimizer = true;
if (watermark.checked) payload.aigc_watermark = true;
return payload;
}
function renderResults(images, id, successCount, failedCount) {
currentImages = images;
imageGrid.innerHTML = '';
if (id) {
resultMeta.innerHTML = `
<span>任务 ID<code>${id}</code></span>
${successCount != null ? `<span>成功 ${successCount}${failedCount ? `,失败 ${failedCount}` : ''}</span>` : ''}
`;
resultMeta.style.display = 'flex';
} else {
resultMeta.style.display = 'none';
}
const fmt = responseFormat.value;
images.forEach((src, i) => {
const card = document.createElement('div');
card.className = 'image-card';
const img = document.createElement('img');
img.src = fmt === 'url' ? src : `data:image/jpeg;base64,${src}`;
img.alt = `生成结果 ${i + 1}`;
img.loading = 'lazy';
const actions = document.createElement('div');
actions.className = 'image-actions';
const downloadBtn = document.createElement('button');
downloadBtn.className = 'action-btn';
downloadBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7,10 12,15 17,10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>下载`;
downloadBtn.addEventListener('click', () => downloadImage(src, fmt, i));
const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn';
copyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>复制`;
copyBtn.addEventListener('click', () => copySrc(src, fmt, copyBtn));
actions.appendChild(downloadBtn);
actions.appendChild(copyBtn);
card.appendChild(img);
card.appendChild(actions);
imageGrid.appendChild(card);
});
resultArea.style.display = 'block';
resultArea.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function downloadImage(src, fmt, index) {
const a = document.createElement('a');
if (fmt === 'url') {
a.href = src;
} else {
a.href = `data:image/jpeg;base64,${src}`;
}
a.download = `生成-${Date.now()}-${index + 1}.jpg`;
a.target = '_blank';
a.click();
}
async function copySrc(src, fmt, btn) {
try {
await navigator.clipboard.writeText(src);
const orig = btn.innerHTML;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,6 9,17 4,12"/></svg>已复制`;
setTimeout(() => { btn.innerHTML = orig; }, 1500);
} catch {
showError('复制失败');
}
}
// ── Init ────────────────────────────────────────────────────────
async function init() {
try {
const cfg = await api('/api/config');
if (!cfg.hasApiKey) apiKeyAlert.style.display = 'flex';
baseUrlInput.value = cfg.baseUrl || 'https://api.minimaxi.com';
} catch { /* non-fatal */ }
}
// ── Generate ────────────────────────────────────────────────────
generateForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (loading) return;
const prompt = promptInput.value.trim();
if (!prompt) { showError('请输入图片描述'); return; }
clearError();
resultArea.style.display = 'none';
setLoading(true);
try {
const payload = buildPayload();
const data = await api('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const images = data.images || [];
if (images.length === 0) {
throw new Error('未返回任何图片');
}
renderResults(images, data.id, data.success_count, data.failed_count);
} catch (err) {
showError(err.message);
} finally {
setLoading(false);
}
});
// ── Quick prompt tags ──────────────────────────────────────────
document.querySelectorAll('.tag-pill').forEach(btn => {
btn.addEventListener('click', () => {
const tag = btn.dataset.tag;
const current = promptInput.value.trim();
if (current) {
promptInput.value = current + '' + tag;
} else {
promptInput.value = tag;
}
promptInput.dispatchEvent(new Event('input'));
promptInput.focus();
});
});
// ── Prompt char count ──────────────────────────────────────────
promptInput.addEventListener('input', () => {
charCount.textContent = `${promptInput.value.length} / 1500`;
});
// ── Settings modal ──────────────────────────────────────────────
function openModal() {
settingsModal.classList.add('open');
apiKeyInput.focus();
}
function closeModal() {
settingsModal.classList.remove('open');
clearError();
}
openSettings.addEventListener('click', openModal);
alertSettings.addEventListener('click', openModal);
closeSettings.addEventListener('click', closeModal);
cancelSettings.addEventListener('click', closeModal);
settingsModal.addEventListener('click', e => { if (e.target === settingsModal) closeModal(); });
toggleKey.addEventListener('click', () => {
apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
});
saveSettingsBtn.addEventListener('click', async () => {
clearError();
const apiKey = apiKeyInput.value.trim();
const baseUrl = baseUrlInput.value.trim() || 'https://api.minimaxi.com';
try {
await api('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey, baseUrl }),
});
if (apiKey) apiKeyAlert.style.display = 'none';
closeModal();
} catch (err) {
showError(err.message);
}
});
// ── Theme switcher ─────────────────────────────────────────────
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem('imgGen-theme', theme);
document.querySelectorAll('.theme-btn').forEach(btn =>
btn.classList.toggle('active', btn.dataset.theme === theme)
);
}
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => applyTheme(btn.dataset.theme));
});
// Restore saved theme
const savedTheme = localStorage.getItem('imgGen-theme');
if (savedTheme) applyTheme(savedTheme);
// ── Start ──────────────────────────────────────────────────────
init();