document.addEventListener('DOMContentLoaded', function () { const btnMicToggle = document.getElementById('btn-mic-toggle'); const txtTranscription = document.getElementById('transcription-text'); const micStatusContainer = document.getElementById('mic-status-container'); let recognition; let isRecording = false; function showError(msg) { if (micStatusContainer) { micStatusContainer.innerHTML = `Error: ${msg}`; } else { alert(msg); } } if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; recognition = new SpeechRecognition(); recognition.continuous = true; recognition.interimResults = true; recognition.lang = 'en-US'; recognition.onstart = function () { isRecording = true; btnMicToggle.innerHTML = ' หยุดบันทึก (Stop)'; btnMicToggle.style.backgroundColor = '#ffcccc'; const boxMic = document.getElementById('box-mic'); if (boxMic) { boxMic.innerText = "MIC ON (Say Stop)"; boxMic.style.backgroundColor = "rgba(231, 76, 60, 0.8)"; } if (micStatusContainer) micStatusContainer.innerText = 'กำลังฟัง... (Listening...)'; }; recognition.onend = function () { isRecording = false; btnMicToggle.innerHTML = ' เริ่มบันทึกเสียง (Start)'; btnMicToggle.style.backgroundColor = '#ddd'; const boxMic = document.getElementById('box-mic'); if (boxMic) { boxMic.innerText = "MIC OFF"; boxMic.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; boxMic.style.border = "1px solid rgba(255, 255, 255, 0.3)"; } }; recognition.onresult = function (event) { let interimTranscript = ''; let newFinalTranscript = ''; for (let i = event.resultIndex; i < event.results.length; ++i) { if (event.results[i].isFinal) { newFinalTranscript += event.results[i][0].transcript; } else { interimTranscript += event.results[i][0].transcript; } } if (newFinalTranscript) { const active = document.activeElement; if (active && (active.tagName === 'TEXTAREA' || (active.tagName === 'INPUT' && active.type === 'text'))) { if (active.value && !active.value.endsWith(' ')) { active.value += ' '; } active.value += newFinalTranscript; active.dispatchEvent(new Event('input', { bubbles: true })); } else { if (txtTranscription) { if (txtTranscription.value && !txtTranscription.value.endsWith(' ')) { txtTranscription.value += ' '; } txtTranscription.value += newFinalTranscript; } } } if (micStatusContainer) { if (interimTranscript) { micStatusContainer.innerHTML = ' กำลังฟัง: ' + interimTranscript; micStatusContainer.style.color = '#888'; } } }; recognition.onerror = function (event) { isRecording = false; btnMicToggle.innerHTML = ' เริ่มบันทึกเสียง (Start)'; btnMicToggle.style.backgroundColor = '#ddd'; if (event.error === 'not-allowed') { showError("ไม่อนุญาตให้ใช้ไมโครโฟน (Not Allowed). กรุณากด 'Allow' ที่แถบ URL หรือตรวจสอบการตั้งค่า"); } else if (event.error === 'network') { showError("เกิดข้อผิดพลาดเครือข่าย (Network). ตรวจสอบอินเทอร์เน็ต หรือหากใช้ Chrome ปัญหาอาจเกิดจากการไม่ได้ใช้ HTTPS"); } else if (event.error === 'no-speech') { if (micStatusContainer) micStatusContainer.innerText = "ไม่ได้รับเสียง (No Speech Detected)"; } else { showError("ข้อผิดพลาด: " + event.error); } }; btnMicToggle.addEventListener('click', function () { if (isRecording) { recognition.stop(); } else { if (micStatusContainer) micStatusContainer.innerText = 'กำลังเริ่ม... (Starting...)'; try { recognition.start(); } catch (e) { showError("ไม่สามารถเริ่มไมค์ได้: " + e.message); } } }); } else { btnMicToggle.style.display = 'none'; showError("เบราว์เซอร์นี้ไม่รองรับ Web Speech API กรุณาใช้ Chrome หรือ Edge"); } const videoElement = document.querySelector('.input_video'); const canvasElement = document.querySelector('.output_canvas'); let canvasCtx = null; if (!window.isSecureContext) { showError("Camera Error: App is NOT running in a Secure Context (HTTPS)."); } else if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showError("Camera Error: Browser API 'navigator.mediaDevices' is missing."); } if (canvasElement) { canvasCtx = canvasElement.getContext('2d'); } let lastActionTime = 0; const ACTION_COOLDOWN = 800; function onResults(results) { if (!canvasCtx) return; canvasCtx.save(); canvasCtx.translate(canvasElement.width, 0); canvasCtx.scale(-1, 1); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); if (results.multiHandLandmarks) { for (const landmarks of results.multiHandLandmarks) { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, { color: '#00FF00', lineWidth: 2 }); drawLandmarks(canvasCtx, landmarks, { color: '#FF0000', lineWidth: 1 }); detectGesture(landmarks); } } canvasCtx.restore(); } function detectGesture(landmarks) { const thumbTip = landmarks[4]; const indexTip = landmarks[8]; const distance = Math.sqrt( Math.pow(thumbTip.x - indexTip.x, 2) + Math.pow(thumbTip.y - indexTip.y, 2) ); const cursorX_norm = 1 - ((thumbTip.x + indexTip.x) / 2); const cursorY_norm = (thumbTip.y + indexTip.y) / 2; const rect = canvasElement.getBoundingClientRect(); const clientX = rect.left + (cursorX_norm * rect.width); const clientY = rect.top + (cursorY_norm * rect.height); const midX = (thumbTip.x + indexTip.x) / 2; const midY = (thumbTip.y + indexTip.y) / 2; const PINCH_THRESHOLD = 0.06; canvasCtx.beginPath(); canvasCtx.arc(midX * canvasElement.width, midY * canvasElement.height, 5, 0, 2 * Math.PI); canvasCtx.fillStyle = distance < PINCH_THRESHOLD ? "rgba(0, 255, 0, 0.5)" : "rgba(255, 255, 255, 0.5)"; canvasCtx.fill(); if (distance < PINCH_THRESHOLD) { const element = document.elementFromPoint(clientX, clientY); if (element && element.classList.contains('gesture-box')) { element.classList.add('active'); setTimeout(() => element.classList.remove('active'), 200); const now = Date.now(); if (now - lastActionTime > ACTION_COOLDOWN) { const action = element.getAttribute('data-action'); triggerAction(action); lastActionTime = now; } } } } // --- อัปเดตฟังก์ชันเพื่อค้นหาช่องสี่เหลี่ยม/วงกลมโดยเฉพาะ --- function getVisual(el) { if (el.type === 'checkbox' || el.type === 'radio') { // ดึง element ตัวถัดไป (ซึ่งเราเขียน span จำลองสี่เหลี่ยมไว้ใน HTML) if (el.nextElementSibling) { return el.nextElementSibling; } return el.parentElement; // กรณีฉุกเฉิน } return el; // ถ้าเป็นช่อง Text ให้ล็อคที่ช่อง Text } function triggerAction(action) { switch (action) { case 'CLEAR': const activeElement = document.activeElement; if (activeElement) { if (activeElement.type === 'text' || activeElement.tagName === 'TEXTAREA') { activeElement.value = ''; } else if (activeElement.type === 'checkbox' || activeElement.type === 'radio') { activeElement.checked = false; const parent = activeElement.parentElement; if (parent && parent.classList.contains('circle-option')) { const span = parent.querySelector('span'); if (span) span.style = ""; } } activeElement.classList.remove('low-confidence-highlight'); if (activeElement.nextElementSibling && activeElement.nextElementSibling.classList.contains('checkbox-visual')) { activeElement.nextElementSibling.classList.remove('low-confidence-highlight'); } } break; case 'SCROLL_UP': document.querySelector('.document-pane').scrollBy({ top: -200, behavior: 'smooth' }); break; case 'SCROLL_DOWN': document.querySelector('.document-pane').scrollBy({ top: 200, behavior: 'smooth' }); break; case 'PREV': moveFocus(-1); break; case 'NEXT': moveFocus(1); break; case 'SELECT': const active = document.activeElement; if (active && (active.type === 'checkbox' || active.type === 'radio')) { active.click(); // ให้วงกลมกระพริบที่กรอบสี่เหลี่ยม ไม่ใช่ครอบทั้งประโยค const visualEl = getVisual(active); if (visualEl) { visualEl.classList.add('gesture-focus'); setTimeout(() => visualEl.classList.remove('gesture-focus'), 200); setTimeout(() => visualEl.classList.add('gesture-focus'), 400); } } else if (txtTranscription) { txtTranscription.select(); } break; case 'MIC_TOGGLE': const micBtn = document.getElementById('btn-mic-toggle'); if (micBtn) micBtn.click(); break; case 'SAVE': const downloadBtn = document.getElementById('btn-download-pdf'); const saveBtn = document.getElementById('btn-save-submit'); if (downloadBtn) { window.location.href = downloadBtn.href; } else if (saveBtn) { const form = saveBtn.closest('form'); if (form) form.submit(); else saveBtn.click(); } break; } } function moveFocus(direction) { const inputs = Array.from(document.querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="radio"]')); const current = document.activeElement; const currentIndex = inputs.indexOf(current); // ถอด Focus เดิมออก if (current) { const currentVisual = getVisual(current); if (currentVisual) currentVisual.classList.remove('gesture-focus'); } let nextIndex = 0; if (currentIndex !== -1) { nextIndex = currentIndex + direction; } if (nextIndex < 0) nextIndex = inputs.length - 1; if (nextIndex >= inputs.length) nextIndex = 0; if (nextIndex >= 0 && nextIndex < inputs.length) { const target = inputs[nextIndex]; target.focus(); // โฟกัส Input ซ่อนไว้ // ล็อคเป้ากรอบแดงไปที่กล่องสี่เหลี่ยม / วงกลม / Text const targetVisual = getVisual(target); if (targetVisual) { targetVisual.classList.add('gesture-focus'); targetVisual.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } } if (typeof Hands !== 'undefined') { const hands = new Hands({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; } }); hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 }); hands.onResults(onResults); if (videoElement) { const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({ image: videoElement }); }, width: 480, height: 360 }); camera.start() .catch(err => { const overlay = document.querySelector('.camera-overlay-text'); if (overlay) { overlay.innerHTML = `Camera Error: ${err.message || err.name}. Please allow camera access.`; } showError("Camera Error: " + (err.message || err.name)); }); } } else { console.warn("MediaPipe Hands library not loaded."); } });