AMAMnarin/integer-web-app-demo
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">
6 <title>🧮 การบวกจำนวนเต็ม | AI-Powered EdTech (Custom API)</title>
7
8 <!-- M1 & M3: Math Rendering & Sanitization Stack -->
9 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
10 <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
11 <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
12 <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
13 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
15 <style>
16 /* --- Original Styles --- */
17 * { box-sizing: border-box; margin: 0; padding: 0; }
18 body {
19 font-family: 'Sarabun', 'Kanit', sans-serif;
20 background: linear-gradient(145deg, #fff9f0 0%, #ffe8d6 100%);
21 min-height: 100vh;
22 padding: 20px;
23 display: flex;
24 flex-direction: column;
25 align-items: center;
26 }
27 .main-wrapper {
28 display: flex;
29 gap: 20px;
30 width: 100%;
31 max-width: 1200px;
32 align-items: flex-start;
33 }
34 .stream-container {
35 flex: 2;
36 background: white;
37 border-radius: 20px;
38 box-shadow: 0 10px 30px rgba(0,0,0,0.1);
39 overflow: hidden;
40 position: relative;
41 }
42 .stream-header { background: #f39c12; color: white; padding: 20px; text-align: center; }
43 .step-indicator { display: flex; justify-content: space-around; background: #fdebd0; padding: 15px 10px; }
44 .step-dot { display: flex; flex-direction: column; align-items: center; cursor: pointer; opacity: 0.6; transition: 0.2s; }
45 .step-dot.active { opacity: 1; font-weight: bold; transform: scale(1.05); }
46 .step-dot span:first-child { background: #e67e22; color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 5px; }
47 .step-dot.active span:first-child { background: #d35400; box-shadow: 0 0 10px #f39c12; }
48 .step-name { font-size: 0.8rem; color: #5d4037; }
49 .content-area { padding: 25px 30px; min-height: 400px; background: #fff; }
50 .step-section { display: none; animation: fadeSlide 0.4s ease; }
51 .step-section.active { display: block; }
52 @keyframes fadeSlide { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
53 h2 { color: #d35400; margin-bottom: 15px; font-size: 1.5rem; border-left: 6px solid #f39c12; padding-left: 15px; }
54 .example-box { background: #faf5ef; padding: 15px; border-radius: 15px; margin: 12px 0; }
55 details { margin: 10px 0; background: #fff; border: 1px solid #f1c40f; border-radius: 10px; padding: 10px 15px; cursor: pointer; }
56
57 /* --- Exercises --- */
58 .exercise-item { background: #fdf2e9; padding: 15px; border-radius: 14px; margin-bottom: 15px; }
59 .exercise-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
60 .exercise-item input { width: 90px; padding: 8px; border: 2px solid #f39c12; border-radius: 8px; text-align: center; }
61 .feedback { font-weight: bold; margin-left: 8px; }
62 .btn-check, .btn-action { background: #27ae60; color: white; border: none; padding: 10px 20px; border-radius: 20px; cursor: pointer; transition: 0.2s; font-family: inherit;}
63 .btn-check:hover { background: #2ecc71; }
64 .btn-action.secondary { background: #3498db; }
65 .btn-explain { background: #e74c3c; color: white; border: none; padding: 5px 12px; border-radius: 15px; cursor: pointer; font-size: 0.85rem; display: none; margin-top: 10px;}
66 .ai-feedback-box { background: #f0f8ff; border-left: 4px solid #3498db; padding: 15px; margin-top: 10px; border-radius: 0 10px 10px 0; display: none; font-size: 0.95rem;}
67
68 .nav-buttons { display: flex; justify-content: space-between; padding: 15px 30px; background: #f9f3e7; }
69 .nav-btn { background: #f39c12; border: none; color: white; padding: 10px 20px; border-radius: 20px; cursor: pointer; }
70 .nav-btn:disabled { background: #ccc; cursor: not-allowed; }
71
72 /* --- Settings Panel --- */
73 .settings-btn { position: fixed; bottom: 20px; right: 20px; background: #34495e; color: white; border: none; width: 50px; height: 50px; border-radius: 50%; font-size: 1.5rem; cursor: pointer; z-index: 1000; box-shadow: 0 4px 10px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center;}
74 .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: none; justify-content: center; align-items: center; z-index: 1001; }
75 .modal { background: white; padding: 25px; border-radius: 20px; width: 90%; max-width: 450px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); }
76 .modal h3 { margin-bottom: 20px; color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }
77 .field-group { margin-bottom: 15px; }
78 .field-group label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 0.9rem; color: #7f8c8d; }
79 .modal input, .modal select { width: 100%; padding: 10px; border: 1px solid #bdc3c7; border-radius: 8px; font-family: inherit; }
80 .modal .btn-save { width: 100%; background: #27ae60; color: white; padding: 12px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; margin-top: 10px; }
81 .modal .btn-close { width: 100%; background: #95a5a6; color: white; padding: 8px; border: none; border-radius: 8px; cursor: pointer; margin-top: 10px; }
82
83 /* --- Chat --- */
84 .chat-container { flex: 1; min-width: 300px; background: white; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); display: flex; flex-direction: column; height: 80vh; max-height: 700px; position: sticky; top: 20px;}
85 .chat-header { background: #2980b9; color: white; padding: 15px; border-radius: 20px 20px 0 0; text-align: center; }
86 .mission-progress { width: 100%; background: #1a5276; height: 8px; border-radius: 4px; margin-top: 10px; overflow: hidden;}
87 .progress-bar { width: 0%; height: 100%; background: #f1c40f; transition: width 0.5s; }
88 .chat-messages { flex: 1; padding: 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; background: #f4f6f7; }
89 .msg { padding: 10px 15px; border-radius: 15px; max-width: 85%; font-size: 0.95rem; line-height: 1.4;}
90 .msg.ai { background: white; align-self: flex-start; border-bottom-left-radius: 0; box-shadow: 0 2px 5px rgba(0,0,0,0.05); border: 1px solid #e5e8e8;}
91 .msg.user { background: #d6eaf8; align-self: flex-end; border-bottom-right-radius: 0; }
92 .chat-input { display: flex; padding: 15px; border-top: 1px solid #eee; background: white; border-radius: 0 0 20px 20px;}
93 .chat-input input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 20px; outline: none; }
94 .chat-input button { background: #2980b9; color: white; border: none; padding: 10px 15px; margin-left: 10px; border-radius: 50%; cursor: pointer; }
95
96 .spinner { width: 20px; height: 20px; border: 3px solid rgba(0,0,0,0.1); border-top-color: #3498db; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; vertical-align: middle; }
97 @keyframes spin { to { transform: rotate(360deg); } }
98
99 @media (max-width: 900px) {
100 .main-wrapper { flex-direction: column; }
101 .chat-container { width: 100%; height: 500px; position: static; }
102 }
103 </style>
104</head>
105<body>
106
107<!-- Settings Button -->
108<button class="settings-btn" onclick="toggleSettings(true)" title="ตั้งค่า API">⚙️</button>
109
110<!-- Settings Modal -->
111<div class="overlay" id="settingsOverlay">
112 <div class="modal">
113 <h3>⚙️ ตั้งค่าการเชื่อมต่อ AI</h3>
114
115 <div class="field-group">
116 <label>AI Provider</label>
117 <select id="apiProvider" onchange="handleProviderChange()">
118 <option value="google">Google Gemini</option>
119 <option value="openai">OpenAI (ChatGPT)</option>
120 </select>
121 </div>
122
123 <div class="field-group">
124 <label id="apiKeyLabel">Google API Key</label>
125 <input type="password" id="apiKeyInput" placeholder="ใส่รหัส API ของคุณที่นี่">
126 </div>
127
128 <div class="field-group">
129 <label>Model Name</label>
130 <input type="text" id="apiModelInput" placeholder="เช่น gemini-2.5-flash-preview-09-2025">
131 </div>
132
133 <p style="font-size: 0.75rem; color: #7f8c8d; margin-bottom: 15px;">* ข้อมูลถูกเก็บไว้ในเบราว์เซอร์ของคุณเท่านั้น ไม่มีการส่งไปที่อื่น</p>
134
135 <button class="btn-save" onclick="saveSettings()">💾 บันทึกการตั้งค่า</button>
136 <button class="btn-close" onclick="toggleSettings(false)">ปิด</button>
137 </div>
138</div>
139
140<div class="main-wrapper">
141 <!-- Main Math App Container -->
142 <div class="stream-container">
143 <div class="stream-header">
144 <h1>🧮 จำนวนเต็ม : การบวก</h1>
145 <p>เรียนรู้แบบ Interactive พร้อม AI ผู้ช่วยส่วนตัว</p>
146 </div>
147
148 <div class="step-indicator">
149 <div class="step-dot" onclick="changeStepTo(0)" id="dot0"><span>1</span><span class="step-name">ขั้นนำ</span></div>
150 <div class="step-dot" onclick="changeStepTo(1)" id="dot1"><span>2</span><span class="step-name">ขั้นสอน</span></div>
151 <div class="step-dot" onclick="changeStepTo(2)" id="dot2"><span>3</span><span class="step-name">ขั้นสรุป</span></div>
152 <div class="step-dot" onclick="changeStepTo(3)" id="dot3"><span>4</span><span class="step-name">แบบฝึกหัด</span></div>
153 </div>
154
155 <div class="content-area">
156 <div class="step-section" id="step0">
157 <h2>🌟 ขั้นนำ : เปิดโลกจำนวนเต็ม</h2>
158 <p>ในชีวิตเรามีทั้ง <strong>เพิ่มขึ้น</strong> และ <strong>ลดลง</strong> จำนวนเต็มช่วยบอกปริมาณและทิศทาง</p>
159 <div class="example-box">
160 <p>🏢 ลิฟต์: เริ่มชั้น G <math-span>0</math-span> ขึ้น 3 ชั้น <math-span>+3</math-span> แล้วลง 2 ชั้น <math-span>-2</math-span> → <math-span>+1</math-span></p>
161 <p>🌡️ อากาศ: <math-span>-3^\circ C</math-span> ร้อนขึ้น <math-span>5^\circ C</math-span> → <math-span>+2^\circ C</math-span></p>
162 </div>
163 </div>
164
165 <div class="step-section" id="step1">
166 <h2>📖 ขั้นสอน : หลักการบวกจำนวนเต็ม</h2>
167 <ul>
168 <li><strong>บวก + บวก</strong> → บวกปกติ</li>
169 <li><strong>ลบ + ลบ</strong> → บวกค่าสัมบูรณ์ แล้วใส่ลบ</li>
170 <li><strong>บวก + ลบ</strong> → เอาค่าสัมบูรณ์มากลบน้อย ใส่เครื่องหมายของตัวมาก</li>
171 </ul>
172 <div class="example-box">
173 <strong>🔎 Productive Struggle (ลองคิดก่อนกดดู)</strong>
174 <details>
175 <summary><math-span>(+4) + (-7) = ?</math-span></summary>
176 <p>เครื่องหมายต่าง → ลบกัน <math-span>7-4=3</math-span> → ใช้เครื่องหมายของตัวมาก(ลบ) → <math-span>-3</math-span></p>
177 </details>
178 <details>
179 <summary><math-span>(-2) + (-5) = ?</math-span></summary>
180 <p>เครื่องหมายเหมือน → บวกกัน <math-span>2+5=7</math-span> → ใส่ลบ → <math-span>-7</math-span></p>
181 </details>
182 </div>
183 </div>
184
185 <div class="step-section" id="step2">
186 <h2>📝 ขั้นสรุป</h2>
187 <math-div mode="displaystyle">a + (-b) = a - b</math-div>
188 <p style="text-align: center; margin-top: 10px;">✅ <strong>ขั้นตอน:</strong> ดูเครื่องหมาย → คิดค่าสัมบูรณ์ → คำนวณ → ใส่เครื่องหมาย</p>
189 </div>
190
191 <div class="step-section" id="step3">
192 <h2>✏️ แบบฝึกหัดท้าทาย</h2>
193 <div id="exerciseContainer"></div>
194 <div style="display: flex; gap: 10px; margin-top: 20px;">
195 <button class="btn-check" onclick="checkExercises()">✅ ตรวจคำตอบ</button>
196 <button class="btn-action secondary" onclick="generateNewQuestions()" id="btnGenerate">
197 <span id="genSpinner" class="spinner" style="display:none; border-top-color:white; margin-right:5px;"></span>
198 🔄 สร้างโจทย์ใหม่ด้วย AI
199 </button>
200 </div>
201 <div id="scoreDisplay" style="font-size: 1.2rem; font-weight: bold; margin-top: 15px;"></div>
202 </div>
203 </div>
204
205 <div class="nav-buttons">
206 <button class="nav-btn" id="prevBtn" onclick="changeStep(-1)">◀ ก่อนหน้า</button>
207 <span style="font-weight:bold;color:#d35400;" id="stepInfo">1 / 4</span>
208 <button class="nav-btn" id="nextBtn" onclick="changeStep(1)">ถัดไป ▶</button>
209 </div>
210 </div>
211
212 <!-- Chat -->
213 <div class="chat-container">
214 <div class="chat-header">
215 <h3>🤠 กัปตันจำนวนเต็ม</h3>
216 <p style="font-size: 0.8rem;">ภารกิจ: สำรวจถ้ำสมการ</p>
217 <div class="mission-progress"><div class="progress-bar" id="missionProgress"></div></div>
218 </div>
219 <div class="chat-messages" id="chatMessages">
220 <div class="msg ai">สวัสดีนักสำรวจ! ฉันคือกัปตันจำนวนเต็ม วันนี้เรามีภารกิจตามหาสมบัติในถ้ำสมการ เธอพร้อมจะใช้ความรู้เรื่องการบวกจำนวนเต็มมาช่วยไขปริศนาเปิดประตูถ้ำหรือยัง?</div>
221 </div>
222 <div class="chat-input">
223 <input type="text" id="chatInput" placeholder="พิมพ์โต้ตอบกับกัปตัน..." onkeypress="if(event.key === 'Enter') sendChatMessage()">
224 <button onclick="sendChatMessage()">➤</button>
225 </div>
226 </div>
227</div>
228
229<script>
230 /* ==========================================
231 SETTINGS & API MANAGEMENT
232 ========================================== */
233 let apiSettings = {
234 provider: 'google',
235 key: '',
236 model: 'gemini-2.5-flash-preview-09-2025'
237 };
238
239 function loadSettings() {
240 const saved = localStorage.getItem('math_ai_settings');
241 if (saved) {
242 apiSettings = JSON.parse(saved);
243 document.getElementById('apiProvider').value = apiSettings.provider;
244 document.getElementById('apiKeyInput').value = apiSettings.key;
245 document.getElementById('apiModelInput').value = apiSettings.model;
246 handleProviderChange();
247 }
248 }
249
250 function saveSettings() {
251 apiSettings.provider = document.getElementById('apiProvider').value;
252 apiSettings.key = document.getElementById('apiKeyInput').value;
253 apiSettings.model = document.getElementById('apiModelInput').value;
254
255 localStorage.setItem('math_ai_settings', JSON.stringify(apiSettings));
256 alert('✅ บันทึกการตั้งค่าเรียบร้อยแล้ว!');
257 toggleSettings(false);
258 }
259
260 function toggleSettings(show) {
261 document.getElementById('settingsOverlay').style.display = show ? 'flex' : 'none';
262 }
263
264 function handleProviderChange() {
265 const provider = document.getElementById('apiProvider').value;
266 const keyLabel = document.getElementById('apiKeyLabel');
267 const modelInput = document.getElementById('apiModelInput');
268
269 if (provider === 'google') {
270 keyLabel.innerText = "Google API Key";
271 if(!modelInput.value || modelInput.value.includes('gpt')) {
272 modelInput.value = "gemini-2.5-flash-preview-09-2025";
273 }
274 } else {
275 keyLabel.innerText = "OpenAI API Key";
276 if(!modelInput.value || modelInput.value.includes('gemini')) {
277 modelInput.value = "gpt-4o";
278 }
279 }
280 }
281
282 /* ==========================================
283 PEDAGOGY CONSTRAINTS
284 ========================================== */
285 const PEDAGOGY_SYSTEM_PROMPT = `
286คุณคือ AI ผู้ช่วยสอนวิชาคณิตศาสตร์ (Math EdTech Developer/Pedagogy Specialist)
287ข้อบังคับขั้นสูงสุด (Strict Constraints):
2881. **Socratic Method:** ห้ามบอกคำตอบสุดท้าย หรือเฉลยตรงๆ เด็ดขาด! ให้ใช้วิธีตั้งคำถามกลับเพื่อให้ผู้เรียนฉุกคิด
2892. **Productive Struggle:** ชื่นชมความพยายาม ให้คำใบ้ทีละนิด (Scaffolding) เพื่อให้เด็กแก้ปัญหาด้วยตัวเอง
2903. **Format:** สมการคณิตศาสตร์ต้องใช้ LaTeX format โดยหุ้มด้วย $ สำหรับ inline และ $$ สำหรับ block เสมอ
2914. ตอบเป็นภาษาไทยที่เป็นมิตร เข้าใจง่าย เหมาะกับเด็ก ม.1
292`;
293
294 /* ==========================================
295 UNIVERSAL API CALLER (Supports Google & OpenAI)
296 ========================================== */
297 async function callLLM(userPrompt, specificSystemInstruction, requireJSON = false, history = []) {
298 if (!apiSettings.key) {
299 toggleSettings(true);
300 throw new Error("กรุณาใส่ API Key ในการตั้งค่าก่อนใช้งาน");
301 }
302
303 const finalSystemInstruction = PEDAGOGY_SYSTEM_PROMPT + "\n\n" + specificSystemInstruction;
304
305 if (apiSettings.provider === 'google') {
306 return callGemini(userPrompt, finalSystemInstruction, requireJSON, history);
307 } else {
308 return callOpenAI(userPrompt, finalSystemInstruction, requireJSON, history);
309 }
310 }
311
312 async function callGemini(userPrompt, sysInst, requireJSON, history) {
313 const url = `https://generativelanguage.googleapis.com/v1beta/models/${apiSettings.model}:generateContent?key=${apiSettings.key}`;
314
315 let contents = history.map(msg => ({
316 role: msg.role === 'ai' ? 'model' : 'user',
317 parts: [{ text: msg.text }]
318 }));
319 contents.push({ role: 'user', parts: [{ text: userPrompt }] });
320
321 const payload = {
322 systemInstruction: { parts: [{ text: sysInst }] },
323 contents: contents
324 };
325
326 if (requireJSON) {
327 payload.generationConfig = { responseMimeType: "application/json" };
328 }
329
330 const response = await fetch(url, {
331 method: 'POST',
332 headers: { 'Content-Type': 'application/json' },
333 body: JSON.stringify(payload)
334 });
335
336 const data = await response.json();
337 if (!response.ok) throw new Error(data.error?.message || "API Error");
338 return data.candidates[0].content.parts[0].text;
339 }
340
341 async function callOpenAI(userPrompt, sysInst, requireJSON, history) {
342 const url = `https://api.openai.com/v1/chat/completions`;
343
344 let messages = [{ role: 'system', content: sysInst }];
345 history.forEach(msg => {
346 messages.push({ role: msg.role === 'ai' ? 'assistant' : 'user', content: msg.text });
347 });
348 messages.push({ role: 'user', content: userPrompt });
349
350 const payload = {
351 model: apiSettings.model,
352 messages: messages
353 };
354
355 if (requireJSON) {
356 payload.response_format = { type: "json_object" };
357 }
358
359 const response = await fetch(url, {
360 method: 'POST',
361 headers: {
362 'Content-Type': 'application/json',
363 'Authorization': `Bearer ${apiSettings.key}`
364 },
365 body: JSON.stringify(payload)
366 });
367
368 const data = await response.json();
369 if (!response.ok) throw new Error(data.error?.message || "API Error");
370 return data.choices[0].message.content;
371 }
372
373 /* ==========================================
374 MATH RENDERING & LOGIC
375 ========================================== */
376 class MathSpan extends HTMLElement {
377 connectedCallback() {
378 const math = this.innerHTML;
379 this.innerHTML = '';
380 try { katex.render(math, this, { throwOnError: false, displayMode: false }); } catch(e) { this.innerText = math; }
381 }
382 }
383 class MathDiv extends HTMLElement {
384 connectedCallback() {
385 const math = this.innerHTML;
386 this.innerHTML = '';
387 try { katex.render(math, this, { throwOnError: false, displayMode: true }); } catch(e) { this.innerText = math; }
388 }
389 }
390 customElements.define('math-span', MathSpan);
391 customElements.define('math-div', MathDiv);
392
393 function renderMathFeedback(container, text) {
394 const rawHtml = marked.parse(text);
395 const cleanHtml = DOMPurify.sanitize(rawHtml);
396 container.innerHTML = cleanHtml;
397 renderMathInElement(container, {
398 delimiters: [
399 {left: '$$', right: '$$', display: true},
400 {left: '$', right: '$', display: false}
401 ],
402 throwOnError: false
403 });
404 }
405
406 /* ==========================================
407 UI NAVIGATION
408 ========================================== */
409 let currentStep = 0;
410 const totalSteps = 4;
411
412 function showStep(step) {
413 document.querySelectorAll('.step-section').forEach(el => el.classList.remove('active'));
414 document.getElementById('step' + step).classList.add('active');
415 document.querySelectorAll('.step-dot').forEach((dot, index) => {
416 dot.classList.toggle('active', index === step);
417 });
418 document.getElementById('stepInfo').innerText = (step+1) + ' / ' + totalSteps;
419 document.getElementById('prevBtn').disabled = (step === 0);
420 document.getElementById('nextBtn').disabled = (step === totalSteps-1);
421 }
422 function changeStep(delta) { changeStepTo(currentStep + delta); }
423 function changeStepTo(step) {
424 if (step >= 0 && step < totalSteps) { currentStep = step; showStep(currentStep); }
425 }
426
427 /* ==========================================
428 EXERCISES & AI MODULES
429 ========================================== */
430 let exercises = [
431 { id: 'q1', q: "(-8) + 3", a: -5 },
432 { id: 'q2', q: "15 + (-22)", a: -7 },
433 { id: 'q3', q: "(-9) + (-6) + 4", a: -11 }
434 ];
435
436 function renderExercises() {
437 const container = document.getElementById('exerciseContainer');
438 container.innerHTML = '';
439 exercises.forEach((ex, index) => {
440 const divItem = document.createElement('div');
441 divItem.className = 'exercise-item';
442 divItem.id = `item_${ex.id}`;
443
444 const divRow = document.createElement('div');
445 divRow.className = 'exercise-row';
446 divRow.innerHTML = `<span>ข้อ ${index+1}) </span><math-span>${ex.q} = </math-span>
447 <input type="number" id="input_${ex.id}" placeholder="?">
448 <span class="feedback" id="fb_${ex.id}"></span>`;
449
450 const explainBtn = document.createElement('button');
451 explainBtn.className = 'btn-explain';
452 explainBtn.id = `btn_explain_${ex.id}`;
453 explainBtn.onclick = () => explainError(ex.id);
454 explainBtn.innerText = '❓ ทำไมข้อนี้ถึงผิด?';
455
456 const aiBox = document.createElement('div');
457 aiBox.className = 'ai-feedback-box';
458 aiBox.id = `ai_box_${ex.id}`;
459
460 divItem.appendChild(divRow);
461 divItem.appendChild(explainBtn);
462 divItem.appendChild(aiBox);
463 container.appendChild(divItem);
464 });
465 }
466
467 function checkExercises() {
468 let score = 0;
469 exercises.forEach(ex => {
470 const userVal = document.getElementById(`input_${ex.id}`).value;
471 const fb = document.getElementById(`fb_${ex.id}`);
472 const btn = document.getElementById(`btn_explain_${ex.id}`);
473 if (userVal !== '' && Number(userVal) === ex.a) {
474 fb.innerHTML = '✅ ถูกต้อง'; fb.style.color = '#27ae60'; btn.style.display = 'none';
475 score++;
476 } else if(userVal !== '') {
477 fb.innerHTML = '❌ ผิด'; fb.style.color = '#e74c3c'; btn.style.display = 'inline-block';
478 }
479 });
480 document.getElementById('scoreDisplay').innerHTML = `คะแนน: ${score}/${exercises.length}`;
481 }
482
483 async function explainError(id) {
484 const ex = exercises.find(e => e.id === id);
485 const val = document.getElementById(`input_${id}`).value;
486 const box = document.getElementById(`ai_box_${id}`);
487 box.style.display = 'block';
488 box.innerHTML = '<span class="spinner"></span> กำลังวิเคราะห์...';
489
490 try {
491 const prompt = `นักเรียนตอบผิดในโจทย์ $$${ex.q} = ?$$ โดยตอบเป็น "${val}" (คำตอบที่ถูกคือ ${ex.a}) ช่วยสอนวิธีคิดที่ถูกโดยใช้ Socratic Method`;
492 const res = await callLLM(prompt, "คุณคือครูที่วิเคราะห์ข้อผิดพลาดของเด็ก");
493 renderMathFeedback(box, res);
494 } catch (e) { box.innerHTML = `<span style="color:red;">❌ ${e.message}</span>`; }
495 }
496
497 async function generateNewQuestions() {
498 const btn = document.getElementById('btnGenerate');
499 btn.disabled = true;
500 document.getElementById('genSpinner').style.display = 'inline-block';
501
502 try {
503 const prompt = `สร้างโจทย์การบวกจำนวนเต็ม ม.1 ใหม่ 3 ข้อ ส่งเป็น JSON Array เท่านั้น รูปแบบ: [{"id":"new1","q":"(-5)+2","a":-3}]`;
504 let res = await callLLM(prompt, "คุณคือโจทย์ Generator ส่งแค่ JSON เปล่าๆ ห้ามมีคำนำหน้าหรือ markdown", true);
505 res = res.replace(/```json/g, '').replace(/```/g, '').trim();
506 const newEx = JSON.parse(res);
507 if(Array.isArray(newEx)) { exercises = newEx; renderExercises(); }
508 } catch (e) { alert(e.message); } finally {
509 btn.disabled = false;
510 document.getElementById('genSpinner').style.display = 'none';
511 }
512 }
513
514 /* ==========================================
515 CHAT MODULE
516 ========================================== */
517 let chatHistory = [];
518 let missionScore = 0;
519
520 async function sendChatMessage() {
521 const input = document.getElementById('chatInput');
522 const text = input.value.trim();
523 if(!text) return;
524
525 appendMessage('user', text);
526 input.value = '';
527 const loadId = appendMessage('ai', '<span class="spinner"></span> กำลังคิด...');
528
529 try {
530 const res = await callLLM(text, "คุณคือกัปตันจำนวนเต็ม ตัวละครสอนเลขแนวผจญภัย ให้คะแนนผ่าน [SCORE+10]", false, chatHistory);
531 let clean = res;
532 if(res.includes("[SCORE+10]")) {
533 clean = res.replace("[SCORE+10]", "");
534 missionScore = Math.min(100, missionScore + 10);
535 document.getElementById('missionProgress').style.width = missionScore + '%';
536 }
537 chatHistory.push({role:'user', text:text}, {role:'ai', text:clean});
538 renderMathFeedback(document.getElementById(loadId), clean);
539 } catch (e) { document.getElementById(loadId).innerHTML = `<span style="color:red;">❌ ${e.message}</span>`; }
540 }
541
542 function appendMessage(role, html) {
543 const div = document.createElement('div');
544 div.className = `msg ${role}`;
545 const id = 'msg_' + Date.now();
546 div.id = id;
547 div.innerHTML = html;
548 document.getElementById('chatMessages').appendChild(div);
549 document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
550 return id;
551 }
552
553 window.onload = () => { loadSettings(); renderExercises(); showStep(0); };
554</script>
555</body>
556</html>