Spaces:
Runtime error
Runtime error
| document.addEventListener("DOMContentLoaded", () => { | |
| // DOM Elements - Navigation | |
| const btnSimulator = document.getElementById("btn-simulator"); | |
| const btnDeveloper = document.getElementById("btn-developer"); | |
| const btnHistory = document.getElementById("btn-history"); | |
| const sectionSimulator = document.getElementById("section-simulator-view"); | |
| const sectionDeveloper = document.getElementById("section-developer-view"); | |
| const sectionHistory = document.getElementById("section-history-view"); | |
| const pageTitle = document.getElementById("page-title"); | |
| const pageDesc = document.getElementById("page-desc"); | |
| // DOM Elements - Chat Simulator | |
| const chatInput = document.getElementById("chat-input"); | |
| const btnSendMessage = document.getElementById("btn-send-message"); | |
| const chatMessagesContainer = document.getElementById("chat-messages-container"); | |
| // DOM Elements - Inspector Panel (Phase 1) | |
| const previewEmptyState = document.getElementById("preview-empty-state"); | |
| const previewLoadingState = document.getElementById("preview-loading-state"); | |
| const previewImage = document.getElementById("preview-image"); | |
| const previewMetadataPanel = document.getElementById("preview-metadata-panel"); | |
| const loadingStepText = document.getElementById("loading-step-text"); | |
| const loadingFunnyQuote = document.getElementById("loading-funny-quote"); | |
| const metaQuery = document.getElementById("meta-query"); | |
| const metaGreeting = document.getElementById("meta-greeting"); | |
| const metaPrompt = document.getElementById("meta-prompt"); | |
| const btnDownload = document.getElementById("btn-download"); | |
| const btnShowApiDetails = document.getElementById("btn-show-api-details"); | |
| // DOM Elements - Settings Modal | |
| const settingsModal = document.getElementById("settings-modal"); | |
| const btnOpenSettings = document.getElementById("btn-open-settings"); | |
| const btnCloseSettings = document.getElementById("btn-close-settings"); | |
| const btnSaveSettings = document.getElementById("btn-save-settings"); | |
| const btnResetSettings = document.getElementById("btn-reset-settings"); | |
| const inputApiKey = document.getElementById("input-api-key"); | |
| const inputCustomGreeting = document.getElementById("input-custom-greeting"); | |
| const selectHeaderPreset = document.getElementById("select-header-preset"); | |
| // DOM Elements - Developer Hub | |
| const urlLatest = document.getElementById("url-latest"); | |
| const urlGenerate = document.getElementById("url-generate"); | |
| const btnCopies = document.querySelectorAll(".btn-copy"); | |
| const tabBtns = document.querySelectorAll(".tab-btn"); | |
| const tabContents = document.querySelectorAll(".tab-content"); | |
| // DOM Elements - History | |
| const historyGridContainer = document.getElementById("history-grid-container"); | |
| const historyCountText = document.getElementById("history-count-text"); | |
| // Configuration / Local State | |
| let config = { | |
| apiKey: localStorage.getItem("GOOGLE_API_KEY") || "", | |
| customGreeting: localStorage.getItem("CUSTOM_GREETING") || "", | |
| headerPreset: localStorage.getItem("HEADER_PRESET") || "random" | |
| }; | |
| // Initialize Settings Inputs | |
| inputApiKey.value = config.apiKey; | |
| inputCustomGreeting.value = config.customGreeting; | |
| selectHeaderPreset.value = config.headerPreset; | |
| // Set Dynamic host URLs based on window.location | |
| const host = window.location.origin; | |
| urlLatest.value = `${host}/api/latest`; | |
| urlGenerate.value = `${host}/api/generate`; | |
| // Funny quotes to rotate during loading | |
| const loadingQuotes = [ | |
| "「正在池塘裡培育第一朵金蓮花...」", | |
| "「正在將太陽緩緩拉出地平線...」", | |
| "「正在調配紅、橙、黃、綠、藍、靛、紫飽和度...」", | |
| "「正在使用 90 年代頂級繪圖軟體套用漸層濾鏡...」", | |
| "「正在祈求大慈大悲觀世音菩薩加持...」", | |
| "「正在用微軟正黑體與標楷體在畫布邊緣雕琢...」", | |
| "「正在呼叫隊友串接的通訊軟體 API Webhook...」", | |
| "「健康就是福,請稍候,認同請分享...」" | |
| ]; | |
| let loadingInterval = null; | |
| // Navigation Click Handlers | |
| btnSimulator.addEventListener("click", () => { | |
| switchTab(btnSimulator, sectionSimulator, "聊天指令模擬器", "在這裡模擬通訊軟體的對話,即時檢視生成的「早安圖」效果。"); | |
| }); | |
| btnDeveloper.addEventListener("click", () => { | |
| switchTab(btnDeveloper, sectionDeveloper, "FastAPI 串接中心", "隊友 (成員 A) 可以直接呼叫以下 FastAPI 端點,將同一張早安圖回傳至通訊軟體群組。"); | |
| }); | |
| btnHistory.addEventListener("click", () => { | |
| switchTab(btnHistory, sectionHistory, "歷史生成紀錄", "瀏覽過去所有生成且包含文字浮水印的早安長輩圖。"); | |
| fetchHistory(); | |
| }); | |
| function switchTab(activeBtn, activeSection, title, description) { | |
| // Toggle Nav Buttons | |
| [btnSimulator, btnDeveloper, btnHistory].forEach(btn => btn.classList.remove("active")); | |
| activeBtn.classList.add("active"); | |
| // Toggle Sections | |
| [sectionSimulator, sectionDeveloper, sectionHistory].forEach(sec => sec.classList.add("hidden")); | |
| activeSection.classList.remove("hidden"); | |
| // Update Title & Desc | |
| pageTitle.textContent = title; | |
| pageDesc.textContent = description; | |
| } | |
| // Modal Operations | |
| btnOpenSettings.addEventListener("click", () => settingsModal.classList.remove("hidden")); | |
| btnCloseSettings.addEventListener("click", () => settingsModal.classList.add("hidden")); | |
| settingsModal.addEventListener("click", (e) => { | |
| if (e.target === settingsModal) settingsModal.classList.add("hidden"); | |
| }); | |
| btnSaveSettings.addEventListener("click", () => { | |
| config.apiKey = inputApiKey.value.trim(); | |
| config.customGreeting = inputCustomGreeting.value.trim(); | |
| config.headerPreset = selectHeaderPreset.value; | |
| localStorage.setItem("GOOGLE_API_KEY", config.apiKey); | |
| localStorage.setItem("CUSTOM_GREETING", config.customGreeting); | |
| localStorage.setItem("HEADER_PRESET", config.headerPreset); | |
| settingsModal.classList.add("hidden"); | |
| showToast("🌸 設定已成功儲存!"); | |
| }); | |
| btnResetSettings.addEventListener("click", () => { | |
| inputApiKey.value = ""; | |
| inputCustomGreeting.value = ""; | |
| selectHeaderPreset.value = "random"; | |
| }); | |
| // Copy to clipboard helper | |
| btnCopies.forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| const targetId = btn.getAttribute("data-target"); | |
| const targetInput = document.getElementById(targetId); | |
| targetInput.select(); | |
| document.execCommand("copy"); | |
| btn.classList.add("copied"); | |
| btn.innerHTML = `<i class="fa-solid fa-check"></i> 已複製`; | |
| setTimeout(() => { | |
| btn.classList.remove("copied"); | |
| btn.innerHTML = `<i class="fa-regular fa-copy"></i> 複製`; | |
| }, 2000); | |
| }); | |
| }); | |
| // Developer Code Tabs | |
| tabBtns.forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| tabBtns.forEach(b => b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| const tabId = btn.getAttribute("data-tab"); | |
| tabContents.forEach(c => { | |
| if (c.id === tabId) { | |
| c.classList.remove("hidden"); | |
| } else { | |
| c.classList.add("hidden"); | |
| } | |
| }); | |
| }); | |
| }); | |
| // Send Message / Simulate command | |
| btnSendMessage.addEventListener("click", sendSimulatedMessage); | |
| chatInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") sendSimulatedMessage(); | |
| }); | |
| btnShowApiDetails.addEventListener("click", () => { | |
| btnDeveloper.click(); | |
| }); | |
| function getFormattedTime() { | |
| const now = new Date(); | |
| return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| } | |
| function addMessageToChat(avatar, text, isUser = false, imageUrl = null) { | |
| const msgDiv = document.createElement("div"); | |
| msgDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`; | |
| let content = `<p>${text}</p>`; | |
| if (imageUrl) { | |
| content += `<img src="${imageUrl}" class="chat-result-img" alt="Good Morning Image">`; | |
| } | |
| msgDiv.innerHTML = ` | |
| <div class="avatar">${avatar}</div> | |
| <div class="msg-bubble"> | |
| ${content} | |
| <span class="time">${getFormattedTime()}</span> | |
| </div> | |
| `; | |
| chatMessagesContainer.appendChild(msgDiv); | |
| chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight; | |
| } | |
| function startLoadingAnimation() { | |
| previewEmptyState.classList.add("hidden"); | |
| previewImage.classList.add("hidden"); | |
| previewMetadataPanel.classList.add("hidden"); | |
| previewLoadingState.classList.remove("hidden"); | |
| let quoteIndex = 0; | |
| loadingStepText.textContent = "正在聯絡擴散模型伺服器..."; | |
| loadingFunnyQuote.textContent = loadingQuotes[quoteIndex]; | |
| loadingInterval = setInterval(() => { | |
| quoteIndex = (quoteIndex + 1) % loadingQuotes.length; | |
| loadingFunnyQuote.textContent = loadingQuotes[quoteIndex]; | |
| if (quoteIndex === 2) { | |
| loadingStepText.textContent = "正在將提示詞轉譯為大自然蓮花之氣..."; | |
| } else if (quoteIndex === 4) { | |
| loadingStepText.textContent = "Stable Diffusion 3.5 努力生圖中..."; | |
| } else if (quoteIndex === 6) { | |
| loadingStepText.textContent = "Pillow 正在雕琢霓虹漸層大字..."; | |
| } | |
| }, 3000); | |
| } | |
| function stopLoadingAnimation() { | |
| if (loadingInterval) { | |
| clearInterval(loadingInterval); | |
| loadingInterval = null; | |
| } | |
| previewLoadingState.classList.add("hidden"); | |
| } | |
| function sendSimulatedMessage() { | |
| let text = chatInput.value.trim(); | |
| if (!text) return; | |
| // Auto format command prefix if not typed | |
| if (!text.startsWith("@create image")) { | |
| text = `@create image ${text}`; | |
| } | |
| // Add user message to chat | |
| addMessageToChat("👤", text, true); | |
| chatInput.value = ""; | |
| // Add a placeholder bot typing message | |
| const botTypingDiv = document.createElement("div"); | |
| botTypingDiv.className = "message bot-message"; | |
| botTypingDiv.id = "bot-typing-placeholder"; | |
| botTypingDiv.innerHTML = ` | |
| <div class="avatar">🤖</div> | |
| <div class="msg-bubble"> | |
| <p><i class="fa-solid fa-spinner fa-spin"></i> 正在吸收指令能量,著手繪製早安圖...</p> | |
| <span class="time">${getFormattedTime()}</span> | |
| </div> | |
| `; | |
| chatMessagesContainer.appendChild(botTypingDiv); | |
| chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight; | |
| // Start Inspector Loader | |
| startLoadingAnimation(); | |
| // Prepare request payload | |
| const payload = { | |
| message: text, | |
| api_key: config.apiKey || null, | |
| custom_greeting: config.customGreeting || null | |
| }; | |
| // Call FastAPI generate endpoint | |
| fetch("/api/generate", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify(payload) | |
| }) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error("HTTP error " + response.status); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| // Remove typing placeholder | |
| const placeholder = document.getElementById("bot-typing-placeholder"); | |
| if (placeholder) placeholder.remove(); | |
| stopLoadingAnimation(); | |
| if (data.success) { | |
| // Display result in chat simulator | |
| addMessageToChat("🤖", `早安圖製作完成!祝您健康快樂,認同請分享!`, false, data.image_base64); | |
| // Update Inspector Panel (Phase 1 Preview) | |
| previewImage.src = data.image_base64; | |
| previewImage.classList.remove("hidden"); | |
| metaQuery.textContent = data.query; | |
| metaGreeting.textContent = data.greeting_text; | |
| metaPrompt.textContent = data.expanded_prompt; | |
| // Set download link | |
| btnDownload.href = data.image_base64; | |
| btnDownload.download = `morning_card_${data.id}.jpg`; | |
| previewMetadataPanel.classList.remove("hidden"); | |
| showToast("🌸 早安圖生成成功!"); | |
| } else { | |
| throw new Error(data.error || "未知錯誤"); | |
| } | |
| }) | |
| .catch(err => { | |
| // Remove typing placeholder | |
| const placeholder = document.getElementById("bot-typing-placeholder"); | |
| if (placeholder) placeholder.remove(); | |
| stopLoadingAnimation(); | |
| previewEmptyState.classList.remove("hidden"); | |
| addMessageToChat("🤖", `❌ 生成失敗:${err.message}。請確認您的網路連線或 API Key 設定。`, false); | |
| showToast("❌ 生成失敗,請檢查設定", "error"); | |
| }); | |
| } | |
| // Fetch History List | |
| function fetchHistory() { | |
| historyGridContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fa-solid fa-circle-notch fa-spin"></i> | |
| <p>載入生成紀錄中...</p> | |
| </div> | |
| `; | |
| fetch("/api/history") | |
| .then(res => res.json()) | |
| .then(data => { | |
| historyGridContainer.innerHTML = ""; | |
| historyCountText.textContent = `共 ${data.length} 張早安圖`; | |
| if (data.length === 0) { | |
| historyGridContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fa-regular fa-folder-open"></i> | |
| <p>尚無歷史生成紀錄</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| data.forEach(item => { | |
| const date = new Date(item.created_at).toLocaleString(); | |
| const card = document.createElement("div"); | |
| card.className = "history-item"; | |
| card.innerHTML = ` | |
| <div class="history-img-wrapper"> | |
| <img src="/api/image/${item.id}" alt="長輩圖" loading="lazy"> | |
| </div> | |
| <div class="history-info"> | |
| <div class="history-title">主題: ${item.query}</div> | |
| <div class="history-text">${item.greeting_text}</div> | |
| <div class="history-footer"> | |
| <span>ID: ${item.id.slice(0, 8)}...</span> | |
| <span>${date}</span> | |
| </div> | |
| </div> | |
| `; | |
| card.addEventListener("click", () => { | |
| // Switch to simulator view to inspect this historical image | |
| switchTab(btnSimulator, sectionSimulator, "聊天指令模擬器", "檢視歷史長輩圖的詳細提示詞與大字配置。"); | |
| // Show in preview canvas | |
| previewEmptyState.classList.add("hidden"); | |
| previewImage.src = `/api/image/${item.id}`; | |
| previewImage.classList.remove("hidden"); | |
| metaQuery.textContent = item.query; | |
| metaGreeting.textContent = item.greeting_text; | |
| metaPrompt.textContent = item.expanded_prompt; | |
| btnDownload.href = `/api/image/${item.id}`; | |
| btnDownload.download = `morning_card_${item.id}.jpg`; | |
| previewMetadataPanel.classList.remove("hidden"); | |
| }); | |
| historyGridContainer.appendChild(card); | |
| }); | |
| }) | |
| .catch(err => { | |
| historyGridContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fa-solid fa-triangle-exclamation"></i> | |
| <p>載入失敗: ${err.message}</p> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // Helper: Toast message | |
| function showToast(message, type = "success") { | |
| const toast = document.createElement("div"); | |
| toast.className = `toast-notification ${type}`; | |
| toast.style.position = "fixed"; | |
| toast.style.bottom = "20px"; | |
| toast.style.right = "20px"; | |
| toast.style.background = type === "success" ? "rgba(16, 185, 129, 0.95)" : "rgba(239, 68, 68, 0.95)"; | |
| toast.style.color = "white"; | |
| toast.style.padding = "12px 24px"; | |
| toast.style.borderRadius = "8px"; | |
| toast.style.zIndex = "1000"; | |
| toast.style.boxShadow = "0 4px 15px rgba(0, 0, 0, 0.2)"; | |
| toast.style.fontFamily = "inherit"; | |
| toast.style.fontSize = "14px"; | |
| toast.style.fontWeight = "600"; | |
| toast.style.backdropFilter = "blur(8px)"; | |
| toast.style.transition = "all 0.3s ease"; | |
| toast.style.transform = "translateY(50px)"; | |
| toast.style.opacity = "0"; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| // Trigger reflow | |
| toast.offsetHeight; | |
| toast.style.transform = "translateY(0)"; | |
| toast.style.opacity = "1"; | |
| setTimeout(() => { | |
| toast.style.transform = "translateY(50px)"; | |
| toast.style.opacity = "0"; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| }); | |