UNPKG

@empirica/core

Version:
1,604 lines (1,570 loc) 68.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/player/classic/react/index.ts var react_exports = {}; __export(react_exports, { Attribute: () => Attribute, Attributes: () => Attributes, Chat: () => Chat, EmpiricaContext: () => EmpiricaContext, Game: () => Game, Lobby: () => Lobby, Player: () => Player, PlayerGame: () => PlayerGame, PlayerRound: () => PlayerRound, PlayerStage: () => PlayerStage, Quiz: () => Quiz, Round: () => Round, Scope: () => Scope2, Scopes: () => Scopes2, Slider: () => Slider, Stage: () => Stage, Step: () => Step, Steps: () => Steps, Sweeper: () => Sweeper, useGame: () => useGame, usePartModeCtx: () => usePartModeCtx, usePartModeCtxKey: () => usePartModeCtxKey, usePlayer: () => usePlayer, usePlayers: () => usePlayers, useRound: () => useRound, useStage: () => useStage, useStageTimer: () => useStageTimer }); module.exports = __toCommonJS(react_exports); // src/shared/attributes.ts var import_rxjs = require("rxjs"); // src/utils/console.ts var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; var logsMock; var colorHex = { [1 /* Bold */]: "font-weight: bold", [30 /* Black */]: "color: #000000", [31 /* Red */]: "color: #cc0000", [32 /* Green */]: "color: #4e9a06", [33 /* Yellow */]: "color: #c4a000", [34 /* Blue */]: "color: #729fcf", [35 /* Magenta */]: "color: #75507b", [36 /* Cyan */]: "color: #06989a", [37 /* White */]: "color: #d3d7cf", [90 /* DarkGray */]: "color: #555753" }; var levels = { trace: 0, debug: 1, log: 2, info: 2, warn: 3, error: 4 }; var reversLevels = {}; for (const key in levels) { reversLevels[levels[key]] = key; } var currentLevel = 2; function formatConsoleDate(date, level) { var hour = date.getHours(); var minutes = date.getMinutes(); var seconds = date.getSeconds(); var milliseconds = date.getMilliseconds(); const str = (hour < 10 ? "0" + hour : hour) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + ("00" + milliseconds).slice(-3); if (isBrowser) { const ts = colorize(str, 90 /* DarkGray */).concat(level); return [ts[0] + " " + level[0], ts[1], level[1]]; } return colorize(str, 90 /* DarkGray */).concat(level); } var createLogger = (lvl, level) => { return (...args) => { if (lvl < currentLevel) { return; } if (logsMock) { logsMock.log({ level: reversLevels[lvl], args }); return; } if (args.length === 1) { switch (typeof args[0]) { case "string": for (const line of args[0].split("\n")) { console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(line)); } return; case "object": if (args[0] instanceof Error) { const error2 = args[0]; const prettyErr = error2.name + ": " + error2.message.replace(new RegExp(`^${error2.name}[: ]*`), "") + "\n" + (error2.stack || "").split("\n").map((line) => line.trim()).map((line) => { if (line.startsWith(error2.name + ": " + error2.message)) return null; if (line.startsWith("at")) { return " " + line; } return line; }).filter(Boolean).join("\n"); for (const line of prettyErr.split("\n")) { console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(line)); } return; } } } console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(args)); }; }; function colorize(s, ...cc) { if (isBrowser) { const attr = []; for (const c of cc) { attr.push(colorHex[c]); } return [`%c${s}`, attr.join("; ")]; } let out = ""; for (const c of cc) { out += `\x1B[${c}m`; } out += `${s}\x1B[0m`; return [out]; } var trace = createLogger(0, colorize("TRC", 35 /* Magenta */)); var debug = createLogger(1, colorize("DBG", 33 /* Yellow */)); var log = createLogger(2, colorize("LOG", 33 /* Yellow */)); var info = createLogger(2, colorize("INF", 32 /* Green */)); var warn = createLogger(3, colorize("WRN", 36 /* Cyan */)); var error = createLogger(4, colorize("ERR", 31 /* Red */, 1 /* Bold */)); // src/shared/attributes.ts var Attributes = class { constructor(attributesObs, donesObs, setAttributes) { this.setAttributes = setAttributes; this.attrs = /* @__PURE__ */ new Map(); this.updates = /* @__PURE__ */ new Map(); attributesObs.subscribe({ next: ({ attribute, removed }) => { this.update(attribute, removed); } }); donesObs.subscribe({ next: (scopeIDs) => { this.next(scopeIDs); } }); } attribute(scopeID, key) { let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } let attr = scopeMap.get(key); if (!attr) { attr = new Attribute(this.setAttributes, scopeID, key); scopeMap.set(key, attr); } return attr; } attributes(scopeID) { let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } return Array.from(scopeMap.values()); } attributePeek(scopeID, key) { let scopeUpdateMap = this.updates.get(scopeID); if (scopeUpdateMap) { const updated = scopeUpdateMap.get(key); if (updated) { if (typeof updated === "boolean") { return; } else { if (!updated.val) { return; } else { const attr2 = new Attribute(this.setAttributes, scopeID, key); attr2._update(updated); return attr2; } } } } let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { return; } let attr = scopeMap.get(key); if (!attr) { return; } if (attr.value === void 0) { return; } return attr; } nextAttributeValue(scopeID, key) { const attr = this.attributePeek(scopeID, key); if (!attr) { return; } return attr.value; } update(attr, removed) { let nodeID = attr.nodeID; if (!nodeID) { if (!attr.node?.id) { error(`new attribute without node ID`); return; } nodeID = attr.node.id; } let scopeMap = this.updates.get(nodeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.updates.set(nodeID, scopeMap); } if (removed) { scopeMap.set(attr.key, true); } else { let key = attr.key; if (attr.index !== void 0 && attr.index !== null) { key = `${key}[${attr.index}]`; } scopeMap.set(key, attr); } } scopeWasUpdated(scopeID) { if (!scopeID) { return false; } return this.updates.has(scopeID); } next(scopeIDs) { for (const [scopeID, attrs] of this.updates) { if (!scopeIDs.includes(scopeID)) { continue; } let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } for (const [key, attrOrDel] of attrs) { if (typeof attrOrDel === "boolean") { let attr = scopeMap.get(key); if (attr) { attr._update(void 0); } } else { let attr = scopeMap.get(attrOrDel.key); if (!attr) { attr = new Attribute(this.setAttributes, scopeID, attrOrDel.key); scopeMap.set(attrOrDel.key, attr); } attr._update(attrOrDel); } } } for (const scopeID of scopeIDs) { this.updates.delete(scopeID); } } }; var Attribute = class { constructor(setAttributes, scopeID, key) { this.setAttributes = setAttributes; this.scopeID = scopeID; this.key = key; this.val = new import_rxjs.BehaviorSubject(void 0); } get id() { return this.attr?.id; } get createdAt() { return this.attr ? new Date(this.attr.createdAt) : null; } get obs() { return this.val; } get value() { return this.val.getValue(); } get nodeID() { return this.scopeID; } // items returns the attribute changes for the current attribute, if it is a // vector. Otherwise it returns null; get items() { if (!this.attrs) { return null; } return this.attrs; } set(value, ao) { const attrProps = this._prepSet(value, ao); if (!attrProps) { return; } this.setAttributes([attrProps]); trace(`SET ${this.key} = ${value} (${this.scopeID})`); } _prepSet(value, ao, item) { if (ao?.append !== void 0 && ao.index !== void 0) { error(`cannot set both append and index`); throw new Error(`cannot set both append and index`); } const serVal = JSON.stringify(value); if (!item && (ao?.index !== void 0 || ao?.append)) { let index = ao.index || 0; if (ao?.append) { index = this.attrs?.length || 0; } if (!this.attrs) { this.attrs = []; } if (!this.attrs[index]) { this.attrs[index] = new Attribute( this.setAttributes, this.scopeID, this.key ); } else { const existing = this.attrs[index]; if (existing && existing.serVal === serVal) { return; } } this.attrs[index]._prepSet(value, ao, true); const v = this._recalcVectorVal(); this.val.next(v); } else { if (this.serVal === serVal) { return; } this.val.next(value); } this.serVal = serVal; const attrProps = { key: this.key, nodeID: this.scopeID, val: serVal }; if (ao) { attrProps.private = ao.private; attrProps.protected = ao.protected; attrProps.immutable = ao.immutable; attrProps.ephemeral = ao.ephemeral; attrProps.append = ao.append; attrProps.index = ao.index; } return attrProps; } _recalcVectorVal() { return this.attrs.map( (a) => !a || a.val == void 0 ? null : a.value || null ); } // internal only _update(attr, item) { if (attr && this.attr && this.attr.id === attr.id) { return; } if (attr && attr.vector && !item) { if (attr.index === void 0) { error(`vector attribute missing index`); return; } if (this.attrs == void 0) { this.attrs = []; } while (this.attrs.length < attr.index + 1) { const newAttr2 = new Attribute( this.setAttributes, this.scopeID, this.key ); this.attrs.push(newAttr2); } const newAttr = new Attribute(this.setAttributes, this.scopeID, this.key); newAttr._update(attr, true); this.attrs[attr.index] = newAttr; const value2 = this._recalcVectorVal(); this.val.next(value2); return; } this.attr = attr; this.serVal = attr?.val === void 0 || attr?.val === null ? "" : attr.val; let value = void 0; if (this.attr?.val) { value = JSON.parse(this.attr.val); } this.val.next(value); } }; // src/shared/scopes.ts var import_rxjs2 = require("rxjs"); var Scopes = class { constructor(scopesObs, donesObs, ctx, kinds, attributes) { this.ctx = ctx; this.kinds = kinds; this.attributes = attributes; this.scopes = /* @__PURE__ */ new Map(); // newScopes is used to track scopes that have appeared for the first time. this.newScopes = /* @__PURE__ */ new Map(); this.scopesByKind = /* @__PURE__ */ new Map(); this.kindUpdated = /* @__PURE__ */ new Set(); scopesObs.subscribe({ next: ({ scope, removed }) => { this.update(scope, removed); } }); donesObs.subscribe({ next: (scopeIDs) => { this.next(scopeIDs); } }); } scope(id) { return this.scopes.get(id)?.getValue(); } scopeObs(id) { return this.scopes.get(id); } byKind(kind) { let map2 = this.scopesByKind.get(kind); if (!map2) { map2 = /* @__PURE__ */ new Map(); this.scopesByKind.set(kind, map2); } return map2; } kindWasUpdated(kind) { return this.kindUpdated.has(kind); } next(scopeIDs) { this.kindUpdated.clear(); for (const [_, scopeSubject] of this.scopes) { const scope = scopeSubject.getValue(); if ((scope._updated || this.attributes.scopeWasUpdated(scope.id)) && scopeIDs.includes(scope.id)) { scope._updated = false; scopeSubject.next(scope); } } } update(scope, removed) { const existing = this.scopes.get(scope.id)?.getValue(); if (removed) { if (!existing) { warn("scopes: missing scope on removal", scope.id, scope.kind); return; } existing._deleted = true; existing._updated = true; this.scopes.delete(scope.id); if (!scope.kind) { warn("scopes: scope missing kind on scope on removal"); return; } const kind2 = scope.kind; this.scopesByKind.get(kind2).delete(scope.id); this.kindUpdated.add(kind2); return; } if (existing) { existing._deleted = false; return; } if (!scope.kind) { warn("scopes: scope missing kind on scope"); return; } const kind = scope.kind; const scopeClass = this.kinds[kind]; if (!scopeClass) { warn(`scopes: unknown scope kind: ${scope.kind}`); return; } const obj = this.create(scopeClass, scope); const subj = new import_rxjs2.BehaviorSubject(obj); this.scopes.set(scope.id, subj); this.newScopes.set(scope.id, true); let skm = this.scopesByKind.get(kind); if (!skm) { skm = /* @__PURE__ */ new Map(); this.scopesByKind.set(kind, skm); } skm.set(scope.id, obj); obj._updated = true; this.kindUpdated.add(kind); } create(scopeClass, scope) { return new scopeClass(this.ctx, scope, this.attributes); } }; var Scope = class { constructor(ctx, scope, attributes) { this.ctx = ctx; this.scope = scope; this.attributes = attributes; /** * @internal */ this._deleted = false; /** * @internal */ this._updated = false; } get id() { return this.scope.id; } /** * @internal */ get kind() { return this.scope.kind; } get(key) { return this.attributes.attribute(this.scope.id, key).value; } getAttribute(key) { return this.attributes.attribute(this.scope.id, key); } obs(key) { return this.attributes.attribute(this.scope.id, key).obs; } set(keyOrAttributes, value, ao) { if (typeof keyOrAttributes === "string") { if (value === void 0) { value = null; } return this.attributes.attribute(this.scope.id, keyOrAttributes).set(value, ao); } const nextProps = []; for (const attr of keyOrAttributes) { const at = this.attributes.attribute(this.scope.id, attr.key)._prepSet(attr.value, attr.ao); if (!at) { continue; } nextProps.push(at); } if (nextProps.length === 0) { return; } this.attributes.setAttributes(nextProps); } append(key, value, ao) { if (!ao) { ao = {}; } ao.append = true; return this.attributes.attribute(this.scope.id, key).set(value, ao); } inspect() { const attrs = this.attributes.attributes(this.scope.id); const out = {}; for (const attr of attrs) { out[attr.key] = attr.value; } return out; } /** * @internal */ hasUpdated() { return this._updated || this.attributes.scopeWasUpdated(this.id); } }; // src/player/scopes.ts var Scopes2 = class extends Scopes { constructor(scopesObs, donesObs, ctx, kinds, attributes, steps) { super(scopesObs, donesObs, ctx, kinds, attributes); this.steps = steps; } create(scopeClass, scope) { return new scopeClass( this.ctx, scope, this, this.attributes, this.steps ); } }; var Scope2 = class extends Scope { constructor(ctx, scope, scopes, attributes, steps) { super(ctx, scope, attributes); this.scopes = scopes; this.steps = steps; } scopeByKey(key) { const id = this.get(key); if (!id || typeof id !== "string") { return; } return this.scopes.scope(id); } ticker(id) { return this.steps.step(id); } tickerByKey(key) { const id = this.get(key); if (!id || typeof id !== "string") { return; } return this.ticker(id); } }; // src/player/steps.ts var import_rxjs3 = require("rxjs"); var scheduled = []; var mockNow = null; function pnow() { if (mockNow !== null) { return mockNow; } else { return performance.now(); } } function timeout(callback, ms) { if (mockNow !== null) { const schd = { cb: callback, from: mockNow, dur: ms }; scheduled.push(schd); } else { setTimeout(callback, ms); } } var Step = class { constructor(step, ticker) { this.running = false; this.ticker = new import_rxjs3.BehaviorSubject(void 0); this.startAt = 0; this.endAt = 0; ticker.pipe((0, import_rxjs3.map)(this.recalc.bind(this))).subscribe({ next: (val) => { this.ticker.next(val); } }); this._update(step); } recalc(t) { if (!this.running) { return void 0; } return { started: t >= this.startAt, ended: t >= this.endAt, elapsed: Math.round(t - this.startAt), remaining: Math.round(this.endAt - t), duration: this.endAt - this.startAt }; } obs() { return this.ticker; } get current() { return this.recalc(pnow()); } // internal only _update(step) { if (!step.running) { this.running = false; this.ticker.next(void 0); return; } if (step.elapsed === null || step.remaining === null || step.elapsed === void 0 || step.remaining === void 0) { this.running = false; return; } const now = pnow(); this.startAt = now - step.elapsed * 1e3; this.endAt = now + step.remaining * 1e3; this.running = step.elapsed >= 0 && step.remaining >= 0; this.ticker.next(this.recalc(now)); } // internal only _stop() { this.running = false; this.ticker.next(void 0); } }; var Steps = class { constructor(stepsObs, donesObs) { this.steps = /* @__PURE__ */ new Map(); this.updates = /* @__PURE__ */ new Map(); this._hadUpdates = false; stepsObs.subscribe({ next: ({ step, removed }) => { this.update(step, removed); } }); donesObs.subscribe({ next: () => { this.next(); } }); this.ticker = new import_rxjs3.BehaviorSubject(Math.floor(pnow())); const controller = new AbortController(); timerInterval(1e3, controller.signal, (t) => { this.ticker.next(t); }); } step(stepID) { return this.steps.get(stepID); } hadUpdates() { const hadUpdates = this._hadUpdates; this._hadUpdates = false; return hadUpdates; } update(step, removed) { if (removed) { this.updates.set(step.id, true); } else { this.updates.set(step.id, step); } this._hadUpdates = true; } next() { for (const [id, stepOrDel] of this.updates) { let step = this.steps.get(id); if (typeof stepOrDel === "boolean") { if (step) { step._stop(); this.steps.delete(id); } } else { if (!step) { step = new Step(stepOrDel, this.ticker); this.steps.set(id, step); } step._update(stepOrDel); } } this.updates.clear(); } }; var root = typeof self === "object" && self.self == self ? self : typeof global === "object" && global.global == global ? global : {}; if (!root["requestAnimationFrame"]) { root["requestAnimationFrame"] = (cb) => cb(pnow()); } function timerInterval(ms = 1e3, signal, callback) { const start = Math.floor(pnow() / 1e3) * 1e3; function frame(time) { if (signal.aborted) return; callback(time); scheduleFrame(time); } function scheduleFrame(time) { const elapsed = time - start; const roundedElapsed = Math.round(elapsed / ms) * ms; const targetNext = start + roundedElapsed + ms; const delay = targetNext - pnow(); timeout(() => requestAnimationFrame(frame), delay); } scheduleFrame(start); } // src/player/classic/classic.ts var import_rxjs5 = require("rxjs"); // src/shared/globals.ts var import_rxjs4 = require("rxjs"); // src/player/classic/classic.ts var endedStatuses = ["ended", "terminated", "failed"]; var Game = class extends Scope2 { get hasEnded() { return endedStatuses.includes(this.get("status")); } get stage() { return this.scopeByKey("stageID"); } get round() { return this.stage?.round; } }; var Player = class extends Scope2 { get game() { const { game } = this.ctx; if (!game) { return; } const key = `playerGameID-${game.id}`; return this.scopeByKey(key); } get round() { const { stage } = this.ctx; if (!stage) { return; } const { round } = stage; if (!round) { return; } const key = `playerRoundID-${round.id}`; return this.scopeByKey(key); } get stage() { const { stage } = this.ctx; if (!stage) { return; } const key = `playerStageID-${stage.id}`; return this.scopeByKey(key); } hasUpdated() { if (super.hasUpdated()) { return true; } return Boolean( this.round?.hasUpdated() || this.stage?.hasUpdated() || this.game?.hasUpdated() ); } }; var PlayerGame = class extends Scope2 { }; var PlayerRound = class extends Scope2 { }; var PlayerStage = class extends Scope2 { }; var Round = class extends Scope2 { }; var Stage = class extends Scope2 { get round() { return this.scopeByKey("roundID"); } get timer() { return this.tickerByKey("timerID"); } }; // src/player/classic/react/EmpiricaContext.tsx var import_react11 = __toESM(require("react"), 1); // src/player/react/Consent.tsx var import_react = __toESM(require("react"), 1); var defaultTitle = "Do you consent to participate in this experiment?"; var defaultText = `This experiment is part of a scientific project. Your decision to participate in this experiment is entirely voluntary. There are no known or anticipated risks to participating in this experiment. There is no way for us to identify you. The only information we will have, in addition to your responses, is the timestamps of your interactions with our site. The results of our research may be presented at scientific meetings or published in scientific journals. Clicking on the "I AGREE" button indicates that you are at least 18 years of age, and agree to participate voluntary.`; var defaultButtonText = "I AGREE"; function Consent({ title = defaultTitle, text = defaultText, buttonText = defaultButtonText, onConsent }) { return /* @__PURE__ */ import_react.default.createElement( "div", { className: "relative h-full z-10 overflow-y-auto", "aria-labelledby": "modal-title", role: "dialog", "aria-modal": "true" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" }, /* @__PURE__ */ import_react.default.createElement( "div", { className: "absolute inset-0 bg-gray-500 bg-opacity-75 transition-opacity", "aria-hidden": "true" } ), /* @__PURE__ */ import_react.default.createElement( "span", { className: "hidden sm:inline-block sm:align-middle sm:h-screen", "aria-hidden": "true" }, "\u200B" ), /* @__PURE__ */ import_react.default.createElement("div", { className: "inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100" }, /* @__PURE__ */ import_react.default.createElement( "svg", { className: "h-6 w-6 text-green-600", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true" }, /* @__PURE__ */ import_react.default.createElement( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M5 13l4 4L19 7" } ) )), /* @__PURE__ */ import_react.default.createElement("div", { className: "mt-3 sm:mt-5" }, /* @__PURE__ */ import_react.default.createElement( "h3", { className: "text-lg text-center leading-6 font-medium text-gray-900", id: "modal-title" }, title ), /* @__PURE__ */ import_react.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "text-sm text-gray-500 text-justify" }, text)))), /* @__PURE__ */ import_react.default.createElement("div", { className: "mt-5 sm:mt-6" }, /* @__PURE__ */ import_react.default.createElement( "button", { type: "button", className: "inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-empirica-600 text-base font-medium text-white hover:bg-empirica-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-empirica-500 sm:text-sm", onClick: onConsent }, buttonText )))) ); } // src/player/react/Finished.tsx var import_react2 = __toESM(require("react"), 1); function Finished() { return /* @__PURE__ */ import_react2.default.createElement("div", { className: "h-full flex flex-col items-center justify-center" }, /* @__PURE__ */ import_react2.default.createElement("h2", { className: "font-medium text-gray-700" }, "Finished"), /* @__PURE__ */ import_react2.default.createElement("p", { className: "mt-2 text-gray-400" }, "Thank you for participating")); } // src/player/react/Loading.tsx var import_react3 = __toESM(require("react"), 1); function Loading() { return /* @__PURE__ */ import_react3.default.createElement("div", { className: "h-full w-full flex items-center justify-center" }, /* @__PURE__ */ import_react3.default.createElement( "svg", { width: "44", height: "44", viewBox: "0 0 44 44", xmlns: "http://www.w3.org/2000/svg", className: "text-empirica-200 stroke-current" }, /* @__PURE__ */ import_react3.default.createElement("g", { fill: "none", fillRule: "evenodd", strokeWidth: "2" }, /* @__PURE__ */ import_react3.default.createElement("circle", { cx: "22", cy: "22", r: "1" }, /* @__PURE__ */ import_react3.default.createElement( "animate", { attributeName: "r", begin: "0s", dur: "1.8s", values: "1; 20", calcMode: "spline", keyTimes: "0; 1", keySplines: "0.165, 0.84, 0.44, 1", repeatCount: "indefinite" } ), /* @__PURE__ */ import_react3.default.createElement( "animate", { attributeName: "stroke-opacity", begin: "0s", dur: "1.8s", values: "1; 0", calcMode: "spline", keyTimes: "0; 1", keySplines: "0.3, 0.61, 0.355, 1", repeatCount: "indefinite" } )), /* @__PURE__ */ import_react3.default.createElement("circle", { cx: "22", cy: "22", r: "1" }, /* @__PURE__ */ import_react3.default.createElement( "animate", { attributeName: "r", begin: "-0.9s", dur: "1.8s", values: "1; 20", calcMode: "spline", keyTimes: "0; 1", keySplines: "0.165, 0.84, 0.44, 1", repeatCount: "indefinite" } ), /* @__PURE__ */ import_react3.default.createElement( "animate", { attributeName: "stroke-opacity", begin: "-0.9s", dur: "1.8s", values: "1; 0", calcMode: "spline", keyTimes: "0; 1", keySplines: "0.3, 0.61, 0.355, 1", repeatCount: "indefinite" } ))) )); } // src/player/react/NoGames.tsx var import_react4 = __toESM(require("react"), 1); // src/player/utils.ts var isDevelopment = process.env.NODE_ENV === "development"; var isProduction = process.env.NODE_ENV === "production"; var isTest = process.env.NODE_ENV === "test"; // src/player/react/NoGames.tsx function NoGames() { return /* @__PURE__ */ import_react4.default.createElement("div", { className: "h-screen flex items-center justify-center" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-92 flex flex-col items-center" }, /* @__PURE__ */ import_react4.default.createElement("h2", { className: "text-gray-700 font-medium" }, "No experiments available"), /* @__PURE__ */ import_react4.default.createElement("p", { className: "mt-2 text-gray-400 text-justify" }, "There are currently no available experiments. Please wait until an experiment becomes available or come back at a later date."), isDevelopment ? /* @__PURE__ */ import_react4.default.createElement("p", { className: "mt-4 text-gray-700" }, "Go to", " ", /* @__PURE__ */ import_react4.default.createElement( "a", { href: "/admin", target: "empirica-admin", className: "text-empirica-500" }, "Admin" ), " ", "to get started") : "")); } // src/player/react/PlayerCreate.tsx var import_react5 = __toESM(require("react"), 1); function PlayerCreate({ onPlayerID, connecting }) { const [playerID, setPlayerID] = (0, import_react5.useState)(""); const handleSubmit = (evt) => { evt.preventDefault(); if (!playerID || playerID.trim() === "") { return; } onPlayerID(playerID); }; return /* @__PURE__ */ import_react5.default.createElement("div", { className: "min-h-screen bg-empirica-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "sm:mx-auto sm:w-full sm:max-w-md" }, /* @__PURE__ */ import_react5.default.createElement("h2", { className: "mt-6 text-center text-3xl font-extrabold text-gray-900" }, "Enter your Player Identifier")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mt-8 sm:mx-auto sm:w-full sm:max-w-md" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10" }, /* @__PURE__ */ import_react5.default.createElement( "form", { className: "space-y-6", action: "#", method: "POST", onSubmit: handleSubmit }, /* @__PURE__ */ import_react5.default.createElement("fieldset", { disabled: connecting }, /* @__PURE__ */ import_react5.default.createElement("div", null, /* @__PURE__ */ import_react5.default.createElement( "label", { htmlFor: "email", className: "block text-sm font-medium text-gray-700" }, "Identifier" ), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mt-1" }, /* @__PURE__ */ import_react5.default.createElement( "input", { id: "playerID", name: "playerID", type: "text", autoComplete: "off", required: true, autoFocus: true, className: "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-empirica-500 focus:border-empirica-500 sm:text-sm", value: playerID, onChange: (e) => setPlayerID(e.target.value) } ), /* @__PURE__ */ import_react5.default.createElement( "p", { className: "mt-2 text-sm text-gray-500", id: "playerID-description" }, "This should be given to you. E.g. email, code..." ))), /* @__PURE__ */ import_react5.default.createElement("div", null, /* @__PURE__ */ import_react5.default.createElement( "button", { type: "submit", className: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-empirica-600 hover:bg-empirica-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-empirica-500" }, "Enter" ))) )))); } // src/player/react/hooks.ts var import_react7 = require("react"); var import_rxjs11 = require("rxjs"); // src/player/react/EmpiricaParticipant.tsx var import_react6 = __toESM(require("react"), 1); // src/player/context.ts var import_rxjs10 = require("rxjs"); // src/admin/observables.ts var import_async_mutex = require("async-mutex"); var import_rxjs6 = require("rxjs"); // src/shared/tajriba_connection.ts var import_tajriba = require("@empirica/tajriba"); // src/utils/object.ts var import_rxjs7 = require("rxjs"); function bsu(init = void 0) { return new import_rxjs7.BehaviorSubject(init); } // src/shared/tajriba_connection.ts var ErrNotConnected = new Error("not connected"); // src/player/connection.ts var import_rxjs8 = require("rxjs"); var MemStorage = class { static clear() { this.vals = {}; } static getItem(key) { return this.vals[key]; } static removeItem(key) { delete this.vals[key]; } static setItem(key, value) { this.vals[key] = value; } }; MemStorage.vals = {}; var isBrowser2 = typeof window !== "undefined" && typeof window.document !== "undefined"; var storage; if (isBrowser2) { storage = window.localStorage; } var _ParticipantSession = class { constructor(ns, resetSession) { this.ns = ns; this._token = this.strg.getItem(this.tokenKey) || void 0; const participantStr = this.strg.getItem(this.partKey) || void 0; if (participantStr) { this._participant = JSON.parse(participantStr); } const sess = this.calcSession(); this._sessions = bsu(sess); resetSession.subscribe({ next: () => { this.clearSession(); } }); } get sessions() { return this._sessions; } get session() { return this._sessions.getValue(); } get token() { return this._token; } get participant() { return this._participant; } get tokenKey() { return `${_ParticipantSession.tokenKey}:${this.ns}`; } get partKey() { return `${_ParticipantSession.partKey}:${this.ns}`; } updateSession(token, participant) { this.strg.setItem(this.tokenKey, token); this.strg.setItem(this.partKey, JSON.stringify(participant)); this._token = token; this._participant = participant; this._sessions.next(this.calcSession()); } clearSession() { delete this._token; delete this._participant; this.strg.removeItem(this.tokenKey); this.strg.removeItem(this.partKey); this._sessions.next(void 0); } calcSession() { if (this._token && this._participant) { return { token: this._token, participant: this._participant }; } return void 0; } get strg() { return _ParticipantSession.storage; } }; var ParticipantSession = _ParticipantSession; ParticipantSession.tokenKey = "empirica:token"; ParticipantSession.partKey = "empirica:participant"; ParticipantSession.storage = storage; // src/player/provider.ts var import_rxjs9 = require("rxjs"); // src/player/react/EmpiricaParticipant.tsx var ParticipantCtx = import_react6.default.createContext(void 0); // src/player/react/hooks.ts function useParticipantContext() { return (0, import_react7.useContext)(ParticipantCtx); } function useTajribaConnected() { return useTajribaCtxKey("connected"); } function usePartConnected() { return usePartCtxKey("connected"); } function useTajriba() { const ctx = useParticipantContext(); return ctx?.tajriba; } function useGlobal() { const ctx = usePartCtxKey("globals"); const [val, setVal] = (0, import_react7.useState)({ g: void 0 }); (0, import_react7.useEffect)(() => { if (!ctx || !ctx.self) { return; } const sub = ctx.self.subscribe({ next(g) { setVal({ g }); } }); return sub.unsubscribe.bind(sub); }, [ctx]); return val.g; } var defaultConsentKey = "empirica:consent"; function useConsent(ns = "") { const key = `${defaultConsentKey}${ns ? `:${ns}` : ""}`; const getConsented = () => Boolean(window.localStorage[key]); const [consented, setConsented] = (0, import_react7.useState)(getConsented()); function onConsent() { window.localStorage[key] = true; setConsented(true); } return [consented, consented ? void 0 : onConsent]; } function usePlayerID() { const ctx = useParticipantContext(); const [connecting, setConnecting] = (0, import_react7.useState)(true); const [playerID, setPlayerID] = (0, import_react7.useState)(void 0); const [changePlayerID, setChangePlayerID] = (0, import_react7.useState)(void 0); (0, import_react7.useEffect)(() => { if (!ctx) { return; } let _connecting = true; let session; const sub = (0, import_rxjs11.merge)( ctx.participant.connecting, ctx.session.sessions ).subscribe({ next(sessionOrConnecting) { if (typeof sessionOrConnecting === "boolean") { setConnecting(sessionOrConnecting); _connecting = sessionOrConnecting; } else { session = sessionOrConnecting; } if (_connecting) { setPlayerID(void 0); setChangePlayerID(void 0); } else if (!session) { setPlayerID(void 0); setChangePlayerID(() => async (playerIdentifier) => { await ctx.register(playerIdentifier); }); } else { setPlayerID(session.participant.identifier); setChangePlayerID(void 0); } } }); return sub.unsubscribe.bind(sub); }, [ctx]); return [connecting, playerID, changePlayerID]; } function useTajribaCtxKey(name) { return useCtxKey(useTajriba, name); } function usePartCtxKey(name) { return useCtxKey(useParticipantContext, name); } function useCtxKey(ctxFunc, name) { const ctx = ctxFunc(); const [val, setVal] = (0, import_react7.useState)(void 0); (0, import_react7.useEffect)(() => { if (!ctx || !ctx[name]) { return; } const obs = ctx[name]; const sub = obs.subscribe({ next(g) { setVal(g); } }); return sub.unsubscribe.bind(sub); }, [ctx]); return val; } // src/player/classic/react/Lobby.tsx var import_react9 = __toESM(require("react"), 1); // src/player/classic/react/hooks.ts var import_react8 = require("react"); function usePlayer() { return usePartModeCtxKey( "player" ); } function useGame() { return usePartModeCtxKey("game"); } function useRound() { return usePartModeCtxKey( "round" ); } function useStage() { return usePartModeCtxKey( "stage" ); } function useStageTimer() { const stage = useStage(); const [val, setVal] = (0, import_react8.useState)({ tick: stage?.timer?.current }); const timerSubscription = (0, import_react8.useRef)(null); (0, import_react8.useEffect)(() => { if (!stage || !stage.timer) { return; } if (timerSubscription.current === null) { timerSubscription.current = stage.timer.obs().subscribe({ next(val2) { setVal({ tick: val2 }); } }); } return () => { timerSubscription?.current?.unsubscribe.bind(timerSubscription.current); timerSubscription.current = null; }; }, [stage?.timer]); return val.tick; } function usePlayers() { return usePartModeCtxKey( "players" ); } function usePartModeCtx() { const ctx = useParticipantContext(); const [mode, setMode] = (0, import_react8.useState)({ data: ctx?.mode?.getValue() }); (0, import_react8.useEffect)(() => { if (!ctx || !ctx.mode) { return; } const sub = ctx.mode.subscribe({ next(m) { setMode({ data: m }); } }); return sub.unsubscribe.bind(sub); }, [ctx]); return mode.data; } function usePartModeCtxKey(name) { const mode = usePartModeCtx(); const iniVal = mode && mode[name]; const [val, setVal] = (0, import_react8.useState)({ data: iniVal?.getValue() }); (0, import_react8.useEffect)(() => { if (!mode) { return; } const obs = mode[name]; const sub = obs.subscribe({ next(val2) { setVal({ data: val2 }); } }); return sub.unsubscribe.bind(sub); }, [mode]); return val.data; } // src/player/classic/react/Lobby.tsx function Lobby() { const player = usePlayer(); if (!player) { return /* @__PURE__ */ import_react9.default.createElement(Loading, null); } const treatment = player.get("treatment"); if (!treatment || !treatment.playerCount) { warn("lobby: no treatment found on player"); return /* @__PURE__ */ import_react9.default.createElement(Loading, null); } return /* @__PURE__ */ import_react9.default.createElement("div", { className: "flex h-full items-center justify-center" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ import_react9.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 640 512", className: "mx-auto h-12 w-12 text-gray-400", stroke: "none", fill: "currentColor", "aria-hidden": "true" }, /* @__PURE__ */ import_react9.default.createElement("path", { d: "M544 224c44.2 0 80-35.8 80-80s-35.8-80-80-80-80 35.8-80 80 35.8 80 80 80zm0-128c26.5 0 48 21.5 48 48s-21.5 48-48 48-48-21.5-48-48 21.5-48 48-48zM320 256c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm0-192c44.1 0 80 35.9 80 80s-35.9 80-80 80-80-35.9-80-80 35.9-80 80-80zm244 192h-40c-15.2 0-29.3 4.8-41.1 12.9 9.4 6.4 17.9 13.9 25.4 22.4 4.9-2.1 10.2-3.3 15.7-3.3h40c24.2 0 44 21.5 44 48 0 8.8 7.2 16 16 16s16-7.2 16-16c0-44.1-34.1-80-76-80zM96 224c44.2 0 80-35.8 80-80s-35.8-80-80-80-80 35.8-80 80 35.8 80 80 80zm0-128c26.5 0 48 21.5 48 48s-21.5 48-48 48-48-21.5-48-48 21.5-48 48-48zm304.1 180c-33.4 0-41.7 12-80.1 12-38.4 0-46.7-12-80.1-12-36.3 0-71.6 16.2-92.3 46.9-12.4 18.4-19.6 40.5-19.6 64.3V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-44.8c0-23.8-7.2-45.9-19.6-64.3-20.7-30.7-56-46.9-92.3-46.9zM480 432c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16v-44.8c0-16.6 4.9-32.7 14.1-46.4 13.8-20.5 38.4-32.8 65.7-32.8 27.4 0 37.2 12 80.2 12s52.8-12 80.1-12c27.3 0 51.9 12.3 65.7 32.8 9.2 13.7 14.1 29.8 14.1 46.4V432zM157.1 268.9c-11.9-8.1-26-12.9-41.1-12.9H76c-41.9 0-76 35.9-76 80 0 8.8 7.2 16 16 16s16-7.2 16-16c0-26.5 19.8-48 44-48h40c5.5 0 10.8 1.2 15.7 3.3 7.5-8.5 16.1-16 25.4-22.4z" }) ), /* @__PURE__ */ import_react9.default.createElement("h3", { className: "mt-2 text-sm font-medium text-gray-900" }, treatment.playerCount > 1 ? "Waiting for other players" : "Game loading"), /* @__PURE__ */ import_react9.default.createElement("p", { className: "mt-1 text-sm text-gray-500" }, "Please wait for the game to be ready."))); } // src/player/classic/react/Steps.tsx var import_react10 = __toESM(require("react"), 1); function Steps2({ steps, progressKey, doneKey, object, children }) { let obj; const game = useGame(); const player = usePlayer(); const [stps, setStps] = (0, import_react10.useState)([]); const [stpsSet, setStpsSet] = (0, import_react10.useState)(false); (0, import_react10.useEffect)(() => { let s; if (typeof steps === "function") { s = steps({ game, player }); } else { s = steps; } setStps(s); setStpsSet(true); }, [steps]); (0, import_react10.useEffect)(() => { if (obj && !obj.get(doneKey) && stpsSet && (!stps || stps.length === 0)) { obj.set(doneKey, true); } }, [stps]); if (object) { obj = object; } else if (player) { obj = player; } else { error("no receiver and no player in Steps"); return /* @__PURE__ */ import_react10.default.createElement("div", null, "Missing attribute"); } if (obj.get(doneKey)) { return /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null, children); } const index = obj.get(progressKey) || 0; const next = () => { if (index + 1 >= stps.length) { obj.set(doneKey, true); } else { obj.set(progressKey, index + 1); } }; const previous = () => { if (index > 0) { obj.set(progressKey, index - 1); } }; const Step2 = stps[index]; if (!Step2) { return /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null); } return /* @__PURE__ */ import_react10.default.createElement(Step2, { index, previous, next }); } // src/player/classic/react/EmpiricaContext.tsx function EmpiricaContext({ noGames: NoGamesComp = NoGames, consent: ConsentComp = Consent, playerCreate: PlayerCreateForm = PlayerCreate, introSteps = [], lobby = Lobby, exitSteps = [], finished = Finished, loading: LoadingComp = Loading, connecting: ConnectingComp = Loading, unmanagedGame = false, unmanagedAssignment = false, disableConsent = false, disableNoGames = false, disableURLParamsCapture = false, children }) { const tajribaConnected = useTajribaConnected(); const participantConnected = usePartConnected(); const globals = useGlobal(); const player = usePlayer(); const game = useGame(); const [connecting, hasPlayer, onPlayerID] = usePlayerID(); const [consented, onConsent] = useConsent(); if (!tajribaConnected || connecting) { return /* @__PURE__ */ import_react11.default.createElement(ConnectingComp, null); } if (player && player.get("ended")) { return /* @__PURE__ */ import_react11.default.createElement(Exit, { exitSteps, finished }); } if (!globals || hasPlayer && (!participantConnected || !player || game === void 0)) { return /* @__PURE__ */ import_react11.default.createElement(LoadingComp, null); } if (!disableNoGames && !globals.get("experimentOpen") && (!hasPlayer || !player?.get("gameID"))) { return /* @__PURE__ */ import_react11.default.createElement(NoGamesComp, null); } if (!disableConsent && !consented) { return /* @__PURE__ */ import_react11.default.createElement(ConsentComp, { onConsent }); } if (!hasPlayer) { return /* @__PURE__ */ import_react11.default.createElement(PlayerCreateForm, { onPlayerID, connecting }); } if (!player || !unmanagedGame && !game) { return /* @__PURE__ */ import_react11.default.createElement(LoadingComp, null); } if (!disableURLParamsCapture && !player.get("urlParams")) { const urlParams = new URLSearchParams(window.location.search); player.set("urlParams", Object.fromEntries(urlParams.entries())); } if (unmanagedAssignment) { return /* @__PURE__ */ import_react11.default.createElement(import_react11.default.Fragment, null, children); } if (game && game.hasEnded) { if (!player.get("ended")) { return /* @__PURE__ */ import_react11.default.createElement(LoadingComp, null); } return /* @__PURE__ */ import_react11.default.createElement(Exit, { exitSteps, finished }); } return /* @__PURE__ */ import_react11.default.createElement(Steps2, { progressKey: "intro", doneKey: "introDone", steps: introSteps }, /* @__PURE__ */ import_react11.default.createElement( EmpiricaInnerContext, { exitSteps, lobby, finished, loading: LoadingComp, unmanagedGame }, children )); } function EmpiricaInnerContext({ children, lobby: Lobby2, finished, exitSteps, loading: LoadingComp, unmanagedGame = false }) { const player = usePlayer(); const game = useGame(); const allReady = useAllReady(); if (!game) { if (unmanagedGame) { return /* @__PURE__ */ import_react11.default.createElement(import_react11.default.Fragment, null, children); } else { return /* @__PURE__ */ import_react11.default.createElement(LoadingComp, null); } } if (!Boolean(game.get("status"))) { return /* @__PURE__ */ import_react11.default.createElement(Lobby2, null); } if (game.hasEnded) { if (!player?.get("ended")) { return /* @__PURE__ */ import_react11.default.createElement(LoadingComp, null);