Skip to content

Commit df593be

Browse files
committed
Added trial history feature
1 parent 353d6a8 commit df593be

13 files changed

Lines changed: 513 additions & 6 deletions

File tree

client/app.css

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,99 @@
11871187
line-height: 1.3;
11881188
}
11891189

1190+
.bespoke .pl-monitor-actions {
1191+
margin-top: var(--UI-Spacing-spacing-s);
1192+
display: flex;
1193+
justify-content: flex-end;
1194+
}
1195+
1196+
/* Trial history modal */
1197+
/* Override modal content centering for history modal */
1198+
.bespoke .modal-content:has(.pl-history-modal) {
1199+
align-items: stretch;
1200+
padding-top: 0;
1201+
padding-bottom: var(--UI-Spacing-spacing-ml);
1202+
padding-left: var(--UI-Spacing-spacing-mxl);
1203+
padding-right: var(--UI-Spacing-spacing-mxl);
1204+
}
1205+
1206+
.bespoke .pl-history-modal {
1207+
display: flex;
1208+
flex-direction: column;
1209+
gap: var(--UI-Spacing-spacing-m);
1210+
width: 100%;
1211+
}
1212+
1213+
.bespoke .pl-history-toolbar {
1214+
display: flex;
1215+
align-items: center;
1216+
justify-content: space-between;
1217+
gap: var(--UI-Spacing-spacing-s);
1218+
padding-bottom: var(--UI-Spacing-spacing-xs);
1219+
border-bottom: 1px solid var(--Colors-Stroke-Default);
1220+
}
1221+
1222+
.bespoke .pl-history-toolbar-actions {
1223+
display: flex;
1224+
gap: var(--UI-Spacing-spacing-xs);
1225+
}
1226+
1227+
.bespoke .pl-history-scroller {
1228+
position: relative;
1229+
max-height: calc(100vh - 300px);
1230+
min-height: 200px;
1231+
overflow: auto;
1232+
border: 1px solid var(--Colors-Stroke-Default);
1233+
border-radius: var(--UI-Radius-radius-m);
1234+
background: var(--Colors-Backgrounds-Main-Top);
1235+
box-shadow: inset 0 2px 4px var(--Colors-Shadow-Soft);
1236+
scrollbar-gutter: stable;
1237+
overscroll-behavior: contain;
1238+
}
1239+
1240+
.bespoke .pl-history-viewport {
1241+
width: 100%;
1242+
}
1243+
1244+
.bespoke .pl-history-items {
1245+
position: absolute;
1246+
inset: 0;
1247+
}
1248+
1249+
.bespoke .pl-history-row {
1250+
position: absolute;
1251+
left: 0;
1252+
right: 0;
1253+
height: 28px;
1254+
display: grid;
1255+
grid-template-columns: 84px minmax(0, 1fr);
1256+
align-items: center;
1257+
gap: var(--UI-Spacing-spacing-s);
1258+
padding: 0 var(--UI-Spacing-spacing-m);
1259+
border-bottom: 1px solid var(--Colors-Stroke-Default);
1260+
overflow-wrap: anywhere;
1261+
transition: background-color 0.15s ease;
1262+
}
1263+
1264+
.bespoke .pl-history-row:hover {
1265+
background-color: var(--Colors-Backgrounds-Main-Medium);
1266+
}
1267+
1268+
.bespoke .pl-history-row:last-child {
1269+
border-bottom: none;
1270+
}
1271+
1272+
.bespoke .pl-history-trial {
1273+
color: var(--Colors-Text-Body-Medium);
1274+
font-variant-numeric: tabular-nums;
1275+
white-space: nowrap;
1276+
}
1277+
1278+
.bespoke .pl-history-outcome {
1279+
color: var(--Colors-Text-Body-Default);
1280+
min-width: 0;
1281+
}
1282+
11901283
/* Chart context */
11911284
.bespoke .pl-chart-header {
11921285
display: flex;
@@ -1336,4 +1429,4 @@
13361429

13371430
.bespoke [data-tooltip]:hover::after {
13381431
opacity: 1;
1339-
}
1432+
}

client/app.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import simulateTwoTrials from './src/probability-lab/engine/simulate-two.js';
1313
import { createRunner } from './src/probability-lab/engine/runner.js';
1414
import render from './src/probability-lab/ui/render.js';
1515
import { createStore } from './src/probability-lab/state/store.js';
16+
import { IndexHistory, PackedPairHistory } from './src/probability-lab/state/trial-history.js';
17+
import { createHistoryView } from './src/probability-lab/ui/history-view.js';
1618
import NumericSlider from './design-system/components/numeric-slider/numeric-slider.js';
1719

