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 = ` 已複製`; setTimeout(() => { btn.classList.remove("copied"); btn.innerHTML = ` 複製`; }, 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 = `

${text}

`; if (imageUrl) { content += `Good Morning Image`; } msgDiv.innerHTML = `
${avatar}
${content} ${getFormattedTime()}
`; 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 = `
🤖

正在吸收指令能量,著手繪製早安圖...

${getFormattedTime()}
`; 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 = `

載入生成紀錄中...

`; fetch("/api/history") .then(res => res.json()) .then(data => { historyGridContainer.innerHTML = ""; historyCountText.textContent = `共 ${data.length} 張早安圖`; if (data.length === 0) { historyGridContainer.innerHTML = `

尚無歷史生成紀錄

`; return; } data.forEach(item => { const date = new Date(item.created_at).toLocaleString(); const card = document.createElement("div"); card.className = "history-item"; card.innerHTML = `
長輩圖
主題: ${item.query}
${item.greeting_text}
`; 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 = `

載入失敗: ${err.message}

`; }); } // 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); } });