File size: 18,255 Bytes
b39cf15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
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);
    }
});