'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 = ` 任务 ID:${id} ${successCount != null ? `成功 ${successCount} 张${failedCount ? `,失败 ${failedCount} 张` : ''}` : ''} `; 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 = `下载`; downloadBtn.addEventListener('click', () => downloadImage(src, fmt, i)); const copyBtn = document.createElement('button'); copyBtn.className = 'action-btn'; copyBtn.innerHTML = `复制`; 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 = `已复制`; 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); } }); // ── 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); } }); // ── Start ────────────────────────────────────────────────────── init();