GoodMorning / static /app.js
PhilipL's picture
Upload 3 files
b39cf15 verified
Raw
History Blame Contribute Delete
18.3 kB
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);
}
});