import { EXAMPLE_PROMPTS, GENERATION_DEFAULTS, PROCESSING_STAGES, STORAGE_KEY, buildDownloadStem, buildPromptStats, createProcessingSnapshot, sanitizePrompt, } from "./prompt-builder.js"; const sessionStorageKey = "image-prompt-studio:session-id:v1"; const elements = { promptInput: document.querySelector("#promptInput"), promptStats: document.querySelector("#promptStats"), examplePromptList: document.querySelector("#examplePromptList"), clearPromptButton: document.querySelector("#clearPromptButton"), generateButton: document.querySelector("#generateButton"), healthLabel: document.querySelector("#healthLabel"), modelLabel: document.querySelector("#modelLabel"), processingBadge: document.querySelector("#processingBadge"), processingLead: document.querySelector("#processingLead"), processingSteps: document.querySelector("#processingSteps"), progressFill: document.querySelector("#progressFill"), requestMeta: document.querySelector("#requestMeta"), resultStage: document.querySelector("#resultStage"), errorBanner: document.querySelector("#errorBanner"), downloadButton: document.querySelector("#downloadButton"), }; const state = { prompt: loadPrompt(), isGenerating: false, health: { configured: false, model: "gpt-image-2", }, results: [], error: "", requestMeta: null, sessionId: getSessionId(), processing: { progress: 0, activeStageIndex: -1, lead: "准备就绪,输入 Prompt 后即可开始生成。", }, processingTimer: null, startedAt: 0, }; renderExamplePrompts(); elements.promptInput.value = state.prompt; updatePromptStats(); render(); wireEvents(); void checkHealth(); function wireEvents() { elements.promptInput.addEventListener("input", () => { state.prompt = elements.promptInput.value; persistPrompt(); updatePromptStats(); render(); }); elements.clearPromptButton.addEventListener("click", () => { state.prompt = ""; state.results = []; state.requestMeta = null; state.error = ""; state.processing = { progress: 0, activeStageIndex: -1, lead: "准备就绪,输入 Prompt 后即可开始生成。", }; elements.promptInput.value = ""; persistPrompt(); updatePromptStats(); render(); }); elements.generateButton.addEventListener("click", () => { void generateImages(); }); elements.downloadButton.addEventListener("click", () => { const result = state.results[0]; if (result) { downloadResult(result); } }); document.body.addEventListener("keydown", (event) => { if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { void generateImages(); } }); elements.examplePromptList.addEventListener("click", (event) => { const button = event.target.closest("[data-prompt]"); if (!button) { return; } state.prompt = button.dataset.prompt ?? ""; elements.promptInput.value = state.prompt; persistPrompt(); updatePromptStats(); render(); }); } async function generateImages() { if (state.isGenerating) { return; } const prompt = sanitizePrompt(state.prompt); if (!prompt) { setError("请输入 Prompt 后再生成图片。"); return; } state.prompt = prompt; elements.promptInput.value = prompt; persistPrompt(); state.isGenerating = true; state.error = ""; state.results = []; state.requestMeta = null; startProcessingTicker(); render(); try { const response = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ prompt, ...GENERATION_DEFAULTS, user: state.sessionId, }), }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || "图片生成失败。"); } const stem = buildDownloadStem(prompt); state.results = payload.images.map((image, index) => ({ ...image, previewUrl: `data:${image.mimeType};base64,${image.base64}`, fileName: `${stem}-${index + 1}.${resolveExtension(image.mimeType)}`, })); state.requestMeta = payload.meta; state.processing = createProcessingSnapshot( Date.now() - state.startedAt, true, ); state.error = ""; } catch (error) { setError(error.message || "图片生成失败,请稍后重试。"); } finally { state.isGenerating = false; stopProcessingTicker(); render(); } } async function checkHealth() { try { const response = await fetch("/api/health"); const payload = await response.json(); state.health = payload; } catch { state.health = { configured: false, model: "gpt-image-2", }; } render(); } function startProcessingTicker() { stopProcessingTicker(); state.startedAt = Date.now(); state.processing = createProcessingSnapshot(0); state.processingTimer = window.setInterval(() => { state.processing = createProcessingSnapshot(Date.now() - state.startedAt); render(); }, 140); } function stopProcessingTicker() { if (state.processingTimer) { window.clearInterval(state.processingTimer); state.processingTimer = null; } } function render() { elements.modelLabel.textContent = state.health.model || "gpt-image-2"; elements.healthLabel.textContent = state.health.configured ? "已连接服务端" : "缺少 OPENAI_API_KEY"; elements.processingLead.textContent = state.processing.lead; elements.processingBadge.textContent = state.isGenerating ? "处理中" : state.results.length > 0 ? "已完成" : "待命"; elements.progressFill.style.width = `${Math.round(state.processing.progress * 100)}%`; elements.requestMeta.textContent = buildRequestMeta(); elements.errorBanner.hidden = !state.error; elements.errorBanner.textContent = state.error; elements.generateButton.disabled = state.isGenerating; elements.generateButton.textContent = state.isGenerating ? "生成中..." : "生成图片"; elements.downloadButton.disabled = state.results.length === 0; renderProcessingSteps(); renderResultStage(); } function renderExamplePrompts() { elements.examplePromptList.innerHTML = EXAMPLE_PROMPTS.map( (prompt) => ` `, ).join(""); } function renderProcessingSteps() { elements.processingSteps.innerHTML = PROCESSING_STAGES.map((stage, index) => { const status = resolveStageStatus(index); return `
  • ${escapeHtml(stage.title)}

    ${escapeHtml(stage.detail)}

  • `; }).join(""); } function renderResultStage() { if (state.isGenerating) { elements.resultStage.className = "result-stage loading-state"; elements.resultStage.innerHTML = `
    `; return; } if (state.results.length === 0) { elements.resultStage.className = "result-stage empty-state"; elements.resultStage.innerHTML = `

    这里会显示最新生成的图片

    当前只保留最简流程:输入 Prompt、点击生成、等待完成、下载图片。

    `; return; } const result = state.results[0]; const revisedPromptMarkup = result.revisedPrompt ? `
    模型调整后的 Prompt

    ${escapeHtml(result.revisedPrompt)}

    ` : ""; elements.resultStage.className = "result-stage"; elements.resultStage.innerHTML = `
    ${escapeHtml(result.fileName)}
    你的 Prompt

    ${escapeHtml(state.prompt)}

    ${revisedPromptMarkup} `; } function resolveStageStatus(index) { if (!state.isGenerating && state.results.length > 0) { return "done"; } if (!state.isGenerating) { return "idle"; } if (index < state.processing.activeStageIndex) { return "done"; } if (index === state.processing.activeStageIndex) { return "active"; } return "idle"; } function buildRequestMeta() { if (state.requestMeta) { return `${state.requestMeta.model} · ${state.requestMeta.size} · ${state.requestMeta.outputFormat.toUpperCase()} · ${state.requestMeta.durationMs} ms`; } if (state.isGenerating) { return "复杂图片生成可能需要几十秒,请耐心等待当前步骤完成。"; } return "生成完成后会在这里显示耗时和输出信息。"; } function updatePromptStats() { elements.promptStats.textContent = buildPromptStats(state.prompt); } function persistPrompt() { localStorage.setItem(STORAGE_KEY, state.prompt); } function loadPrompt() { return String(localStorage.getItem(STORAGE_KEY) ?? ""); } function getSessionId() { const existing = localStorage.getItem(sessionStorageKey); if (existing) { return existing; } const sessionId = `browser-${crypto.randomUUID()}`; localStorage.setItem(sessionStorageKey, sessionId); return sessionId; } function setError(message) { state.error = message; stopProcessingTicker(); state.processing = { progress: 0, activeStageIndex: -1, lead: "本次生成未完成,请调整 Prompt 后重试。", }; render(); } function downloadResult(result) { const blob = base64ToBlob(result.base64, result.mimeType); const blobUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = blobUrl; anchor.download = result.fileName; anchor.click(); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 2000); } function base64ToBlob(base64, mimeType) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return new Blob([bytes], { type: mimeType }); } function resolveExtension(mimeType) { if (mimeType === "image/jpeg") { return "jpg"; } if (mimeType === "image/webp") { return "webp"; } return "png"; } function truncate(value, maxLength) { const text = String(value ?? ""); if (text.length <= maxLength) { return text; } return `${text.slice(0, maxLength - 1)}…`; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }