UNPKG

aiom

Version:

A Framework for interdependent (mcmc-like) behavioral experiments

150 lines (135 loc) 6.67 kB
const express = require('express'); const path = require('path'); const fs = require('fs'); const fileUpload = require('express-fileupload'); const cors = require('cors'); const errorHandler = require('../middleware/errorHandler'); const { ExperimentConfig } = require('./config'); class Experiment { constructor(options = {}) { this.paint_AIOM_in_CLI(); this.experimentPath = options.experimentPath || process.cwd(); this.config = new ExperimentConfig(this.experimentPath); this.experiment = this.config.get('experiment'); this.app = express(); this.setupMiddleware(); this.setupResources(); this.setupBasicRoutes(); this.setupExperimentRoutes(); this.setupOrderedExp(); } paint_AIOM_in_CLI() { console.log(` █████╗ ██╗ ██████╗ ███╗ ███╗ ██╔══██╗██║██╔═══██╗████╗ ████║ ███████║██║██║ ██║██╔████╔██║ ██╔══██║██║██║ ██║██║╚██╔╝██║ ██║ ██║██║╚██████╔╝██║ ╚═╝ ██║ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ `); } setupMiddleware() { this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); this.app.use(require('cookie-parser')()); this.app.use(fileUpload({ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit useTempFiles: false, createParentPath: true })); this.app.use(errorHandler); } setupResources() { // Serve package static files --- css styles, js scripts, etc. const packageStatic = path.join(__dirname, '..', 'static'); this.app.use('/pkg-static', express.static(packageStatic)); // Serve experiment static files --- custom text, stimuli, etc. const customTextDir = path.join(this.experimentPath, 'custom_text'); this.app.use('/exp-static', express.static(customTextDir)); this.app.use('/exp-production-example', express.static(path.join(this.experimentPath, 'static', 'stimuli', 'production_example'))); } setupBasicRoutes() { // Base routes that all experiments need this.app.get('/', this.renderTemplate_prolific.bind(this, 'index')); this.app.get('/consent', this.renderTemplate.bind(this, 'consent')); this.app.get('/instruction', this.renderTemplate.bind(this, 'instruction')); // this.app.get('/waitingroom', this.renderTemplate.bind(this, 'waitingroom')); this.app.get("/early_stop", this.renderTemplate_prolific.bind(this, 'early_stop')); this.app.get('/complete_redirect', this.handleCompleteRedirect.bind(this)); } setupExperimentRoutes() { const { api_router } = require('./api'); this.api_handler = new api_router(this.app, this.config, this.experimentPath); } setupOrderedExp() { const task_order = this.config.getArray('task_order'); this.app.get("/experiment/:stage", (req, res) => { const stage = req.params.stage; if (stage === 'begin') { this.sendTask(task_order[0], req, res); } else if (task_order.includes(stage)) { const currentIndex = task_order.indexOf(stage); if (currentIndex < task_order.length - 1) { this.sendTask(task_order[currentIndex + 1], req, res); } else { this.renderTemplate_prolific('thanks', req, res); } } else { // Handle invalid stage console.error(`Invalid stage requested: ${stage}`); res.status(404).send('Stage not found'); } }); } sendTask(task, req, res) { const templatePath = task === 'main' ? this.getTemplatePath(this.experiment, 'experiment') : this.getTemplatePath(task); res.sendFile(templatePath); } handleCompleteRedirect(req, res) { if (this.config.getBoolean('prolific')) { require('../controllers/addon/prolific_Controller').complete_redirect(req, res); } else { res.status(200).json({ success: true }); } } renderTemplate(templateName, req, res) { const templatePath = this.getTemplatePath(templateName); res.sendFile(templatePath); } renderTemplate_prolific(templateName, req, res) { const templatePath = this.config.getBoolean('prolific') ? this.getTemplatePath(templateName + '-prolific') : this.getTemplatePath(templateName); res.sendFile(templatePath); } getTemplatePath(templateName, fileCat='base') { // First check experiment directory const expTemplate = path.join(this.experimentPath, 'public', fileCat, `${templateName}.html`); // if not exist, Fall back to package template return fs.existsSync(expTemplate) ? expTemplate : path.join(__dirname, '..', 'templates', fileCat, `${templateName}.html`); } async start(port = 3000) { try { const { pool } = require('./database'); const result = await pool.query('SELECT NOW() as current_time, version() as pg_version'); console.log('✅ Database connected successfully'); console.log(`📅 Server time: ${result.rows[0].current_time}`); console.log(`🐘 PostgreSQL version: ${result.rows[0].pg_version.split(' ')[0]}`); } catch (error) { console.error('❌ Database initialization failed:', error.message); throw error; } return this.app.listen(port, () => { console.log(`🧪 Experiment type: ${this.config.get('experiment')}`); const test_url = this.config.getBoolean('prolific') ? `🌐 Server: http://localhost:${port}/?PROLIFIC_PID=test${Math.floor(Math.random()*10000)}&STUDY_ID=test${Math.floor(Math.random()*10000)}&SESSION_ID=test${Math.floor(Math.random()*10000)}` : `🌐 Server: http://localhost:${port}`; console.log(test_url); }); } } module.exports = { Experiment };