Team Ai
Apppublic

Wisetsut/integer-web-app

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
integerwai.html689 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, maximum-scale=1.0, user-scalable=yes, shrink-to-fit=no">
6  <title>การบวกจำนวนเต็ม • ม.1 | สื่อการสอนพร้อม AI Assistant</title>
7  
8  <!-- Tailwind CSS -->
9  <script src="https://cdn.tailwindcss.com"></script>
10  
11  <!-- Font Sarabun -->
12  <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">
13  
14  <!-- Module M1 & M3: Math Rendering Stack -->
15  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
16  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
17  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
18  
19  <!-- DOMPurify -->
20  <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js"></script>
21  
22  <!-- Marked.js (For Markdown Parsing) -->
23  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
24
25  <style>
26    body {
27      font-family: 'Sarabun', sans-serif;
28      background: linear-gradient(135deg, #f3e8ff 0%, #faf5ff 50%, #ffffff 100%);
29      min-height: 100vh;
30    }
31    
32    /* Animations */
33    .fade-in-up { animation: fadeInUp 0.6s ease-out forwards; opacity: 0; transform: translateY(20px); }
34    @keyframes fadeInUp { to { opacity: 1; transform: translateY(0); } }
35    .delay-1 { animation-delay: 0.2s; }
36    .delay-2 { animation-delay: 0.4s; }
37    .delay-3 { animation-delay: 0.6s; }
38
39    /* UI Components */
40    .card {
41      background: rgba(255, 255, 255, 0.85);
42      backdrop-filter: blur(12px);
43      border-radius: 1.5rem;
44      border: 1px solid rgba(196, 181, 253, 0.5);
45      box-shadow: 0 20px 40px -12px rgba(124, 58, 237, 0.15);
46    }
47    .btn-primary {
48      background: linear-gradient(120deg, #7c3aed, #a78bfa);
49      transition: all 0.3s;
50      border: none;
51      cursor: pointer;
52    }
53    .btn-primary:hover {
54      background: linear-gradient(120deg, #6d28d9, #8b5cf6);
55      box-shadow: 0 8px 20px -6px rgba(124, 58, 237, 0.5);
56      transform: scale(1.03);
57    }
58    .feedback-correct { background: #d1fae5; border-left: 4px solid #10b981; animation: slideIn 0.4s ease-out; }
59    .feedback-incorrect { background: #fee2e2; border-left: 4px solid #ef4444; animation: slideIn 0.4s ease-out; }
60    .feedback-ai { background: #eff6ff; border-left: 4px solid #3b82f6; animation: slideIn 0.4s ease-out; }
61    
62    @keyframes slideIn {
63      from { opacity: 0; transform: translateX(-10px); }
64      to { opacity: 1; transform: translateX(0); }
65    }
66
67    /* Math Content Styles within AI Response */
68    .ai-response-content p { margin-bottom: 0.75rem; }
69    .ai-response-content ul { list-style-type: disc; margin-left: 1.5rem; margin-bottom: 0.75rem; }
70    .ai-response-content ol { list-style-type: decimal; margin-left: 1.5rem; margin-bottom: 0.75rem; }
71    
72    /* Module B: Chatbot UI */
73    #chatbot-widget {
74      transition: all 0.3s ease-in-out;
75      transform-origin: bottom right;
76    }
77    .chat-bubble { max-width: 85%; }
78    .chat-user { background: #7c3aed; color: white; border-radius: 1rem 1rem 0 1rem; }
79    .chat-ai { background: #f3f4f6; color: #1f2937; border-radius: 1rem 1rem 1rem 0; }
80  </style>
81
82  <script>
83    // Module M3: Static Math Web Components
84    class MathSpan extends HTMLElement {
85      connectedCallback() {
86        if(window.katex) {
87          katex.render(this.textContent, this, { throwOnError: false, displayMode: false });
88        }
89      }
90    }
91    class MathDiv extends HTMLElement {
92      connectedCallback() {
93        if(window.katex) {
94          katex.render(this.textContent, this, { throwOnError: false, displayMode: true });
95        }
96      }
97    }
98    customElements.define('math-span', MathSpan);
99    customElements.define('math-div', MathDiv);
100  </script>
101</head>
102<body class="antialiased text-gray-800 p-4 md:p-6 pb-32">
103
104  <!-- ================= Settings & Security Overlay ================= -->
105  <div id="settings-overlay" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center hidden backdrop-blur-sm">
106    <div class="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl transform transition-all scale-95 opacity-0" id="settings-modal">
107      <div class="flex justify-between items-center mb-4">
108        <h2 class="text-2xl font-bold text-purple-900">⚙️ ตั้งค่า AI API</h2>
109        <button onclick="closeSettings()" class="text-gray-400 hover:text-red-500 text-xl font-bold">&times;</button>
110      </div>
111      <p class="text-sm text-gray-600 mb-4">กรุณาตั้งค่า API เพื่อใช้งานฟีเจอร์ AI (ข้อมูลบันทึกในเครื่องคุณเท่านั้น)</p>
112      
113      <div class="space-y-4">
114        <div>
115          <label class="block text-sm font-semibold text-gray-700 mb-1">Provider</label>
116          <select id="api-provider" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-400" onchange="toggleProviderSettings()">
117            <option value="gemini">Google Gemini</option>
118            <option value="openai">OpenAI Compatible (เช่น Typhoon)</option>
119          </select>
120        </div>
121        
122        <div>
123          <label class="block text-sm font-semibold text-gray-700 mb-1">API Key</label>
124          <input type="password" id="api-key" placeholder="วาง API Key ที่นี่..." class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-400">
125        </div>
126        
127        <div>
128          <label class="block text-sm font-semibold text-gray-700 mb-1">Model Name</label>
129          <input type="text" id="api-model" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-400">
130        </div>
131        
132        <div id="base-url-container" class="hidden">
133          <label class="block text-sm font-semibold text-gray-700 mb-1">Base URL</label>
134          <input type="text" id="api-baseurl" placeholder="https://api.opentyphoon.ai/v1" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-400">
135        </div>
136
137        <button onclick="saveSettings()" class="w-full btn-primary text-white font-bold py-3 rounded-xl mt-2">
138          💾 บันทึกการตั้งค่า
139        </button>
140      </div>
141    </div>
142  </div>
143
144  <!-- Header -->
145  <div class="max-w-5xl mx-auto text-center mb-6 fade-in-up relative">
146    <button onclick="openSettings()" class="absolute right-0 top-0 text-2xl text-purple-400 hover:text-purple-700 transition">⚙️</button>
147    <span class="text-4xl md:text-5xl">➕</span>
148    <h1 class="text-3xl md:text-4xl font-bold text-purple-900 mt-2">
149      การบวกจำนวนเต็ม <span class="text-purple-600">ม.1</span>
150    </h1>
151    <p class="text-purple-500 text-lg mt-1">🔢 เรียนรู้ผ่านกิจกรรม พร้อม AI ช่วยไกด์ความคิด</p>
152  </div>
153
154  <main class="max-w-5xl mx-auto space-y-8">
155    
156    <!-- ==================== ขั้นนำ ==================== -->
157    <section class="card p-6 md:p-8 fade-in-up delay-1">
158      <div class="flex items-center gap-3 mb-4">
159        <span class="bg-purple-600 text-white w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg shadow-lg">1</span>
160        <h2 class="text-2xl font-bold text-purple-900">ขั้นนำ : สถานการณ์รอบตัว</h2>
161      </div>
162      
163      <div class="bg-purple-50 p-4 rounded-xl border border-purple-200 mb-5">
164        <p class="text-gray-700 leading-relaxed">
165          สมมติว่าคุณมีเงิน <math-span>200</math-span> บาท แต่ต้องจ่ายค่าขนมให้เพื่อน <math-span>350</math-span> บาท 
166          สถานะการเงินของคุณจะเป็นอย่างไร? เขียนเป็นประโยคสัญลักษณ์ได้ว่า <math-span>200 + (-350) = ?</math-span>
167        </p>
168      </div>
169    </section>
170
171    <!-- ==================== ขั้นสอน ==================== -->
172    <section class="card p-6 md:p-8 fade-in-up delay-2">
173      <div class="flex items-center gap-3 mb-4">
174        <span class="bg-purple-600 text-white w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg shadow-lg">2</span>
175        <h2 class="text-2xl font-bold text-purple-900">ขั้นสอน : 4 กรณีการบวก</h2>
176      </div>
177      
178      <div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
179        <!-- กรณีตัวอย่างใช้ Math Web Component -->
180        <div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition">
181          <h3 class="text-lg font-bold text-green-700">บวก + บวก</h3>
182          <math-div>(+4) + (+3) = +7</math-div>
183        </div>
184        <div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition">
185          <h3 class="text-lg font-bold text-red-700">ลบ + ลบ</h3>
186          <math-div>(-5) + (-2) = -7</math-div>
187        </div>
188        <div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition">
189          <h3 class="text-lg font-bold text-blue-700">บวก + ลบ</h3>
190          <math-div>(+8) + (-3) = +5</math-div>
191        </div>
192        <div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition">
193          <h3 class="text-lg font-bold text-indigo-700">ลบ + บวก</h3>
194          <math-div>(-7) + (+4) = -3</math-div>
195        </div>
196      </div>
197    </section>
198
199    <!-- ==================== ขั้นสรุป & แบบฝึกหัด (Module D & F) ==================== -->
200    <section class="card p-6 md:p-8 fade-in-up delay-3">
201      <div class="flex items-center gap-3 mb-6">
202        <span class="bg-purple-600 text-white w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg shadow-lg">3</span>
203        <h2 class="text-2xl font-bold text-purple-900">แบบฝึกหัดท้าทาย</h2>
204      </div>
205
206      <div class="space-y-6" id="exercise-container">
207        
208        <!-- Exercise Item (Original Structure) -->
209        <div class="bg-white p-5 rounded-xl border border-purple-200 shadow-sm relative group" id="q-block-1">
210          <div class="absolute top-4 right-4">
211            <!-- Module F: Infinite Scenario Generator Button -->
212            <button onclick="generateNewScenario('q-block-1')" class="text-purple-500 hover:text-purple-800 text-sm flex items-center gap-1 bg-purple-50 px-2 py-1 rounded border border-purple-100 transition">
213              <span id="spin-btn-1">🔄</span> สร้างโจทย์ใหม่
214            </button>
215          </div>
216          
217          <p class="font-semibold text-gray-800 w-5/6" id="q-text-1">อุณหภูมิที่ขั้วโลกเหนือตอนเที่ยงคืนวัดได้ -25°C อีก 6 ชั่วโมงต่อมาอุณหภูมิลดลงอีก 13°C อุณหภูมิ ณ เวลานั้นเป็นเท่าใด?</p>
218          
219          <div class="mt-4 flex flex-wrap items-center gap-2" id="q-input-area-1">
220            <input type="text" id="ans-1" class="flex-1 min-w-[150px] p-2 border border-purple-200 rounded-lg focus:ring-2 focus:ring-purple-400" placeholder="คำตอบ (ตัวเลข)...">
221            <button class="btn-primary text-white px-4 py-2 rounded-full font-semibold text-sm" onclick="checkAnswer(1, '-38')">✅ ตรวจ</button>
222          </div>
223          
224          <div id="fb-1" class="mt-3 hidden"></div>
225        </div>
226
227        <!-- Exercise 2 -->
228        <div class="bg-white p-5 rounded-xl border border-purple-200 shadow-sm relative group" id="q-block-2">
229          <div class="absolute top-4 right-4">
230            <button onclick="generateNewScenario('q-block-2')" class="text-purple-500 hover:text-purple-800 text-sm flex items-center gap-1 bg-purple-50 px-2 py-1 rounded border border-purple-100 transition">
231              <span id="spin-btn-2">🔄</span> สร้างโจทย์ใหม่
232            </button>
233          </div>
234          <p class="font-semibold text-gray-800 w-5/6" id="q-text-2">นักดำน้ำอยู่ที่ระดับความลึก 15 เมตรจากผิวน้ำ (-15) จากนั้นว่ายขึ้นมา 9 เมตร (+9) ตอนนี้เขาอยู่ที่ระดับใด?</p>
235          
236          <div class="mt-4 flex flex-wrap items-center gap-2" id="q-input-area-2">
237            <input type="text" id="ans-2" class="flex-1 min-w-[150px] p-2 border border-purple-200 rounded-lg focus:ring-2 focus:ring-purple-400" placeholder="คำตอบ (ตัวเลข)...">
238            <button class="btn-primary text-white px-4 py-2 rounded-full font-semibold text-sm" onclick="checkAnswer(2, '-6')">✅ ตรวจ</button>
239          </div>
240          <div id="fb-2" class="mt-3 hidden"></div>
241        </div>
242
243      </div>
244    </section>
245  </main>
246
247  <!-- ==================== Module B: Roleplay Chatbot ==================== -->
248  <div id="chatbot-widget" class="fixed bottom-6 right-6 w-16 h-16 z-40 flex items-end justify-end">
249    <!-- Chat Window -->
250    <div id="chat-window" class="bg-white rounded-2xl shadow-2xl border border-gray-200 w-80 md:w-96 h-[450px] flex flex-col hidden absolute bottom-20 right-0 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
251      <div class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-4 font-bold flex justify-between items-center">
252        <div class="flex items-center gap-2">
253          <span class="text-2xl">⚓</span>
254          <span>กัปตันแมท (เรือดำน้ำ)</span>
255        </div>
256        <button onclick="toggleChat()" class="text-white hover:text-gray-200 text-xl">&times;</button>
257      </div>
258      
259      <!-- Progress Mission -->
260      <div class="bg-indigo-50 px-4 py-2 border-b border-indigo-100 flex items-center gap-2 text-xs font-semibold text-indigo-800">
261        <span>ภารกิจ: สำรวจร่องลึก</span>
262        <div class="flex-1 bg-gray-200 h-2 rounded-full overflow-hidden">
263          <div class="bg-indigo-500 h-full w-1/3"></div>
264        </div>
265      </div>
266
267      <div id="chat-messages" class="flex-1 p-4 overflow-y-auto space-y-3 bg-gray-50 text-sm flex flex-col">
268        <div class="chat-bubble chat-ai p-3 self-start shadow-sm">
269          สวัสดีลูกเรือ! ฉันกัปตันแมท วันนี้เราจะดำดิ่งสำรวจโลกจำนวนเต็มกัน พร้อมจะแก้ปัญหาระดับความลึกแล้วหรือยัง? ลองทักทายมาได้เลย!
270        </div>
271      </div>
272
273      <div class="p-3 bg-white border-t border-gray-200 flex gap-2 items-center">
274        <input type="text" id="chat-input" class="flex-1 p-2 rounded-xl border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-400 text-sm" placeholder="พิมพ์คุยกับกัปตัน..." onkeypress="handleChatEnter(event)">
275        <button onclick="sendChatMessage()" class="bg-purple-600 text-white p-2 rounded-full hover:bg-purple-700 transition">
276          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" /></svg>
277        </button>
278      </div>
279    </div>
280
281    <!-- Floating Button -->
282    <button onclick="toggleChat()" class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white w-16 h-16 rounded-full shadow-2xl flex items-center justify-center hover:scale-110 transition-transform focus:outline-none">
283      <span class="text-2xl">🤖</span>
284    </button>
285  </div>
286
287  <script>
288    // ================= Core Settings & Setup =================
289    const PEDAGOGY_RULES = `คุณคือ AI Pedagogy Specialist ด้านคณิตศาสตร์ 
290กฎเหล็ก (ห้ามฝ่าฝืน):
2911. Socratic Method: ห้ามบอกเฉลยคำตอบสุดท้ายตรงๆ ทันที ให้ตั้งคำถามกลับเพื่อไกด์ความคิดเสมอ
2922. Productive Struggle: ปล่อยให้ผู้เรียนได้พยายามคิดและลองผิดลองถูก
2933. PDPA: ห้ามถามหรือจัดเก็บข้อมูลส่วนตัวของผู้เรียนเด็ดขาด
2944. รูปแบบคณิตศาสตร์: ให้ใช้ KaTeX format เสมอ โดยใช้ $สมการ$ สำหรับ inline และ $$สมการ$$ สำหรับ block
2955. ภาษา: ใช้ภาษาไทยที่เป็นมิตร ให้กำลังใจ และเข้าใจง่าย`;
296
297    function getSettings() {
298      return {
299        provider: localStorage.getItem('math_ai_provider') || 'gemini',
300        apiKey: localStorage.getItem('math_ai_apikey') || '',
301        model: localStorage.getItem('math_ai_model') || 'gemini-3.1-flash-preview',
302        baseUrl: localStorage.getItem('math_ai_baseurl') || 'https://api.opentyphoon.ai/v1'
303      };
304    }
305
306    function checkSetup() {
307      const config = getSettings();
308      if (!config.apiKey) openSettings();
309    }
310
311    function openSettings() {
312      const modal = document.getElementById('settings-modal');
313      const overlay = document.getElementById('settings-overlay');
314      const config = getSettings();
315      
316      document.getElementById('api-provider').value = config.provider;
317      document.getElementById('api-key').value = config.apiKey;
318      document.getElementById('api-model').value = config.model;
319      document.getElementById('api-baseurl').value = config.baseUrl;
320      
321      toggleProviderSettings();
322      
323      overlay.classList.remove('hidden');
324      setTimeout(() => {
325        modal.classList.remove('scale-95', 'opacity-0');
326        modal.classList.add('scale-100', 'opacity-100');
327      }, 10);
328    }
329
330    function closeSettings() {
331      const modal = document.getElementById('settings-modal');
332      modal.classList.remove('scale-100', 'opacity-100');
333      modal.classList.add('scale-95', 'opacity-0');
334      setTimeout(() => {
335        document.getElementById('settings-overlay').classList.add('hidden');
336      }, 300);
337    }
338
339    function toggleProviderSettings() {
340      const provider = document.getElementById('api-provider').value;
341      const baseUrlContainer = document.getElementById('base-url-container');
342      const modelInput = document.getElementById('api-model');
343      
344      if (provider === 'openai') {
345        baseUrlContainer.classList.remove('hidden');
346        if(modelInput.value === 'gemini-3.1-flash-preview' || modelInput.value === '') {
347          modelInput.value = 'typhoon-v2.5-30b-a3b-instruct';
348        }
349      } else {
350        baseUrlContainer.classList.add('hidden');
351        if(modelInput.value === 'typhoon-v2.5-30b-a3b-instruct' || modelInput.value === '') {
352          modelInput.value = 'gemini-3.1-flash-preview';
353        }
354      }
355    }
356
357    function saveSettings() {
358      localStorage.setItem('math_ai_provider', document.getElementById('api-provider').value);
359      localStorage.setItem('math_ai_apikey', document.getElementById('api-key').value);
360      localStorage.setItem('math_ai_model', document.getElementById('api-model').value);
361      localStorage.setItem('math_ai_baseurl', document.getElementById('api-baseurl').value);
362      closeSettings();
363    }
364
365    // ================= Universal API Caller =================
366    async function callLLM(promptText, systemInstruction, requireJson = false) {
367      const config = getSettings();
368      if (!config.apiKey) {
369        openSettings();
370        throw new Error("กรุณาตั้งค่า API Key ก่อนใช้งาน");
371      }
372
373      const fullSystemPrompt = `${PEDAGOGY_RULES}\n\n${systemInstruction}`;
374
375      try {
376        if (config.provider === 'gemini') {
377          // Gemini API Call
378          const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
379          
380          const payload = {
381            contents: [{ role: "user", parts: [{ text: promptText }] }],
382            systemInstruction: { parts: [{ text: fullSystemPrompt }] }
383          };
384
385          if (requireJson) {
386            payload.generationConfig = { responseMimeType: "application/json" };
387          }
388
389          const response = await fetch(url, {
390            method: 'POST',
391            headers: { 'Content-Type': 'application/json' },
392            body: JSON.stringify(payload)
393          });
394          
395          const data = await response.json();
396          if(!response.ok) throw new Error(data.error?.message || "Gemini API Error");
397          
398          return data.candidates[0].content.parts[0].text;
399
400        } else {
401          // OpenAI Compatible API Call (Typhoon, etc.)
402          const url = `${config.baseUrl}/chat/completions`;
403          
404          const payload = {
405            model: config.model,
406            messages: [
407              { role: "system", content: fullSystemPrompt },
408              { role: "user", content: requireJson ? promptText + "\n\nCRITICAL: Output ONLY raw valid JSON." : promptText }
409            ]
410          };
411
412          const response = await fetch(url, {
413            method: 'POST',
414            headers: { 
415              'Content-Type': 'application/json',
416              'Authorization': `Bearer ${config.apiKey}`
417            },
418            body: JSON.stringify(payload)
419          });
420
421          const data = await response.json();
422          if(!response.ok) throw new Error(data.error?.message || "OpenAI API Error");
423          
424          let text = data.choices[0].message.content;
425          
426          if(requireJson) {
427             // Extract JSON if model wrapped it in markdown codeblocks
428             const match = text.match(/\{[\s\S]*\}/);
429             if(match) text = match[0];
430          }
431          
432          return text;
433        }
434      } catch (err) {
435        console.error("LLM Call failed:", err);
436        throw err;
437      }
438    }
439
440    // ================= Module M1 & M3: DOMPurify + KaTeX Feedback =================
441    function renderMathFeedback(containerId, markdownText, isError = false) {
442      const container = document.getElementById(containerId);
443      container.classList.remove('hidden');
444      
445      // Add visual styling
446      container.className = `mt-3 p-4 rounded-xl text-sm ai-response-content ${isError ? 'feedback-incorrect' : 'feedback-ai'}`;
447      
448      // Parse Markdown -> HTML safely
449      const rawHtml = marked.parse(markdownText);
450      const cleanHtml = DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
451      
452      container.innerHTML = cleanHtml;
453      
454      // Render Math
455      if(window.renderMathInElement) {
456        renderMathInElement(container, {
457          delimiters: [
458            {left: '$$', right: '$$', display: true},
459            {left: '$', right: '$', display: false}
460          ],
461          throwOnError: false
462        });
463      }
464    }
465
466    // Spinner Generator
467    function getLoadingHTML(text) {
468      return `<div class="flex items-center gap-2 text-blue-600 font-semibold">
469                <svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
470                  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
471                  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
472                </svg>
473                ${text}
474              </div>`;
475    }
476
477    // ================= Checking Answers & Module D =================
478    function checkAnswer(id, expected) {
479      const ansInput = document.getElementById(`ans-${id}`);
480      const val = ansInput.value.trim();
481      const fb = document.getElementById(`fb-${id}`);
482      const qText = document.getElementById(`q-text-${id}`).innerText;
483      
484      fb.classList.remove('hidden');
485      
486      if (!val) {
487        fb.className = 'mt-3 feedback-incorrect p-3 rounded-lg text-sm';
488        fb.innerHTML = '⚠️ กรุณาใส่คำตอบก่อนครับ';
489        return;
490      }
491
492      if (val === expected) {
493        fb.className = 'mt-3 feedback-correct p-3 rounded-lg text-sm';
494        fb.innerHTML = '✅ ถูกต้องเก่งมากครับ!';
495      } else {
496        // Module D: Show "Why I'm wrong?" button
497        fb.className = 'mt-3 feedback-incorrect p-3 rounded-lg text-sm flex justify-between items-center';
498        fb.innerHTML = `
499          <span>❌ ยังไม่ถูกนะ ลองคิดดูใหม่</span>
500          <button onclick="explainMistake('${id}', '${qText}', '${val}')" class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-full text-xs font-bold transition flex items-center gap-1 shadow-md">
501            ❓ ทำไมข้อนี้ถึงผิด?
502          </button>
503        `;
504      }
505    }
506
507    // Module D: AI Explains Misconception Socraticly
508    async function explainMistake(id, questionText, studentAns) {
509      const fb = document.getElementById(`fb-${id}`);
510      fb.innerHTML = getLoadingHTML("AI กำลังวิเคราะห์ความคิดของคุณ...");
511      
512      const sysInstruction = `นักเรียนตอบผิดในข้อคณิตศาสตร์
513วิเคราะห์ Misconception ที่ทำให้เขาตอบ ${studentAns} อธิบายว่ากระบวนการคิดนั้นผิดอย่างไรในบริบทของจำนวนเต็ม แล้วไกด์ให้เขาเห็นจุดผิดของตัวเองโดยไม่บอกคำตอบตรงๆ ทันที ยกตัวอย่างรูปธรรมประกอบ`;
514      const prompt = `โจทย์: ${questionText}\nคำตอบของนักเรียน: ${studentAns}`;
515
516      try {
517        const aiResponse = await callLLM(prompt, sysInstruction);
518        renderMathFeedback(`fb-${id}`, `💡 **AI วิเคราะห์:**\n\n${aiResponse}`);
519      } catch (err) {
520        fb.innerHTML = `❌ Error: ${err.message}`;
521      }
522    }
523
524    // ================= Module F: Infinite Scenario Generator =================
525    async function generateNewScenario(blockId) {
526      const idNum = blockId.split('-')[2];
527      const oldText = document.getElementById(`q-text-${idNum}`).innerText;
528      const fb = document.getElementById(`fb-${idNum}`);
529      const spinBtn = document.getElementById(`spin-btn-${idNum}`);
530      const inputArea = document.getElementById(`q-input-area-${idNum}`);
531      
532      spinBtn.classList.add('animate-spin');
533      fb.classList.remove('hidden');
534      fb.className = 'mt-3 p-3 rounded-lg text-sm bg-purple-50 text-purple-700';
535      fb.innerHTML = getLoadingHTML("AI กำลังสร้างสถานการณ์ใหม่ที่ท้าทาย...");
536      inputArea.classList.add('hidden'); // Hide input while loading
537
538      const sysInstruction = `คุณคือผู้เชี่ยวชาญการสร้างข้อสอบ สร้างโจทย์คณิตศาสตร์ 1 ข้อ เรื่อง 'การบวกจำนวนเต็ม' โดยอ้างอิงระดับความยากและโครงสร้างบริบทใกล้เคียงกับโจทย์เดิม
539สำคัญมาก: ต้องตอบกลับเป็น JSON Format ที่มี Key ดังนี้เท่านั้น (ไม่ต้องมี Markdown ครอบ)
540{
541  "question": "ข้อความโจทย์ใหม่",
542  "options": ["A) ตัวเลือก1", "B) ตัวเลือก2", "C) ตัวเลือก3", "D) ตัวเลือก4"],
543  "correct": "A, B, C หรือ D",
544  "explanation": "คำใบ้หรือเหตุผล (ห้ามเฉลยตรงๆ)"
545}`;
546
547      try {
548        const jsonString = await callLLM(`โจทย์ต้นฉบับ: ${oldText}`, sysInstruction, true);
549        const newData = JSON.parse(jsonString);
550        
551        // Render New Scenario (Multiple Choice format based on Module F strict spec)
552        document.getElementById(`q-text-${idNum}`).innerText = newData.question;
553        
554        let optionsHtml = '<div class="grid grid-cols-1 md:grid-cols-2 gap-2 w-full mt-2">';
555        newData.options.forEach((opt, idx) => {
556           const val = ['A','B','C','D'][idx];
557           optionsHtml += `
558             <button onclick="checkMultipleChoice('${idNum}', '${val}', '${newData.correct}', '${encodeURIComponent(newData.explanation)}')" 
559                     class="text-left p-3 border border-purple-200 rounded-lg hover:bg-purple-100 transition focus:ring-2 focus:ring-purple-400">
560               ${opt}
561             </button>
562           `;
563        });
564        optionsHtml += '</div>';
565        
566        inputArea.innerHTML = optionsHtml;
567        inputArea.classList.remove('hidden');
568        fb.classList.add('hidden');
569
570      } catch (err) {
571        fb.innerHTML = `❌ เกิดข้อผิดพลาดในการสร้างโจทย์: ${err.message}`;
572        inputArea.classList.remove('hidden');
573      } finally {
574        spinBtn.classList.remove('animate-spin');
575      }
576    }
577
578    function checkMultipleChoice(id, selected, correct, encodedExplanation) {
579      const fb = document.getElementById(`fb-${id}`);
580      const explanation = decodeURIComponent(encodedExplanation);
581      
582      if(selected === correct) {
583        renderMathFeedback(`fb-${id}`, `✅ **ถูกต้อง!** ยอดเยี่ยมมาก\n\n${explanation}`);
584      } else {
585        renderMathFeedback(`fb-${id}`, `❌ **ยังไม่ถูก** ลองพิจารณาคำใบ้นี้ดู:\n\n${explanation}`, true);
586      }
587    }
588
589    // ================= Module B: Roleplay Chatbot =================
590    let chatHistory = [];
591    
592    function toggleChat() {
593      const windowEl = document.getElementById('chat-window');
594      if (windowEl.classList.contains('hidden')) {
595        windowEl.classList.remove('hidden');
596        setTimeout(() => {
597          windowEl.classList.remove('scale-95', 'opacity-0');
598        }, 10);
599      } else {
600        windowEl.classList.add('scale-95', 'opacity-0');
601        setTimeout(() => {
602          windowEl.classList.add('hidden');
603        }, 300);
604      }
605    }
606
607    function handleChatEnter(e) {
608      if (e.key === 'Enter') sendChatMessage();
609    }
610
611    async function sendChatMessage() {
612      const input = document.getElementById('chat-input');
613      const text = input.value.trim();
614      if (!text) return;
615
616      appendChatMessage(text, 'user');
617      input.value = '';
618      
619      const typingId = appendTypingIndicator();
620      const container = document.getElementById('chat-messages');
621      container.scrollTop = container.scrollHeight;
622
623      const sysInstruction = `คุณคือ 'กัปตันแมท' กัปตันเรือดำน้ำที่กำลังพาลูกเรือ (นักเรียน) สำรวจโลกใต้ทะเลลึก
624กติกา:
6251. In-Character เสมอ: ใช้ศัพท์เกี่ยวกับการเดินเรือ ระดับน้ำ (0 คือระดับผิวน้ำ, ติดลบคือใต้ทะเล, บวกคือบินขึ้นไป)
6262. มีภารกิจให้เด็กๆ ค่อยๆ คิด เช่น "ตอนนี้เราอยู่ระดับ -50 เมตร ถ้าดำลงไปอีก 20 เมตร เราจะอยู่ระดับไหน?"
6273. ใช้ Socratic Method และ Productive Struggle ห้ามเฉลยทันทีเด็ดขาด
6284. ใช้ KaTeX $...$ สำหรับตัวเลขและสมการ`;
629
630      // Build context history string (simplified for prompt)
631      let conversationContext = chatHistory.map(m => `${m.role === 'user' ? 'นักเรียน' : 'กัปตัน'}: ${m.text}`).join('\n');
632      let prompt = `บทสนทนาที่ผ่านมา:\n${conversationContext}\n\nนักเรียน: ${text}\n\nกัปตันแมท:`;
633
634      try {
635        const response = await callLLM(prompt, sysInstruction);
636        document.getElementById(typingId).remove();
637        appendChatMessage(response, 'ai');
638      } catch (err) {
639        document.getElementById(typingId).remove();
640        appendChatMessage(`(ระบบขัดข้อง: ${err.message})`, 'ai');
641      }
642    }
643
644    function appendChatMessage(text, sender) {
645      chatHistory.push({ role: sender, text: text });
646      
647      const container = document.getElementById('chat-messages');
648      const bubble = document.createElement('div');
649      
650      bubble.className = `chat-bubble p-3 shadow-sm text-sm ai-response-content ${sender === 'user' ? 'chat-user self-end' : 'chat-ai self-start'}`;
651      
652      // Parse markdown and math for AI messages
653      if (sender === 'ai') {
654        bubble.innerHTML = DOMPurify.sanitize(marked.parse(text));
655        if(window.renderMathInElement) {
656          renderMathInElement(bubble, {
657            delimiters: [{left: '$', right: '$', display: false}, {left: '$$', right: '$$', display: true}],
658            throwOnError: false
659          });
660        }
661      } else {
662        bubble.innerText = text;
663      }
664      
665      container.appendChild(bubble);
666      container.scrollTop = container.scrollHeight;
667    }
668
669    function appendTypingIndicator() {
670      const id = 'typing-' + Date.now();
671      const container = document.getElementById('chat-messages');
672      const bubble = document.createElement('div');
673      bubble.id = id;
674      bubble.className = 'chat-bubble chat-ai p-3 self-start shadow-sm flex gap-1 items-center';
675      bubble.innerHTML = `<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
676                          <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
677                          <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>`;
678      container.appendChild(bubble);
679      container.scrollTop = container.scrollHeight;
680      return id;
681    }
682
683    // Init App
684    window.onload = () => {
685      checkSetup();
686    };
687  </script>
688</body>
689</html>