rapan/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>Pythagoras Quest: AI Interactive Math</title>
7 <!-- Tailwind CSS for styling -->
8 <script src="https://cdn.tailwindcss.com"></script>
9 <!-- FontAwesome for Icons -->
10 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11 <!-- Google Fonts -->
12 <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@300;400;600;700&display=swap" rel="stylesheet">
13
14 <style>
15 body {
16 font-family: 'Noto Sans Thai', sans-serif;
17 background-color: #f0fdf4; /* Light green tint for math theme */
18 }
19 .canvas-container {
20 position: relative;
21 width: 100%;
22 max-width: 400px;
23 margin: 0 auto;
24 }
25 /* Markdown simple styling */
26 .ai-markdown strong { font-weight: 700; color: #166534; }
27 .ai-markdown p { margin-bottom: 0.5rem; }
28 .ai-markdown ul { list-style-type: disc; margin-left: 1.5rem; margin-bottom: 0.5rem; }
29
30 /* Custom Scrollbar for chat */
31 .chat-container::-webkit-scrollbar { width: 6px; }
32 .chat-container::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
33 </style>
34</head>
35<body class="text-gray-800 h-screen flex flex-col overflow-hidden">
36
37 <!-- Header -->
38 <header class="bg-green-700 text-white p-4 shadow-md flex justify-between items-center z-10 shrink-0">
39 <div class="flex items-center gap-3">
40 <i class="fa-solid fa-shapes text-2xl"></i>
41 <h1 class="text-xl font-bold">Pythagoras Quest (AI Tutor)</h1>
42 </div>
43 <button onclick="openSettings()" class="hover:bg-green-600 p-2 rounded-full transition" title="ตั้งค่าระบบ">
44 <i class="fa-solid fa-gear text-xl"></i>
45 </button>
46 </header>
47
48 <!-- Main Content Area -->
49 <main class="flex-1 flex flex-col md:flex-row overflow-hidden relative">
50
51 <!-- Left Panel: Interactive Math Visual & Tasks -->
52 <div class="w-full md:w-1/2 p-4 md:p-6 overflow-y-auto bg-white shadow-[2px_0_10px_rgba(0,0,0,0.05)] z-0 flex flex-col">
53
54 <!-- Progress Bar (Module B Support) -->
55 <div class="mb-6 bg-gray-100 rounded-full h-3 w-full border border-gray-200 shadow-inner">
56 <div id="missionProgress" class="bg-gradient-to-r from-green-400 to-green-600 h-3 rounded-full transition-all duration-500 w-0"></div>
57 </div>
58
59 <!-- Visual Canvas for Pythagorean Theorem -->
60 <div class="canvas-container bg-gray-50 rounded-xl border-2 border-dashed border-gray-300 p-4 mb-6 flex justify-center items-center h-[250px]">
61 <canvas id="mathCanvas" width="300" height="200"></canvas>
62 </div>
63
64 <!-- Task Container (Dynamically swapped based on stage) -->
65 <div id="taskContainer" class="flex-1">
66 <!-- Content injected via JS -->
67 </div>
68
69 </div>
70
71 <!-- Right Panel: AI Chat / Feedback Interface -->
72 <div class="w-full md:w-1/2 bg-gray-50 flex flex-col border-t md:border-t-0 md:border-l border-gray-200">
73 <div class="bg-green-100 px-4 py-3 border-b border-green-200 flex justify-between items-center shrink-0">
74 <span class="font-semibold text-green-800"><i class="fa-solid fa-robot mr-2"></i>ผู้ช่วย AI (Socratic Tutor)</span>
75 <span id="aiStatusBadge" class="text-xs bg-green-500 text-white px-2 py-1 rounded-full animate-pulse hidden">กำลังคิด...</span>
76 </div>
77
78 <div id="chatHistory" class="chat-container flex-1 overflow-y-auto p-4 space-y-4">
79 <!-- Chat messages go here -->
80 </div>
81
82 <!-- Chat Input (Used primarily in Roleplay) -->
83 <div id="chatInputArea" class="p-3 bg-white border-t border-gray-200 shrink-0">
84 <form id="chatForm" onsubmit="handleChatSubmit(event)" class="flex gap-2">
85 <input type="text" id="userInput" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="พิมพ์ข้อความที่นี่..." autocomplete="off">
86 <button type="submit" class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition"><i class="fa-solid fa-paper-plane"></i></button>
87 </form>
88 </div>
89 </div>
90 </main>
91
92 <!-- Settings Modal -->
93 <div id="settingsModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50 backdrop-blur-sm">
94 <div class="bg-white rounded-xl shadow-2xl p-6 w-[90%] max-w-md transform transition-all scale-95 opacity-0" id="settingsBox">
95 <h2 class="text-2xl font-bold mb-4 text-gray-800"><i class="fa-solid fa-sliders mr-2 text-green-600"></i>ตั้งค่าระบบ AI</h2>
96
97 <div class="space-y-4">
98 <div>
99 <label class="block text-sm font-semibold text-gray-700 mb-1">AI Provider <span class="text-red-500">*</span></label>
100 <select id="providerInput" onchange="toggleProviderSettings()" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-green-500">
101 <option value="gemini">Google Gemini (Official)</option>
102 <option value="openai">OpenAI Compatible (Custom API)</option>
103 </select>
104 </div>
105 <div id="baseUrlGroup" class="hidden">
106 <label class="block text-sm font-semibold text-gray-700 mb-1">Base URL (สำหรับ Custom API)</label>
107 <input type="text" id="baseUrlInput" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-green-500" placeholder="https://api.openai.com/v1/chat/completions">
108 </div>
109 <div>
110 <label class="block text-sm font-semibold text-gray-700 mb-1">API Key <span class="text-red-500">*</span></label>
111 <input type="password" id="apiKeyInput" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-green-500" placeholder="ใส่ API Key ที่นี่...">
112 <p class="text-xs text-gray-500 mt-1">คีย์จะถูกบันทึกไว้ในเบราว์เซอร์ของคุณเท่านั้น (localStorage)</p>
113 </div>
114 <div>
115 <label class="block text-sm font-semibold text-gray-700 mb-1">Model Name</label>
116 <input type="text" id="modelInput" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-green-500" value="gemini-2.5-flash">
117 </div>
118 </div>
119
120 <div id="settingsError" class="mt-3 text-sm text-red-600 hidden"></div>
121
122 <div class="mt-6 flex justify-end gap-3">
123 <button onclick="closeSettings()" id="btnCancelSettings" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition">ยกเลิก</button>
124 <button onclick="saveSettings()" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">บันทึกและเริ่มเรียน</button>
125 </div>
126 </div>
127 </div>
128
129 <!-- JavaScript Logic -->
130 <script>
131 /* ==========================================
132 1. Core Infrastructure & State Management
133 ========================================== */
134
135 let currentState = {
136 provider: 'gemini', // 'gemini' or 'openai'
137 baseUrl: '',
138 apiKey: '',
139 modelName: 'gemini-2.5-flash',
140 stage: 'init', // init, roleplay, module_d (MCQ), module_i (Text), summary
141 logs: [], // Store interactions for Module H
142 triangle: { a: 3, b: 4, c: 5 } // Current math state
143 };
144
145 // 1.3 Math-Specific Pedagogy Constraints (Strict System Prompt)
146 const PEDAGOGY_RULES = `
147กฎเหล็กด้านการสอน (Pedagogy Constraints):
1481. ห้ามบอกคำตอบหรือเฉลยตรงๆ โดยเด็ดขาด
1492. ใช้ Socratic Method เสมอ: ถามคำถามกลับเพื่อให้นักเรียนคิดทบทวน (Productive Struggle)
1503. หากนักเรียนตอบผิด ให้ชี้ให้เห็นว่าผิดตรงไหน และใบ้กระบวนการต่อไป
1514. ห้ามถามหรือบันทึกข้อมูลส่วนบุคคลของนักเรียน (PDPA Compliance)
1525. ใช้ภาษาไทย เป็นมิตร ให้กำลังใจ และ In-Character ตามบริบท
153`;
154
155 window.onload = () => {
156 loadSettings();
157 drawTriangle(currentState.triangle.a, currentState.triangle.b, "?");
158 if (!currentState.apiKey) {
159 openSettings(true); // Force open if no key
160 } else {
161 startRoleplay(); // Module B Start
162 }
163 };
164
165 /* --- UI & Canvas Helpers --- */
166
167 function drawTriangle(a, b, cLabel) {
168 const canvas = document.getElementById('mathCanvas');
169 const ctx = canvas.getContext('2d');
170 ctx.clearRect(0, 0, canvas.width, canvas.height);
171
172 // Base scale and offsets
173 const scale = 20;
174 const startX = 80;
175 const startY = 160;
176
177 const width = b * scale;
178 const height = a * scale;
179
180 // Draw Triangle
181 ctx.beginPath();
182 ctx.moveTo(startX, startY); // Bottom Left (Right angle)
183 ctx.lineTo(startX + width, startY); // Bottom Right
184 ctx.lineTo(startX, startY - height); // Top Left
185 ctx.closePath();
186
187 // Style
188 ctx.lineWidth = 3;
189 ctx.strokeStyle = '#15803d'; // green-700
190 ctx.fillStyle = '#bbf7d0'; // green-200
191 ctx.fill();
192 ctx.stroke();
193
194 // Draw Right Angle symbol
195 ctx.beginPath();
196 ctx.moveTo(startX + 15, startY);
197 ctx.lineTo(startX + 15, startY - 15);
198 ctx.lineTo(startX, startY - 15);
199 ctx.strokeStyle = '#166534';
200 ctx.stroke();
201
202 // Labels
203 ctx.fillStyle = '#1f2937';
204 ctx.font = 'bold 16px Noto Sans Thai';
205 ctx.fillText(`ฐาน (b) = ${b}`, startX + width/2 - 30, startY + 25);
206 ctx.fillText(`สูง (a) = ${a}`, startX - 70, startY - height/2 + 5);
207
208 // Hypotenuse
209 ctx.fillStyle = '#dc2626'; // red-600
210 ctx.font = 'bold 18px Noto Sans Thai';
211 ctx.fillText(`c = ${cLabel}`, startX + width/2 + 10, startY - height/2 - 10);
212 }
213
214 function updateProgress(percent) {
215 document.getElementById('missionProgress').style.width = `${percent}%`;
216 }
217
218 function renderMarkdown(text) {
219 // Very simple markdown parser for bold, italics, lists, and line breaks
220 let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
221 .replace(/\*(.*?)\*/g, '<em>$1</em>')
222 .replace(/\n/g, '<br>');
223 // Handle simple unordered lists
224 html = html.replace(/<br>- (.*?)(?=<br>|$)/g, '<ul><li>$1</li></ul>');
225 html = html.replace(/<\/ul><ul>/g, ''); // combine adjacent lists
226 return `<div class="ai-markdown text-sm text-gray-700">${html}</div>`;
227 }
228
229 function appendChat(role, text) {
230 const chatHistory = document.getElementById('chatHistory');
231 const msgDiv = document.createElement('div');
232 msgDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} animate-fade-in`;
233
234 const innerDiv = document.createElement('div');
235 innerDiv.className = `max-w-[85%] rounded-2xl px-4 py-3 ${
236 role === 'user'
237 ? 'bg-green-600 text-white rounded-tr-none shadow-md'
238 : 'bg-white border border-gray-200 rounded-tl-none shadow-sm'
239 }`;
240
241 if (role === 'ai') {
242 innerDiv.innerHTML = renderMarkdown(text);
243 } else {
244 innerDiv.textContent = text;
245 }
246
247 msgDiv.appendChild(innerDiv);
248 chatHistory.appendChild(msgDiv);
249 chatHistory.scrollTop = chatHistory.scrollHeight;
250 }
251
252 /* --- Universal API Caller (1.1) --- */
253
254 async function callLLM(prompt, systemInstruction) {
255 if (!currentState.apiKey) {
256 alert("กรุณาตั้งค่า API Key ก่อนใช้งาน");
257 openSettings();
258 return null;
259 }
260
261 const badge = document.getElementById('aiStatusBadge');
262 badge.classList.remove('hidden');
263
264 try {
265 let retries = 3;
266 let delay = 1000;
267 let response;
268 let data;
269
270 while (retries > 0) {
271 if (currentState.provider === 'openai') {
272 // --- OpenAI Compatible Format ---
273 const endpoint = currentState.baseUrl || 'https://api.openai.com/v1/chat/completions';
274 const payload = {
275 model: currentState.modelName,
276 messages: [
277 { role: "system", content: PEDAGOGY_RULES + "\n" + systemInstruction },
278 { role: "user", content: prompt }
279 ]
280 };
281 response = await fetch(endpoint, {
282 method: 'POST',
283 headers: {
284 'Content-Type': 'application/json',
285 'Authorization': `Bearer ${currentState.apiKey}`
286 },
287 body: JSON.stringify(payload)
288 });
289 } else {
290 // --- Native Gemini Format ---
291 const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${currentState.modelName}:generateContent?key=${currentState.apiKey}`;
292 const payload = {
293 contents: [{ parts: [{ text: prompt }] }],
294 systemInstruction: { parts: [{ text: PEDAGOGY_RULES + "\n" + systemInstruction }] }
295 };
296 response = await fetch(endpoint, {
297 method: 'POST',
298 headers: { 'Content-Type': 'application/json' },
299 body: JSON.stringify(payload)
300 });
301 }
302
303 if (response.ok) {
304 data = await response.json();
305 break;
306 }
307
308 // Specific check for Rate Limit (429)
309 if (response.status === 429) {
310 throw new Error("RATE_LIMIT");
311 }
312
313 retries--;
314 if(retries === 0) throw new Error(`API Error: ${response.status}`);
315 await new Promise(r => setTimeout(r, delay));
316 delay *= 2;
317 }
318
319 badge.classList.add('hidden');
320
321 // Extract text based on provider format
322 if (currentState.provider === 'openai') {
323 if (data.choices && data.choices[0] && data.choices[0].message) {
324 return data.choices[0].message.content;
325 }
326 } else {
327 if (data.candidates && data.candidates[0].content.parts[0].text) {
328 return data.candidates[0].content.parts[0].text;
329 }
330 }
331
332 throw new Error("Invalid response format from API");
333
334 } catch (error) {
335 console.error("LLM Call Failed:", error);
336 badge.classList.add('hidden');
337
338 if (error.message === "RATE_LIMIT") {
339 appendChat('ai', "⚠️ **ขออภัยครับ** ระบบประมวลผลถูกเรียกใช้งานมากเกินไป (Rate Limit 429) กรุณารอสัก 10-15 วินาที แล้วลองส่งข้อความหรือกดปุ่มใหม่อีกครั้งครับ");
340 } else {
341 appendChat('ai', "❌ **เกิดข้อผิดพลาด** ไม่สามารถติดต่อสมองกลได้ในขณะนี้ โปรดตรวจสอบการตั้งค่า API, โมเดล, หรือ URL ให้ถูกต้อง");
342 }
343 return null;
344 }
345 }
346
347 /* ==========================================
348 1.2 Settings Modal Management
349 ========================================== */
350
351 function loadSettings() {
352 currentState.provider = localStorage.getItem('pythagoras_provider') || 'gemini';
353 currentState.baseUrl = localStorage.getItem('pythagoras_baseurl') || '';
354 currentState.apiKey = localStorage.getItem('pythagoras_apikey') || '';
355 currentState.modelName = localStorage.getItem('pythagoras_model') || 'gemini-2.5-flash';
356 }
357
358 function toggleProviderSettings() {
359 const provider = document.getElementById('providerInput').value;
360 const baseUrlGroup = document.getElementById('baseUrlGroup');
361 const modelInput = document.getElementById('modelInput');
362
363 if (provider === 'openai') {
364 baseUrlGroup.classList.remove('hidden');
365 if (modelInput.value.includes('gemini')) modelInput.value = 'gpt-3.5-turbo'; // default example for openai
366 } else {
367 baseUrlGroup.classList.add('hidden');
368 if (!modelInput.value.includes('gemini')) modelInput.value = 'gemini-2.5-flash';
369 }
370 }
371
372 function openSettings(force = false) {
373 document.getElementById('providerInput').value = currentState.provider;
374 document.getElementById('baseUrlInput').value = currentState.baseUrl;
375 document.getElementById('apiKeyInput').value = currentState.apiKey;
376 document.getElementById('modelInput').value = currentState.modelName;
377
378 toggleProviderSettings(); // Set initial UI state
379
380 document.getElementById('btnCancelSettings').style.display = force ? 'none' : 'block';
381
382 const modal = document.getElementById('settingsModal');
383 const box = document.getElementById('settingsBox');
384 modal.classList.remove('hidden');
385 modal.classList.add('flex');
386 // Animation trick
387 setTimeout(() => {
388 box.classList.remove('scale-95', 'opacity-0');
389 box.classList.add('scale-100', 'opacity-100');
390 }, 10);
391 }
392
393 function closeSettings() {
394 const modal = document.getElementById('settingsModal');
395 const box = document.getElementById('settingsBox');
396 box.classList.remove('scale-100', 'opacity-100');
397 box.classList.add('scale-95', 'opacity-0');
398 setTimeout(() => {
399 modal.classList.remove('flex');
400 modal.classList.add('hidden');
401 }, 200);
402 }
403
404 function saveSettings() {
405 const provider = document.getElementById('providerInput').value;
406 const baseUrl = document.getElementById('baseUrlInput').value.trim();
407 const key = document.getElementById('apiKeyInput').value.trim();
408 const model = document.getElementById('modelInput').value.trim();
409 const errorDiv = document.getElementById('settingsError');
410
411 if (!key) {
412 errorDiv.textContent = "กรุณาใส่ API Key";
413 errorDiv.classList.remove('hidden');
414 return;
415 }
416
417 if (provider === 'openai' && !baseUrl) {
418 errorDiv.textContent = "กรุณาระบุ Base URL สำหรับ Custom API";
419 errorDiv.classList.remove('hidden');
420 return;
421 }
422
423 localStorage.setItem('pythagoras_provider', provider);
424 localStorage.setItem('pythagoras_baseurl', baseUrl);
425 localStorage.setItem('pythagoras_apikey', key);
426 localStorage.setItem('pythagoras_model', model);
427
428 currentState.provider = provider;
429 currentState.baseUrl = baseUrl;
430 currentState.apiKey = key;
431 currentState.modelName = model;
432
433 errorDiv.classList.add('hidden');
434 closeSettings();
435
436 // Restart game if it was stuck
437 if (currentState.stage === 'init') {
438 startRoleplay();
439 }
440 }
441
442 /* ==========================================
443 2. AI Modules Implementation
444 ========================================== */
445
446 // Global chat handler
447 async function handleChatSubmit(e) {
448 e.preventDefault();
449 const input = document.getElementById('userInput');
450 const text = input.value.trim();
451 if (!text) return;
452
453 appendChat('user', text);
454 input.value = '';
455
456 // Handle based on current stage
457 if (currentState.stage === 'roleplay') {
458 currentState.logs.push({ event: "Roleplay Chat User", detail: text });
459
460 const sysInstruction = `
461คุณคือ "พีทาโกรัส" นักคณิตศาสตร์ชาวกรีก
462ภารกิจตอนนี้: คุณกำลังพยายามสร้างวิหาร แต่ขาดเชือกสำหรับวัดมุมฉาก คุณต้องการสร้างสามเหลี่ยมมุมฉากที่มีด้าน a=3, b=4
463ให้นักเรียนช่วยคิดว่า ด้านตรงข้ามมุมฉาก (c) ต้องยาวเท่าไหร่?
464คุยกับนักเรียนในบทบาทนี้ ใช้ Socratic method ใบ้สูตร a^2 + b^2 = c^2 อย่างแนบเนียน
465ถ้าผู้ใช้ตอบ "5" หรือใกล้เคียง ให้กล่าวชม และบอกว่า "ท่านพร้อมแล้วสำหรับการทดสอบต่อไป! พิมพ์ 'พร้อม' เพื่อเริ่ม"`;
466
467 const aiResponse = await callLLM(`นักเรียนตอบว่า: ${text}`, sysInstruction);
468 if (aiResponse) {
469 appendChat('ai', aiResponse);
470 currentState.logs.push({ event: "Roleplay Chat AI", detail: aiResponse });
471
472 if (text.includes("พร้อม") || aiResponse.includes("การทดสอบต่อไป")) {
473 setTimeout(startModuleD, 2000);
474 }
475 }
476 }
477 }
478
479
480 /* --- Module B: Roleplay Simulation --- */
481 async function startRoleplay() {
482 currentState.stage = 'roleplay';
483 updateProgress(10);
484
485 document.getElementById('taskContainer').innerHTML = `
486 <div class="bg-blue-50 border border-blue-200 p-4 rounded-xl">
487 <h3 class="font-bold text-blue-800 text-lg mb-2"><i class="fa-solid fa-scroll mr-2"></i>Mission 1: พบพีทาโกรัส</h3>
488 <p class="text-sm text-blue-700">พูดคุยกับ AI ทางขวามือเพื่อรับภารกิจแรกของคุณให้สำเร็จ! (หาค่า c ของสามเหลี่ยมด้านบน)</p>
489 </div>
490 `;
491
492 const sysInstruction = `คุณคือ "พีทาโกรัส" ทักทายผู้เล่นด้วยความตื่นเต้น และแจ้งภารกิจว่าคุณกำลังสร้างวิหาร แต่ต้องการเชือกที่ยาวพอดีสำหรับด้านตรงข้ามมุมฉาก (c) เมื่อด้านประกอบมุมฉากคือ 3 และ 4 ให้ถามผู้เล่นว่าเขาจะหามันได้อย่างไร`;
493
494 const aiResponse = await callLLM("เริ่มการสนทนา ทักทายผู้เล่น", sysInstruction);
495 if (aiResponse) appendChat('ai', aiResponse);
496 }
497
498 /* --- Module D: Explain "Why I'm Wrong" --- */
499 function startModuleD() {
500 currentState.stage = 'module_d';
501 updateProgress(40);
502 document.getElementById('chatInputArea').style.display = 'none'; // Disable general chat
503
504 // Update Canvas to new problem
505 currentState.triangle = { a: 6, b: 8 };
506 drawTriangle(6, 8, "?");
507
508 const taskHTML = `
509 <div class="bg-white border border-gray-200 p-4 rounded-xl shadow-sm">
510 <h3 class="font-bold text-gray-800 text-lg mb-3">ด่านที่ 1: หอคอยพังทลาย</h3>
511 <p class="text-sm text-gray-600 mb-4">หอคอยสูง 6 เมตร (a) ต้องการสร้างบันไดพาดให้ห่างจากฐาน 8 เมตร (b) บันไดต้องยาวเท่าไหร่ (c) ?</p>
512
513 <div class="space-y-2" id="mcqOptions">
514 <button onclick="checkMCQAnswer(14)" class="w-full text-left px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition">A. 14 เมตร</button>
515 <button onclick="checkMCQAnswer(10)" class="w-full text-left px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition">B. 10 เมตร</button>
516 <button onclick="checkMCQAnswer(100)" class="w-full text-left px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition">C. 100 เมตร</button>
517 </div>
518
519 <div id="wrongFeedbackArea" class="mt-4 hidden p-3 bg-red-50 border border-red-200 rounded-lg">
520 <p class="text-red-700 text-sm mb-2"><i class="fa-solid fa-xmark-circle mr-1"></i> ยังไม่ถูกต้อง ลองคิดอีกนิด!</p>
521 <button onclick="explainWhyWrong()" class="bg-yellow-500 text-white text-xs px-3 py-1.5 rounded shadow hover:bg-yellow-600 transition">
522 <i class="fa-solid fa-circle-question mr-1"></i> ทำไมข้อนี้ถึงผิด? (AI ช่วยอธิบาย)
523 </button>
524 </div>
525 </div>
526 `;
527 document.getElementById('taskContainer').innerHTML = taskHTML;
528 appendChat('ai', "📝 **ด่านที่ 1 มาแล้ว!** ลองดูโจทย์ที่แผงควบคุมด้านซ้าย แล้วเลือกคำตอบที่คิดว่าถูกต้องครับ");
529 }
530
531 let lastWrongAnswer = null;
532
533 function checkMCQAnswer(ans) {
534 currentState.logs.push({ event: "Module D Answer", detail: `User selected ${ans} for a=6, b=8` });
535 const btnArea = document.getElementById('mcqOptions');
536
537 if (ans === 10) {
538 btnArea.innerHTML = `<div class="p-3 bg-green-100 text-green-800 rounded-lg font-bold"><i class="fa-solid fa-check-circle mr-2"></i> ถูกต้อง! (c = 10)</div>`;
539 document.getElementById('wrongFeedbackArea').classList.add('hidden');
540 drawTriangle(6, 8, "10");
541 appendChat('ai', "🎉 ยอดเยี่ยมมาก! ท่านเข้าใจพื้นฐานแล้ว \n\nมาถึงด่านสุดท้ายกันครับ!");
542 setTimeout(startModuleI, 2500);
543 } else {
544 lastWrongAnswer = ans;
545 document.getElementById('wrongFeedbackArea').classList.remove('hidden');
546 }
547 }
548
549 async function explainWhyWrong() {
550 // Module D Core Functionality
551 const btn = document.querySelector('#wrongFeedbackArea button');
552 btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> กำลังวิเคราะห์...`;
553 btn.disabled = true;
554
555 const sysInstruction = `
556ผู้เล่นเลือกตอบ ${lastWrongAnswer} สำหรับโจทย์พีทาโกรัส: a=6, b=8 หาค่า c
557หน้าที่ของคุณ (Module D): อธิบาย "Misconception" ว่าทำไมการคิดให้ได้ ${lastWrongAnswer} ถึงไม่ถูกต้องตามหลักคณิตศาสตร์
558ตัวอย่าง: ถ้าตอบ 14 แปลว่าเอา 6+8 โดยไม่ได้ยกกำลังสอง, ถ้าตอบ 100 คือลืมถอดรูท
559**ห้ามบอกเฉลยว่าตอบ 10** ให้ไกด์ว่าพวกเขาข้ามขั้นตอนไหนไป ยกตัวอย่างเปรียบเทียบให้เห็นภาพ`;
560
561 const aiResponse = await callLLM(`ทำไมฉันถึงตอบ ${lastWrongAnswer} แล้วผิด? ช่วยอธิบายหน่อย`, sysInstruction);
562
563 if (aiResponse) {
564 appendChat('user', `❓ ทำไมตอบ ${lastWrongAnswer} ถึงผิดครับ?`);
565 appendChat('ai', aiResponse);
566 currentState.logs.push({ event: "Module D Explanation", detail: aiResponse });
567 }
568
569 btn.innerHTML = `<i class="fa-solid fa-circle-question mr-1"></i> อธิบายข้อนี้อีกครั้ง`;
570 btn.disabled = false;
571 }
572
573 /* --- Module I: AI Answer Checker --- */
574 function startModuleI() {
575 currentState.stage = 'module_i';
576 updateProgress(70);
577
578 // Update Canvas to new visual (Ship)
579 document.getElementById('mathCanvas').style.display = 'none';
580 const visualHTML = `
581 <div class="h-full w-full flex flex-col items-center justify-center text-center p-2">
582 <i class="fa-solid fa-ship text-5xl text-blue-800 mb-3"></i>
583 <p class="text-sm font-bold text-gray-700">เรือใบของพีทาโกรัส</p>
584 <p class="text-xs text-gray-500">เสากระโดงสูง 5m, ระยะกางใบเรือ 12m</p>
585 </div>
586 `;
587 document.querySelector('.canvas-container').innerHTML = visualHTML;
588
589 const taskHTML = `
590 <div class="bg-white border border-gray-200 p-4 rounded-xl shadow-sm">
591 <h3 class="font-bold text-purple-800 text-lg mb-2">ด่านสุดท้าย: แล่นเรือกลับบ้าน (อัตนัย)</h3>
592 <p class="text-sm text-gray-600 mb-4">เสากระโดงเรือสูง 5 เมตร ผูกเชือกขึงใบเรือไปที่หัวเรือยาว 12 เมตร เชือกเส้นนั้น (ด้านตรงข้ามมุมฉาก) ยาวเท่าไหร่? <b>จงอธิบายวิธีทำอย่างละเอียด</b></p>
593
594 <textarea id="subjectiveInput" rows="4" class="w-full border border-gray-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-purple-500 outline-none mb-3" placeholder="เขียนอธิบายวิธีทำ เช่น c^2 = a^2 + b^2..."></textarea>
595
596 <button onclick="checkSubjectiveAnswer()" id="btnCheckSubj" class="w-full bg-purple-600 text-white font-bold py-2 rounded-lg hover:bg-purple-700 transition shadow-md">
597 <i class="fa-solid fa-magnifying-glass-chart mr-1"></i> ตรวจคำตอบ (AI Rubric)
598 </button>
599 </div>
600 `;
601 document.getElementById('taskContainer').innerHTML = taskHTML;
602 appendChat('ai', "📝 ด่านนี้ขอให้พิมพ์ **อธิบายวิธีทำ** ลงในช่องว่างนะครับ ระบบ AI ของเราจะตรวจตาม Rubric (ความเข้าใจ, ขั้นตอน, คำตอบ) ขอให้โชคดี!");
603 }
604
605 async function checkSubjectiveAnswer() {
606 const input = document.getElementById('subjectiveInput').value.trim();
607 if(!input) {
608 alert("กรุณาพิมพ์คำตอบก่อนกดตรวจครับ");
609 return;
610 }
611
612 const btn = document.getElementById('btnCheckSubj');
613 btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> กำลังตรวจตาม Rubric...`;
614 btn.disabled = true;
615
616 currentState.logs.push({ event: "Module I Submit", detail: input });
617
618 const sysInstruction = `
619(Module I: AI Answer Checker)
620ประเมินคำตอบอัตนัยของนักเรียน สำหรับโจทย์: a=5, b=12, หา c (คำตอบที่ถูกคือ 13)
621คำตอบที่นักเรียนส่งมา: "${input}"
622
623ประเมินและตอบกลับโดยจัดรูปแบบเป็น Markdown ตามหัวข้อต่อไปนี้ (ส่งมาเป็น text markdown ปกติ ไม่ต้องใส่ block code JSON):
624**ผลการตรวจให้คะแนน**
625- 🎯 **ความเข้าใจ Concept:** [ระบุว่าเข้าใจสูตรไหม]
626- 📝 **ความสมบูรณ์ของขั้นตอน:** [ระบุว่าแสดงวิธีทำครบไหม เช่น การยกกำลัง การถอดรูท]
627- ✅ **ความถูกต้องของคำตอบ:** [ระบุว่าคำตอบสุดท้ายถูกไหม]
628- ⭐️ **คะแนนรวม:** [ให้คะแนนเต็ม 10]
629- 💡 **Feedback/ข้อเสนอแนะ:** [คำแนะนำเพื่อปรับปรุง ถ้ายังไม่ถูกห้ามเฉลย ให้ไกด์ต่อ]
630
631ถ้าคะแนนรวมมากกว่า 7/10 ให้ลงท้ายด้วยคำว่า "[PASS]" เพื่อให้ระบบทำงานต่อ
632`;
633
634 const aiResponse = await callLLM(`ประเมินคำตอบนี้: ${input}`, sysInstruction);
635
636 if (aiResponse) {
637 appendChat('user', `ส่งคำตอบ: "${input}"`);
638 appendChat('ai', aiResponse);
639 currentState.logs.push({ event: "Module I Feedback", detail: aiResponse });
640
641 if (aiResponse.includes("[PASS]")) {
642 setTimeout(generateFinalReport, 3000);
643 }
644 }
645
646 btn.innerHTML = `<i class="fa-solid fa-magnifying-glass-chart mr-1"></i> ตรวจคำตอบอีกครั้ง`;
647 btn.disabled = false;
648 }
649
650 /* --- Module H: Performance Summary --- */
651 async function generateFinalReport() {
652 currentState.stage = 'summary';
653 updateProgress(100);
654
655 document.getElementById('taskContainer').innerHTML = `
656 <div class="flex flex-col items-center justify-center h-full text-center">
657 <i class="fa-solid fa-trophy text-6xl text-yellow-500 mb-4 animate-bounce"></i>
658 <h2 class="text-2xl font-bold text-gray-800">ภารกิจสำเร็จ!</h2>
659 <p class="text-gray-600 mt-2">AI กำลังประมวลผล Report Card สรุปผลการเรียนของคุณ...</p>
660 </div>
661 `;
662
663 // Prepare log data payload
664 const logString = JSON.stringify(currentState.logs.map(l => `${l.event}: ${l.detail}`));
665
666 const sysInstruction = `
667(Module H: Performance Summary)
668นี่คือ Log การเรียนรู้ของนักเรียนในภารกิจ "ทฤษฎีบทพีทาโกรัส":
669${logString}
670
671จงสร้าง "Report Card สรุปผลการเรียน" ออกมาในรูปแบบ Markdown ที่สวยงาม โดยประกอบด้วย:
6721. 🏆 **จุดแข็ง (Strengths):** สิ่งที่ทำได้ดี (อ้างอิงจาก log)
6732. ⚠️ **จุดอ่อน / Misconception ที่พบ:** ข้อผิดพลาดที่เกิดขึ้นและวิเคราะห์ว่าเกิดจากความเข้าใจผิดใด
6743. 📈 **คำแนะนำเพื่อการพัฒนา (Recommendations):** ก้าวต่อไปควรฝึกเรื่องใด หรือระวังเรื่องใด
675
676พิมพ์ด้วยความเป็นทางการแต่น้ำเสียงให้กำลังใจ (Educator Tone)`;
677
678 const aiResponse = await callLLM("สร้าง Report Card สรุปผลจาก Log", sysInstruction);
679
680 if (aiResponse) {
681 // Clear chat and show full screen report
682 document.getElementById('chatInputArea').style.display = 'none';
683 const chatHistory = document.getElementById('chatHistory');
684 chatHistory.innerHTML = '';
685
686 const reportHTML = `
687 <div class="bg-white border-2 border-green-400 rounded-xl p-6 shadow-lg animate-fade-in mx-2 my-4">
688 <div class="flex items-center gap-3 border-b border-gray-200 pb-4 mb-4">
689 <i class="fa-solid fa-clipboard-check text-3xl text-green-600"></i>
690 <h2 class="text-xl font-bold text-gray-800">AI Performance Report Card</h2>
691 </div>
692 ${renderMarkdown(aiResponse)}
693 <div class="mt-6 text-center">
694 <button onclick="location.reload()" class="bg-green-600 text-white px-6 py-2 rounded-full hover:bg-green-700 transition">
695 <i class="fa-solid fa-rotate-right mr-2"></i>เริ่มเรียนใหม่อีกครั้ง
696 </button>
697 </div>
698 </div>
699 `;
700 chatHistory.innerHTML = reportHTML;
701 }
702 }
703 </script>
704</body>
705</html>