UNPKG

aiom_pack

Version:

Framework for interdependent (mcmc-like) behavioral experiments

158 lines (142 loc) โ€ข 7.88 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 { api_router } = require('./api'); const { ExperimentConfig } = require('./config'); class Experiment { constructor(options = {}) { this.experimentPath = options.experimentPath || process.cwd(); this.config = new ExperimentConfig(this.experimentPath); this.app = express(); this.setupMiddleware(); this.setupRoutes(); this.setupExperimentSpecificRoutes(); this.setupEndRoutes(); } 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); // Serve package static files const packageStatic = path.join(__dirname, '..', 'static'); this.app.use('/pkg-static', express.static(packageStatic)); // Serve experiment static files 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'))); } setupRoutes() { // Base routes that all experiments need this.app.get('/', this.renderTemplate.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')); } setupExperimentSpecificRoutes() { // Load experiment-specific controllers based on config const experiment = this.config.get('experiment'); this.api_handler = new api_router(this.app, this.config, this.experimentPath); this.app.get("/experiment", (req, res) => { const expTemplate = path.join(this.experimentPath, 'public', 'experiment', `${experiment}.html`); if (fs.existsSync(expTemplate)) { res.sendFile(expTemplate); } else { res.sendFile(path.join(__dirname, '..', 'templates', 'experiment', `${experiment}.html`)); } }); } setupEndRoutes() { if (this.config.get('categorization')==='true' && this.config.get('production')==='true') { this.app.get("/thanks", this.renderTemplate.bind(this, 'categorization')); this.app.get("/categorization_finished", this.renderTemplate.bind(this, 'upload')); this.app.get("/upload_finished", this.renderTemplate.bind(this, 'thanks')); } else if (this.config.get('categorization')==='true' && this.config.get('production')==='false') { this.app.get("/thanks", this.renderTemplate.bind(this, 'categorization')); this.app.get("/categorization_finished", this.renderTemplate.bind(this, 'thanks')); } else if (this.config.get('categorization')==='false' && this.config.get('production')==='true') { this.app.get("/thanks", this.renderTemplate.bind(this, 'upload')); this.app.get("/upload_finished", this.renderTemplate.bind(this, 'thanks')); } else if (this.config.get('categorization')==='false' && this.config.get('production')==='false') { this.app.get("/thanks", this.renderTemplate.bind(this, 'thanks')); } this.app.get("/early_stop", this.renderTemplate.bind(this, 'early_stop')); } renderTemplate(templateName, req, res) { const templatePath = this.getTemplatePath(templateName); let htmlContent = fs.readFileSync(templatePath, 'utf8'); // Inject environment variables based on template htmlContent = this.injectEnvironmentVariables(htmlContent, templateName); res.send(htmlContent); } getTemplatePath(templateName) { // First check experiment directory const expTemplate = path.join(this.experimentPath, 'public', 'base', `${templateName}.html`); if (fs.existsSync(expTemplate)) { return expTemplate; } // Fall back to package template return path.join(__dirname, '..', 'templates', 'base', `${templateName}.html`); } injectEnvironmentVariables(htmlContent, templateName) { const example_path = path.join(this.experimentPath, 'static', 'stimuli', 'production_example'); // list all files in the production_example directory const exampleFiles = fs.readdirSync(example_path); if (exampleFiles.length > 0) { this.config.set('example_file', exampleFiles[0]); } const injections = { 'index': { prolific: this.config.get('prolific'), index_text: `/exp-static/${templateName}.html`}, 'instruction': { instruction_text: `/exp-static/${templateName}.html` }, 'consent': { pinfo: '/exp-static/pinfo.html', consent: `/exp-static/${templateName}.html` }, 'categorization': { instruction_text: `/exp-static/${templateName}_instruction.html` }, 'upload': { instruction_text: `/exp-static/${templateName}_instruction.html`, example_img: `/exp-production-example/${this.config.get('example_file')}`, production_mode: this.config.get('production_mode') }, 'thanks': { prolific: this.config.get('prolific') }, 'early_stop': { prolific: this.config.get('prolific') }, }; const envVars = injections[templateName] || {}; let injected_content = ''; if (templateName === 'consent') { injected_content = `<script type = "text/javascript" src="/pkg-static/scripts/${this.config.get('experiment')}.js"></script>\n<script>const ENV = ${JSON.stringify(envVars)};</script>\n</head>` } else { injected_content = `<script>const ENV = ${JSON.stringify(envVars)};</script>\n</head>` } return htmlContent.replace( '</head>', injected_content ); } 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 };