├── .empirica ├── .gitignore ├── id ├── README.md ├── lobbies.yaml ├── empirica.toml └── treatments.yaml ├── .dockerignore ├── internal ├── templates │ ├── source │ │ ├── react │ │ │ ├── public │ │ │ │ └── .gitkeep │ │ │ ├── .gitignore │ │ │ ├── jsconfig.json │ │ │ ├── src │ │ │ │ ├── components │ │ │ │ │ ├── Avatar.jsx │ │ │ │ │ ├── Button.jsx │ │ │ │ │ ├── Timer.jsx │ │ │ │ │ └── Alert.jsx │ │ │ │ ├── index.css │ │ │ │ ├── index.jsx │ │ │ │ ├── examples │ │ │ │ │ └── MineSweeper.jsx │ │ │ │ ├── intro-exit │ │ │ │ │ └── Introduction.jsx │ │ │ │ ├── Game.jsx │ │ │ │ ├── Stage.jsx │ │ │ │ ├── App.jsx │ │ │ │ └── Profile.jsx │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── uno.config.ts │ │ │ └── vite.config.js │ │ ├── admin-ui │ │ ├── callbacks │ │ │ ├── .gitignore │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src │ │ │ │ ├── index.js │ │ │ │ └── callbacks.js │ │ ├── svelte │ │ │ ├── .gitignore │ │ │ ├── src │ │ │ │ ├── vite-env.d.ts │ │ │ │ ├── assets │ │ │ │ │ └── svelte.png │ │ │ │ ├── main.js │ │ │ │ ├── lib │ │ │ │ │ └── Timer.svelte │ │ │ │ └── App.svelte │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── vite.config.js │ │ │ ├── package.json │ │ │ ├── index.html │ │ │ └── jsconfig.json │ │ └── export │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src │ │ │ └── index.js │ └── .gitignore ├── cloud │ ├── deploy.go │ └── cloud.go ├── term │ └── context.go ├── lobbies │ └── lobbies.go ├── treatments │ └── treatments.go ├── build │ ├── releases.go │ ├── download.go │ └── progress.go ├── experiment │ ├── utils.go │ └── upgrade.go ├── server │ └── config.go ├── utils │ └── log │ │ ├── config.go │ │ └── log.go ├── player │ └── config.go └── export │ ├── debug │ └── main.go │ └── csv.go ├── lib ├── @empirica │ └── core │ │ ├── src │ │ ├── player │ │ │ ├── index.css │ │ │ ├── react │ │ │ │ ├── helpers.ts │ │ │ │ ├── Finished.tsx │ │ │ │ ├── NoGames.tsx │ │ │ │ ├── EmpiricaParticipant.tsx │ │ │ │ ├── index.ts │ │ │ │ └── Loading.tsx │ │ │ ├── utils.ts │ │ │ ├── index.ts │ │ │ ├── classic │ │ │ │ ├── index.ts │ │ │ │ └── react │ │ │ │ │ ├── Slider.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Steps.tsx │ │ │ │ │ ├── slider.css │ │ │ │ │ └── Quiz.tsx │ │ │ ├── provider_test.ts │ │ │ └── scopes.ts │ │ ├── admin │ │ │ ├── classic │ │ │ │ ├── export │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── export_test.ts │ │ │ │ │ ├── export.ts │ │ │ │ │ └── bytes.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── schemas.ts │ │ │ │ ├── api │ │ │ │ │ └── api_test.ts │ │ │ │ ├── index.ts │ │ │ │ └── stage_timer_fix_test.ts │ │ │ ├── promises.ts │ │ │ ├── user.ts │ │ │ ├── transitions.ts │ │ │ ├── globals.ts │ │ │ └── index.ts │ │ ├── shared │ │ │ ├── helpers.ts │ │ │ ├── globals.ts │ │ │ └── tajriba_connection.ts │ │ ├── utils │ │ │ ├── json.ts │ │ │ ├── random.ts │ │ │ └── object.ts │ │ ├── index.ts │ │ └── types.d.ts │ │ ├── .gitignore │ │ ├── postcss.config.cjs │ │ ├── modd.conf │ │ ├── tsup.config.ts │ │ ├── docs.sh │ │ ├── uno.config.ts │ │ ├── README.md │ │ └── assets │ │ └── slider.css ├── admin-ui │ ├── src │ │ ├── components │ │ │ ├── NotFound.svelte │ │ │ ├── common │ │ │ │ ├── Badge.svelte │ │ │ │ ├── LabelBox.svelte │ │ │ │ ├── TimeSince.svelte │ │ │ │ ├── Duplicate.svelte │ │ │ │ ├── EmptyState.svelte │ │ │ │ ├── FormTip.svelte │ │ │ │ ├── Trash.svelte │ │ │ │ ├── Button.svelte │ │ │ │ ├── Alert.svelte │ │ │ │ ├── Select.svelte │ │ │ │ ├── ButtonGroup.svelte │ │ │ │ ├── Input.svelte │ │ │ │ ├── Loading.svelte │ │ │ │ └── Page.svelte │ │ │ ├── StopIcon.svelte │ │ │ ├── PlayIcon.svelte │ │ │ ├── ArrowIcon.svelte │ │ │ ├── treatments │ │ │ │ ├── FactorsPage.svelte │ │ │ │ ├── TreatmentsPage.svelte │ │ │ │ └── FactorsString.svelte │ │ │ ├── batches │ │ │ │ ├── FetchLobbies.svelte │ │ │ │ ├── FetchTreatments.svelte │ │ │ │ └── CustomAssignment.svelte │ │ │ ├── layout │ │ │ │ ├── Layout.svelte │ │ │ │ ├── SidebarButton.svelte │ │ │ │ └── Logo.svelte │ │ │ ├── Export.svelte │ │ │ └── UnderConstruction.svelte │ │ ├── utils │ │ │ ├── use.js │ │ │ ├── md.js │ │ │ ├── typeValue.js │ │ │ ├── auth.js │ │ │ ├── clickoutside.js │ │ │ ├── lobbies.js │ │ │ ├── equal.js │ │ │ ├── treatments.js │ │ │ └── batches.js │ │ ├── main.js │ │ ├── style.css │ │ ├── constants.js │ │ └── routes.js │ ├── .gitignore │ ├── globals.d.ts │ ├── public │ │ └── favicon.ico │ ├── index.html │ ├── README.md │ ├── vite.config.js │ ├── jsconfig.json │ ├── uno.config.ts │ └── package.json └── .gitignore ├── tests └── stress │ ├── experiment │ ├── .empirica │ │ ├── .gitignore │ │ ├── id │ │ ├── release │ │ ├── empirica.toml │ │ ├── lobbies.yaml │ │ └── treatments.yaml │ ├── server │ │ ├── .gitignore │ │ ├── jsconfig.json │ │ ├── package.json │ │ └── src │ │ │ ├── index.js │ │ │ └── callbacks.js │ └── client │ │ ├── .gitignore │ │ ├── jsconfig.json │ │ ├── src │ │ ├── index.css │ │ └── index.jsx │ │ ├── index.html │ │ ├── package.json │ │ ├── uno.config.ts │ │ └── vite.config.js │ ├── pnpm-workspace.yaml │ ├── tests │ ├── index.d.ts │ ├── helpers.js │ ├── step.js │ ├── utils.js │ └── intro.spec.js │ ├── .gitignore │ ├── package.json │ └── README.md ├── .changeset ├── minor.template ├── patch.template ├── config.json └── README.md ├── .github ├── actions │ └── upload-empirica-cli │ │ ├── .gitignore │ │ ├── index.js │ │ ├── README.md │ │ ├── package.json │ │ └── action.yaml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── FEATURES.yml └── workflows │ ├── check_pre_release.yaml │ └── on_push_proxy.yaml ├── .devcontainer ├── post-create.sh └── devcontainer.json ├── cmds ├── proxy │ └── main.go └── empirica │ ├── main.go │ └── cmd │ ├── paths.go │ ├── cloud │ ├── signin.go │ ├── random.go │ └── cloud.go │ ├── node.go │ ├── npm.go │ ├── yarn.go │ ├── setup.go │ ├── bundle.go │ ├── create.go │ ├── version.go │ └── serve.go ├── tools.go ├── .gitignore ├── .editorconfig ├── README.md ├── package.json └── modd.conf /.empirica/.gitignore: -------------------------------------------------------------------------------- 1 | local -------------------------------------------------------------------------------- /.empirica/id: -------------------------------------------------------------------------------- 1 | pMgBAquEQjlrazPn -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | *.exe 3 | -------------------------------------------------------------------------------- /internal/templates/source/react/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/index.css: -------------------------------------------------------------------------------- 1 | @unocss; -------------------------------------------------------------------------------- /lib/@empirica/core/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | docs 3 | -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/.gitignore: -------------------------------------------------------------------------------- 1 | local -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/id: -------------------------------------------------------------------------------- 1 | WesXpCxomXKzqqgd -------------------------------------------------------------------------------- /internal/templates/.gitignore: -------------------------------------------------------------------------------- 1 | bindata.go 2 | dist/ 3 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/export/.gitignore: -------------------------------------------------------------------------------- 1 | out -------------------------------------------------------------------------------- /lib/admin-ui/src/components/NotFound.svelte: -------------------------------------------------------------------------------- 1 | Not found! 2 | -------------------------------------------------------------------------------- /tests/stress/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | -------------------------------------------------------------------------------- /internal/templates/source/admin-ui: -------------------------------------------------------------------------------- 1 | ../../../lib/admin-ui/dist -------------------------------------------------------------------------------- /.changeset/minor.template: -------------------------------------------------------------------------------- 1 | --- 2 | "@empirica/core": minor 3 | --- 4 | -------------------------------------------------------------------------------- /.changeset/patch.template: -------------------------------------------------------------------------------- 1 | --- 2 | "@empirica/core": patch 3 | --- 4 | -------------------------------------------------------------------------------- /.github/actions/upload-empirica-cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !*.json 3 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | yarn-error.log 4 | .parcel-cache 5 | dist 6 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/use.js: -------------------------------------------------------------------------------- 1 | export function focus(el) { 2 | el.focus(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/stress/experiment/server/.gitignore: -------------------------------------------------------------------------------- 1 | trigger 2 | node_modules 3 | modd.conf 4 | dist -------------------------------------------------------------------------------- /internal/templates/source/callbacks/.gitignore: -------------------------------------------------------------------------------- 1 | trigger 2 | node_modules 3 | modd.conf 4 | dist -------------------------------------------------------------------------------- /.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | cd /workspaces 2 | git clone https://github.com/empiricaly/tajriba.git -------------------------------------------------------------------------------- /lib/admin-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /.vscode/ 4 | .DS_Store 5 | package-lock.json -------------------------------------------------------------------------------- /internal/templates/source/react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /internal/templates/source/svelte/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /.vscode/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /lib/admin-ui/globals.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/shared/helpers.ts: -------------------------------------------------------------------------------- 1 | export type Constructor = new (...args: any[]) => T; 2 | -------------------------------------------------------------------------------- /lib/admin-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empiricaly/empirica/HEAD/lib/admin-ui/public/favicon.ico -------------------------------------------------------------------------------- /tests/stress/tests/index.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | lastNewBatch: any; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /internal/cloud/deploy.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | func Deploy(pkg string, domain string) { 4 | panic("not implemented") 5 | } 6 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tests/stress/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | /screenshots -------------------------------------------------------------------------------- /internal/templates/source/svelte/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empiricaly/empirica/HEAD/internal/templates/source/svelte/public/favicon.ico -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/react/helpers.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export type WithChildren = T & { children?: React.ReactNode }; 4 | -------------------------------------------------------------------------------- /tests/stress/experiment/server/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "nodenext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/stress/tests/helpers.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export function error(message) { 4 | console.info(chalk.redBright(message)); 5 | } 6 | -------------------------------------------------------------------------------- /internal/templates/source/callbacks/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "nodenext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /internal/templates/source/export/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "nodenext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/src/assets/svelte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empiricaly/empirica/HEAD/internal/templates/source/svelte/src/assets/svelte.png -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/release: -------------------------------------------------------------------------------- 1 | version: v1.2.3 2 | sha: abcd123 3 | build: "42" 4 | tag: v1.2.3 5 | branch: thisbranch 6 | time: "2023-11-26T04:01:14Z" 7 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/md.js: -------------------------------------------------------------------------------- 1 | import { Remarkable } from "remarkable"; 2 | export const md = new Remarkable(); 3 | 4 | // console.log(md.render('# Remarkable rulezz!')); 5 | -------------------------------------------------------------------------------- /.empirica/README.md: -------------------------------------------------------------------------------- 1 | This .empirica folder is an example configuration that is here solely for 2 | conveniencen during during development. It is not used while building empirica. 3 | -------------------------------------------------------------------------------- /internal/templates/source/react/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "nodenext", 5 | "jsx": "react" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "nodenext", 5 | "jsx": "react" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.getElementById('app') 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/typeValue.js: -------------------------------------------------------------------------------- 1 | export function castValue(value) { 2 | try { 3 | const newVal = JSON.parse(value); 4 | return newVal; 5 | } catch (_) { 6 | return value; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/@empirica/core/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "@unocss/postcss": { 4 | // Optional 5 | content: ["**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}"], 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /tests/stress/tests/step.js: -------------------------------------------------------------------------------- 1 | export class Step { 2 | constructor(name, fn) { 3 | this.name = name; 4 | this.fn = fn; 5 | } 6 | 7 | async run(actor) { 8 | await this.fn(actor); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cmds/proxy/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | package main 6 | 7 | import "github.com/empiricaly/empirica/cmds/proxy/cmd" 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | const admin = writable(null); 4 | 5 | export const currentAdmin = { subscribe: admin.subscribe }; 6 | export const setCurrentAdmin = admin.set; 7 | -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/empirica.toml: -------------------------------------------------------------------------------- 1 | name = "myexperiment" 2 | 3 | [tajriba.auth] 4 | srtoken = "DnvLYqznScavnCMT" 5 | 6 | [[tajriba.auth.users]] 7 | name = "Admin" 8 | username = "admin" 9 | password = "rCaNMTJM" 10 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()] 7 | }) 8 | -------------------------------------------------------------------------------- /cmds/empirica/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/cmds/empirica/cmd" 5 | ) 6 | 7 | func main() { 8 | // defer profile.Start(profile.ClockProfile, profile.NoShutdownHook).Stop() 9 | 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /lib/admin-ui/src/main.js: -------------------------------------------------------------------------------- 1 | import "@unocss/reset/tailwind-compat.css"; 2 | import "virtual:uno.css"; 3 | import App from "./App.svelte"; 4 | import "./style.css"; 5 | 6 | const app = new App({ 7 | target: document.body, 8 | }); 9 | 10 | export default app; 11 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Badge.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // package tools is a place to list all tools used in go generate and tests, so 5 | // that `go mod tidy` will not shoot down all those deps. 6 | package tools 7 | 8 | import ( 9 | _ "github.com/go-bindata/go-bindata" 10 | ) 11 | -------------------------------------------------------------------------------- /.empirica/lobbies.yaml: -------------------------------------------------------------------------------- 1 | lobbies: 2 | - name: Default shared fail 3 | kind: shared 4 | duration: 5m 5 | strategy: fail 6 | - name: Default shared ignore 7 | kind: shared 8 | duration: 5m 9 | strategy: ignore 10 | - name: Default individual 11 | kind: individual 12 | duration: 5m 13 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/utils/json.ts: -------------------------------------------------------------------------------- 1 | export type JsonValue = 2 | | string 3 | | number 4 | | boolean 5 | | Date 6 | | Json 7 | | JsonArray 8 | | null; 9 | 10 | export interface Json { 11 | [x: string]: JsonValue; 12 | } 13 | 14 | export interface JsonArray extends Array {} 15 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/src/lib/Timer.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

7 | {$stageTimer === null ? "-" : $stageTimer} 8 |

9 | seconds 10 |
11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.db 3 | *.tgz 4 | *.tar.zst 5 | *.zip 6 | tajriba.json 7 | internal/vendored/transport 8 | yarn-error.log 9 | node_modules 10 | go.work 11 | go.work.sum 12 | 13 | playwright-results 14 | playwright-report 15 | test-results 16 | e2e-tests/test-experiment-* 17 | e2e-tests/cache 18 | 19 | out 20 | .vscode 21 | NOTES -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/lobbies.yaml: -------------------------------------------------------------------------------- 1 | lobbies: 2 | - name: Default shared fail 3 | kind: shared 4 | duration: 50m 5 | strategy: fail 6 | - name: Default shared ignore 7 | kind: shared 8 | duration: 50m 9 | strategy: ignore 10 | - name: Default individual 11 | kind: individual 12 | duration: 50m 13 | -------------------------------------------------------------------------------- /.empirica/empirica.toml: -------------------------------------------------------------------------------- 1 | name = "myexperiment" 2 | 3 | [callbacks] 4 | path = "internal/templates/source/callbacks" 5 | 6 | [player] 7 | path = "internal/templates/source/react" 8 | 9 | [tajriba.auth] 10 | srtoken = "01234567890123456789" 11 | 12 | [[tajriba.auth.users]] 13 | name = "Admin" 14 | username = "admin" 15 | password = "123123123" 16 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/components/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Avatar({ player }) { 4 | return ( 5 | Avatar 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /lib/@empirica/core/modd.conf: -------------------------------------------------------------------------------- 1 | package.json src/**/*.ts src/**/*.tsx src/**/*.js !src/**/*_test.ts !src/**/*_test.tsx !src/**/*_test.js { 2 | prep: rm -rf dist & 3 | prep: npm run build 4 | prep: npm run check 5 | } 6 | 7 | # { 8 | # daemon: npm run test:watch 9 | # } 10 | 11 | # src/**/*_test.ts src/**/*_test.tsx { 12 | # prep: npm run test 13 | # } 14 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/StopIcon.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ListenersCollector } from "./admin"; 2 | export { 3 | Classic, 4 | classicKinds, 5 | ClassicListenersCollector, 6 | ClassicLoader, 7 | } from "./admin/classic"; 8 | export * from "./player/classic"; 9 | export * from "./player/classic/react"; 10 | export * from "./player/react"; 11 | export * from "./utils/console"; 12 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/PlayIcon.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/promises.ts: -------------------------------------------------------------------------------- 1 | export interface PromiseHandle { 2 | promise: Promise; 3 | result: (value: T) => void; 4 | } 5 | 6 | export function promiseHandle(): PromiseHandle { 7 | let ret = {} as PromiseHandle; 8 | ret.promise = new Promise((r) => { 9 | ret.result = r; 10 | }); 11 | 12 | return ret; 13 | } 14 | -------------------------------------------------------------------------------- /.github/actions/upload-empirica-cli/index.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | const github = require("@actions/github"); 3 | const S3 = require("aws-sdk/clients/s3"); 4 | const fs = require("fs"); 5 | const action = require("./action.js"); 6 | 7 | action.run(core, github, S3, fs).catch((err) => { 8 | core.error(err); 9 | core.setFailed(err.message); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/react/Finished.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Finished() { 4 | return ( 5 |
6 |

Finished

7 |

Thank you for participating

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/admin-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Empirica Admin 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/LabelBox.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/index.css: -------------------------------------------------------------------------------- 1 | @layer utilities { 2 | /* Chrome, Safari, Edge, Opera */ 3 | input.number-arrows-none::-webkit-outer-spin-button, 4 | input.number-arrows-none::-webkit-inner-spin-button { 5 | -webkit-appearance: none; 6 | margin: 0; 7 | } 8 | 9 | /* Firefox */ 10 | input.number-arrows-none[type="number"] { 11 | -moz-appearance: textfield; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/src/index.css: -------------------------------------------------------------------------------- 1 | @layer utilities { 2 | /* Chrome, Safari, Edge, Opera */ 3 | input.number-arrows-none::-webkit-outer-spin-button, 4 | input.number-arrows-none::-webkit-inner-spin-button { 5 | -webkit-appearance: none; 6 | margin: 0; 7 | } 8 | 9 | /* Firefox */ 10 | input.number-arrows-none[type="number"] { 11 | -moz-appearance: textfield; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Slack Community Support 4 | url: https://join.slack.com/t/empirica-ly/shared_invite/zt-1fb34yq47-YlgYUJmXJAdv7QmHsa_fdw 5 | about: "Please ask and answer questions in our #tech-support Slack channel." 6 | - name: Empirica Documentation 7 | url: https://docs.empirica.ly 8 | about: Check the Empirica documentation. 9 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@sveltejs/vite-plugin-svelte": "^1.0.0-next.11", 13 | "svelte": "^3.37.0", 14 | "vite": "^2.4.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/ArrowIcon.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [[ 6 | "@empirica/core" 7 | ]], 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": [], 13 | "bumpVersionsWithWorkspaceProtocolOnly": true 14 | } 15 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/clickoutside.js: -------------------------------------------------------------------------------- 1 | export function clickOutside(node, handler) { 2 | const onClick = (event) => 3 | node && 4 | !node.contains(event.target) && 5 | !event.defaultPrevented && 6 | handler(); 7 | 8 | document.addEventListener("click", onClick, true); 9 | 10 | return { 11 | destroy() { 12 | document.removeEventListener("click", onClick, true); 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "youch-terminal" { 2 | function YouchForTerminal( 3 | err: { error: Error }, 4 | opts: YouchTerminalOptions 5 | ): void; 6 | interface YouchTerminalOptions { 7 | prefix: String; 8 | displayShortPath: Boolean; 9 | hideErrorTitle: Boolean; 10 | hideMessage: Boolean; 11 | displayMainFrameOnly: Boolean; 12 | } 13 | export = YouchForTerminal; 14 | } 15 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte + Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Empirica Experiment 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /internal/templates/source/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Empirica Experiment 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/utils.ts: -------------------------------------------------------------------------------- 1 | export const isDevelopment = process.env.NODE_ENV === "development"; 2 | export const isProduction = process.env.NODE_ENV === "production"; 3 | export const isTest = process.env.NODE_ENV === "test"; 4 | 5 | export const createNewParticipant = (key = "participantKey") => { 6 | const url = new URL(document.location.href); 7 | url.searchParams.set(key, new Date().getTime().toString()); 8 | window.open(url.href, "_blank")?.focus(); 9 | }; 10 | -------------------------------------------------------------------------------- /.empirica/treatments.yaml: -------------------------------------------------------------------------------- 1 | factors: 2 | - desc: playerCount determines the number of Players are in a Game. 3 | name: playerCount 4 | values: 5 | - value: 1 6 | - value: 2 7 | - value: 3 8 | - value: 5 9 | - value: 8 10 | - value: 13 11 | treatments: 12 | - desc: "Single-player Game" 13 | factors: 14 | playerCount: 1 15 | name: Solo 16 | - desc: "Two-player Game" 17 | factors: 18 | playerCount: 2 19 | name: Two Players 20 | -------------------------------------------------------------------------------- /.github/actions/upload-empirica-cli/README.md: -------------------------------------------------------------------------------- 1 | # Upload Empirica CLI 2 | 3 | This action uploads the Empirica CLI. 4 | 5 | ## Contributing 6 | 7 | If the `upload-empirica-cli` has been changed, the action needs to be built again: 8 | 9 | 1. Go to .github folder and install packages 10 | `$ cd .github/actions/upload-empirica-cli` 11 | `$ npm ci` 12 | 13 | 2. Build the action file 14 | 15 | `$ npm run build` 16 | 17 | 3. Commit the dist folder to the repo and check Github Actions result 18 | -------------------------------------------------------------------------------- /lib/admin-ui/README.md: -------------------------------------------------------------------------------- 1 | # Empirica Admin UI 2 | 3 | ## Running in development mode 4 | 5 | 1. Create an experiment using Empirica CLI 6 | 7 | `$ empirica create ` 8 | 9 | 2. Run the experiment 10 | 11 | `$ cd ` 12 | `$ empirica` 13 | 14 | 3. Run the dev server for the admin UI: 15 | 16 | `$ cd empirica/lib/admin-ui` 17 | `$ npm run dev` 18 | 19 | 4. Navigate to `localhost:3001` 20 | 21 | Note: the requests will be proxied to `localhost:3000` (default port for Empirica) -------------------------------------------------------------------------------- /.github/workflows/check_pre_release.yaml: -------------------------------------------------------------------------------- 1 | name: Check pre-release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | Static: 11 | name: "Check branch does not have a pre-release file in changeset" 12 | runs-on: ubuntu-latest 13 | env: 14 | MODULAR_LOGGER_DEBUG: true 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: "Check that the file .changeset/pre.json is not present" 18 | run: "! test -f .changeset/pre.json" 19 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/utils/random.ts: -------------------------------------------------------------------------------- 1 | export function pickRandom(items: T[]): T { 2 | const random = Math.floor(Math.random() * items.length); 3 | return items[random] as T; 4 | } 5 | 6 | export function shuffle(a: Array) { 7 | for (let i = a.length - 1; i > 0; i--) { 8 | const j = Math.floor(Math.random() * (i + 1)); 9 | [a[i], a[j]] = [a[j], a[i]]; 10 | } 11 | return a; 12 | } 13 | 14 | export function selectRandom(arr: Array, num: number) { 15 | return shuffle(arr.slice()).slice(0, num); 16 | } 17 | -------------------------------------------------------------------------------- /lib/admin-ui/src/style.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | button { 3 | -webkit-appearance: none !important; 4 | background-color: transparent; 5 | } 6 | } 7 | 8 | @layer utilities { 9 | /* Chrome, Safari, Edge, Opera */ 10 | input.number-arrows-none::-webkit-outer-spin-button, 11 | input.number-arrows-none::-webkit-inner-spin-button { 12 | -webkit-appearance: none; 13 | margin: 0; 14 | } 15 | 16 | /* Firefox */ 17 | input.number-arrows-none[type="number"] { 18 | -moz-appearance: textfield; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/stress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stress", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "volta": { 11 | "node": "20.10.0", 12 | "npm": "10.2.3" 13 | }, 14 | "engines": { 15 | "node": ">= 16.17.0" 16 | }, 17 | "devDependencies": { 18 | "@playwright/test": "^1.40.0", 19 | "@types/node": "^20.10.0" 20 | }, 21 | "dependencies": { 22 | "chalk": "4.1.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/treatments/FactorsPage.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | You can edit Factors here or in the .empirica/treatments.yaml file. 9 | Updates are automatically saved on this page. If you make changes to the yaml 10 | file, make sure to reload this page. 11 | 12 | 13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "@unocss/reset/tailwind-compat.css"; 4 | import "virtual:uno.css"; 5 | import "../node_modules/@empirica/core/dist/player.css"; 6 | import App from "./App"; 7 | import "./index.css"; 8 | 9 | const container = document.getElementById("root"); 10 | const root = createRoot(container); // createRoot(container!) if you use TypeScript 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "@unocss/reset/tailwind-compat.css"; 4 | import "virtual:uno.css"; 5 | import "../node_modules/@empirica/core/dist/player.css"; 6 | import App from "./App"; 7 | import "./index.css"; 8 | 9 | const container = document.getElementById("root"); 10 | const root = createRoot(container); // createRoot(container!) if you use TypeScript 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/TimeSince.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/batches/FetchLobbies.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#await getLobbies()} 8 | 9 | {:then conf} 10 | 11 | {:catch error} 12 | 13 | Failed to fetch 14 | .empirica/lobbies.yaml. 15 | Make sure the server is started then reload. ({error.toString()}) 16 | 17 | {/await} 18 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/layout/Layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | 15 | 16 |
17 | 18 | Not found 19 |
20 |
21 | -------------------------------------------------------------------------------- /internal/term/context.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import "context" 4 | 5 | type ctxType int 6 | 7 | // A private key for context that only this package can access. This is 8 | // important to prevent collisions between different context uses. 9 | const ctxKey = ctxType(0) 10 | 11 | // ForContext finds the Runtime from the context. 12 | func ForContext(ctx context.Context) *UI { 13 | raw, _ := ctx.Value(ctxKey).(*UI) 14 | 15 | return raw 16 | } 17 | 18 | // SetContext sets the user on the context. 19 | func SetContext(ctx context.Context, c *UI) context.Context { 20 | return context.WithValue(ctx, ctxKey, c) 21 | } 22 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/treatments/TreatmentsPage.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | You can edit Treatments here or in the 10 | .empirica/treatments.yaml file. Updates are automatically saved 11 | on this page. If you make changes to the yaml file, make sure to reload this 12 | page. 13 | 14 | 15 |
16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/batches/FetchTreatments.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#await getTreatments()} 8 | 9 | {:then conf} 10 | 11 | {:catch error} 12 | 13 | Failed to fetch 14 | .empirica/treatments.yaml. 15 | Make sure the server is started then reload. ({error.toString()}) 16 | 17 | {/await} 18 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/lobbies.js: -------------------------------------------------------------------------------- 1 | import { ORIGIN } from "../constants.js"; 2 | import { durationString } from "./time.js"; 3 | 4 | export async function getLobbies() { 5 | return (await fetch(ORIGIN + "/lobbies")).json(); 6 | } 7 | 8 | export function formatLobby(lobby) { 9 | let name = ""; 10 | 11 | if (lobby.name) { 12 | name = lobby.name + " - "; 13 | } 14 | 15 | if (lobby.kind === "shared") { 16 | return `${name}Shared / ${durationString(lobby.duration)} / ${ 17 | lobby.strategy 18 | }`; 19 | } 20 | 21 | return `${name}Individual / ${durationString(lobby.duration)} / ${ 22 | lobby.extensions || 0 23 | }`; 24 | } 25 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/paths.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/empiricaly/empirica/internal/build" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func addPathsCommand(parent *cobra.Command) error { 11 | cmd := &cobra.Command{ 12 | Use: "paths", 13 | Short: "Print empirica paths", 14 | SilenceUsage: true, 15 | SilenceErrors: true, 16 | Hidden: true, 17 | Args: cobra.NoArgs, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | fmt.Println("versions cache dir:", build.VersionsBasePath()) 20 | 21 | return nil 22 | }, 23 | } 24 | 25 | parent.AddCommand(cmd) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /lib/@empirica/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | index: "src/index.ts", 6 | user: "src/admin/user.ts", 7 | admin: "src/admin/index.ts", 8 | "admin-classic": "src/admin/classic/index.ts", 9 | player: "src/player/index.ts", 10 | "player-react": "src/player/react/index.ts", 11 | "player-classic": "src/player/classic/index.ts", 12 | "player-classic-react": "src/player/classic/react/index.ts", 13 | console: "src/utils/console.ts", 14 | }, 15 | format: ["esm", "cjs"], 16 | // splitting: false, 17 | sourcemap: true, 18 | clean: true, 19 | dts: true, 20 | }); 21 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/export/export_test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { withTajriba } from "../api/connection_test_helper"; 3 | import { ExportFormat, runExport } from "./export"; 4 | 5 | const t = test; 6 | // const t = test.serial; 7 | // const to = test.only; 8 | 9 | t("export csv", async (t) => { 10 | await withTajriba( 11 | async ({ url, srtoken }) => { 12 | await runExport( 13 | url, 14 | null, 15 | srtoken, 16 | ExportFormat.CSV, 17 | "/tmp/out-empirica.zip" 18 | ); 19 | 20 | t.truthy(true); 21 | }, 22 | { tajFile: "src/admin/classic/api/tajriba.json", printLogs: false } 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/examples/MineSweeper.jsx: -------------------------------------------------------------------------------- 1 | import { Sweeper, usePlayer } from "@empirica/core/player/classic/react"; 2 | import React from "react"; 3 | import { Avatar } from "../components/Avatar"; 4 | import { Button } from "../components/Button"; 5 | 6 | export function MineSweeper() { 7 | const player = usePlayer(); 8 | 9 | function handleSubmit() { 10 | player.stage.set("submit", true); 11 | } 12 | 13 | return ( 14 |
15 | 16 | 17 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.github/actions/upload-empirica-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upload-empirica-cli", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "ncc build index.js -o dist", 8 | "test": "ava -v", 9 | "test:watch": "ava --watch -v" 10 | }, 11 | "keywords": [], 12 | "author": "Nicolas Paton", 13 | "private": true, 14 | "volta": { 15 | "node": "20.10.0", 16 | "npm": "10.2.3" 17 | }, 18 | "dependencies": { 19 | "@actions/core": "1.10.0", 20 | "@actions/github": "^4.0.0", 21 | "@zeit/ncc": "^0.22.3", 22 | "aws-sdk": "^2.730.0", 23 | "semver": "7.3.7" 24 | }, 25 | "devDependencies": { 26 | "ava": "4.3.1", 27 | "sinon": "14.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/equal.js: -------------------------------------------------------------------------------- 1 | export function deepEqual(obj1, obj2) { 2 | if (obj1 === obj2) 3 | // it's just the same object. No need to compare. 4 | return true; 5 | 6 | if (isPrimitive(obj1) && isPrimitive(obj2)) 7 | // compare primitives 8 | return obj1 === obj2; 9 | 10 | if (Object.keys(obj1).length !== Object.keys(obj2).length) return false; 11 | 12 | // compare objects with same number of keys 13 | for (let key in obj1) { 14 | if (!(key in obj2)) return false; //other object doesn't have this prop 15 | if (!deepEqual(obj1[key], obj2[key])) return false; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | //check if value is primitive 22 | function isPrimitive(obj) { 23 | return obj !== Object(obj); 24 | } 25 | -------------------------------------------------------------------------------- /lib/admin-ui/src/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TOKEN_KEY = "emp:part:token"; 2 | export const DEFAULT_PART_KEY = "emp:part"; 3 | 4 | export const DEFAULT_TREATMENT = { 5 | name: "", 6 | desc: "", 7 | factors: [{ key: "playerCount", value: 1 }], 8 | }; 9 | export const DEFAULT_FACTOR = { name: "", desc: "", values: [{ value: "" }] }; 10 | export const DEFAULT_LOBBY = { 11 | name: "", 12 | desc: "", 13 | kind: "shared", 14 | strategy: "fail", 15 | duration: "5m", 16 | extensions: 0, 17 | }; 18 | 19 | // URL 20 | 21 | let origin = window.location.origin; 22 | 23 | // When developing the admin-ui (3001), the API is served from 3000. 24 | if (origin === "http://localhost:3001") { 25 | origin = "http://localhost:3000"; 26 | } 27 | 28 | export const ORIGIN = origin; 29 | -------------------------------------------------------------------------------- /internal/lobbies/lobbies.go: -------------------------------------------------------------------------------- 1 | package lobbies 2 | 3 | import "time" 4 | 5 | type LobbyConfig struct { 6 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 7 | Description string `json:"desc,omitempty" yaml:"desc,omitempty"` 8 | Kind string `validate:"required,oneof=shared individual" json:"kind" yaml:"kind"` 9 | Duration time.Duration `validate:"required,min=5s" json:"duration" yaml:"duration"` 10 | Strategy string `validate:"required_if=Kind shared,oneof=fail ignore ''" json:"strategy,omitempty" yaml:"strategy,omitempty"` 11 | Extensions int `validate:"min=0" json:"extensions,omitempty" yaml:"extensions,omitempty"` 12 | } 13 | 14 | type Lobbies struct { 15 | Lobbies []*LobbyConfig `validate:"dive" json:"lobbies" yaml:"lobbies"` 16 | } 17 | -------------------------------------------------------------------------------- /lib/@empirica/core/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | trap "exit" INT 5 | 6 | # define your list of paths here 7 | docs="src/admin/index.ts src/admin/classic/index.ts src/player/index.ts src/player/react/index.ts src/player/classic/index.ts src/player/classic/react/index.ts src/utils/console.ts" 8 | 9 | # "../../../../docsv2/api" 10 | outdir="${OUT:-docs}" 11 | 12 | # loop over each path 13 | for path in $docs; do 14 | echo $path 15 | # remove "src/" and "/index.ts" from the path 16 | cleaned_path=${path#src/} 17 | cleaned_path=${cleaned_path%/index.ts} 18 | underscored_path=$(echo $cleaned_path | tr '/' '_') 19 | npx typedoc --kindsWithOwnFile none --excludeInternal --excludePrivate --excludeProtected --plugin typedoc-plugin-markdown --out "${outdir}/${underscored_path}" $path 20 | done -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Duplicate.svelte: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /tests/stress/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { exec as cpexec } from "child_process"; 2 | 3 | export async function sleep(ms) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)); 5 | } 6 | export function randomString(length) { 7 | const chars = "0123456789abcdefghijklmnopqrstuvwxyz"; 8 | let result = ""; 9 | for (let i = length; i > 0; --i) { 10 | result += chars[Math.floor(Math.random() * chars.length)]; 11 | } 12 | return result; 13 | } 14 | 15 | export function exec(cmd, wd) { 16 | return new Promise((resolve, reject) => { 17 | cpexec( 18 | cmd, 19 | { 20 | cwd: wd, 21 | }, 22 | (error, stdout, stderr) => { 23 | if (error) { 24 | reject(error); 25 | } 26 | 27 | resolve({ stdout, stderr }); 28 | } 29 | ); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/intro-exit/Introduction.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "../components/Button"; 3 | 4 | export function Introduction({ next }) { 5 | return ( 6 |
7 |

8 | Instruction One 9 |

10 |
11 |

12 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius aliquam 13 | laudantium explicabo pariatur iste dolorem animi vitae error totam. At 14 | sapiente aliquam accusamus facere veritatis. 15 |

16 |
17 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /internal/templates/source/export/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "export", 3 | "description": "", 4 | "main": "src/index.js", 5 | "private": true, 6 | "scripts": { 7 | "export": "npm run build && node --trace-warnings --enable-source-maps dist/index.js", 8 | "export:runonly": "node --trace-warnings --enable-source-maps dist/index.js", 9 | "build": "esbuild src/index.js --platform=node --define:process.env.NODE_ENV='\"production\"' --bundle --minify --outfile=dist/index.js --sourcemap", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "version": "1.0.0", 13 | "volta": { 14 | "node": "20.11.1", 15 | "npm": "10.2.4" 16 | }, 17 | "dependencies": { 18 | "@empirica/core": "latest", 19 | "minimist": "1.2.6" 20 | }, 21 | "devDependencies": { 22 | "esbuild": "0.14.47" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/Game.jsx: -------------------------------------------------------------------------------- 1 | import { Chat, useGame } from "@empirica/core/player/classic/react"; 2 | 3 | import React from "react"; 4 | import { Profile } from "./Profile"; 5 | import { Stage } from "./Stage"; 6 | 7 | export function Game() { 8 | const game = useGame(); 9 | const { playerCount } = game.get("treatment"); 10 | 11 | return ( 12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | {playerCount > 1 && ( 21 |
22 | 23 |
24 | )} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/EmptyState.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 28 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/user.ts: -------------------------------------------------------------------------------- 1 | export { Attribute } from "../shared/attributes"; 2 | export type { AttributeOptions } from "../shared/attributes"; 3 | export type { ScopeUpdate } from "../shared/scopes"; 4 | export { TajribaConnection } from "../shared/tajriba_connection"; 5 | export { Attributes } from "./attributes"; 6 | export { AdminConnection } from "./connection"; 7 | export { EventContext, ListenersCollector, TajribaEvent } from "./events"; 8 | export type { Subscriber } from "./events"; 9 | export { participantsSub } from "./participants"; 10 | export type { Participant } from "./participants"; 11 | export { Scope, Scopes } from "./scopes"; 12 | export { Subscriptions } from "./subscriptions"; 13 | export type { ScopeSubscriptionInput, Subs } from "./subscriptions"; 14 | export { transitionsSub } from "./transitions"; 15 | export type { Transition } from "./transitions"; 16 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const base = 4 | "inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-empirica-500"; 5 | const prim = 6 | "border-gray-300 shadow-sm text-gray-700 bg-white hover:bg-gray-50"; 7 | const sec = 8 | "border-transparent shadow-sm text-white bg-empirica-600 hover:bg-empirica-700"; 9 | 10 | export function Button({ 11 | children, 12 | handleClick = null, 13 | className = "", 14 | primary = false, 15 | type = "button", 16 | autoFocus = false, 17 | }) { 18 | return ( 19 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/admin-ui/src/utils/treatments.js: -------------------------------------------------------------------------------- 1 | import { ORIGIN } from "../constants.js"; 2 | 3 | export async function getTreatments() { 4 | return (await fetch(ORIGIN + "/treatments")).json(); 5 | } 6 | 7 | export function formatFactorsToString(factors, sep = " | ") { 8 | let factorArr = []; 9 | for (const key in factors) { 10 | let val = factors[key]; 11 | 12 | // check for object/array type data 13 | if (val === Object(val)) { 14 | // Object 15 | if (!val.length) { 16 | let tempVal = []; 17 | for (const k in val) { 18 | tempVal.push(k + ": " + val[k]); 19 | } 20 | 21 | val = "{" + tempVal.join(", ") + "}"; 22 | } else { 23 | // array object here 24 | val = "[" + val.join(", ") + "]"; 25 | } 26 | } 27 | 28 | factorArr.push(key + ": " + val); 29 | } 30 | 31 | return factorArr.join(sep); 32 | } 33 | -------------------------------------------------------------------------------- /internal/templates/source/callbacks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empirica-server", 3 | "private": true, 4 | "scripts": { 5 | "dev": "npm run build && node --trace-warnings --enable-source-maps --unhandled-rejections=warn-with-error-code dist/index.js", 6 | "build": "rsync -aP --exclude=node_modules --exclude=*.js -m src/* dist/ && esbuild src/index.js --color=true --log-level=warning --log-level=warning --platform=node --define:process.env.NODE_ENV='\"production\"' --bundle --minify --outfile=dist/index.js --sourcemap", 7 | "serve": "node --trace-warnings --enable-source-maps --unhandled-rejections=warn-with-error-code index.js" 8 | }, 9 | "volta": { 10 | "node": "20.11.1", 11 | "npm": "10.2.4" 12 | }, 13 | "dependencies": { 14 | "@empirica/core": "latest", 15 | "minimist": "1.2.6" 16 | }, 17 | "devDependencies": { 18 | "esbuild": "0.14.47" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/loader.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ListenersCollector } from "../events"; 3 | import { ClassicKinds, Context } from "./models"; 4 | 5 | const string = z.string(); 6 | 7 | /** ClassicLoader loads. */ 8 | export function ClassicLoader( 9 | /** This is the listener */ 10 | _: ListenersCollector 11 | ) { 12 | _.on("start", function (ctx) { 13 | ctx.participantsSub(); 14 | ctx.scopeSub({ kinds: ["batch", "player"] }); 15 | }); 16 | 17 | _.on("batch", "status", function (ctx, { batch, status }) { 18 | if (["running", "created"].includes(status)) { 19 | ctx.scopeSub({ 20 | kvs: [{ key: "batchID", val: JSON.stringify(batch.id) }], 21 | }); 22 | } 23 | }); 24 | 25 | _.on("stage", "timerID", function (ctx, { timerID }) { 26 | ctx.transitionsSub(string.parse(timerID)); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const factorsSchema = z.record(z.string().min(1), z.any()); 4 | export const treatmentSchema = z.object({ 5 | factors: factorsSchema, 6 | name: z.string().optional(), 7 | }); 8 | 9 | export const batchConfigSchema = z.discriminatedUnion("kind", [ 10 | z.object({ 11 | kind: z.literal("custom"), 12 | config: z.any(), 13 | }), 14 | z.object({ 15 | kind: z.literal("simple"), 16 | config: z.object({ 17 | count: z.number().int().positive(), 18 | treatments: treatmentSchema.array(), 19 | }), 20 | }), 21 | z.object({ 22 | kind: z.literal("complete"), 23 | config: z.object({ 24 | treatments: z 25 | .object({ 26 | count: z.number().int().positive(), 27 | treatment: treatmentSchema, 28 | }) 29 | .array(), 30 | }), 31 | }), 32 | ]); 33 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/FormTip.svelte: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 | 7 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/cloud/signin.go: -------------------------------------------------------------------------------- 1 | package cloudcmd 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/internal/cloud" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func AddSigninCommand(parent *cobra.Command) error { 11 | cmd := &cobra.Command{ 12 | Use: "signin", 13 | Aliases: []string{"login"}, 14 | Short: "Sign into Empirica Cloud", 15 | SilenceUsage: true, 16 | SilenceErrors: true, 17 | Hidden: true, 18 | Args: cobra.NoArgs, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | ctx := initContext() 21 | _, err := cloud.SignIn(ctx) 22 | return errors.Wrap(err, "sign into empirica cloud") 23 | }, 24 | } 25 | 26 | err := viper.BindPFlags(cmd.Flags()) 27 | if err != nil { 28 | return errors.Wrap(err, "bind bundle flags") 29 | } 30 | 31 | parent.AddCommand(cmd) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/node.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/internal/experiment" 5 | "github.com/empiricaly/empirica/internal/settings" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func addNodeCommand(parent *cobra.Command) error { 11 | cmd := &cobra.Command{ 12 | Use: "node", 13 | Short: "Run node", 14 | // Long: ``, 15 | SilenceUsage: true, 16 | SilenceErrors: true, 17 | // Args: cobra.An, 18 | Hidden: false, 19 | DisableFlagParsing: true, 20 | TraverseChildren: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := initContext() 23 | 24 | if err := settings.InstallVoltaIfNeeded(ctx); err != nil { 25 | return errors.Wrap(err, "check node") 26 | } 27 | 28 | return experiment.RunCmd(ctx, "", "node", args...) 29 | }, 30 | } 31 | 32 | parent.AddCommand(cmd) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/npm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/internal/experiment" 5 | "github.com/empiricaly/empirica/internal/settings" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func addNPMCommand(parent *cobra.Command) error { 11 | cmd := &cobra.Command{ 12 | Use: "npm", 13 | Short: "Run npm commands", 14 | // Long: ``, 15 | SilenceUsage: true, 16 | SilenceErrors: true, 17 | // Args: cobra.An, 18 | Hidden: false, 19 | DisableFlagParsing: true, 20 | TraverseChildren: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := initContext() 23 | 24 | if err := settings.InstallVoltaIfNeeded(ctx); err != nil { 25 | return errors.Wrap(err, "check node") 26 | } 27 | 28 | return experiment.RunCmd(ctx, "", "npm", args...) 29 | }, 30 | } 31 | 32 | parent.AddCommand(cmd) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/yarn.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/internal/experiment" 5 | "github.com/empiricaly/empirica/internal/settings" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func addYarnCommand(parent *cobra.Command) error { 11 | cmd := &cobra.Command{ 12 | Use: "yarn", 13 | Short: "Run yarn commands", 14 | // Long: ``, 15 | SilenceUsage: true, 16 | SilenceErrors: true, 17 | // Args: cobra.An, 18 | Hidden: true, 19 | DisableFlagParsing: true, 20 | TraverseChildren: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := initContext() 23 | 24 | if err := settings.InstallVoltaIfNeeded(ctx); err != nil { 25 | return errors.Wrap(err, "check node") 26 | } 27 | 28 | return experiment.RunCmd(ctx, "", "yarn", args...) 29 | }, 30 | } 31 | 32 | parent.AddCommand(cmd) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Trash.svelte: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /tests/stress/experiment/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@empirica/core": "1.8.5", 4 | "minimist": "1.2.6" 5 | }, 6 | "devDependencies": { 7 | "esbuild": "0.14.47" 8 | }, 9 | "name": "empirica-server", 10 | "private": true, 11 | "scripts": { 12 | "build": "rsync -aP --exclude=node_modules --exclude=*.js -m src/* dist/ && esbuild src/index.js --color=true --log-level=warning --log-level=warning --platform=node --define:process.env.NODE_ENV='\"production\"' --bundle --minify --outfile=dist/index.js --sourcemap", 13 | "dev": "npm run build && node --trace-warnings --enable-source-maps --unhandled-rejections=warn-with-error-code dist/index.js", 14 | "serve": "node --trace-warnings --enable-source-maps --unhandled-rejections=warn-with-error-code index.js" 15 | }, 16 | "volta": { 17 | "node": "20.12.0", 18 | "npm": "10.5.0" 19 | }, 20 | "engines": { 21 | "node": ">= 20.12.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@empirica/core": "1.8.5", 4 | "esbuild": "0.17.15", 5 | "react": "18.2.0", 6 | "react-dom": "18.2.0" 7 | }, 8 | "devDependencies": { 9 | "@types/react": "18.0.14", 10 | "@types/react-dom": "18.0.5", 11 | "@unocss/reset": "^0.51.4", 12 | "@vitejs/plugin-react-refresh": "1.3.6", 13 | "autoprefixer": "10.4.7", 14 | "path": "0.12.7", 15 | "unocss": "^0.51.4", 16 | "vite": "2.9.15", 17 | "vite-plugin-restart": "0.1.1" 18 | }, 19 | "name": "react", 20 | "private": true, 21 | "scripts": { 22 | "build": "vite build", 23 | "dev": "NODE_ENV=\"development\" vite", 24 | "dev:prod": "NODE_ENV=\"production\" vite", 25 | "serve": "vite preview" 26 | }, 27 | "version": "0.0.0", 28 | "volta": { 29 | "node": "20.12.0", 30 | "npm": "10.5.0" 31 | }, 32 | "engines": { 33 | "node": ">= 20.12.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/templates/source/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "NODE_ENV=\"development\" vite", 7 | "dev:prod": "NODE_ENV=\"production\" vite", 8 | "build": "vite build", 9 | "serve": "vite preview" 10 | }, 11 | "volta": { 12 | "node": "20.11.1", 13 | "npm": "10.2.4" 14 | }, 15 | "dependencies": { 16 | "@empirica/core": "latest", 17 | "esbuild": "0.17.15", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "18.0.14", 23 | "@types/react-dom": "18.0.5", 24 | "@unocss/reset": "^0.51.4", 25 | "@vitejs/plugin-react": "4.2.1", 26 | "autoprefixer": "10.4.7", 27 | "path": "0.12.7", 28 | "rollup-plugin-polyfill-node": "^0.13.0", 29 | "unocss": "^0.58.5", 30 | "vite": "5.1.4", 31 | "vite-plugin-restart": "0.4.0" 32 | }, 33 | "version": "0.0.0" 34 | } 35 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/export/export.ts: -------------------------------------------------------------------------------- 1 | import { Conn, connect } from "../api/api"; 2 | import { exportCSV } from "./export_csv"; 3 | 4 | async function exportJSON(_: Conn, __: string) { 5 | throw new Error("JSON export not implemented"); 6 | } 7 | 8 | export enum ExportFormat { 9 | CSV = "csv", 10 | JSON = "json", 11 | } 12 | 13 | export async function runExport( 14 | url: string, 15 | token: string | null, 16 | srtoken: string, 17 | format: ExportFormat, 18 | output: string 19 | ) { 20 | const taj = await connect(url, token, srtoken); 21 | 22 | console.info("\nExporting", format.toUpperCase(), "to", output, "\n"); 23 | 24 | switch (format) { 25 | case ExportFormat.CSV: 26 | await exportCSV(taj, output); 27 | break; 28 | case ExportFormat.JSON: 29 | await exportJSON(taj, output); 30 | break; 31 | default: 32 | throw new Error(`Unknown format ${format}`); 33 | } 34 | 35 | taj.stop(); 36 | } 37 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/index.ts: -------------------------------------------------------------------------------- 1 | export { Attribute, Attributes } from "../shared/attributes"; 2 | export type { 3 | AttributeChange, 4 | AttributeOptions, 5 | AttributeUpdate, 6 | } from "../shared/attributes"; 7 | export { Globals } from "../shared/globals"; 8 | export type { Constructor } from "../shared/helpers"; 9 | export type { 10 | AttributeInput, 11 | ScopeConstructor, 12 | ScopeUpdate, 13 | ScopeIdent, 14 | Scope as SharedScope, 15 | } from "../shared/scopes"; 16 | export type { Json, JsonArray, JsonValue } from "../utils/json"; 17 | export { TajribaProvider } from "./provider"; 18 | export type { ParticipantUpdate } from "./provider"; 19 | export { Scope, Scopes } from "./scopes"; 20 | export { Step, Steps } from "./steps"; 21 | export type { Epoch, StepChange, StepTick, StepUpdate } from "./steps"; 22 | export { 23 | createNewParticipant, 24 | isDevelopment, 25 | isProduction, 26 | isTest, 27 | } from "./utils"; 28 | import "./index.css"; 29 | -------------------------------------------------------------------------------- /internal/treatments/treatments.go: -------------------------------------------------------------------------------- 1 | package treatments 2 | 3 | type FactorValue struct { 4 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 5 | Value interface{} `validate:"required" json:"value" yaml:"value"` 6 | } 7 | 8 | type Factor struct { 9 | Name string `validate:"required,gt=0,alphanumunicode" json:"name" yaml:"name"` 10 | Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` 11 | Values []*FactorValue `json:"values" yaml:"values"` 12 | } 13 | 14 | type Treatment struct { 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` 17 | Factors map[string]interface{} `validate:"required,dive,keys,gt=0,alphanumunicode,endkeys" json:"factors" yaml:"factors"` 18 | } 19 | 20 | type Treatments struct { 21 | Factors []*Factor `validate:"dive" json:"factors" yaml:"factors"` 22 | Treatments []*Treatment `validate:"dive" json:"treatments" yaml:"treatments"` 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/upload-empirica-cli/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Upload empirica CLI" 2 | description: "Uploads empirica CLI to S3" 3 | inputs: 4 | withVariants: 5 | description: "Upload variants" 6 | required: false 7 | default: "true" 8 | fileName: 9 | description: "Binary file name" 10 | required: false 11 | default: "empirica" 12 | bucket: 13 | description: "S3 Bucket" 14 | required: true 15 | root: 16 | description: "Root path in bucket" 17 | required: true 18 | awsEndpoint: 19 | description: "AWS Endpoint (to use S3 alternatives)" 20 | required: false 21 | awsSignatureVersion: 22 | description: "AWS Signature Version" 23 | required: false 24 | AWS_ACCESS_KEY_ID: 25 | description: "AWS Access Key ID" 26 | required: true 27 | AWS_SECRET_ACCESS_KEY: 28 | description: "AWS Secret Access Key" 29 | required: true 30 | path: 31 | description: "Input directory" 32 | required: true 33 | runs: 34 | using: "node16" 35 | main: "dist/index.js" 36 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/setup.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/empiricaly/empirica/internal/settings" 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const postInstallMsg = ` 12 | To get started fast: 13 | 14 | empirica create my-experiment 15 | cd my-experiment 16 | empirica 17 | 18 | Otherwise head over to https://docs.empirica.ly. 19 | ` 20 | 21 | func addSetupCommand(parent *cobra.Command) error { 22 | cmd := &cobra.Command{ 23 | Use: "setup", 24 | Short: "Setup empirica", 25 | // Long: ``, 26 | SilenceUsage: true, 27 | SilenceErrors: true, 28 | Args: cobra.NoArgs, 29 | Hidden: true, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | if err := settings.InstallVoltaIfNeeded(initContext()); err != nil { 32 | return errors.Wrap(err, "install volta") 33 | } 34 | 35 | fmt.Println(postInstallMsg) 36 | 37 | return nil 38 | }, 39 | } 40 | 41 | parent.AddCommand(cmd) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/treatments/FactorsString.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {#each Object.keys(factors) as key, i} 7 | 8 | 9 | {key} 10 | 11 | 12 | {#if factors[key] === Object(factors[key])} 13 | {#if factors[key].length} 14 | [{factors[key].join(", ")}] 15 | {:else} 16 | {"{"} 17 | {#each Object.keys(factors[key]) as k, j} 18 | {k}: {factors[key][k]} 19 | {#if j !== Object.keys(factors[key]).length - 1} 20 | , 21 | {/if} 22 | {/each} 23 | {"}"} 24 | {/if} 25 | {:else} 26 | {factors[key]} 27 | {/if} 28 | {#if !lines && i !== Object.keys(factors).length - 1} 29 | {" | "} 30 | {/if} 31 | 32 | 33 | {/each} 34 | -------------------------------------------------------------------------------- /tests/stress/README.md: -------------------------------------------------------------------------------- 1 | Inside that directory, you can run several commands: 2 | 3 | ``` 4 | pnpm exec playwright test 5 | Runs the end-to-end tests. 6 | 7 | pnpm exec playwright test --ui 8 | Starts the interactive UI mode. 9 | 10 | pnpm exec playwright test --project=chromium 11 | Runs the tests only on Desktop Chrome. 12 | 13 | pnpm exec playwright test example 14 | Runs the tests in a specific file. 15 | 16 | pnpm exec playwright test --debug 17 | Runs the tests in debug mode. 18 | 19 | pnpm exec playwright codegen 20 | Auto generate tests with Codegen. 21 | ``` 22 | 23 | We suggest that you begin by typing: 24 | 25 | ``` 26 | pnpm exec playwright test 27 | ``` 28 | 29 | And check out the following files: 30 | 31 | - ./tests/example.spec.js - Example end-to-end test 32 | - ./tests-examples/demo-todo-app.spec.js - Demo Todo App end-to-end tests 33 | - ./playwright.config.js - Playwright Test configuration 34 | 35 | Visit https://playwright.dev/docs/intro for more information. ✨ 36 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Button.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/Stage.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | usePlayer, 3 | usePlayers, 4 | useRound, 5 | } from "@empirica/core/player/classic/react"; 6 | import { Loading } from "@empirica/core/player/react"; 7 | import React from "react"; 8 | import { JellyBeans } from "./examples/JellyBeans"; 9 | import { MineSweeper } from "./examples/MineSweeper"; 10 | 11 | export function Stage() { 12 | const player = usePlayer(); 13 | const players = usePlayers(); 14 | const round = useRound(); 15 | 16 | if (player.stage.get("submit")) { 17 | if (players.length === 1) { 18 | return ; 19 | } 20 | 21 | return ( 22 |
23 | Please wait for other player(s). 24 |
25 | ); 26 | } 27 | 28 | switch (round.get("task")) { 29 | case "jellybeans": 30 | return ; 31 | case "minesweeper": 32 | return ; 33 | default: 34 | return
Unknown task
; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/templates/source/react/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetUno, 7 | presetWebFonts, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from "unocss"; 11 | 12 | export default defineConfig({ 13 | theme: { 14 | extend: { 15 | colors: { 16 | empirica: { 17 | 50: "#fbfcfe", 18 | 100: "#f2f7fd", 19 | 200: "#bcd8f6", 20 | 300: "#8abbef", 21 | 400: "#549ce8", 22 | 500: "#237fe1", 23 | 600: "#1966b8", 24 | 700: "#124b87", 25 | 800: "#0c325a", 26 | 900: "#06192d", 27 | }, 28 | }, 29 | }, 30 | }, 31 | presets: [ 32 | presetUno(), 33 | presetAttributify(), 34 | presetIcons(), 35 | presetTypography(), 36 | presetWebFonts({ 37 | fonts: { 38 | // ... 39 | }, 40 | }), 41 | ], 42 | transformers: [transformerDirectives(), transformerVariantGroup()], 43 | }); 44 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetUno, 7 | presetWebFonts, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from "unocss"; 11 | 12 | export default defineConfig({ 13 | theme: { 14 | extend: { 15 | colors: { 16 | empirica: { 17 | 50: "#fbfcfe", 18 | 100: "#f2f7fd", 19 | 200: "#bcd8f6", 20 | 300: "#8abbef", 21 | 400: "#549ce8", 22 | 500: "#237fe1", 23 | 600: "#1966b8", 24 | 700: "#124b87", 25 | 800: "#0c325a", 26 | 900: "#06192d", 27 | }, 28 | }, 29 | }, 30 | }, 31 | presets: [ 32 | presetUno(), 33 | presetAttributify(), 34 | presetIcons(), 35 | presetTypography(), 36 | presetWebFonts({ 37 | fonts: { 38 | // ... 39 | }, 40 | }), 41 | ], 42 | transformers: [transformerDirectives(), transformerVariantGroup()], 43 | }); 44 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/export/bytes.ts: -------------------------------------------------------------------------------- 1 | // From https://stackoverflow.com/a/14919494 2 | /** 3 | * Format bytes as human-readable text. 4 | * 5 | * @param bytes Number of bytes. 6 | * @param si True to use metric (SI) units, aka powers of 1000. False to use 7 | * binary (IEC), aka powers of 1024. 8 | * @param dp Number of decimal places to display. 9 | * 10 | * @return Formatted string. 11 | */ 12 | export function humanBytes(bytes: number, si = false, dp = 1) { 13 | const thresh = si ? 1000 : 1024; 14 | 15 | if (Math.abs(bytes) < thresh) { 16 | return bytes + " B"; 17 | } 18 | 19 | const units = si 20 | ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 21 | : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; 22 | let u = -1; 23 | const r = 10 ** dp; 24 | 25 | do { 26 | bytes /= thresh; 27 | ++u; 28 | } while ( 29 | Math.round(Math.abs(bytes) * r) / r >= thresh && 30 | u < units.length - 1 31 | ); 32 | 33 | return bytes.toFixed(dp) + " " + units[u]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/@empirica/core/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetUno, 7 | presetWebFonts, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from "unocss"; 11 | import { presetForms } from "@julr/unocss-preset-forms"; 12 | 13 | export default defineConfig({ 14 | theme: { 15 | colors: { 16 | empirica: { 17 | 50: "#fbfcfe", 18 | 100: "#f2f7fd", 19 | 200: "#bcd8f6", 20 | 300: "#8abbef", 21 | 400: "#549ce8", 22 | 500: "#237fe1", 23 | 600: "#1966b8", 24 | 700: "#124b87", 25 | 800: "#0c325a", 26 | 900: "#06192d", 27 | }, 28 | }, 29 | }, 30 | presets: [ 31 | presetUno(), 32 | presetForms(), 33 | presetAttributify(), 34 | presetIcons(), 35 | presetTypography(), 36 | presetWebFonts({ 37 | fonts: { 38 | // ... 39 | }, 40 | }), 41 | ], 42 | transformers: [transformerDirectives(), transformerVariantGroup()], 43 | }); 44 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/cloud/random.go: -------------------------------------------------------------------------------- 1 | package cloudcmd 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | const ( 11 | letterIdxBits = 6 // 6 bits to represent a letter index 12 | letterIdxMask = 1<= 0; { 22 | if remain == 0 { 23 | cache, remain = src.Int63(), letterIdxMax 24 | } 25 | 26 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 27 | sb.WriteByte(letterBytes[idx]) 28 | i-- 29 | } 30 | 31 | cache >>= letterIdxBits 32 | 33 | remain-- 34 | } 35 | 36 | return sb.String() 37 | } 38 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { BehaviorSubject } from "rxjs"; 4 | 5 | export function bs(init: T) { 6 | return new BehaviorSubject(init); 7 | } 8 | 9 | export function bsu(init: T | undefined = undefined) { 10 | return new BehaviorSubject(init); 11 | } 12 | 13 | export function deepEqual(obj1: any, obj2: any) { 14 | if (obj1 === obj2) 15 | // it's just the same object. No need to compare. 16 | return true; 17 | 18 | if (isPrimitive(obj1) && isPrimitive(obj2)) 19 | // compare primitives 20 | return obj1 === obj2; 21 | 22 | if (Object.keys(obj1).length !== Object.keys(obj2).length) return false; 23 | 24 | // compare objects with same number of keys 25 | for (let key in obj1) { 26 | if (!(key in obj2)) return false; //other object doesn't have this prop 27 | if (!deepEqual(obj1[key], obj2[key])) return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | //check if value is primitive 34 | function isPrimitive(obj: any) { 35 | return obj !== Object(obj); 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Empirica v2 2 | 3 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 4 | 5 | Empirica v2 is currently released and ready for creating experiments. 6 | We will be supporting Empirica v1 until December 2023. 7 | 8 | ## Installation 9 | 10 | Empirca v2 is currently has only been tested on macOS, but should work on Linux 11 | and Windows with WSL2. 12 | 13 | Run the installation script: 14 | 15 | ```sh 16 | curl https://get.empirica.dev | sh 17 | ``` 18 | 19 | To update, run the command again. 20 | 21 | ## Quick Start 22 | 23 | ``` 24 | empirica create my-project 25 | cd my-project 26 | empirica 27 | ``` 28 | 29 | The server will have started, and go to http://localhost:3000 to get started. Go 30 | to http://localhost:3000/admin to access the admin. 31 | 32 | Head over to https://docs.empirica.ly for more information. 33 | 34 | ## Contributing 35 | 36 | Thanks for your interest in contributing! There are many ways to contribute to 37 | this project. Get started here [CONTRIBUTING.md](./CONTRIBUTING.md). 38 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/react/NoGames.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { isDevelopment } from "../utils"; 3 | 4 | export function NoGames() { 5 | return ( 6 |
7 |
8 |

No experiments available

9 |

10 | There are currently no available experiments. Please wait until an 11 | experiment becomes available or come back at a later date. 12 |

13 | {isDevelopment ? ( 14 |

15 | Go to{" "} 16 | 21 | Admin 22 | {" "} 23 | to get started 24 |

25 | ) : ( 26 | "" 27 | )} 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/Export.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | Export all the data from the database. 27 | 28 |
29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /lib/admin-ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 2 | import builtins from "rollup-plugin-polyfill-node"; 3 | import UnoCSS from "unocss/vite"; 4 | import { defineConfig } from "vite"; 5 | 6 | const builtinsPlugin = { 7 | ...builtins({ include: ["fs/promises"] }), 8 | name: "rollup-plugin-polyfill-node", 9 | }; 10 | 11 | export default defineConfig({ 12 | server: { 13 | port: 3001, 14 | open: false, 15 | host: "0.0.0.0", 16 | }, 17 | clearScreen: false, 18 | // resolve: { 19 | // alias: { 20 | // $components: resolve("src/components"), 21 | // $assets: resolve("src/assets"), 22 | // }, 23 | // }, 24 | plugins: [svelte(), UnoCSS()], 25 | define: { 26 | "process.env": { 27 | NODE_ENV: process.env.NODE_ENV || "development", 28 | }, 29 | }, 30 | build: { 31 | minify: false, 32 | sourcemap: true, 33 | rollupOptions: { 34 | preserveEntrySignatures: "strict", 35 | plugins: [builtinsPlugin], 36 | output: { 37 | sourcemap: true, 38 | }, 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empirica-monorepo", 3 | "version": "1.0.0", 4 | "description": "Monorepo for Empirica v2", 5 | "directories": { 6 | "lib": "lib" 7 | }, 8 | "scripts": { 9 | "release:core": "cd ./lib/@empirica/core && npm run build && cd ../../.. && changeset publish", 10 | "install_browsers": "playwright install --with-deps", 11 | "test:e2e": "playwright test --grep-invert @performance", 12 | "bump": "changeset version", 13 | "changeset": "changeset" 14 | }, 15 | "workspaces": [ 16 | "e2e-tests", 17 | "lib/@empirica/*" 18 | ], 19 | "volta": { 20 | "node": "20.12.0", 21 | "npm": "10.5.0" 22 | }, 23 | "engines": { 24 | "node": ">= 20.12.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/empiricaly/empirica.git" 29 | }, 30 | "license": "Apache-2.0", 31 | "bugs": { 32 | "url": "https://github.com/empiricaly/empirica/issues" 33 | }, 34 | "homepage": "https://github.com/empiricaly/empirica#readme", 35 | "dependencies": { 36 | "@changesets/cli": "^2.26.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/admin-ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import Batches from "./components/batches/Batches.svelte"; 2 | import Export from "./components/Export.svelte"; 3 | import LobbiesPage from "./components/lobbies/LobbiesPage.svelte"; 4 | import NotFound from "./components/NotFound.svelte"; 5 | import Players from "./components/players/Players.svelte"; 6 | import FactorsPage from "./components/treatments/FactorsPage.svelte"; 7 | import TreatmentsPage from "./components/treatments/TreatmentsPage.svelte"; 8 | import UnderConstruction from "./components/UnderConstruction.svelte"; 9 | 10 | export const routes = { 11 | "/": Batches, 12 | "/games": UnderConstruction, 13 | "/players": Players, 14 | "/export": Export, 15 | "/treatments": TreatmentsPage, 16 | "/factors": FactorsPage, 17 | "/lobbies": LobbiesPage, 18 | "/users": UnderConstruction, 19 | 20 | // // Using named parameters, with last being optional 21 | // '/author/:first/:last?': Author, 22 | 23 | // // Wildcard parameter 24 | // '/book/*': Book, 25 | 26 | // Catch-all 27 | // This is optional, but if present it must be the last 28 | "*": NotFound, 29 | }; 30 | -------------------------------------------------------------------------------- /internal/build/releases.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | const ( 13 | EmpiricaPackageName = "@empirica/core" 14 | EmpiricaLatestReleasePath = "https://api.github.com/repos/empiricaly/empirica/releases/latest" 15 | ) 16 | 17 | // CurrentLatestRelease returns the latest version of empirica 18 | func CurrentLatestRelease() (string, error) { 19 | res, err := http.Get(EmpiricaLatestReleasePath) 20 | if err != nil { 21 | return "", errors.Wrap(err, "fetch latest release") 22 | } 23 | defer res.Body.Close() 24 | 25 | // parse json body 26 | var release struct { 27 | TagName string `json:"tag_name"` 28 | } 29 | 30 | if err := json.NewDecoder(res.Body).Decode(&release); err != nil { 31 | return "", errors.Wrap(err, "decode json") 32 | } 33 | 34 | v := strings.TrimPrefix(release.TagName, EmpiricaPackageName) 35 | 36 | // remove anything that's not semver 37 | v = strings.TrimLeftFunc(v, func(r rune) bool { 38 | return !unicode.IsDigit(r) && r != '.' 39 | }) 40 | 41 | return v, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/components/Timer.jsx: -------------------------------------------------------------------------------- 1 | import { useStageTimer } from "@empirica/core/player/classic/react"; 2 | import React from "react"; 3 | 4 | export function Timer() { 5 | const timer = useStageTimer(); 6 | 7 | let remaining; 8 | if (timer?.remaining || timer?.remaining === 0) { 9 | remaining = Math.round(timer?.remaining / 1000); 10 | } 11 | 12 | return ( 13 |
14 |

15 | {humanTimer(remaining)} 16 |

17 |
18 | ); 19 | } 20 | 21 | function humanTimer(seconds) { 22 | if (seconds === null || seconds === undefined) { 23 | return "--:--"; 24 | } 25 | 26 | let out = ""; 27 | const s = seconds % 60; 28 | out += s < 10 ? "0" + s : s; 29 | 30 | const min = (seconds - s) / 60; 31 | if (min === 0) { 32 | return `00:${out}`; 33 | } 34 | 35 | const m = min % 60; 36 | out = `${m < 10 ? "0" + m : m}:${out}`; 37 | 38 | const h = (min - m) / 60; 39 | if (h === 0) { 40 | return out; 41 | } 42 | 43 | return `${h}:${out}`; 44 | } 45 | -------------------------------------------------------------------------------- /lib/admin-ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "nodenext", 5 | "target": "esnext", 6 | /** 7 | * svelte-preprocess cannot figure out whether you have 8 | * a value or a type, so tell TypeScript to enforce using 9 | * `import type` instead of `import` for Types. 10 | */ 11 | // "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | /** 14 | * To have warnings / errors of the Svelte compiler at the 15 | * correct position, enable source maps by default. 16 | */ 17 | "sourceMap": true, 18 | "esModuleInterop": true, 19 | "skipLibCheck": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "baseUrl": ".", 22 | /** 23 | * Typecheck JS in `.svelte` and `.js` files by default. 24 | * Disable this if you'd like to use dynamic types. 25 | */ 26 | "checkJs": true 27 | }, 28 | /** 29 | * Use globals.d.ts instead of compilerOptions.types 30 | * to avoid limiting type declarations. 31 | */ 32 | "include": ["globals.d.ts", "src/**/*.js", "src/**/*.svelte"] 33 | } 34 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Alert.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 | 9 | 22 |
23 |
24 | {#if title} 25 |

26 | {title} 27 |

28 | {/if} 29 |
30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | /** 7 | * svelte-preprocess cannot figure out whether you have 8 | * a value or a type, so tell TypeScript to enforce using 9 | * `import type` instead of `import` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | * To have warnings / errors of the Svelte compiler at the 16 | * correct position, enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | /** 24 | * Typecheck JS in `.svelte` and `.js` files by default. 25 | * Disable this if you'd like to use dynamic types. 26 | */ 27 | "checkJs": true 28 | }, 29 | /** 30 | * Use global.d.ts instead of compilerOptions.types 31 | * to avoid limiting type declarations. 32 | */ 33 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] 34 | } 35 | -------------------------------------------------------------------------------- /internal/templates/source/callbacks/src/index.js: -------------------------------------------------------------------------------- 1 | import { AdminContext } from "@empirica/core/admin"; 2 | import { 3 | Classic, 4 | classicKinds, 5 | ClassicLoader, 6 | Lobby, 7 | } from "@empirica/core/admin/classic"; 8 | import { info, setLogLevel } from "@empirica/core/console"; 9 | import minimist from "minimist"; 10 | import process from "process"; 11 | import { Empirica } from "./callbacks"; 12 | 13 | const argv = minimist(process.argv.slice(2), { string: ["token"] }); 14 | 15 | setLogLevel(argv["loglevel"] || "info"); 16 | 17 | (async () => { 18 | const ctx = await AdminContext.init( 19 | argv["url"] || "http://localhost:3000/query", 20 | argv["sessionTokenPath"], 21 | "callbacks", 22 | argv["token"], 23 | {}, 24 | classicKinds 25 | ); 26 | 27 | ctx.register(ClassicLoader); 28 | ctx.register(Classic()); 29 | ctx.register(Lobby()); 30 | ctx.register(Empirica); 31 | ctx.register(function (_) { 32 | _.on("ready", function () { 33 | info("server: started"); 34 | }); 35 | }); 36 | })(); 37 | 38 | process.on("unhandledRejection", function (reason, p) { 39 | process.exitCode = 1; 40 | console.error("Unhandled Promise Rejection. Reason: ", reason); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/layout/SidebarButton.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | {#if svgPath} 21 | 30 | {/if} 31 | 32 | {label} 33 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /tests/stress/experiment/client/vite.config.js: -------------------------------------------------------------------------------- 1 | import reactRefresh from "@vitejs/plugin-react-refresh"; 2 | import { resolve } from "path"; 3 | import { defineConfig, searchForWorkspaceRoot } from "vite"; 4 | import restart from "vite-plugin-restart"; 5 | import UnoCSS from "unocss/vite"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | optimizeDeps: { 10 | exclude: ["@empirica/tajriba", "@empirica/core"], 11 | }, 12 | server: { 13 | port: 8844, 14 | open: false, 15 | strictPort: true, 16 | host: "0.0.0.0", 17 | fs: { 18 | allow: [ 19 | // search up for workspace root 20 | searchForWorkspaceRoot(process.cwd()), 21 | ], 22 | }, 23 | }, 24 | build: { 25 | minify: false, 26 | }, 27 | clearScreen: false, 28 | plugins: [ 29 | restart({ 30 | restart: [ 31 | "./uno.config.cjs", 32 | "./node_modules/@empirica/core/dist/**/*.{js,ts,jsx,tsx,css}", 33 | "./node_modules/@empirica/core/assets/**/*.css", 34 | ], 35 | }), 36 | UnoCSS(), 37 | reactRefresh(), 38 | ], 39 | define: { 40 | "process.env": { 41 | NODE_ENV: process.env.NODE_ENV || "development", 42 | }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Select.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if !selected && placeholder} 14 | {placeholder} 15 | {/if} 16 | 31 |
32 | 33 | 41 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/classic/api/api_test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { connect } from "./api"; 3 | import { withTajriba } from "./connection_test_helper"; 4 | 5 | const t = test; 6 | // const t = test.serial; 7 | // const to = test.only; 8 | 9 | t("query with api", async (t) => { 10 | await withTajriba( 11 | async ({ url, srtoken }) => { 12 | const taj = await connect(url, null, srtoken); 13 | 14 | t.truthy(taj); 15 | for await (const player of taj.players()) { 16 | console.log("player", player.id); 17 | for (const attr of player.attributes) { 18 | console.log(` ${attr.key} ${attr.value}`); 19 | } 20 | } 21 | for await (const batch of taj.batches()) { 22 | console.log("batch", batch.id); 23 | for (const attr of batch.attributes) { 24 | console.log(` ${attr.key} ${attr.value}`); 25 | } 26 | } 27 | for await (const game of taj.games()) { 28 | console.log("game", game.id); 29 | for (const attr of game.attributes) { 30 | console.log(` ${attr.key} ${attr.value}`); 31 | } 32 | } 33 | }, 34 | { tajFile: "src/admin/classic/api/tajriba.json", printLogs: false } 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /internal/templates/source/export/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ExportFormat, 3 | runExport, 4 | withTajriba, 5 | } from "@empirica/core/admin/classic"; 6 | import { setLogLevel } from "@empirica/core/console"; 7 | import minimist from "minimist"; 8 | 9 | const argv = minimist(process.argv.slice(2), { 10 | string: ["token", "srtoken", "filename", "tajfile", "url"], 11 | }); 12 | 13 | setLogLevel(argv["loglevel"] || "info"); 14 | 15 | (async () => { 16 | try { 17 | const run = async (url, srtoken = argv["srtoken"]) => { 18 | await runExport( 19 | url, 20 | argv["token"], 21 | srtoken, 22 | ExportFormat.CSV, 23 | argv["filename"] 24 | ); 25 | }; 26 | 27 | if (argv["url"]) { 28 | await run(argv["url"]); 29 | 30 | return; 31 | } 32 | 33 | if (!argv["tajfile"]) { 34 | throw new Error("Missing tajfile"); 35 | } 36 | 37 | await withTajriba( 38 | async ({ url, srtoken }) => { 39 | await run(url, srtoken); 40 | }, 41 | { 42 | tajFile: argv["tajfile"], 43 | printLogs: false, 44 | } 45 | ); 46 | 47 | process.exit(0); 48 | } catch (err) { 49 | console.error(err); 50 | process.exit(1); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/layout/Logo.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/classic/index.ts: -------------------------------------------------------------------------------- 1 | export { Attribute, Attributes } from "../../shared/attributes"; 2 | export type { 3 | AttributeChange, 4 | AttributeOptions, 5 | AttributeUpdate, 6 | } from "../../shared/attributes"; 7 | export { Globals } from "../../shared/globals"; 8 | export type { Constructor } from "../../shared/helpers"; 9 | export type { 10 | AttributeInput, 11 | ScopeConstructor, 12 | ScopeIdent, 13 | ScopeUpdate, 14 | Scope as SharedScope, 15 | } from "../../shared/scopes"; 16 | export type { Json, JsonArray, JsonValue } from "../../utils/json"; 17 | export { TajribaProvider } from "../provider"; 18 | export type { ParticipantUpdate } from "../provider"; 19 | export { Scope, Scopes } from "../scopes"; 20 | export { Step, Steps } from "../steps"; 21 | export type { Epoch, StepChange, StepTick, StepUpdate } from "../steps"; 22 | export { 23 | createNewParticipant, 24 | isDevelopment, 25 | isProduction, 26 | isTest, 27 | } from "../utils"; 28 | export { 29 | EmpiricaClassic, 30 | Game, 31 | Player, 32 | PlayerGame, 33 | PlayerRound, 34 | PlayerStage, 35 | Round, 36 | Stage, 37 | } from "./classic"; 38 | export type { 39 | Context, 40 | EmpiricaClassicContext, 41 | EmpiricaClassicKinds, 42 | } from "./classic"; 43 | import "../index.css"; 44 | -------------------------------------------------------------------------------- /lib/admin-ui/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { presetForms } from "@julr/unocss-preset-forms"; 2 | import { 3 | defineConfig, 4 | presetTypography, 5 | presetUno, 6 | transformerDirectives, 7 | transformerVariantGroup, 8 | } from "unocss"; 9 | 10 | export default defineConfig({ 11 | safelist: [ 12 | ...["blue", "green", "yellow", "grey"] 13 | .map((color) => [ 14 | `border-${color}-600`, 15 | `text-${color}-600`, 16 | `bg-${color}-600`, 17 | `hover:bg-${color}-700`, 18 | `bg-${color}-100`, 19 | `text-${color}-800`, 20 | ]) 21 | .flat(), 22 | ], 23 | theme: { 24 | colors: { 25 | empirica: { 26 | 50: "#fbfcfe", 27 | 100: "#f2f7fd", 28 | 200: "#bcd8f6", 29 | 300: "#8abbef", 30 | 400: "#549ce8", 31 | 500: "#237fe1", 32 | 600: "#1966b8", 33 | 700: "#124b87", 34 | 800: "#0c325a", 35 | 900: "#06192d", 36 | }, 37 | }, 38 | }, 39 | presets: [ 40 | presetUno(), 41 | presetForms(), 42 | // presetAttributify(), 43 | // presetIcons(), 44 | presetTypography(), 45 | // presetWebFonts({ 46 | // fonts: { 47 | // // ... 48 | // }, 49 | // }), 50 | ], 51 | transformers: [transformerDirectives(), transformerVariantGroup()], 52 | }); 53 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/transitions.ts: -------------------------------------------------------------------------------- 1 | import { EventType, State, TajribaAdmin } from "@empirica/tajriba"; 2 | import { Subject } from "rxjs"; 3 | import { error } from "../utils/console"; 4 | 5 | export interface Step { 6 | id: string; 7 | state: State; 8 | duration: number; 9 | startedAt?: number; 10 | endedAt?: number; 11 | } 12 | 13 | export interface Transition { 14 | id: string; 15 | from: State; 16 | to: State; 17 | step: Step; 18 | } 19 | 20 | export function transitionsSub( 21 | taj: TajribaAdmin, 22 | transitions: Subject, 23 | nodeID: string 24 | ) { 25 | taj.onEvent({ eventTypes: [EventType.TransitionAdd], nodeID }).subscribe({ 26 | next({ node }) { 27 | if (!node) { 28 | return; 29 | } 30 | 31 | if (node.__typename !== "Transition") { 32 | error(`received non-transition`); 33 | 34 | return; 35 | } 36 | 37 | if (node.node.__typename !== "Step") { 38 | error(`received non-step transition`, node.node); 39 | 40 | return; 41 | } 42 | 43 | transitions.next({ 44 | id: node.id, 45 | to: node.to, 46 | from: node.from, 47 | step: { 48 | id: node.node.id, 49 | duration: node.node.duration, 50 | state: node.node.state, 51 | }, 52 | }); 53 | }, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { EmpiricaClassic } from "@empirica/core/player/classic"; 2 | import { EmpiricaContext } from "@empirica/core/player/classic/react"; 3 | import { EmpiricaMenu, EmpiricaParticipant } from "@empirica/core/player/react"; 4 | import React from "react"; 5 | import { Game } from "./Game"; 6 | import { ExitSurvey } from "./intro-exit/ExitSurvey"; 7 | import { Introduction } from "./intro-exit/Introduction"; 8 | 9 | export default function App() { 10 | const urlParams = new URLSearchParams(window.location.search); 11 | const playerKey = urlParams.get("participantKey") || ""; 12 | 13 | const { protocol, host } = window.location; 14 | const url = `${protocol}//${host}/query`; 15 | 16 | function introSteps({ game, player }) { 17 | return [Introduction]; 18 | } 19 | 20 | function exitSteps({ game, player }) { 21 | return [ExitSurvey]; 22 | } 23 | 24 | return ( 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /internal/templates/source/svelte/src/App.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

Hello world!

7 | 8 | 9 | 10 |

11 | Visit svelte.dev to learn how to build Svelte 12 | apps. 13 |

14 | 15 |

16 | Check out SvelteKit for 17 | the officially supported framework, also powered by Vite! 18 |

19 |
20 | 21 | 64 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/bundle.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/empiricaly/empirica/internal/bundle" 5 | "github.com/empiricaly/empirica/internal/settings" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | func addBundleCommand(parent *cobra.Command) error { 12 | cmd := &cobra.Command{ 13 | Use: "bundle", 14 | Short: "Bundle project", 15 | // Long: ``, 16 | SilenceUsage: true, 17 | SilenceErrors: true, 18 | Args: cobra.NoArgs, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | ctx := initContext() 21 | 22 | useGzip, err := cmd.Flags().GetBool("gzip") 23 | if err != nil { 24 | return errors.Wrap(err, "parse gzip flag") 25 | } 26 | 27 | out, err := cmd.Flags().GetString("out") 28 | if err != nil { 29 | return errors.Wrap(err, "parse out flag") 30 | } 31 | 32 | conf := getConfig() 33 | 34 | if err := settings.InstallVoltaIfNeeded(ctx); err != nil { 35 | return errors.Wrap(err, "check node") 36 | } 37 | 38 | return bundle.Bundle(ctx, conf, out, useGzip) 39 | }, 40 | } 41 | 42 | cmd.Flags().Bool("gzip", false, "use gzip") 43 | cmd.Flags().String("out", "", "defaults to current dir") 44 | 45 | err := viper.BindPFlags(cmd.Flags()) 46 | if err != nil { 47 | return errors.Wrap(err, "bind bundle flags") 48 | } 49 | 50 | parent.AddCommand(cmd) 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/ButtonGroup.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 33 | 34 | 35 | {#each options as option, i} 36 | 44 | {/each} 45 | 46 | -------------------------------------------------------------------------------- /tests/stress/experiment/server/src/index.js: -------------------------------------------------------------------------------- 1 | import { AdminContext } from "@empirica/core/admin"; 2 | import { 3 | Classic, 4 | classicKinds, 5 | ClassicLoader, 6 | Lobby, 7 | } from "@empirica/core/admin/classic"; 8 | import { info, setLogLevel } from "@empirica/core/console"; 9 | import minimist from "minimist"; 10 | import process from "process"; 11 | import { Empirica } from "./callbacks"; 12 | 13 | const argv = minimist(process.argv.slice(2), { string: ["token"] }); 14 | 15 | setLogLevel(argv["loglevel"] || "info"); 16 | 17 | (async () => { 18 | const ctx = await AdminContext.init( 19 | argv["url"] || "http://localhost:3000/query", 20 | argv["sessionTokenPath"], 21 | "callbacks", 22 | argv["token"], 23 | {}, 24 | classicKinds 25 | ); 26 | 27 | const preferUnderassignedGames = 28 | process.env.PREFER_UNDERASSIGNED_GAMES === "1"; 29 | 30 | console.log("preferUnderassignedGames", preferUnderassignedGames); 31 | 32 | ctx.register(ClassicLoader); 33 | ctx.register( 34 | Classic({ 35 | preferUnderassignedGames, 36 | }) 37 | ); 38 | ctx.register(Lobby()); 39 | ctx.register(Empirica); 40 | ctx.register(function (_) { 41 | _.on("ready", function () { 42 | info("server: started"); 43 | }); 44 | }); 45 | })(); 46 | 47 | process.on("unhandledRejection", function (reason, p) { 48 | process.exitCode = 1; 49 | console.error("Unhandled Promise Rejection. Reason: ", reason); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/admin-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empirica-admin", 3 | "license": "Apache-2.0", 4 | "description": "Empirica Admin UI", 5 | "version": "1.0.5", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build --base=/admin/", 9 | "serve": "vite preview" 10 | }, 11 | "author": "Nicolas Paton ", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/empiricaly/empirica.git" 15 | }, 16 | "type": "module", 17 | "keywords": [ 18 | "empirica", 19 | "experiment", 20 | "research", 21 | "multiplayer", 22 | "real-time", 23 | "behavioral", 24 | "social-science", 25 | "psychology" 26 | ], 27 | "homepage": "https://github.com/empiricaly/empirica#readme", 28 | "bugs": { 29 | "url": "https://github.com/empiricaly/empirica/issues" 30 | }, 31 | "volta": { 32 | "node": "20.10.0", 33 | "npm": "10.2.3" 34 | }, 35 | "devDependencies": { 36 | "@julr/unocss-preset-forms": "0.0.5", 37 | "@sveltejs/vite-plugin-svelte": "2.4.1", 38 | "path": "0.12.7", 39 | "rollup-plugin-polyfill-node": "0.12.0", 40 | "vite": "4.3.9", 41 | "unocss": "0.53.1" 42 | }, 43 | "dependencies": { 44 | "@empirica/core": "latest", 45 | "@empirica/tajriba": "latest", 46 | "remarkable": "2.0.1", 47 | "svelte": "3.59.1", 48 | "svelte-spa-router": "3.3.0", 49 | "@unocss/reset": "0.51.4", 50 | "yup": "1.2.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/globals.ts: -------------------------------------------------------------------------------- 1 | import { SetAttributeInput, SubAttributesPayload } from "@empirica/tajriba"; 2 | import { Observable } from "rxjs"; 3 | import { AttributeOptions } from "../player"; 4 | import { Globals as SharedGlobals } from "../shared/globals"; 5 | import { JsonValue } from "../utils/json"; 6 | import { bsu } from "../utils/object"; 7 | 8 | export class Globals extends SharedGlobals { 9 | constructor( 10 | globals: Observable, 11 | private globalScopeID: string, 12 | private setAttributes: (input: SetAttributeInput[]) => Promise 13 | ) { 14 | super(globals); 15 | } 16 | 17 | set(key: string, value: JsonValue, ao?: Partial) { 18 | let attr = this.attrs.get(key); 19 | if (!attr) { 20 | attr = bsu(); 21 | this.attrs.set(key, attr); 22 | } 23 | attr.next(value); 24 | 25 | const attrProps: SetAttributeInput = { 26 | key: key, 27 | nodeID: this.globalScopeID, 28 | val: JSON.stringify(value), 29 | }; 30 | 31 | if (ao) { 32 | // TODO Fix this. Should check if compatible with existing attribute and 33 | // only set fields set on ao. 34 | attrProps.private = ao.private; 35 | attrProps.protected = ao.protected; 36 | attrProps.immutable = ao.immutable; 37 | attrProps.ephemeral = ao.ephemeral; 38 | attrProps.append = ao.append; 39 | attrProps.index = ao.index; 40 | } 41 | 42 | this.setAttributes([attrProps]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/stress/experiment/server/src/callbacks.js: -------------------------------------------------------------------------------- 1 | import { ClassicListenersCollector } from "@empirica/core/admin/classic"; 2 | export const Empirica = new ClassicListenersCollector(); 3 | 4 | Empirica.onGameStart(({ game }) => { 5 | const { roundCount, stageCount } = game.get("treatment"); 6 | 7 | for (let i = 0; i < roundCount; i++) { 8 | const round = game.addRound({ name: `Round ${i}` }); 9 | for (let j = 0; j < stageCount; j++) { 10 | round.addStage({ name: `Stage ${j}`, duration: 120 }); 11 | } 12 | } 13 | }); 14 | 15 | Empirica.on("player", "replay", async (ctx, { player, replay }) => { 16 | if (!replay) { 17 | return; 18 | } 19 | 20 | const batches = Array.from(ctx.scopesByKind("batch").values()); 21 | 22 | for (const batch of batches) { 23 | if (!batch.isRunning) { 24 | continue; 25 | } 26 | 27 | for (const game of batch.games) { 28 | if (game.hasEnded) { 29 | continue; 30 | } 31 | 32 | console.log("REPLAYING GAME", game.id, "FOR PLAYER", player.id); 33 | await game.assignPlayer(player); 34 | } 35 | } 36 | 37 | player.set("replay", false); 38 | }); 39 | 40 | Empirica.onRoundStart(({ round }) => {}); 41 | 42 | Empirica.onStageStart(({ stage }) => { 43 | // Used for test called: "attribute as bool, correct equality check" 44 | stage.currentGame.set("key1", false); 45 | }); 46 | 47 | Empirica.onStageEnded(({ stage }) => {}); 48 | 49 | Empirica.onRoundEnded(({ round }) => {}); 50 | 51 | Empirica.onGameEnded(({ game }) => {}); 52 | -------------------------------------------------------------------------------- /internal/templates/source/react/src/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | usePlayer, 3 | useRound, 4 | useStage, 5 | } from "@empirica/core/player/classic/react"; 6 | import React from "react"; 7 | import { Avatar } from "./components/Avatar"; 8 | import { Timer } from "./components/Timer"; 9 | 10 | export function Profile() { 11 | const player = usePlayer(); 12 | const round = useRound(); 13 | const stage = useStage(); 14 | 15 | const score = player.get("score") || 0; 16 | 17 | return ( 18 |
19 |
20 |
21 | {round ? round.get("name") : ""} 22 |
23 |
24 | {stage ? stage.get("name") : ""} 25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 |
33 | Score 34 |
35 |
36 | {score} 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/react/EmpiricaParticipant.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Mode, ParticipantContext, ParticipantModeContext } from "../context"; 3 | import { WithChildren } from "./helpers"; 4 | 5 | export const ParticipantCtx = React.createContext< 6 | ParticipantContext | undefined 7 | >(undefined); 8 | 9 | export type EmpiricaParticipantProps = WithChildren<{ 10 | url: string; 11 | ns: string; 12 | modeFunc?: Mode; 13 | }>; 14 | 15 | // We want to only initialize the connection once per namespace, so we keep 16 | // previously created connections. 17 | // TODO: cleanup old connections. 18 | // It's ok to just keep the previous connection for simple cases where we're 19 | // only using one connection, but if EmpiricaParticipant is used multiple times 20 | // on a page, and some are reset, we would be leaking connections. 21 | const contexts: { [key: string]: ParticipantContext } = {}; 22 | 23 | export function EmpiricaParticipant({ 24 | url, 25 | ns, 26 | modeFunc, 27 | children, 28 | }: EmpiricaParticipantProps) { 29 | let partCtx: ParticipantContext; 30 | 31 | if (ns in contexts) { 32 | partCtx = contexts[ns]!; 33 | } else { 34 | if (modeFunc) { 35 | partCtx = new ParticipantModeContext(url, ns, modeFunc); 36 | } else { 37 | partCtx = new ParticipantContext(url, ns); 38 | } 39 | 40 | contexts[ns] = partCtx; 41 | } 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "customizations": { 4 | "codespaces": { 5 | "repositories": { 6 | "empiricaly/tajriba": { 7 | "permissions": "write-all" 8 | } 9 | } 10 | }, 11 | 12 | "vscode": { 13 | "extensions": [ 14 | "aaron-bond.better-comments", 15 | "akmarnafi.comment-headers", 16 | "axelrindle.duplicate-file", 17 | "bradlc.vscode-tailwindcss", 18 | "ChristophLipp.shruggoff", 19 | "eamodio.gitlens", 20 | "eriklynd.json-tools", 21 | "esbenp.prettier-vscode", 22 | "formulahendry.auto-rename-tag", 23 | "GitHub.codespaces", 24 | "GitHub.copilot", 25 | "GitHub.remotehub", 26 | "github.vscode-github-actions", 27 | "GitHub.vscode-pull-request-github", 28 | "golang.go", 29 | "jobe451.lorem-whatever", 30 | "kumar-harsh.graphql-for-vscode", 31 | "marclipovsky.string-manipulation", 32 | "mitchdenny.ecdc", 33 | "mohsen1.prettify-json", 34 | "mrmlnc.vscode-duplicate", 35 | "ms-azuretools.vscode-docker", 36 | "oderwat.indent-rainbow", 37 | "ow.vscode-subword-navigation", 38 | "stackbreak.comment-divider", 39 | "stkb.rewrap", 40 | "svelte.svelte-vscode", 41 | "Tyriar.lorem-ipsum", 42 | "wmaurer.change-case" 43 | ] 44 | } 45 | }, 46 | "postCreateCommand": "bash -i ./.devcontainer/post-create.sh" 47 | } 48 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/admin/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | AttributeChange, 3 | AttributeOptions, 4 | AttributeUpdate, 5 | Attribute as SharedAttribute, 6 | Attributes as SharedAttributes, 7 | } from "../shared/attributes"; 8 | export { Globals as SharedGlobals } from "../shared/globals"; 9 | export type { Constructor } from "../shared/helpers"; 10 | export type { 11 | Attributable, 12 | AttributeInput, 13 | ScopeConstructor, 14 | ScopeIdent, 15 | ScopeUpdate, 16 | Scope as SharedScope, 17 | } from "../shared/scopes"; 18 | export { TajribaConnection } from "../shared/tajriba_connection"; 19 | export type { Json, JsonArray, JsonValue } from "../utils/json"; 20 | export { AttributeMsg, Attributes } from "./attributes"; 21 | export { AdminConnection } from "./connection"; 22 | export { AdminContext, TajribaAdminAccess } from "./context"; 23 | export type { 24 | AddLinkPayload, 25 | AddScopePayload, 26 | AddTransitionPayload, 27 | Finalizer, 28 | StepPayload, 29 | } from "./context"; 30 | export { 31 | EventContext, 32 | EvtCtxCallback, 33 | ListenersCollector, 34 | ListenersCollectorProxy, 35 | TajribaEvent, 36 | } from "./events"; 37 | export type { Subscriber } from "./events"; 38 | export { Globals } from "./globals"; 39 | export { participantsSub } from "./participants"; 40 | export type { Connection, ConnectionMsg, Participant } from "./participants"; 41 | export { Scope, Scopes } from "./scopes"; 42 | export type { KV, ScopeSubscriptionInput, Subs } from "./subscriptions"; 43 | export type { Step } from "./transitions"; 44 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/react/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Attribute, 3 | AttributeChange, 4 | AttributeOptions, 5 | AttributeUpdate, 6 | } from "../../shared/attributes"; 7 | export { Globals } from "../../shared/globals"; 8 | export type { ScopeIdent, ScopeUpdate } from "../../shared/scopes"; 9 | export { TajribaConnection } from "../../shared/tajriba_connection"; 10 | export type { Json, JsonArray, JsonValue } from "../../utils/json"; 11 | export { ParticipantContext } from "../context"; 12 | export type { Mode } from "../context"; 13 | export { TajribaProvider } from "../provider"; 14 | export type { ParticipantUpdate } from "../provider"; 15 | export type { StepChange, StepUpdate } from "../steps"; 16 | export { Consent } from "./Consent"; 17 | export type { ConsentProps } from "./Consent"; 18 | export { EmpiricaMenu } from "./EmpiricaMenu"; 19 | export type { EmpiricaMenuProps } from "./EmpiricaMenu"; 20 | export { EmpiricaParticipant } from "./EmpiricaParticipant"; 21 | export type { EmpiricaParticipantProps } from "./EmpiricaParticipant"; 22 | export { Finished } from "./Finished"; 23 | export { Loading } from "./Loading"; 24 | export { Logo } from "./Logo"; 25 | export { NoGames } from "./NoGames"; 26 | export { PlayerCreate } from "./PlayerCreate"; 27 | export type { PlayerCreateProps } from "./PlayerCreate"; 28 | export type { WithChildren } from "./helpers"; 29 | export { 30 | useConsent, 31 | useGlobal, 32 | useParticipantContext, 33 | usePlayerID, 34 | useTajriba, 35 | useTajribaConnected, 36 | useTajribaConnecting, 37 | } from "./hooks"; 38 | -------------------------------------------------------------------------------- /internal/experiment/utils.go: -------------------------------------------------------------------------------- 1 | package experiment 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "time" 11 | 12 | "github.com/briandowns/spinner" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const dirPerm = 0o777 17 | 18 | func CreateDir(dir string) error { 19 | if _, err := os.Stat(dir); os.IsNotExist(err) { 20 | if err := os.MkdirAll(dir, dirPerm); err != nil { 21 | return errors.Wrapf(err, "create directory '%s'", dir) 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func RunCmd(ctx context.Context, dir, command string, args ...string) error { 29 | return runCmdSilence(ctx, dir, false, command, args...) 30 | } 31 | 32 | func RunCmdSilent(ctx context.Context, dir, command string, args ...string) error { 33 | return runCmdSilence(ctx, dir, true, command, args...) 34 | } 35 | 36 | func runCmdSilence(ctx context.Context, dir string, silent bool, command string, args ...string) error { 37 | c := exec.CommandContext(ctx, command, args...) 38 | 39 | if !silent { 40 | c.Stderr = os.Stderr 41 | c.Stdout = os.Stdout 42 | } else { 43 | c.Stderr = ioutil.Discard 44 | c.Stdout = ioutil.Discard 45 | } 46 | c.Dir = dir 47 | 48 | if err := c.Run(); err != nil { 49 | return errors.Wrapf(err, "%s %s", command, strings.Join(args, " ")) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func ShowSpinner(text string) func() { 56 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 57 | s.Suffix = " " + text 58 | s.Start() 59 | return func() { 60 | s.Stop() 61 | fmt.Println("✓ " + text) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/shared/globals.ts: -------------------------------------------------------------------------------- 1 | import { SubAttributesPayload } from "@empirica/tajriba"; 2 | import { BehaviorSubject, Observable } from "rxjs"; 3 | import { JsonValue } from "../utils/json"; 4 | 5 | export class Globals { 6 | protected attrs = new Map>(); 7 | private updates = new Map(); 8 | public self: BehaviorSubject; 9 | 10 | constructor(globals: Observable) { 11 | this.self = new BehaviorSubject(undefined); 12 | 13 | globals.subscribe({ 14 | next: ({ attribute, done }) => { 15 | if (attribute) { 16 | let val = undefined; 17 | if (attribute.val) { 18 | val = JSON.parse(attribute.val); 19 | } 20 | 21 | this.updates.set(attribute.key, val); 22 | } 23 | 24 | if (done) { 25 | for (const [key, val] of this.updates) { 26 | this.obs(key).next(val); 27 | } 28 | 29 | this.updates.clear(); 30 | 31 | if (this.self) { 32 | this.self.next(this); 33 | } 34 | } 35 | }, 36 | }); 37 | } 38 | 39 | get(key: string): JsonValue | undefined { 40 | const o = this.attrs.get(key); 41 | if (o) { 42 | return o.getValue(); 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | obs(key: string) { 49 | let o = this.attrs.get(key); 50 | if (!o) { 51 | o = new BehaviorSubject(undefined); 52 | this.attrs.set(key, o); 53 | } 54 | 55 | return o; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Input.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
16 | {#if prefix} 17 |
18 | {prefix} 19 |
20 | {/if} 21 | {#if type === "text"} 22 | 23 | {:else if type === "number"} 24 | 34 | {:else} 35 | Type not implemented ('{type}') 36 | {/if} 37 | {#if suffix} 38 |
39 | 40 | {suffix} 41 | 42 |
43 | {/if} 44 |
45 | 46 | 51 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/classic/react/Slider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler, RefObject, useRef } from "react"; 2 | 3 | export interface SliderProps { 4 | value: number; 5 | onChange: ChangeEventHandler; 6 | min?: number; 7 | max?: number; 8 | stepSize?: number; 9 | disabled?: boolean; 10 | } 11 | 12 | export function Slider({ 13 | value, 14 | onChange, 15 | min = 0, 16 | max = 100, 17 | stepSize = 1, 18 | disabled = false, 19 | }: SliderProps) { 20 | const noVal = value === null || value === undefined; 21 | const val = noVal ? (max - min) / 2 : value; 22 | const cls = noVal ? "slider-thumb-zero" : "slider-thumb"; 23 | const ref: RefObject = useRef(null); 24 | 25 | if (value !== null && ref.current) { 26 | const nmin = min ? min : 0; 27 | const nmax = max ? max : 100; 28 | const newVal = Number(((value - nmin) * 100) / (nmax - nmin)); 29 | 30 | ref.current.style.left = `calc(${newVal}% + (${8 - newVal * 0.15}px))`; 31 | } 32 | 33 | return ( 34 |
35 | 45 | {noVal ? ( 46 | "" 47 | ) : ( 48 | 52 | {value} 53 | 54 | )} 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/@empirica/core/README.md: -------------------------------------------------------------------------------- 1 | # @empirica/core 2 | 3 | The `@empirica/core` package contains multiple modules that are used by the 4 | Empirica platform. 5 | 6 | There are two main categories of modules: 7 | 8 | - **Admin**: modules that are used with full access to Tajriba data and 9 | functionality. These modules are used by the Empirica Admin Panel and the 10 | server. 11 | - **Player**: modules that are used by the Empirica Player. These modules are 12 | used in the frontend. 13 | 14 | There is also a console module that is used throughout for logging. 15 | 16 | Empirica is built on top of Tajriba, which is a server-side framework for 17 | building experiments. Parts of the `@empirica/core` package are building blocks 18 | on top of Tajriba and are not specific to Empirica. 19 | 20 | The parts of the `@empirica/core` package that are specific to Empirica are 21 | called `classic`. They implement the core functionality of Empirica, which are 22 | objects such as Batches, Games, Players, Rounds and Stage, and the logic for 23 | running Empirica experiments. 24 | 25 | The `@empirica/core` package can be used to build different models of 26 | experiments that do not follow the Empirica conventions. 27 | 28 | ## Module details 29 | 30 | - **Admin**: 31 | - `admin`: the main admin module, which is not empirica-specific. 32 | - `admin/classic`: the Empirica logic for admin functionality. 33 | - **Player**: 34 | - `player`: the main player module, which is not empirica-specific. 35 | - `player/react`: React utilities, which are not empirica-specific. 36 | - `player/classic`: the Empirica logic for player functionality. 37 | - `player/classic/react`: Empirica React utilities for players. 38 | - **Utils**: 39 | - `utils/console`: logging facilities. 40 | -------------------------------------------------------------------------------- /internal/templates/source/react/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import builtins from "rollup-plugin-polyfill-node"; 3 | import { defineConfig, searchForWorkspaceRoot } from "vite"; 4 | import restart from "vite-plugin-restart"; 5 | import UnoCSS from "unocss/vite"; 6 | import dns from "dns"; 7 | 8 | dns.setDefaultResultOrder("verbatim"); 9 | 10 | const builtinsPlugin = { 11 | ...builtins({ include: ["fs/promises"] }), 12 | name: "rollup-plugin-polyfill-node", 13 | }; 14 | 15 | // https://vitejs.dev/config/ 16 | export default defineConfig({ 17 | optimizeDeps: { 18 | exclude: ["@empirica/tajriba", "@empirica/core"], 19 | }, 20 | server: { 21 | port: 8844, 22 | open: false, 23 | strictPort: true, 24 | host: "0.0.0.0", 25 | hmr: { 26 | host: "localhost", 27 | protocol: "ws", 28 | port: 8844, 29 | }, 30 | fs: { 31 | allow: [ 32 | // search up for workspace root 33 | searchForWorkspaceRoot(process.cwd()), 34 | ], 35 | }, 36 | }, 37 | build: { 38 | minify: false, 39 | target: "esnext", 40 | sourcemap: true, 41 | rollupOptions: { 42 | preserveEntrySignatures: "strict", 43 | plugins: [builtinsPlugin], 44 | output: { 45 | sourcemap: true, 46 | }, 47 | }, 48 | }, 49 | clearScreen: false, 50 | plugins: [ 51 | restart({ 52 | restart: [ 53 | "./uno.config.cjs", 54 | "./node_modules/@empirica/core/dist/**/*.{js,ts,jsx,tsx,css}", 55 | "./node_modules/@empirica/core/assets/**/*.css", 56 | ], 57 | }), 58 | UnoCSS(), 59 | react(), 60 | ], 61 | define: { 62 | "process.env": { 63 | NODE_ENV: process.env.NODE_ENV || "development", 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /cmds/empirica/cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/empiricaly/empirica/internal/experiment" 9 | "github.com/empiricaly/empirica/internal/settings" 10 | "github.com/pkg/errors" 11 | "github.com/rs/zerolog/log" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func addCreateCommand(parent *cobra.Command) error { 16 | parent.AddCommand(&cobra.Command{ 17 | Use: "create", 18 | Short: "Create a new Empirica project", 19 | // Long: ``, 20 | SilenceUsage: true, 21 | SilenceErrors: true, 22 | Args: cobra.ExactArgs(1), 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | if len(args) != 1 { 25 | return errors.New("missing project name") 26 | } 27 | 28 | ctx := initContext() 29 | 30 | if err := settings.InstallVoltaIfNeeded(ctx); err != nil { 31 | return errors.Wrap(err, "check node") 32 | } 33 | 34 | // current, err := os.Getwd() 35 | // if 36 | 37 | if err := experiment.Create(ctx, args[0]); err != nil { 38 | return errors.Wrap(err, "get current directory") 39 | } 40 | 41 | return nil 42 | }, 43 | }) 44 | 45 | return nil 46 | } 47 | 48 | func commandExists(cmd string) bool { 49 | _, err := exec.LookPath(cmd) 50 | return err == nil 51 | } 52 | 53 | func askForConfirmation() bool { 54 | var response string 55 | 56 | _, err := fmt.Scanln(&response) 57 | if err != nil { 58 | log.Error().Err(err).Msg("Failed to read input") 59 | 60 | return false 61 | } 62 | 63 | switch strings.ToLower(response) { 64 | case "y", "yes": 65 | return true 66 | case "n", "no": 67 | return false 68 | default: 69 | fmt.Println("I'm sorry but I didn't get what you meant, please type (y)es or (n)o and then press enter:") 70 | return askForConfirmation() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/empiricaly/empirica/internal/settings" 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // Config is server configuration. 13 | type Config struct { 14 | Addr string `mapstructure:"addr"` 15 | Treatments string `mapstructure:"treatments"` 16 | Lobbies string `mapstructure:"lobbies"` 17 | 18 | // Player frontend proxy 19 | ProxyAddr string `mapstructure:"proxyaddr"` 20 | 21 | Production bool `mapstructure:"-"` 22 | } 23 | 24 | // Validate configuration is ok. 25 | func (c *Config) Validate() error { 26 | return nil 27 | } 28 | 29 | const DefaultAddr = ":3000" 30 | 31 | // ConfigFlags helps configure cobra and viper flags. 32 | func ConfigFlags(cmd *cobra.Command, prefix string) error { 33 | if cmd == nil { 34 | return errors.New("command required") 35 | } 36 | 37 | if prefix == "" { 38 | return errors.New("prefix required") 39 | } 40 | 41 | viper.SetDefault(prefix, &Config{}) 42 | 43 | flag := prefix + ".addr" 44 | sval := DefaultAddr 45 | cmd.Flags().StringP(flag, "s", sval, "Address of the server") 46 | viper.SetDefault(flag, sval) 47 | 48 | flag = prefix + ".treatments" 49 | sval = fmt.Sprintf("%s/treatments.yaml", settings.EmpiricaDir) 50 | cmd.Flags().String(flag, sval, "Treatments config file") 51 | viper.SetDefault(flag, sval) 52 | 53 | flag = prefix + ".lobbies" 54 | sval = fmt.Sprintf("%s/lobbies.yaml", settings.EmpiricaDir) 55 | cmd.Flags().String(flag, sval, "Lobbies config file") 56 | viper.SetDefault(flag, sval) 57 | 58 | flag = prefix + ".proxyaddr" 59 | sval = "http://127.0.0.1:8844" 60 | cmd.Flags().String(flag, sval, "Frontend proxy address") 61 | viper.SetDefault(flag, sval) 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/templates/source/callbacks/src/callbacks.js: -------------------------------------------------------------------------------- 1 | import { ClassicListenersCollector } from "@empirica/core/admin/classic"; 2 | export const Empirica = new ClassicListenersCollector(); 3 | 4 | Empirica.onGameStart(({ game }) => { 5 | const round = game.addRound({ 6 | name: "Round 1 - Jelly Beans", 7 | task: "jellybeans", 8 | }); 9 | round.addStage({ name: "Answer", duration: 300 }); 10 | round.addStage({ name: "Result", duration: 120 }); 11 | 12 | const round2 = game.addRound({ 13 | name: "Round 2 - Minesweeper", 14 | task: "minesweeper", 15 | }); 16 | round2.addStage({ name: "Play", duration: 300 }); 17 | }); 18 | 19 | Empirica.onRoundStart(({ round }) => {}); 20 | 21 | Empirica.onStageStart(({ stage }) => {}); 22 | 23 | Empirica.onStageEnded(({ stage }) => { 24 | calculateJellyBeansScore(stage); 25 | }); 26 | 27 | Empirica.onRoundEnded(({ round }) => {}); 28 | 29 | Empirica.onGameEnded(({ game }) => {}); 30 | 31 | // Note: this is not the actual number of beans in the pile, it's a guess... 32 | const jellyBeansCount = 634; 33 | 34 | function calculateJellyBeansScore(stage) { 35 | if ( 36 | stage.get("name") !== "Answer" || 37 | stage.round.get("task") !== "jellybeans" 38 | ) { 39 | return; 40 | } 41 | 42 | for (const player of stage.currentGame.players) { 43 | let roundScore = 0; 44 | 45 | const playerGuess = player.round.get("guess"); 46 | 47 | if (playerGuess) { 48 | const deviation = Math.abs(playerGuess - jellyBeansCount); 49 | const score = Math.round((1 - deviation / jellyBeansCount) * 10); 50 | roundScore = Math.max(0, score); 51 | } 52 | 53 | player.round.set("score", roundScore); 54 | 55 | const totalScore = player.get("score") || 0; 56 | player.set("score", totalScore + roundScore); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | # .golangci.yml **/*.go !internal/graph/**/*.go { 2 | # prep: golangci-lint run --color always 3 | # } 4 | 5 | # { 6 | # prep: rm /usr/local/bin/empirica 7 | # } 8 | 9 | # { 10 | # prep: rm internal/templates/source/admin-ui || echo "" 11 | # prep: ln -s ../../../lib/admin-ui/dist internal/templates/source/admin-ui 12 | # } 13 | 14 | internal/templates/sources/**/* { 15 | prep: go generate ./internal/templates/... 16 | } 17 | 18 | go.mod go.sum **/*.go !**/*_test.go ../tajriba/**/*.go !../tajriba/**/*_test.go !internal/graph/generated.go !internal/graph/models_gen.go internal/graph/*.resolvers.go { 19 | prep: go install -race -ldflags "-X 'github.com/empiricaly/empirica/internal/build.DevBuild=true' -X 'github.com/empiricaly/empirica/internal/build.SHA=abcd123' -X 'github.com/empiricaly/empirica/internal/build.Tag=v1.2.3' -X 'github.com/empiricaly/empirica/internal/build.Branch=thisbranch' -X 'github.com/empiricaly/empirica/internal/build.BuildNum=42' -X 'github.com/empiricaly/empirica/internal/build.Time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')'" ./cmds/empirica 20 | prep: mv $(go env GOPATH)/bin/empirica $(go env GOPATH)/bin/emp 21 | 22 | # prep: rm .empirica/local/tajriba.json 2> /dev/null || echo "" 23 | # daemon: unbuffer emp --log.tty --tajriba.log.tty --log.level trace # --tajriba.auth.username "hey" --tajriba.auth.name "hey" --tajriba.auth.password "lalalala" 24 | # prep: unbuffer emp cloud deploy --bundle /tmp/bWJDnWaraj.zstd 25 | # prep: unbuffer emp cloud login 26 | } 27 | 28 | # go.mod go.sum **/*.go !**/*_test.go !internal/graph/generated.go !internal/graph/models_gen.go internal/graph/*.resolvers.go { 29 | # prep: go install ./cmds/proxy 30 | 31 | # # prep: rm .empirica/local/tajriba.json 2> /dev/null || echo "" 32 | # # prep: unbuffer proxy version --help 33 | # } 34 | -------------------------------------------------------------------------------- /internal/utils/log/config.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/rs/zerolog" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // Config configures the logger. 11 | type Config struct { 12 | Level string `mapstructure:"level"` 13 | ForceTTY bool `mapstructure:"tty"` 14 | JSON bool `mapstructure:"json"` 15 | ShowLine bool `mapstructure:"line"` 16 | } 17 | 18 | // Validate configuration is ok. 19 | func (c *Config) Validate() error { 20 | _, err := zerolog.ParseLevel(c.Level) 21 | if err != nil { 22 | return errors.Wrap(err, "parse log level") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // ConfigFlags helps configure cobra and viper flags. 29 | func ConfigFlags(cmd *cobra.Command, prefix, defaultLevel string) error { 30 | if cmd == nil { 31 | return errors.New("command required") 32 | } 33 | 34 | if prefix == "" { 35 | return errors.New("prefix required") 36 | } 37 | 38 | if defaultLevel == "" { 39 | return errors.New("default level required") 40 | } 41 | 42 | viper.SetDefault(prefix, &Config{}) 43 | 44 | flag := prefix + ".level" 45 | cmd.PersistentFlags().String(flag, defaultLevel, "Log level: trace, debug, info, warn, error, fatal or panic") 46 | viper.SetDefault(flag, defaultLevel) 47 | 48 | flag = prefix + ".tty" 49 | cmd.PersistentFlags().Bool(flag, false, "Force behavior of attached TTY (color, human output)") 50 | viper.SetDefault(flag, false) 51 | 52 | flag = prefix + ".json" 53 | cmd.PersistentFlags().Bool(flag, false, "Output JSON formatted logs (takes precedence over forcetty)") 54 | viper.SetDefault(flag, false) 55 | 56 | flag = prefix + ".line" 57 | cmd.PersistentFlags().Bool(flag, false, "Show file and line number of log call") 58 | viper.SetDefault(flag, false) 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/UnderConstruction.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 17 | 18 | Under Construction 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/common/Loading.svelte: -------------------------------------------------------------------------------- 1 | 2 |
3 | 11 | 12 | 13 | 23 | 33 | 34 | 35 | 45 | 55 | 56 | 57 | 58 |
59 | -------------------------------------------------------------------------------- /lib/@empirica/core/src/player/classic/react/index.ts: -------------------------------------------------------------------------------- 1 | export { Attribute, Attributes } from "../../../shared/attributes"; 2 | export type { 3 | AttributeChange, 4 | AttributeOptions, 5 | AttributeUpdate, 6 | } from "../../../shared/attributes"; 7 | export type { Constructor } from "../../../shared/helpers"; 8 | export type { 9 | Attributable, 10 | AttributeInput, 11 | ScopeConstructor, 12 | ScopeIdent, 13 | ScopeUpdate, 14 | Scope as SharedScope, 15 | } from "../../../shared/scopes"; 16 | export type { Json, JsonArray, JsonValue } from "../../../utils/json"; 17 | export type { ConsentProps } from "../../react/Consent"; 18 | export type { PlayerCreateProps } from "../../react/PlayerCreate"; 19 | export type { WithChildren } from "../../react/helpers"; 20 | export { Scope, Scopes } from "../../scopes"; 21 | export { Step, Steps } from "../../steps"; 22 | export type { StepChange, StepTick, StepUpdate } from "../../steps"; 23 | export { 24 | EmpiricaClassicKinds, 25 | Game, 26 | Player, 27 | PlayerGame, 28 | PlayerRound, 29 | PlayerStage, 30 | Round, 31 | Stage, 32 | } from "../classic"; 33 | export type { Context } from "../classic"; 34 | export { EmpiricaContext } from "./EmpiricaContext"; 35 | export type { EmpiricaContextProps } from "./EmpiricaContext"; 36 | export { Lobby } from "./Lobby"; 37 | export { Quiz } from "./Quiz"; 38 | export { Slider } from "./Slider"; 39 | export type { SliderProps } from "./Slider"; 40 | export type { StepsFunc, StepsProps } from "./Steps"; 41 | export { Chat } from "./chat/Chat"; 42 | export type { ChatProps } from "./chat/Chat"; 43 | export { Sweeper } from "./examples/Sweeper"; 44 | export { 45 | useGame, 46 | usePartModeCtx, 47 | usePartModeCtxKey, 48 | usePlayer, 49 | usePlayers, 50 | useRound, 51 | useStage, 52 | useStageTimer, 53 | } from "./hooks"; 54 | import "./slider.css"; 55 | -------------------------------------------------------------------------------- /tests/stress/experiment/.empirica/treatments.yaml: -------------------------------------------------------------------------------- 1 | factors: 2 | - name: playerCount 3 | desc: playerCount determines the number of Players are in a Game. 4 | values: 5 | - value: 1 6 | - value: 2 7 | - value: 3 8 | - value: 5 9 | - value: 8 10 | - value: 10 11 | - value: 13 12 | - name: roundCount 13 | desc: Number of rounds in a game 14 | values: 15 | - value: 10 16 | - value: 50 17 | - value: 100 18 | - name: stageCount 19 | desc: Number of stages in a round 20 | values: 21 | - value: 1 22 | - value: 3 23 | - value: 9 24 | - name: newKeyRate 25 | desc: Rate at which new keys are generated (per second) 26 | values: 27 | - value: 0.25 28 | - value: 0.5 29 | - value: 1 30 | - value: 2 31 | 32 | treatments: 33 | - name: 100player 34 | desc: "100 Players" 35 | factors: 36 | playerCount: 100 37 | roundCount: 10 38 | stageCount: 3 39 | newKeyRate: 0.5 40 | - name: 40player 41 | desc: "40 Players" 42 | factors: 43 | playerCount: 40 44 | roundCount: 10 45 | stageCount: 3 46 | newKeyRate: 0.5 47 | - name: 20player 48 | desc: "20 Players" 49 | factors: 50 | playerCount: 20 51 | roundCount: 10 52 | stageCount: 3 53 | newKeyRate: 0.5 54 | - name: 10player 55 | desc: "10 Players" 56 | factors: 57 | playerCount: 10 58 | roundCount: 10 59 | stageCount: 3 60 | newKeyRate: 0.5 61 | - name: 2player 62 | desc: "2 players" 63 | factors: 64 | playerCount: 2 65 | roundCount: 10 66 | stageCount: 3 67 | newKeyRate: 0.5 68 | - name: solo 69 | desc: "1 player" 70 | factors: 71 | playerCount: 1 72 | roundCount: 1 73 | stageCount: 1 74 | newKeyRate: 0.5 75 | -------------------------------------------------------------------------------- /lib/admin-ui/src/components/batches/CustomAssignment.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 |