nssi-fmri-demo/index.html
2024-07-29 10:11:25 +08:00

151 lines
5.3 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Combandit demo</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<style>
body {
font-family: Arial, sans-serif;
padding: 0;
width: 500px;
margin: 50px auto;
background-color: #f0f0f0;
}
h1, h2 {
text-align: center;
}
.wrapper {
margin: 0 auto;
width: 320px;
height: 320px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
grid-auto-rows: minmax(100px, auto);
}
.gridcell, .gridcell-highlighted {
background-color: #fafafa;
border: 1px solid #ddd;
}
.gridcell-selected {
background-color: #d4380d88;
}
.gridcell-unselected {
background-color: #44444422;
}
.indicator {
margin-left: 50%;
margin-top: 50%;
transform: translate(-50%, -50%);
width: 15%;
height: 15%;
}
.gridcell-highlighted .indicator {
border: 2px solid #d4380d;
border-radius: 50%;
}
#avg-reward {
text-align: center;
font-size: large;
}
</style>
<script type="module">
import { shuffle, randint } from "./random.js";
import { ExpCombandit } from "./combandit.js";
import Utils from "./utils.js";
const combandit = new ExpCombandit({});
let combanditState = combandit.getInitialState();
const handler = {
nTrials: 0,
avgReward: 0,
getCell(idx) {
return document.querySelector(`#gridcell-${idx}`);
},
getCellList() {
const query = '.gridcell, .gridcell-highlighted, .gridcell-selected, .gridcell-unselected'
return Array.from(document.querySelectorAll(query));
},
init() {
this.orders = shuffle(Utils.range(4));
this.currentIdx = 0;
this.action = Array(4).fill(false);
document.querySelector('#prompt').innerHTML = '&nbsp;';
this.getCellList().forEach(cell => cell.className = 'gridcell');
this.highlightCurrent();
this.completed = false;
},
highlightCurrent() {
const { orders, currentIdx } = this;
const cell = this.getCell(orders[currentIdx]);
cell.className = 'gridcell-highlighted';
},
onChoice(choice) {
const { orders, currentIdx, completed } = this;
const cell = this.getCell(orders[currentIdx]);
this.action[orders[currentIdx]] = choice;
cell.className = choice ? 'gridcell-selected' : 'gridcell-unselected';
if (currentIdx < orders.length - 1) {
this.currentIdx++;
const nextCell = this.getCell(orders[this.currentIdx]);
nextCell.className = 'gridcell-highlighted';
} else if (currentIdx === orders.length - 1) {
let reward;
this.completed = true;
({ reward, nextState: combanditState } = combandit.step(combanditState, this.action));
this.avgReward = (this.avgReward * this.nTrials + reward) / (this.nTrials + 1);
this.nTrials++;
const rewardText = reward.toFixed(0).replace('-', '&minus;');
const avgRewardText = this.avgReward.toFixed(2).replace('-', '&minus;');
document.querySelector('#prompt').innerHTML = `You got ${rewardText} points!`;
document.querySelector('#avg-reward').innerHTML = `Avg reward: ${avgRewardText}, Trials: ${this.nTrials}`;
setTimeout(() => this.init(), 2000);
}
},
}
const keysHeld = new Set();
document.addEventListener('DOMContentLoaded', () => handler.init());
document.addEventListener('keydown', function(event) {
const keys = ['f', 'j', 'F', 'J'];
if (keys.includes(event.key) && !keysHeld.has(event.key) && !handler.completed) {
keysHeld.add(event.key);
handler.onChoice(event.key.toLowerCase() === 'f');
}
});
document.addEventListener('keyup', function(event) {
if (event.key.match(/[a-z]/i)) {
keysHeld.delete(event.key);
}
});
</script>
</head>
<body>
<h1>Press <span style="color: #d4380d;">F</span> or <span style="color: #444444;">J</span></h1>
<h2 id="prompt">&nbsp;</h2>
<div class="wrapper" id="grid">
<div class="gridcell" id="gridcell-0"><div class="indicator"></div></div>
<div class="gridcell" id="gridcell-1"><div class="indicator"></div></div>
<div class="gridcell" id="gridcell-2"><div class="indicator"></div></div>
<div class="gridcell" id="gridcell-3"><div class="indicator"></div></div>
</div>
<p id="avg-reward">Avg reward: 0</p>
</body>
</html>