Wisetsut/integer-web-app
0
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">×</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">×</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>