added more functionality to web interface + bug fixes
This commit is contained in:
@ -247,6 +247,20 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Charades</h2>
|
<h2>Charades</h2>
|
||||||
|
|
||||||
|
<label for="group-id">Group ID</label>
|
||||||
|
<input type="text" id="group-id" placeholder="g1">
|
||||||
|
|
||||||
|
<label for="condition-mode">Condition/Mode</label>
|
||||||
|
<select id="condition-mode">
|
||||||
|
<option value="">-- Select Condition --</option>
|
||||||
|
<option value="MODE:1;1;1;0">Dynamic Face</option>
|
||||||
|
<option value="MODE:0;0;0;1">Dynamic Hand</option>
|
||||||
|
<option value="MODE:1;1;1;1">Dynamic Hand+Face</option>
|
||||||
|
<option value="MODE:1;0;0;0">Static Face</option>
|
||||||
|
<option value="MODE:0;0;0;1">Static Hand</option>
|
||||||
|
<option value="MODE:1;0;0;1">Static Hand+Face</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<label for="ip-player1">IP Player 1</label>
|
<label for="ip-player1">IP Player 1</label>
|
||||||
<input type="text" id="ip-player1" placeholder="10.42.0.38">
|
<input type="text" id="ip-player1" placeholder="10.42.0.38">
|
||||||
|
|
||||||
@ -267,10 +281,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="row" style="margin-bottom: 60px;">
|
<div class="row" style="margin-bottom: 20px;">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="time-s">Time (s)</label>
|
<label for="time-s">Time per Word (s)</label>
|
||||||
<input type="number" id="time-s" placeholder="30">
|
<input type="number" id="time-s" placeholder="30" value="30">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="total-duration">Total Duration (min)</label>
|
||||||
|
<input type="number" id="total-duration" placeholder="5" value="0" step="0.5">
|
||||||
|
<small style="color: #666; font-size: 0.85em;">0 = no limit</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -319,11 +338,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="total-timer" style="display: none; width: calc(100% - 40px); margin: 20px; padding: 30px; background-color: #4CAF50; color: white; border-radius: 12px; text-align: center; font-weight: bold; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);">
|
||||||
|
<div style="margin-bottom: 10px; font-size: 1.5em; opacity: 0.95;">Total Time Remaining</div>
|
||||||
|
<div id="total-timer-display" style="font-size: 4em; font-family: 'Courier New', monospace; letter-spacing: 0.1em;">00:00</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let runningWordIndex = -1;
|
let runningWordIndex = -1;
|
||||||
let runningWordList = [];
|
let runningWordList = [];
|
||||||
|
let experimentStartTime = null;
|
||||||
|
let totalDurationMs = 0;
|
||||||
|
|
||||||
function createWordItems() {
|
function createWordItems() {
|
||||||
const wordList = document.getElementById("word-list");
|
const wordList = document.getElementById("word-list");
|
||||||
@ -391,12 +416,44 @@
|
|||||||
function step(timestamp) {
|
function step(timestamp) {
|
||||||
if (last === undefined) {
|
if (last === undefined) {
|
||||||
last = timestamp;
|
last = timestamp;
|
||||||
|
experimentStartTime = timestamp;
|
||||||
runningWordIndex = 0;
|
runningWordIndex = 0;
|
||||||
newWord = true;
|
newWord = true;
|
||||||
lastWordStatus = -1; // First word has no previous word
|
lastWordStatus = -1; // First word has no previous word
|
||||||
}
|
}
|
||||||
const elapsed = timestamp - last;
|
const elapsed = timestamp - last;
|
||||||
last = timestamp;
|
last = timestamp;
|
||||||
|
|
||||||
|
// Check if total duration has been exceeded
|
||||||
|
if (totalDurationMs > 0) {
|
||||||
|
const totalElapsed = timestamp - experimentStartTime;
|
||||||
|
const remainingMs = totalDurationMs - totalElapsed;
|
||||||
|
|
||||||
|
// Update timer display
|
||||||
|
const remainingMinutes = Math.floor(remainingMs / 60000);
|
||||||
|
const remainingSeconds = Math.floor((remainingMs % 60000) / 1000);
|
||||||
|
const timerDisplay = document.getElementById("total-timer-display");
|
||||||
|
timerDisplay.textContent = `${remainingMinutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// Change color when time is running out
|
||||||
|
const timerContainer = document.getElementById("total-timer");
|
||||||
|
if (remainingMs < 60000) { // Less than 1 minute
|
||||||
|
timerContainer.style.backgroundColor = "#f44336"; // Red
|
||||||
|
} else if (remainingMs < 120000) { // Less than 2 minutes
|
||||||
|
timerContainer.style.backgroundColor = "#ff9800"; // Orange
|
||||||
|
} else {
|
||||||
|
timerContainer.style.backgroundColor = "#4CAF50"; // Green
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalElapsed >= totalDurationMs) {
|
||||||
|
console.log("Total duration exceeded, stopping experiment");
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
frameId = undefined;
|
||||||
|
document.getElementById("total-timer").style.display = "none";
|
||||||
|
alert("Experiment completed: Total duration reached");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const runningWord = runningWordList[runningWordIndex];
|
const runningWord = runningWordList[runningWordIndex];
|
||||||
const p = runningWord.querySelector("p");
|
const p = runningWord.querySelector("p");
|
||||||
@ -428,9 +485,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById("button-stop").addEventListener("click", () => {
|
||||||
|
// Stop the word list animation
|
||||||
|
if (frameId) {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
frameId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide timer
|
||||||
|
document.getElementById("total-timer").style.display = "none";
|
||||||
|
|
||||||
|
console.log("Experiment stopped");
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("button-save-to-file").addEventListener("click", () => {
|
document.getElementById("button-save-to-file").addEventListener("click", () => {
|
||||||
|
// Check if experiment is still running
|
||||||
|
if (frameId !== undefined) {
|
||||||
|
alert("Please stop the experiment before saving results.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's data to save
|
||||||
|
if (runningWordList.length === 0) {
|
||||||
|
alert("No data to save. Please run the experiment first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group ID for filename
|
||||||
|
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";
|
let data = "word;correct;time left\n";
|
||||||
for (let i = 0; i < runningWordList.length; ++i) {
|
|
||||||
|
// Only export words that were actually used (up to current index or all if finished)
|
||||||
|
const maxIndex = runningWordIndex >= runningWordList.length ? runningWordList.length : runningWordIndex;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIndex; ++i) {
|
||||||
const p = runningWordList[i].querySelector("p");
|
const p = runningWordList[i].querySelector("p");
|
||||||
const timer = runningWordList[i].querySelector(".word-timer");
|
const timer = runningWordList[i].querySelector(".word-timer");
|
||||||
|
|
||||||
@ -441,7 +531,18 @@
|
|||||||
data += `${word};${isCorrect};${timeLeft}\n`;
|
data += `${word};${isCorrect};${timeLeft}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open('data:text/csv;charset=utf-8,' + escape(data), '_blank');
|
// Create a download with proper filename
|
||||||
|
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", `${groupId}_results_${timestamp}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
console.log(`Saved ${maxIndex} out of ${runningWordList.length} words to CSV`);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("button-shuffle-words").addEventListener("click", async () => {
|
document.getElementById("button-shuffle-words").addEventListener("click", async () => {
|
||||||
@ -479,6 +580,36 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("button-create-word-items").addEventListener("click", () => {
|
document.getElementById("button-create-word-items").addEventListener("click", () => {
|
||||||
|
// Validate inputs
|
||||||
|
const groupId = document.getElementById("group-id").value;
|
||||||
|
const conditionMode = document.getElementById("condition-mode").value;
|
||||||
|
|
||||||
|
if (!groupId || !groupId.trim()) {
|
||||||
|
alert("Please enter a Group ID before starting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conditionMode) {
|
||||||
|
alert("Please select a Condition/Mode before starting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total duration in milliseconds
|
||||||
|
const totalDurationMin = parseFloat(document.getElementById("total-duration").value) || 0;
|
||||||
|
totalDurationMs = totalDurationMin * 60 * 1000;
|
||||||
|
|
||||||
|
// Show/hide timer based on whether duration is set
|
||||||
|
const timerContainer = document.getElementById("total-timer");
|
||||||
|
if (totalDurationMs > 0) {
|
||||||
|
timerContainer.style.display = "block";
|
||||||
|
timerContainer.style.backgroundColor = "#4CAF50";
|
||||||
|
const minutes = Math.floor(totalDurationMs / 60000);
|
||||||
|
const seconds = Math.floor((totalDurationMs % 60000) / 1000);
|
||||||
|
document.getElementById("total-timer-display").textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
timerContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
createWordItems();
|
createWordItems();
|
||||||
if (frameId) {
|
if (frameId) {
|
||||||
cancelAnimationFrame(frameId);
|
cancelAnimationFrame(frameId);
|
||||||
@ -486,7 +617,10 @@
|
|||||||
|
|
||||||
last = undefined;
|
last = undefined;
|
||||||
newWord = undefined;
|
newWord = undefined;
|
||||||
|
experimentStartTime = null;
|
||||||
frameId = requestAnimationFrame(step);
|
frameId = requestAnimationFrame(step);
|
||||||
|
|
||||||
|
console.log(`Experiment started: Group=${groupId}, Mode=${conditionMode}, Duration=${totalDurationMin}min`);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('button-word').addEventListener('click', () => {
|
document.getElementById('button-word').addEventListener('click', () => {
|
||||||
@ -572,88 +706,8 @@
|
|||||||
body: JSON.stringify(dataOther),
|
body: JSON.stringify(dataOther),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
// console.log("Request complete! response:", res);
|
// console.log("Request complete! response:", res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="margin-top: 40px; background-color: #f9f9f9; border-left: 4px solid #4CAF50; padding: 20px;">
|
|
||||||
<h2>How to Use This Interface</h2>
|
|
||||||
|
|
||||||
<div class="flex">
|
|
||||||
<div class="container" style="background-color: transparent; box-shadow: none;">
|
|
||||||
<h3>Setting Up a Charades Session</h3>
|
|
||||||
<ol>
|
|
||||||
<li><strong>Configure Player IPs</strong>
|
|
||||||
<ul>
|
|
||||||
<li>Enter the IP addresses of Player 1 and Player 2 VR headsets</li>
|
|
||||||
<li>These should match the IPs you used in the <code>control.py</code> commands</li>
|
|
||||||
<li><strong>Note:</strong> The default placeholders are examples - use your actual headset IPs</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>Prepare Word List</strong>
|
|
||||||
<ul>
|
|
||||||
<li>Copy your word list from <code>word-list.txt</code> and paste it into the large text area</li>
|
|
||||||
<li>Click the <strong>"Shuffle"</strong> button to randomize the word order</li>
|
|
||||||
<li>Click the <strong>"Modify"</strong> button to generate interactive word items</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>Set Game Parameters</strong>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Current Player For Acting:</strong> Select which player will be acting out the words</li>
|
|
||||||
<li><strong>Time (s):</strong> Set the time limit for each word (e.g., 30 seconds)</li>
|
|
||||||
<li><strong>Last Word Status:</strong> Set to "None" for the first word</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" style="background-color: transparent; box-shadow: none;">
|
|
||||||
<h3>Running the Experiment</h3>
|
|
||||||
|
|
||||||
<h4>Manual Mode (Individual Words)</h4>
|
|
||||||
<ol>
|
|
||||||
<li>Enter a word in the "Word" field</li>
|
|
||||||
<li>Select the acting player (Player 1 or Player 2)</li>
|
|
||||||
<li>Set the time limit</li>
|
|
||||||
<li>Click <strong>"Send"</strong> to transmit the word to the VR headsets</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h4>Automatic Mode (Word List)</h4>
|
|
||||||
<ol>
|
|
||||||
<li>After clicking "Modify" with your word list, the interface shows all words with timers</li>
|
|
||||||
<li>The system automatically starts with the first word and counts down</li>
|
|
||||||
<li><strong>During the session:</strong>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Mark words correct:</strong> Hover over a word and check the checkbox if guessed correctly</li>
|
|
||||||
<li><strong>Visual indicators:</strong> ▶ = Current word, ✅ = Correct, ❌ = Time expired</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>Stop the session:</strong> Click <strong>"Stop"</strong> to end early</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" style="background-color: transparent; box-shadow: none;">
|
|
||||||
<h3>Exporting Results</h3>
|
|
||||||
<ol>
|
|
||||||
<li>After completing the word list, click <strong>"Save as CSV"</strong></li>
|
|
||||||
<li>This downloads a CSV file with:
|
|
||||||
<ul>
|
|
||||||
<li>Word name</li>
|
|
||||||
<li>Whether it was guessed correctly (true/false)</li>
|
|
||||||
<li>Time remaining when completed</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h3>Troubleshooting</h3>
|
|
||||||
<ul>
|
|
||||||
<li>If words don't appear in VR headsets, check that <code>control.py</code> commands were sent correctly</li>
|
|
||||||
<li>If timer doesn't start, ensure you clicked "Modify" after entering the word list</li>
|
|
||||||
<li>If players can't see each other, verify the MODE commands were sent with correct parameters</li>
|
|
||||||
<li>If shuffle doesn't work, make sure the word list isn't currently running (click "Stop" first)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user