Files
image-generator/index.html

371 lines
14 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Generator</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="app">
<!-- Header -->
<header>
<div class="logo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
</svg>
<span>Image Generator</span>
</div>
<button class="icon-btn" id="openSettings" title="Settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
</header>
<!-- API Key Alert -->
<div class="alert-banner" id="apiKeyAlert" style="display:none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<span>API key not set. Please configure your MiniMax API key in <button class="link-btn" id="alertSettingsBtn">Settings</button>.</span>
</div>
<!-- Main form -->
<main>
<form id="generateForm">
<div class="prompt-row">
<textarea
id="promptInput"
placeholder="Describe the image you want to generate..."
rows="3"
maxlength="1500"
autofocus
></textarea>
<div class="prompt-footer">
<span class="char-count" id="charCount">0 / 1500</span>
</div>
</div>
<div class="controls">
<div class="control-group">
<label for="aspectRatio">Aspect Ratio</label>
<select id="aspectRatio">
<option value="1:1">1:1 — Square (1024×1024)</option>
<option value="16:9" selected>16:9 — Widescreen (1280×720)</option>
<option value="4:3">4:3 — Standard (1152×864)</option>
<option value="3:2">3:2 — Photo (1248×832)</option>
<option value="2:3">2:3 — Portrait (832×1248)</option>
<option value="3:4">3:4 — Portrait (864×1152)</option>
<option value="9:16">9:16 — Vertical (720×1280)</option>
<option value="21:9">21:9 — Ultrawide (1344×576)</option>
</select>
</div>
<button type="submit" class="generate-btn" id="generateBtn">
<svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"/>
</svg>
<span class="btn-text">Generate</span>
<svg class="spinner" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="20"/>
</svg>
</button>
</div>
</form>
<!-- Error message -->
<div class="error-msg" id="errorMsg" style="display:none"></div>
<!-- Generated image(s) -->
<div class="result-area" id="resultArea" style="display:none">
<div class="result-header">
<span id="resultLabel">Generated</span>
<div class="result-actions">
<button class="action-btn" id="downloadBtn" title="Download">
<svg width="16" height="16" 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>
Download
</button>
<button class="action-btn" id="copyBtn" title="Copy as Base64">
<svg width="16" height="16" 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>
Copy
</button>
</div>
</div>
<div class="image-wrap" id="imageWrap">
<img id="resultImg" src="" alt="Generated image" />
</div>
</div>
</main>
</div>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="modal">
<div class="modal-header">
<h2>Settings</h2>
<button class="icon-btn" id="closeSettings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="field">
<label for="apiKeyInput">MiniMax API Key</label>
<div class="input-row">
<input type="password" id="apiKeyInput" placeholder="eyJhbGciOiJIUzI1NiIsInR..." />
<button class="icon-btn" id="toggleKey" type="button" title="Show/Hide">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
<p class="field-hint">Get your API key from <a href="https://platform.minimax.io/" target="_blank" rel="noopener">platform.minimax.io</a></p>
</div>
<div class="field">
<label for="baseUrlInput">API Base URL</label>
<input type="text" id="baseUrlInput" placeholder="https://api.minimax.io" />
<p class="field-hint">Only change if using an API proxy.</p>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="cancelSettings">Cancel</button>
<button class="btn-primary" id="saveSettings">Save</button>
</div>
</div>
</div>
<script>
'use strict';
// ============================================================
// DOM refs
// ============================================================
const promptInput = document.getElementById('promptInput');
const charCount = document.getElementById('charCount');
const aspectRatio = document.getElementById('aspectRatio');
const generateForm = document.getElementById('generateForm');
const generateBtn = document.getElementById('generateBtn');
const btnIcon = generateBtn.querySelector('.btn-icon');
const btnText = generateBtn.querySelector('.btn-text');
const spinner = generateBtn.querySelector('.spinner');
const errorMsg = document.getElementById('errorMsg');
const resultArea = document.getElementById('resultArea');
const resultLabel = document.getElementById('resultLabel');
const resultImg = document.getElementById('resultImg');
const imageWrap = document.getElementById('imageWrap');
const downloadBtn = document.getElementById('downloadBtn');
const copyBtn = document.getElementById('copyBtn');
const apiKeyAlert = document.getElementById('apiKeyAlert');
const alertSettings = document.getElementById('alertSettingsBtn');
const settingsModal = document.getElementById('settingsModal');
const openSettings = document.getElementById('openSettings');
const closeSettings = document.getElementById('closeSettings');
const cancelSettings = document.getElementById('cancelSettings');
const saveSettingsBtn= document.getElementById('saveSettings');
const apiKeyInput = document.getElementById('apiKeyInput');
const toggleKey = document.getElementById('toggleKey');
const baseUrlInput = document.getElementById('baseUrlInput');
// ============================================================
// State
// ============================================================
let currentBase64 = '';
let loading = false;
// ============================================================
// 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';
}
function clearError() {
errorMsg.style.display = 'none';
}
function showResult(base64Data) {
currentBase64 = base64Data;
resultImg.src = 'data:image/jpeg;base64,' + base64Data;
resultArea.style.display = 'block';
imageWrap.classList.add('pop-in');
setTimeout(() => imageWrap.classList.remove('pop-in'), 600);
}
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;
}
// ============================================================
// Init: load config
// ============================================================
async function init() {
try {
const cfg = await api('/api/config');
if (!cfg.hasApiKey) {
apiKeyAlert.style.display = 'flex';
}
baseUrlInput.value = cfg.baseUrl || 'https://api.minimax.io';
} catch (e) {
// non-fatal
}
}
// ============================================================
// Generate
// ============================================================
generateForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (loading) return;
const prompt = promptInput.value.trim();
if (!prompt) {
showError('Please enter a prompt.');
return;
}
clearError();
resultArea.style.display = 'none';
setLoading(true);
try {
const data = await api('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: prompt,
aspect_ratio: aspectRatio.value,
}),
});
if (!data.images || data.images.length === 0) {
throw new Error('No images returned.');
}
showResult(data.images[0]);
} catch (err) {
showError(err.message);
} finally {
setLoading(false);
}
});
// ============================================================
// Prompt char count
// ============================================================
promptInput.addEventListener('input', () => {
charCount.textContent = `${promptInput.value.length} / 1500`;
});
// ============================================================
// Download
// ============================================================
downloadBtn.addEventListener('click', () => {
if (!currentBase64) return;
const a = document.createElement('a');
a.href = 'data:image/jpeg;base64,' + currentBase64;
a.download = 'generated-' + Date.now() + '.jpg';
a.click();
});
copyBtn.addEventListener('click', async () => {
if (!currentBase64) return;
try {
await navigator.clipboard.writeText(currentBase64);
copyBtn.textContent = 'Copied!';
setTimeout(() => { copyBtn.innerHTML = `<svg width="16" height="16" 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> Copy`; }, 1500);
} catch {
showError('Clipboard copy failed.');
}
});
// ============================================================
// 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', () => {
const isPw = apiKeyInput.type === 'password';
apiKeyInput.type = isPw ? 'text' : 'password';
});
saveSettingsBtn.addEventListener('click', async () => {
clearError();
const apiKey = apiKeyInput.value.trim();
const baseUrl = baseUrlInput.value.trim() || 'https://api.minimax.io';
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);
}
});
// ============================================================
// Init
// ============================================================
init();
</script>