Files
charade_experiment/experiment-scripts/static/script.js
2025-10-21 19:40:02 +02:00

482 lines
16 KiB
JavaScript

// 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, player1Ip, player2Ip, activePlayerIp) {
try {
const response = await fetch("/tracking/start", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
group_id: groupId,
condition: condition,
player1_ip: player1Ip,
player2_ip: player2Ip,
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, ipPlayer1, ipPlayer2, 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();
});