// Global state variables let runningWordIndex = -1; let runningWordList = []; let experimentStartTime = null; let totalDurationMs = 0; let frameId = undefined; let last = undefined; let newWord; let lastWordStatus = -1; let experimentRunning = false; // Update button states based on experiment status function updateButtonStates() { document.getElementById("btn-stop").disabled = !experimentRunning; document.getElementById("btn-save").disabled = experimentRunning || runningWordList.length === 0; document.getElementById("btn-start").disabled = experimentRunning; document.getElementById("btn-shuffle").disabled = experimentRunning; } // Create word items from word list function createWordItems() { const wordList = document.getElementById("word-list"); const text = wordList.value; const display = document.getElementById("word-list-display"); display.innerHTML = ""; runningWordList = []; runningWordIndex = -1; const words = text.trim().split('\n').filter(w => w.trim()); words.forEach((word, index) => { const div = document.createElement("div"); div.classList.add("word-item"); div.setAttribute("data-word", word.trim()); div.setAttribute("data-index", index); const content = document.createElement("div"); content.classList.add("word-content"); const indexSpan = document.createElement("span"); indexSpan.classList.add("word-index"); indexSpan.textContent = `${(index + 1).toString().padStart(2, '0')}.`; const textSpan = document.createElement("span"); textSpan.classList.add("word-text"); textSpan.textContent = word.trim(); content.appendChild(indexSpan); content.appendChild(textSpan); const timer = document.createElement("div"); timer.classList.add("word-timer"); const timeSeconds = parseFloat(document.getElementById("time-s").value); timer.textContent = timeSeconds.toFixed(1) + "s"; timer.setAttribute("data-remaining", timeSeconds); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.classList.add("word-checkbox"); checkbox.addEventListener("change", () => { if (checkbox.checked) { div.classList.add("correct"); div.classList.remove("incorrect"); } else { div.classList.remove("correct"); } }); div.appendChild(content); div.appendChild(timer); div.appendChild(checkbox); display.appendChild(div); runningWordList.push(div); }); } // Main animation loop function step(timestamp) { if (last === undefined) { last = timestamp; experimentStartTime = timestamp; runningWordIndex = 0; newWord = true; lastWordStatus = -1; } const elapsed = timestamp - last; last = timestamp; // Update total timer const timerDisplay = document.getElementById("timer-display"); if (totalDurationMs > 0) { const totalElapsed = timestamp - experimentStartTime; const remainingMs = totalDurationMs - totalElapsed; const remainingMinutes = Math.floor(remainingMs / 60000); const remainingSeconds = Math.floor((remainingMs % 60000) / 1000); timerDisplay.textContent = `${remainingMinutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; timerDisplay.className = "timer-display"; if (remainingMs < 60000) { timerDisplay.classList.add("danger"); } else if (remainingMs < 120000) { timerDisplay.classList.add("warning"); } if (totalElapsed >= totalDurationMs) { stopExperiment(); alert("Experiment completed: Total duration reached"); return; } } if (runningWordIndex >= runningWordList.length) { stopExperiment(); return; } const currentWordDiv = runningWordList[runningWordIndex]; const timer = currentWordDiv.querySelector(".word-timer"); const checkbox = currentWordDiv.querySelector('input[type="checkbox"]'); let remainingTime = parseFloat(timer.getAttribute("data-remaining")) - (elapsed / 1000); if (newWord) { newWord = false; const word = currentWordDiv.getAttribute("data-word"); sendNewWord(word, lastWordStatus, remainingTime); // Remove active class from all runningWordList.forEach(div => div.classList.remove("active")); currentWordDiv.classList.add("active"); } if (checkbox.checked || remainingTime < 0) { if (remainingTime < 0) remainingTime = 0; lastWordStatus = checkbox.checked ? 1 : 0; timer.textContent = remainingTime.toFixed(1) + "s"; currentWordDiv.classList.remove("active"); if (checkbox.checked) { currentWordDiv.classList.add("correct"); } else { currentWordDiv.classList.add("incorrect"); } runningWordIndex++; newWord = true; } else { timer.textContent = remainingTime.toFixed(1) + "s"; timer.setAttribute("data-remaining", remainingTime); } if (runningWordIndex < runningWordList.length) { frameId = requestAnimationFrame(step); } else { stopExperiment(); } } // Stop the experiment function stopExperiment() { if (frameId) { cancelAnimationFrame(frameId); frameId = undefined; } experimentRunning = false; updateButtonStates(); // Stop tracking recording stopTracking(); console.log("Experiment stopped"); } // Start tracking recording async function startTracking(groupId, condition, activePlayerIp) { try { const response = await fetch("/tracking/start", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ group_id: groupId, condition: condition, active_player_ip: activePlayerIp }), }); const result = await response.json(); console.log("Tracking started:", result.message); } catch (error) { console.error("Error starting tracking:", error); } } // Stop tracking recording async function stopTracking() { try { const response = await fetch("/tracking/stop", { method: "POST", }); const result = await response.json(); console.log("Tracking stopped:", result.message); } catch (error) { console.error("Error stopping tracking:", error); } } // Download tracking data async function downloadTracking() { try { const response = await fetch("/tracking/download"); if (response.ok) { // Get filename from Content-Disposition header const disposition = response.headers.get('Content-Disposition'); let filename = 'tracking_data.csv'; if (disposition && disposition.includes('filename=')) { filename = disposition.split('filename=')[1].replace(/"/g, ''); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); console.log("Tracking data downloaded"); } else { const result = await response.json(); alert("Error: " + (result.message || "Failed to download tracking data")); } } catch (error) { console.error("Error downloading tracking:", error); alert("Failed to download tracking data: " + error.message); } } // Send word to VR headsets function sendNewWord(word, lastWordStatus, timeSeconds) { const isPlayer1 = document.querySelector('input[name="active-player"]:checked').value === "player1"; const ipPlayer1 = document.getElementById("ip-player1").value; const ipPlayer2 = document.getElementById("ip-player2").value; const target = isPlayer1 ? ipPlayer1 : ipPlayer2; const targetOther = isPlayer1 ? ipPlayer2 : ipPlayer1; fetch("/word", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ target: target, lastWordStatus: lastWordStatus, timeSeconds: timeSeconds, word: word, }), }); fetch("/word", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ target: targetOther, lastWordStatus: lastWordStatus, timeSeconds: 0.0, word: "", }), }); } // Send VR headset configuration (IP and MODE) async function sendVRConfig() { const ipPlayer1 = document.getElementById("ip-player1").value; const ipPlayer2 = document.getElementById("ip-player2").value; const mode = document.getElementById("experiment-mode").value; const serverIp = document.getElementById("server-ip").value; if (!ipPlayer1 || !ipPlayer2) { showConfigStatus("Please enter both player IP addresses", "error"); return; } if (!serverIp) { showConfigStatus("Please enter the server IP address", "error"); return; } try { const response = await fetch("/send-config", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ player1_ip: ipPlayer1, player2_ip: ipPlayer2, server_ip: serverIp, mode: mode, }), }); const result = await response.json(); if (result.status === "ok") { showConfigStatus(`Configuration sent successfully! Mode: ${getModeDescription(mode)}`, "success"); } else { showConfigStatus("Failed to send configuration", "error"); } } catch (error) { console.error("Error sending configuration:", error); showConfigStatus("Error sending configuration: " + error.message, "error"); } } // Get human-readable mode description function getModeDescription(mode) { const modes = { "1;1;1;0": "Dynamic Face", "0;0;0;1": "Dynamic Hands", "1;1;1;1": "Dynamic Hands+Face", "1;0;0;0": "Static Face", "0;0;0;1": "Static Hands", "1;0;0;1": "Static Hands+Face" }; return modes[mode] || mode; } // Show configuration status message function showConfigStatus(message, type) { const statusDiv = document.getElementById("config-status"); statusDiv.textContent = message; statusDiv.className = `status-message ${type}`; statusDiv.style.display = "block"; // Auto-hide after 5 seconds setTimeout(() => { statusDiv.style.display = "none"; }, 5000); } // Event Listeners document.addEventListener("DOMContentLoaded", () => { // Start Experiment document.getElementById("btn-start").addEventListener("click", () => { const groupId = document.getElementById("group-id").value; if (!groupId || !groupId.trim()) { alert("Please enter a Group ID before starting."); return; } const wordList = document.getElementById("word-list").value; if (!wordList.trim()) { alert("Please enter a word list before starting."); return; } const totalDurationMin = parseFloat(document.getElementById("total-duration").value) || 0; totalDurationMs = totalDurationMin * 60 * 1000; const timerDisplay = document.getElementById("timer-display"); if (totalDurationMs > 0) { const minutes = Math.floor(totalDurationMs / 60000); const seconds = Math.floor((totalDurationMs % 60000) / 1000); timerDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; timerDisplay.className = "timer-display"; } else { timerDisplay.textContent = "∞"; timerDisplay.className = "timer-display"; } createWordItems(); if (frameId) cancelAnimationFrame(frameId); last = undefined; newWord = undefined; experimentStartTime = null; experimentRunning = true; updateButtonStates(); // Start tracking recording const isPlayer1 = document.querySelector('input[name="active-player"]:checked').value === "player1"; const ipPlayer1 = document.getElementById("ip-player1").value; const ipPlayer2 = document.getElementById("ip-player2").value; const activePlayerIp = isPlayer1 ? ipPlayer1 : ipPlayer2; const condition = document.getElementById("experiment-mode").value; const conditionName = getModeDescription(condition); startTracking(groupId, conditionName, activePlayerIp); frameId = requestAnimationFrame(step); console.log(`Experiment started: Group=${groupId}, Duration=${totalDurationMin}min, Condition=${conditionName}`); }); // Stop Experiment document.getElementById("btn-stop").addEventListener("click", stopExperiment); // Save Results document.getElementById("btn-save").addEventListener("click", () => { if (runningWordList.length === 0) { alert("No data to save. Please run the experiment first."); return; } const groupId = document.getElementById("group-id").value || "group"; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); let data = "word;correct;time_left\n"; const maxIndex = runningWordIndex >= runningWordList.length ? runningWordList.length : runningWordIndex; for (let i = 0; i < maxIndex; i++) { const div = runningWordList[i]; const word = div.getAttribute("data-word"); const checkbox = div.querySelector('input[type="checkbox"]'); const timer = div.querySelector(".word-timer"); const isCorrect = checkbox.checked; const timeLeft = parseFloat(timer.getAttribute("data-remaining")); data += `${word};${isCorrect};${timeLeft}\n`; } const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); const url = URL.createObjectURL(blob); link.href = url; link.download = `${groupId}_results_${timestamp}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log(`Saved ${maxIndex} out of ${runningWordList.length} words to CSV`); }); // Shuffle Words document.getElementById("btn-shuffle").addEventListener("click", async () => { const wordList = document.getElementById("word-list"); const text = wordList.value.trim(); if (!text) { alert("Please enter words to shuffle first."); return; } const words = text.split('\n').map(word => word.trim()).filter(word => word); try { const response = await fetch("/shuffle", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ words: words }), }); const result = await response.json(); if (result.status === "ok") { wordList.value = result.shuffled_words.join('\n'); } } catch (error) { console.error("Error shuffling words:", error); alert("Failed to shuffle words. Please try again."); } }); // Send VR Configuration document.getElementById("btn-send-config").addEventListener("click", sendVRConfig); // Download Tracking Data document.getElementById("btn-download-tracking").addEventListener("click", downloadTracking); // Initialize button states updateButtonStates(); });