Team Ai
Apppublic

CatThanapha/integer

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
integer.with_ai.html601 linesDownload Raw Back to root
1<!DOCTYPE html>
2<html lang="th">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>🕵️‍♂️ Detective Equation — ภารกิจตามหาสมการที่หายไป</title>
7
8    <!-- ─────────── CDN Libraries ─────────── -->
9    <!-- Tailwind CSS -->
10    <script src="https://cdn.tailwindcss.com"></script>
11    
12    <!-- Google Fonts -->
13    <link href="https://fonts.googleapis.com/css2?family=Sarabun:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
14    
15    <!-- Font Awesome -->
16    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
17    
18    <!-- KaTeX (Module M1) -->
19    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
20    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
21    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
22    
23    <!-- MathLive (Module M1) -->
24    <script defer src="https://unpkg.com/mathlive"></script>
25    
26    <!-- DOMPurify (Module M3) -->
27    <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js"></script>
28
29    <style>
30        :root {
31            --primary: #1d4ed8;
32            --primary-mid: #2563eb;
33            --bg: #f8fafc;
34            --card-bg: #ffffff;
35            --text: #0f172a;
36            --border: #e2e8f0;
37        }
38
39        body {
40            font-family: 'Sarabun', sans-serif;
41            background: var(--bg);
42            color: var(--text);
43            margin: 0;
44            height: 100vh;
45            display: flex;
46            flex-direction: column;
47        }
48
49        /* ── Header ── */
50        .hero {
51            background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
52            position: relative;
53            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
54        }
55
56        /* ── Chat & Scenario Area ── */
57        .main-container {
58            display: flex;
59            flex-direction: column;
60            flex: 1;
61            overflow: hidden;
62        }
63        
64        @media (min-width: 1024px) {
65            .main-container {
66                flex-direction: row;
67            }
68        }
69
70        .panel {
71            flex: 1;
72            display: flex;
73            flex-direction: column;
74            overflow-y: auto;
75            padding: 1.5rem;
76        }
77
78        .scenario-panel {
79            background: var(--card-bg);
80            border-right: 1px solid var(--border);
81        }
82
83        .chat-panel {
84            background: #f1f5f9;
85        }
86
87        /* ── Chat Messages ── */
88        .chat-msg {
89            max-width: 85%;
90            padding: 1rem;
91            border-radius: 1rem;
92            margin-bottom: 1rem;
93            line-height: 1.6;
94            animation: fadeIn 0.3s ease-out;
95        }
96        
97        .msg-ai {
98            background: white;
99            border: 1px solid var(--border);
100            border-bottom-left-radius: 0.25rem;
101            align-self: flex-start;
102        }
103
104        .msg-user {
105            background: var(--primary);
106            color: white;
107            border-bottom-right-radius: 0.25rem;
108            align-self: flex-end;
109        }
110
111        .messages-wrapper {
112            flex: 1;
113            overflow-y: auto;
114            display: flex;
115            flex-direction: column;
116            padding-right: 0.5rem;
117        }
118
119        /* ── Spinner ── */
120        .loader {
121            border: 3px solid #f3f3f3;
122            border-top: 3px solid var(--primary);
123            border-radius: 50%;
124            width: 24px;
125            height: 24px;
126            animation: spin 1s linear infinite;
127            display: inline-block;
128        }
129
130        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
131        @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
132
133        /* Options */
134        .option-btn {
135            border: 2px solid var(--border);
136            border-radius: 0.75rem;
137            padding: 1rem;
138            text-align: left;
139            transition: all 0.2s;
140        }
141        .option-btn:hover:not(:disabled) {
142            border-color: var(--primary-mid);
143            background: #eff6ff;
144        }
145        .option-btn.selected-correct {
146            border-color: #22c55e;
147            background: #dcfce7;
148        }
149        .option-btn.selected-wrong {
150            border-color: #ef4444;
151            background: #fee2e2;
152        }
153    </style>
154</head>
155
156<body>
157
158    <!-- ── Header ── -->
159    <header class="hero text-white py-4 px-6 flex justify-between items-center z-10 shrink-0">
160        <div class="flex items-center gap-4">
161            <div class="bg-indigo-900 rounded-full w-12 h-12 flex items-center justify-center text-2xl border-2 border-indigo-400">
162                🕵️‍♂️
163            </div>
164            <div>
165                <h1 class="text-xl font-bold tracking-tight">นักสืบสมการ (Detective Equation)</h1>
166                <p class="text-indigo-200 text-sm">ภารกิจ: ตามหาสมการที่หายไป</p>
167            </div>
168        </div>
169        <button onclick="openSettings()" class="text-indigo-200 hover:text-white transition text-xl p-2">
170            <i class="fa-solid fa-gear"></i>
171        </button>
172    </header>
173
174    <!-- ── Mission Progress (Module B) ── -->
175    <div class="bg-indigo-50 border-b border-indigo-100 px-6 py-3 flex items-center gap-4 shrink-0">
176        <span class="font-bold text-indigo-900 text-sm min-w-[max-content]"><i class="fa-solid fa-map-location-dot"></i> ความคืบหน้าคดี:</span>
177        <div class="w-full bg-indigo-200 rounded-full h-4 overflow-hidden relative">
178            <div id="missionProgress" class="bg-indigo-600 h-4 rounded-full transition-all duration-500 w-0"></div>
179        </div>
180        <span id="missionText" class="text-sm font-semibold text-indigo-700 min-w-[max-content]">0 / 3 ภารกิจ</span>
181    </div>
182
183    <!-- ── Main Area ── -->
184    <main class="main-container">
185        
186        <!-- Left Panel: Scenario (Module F) -->
187        <div class="panel scenario-panel">
188            <div class="flex justify-between items-center mb-6">
189                <h2 class="text-lg font-bold text-gray-800"><i class="fa-solid fa-file-signature text-blue-600"></i> แฟ้มคดีปริศนา (โจทย์ปัจจุบัน)</h2>
190                <!-- Module F: Infinite Scenario Generator -->
191                <button onclick="generateNewScenario()" id="btnNewScenario" class="bg-blue-100 text-blue-700 hover:bg-blue-200 px-4 py-2 rounded-lg font-semibold text-sm transition flex items-center gap-2 shadow-sm">
192                    <i class="fa-solid fa-arrows-rotate"></i> สร้างโจทย์ใหม่
193                </button>
194            </div>
195
196            <div id="scenarioContainer" class="flex-1 flex flex-col items-center justify-center text-center p-4">
197                <div class="text-gray-400 mb-4">
198                    <i class="fa-solid fa-folder-open text-6xl mb-2"></i>
199                    <p>กด "สร้างโจทย์ใหม่" เพื่อเริ่มแฟ้มคดี</p>
200                </div>
201            </div>
202        </div>
203
204        <!-- Right Panel: Chatbot Agent (Module B & D) -->
205        <div class="panel chat-panel relative">
206            <div class="messages-wrapper" id="chatMessages">
207                <!-- Messages will be injected here via renderMathFeedback() -->
208            </div>
209
210            <!-- Chat Input -->
211            <div class="mt-4 bg-white rounded-xl border border-gray-300 p-2 flex items-center gap-2 shadow-sm shrink-0">
212                <input type="text" id="chatInput" placeholder="ถามเบาะแสเพิ่มเติมกับนักสืบสมการ..." 
213                    class="flex-1 px-3 py-2 outline-none text-gray-700 bg-transparent"
214                    onkeypress="if(event.key === 'Enter') sendUserMessage()">
215                <button onclick="sendUserMessage()" class="bg-blue-600 hover:bg-blue-700 text-white w-10 h-10 rounded-lg flex items-center justify-center transition">
216                    <i class="fa-solid fa-paper-plane"></i>
217                </button>
218            </div>
219        </div>
220
221    </main>
222
223    <!-- ── Settings Modal (Universal API Caller) ── -->
224    <div id="settingsModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
225        <div class="bg-white rounded-2xl max-w-md w-full p-6 shadow-2xl relative">
226            <h3 class="text-xl font-bold mb-4 flex items-center gap-2"><i class="fa-solid fa-key text-yellow-500"></i> ตั้งค่า API (LLM Integration)</h3>
227            
228            <div class="mb-4">
229                <label class="block text-sm font-semibold text-gray-700 mb-1">Gemini API Key</label>
230                <input type="password" id="inputApiKey" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 outline-none" placeholder="AIzaSy...">
231                <p class="text-xs text-gray-500 mt-1">ถูกบันทึกไว้ใน localStorage ของเบราว์เซอร์คุณเท่านั้น</p>
232            </div>
233
234            <div class="mb-6">
235                <label class="block text-sm font-semibold text-gray-700 mb-1">Model Name</label>
236                <input type="text" id="inputModelName" value="gemini-2.0-flash" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 outline-none">
237            </div>
238
239            <div class="flex justify-end gap-3">
240                <button onclick="closeSettings()" id="btnCancelSettings" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg font-medium transition">ยกเลิก</button>
241                <button onclick="saveSettings()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition shadow-md">บันทึก</button>
242            </div>
243        </div>
244    </div>
245
246    <!-- ── JavaScript Logic ── -->
247    <script>
248        /* 
249         * 1. 🛠️ Web Components สำหรับ Math Display (Module M3)
250         * สร้าง custom elements <math-span> และ <math-div> ให้เรนเดอร์ KaTeX อัตโนมัติ
251         */
252        class MathSpan extends HTMLElement {
253            connectedCallback() {
254                // Inline math
255                katex.render(this.textContent, this, { throwOnError: false, displayMode: false });
256            }
257        }
258        class MathDiv extends HTMLElement {
259            connectedCallback() {
260                // Block math
261                katex.render(this.textContent, this, { throwOnError: false, displayMode: true });
262            }
263        }
264        customElements.define('math-span', MathSpan);
265        customElements.define('math-div', MathDiv);
266
267        /* 
268         * 2. 🛠️ State Management 
269         */
270        let currentScenario = null;
271        let missionSolved = 0;
272        const totalMissions = 3;
273
274        /* 
275         * 3. 🛠️ Settings & Universal API Caller 
276         */
277        function openSettings() {
278            document.getElementById('settingsModal').classList.remove('hidden');
279            document.getElementById('inputApiKey').value = localStorage.getItem('geminiApiKey') || '';
280            document.getElementById('inputModelName').value = localStorage.getItem('geminiModel') || 'gemini-2.0-flash';
281            
282            // บังคับตั้งค่าถ้าไม่มี Key
283            if(!localStorage.getItem('geminiApiKey')) {
284                document.getElementById('btnCancelSettings').style.display = 'none';
285            } else {
286                document.getElementById('btnCancelSettings').style.display = 'block';
287            }
288        }
289
290        function closeSettings() {
291            if(!localStorage.getItem('geminiApiKey')) return; // ห้ามปิดถ้าไม่มี Key
292            document.getElementById('settingsModal').classList.add('hidden');
293        }
294
295        function saveSettings() {
296            const key = document.getElementById('inputApiKey').value.trim();
297            const model = document.getElementById('inputModelName').value.trim();
298            if(key) {
299                localStorage.setItem('geminiApiKey', key);
300                localStorage.setItem('geminiModel', model || 'gemini-2.0-flash');
301                closeSettings();
302                
303                // ถ้ายกเลิกการแสดงผลเริ่มต้น ให้ทักทาย
304                if(document.getElementById('chatMessages').children.length === 0) {
305                    appendSystemGreeting();
306                }
307            } else {
308                alert("กรุณาใส่ API Key");
309            }
310        }
311
312        // ระบบ Socratic Method และ Constraints (1.3)
313        const SYSTEM_INSTRUCTION = `
314คุณคือ "สารวัตรสมการ" (Detective Equation) สวมบทบาทนักสืบชั้นยอดแห่งกรมตำรวจคณิตศาสตร์ In-Character ตลอดเวลา
315เป้าหมาย: พานักเรียนทำภารกิจไขคดี "การหาสมการ" ให้สำเร็จ
316
317Math-Specific Pedagogy Constraints (ข้อบังคับเด็ดขาด):
3181. Socratic Method: ห้ามบอกเฉลยตรงๆ ทันที ให้ใช้วิธีตั้งคำถามย้อนกลับเพื่อให้นักเรียนค้นพบคำตอบด้วยตัวเอง
3192. Productive Struggle: ปล่อยให้นักเรียนได้พยายามและทำผิดพลาดก่อน หากผิดให้ไกด์ถึง Misconception อย่างนุ่มนวล
3203. PDPA: ห้ามบันทึก ขอ หรืออ้างอิงข้อมูลส่วนตัว/ชื่อจริงของนักเรียนเด็ดขาด
321
322รูปแบบ:
323- ใช้ภาษาไทย สุภาพ เป็นกันเอง มีโทนของนักสืบ (เช่น "เอาล่ะ คู่หู...", "มาดูเบาะแสกัน...")
324- ใช้ $...$ สำหรับสมการบรรทัดเดียวกัน และ $$...$$ สำหรับสมการแยกบรรทัด
325`;
326
327        // Universal API Caller ฟังก์ชันหลัก
328        async function callLLM(prompt, isJsonMode = false) {
329            const apiKey = localStorage.getItem('geminiApiKey');
330            const modelName = localStorage.getItem('geminiModel') || 'gemini-2.0-flash';
331            if (!apiKey) {
332                openSettings();
333                throw new Error("Missing API Key");
334            }
335
336            const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;
337            
338            const payload = {
339                system_instruction: { parts: [{ text: SYSTEM_INSTRUCTION }] },
340                contents: [{ role: "user", parts: [{ text: prompt }] }],
341                generationConfig: {
342                    temperature: isJsonMode ? 0.2 : 0.7,
343                }
344            };
345
346            if(isJsonMode) {
347                payload.generationConfig.responseMimeType = "application/json";
348            }
349
350            try {
351                const response = await fetch(endpoint, {
352                    method: 'POST',
353                    headers: { 'Content-Type': 'application/json' },
354                    body: JSON.stringify(payload)
355                });
356
357                const data = await response.json();
358                if (!response.ok) {
359                    throw new Error(data.error?.message || "API Error");
360                }
361                return data.candidates[0].content.parts[0].text;
362            } catch (error) {
363                console.error("LLM API Error:", error);
364                alert(`เกิดข้อผิดพลาดในการเชื่อมต่อ API: \n${error.message}`);
365                return null;
366            }
367        }
368
369        /* 
370         * 4. 🛠️ Module F: Infinite Scenario Generator 
371         */
372        async function generateNewScenario() {
373            const btn = document.getElementById('btnNewScenario');
374            const container = document.getElementById('scenarioContainer');
375            
376            btn.disabled = true;
377            btn.innerHTML = `<span class="loader w-4 h-4 border-2"></span> กำลังสร้างเบาะแส...`;
378            container.innerHTML = `<div class="flex flex-col items-center text-blue-500"><span class="loader mb-4"></span><p>สารวัตรกำลังเตรียมแฟ้มคดีใหม่...</p></div>`;
379
380            const prompt = `
381สร้างโจทย์คณิตศาสตร์ 1 ข้อ เรื่อง "การหาสมการ" (เช่น หาสมการเส้นตรงจากจุด, หาสมการพหุนาม, ฯลฯ) ระดับมัธยม
382บังคับ: ส่งคืนข้อมูลเป็น Strict JSON format เท่านั้น ห้ามมีข้อความอื่นปน
383Schema:
384{
385    "question": "ข้อความโจทย์ปัญหา (ใช้ $...$ สำหรับสมการในโจทย์)",
386    "mainEquation": "สมการหลักที่เกี่ยวข้องหรือสูตรตั้งต้น (ถ้ามี)",
387    "options": ["ตัวเลือก 1", "ตัวเลือก 2", "ตัวเลือก 3", "ตัวเลือก 4"],
388    "correctIndex": 0 // index ของคำตอบที่ถูกต้อง (0 ถึง 3)
389}`;
390
391            const responseText = await callLLM(prompt, true);
392            
393            if (responseText) {
394                try {
395                    // ทำความสะอาด JSON ในกรณีที่ติด Markdown backticks มา
396                    let cleanJson = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
397                    currentScenario = JSON.parse(cleanJson);
398                    renderScenarioUI();
399                    appendAiMessage(`แฟ้มคดีใหม่พร้อมแล้วคู่หู! ลองดูเบาะแสบนหน้ากระดานแล้วเลือกคำตอบที่คิดว่าใช่ดูสิ`);
400                } catch (e) {
401                    console.error("JSON Parse Error:", e);
402                    container.innerHTML = `<div class="text-red-500"><i class="fa-solid fa-triangle-exclamation text-3xl mb-2"></i><br>เกิดข้อผิดพลาดในการสร้างโจทย์ โปรดลองอีกครั้ง</div>`;
403                }
404            }
405            
406            btn.disabled = false;
407            btn.innerHTML = `<i class="fa-solid fa-arrows-rotate"></i> สร้างโจทย์ใหม่`;
408        }
409
410        function renderScenarioUI() {
411            if(!currentScenario) return;
412            const container = document.getElementById('scenarioContainer');
413            
414            // สร้าง HTML สำหรับโจทย์ โดยใช้ DOMPurify (Module M3) และ custom element
415            let html = `
416                <div class="bg-blue-50 rounded-xl p-6 w-full shadow-sm text-left border border-blue-100 mb-4">
417                    <h3 class="text-lg font-bold text-blue-900 mb-3"><i class="fa-solid fa-magnifying-glass"></i> คดีปริศนา</h3>
418                    <p class="text-gray-800 text-lg leading-relaxed mb-4" id="qText">${currentScenario.question}</p>
419                    ${currentScenario.mainEquation ? `<div class="bg-white p-3 rounded-lg border text-center mb-4"><math-div mode="displaystyle">${currentScenario.mainEquation}</math-div></div>` : ''}
420                </div>
421                <div class="grid grid-cols-1 md:grid-cols-2 gap-3 w-full" id="optionsGrid">
422            `;
423
424            currentScenario.options.forEach((opt, idx) => {
425                html += `
426                    <button onclick="handleAnswer(${idx}, this)" class="option-btn bg-white w-full shadow-sm flex items-center gap-3">
427                        <div class="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center font-bold text-gray-500 shrink-0 border border-gray-300">
428                            ${String.fromCharCode(65 + idx)}
429                        </div>
430                        <div class="flex-1 text-gray-700 text-left">${opt}</div>
431                    </button>
432                `;
433            });
434
435            html += `</div>`;
436            
437            // M1 & M3: ใช้ renderMathFeedback จัดการ XSS และ Render Math
438            renderMathFeedback(container, html);
439        }
440
441        /* 
442         * 5. 🛠️ Module D: Explain "Why I'm Wrong" & Answer Logic
443         */
444        function handleAnswer(selectedIndex, btnElement) {
445            const isCorrect = (selectedIndex === currentScenario.correctIndex);
446            const grid = document.getElementById('optionsGrid');
447            const buttons = grid.querySelectorAll('.option-btn');
448            
449            // ปิดปุ่มทั้งหมด
450            buttons.forEach(b => b.disabled = true);
451
452            if (isCorrect) {
453                btnElement.classList.add('selected-correct');
454                btnElement.innerHTML += `<i class="fa-solid fa-circle-check text-green-500 text-xl ml-auto"></i>`;
455                updateMissionProgress(1);
456                appendAiMessage(`ยอดเยี่ยมมาก! นั่นคือคำตอบที่ถูกต้อง คดีนี้ถูกไขกระจ่างแล้ว 🎉<br>กด <b>"สร้างโจทย์ใหม่"</b> เพื่อสืบคดีต่อไป!`);
457            } else {
458                btnElement.classList.add('selected-wrong');
459                btnElement.innerHTML += `<i class="fa-solid fa-circle-xmark text-red-500 text-xl ml-auto"></i>`;
460                
461                // Highlight ข้อที่ถูกให้เห็น
462                buttons[currentScenario.correctIndex].classList.add('selected-correct', 'opacity-50');
463                
464                // แทรกปุ่ม "ทำไมข้อนี้ถึงผิด?" ใต้ Options
465                const explanationDiv = document.createElement('div');
466                explanationDiv.className = "w-full mt-4 text-center fade-in";
467                explanationDiv.innerHTML = `
468                    <button onclick="explainWhyWrong(${selectedIndex})" class="bg-amber-100 text-amber-800 hover:bg-amber-200 border border-amber-300 px-5 py-2 rounded-full font-bold shadow-sm transition">
469                        ❓ ทำไมข้อนี้ถึงผิด? (ถามสารวัตร)
470                    </button>
471                `;
472                document.getElementById('scenarioContainer').appendChild(explanationDiv);
473            }
474        }
475
476        async function explainWhyWrong(wrongIndex) {
477            const wrongText = currentScenario.options[wrongIndex];
478            const correctText = currentScenario.options[currentScenario.correctIndex];
479            
480            appendUserMessage(`ทำไมตัวเลือก "${wrongText}" ถึงผิดครับ/ค่ะ?`);
481            showTypingIndicator();
482
483            const prompt = `นักเรียนเลือกคำตอบผิดคือ "${wrongText}" จากคำถาม: "${currentScenario.question}" (คำตอบที่ถูกคือ "${correctText}")
484หน้าที่ของคุณ (สารวัตรสมการ):
4851. อธิบาย Misconception หรือจุดที่มักเข้าใจผิดเกี่ยวกับสมการในตัวเลือกนี้
4862. อธิบายกระบวนการคิดที่นำไปสู่คำตอบที่ถูกต้อง โดยยกตัวอย่างรูปธรรม
4873. ใช้ Socratic Method ถามนำในตอนท้ายเพื่อทดสอบความเข้าใจ
4884. ห้ามดุ ให้กำลังใจในฐานะคู่หูนักสืบ`;
489
490            const reply = await callLLM(prompt, false);
491            removeTypingIndicator();
492            if (reply) appendAiMessage(reply);
493        }
494
495        /* 
496         * 6. 🛠️ Chat Interface Logic 
497         */
498        async function sendUserMessage() {
499            const input = document.getElementById('chatInput');
500            const text = input.value.trim();
501            if(!text) return;
502
503            appendUserMessage(text);
504            input.value = '';
505            
506            showTypingIndicator();
507            const reply = await callLLM(`บริบทการสนทนา: ${text}`, false);
508            removeTypingIndicator();
509            
510            if (reply) appendAiMessage(reply);
511        }
512
513        function appendSystemGreeting() {
514            appendAiMessage(`สวัสดีคู่หู! ผมคือ <b>สารวัตรสมการ</b> ยินดีต้อนรับสู่กองสืบสวนคณิตศาสตร์ 🕵️‍♂️<br><br>คดีวันนี้คือการ "ตามหาสมการที่หายไป" คุณพร้อมที่จะสืบหาความจริงแล้วหรือยัง? กดปุ่ม <span class="bg-blue-100 text-blue-800 px-2 py-0.5 rounded text-xs font-bold"><i class="fa-solid fa-arrows-rotate"></i> สร้างโจทย์ใหม่</span> ด้านซ้ายมือเพื่อเริ่มคดีแรกได้เลย!`);
515        }
516
517        function appendUserMessage(text) {
518            const wrapper = document.getElementById('chatMessages');
519            const msgDiv = document.createElement('div');
520            msgDiv.className = 'chat-msg msg-user';
521            
522            // XSS Protection for User Input
523            msgDiv.textContent = text; 
524            wrapper.appendChild(msgDiv);
525            wrapper.scrollTop = wrapper.scrollHeight;
526        }
527
528        function appendAiMessage(htmlText) {
529            const wrapper = document.getElementById('chatMessages');
530            const msgDiv = document.createElement('div');
531            msgDiv.className = 'chat-msg msg-ai shadow-sm';
532            
533            wrapper.appendChild(msgDiv);
534            // เรนเดอร์ Math และ Sanitize (Module M1 & M3)
535            renderMathFeedback(msgDiv, htmlText);
536            wrapper.scrollTop = wrapper.scrollHeight;
537        }
538
539        function showTypingIndicator() {
540            const wrapper = document.getElementById('chatMessages');
541            const typingDiv = document.createElement('div');
542            typingDiv.id = 'typingIndicator';
543            typingDiv.className = 'chat-msg msg-ai text-gray-400 flex gap-1 items-center py-3';
544            typingDiv.innerHTML = `<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></span><span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></span><span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></span>`;
545            wrapper.appendChild(typingDiv);
546            wrapper.scrollTop = wrapper.scrollHeight;
547        }
548
549        function removeTypingIndicator() {
550            const el = document.getElementById('typingIndicator');
551            if(el) el.remove();
552        }
553
554        /* 
555         * 7. 🛠️ Module M1 & M3: Math Rendering Engine & Sanitization
556         */
557        function renderMathFeedback(container, htmlText) {
558            // M3: ป้องกัน XSS
559            container.innerHTML = DOMPurify.sanitize(htmlText, {
560                ADD_TAGS: ['math-span', 'math-div'] // อนุญาต Custom Elements
561            });
562            
563            // M1: แปลง $...$ และ $$...$$ ให้เป็นสมการ
564            renderMathInElement(container, {
565                delimiters: [
566                    {left: '$$', right: '$$', display: true},
567                    {left: '$', right: '$', display: false}
568                ],
569                throwOnError: false,
570                errorColor: '#ef4444'
571            });
572        }
573
574        /* 
575         * 8. 🛠️ Module B: Progress System 
576         */
577        function updateMissionProgress(add) {
578            missionSolved += add;
579            const percentage = Math.min((missionSolved / totalMissions) * 100, 100);
580            document.getElementById('missionProgress').style.width = percentage + '%';
581            document.getElementById('missionText').innerText = `${missionSolved} / ${totalMissions} ภารกิจ`;
582            
583            if (missionSolved >= totalMissions) {
584                setTimeout(() => {
585                    appendAiMessage(`🌟 <b>สุดยอดมาก คู่หู!</b> เราไขรหัสผ่านตู้เซฟแห่งสมการได้ครบ ${totalMissions} คดีแล้ว คดีนี้ปิดแฟ้มได้อย่างสวยงาม!`);
586                }, 1000);
587            }
588        }
589
590        // Initialize App
591        window.onload = () => {
592            if(!localStorage.getItem('geminiApiKey')) {
593                openSettings();
594            } else {
595                appendSystemGreeting();
596            }
597        };
598
599    </script>
600</body>
601</html>