aiom_pack
Version:
Framework for interdependent (mcmc-like) behavioral experiments
158 lines (142 loc) โข 7.88 kB
JavaScript
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 };