1820
function $(id) {
@@ -58,6 +60,15 @@ const els = {
5860
deviceView: $('pl-device-view'),
5961
trials: $('pl-trials'),
6062
last: $('pl-last'),
63+
historyButton: $('pl-history'),
64+
historyModal: $('pl-history-modal'),
65+
historySummary: $('pl-history-summary'),
66+
historyEmpty: $('pl-history-empty'),
67+
historyScroller: $('pl-history-scroller'),
68+
historyViewport: $('pl-history-viewport'),
69+
historyItems: $('pl-history-items'),
70+
historyJumpTop: $('pl-history-jump-top'),
71+
historyJumpLatest: $('pl-history-jump-latest'),
6172

6273
barChart: $('pl-bar-chart'),
6374
lineChart: $('pl-line-chart'),
@@ -87,6 +98,8 @@ const sliderInstances = {
8798
// Store speed slider instance
8899
let speedSlider = null;
89100
let settingsModal = null;
101+
let historyModal = null;
102+
let historyView = null;
90103

91104
/**
92105
* Generates a random numeric seed string for the RNG
@@ -137,6 +150,7 @@ const store = createStore({
137150
counts: [],
138151
lastIndex: null,
139152
history: [],
153+
trialHistory: new IndexHistory(),
140154
eventSelected: new Set(),
141155
},
142156

@@ -165,6 +179,7 @@ const store = createStore({
165179
lastA: null,
166180
lastB: null,
167181
selectedCell: null,
182+
trialHistory: new PackedPairHistory(),
168183
},
169184
});
170185

@@ -176,6 +191,9 @@ const runner = createRunner(store.getState().running, {
176191
const state = store.getState();
177192
render(els, state);
178193
applyVisibility(state);
194+
if (historyModal?.isOpen && historyView) {
195+
historyView.sync(state, { preserveScroll: true });
196+
}
179197
activityLogger.maybeLogStatus(state);
180198
},
181199
onDone: updateControls,
@@ -210,6 +228,7 @@ function resetSingleSimulation() {
210228
draft.single.counts = Array(def.labels.length).fill(0);
211229
draft.single.lastIndex = null;
212230
draft.single.history = [];
231+
draft.single.trialHistory?.clear();
213232
draft.single.eventSelected = filtered;
214233
});
215234

@@ -247,6 +266,7 @@ function resetTwoSimulation() {
247266
draft.two.lastA = null;
248267
draft.two.lastB = null;
249268
draft.two.selectedCell = null;
269+
draft.two.trialHistory?.clear();
250270
});
251271
}
252272

@@ -269,6 +289,9 @@ function resetSimulation({ reason = 'unknown', logSettings = false } = {}) {
269289
render(els, store.getState());
270290
applyVisibility(store.getState());
271291
syncUiFromState();
292+
if (historyModal?.isOpen && historyView) {
293+
historyView.sync(store.getState(), { scrollToLatest: true });
294+
}
272295

273296
const currentState = store.getState();
274297
if (logSettings) {
@@ -800,6 +823,42 @@ function initSettingsModal() {
800823
}
801824
}
802825

826+
function initHistoryModal() {
827+
if (historyModal) return;
828+
if (!els.historyModal) return;
829+
830+
const modalContent = els.historyModal;
831+
const wasHidden = modalContent.hidden;
832+
modalContent.hidden = true;
833+
834+
historyModal = new Modal({
835+
size: 'medium',
836+
title: 'Trial History',
837+
content: modalContent,
838+
onOpen: () => {
839+
if (historyView) historyView.sync(store.getState(), { scrollToLatest: true });
840+
},
841+
});
842+
843+
if (wasHidden) modalContent.hidden = false;
844+
845+
historyView = createHistoryView({
846+
summaryEl: els.historySummary,
847+
emptyEl: els.historyEmpty,
848+
scrollerEl: els.historyScroller,
849+
viewportEl: els.historyViewport,
850+
itemsEl: els.historyItems,
851+
jumpTopButton: els.historyJumpTop,
852+
jumpLatestButton: els.historyJumpLatest,
853+
});
854+
855+
if (els.historyButton) {
856+
els.historyButton.addEventListener('click', () => {
857+
historyModal.open();
858+
});
859+
}
860+
}
861+
803862
function syncUiFromState() {
804863
const state = store.getState();
805864
els.stepSize.value = String(state.stepSize);
@@ -887,6 +946,7 @@ function updateTwoControlsForRelationship() {
887946

888947
function initEventListeners() {
889948
initSettingsModal();
949+
initHistoryModal();
890950

891951
els.spinnerSectors.addEventListener('change', () => {
892952
const clamped = clamp(parseInt(els.spinnerSectors.value, 10), 2, 12);
@@ -1057,6 +1117,9 @@ function initEventListeners() {
10571117
const state = store.getState();
10581118
render(els, state);
10591119
applyVisibility(state);
1120+
if (historyModal?.isOpen && historyView) {
1121+
historyView.sync(state, { preserveScroll: true });
1122+
}
10601123
});
10611124
}
10621125

client/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"name": "Exam",
66
"icon": "📚",
77
"outcomes": ["Pass", "Fail"],
8-
"probabilities": [0.7, 0.3]
8+
"probabilities": [0.5, 0.5]
99
},
1010
"visualElements": {
1111
"editExperimentButton": true,

client/index.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,31 @@ <h2 class="heading-xsmall">Live Trial</h2>
161161
</div>
162162
</div>
163163
</div>
164+
<div class="pl-monitor-actions">
165+
<button id="pl-history" class="button button-secondary button-small" type="button" disabled aria-haspopup="dialog" aria-controls="pl-history-modal">
166+
History
167+
</button>
168+
</div>
164169
</section>
165170
</div>
166171

172+
<div id="pl-history-modal" class="pl-history-modal" hidden>
173+
<div class="pl-history-toolbar">
174+
<div id="pl-history-summary" class="body-xsmall pl-muted">Trials: 0</div>
175+
<div class="pl-history-toolbar-actions">
176+
<button id="pl-history-jump-top" class="button button-tertiary button-small" type="button">Top</button>
177+
<button id="pl-history-jump-latest" class="button button-tertiary button-small" type="button">Latest</button>
178+
</div>
179+
</div>
180+
181+
<div id="pl-history-empty" class="body-small pl-muted">No trials yet. Run a trial to start building history.</div>
182+
183+
<div id="pl-history-scroller" class="pl-history-scroller" hidden>
184+
<div id="pl-history-viewport" class="pl-history-viewport"></div>
185+
<div id="pl-history-items" class="pl-history-items" role="list"></div>
186+
</div>
187+
</div>
188+
167189
<div id="pl-settings-modal" class="pl-settings-modal" hidden>
168190
<p class="pl-helper-text body-xsmall">Changing settings resets trials for the current experiment.</p>
169191

client/src/probability-lab/engine/simulate-single.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { sampleIndex } from '../domain/cdf.js';
1717
export default function simulateSingleTrials(stateSlice, rng, count) {
1818
const def = stateSlice.definition;
1919
if (!def) return;
20+
const trialHistory = stateSlice.trialHistory;
2021

2122
for (let i = 0; i < count; i += 1) {
2223
const idx = sampleIndex(rng, def.cdf);
2324
stateSlice.counts[idx] += 1;
2425
stateSlice.trials += 1;
2526
stateSlice.lastIndex = idx;
27+
if (trialHistory) trialHistory.push(idx);
2628
}
2729

2830
if (stateSlice.trials > 0) {
@@ -33,4 +35,3 @@ export default function simulateSingleTrials(stateSlice, rng, count) {
3335
}
3436
}
3537
}
36-

client/src/probability-lab/engine/simulate-two.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default function simulateTwoTrials(stateSlice, rng, count) {
5656
if (!defA || !defB) return;
5757

5858
const relationship = stateSlice.relationship;
59+
const trialHistory = stateSlice.trialHistory;
5960

6061
for (let i = 0; i < count; i += 1) {
6162
const a = sampleIndex(rng, defA.cdf);
@@ -88,6 +89,6 @@ export default function simulateTwoTrials(stateSlice, rng, count) {
8889
stateSlice.trials += 1;
8990
stateSlice.lastA = a;
9091
stateSlice.lastB = b;
92+
if (trialHistory) trialHistory.pushPair(a, b);
9193
}
9294
}
93-

0 commit comments

Comments
 (0)